Browse Source

feat(dashboard-layout): Add Dashboard preview to Manage page (#31677)

Nar Saynorath 3 years ago
parent
commit
7229e2cf5d

+ 28 - 24
static/app/views/dashboardsV2/layoutUtils.tsx

@@ -7,9 +7,8 @@ import zip from 'lodash/zip';
 import {defined} from 'sentry/utils';
 import {uniqueId} from 'sentry/utils/guid';
 
-import {ADD_WIDGET_BUTTON_DRAG_ID} from './addWidget';
 import {NUM_DESKTOP_COLS} from './dashboard';
-import {DisplayType, Widget} from './types';
+import {DisplayType, Widget, WidgetLayout} from './types';
 
 export const DEFAULT_WIDGET_WIDTH = 2;
 
@@ -18,10 +17,7 @@ const WIDGET_PREFIX = 'grid-item';
 // Keys for grid layout values we track in the server
 const STORE_KEYS = ['x', 'y', 'w', 'h', 'minW', 'maxW', 'minH', 'maxH'];
 
-export type Position = {
-  x: number;
-  y: number;
-};
+export type Position = Pick<Layout, 'x' | 'y'>;
 
 type NextPosition = [position: Position, columnDepths: number[]];
 
@@ -29,7 +25,7 @@ export function generateWidgetId(widget: Widget, index: number) {
   return widget.id ? `${widget.id}-index-${index}` : `index-${index}`;
 }
 
-export function constructGridItemKey(widget: Widget) {
+export function constructGridItemKey(widget: {id?: string; tempId?: string}) {
   return `${WIDGET_PREFIX}-${widget.id ?? widget.tempId}`;
 }
 
@@ -81,16 +77,21 @@ export function getMobileLayout(desktopLayout: Layout[], widgets: Widget[]) {
  * Reads the layout from an array of widgets.
  */
 export function getDashboardLayout(widgets: Widget[]): Layout[] {
+  type WidgetWithDefinedLayout = Omit<Widget, 'layout'> & {layout: WidgetLayout};
   return widgets
-    .filter(({layout}) => defined(layout))
+    .filter((widget): widget is WidgetWithDefinedLayout => defined(widget.layout))
     .map(({layout, ...widget}) => ({
-      ...(layout as Layout),
+      ...layout,
       i: constructGridItemKey(widget),
     }));
 }
 
-export function pickDefinedStoreKeys(layout: Layout): Partial<Layout> {
-  return pickBy(layout, (value, key) => defined(value) && STORE_KEYS.includes(key));
+export function pickDefinedStoreKeys(layout: Layout): WidgetLayout {
+  // TODO(nar): Fix the types here
+  return pickBy(
+    layout,
+    (value, key) => defined(value) && STORE_KEYS.includes(key)
+  ) as WidgetLayout;
 }
 
 export function getDefaultWidgetHeight(displayType: DisplayType): number {
@@ -104,18 +105,18 @@ export function getInitialColumnDepths() {
 /**
  * Creates an array from layouts where each column stores how deep it is.
  */
-export function calculateColumnDepths(layouts: Layout[]): number[] {
+export function calculateColumnDepths(
+  layouts: Pick<Layout, 'h' | 'w' | 'x' | 'y'>[]
+): number[] {
   const depths = getInitialColumnDepths();
 
   // For each layout's x, record the max depth
-  layouts
-    .filter(({i}) => i !== ADD_WIDGET_BUTTON_DRAG_ID)
-    .forEach(({x, w, y, h}) => {
-      // Adjust the column depths for each column the widget takes up
-      for (let col = x; col < x + w; col++) {
-        depths[col] = Math.max(y + h, depths[col]);
-      }
-    });
+  layouts.forEach(({x, w, y, h}) => {
+    // Adjust the column depths for each column the widget takes up
+    for (let col = x; col < x + w; col++) {
+      depths[col] = Math.max(y + h, depths[col]);
+    }
+  });
 
   return depths;
 }
@@ -165,16 +166,15 @@ export function getNextAvailablePosition(
   return [{x: 0, y: maxColumnDepth}, [...columnDepths]];
 }
 
-export function assignDefaultLayout(
-  widgets: Widget[],
+export function assignDefaultLayout<T extends Pick<Widget, 'displayType' | 'layout'>>(
+  widgets: T[],
   initialColumnDepths: number[]
-): Widget[] {
+): T[] {
   let columnDepths = [...initialColumnDepths];
   const newWidgets = widgets.map(widget => {
     if (defined(widget.layout)) {
       return widget;
     }
-
     const height = getDefaultWidgetHeight(widget.displayType);
     const [nextPosition, nextColumnDepths] = getNextAvailablePosition(
       columnDepths,
@@ -195,6 +195,10 @@ export function enforceWidgetHeightValues(widget: Widget): Widget {
   const nextWidget = {
     ...widget,
   };
+  if (!defined(layout)) {
+    return nextWidget;
+  }
+
   const minH = getDefaultWidgetHeight(displayType);
   const nextLayout = {
     ...layout,

+ 1 - 0
static/app/views/dashboardsV2/manage/dashboardCard.tsx

@@ -115,6 +115,7 @@ const CardBody = styled('div')`
   max-height: 150px;
   min-height: 150px;
   overflow: hidden;
+  border-bottom: 1px solid ${p => p.theme.gray100};
 `;
 
 const CardFooter = styled('div')`

+ 31 - 43
static/app/views/dashboardsV2/manage/dashboardList.tsx

@@ -3,13 +3,6 @@ import {browserHistory} from 'react-router';
 import styled from '@emotion/styled';
 import {Location, Query} from 'history';
 
-import WidgetArea from 'sentry-images/dashboard/widget-area.svg';
-import WidgetBar from 'sentry-images/dashboard/widget-bar.svg';
-import WidgetBigNumber from 'sentry-images/dashboard/widget-big-number.svg';
-import WidgetLine from 'sentry-images/dashboard/widget-line-1.svg';
-import WidgetTable from 'sentry-images/dashboard/widget-table.svg';
-import WidgetWorldMap from 'sentry-images/dashboard/widget-world-map.svg';
-
 import {
   createDashboard,
   deleteDashboard,
@@ -31,9 +24,10 @@ import {trackAnalyticsEvent} from 'sentry/utils/analytics';
 import withApi from 'sentry/utils/withApi';
 import {DashboardListItem, DisplayType} from 'sentry/views/dashboardsV2/types';
 
-import {cloneDashboard} from '../utils';
+import {cloneDashboard, miniWidget} from '../utils';
 
 import DashboardCard from './dashboardCard';
+import GridPreview from './gridPreview';
 
 type Props = {
   api: Client;
@@ -52,25 +46,6 @@ function DashboardList({
   pageLinks,
   onDashboardsChange,
 }: Props) {
-  function miniWidget(displayType: DisplayType): string {
-    switch (displayType) {
-      case DisplayType.BAR:
-        return WidgetBar;
-      case DisplayType.AREA:
-      case DisplayType.TOP_N:
-        return WidgetArea;
-      case DisplayType.BIG_NUMBER:
-        return WidgetBigNumber;
-      case DisplayType.TABLE:
-        return WidgetTable;
-      case DisplayType.WORLD_MAP:
-        return WidgetWorldMap;
-      case DisplayType.LINE:
-      default:
-        return WidgetLine;
-    }
-  }
-
   function handleDelete(dashboard: DashboardListItem) {
     deleteDashboard(api, organization.slug, dashboard.id)
       .then(() => {
@@ -155,8 +130,35 @@ function DashboardList({
     );
   }
 
+  function renderDndPreview(dashboard) {
+    return (
+      <WidgetGrid>
+        {dashboard.widgetDisplay.map((displayType, i) => {
+          return displayType === DisplayType.BIG_NUMBER ? (
+            <BigNumberWidgetWrapper key={`${i}-${displayType}`}>
+              <WidgetImage src={miniWidget(displayType)} />
+            </BigNumberWidgetWrapper>
+          ) : (
+            <MiniWidgetWrapper key={`${i}-${displayType}`}>
+              <WidgetImage src={miniWidget(displayType)} />
+            </MiniWidgetWrapper>
+          );
+        })}
+      </WidgetGrid>
+    );
+  }
+
+  function renderGridPreview(dashboard) {
+    return <GridPreview widgetPreview={dashboard.widgetPreview} />;
+  }
+
   function renderMiniDashboards() {
+    const isUsingGrid = organization.features.includes('dashboard-grid-layout');
     return dashboards?.map((dashboard, index) => {
+      const widgetRenderer = isUsingGrid ? renderGridPreview : renderDndPreview;
+      const widgetCount = isUsingGrid
+        ? dashboard.widgetPreview.length
+        : dashboard.widgetDisplay.length;
       return (
         <DashboardCard
           key={`${index}-${dashboard.id}`}
@@ -167,26 +169,12 @@ function DashboardList({
             pathname: `/organizations/${organization.slug}/dashboard/${dashboard.id}/`,
             query: {...location.query},
           }}
-          detail={tn('%s widget', '%s widgets', dashboard.widgetDisplay.length)}
+          detail={tn('%s widget', '%s widgets', widgetCount)}
           dateStatus={
             dashboard.dateCreated ? <TimeSince date={dashboard.dateCreated} /> : undefined
           }
           createdBy={dashboard.createdBy}
-          renderWidgets={() => (
-            <WidgetGrid>
-              {dashboard.widgetDisplay.map((displayType, i) => {
-                return displayType === DisplayType.BIG_NUMBER ? (
-                  <BigNumberWidgetWrapper key={`${i}-${displayType}`}>
-                    <WidgetImage src={miniWidget(displayType)} />
-                  </BigNumberWidgetWrapper>
-                ) : (
-                  <MiniWidgetWrapper key={`${i}-${displayType}`}>
-                    <WidgetImage src={miniWidget(displayType)} />
-                  </MiniWidgetWrapper>
-                );
-              })}
-            </WidgetGrid>
-          )}
+          renderWidgets={() => widgetRenderer(dashboard)}
           renderContextMenu={() => renderDropdownMenu(dashboard)}
         />
       );

File diff suppressed because it is too large
+ 9 - 0
static/app/views/dashboardsV2/manage/gridPreview/chartPreviews/area.tsx


+ 26 - 0
static/app/views/dashboardsV2/manage/gridPreview/chartPreviews/bar.tsx

@@ -0,0 +1,26 @@
+const BarPreview = () => (
+  <svg
+    viewBox="0 0 140 48"
+    xmlns="http://www.w3.org/2000/svg"
+    preserveAspectRatio="none"
+    fill="#B85586"
+    height="100%"
+    width="100%"
+  >
+    <rect width="8" height="22" transform="matrix(-1 0 0 1 129 26)" />
+    <rect width="8" height="40" transform="matrix(-1 0 0 1 107 8)" />
+    <rect width="8" height="26" transform="matrix(-1 0 0 1 85 22)" />
+    <rect width="8" height="29" transform="matrix(-1 0 0 1 63 19)" />
+    <rect width="8" height="40" transform="matrix(-1 0 0 1 41 8)" />
+    <rect width="8" height="29" transform="matrix(-1 0 0 1 19 19)" />
+    <rect width="8" height="29" transform="matrix(-1 0 0 1 118 19)" />
+    <rect width="8" height="35" transform="matrix(-1 0 0 1 140 13)" />
+    <rect width="8" height="35" transform="matrix(-1 0 0 1 96 13)" />
+    <rect width="8" height="35" transform="matrix(-1 0 0 1 74 13)" />
+    <rect width="8" height="40" transform="matrix(-1 0 0 1 52 8)" />
+    <rect width="8" height="48" transform="matrix(-1 0 0 1 30 0)" />
+    <rect width="8" height="22" transform="matrix(-1 0 0 1 8 26)" />
+  </svg>
+);
+
+export default BarPreview;

File diff suppressed because it is too large
+ 13 - 0
static/app/views/dashboardsV2/manage/gridPreview/chartPreviews/line.tsx


+ 14 - 0
static/app/views/dashboardsV2/manage/gridPreview/chartPreviews/number.tsx

@@ -0,0 +1,14 @@
+const NumberPreview = () => (
+  <svg
+    viewBox="0 0 70 17"
+    xmlns="http://www.w3.org/2000/svg"
+    fill="#444674"
+    preserveAspectRatio="none"
+    height="100%"
+    width="100%"
+  >
+    <rect width="50" height="16" rx="1" />
+  </svg>
+);
+
+export default NumberPreview;

+ 78 - 0
static/app/views/dashboardsV2/manage/gridPreview/chartPreviews/table.tsx

@@ -0,0 +1,78 @@
+const TablePreview = () => (
+  <svg
+    height="155px"
+    width="100%"
+    viewBox="0 0 157 155"
+    preserveAspectRatio="none"
+    fill="none"
+    xmlns="http://www.w3.org/2000/svg"
+  >
+    <rect width="157" height="13" fill="#B5AEE2" />
+    <rect x="8" y="6" width="42" height="2" rx="1" fill="#9086D4" />
+    <rect x="8" y="21" width="42" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="8" y="30" width="42" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="8" y="39" width="42" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="8" y="92" width="42" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="8" y="101" width="42" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="8" y="110" width="42" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="8" y="119" width="42" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="137" y="92" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="137" y="101" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="137" y="110" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="137" y="119" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="8" y="128" width="42" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="8" y="137" width="42" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="8" y="146" width="42" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="137" y="128" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="137" y="137" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="137" y="146" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="119" y="92" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="119" y="101" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="119" y="110" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="119" y="119" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="119" y="128" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="119" y="137" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="119" y="146" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="101" y="92" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="101" y="101" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="101" y="110" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="101" y="119" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="101" y="128" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="101" y="137" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="101" y="146" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="8" y="48" width="42" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="137" y="6" width="12" height="2" rx="1" fill="#9086D4" />
+    <rect x="137" y="21" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="137" y="30" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="137" y="39" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="137" y="48" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="8" y="57" width="42" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="8" y="66" width="42" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="8" y="75" width="42" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="8" y="84" width="42" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="137" y="57" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="137" y="66" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="137" y="75" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="137" y="84" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="119" y="6" width="12" height="2" rx="1" fill="#9086D4" />
+    <rect x="119" y="21" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="119" y="30" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="119" y="39" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="119" y="48" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="119" y="57" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="119" y="66" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="119" y="75" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="119" y="84" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="101" y="6" width="12" height="2" rx="1" fill="#9086D4" />
+    <rect x="101" y="21" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="101" y="30" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="101" y="39" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="101" y="48" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="101" y="57" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="101" y="66" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="101" y="75" width="12" height="2" rx="1" fill="#D4D1EC" />
+    <rect x="101" y="84" width="12" height="2" rx="1" fill="#D4D1EC" />
+  </svg>
+);
+
+export default TablePreview;

File diff suppressed because it is too large
+ 11 - 0
static/app/views/dashboardsV2/manage/gridPreview/chartPreviews/world.tsx


+ 114 - 0
static/app/views/dashboardsV2/manage/gridPreview/index.tsx

@@ -0,0 +1,114 @@
+import 'react-grid-layout/css/styles.css';
+import 'react-resizable/css/styles.css';
+
+import GridLayout, {WidthProvider} from 'react-grid-layout';
+import styled from '@emotion/styled';
+
+import {defined} from 'sentry/utils';
+import {uniqueId} from 'sentry/utils/guid';
+import {
+  assignDefaultLayout,
+  calculateColumnDepths,
+} from 'sentry/views/dashboardsV2/layoutUtils';
+import {DisplayType, WidgetLayout, WidgetPreview} from 'sentry/views/dashboardsV2/types';
+
+import WidgetArea from './chartPreviews/area';
+import WidgetBar from './chartPreviews/bar';
+import WidgetLine from './chartPreviews/line';
+import WidgetBigNumber from './chartPreviews/number';
+import WidgetTable from './chartPreviews/table';
+import WidgetWorldMap from './chartPreviews/world';
+
+function miniWidget(displayType: DisplayType): () => JSX.Element {
+  switch (displayType) {
+    case DisplayType.BAR:
+      return WidgetBar;
+    case DisplayType.AREA:
+    case DisplayType.TOP_N:
+      return WidgetArea;
+    case DisplayType.BIG_NUMBER:
+      return WidgetBigNumber;
+    case DisplayType.TABLE:
+      return WidgetTable;
+    case DisplayType.WORLD_MAP:
+      return WidgetWorldMap;
+    case DisplayType.LINE:
+    default:
+      return WidgetLine;
+  }
+}
+
+type Props = {
+  widgetPreview: WidgetPreview[];
+};
+
+function GridPreview({widgetPreview}: Props) {
+  const definedLayouts = widgetPreview
+    .map(({layout}) => layout)
+    .filter((layout): layout is WidgetLayout => defined(layout));
+  const columnDepths = calculateColumnDepths(definedLayouts);
+  const renderPreview = assignDefaultLayout(widgetPreview, columnDepths);
+
+  return (
+    <StyledGridLayout
+      cols={6}
+      rowHeight={40}
+      margin={[4, 4]}
+      isResizable={false}
+      isDraggable={false}
+      useCSSTransforms={false}
+      measureBeforeMount
+    >
+      {renderPreview.map(({displayType, layout}) => {
+        const Preview = miniWidget(displayType);
+        return (
+          <Chart key={uniqueId()} data-grid={{...layout}}>
+            <PreviewWrapper>
+              <Preview />
+            </PreviewWrapper>
+          </Chart>
+        );
+      })}
+    </StyledGridLayout>
+  );
+}
+
+export default GridPreview;
+
+const PreviewWrapper = styled('div')`
+  padding: 20px 8px 4px 12px;
+  height: 100%;
+  width: 100%;
+  overflow: hidden;
+`;
+
+// ::before is the widget title and ::after is the border
+const Chart = styled('div')`
+  background: white;
+  position: relative;
+
+  &::before {
+    content: '';
+    position: absolute;
+    left: 12px;
+    top: 10px;
+    width: max(30px, 30%);
+    height: 4px;
+    background-color: #d4d1ec;
+    border-radius: 8px;
+  }
+
+  &::after {
+    content: '';
+    position: absolute;
+    left: 2px;
+    top: 2px;
+    width: 100%;
+    height: 100%;
+    border: 2px solid #444674;
+  }
+`;
+
+const StyledGridLayout = styled(WidthProvider(GridLayout))`
+  margin: -4px;
+`;

Some files were not shown because too many files changed in this diff