From e58d21e9a4f5b521aac34b5b6cfd9ff27eeff8d1 Mon Sep 17 00:00:00 2001 From: Krist Wongsuphasawat Date: Tue, 12 Nov 2019 14:13:55 -0800 Subject: [PATCH] feat: improve table performance (#246) * feat: optimize table rendering * fix: memoize * fix: more opti * fix: bug * fix: render 0 * fix: return type * Update HTMLRenderer.tsx --- .../src/Table.tsx | 2 +- .../src/components/HTMLRenderer.tsx | 17 ++ .../src/getRenderer.tsx | 143 +++++++++++++++++ .../src/renderer.tsx | 145 ------------------ 4 files changed, 161 insertions(+), 146 deletions(-) create mode 100644 superset-frontend/temporary_superset_ui/superset-ui/plugins/superset-ui-plugins/packages/superset-ui-plugin-chart-table/src/components/HTMLRenderer.tsx create mode 100644 superset-frontend/temporary_superset_ui/superset-ui/plugins/superset-ui-plugins/packages/superset-ui-plugin-chart-table/src/getRenderer.tsx delete mode 100644 superset-frontend/temporary_superset_ui/superset-ui/plugins/superset-ui-plugins/packages/superset-ui-plugin-chart-table/src/renderer.tsx diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/superset-ui-plugins/packages/superset-ui-plugin-chart-table/src/Table.tsx b/superset-frontend/temporary_superset_ui/superset-ui/plugins/superset-ui-plugins/packages/superset-ui-plugin-chart-table/src/Table.tsx index 58309b424eef4..49c61e671d6eb 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/superset-ui-plugins/packages/superset-ui-plugin-chart-table/src/Table.tsx +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/superset-ui-plugins/packages/superset-ui-plugin-chart-table/src/Table.tsx @@ -6,7 +6,7 @@ import withStyles, { WithStylesProps } from '@airbnb/lunar/lib/composers/withSty import { Renderers, ParentRow, ColumnMetadata } from '@airbnb/lunar/lib/components/DataTable/types'; import dompurify from 'dompurify'; import { createSelector } from 'reselect'; -import { getRenderer, ColumnType, Cell } from './renderer'; +import getRenderer, { ColumnType, Cell } from './getRenderer'; type Props = { data: ParentRow[]; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/superset-ui-plugins/packages/superset-ui-plugin-chart-table/src/components/HTMLRenderer.tsx b/superset-frontend/temporary_superset_ui/superset-ui/plugins/superset-ui-plugins/packages/superset-ui-plugin-chart-table/src/components/HTMLRenderer.tsx new file mode 100644 index 0000000000000..5ee7a41b6c3f1 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/superset-ui-plugins/packages/superset-ui-plugin-chart-table/src/components/HTMLRenderer.tsx @@ -0,0 +1,17 @@ +import React, { useMemo } from 'react'; +import dompurify from 'dompurify'; + +const isHTML = RegExp.prototype.test.bind(/(<([^>]+)>)/i); + +export default function HTMLRenderer({ value }: { value: string }) { + if (isHTML(value)) { + const html = useMemo(() => ({ __html: dompurify.sanitize(value) }), [value]); + + return ( + // eslint-disable-next-line react/no-danger +
+ ); + } + + return <>{value}; +} diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/superset-ui-plugins/packages/superset-ui-plugin-chart-table/src/getRenderer.tsx b/superset-frontend/temporary_superset_ui/superset-ui/plugins/superset-ui-plugins/packages/superset-ui-plugin-chart-table/src/getRenderer.tsx new file mode 100644 index 0000000000000..3debfa5243638 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/superset-ui-plugins/packages/superset-ui-plugin-chart-table/src/getRenderer.tsx @@ -0,0 +1,143 @@ +/* eslint-disable complexity */ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ +/* eslint-disable no-magic-numbers */ +import React, { CSSProperties, useMemo } from 'react'; +import { HEIGHT_TO_PX } from '@airbnb/lunar/lib/components/DataTable/constants'; +import { RendererProps } from '@airbnb/lunar/lib/components/DataTable/types'; +import { NumberFormatter } from '@superset-ui/number-format'; +import { TimeFormatter } from '@superset-ui/time-format'; +import HTMLRenderer from './components/HTMLRenderer'; + +const NEGATIVE_COLOR = '#FFA8A8'; +const POSITIVE_COLOR = '#ced4da'; +const SELECTION_COLOR = '#EBEBEB'; + +const NOOP = () => {}; + +const HEIGHT = HEIGHT_TO_PX.micro; + +export type ColumnType = { + key: string; + label: string; + format?: NumberFormatter | TimeFormatter | undefined; + type: 'metric' | 'string'; + maxValue?: number; + minValue?: number; +}; + +export type Cell = { + key: string; + value: any; +}; + +const NUMBER_STYLE: CSSProperties = { + marginLeft: 'auto', + marginRight: '4px', + zIndex: 10, +}; + +export default function getRenderer({ + column, + alignPositiveNegative, + colorPositiveNegative, + enableFilter, + isSelected, + handleCellSelected, +}: { + column: ColumnType; + alignPositiveNegative: boolean; + colorPositiveNegative: boolean; + enableFilter: boolean; + isSelected: (cell: Cell) => boolean; + handleCellSelected: (cell: Cell) => (...args: any[]) => void; +}) { + const { format, type } = column; + + const isMetric = type === 'metric'; + const cursorStyle = enableFilter && !isMetric ? 'pointer' : 'default'; + + const boxContainerStyle: CSSProperties = { + alignItems: 'center', + display: 'flex', + margin: '0px 16px', + position: 'relative', + textAlign: isMetric ? 'right' : 'left', + }; + + const baseBoxStyle: CSSProperties = { + cursor: cursorStyle, + margin: '4px -16px', + }; + + const selectedBoxStyle: CSSProperties = { + ...baseBoxStyle, + backgroundColor: SELECTION_COLOR, + }; + + const getBoxStyle = enableFilter + ? (selected: boolean) => (selected ? selectedBoxStyle : baseBoxStyle) + : () => baseBoxStyle; + + const posExtent = Math.abs(Math.max(column.maxValue!, 0)); + const negExtent = Math.abs(Math.min(column.minValue!, 0)); + const total = posExtent + negExtent; + + return ({ keyName, row }: RendererProps) => { + const value = row.rowData.data[keyName]; + const cell = { key: keyName as string, value }; + const handleClick = isMetric ? NOOP : useMemo(() => handleCellSelected(cell), [keyName, value]); + + let Parent; + if (isMetric) { + let left = 0; + let width = 0; + const numericValue = value as number; + if (alignPositiveNegative) { + width = Math.abs( + Math.round((numericValue / Math.max(column.maxValue!, Math.abs(column.minValue!))) * 100), + ); + } else { + left = Math.round((Math.min(negExtent + numericValue, negExtent) / total) * 100); + width = Math.round((Math.abs(numericValue) / total) * 100); + } + const color = colorPositiveNegative && numericValue < 0 ? NEGATIVE_COLOR : POSITIVE_COLOR; + + Parent = ({ children }: { children: React.ReactNode }) => { + const barStyle: CSSProperties = { + background: color, + borderRadius: 3, + height: HEIGHT / 2 + 4, + left: `${left}%`, + position: 'absolute', + width: `${width}%`, + }; + + return ( + <> +
+
{children}
+ + ); + }; + } else { + Parent = React.Fragment; + } + + return ( +
+
+
+ + {format ? ( + format.format(value as number & Date) + ) : ( + + )} + +
+
+
+ ); + }; +} diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/superset-ui-plugins/packages/superset-ui-plugin-chart-table/src/renderer.tsx b/superset-frontend/temporary_superset_ui/superset-ui/plugins/superset-ui-plugins/packages/superset-ui-plugin-chart-table/src/renderer.tsx deleted file mode 100644 index bccc2e95ed21c..0000000000000 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/superset-ui-plugins/packages/superset-ui-plugin-chart-table/src/renderer.tsx +++ /dev/null @@ -1,145 +0,0 @@ -/* eslint-disable complexity */ -/* eslint-disable jsx-a11y/no-static-element-interactions */ -/* eslint-disable jsx-a11y/click-events-have-key-events */ -/* eslint-disable no-magic-numbers */ -import React, { CSSProperties } from 'react'; -import { HEIGHT_TO_PX } from '@airbnb/lunar/lib/components/DataTable/constants'; -import { RendererProps } from '@airbnb/lunar/lib/components/DataTable/types'; -import { NumberFormatter } from '@superset-ui/number-format'; -import { TimeFormatter } from '@superset-ui/time-format'; -import dompurify from 'dompurify'; - -const NEGATIVE_COLOR = '#FFA8A8'; -const POSITIVE_COLOR = '#ced4da'; -const SELECTION_COLOR = '#EBEBEB'; - -export const heightType = 'micro'; - -export type ColumnType = { - key: string; - label: string; - format?: NumberFormatter | TimeFormatter | undefined; - type: 'metric' | 'string'; - maxValue?: number; - minValue?: number; -}; - -export type Cell = { - key: string; - value: any; -}; - -const numberStyle: CSSProperties = { - marginLeft: 'auto', - marginRight: '4px', - zIndex: 10, -}; - -export const getRenderer = ({ - column, - alignPositiveNegative, - colorPositiveNegative, - enableFilter, - isSelected, - handleCellSelected, -}: { - column: ColumnType; - alignPositiveNegative: boolean; - colorPositiveNegative: boolean; - enableFilter: boolean; - isSelected: (cell: Cell) => boolean; - handleCellSelected: (cell: Cell) => any; -}) => (props: RendererProps) => { - const { keyName } = props; - let value = props.row.rowData.data[keyName]; - if (!column.format) { - value = dompurify.sanitize(value as string); - } - const isMetric = column.type === 'metric'; - let Parent; - - const cursorStyle = enableFilter && !isMetric ? 'pointer' : 'default'; - - const boxStyle: CSSProperties = { - backgroundColor: - enableFilter && isSelected({ key: keyName as string, value }) ? SELECTION_COLOR : undefined, - cursor: cursorStyle, - margin: '4px -16px', - }; - - const boxContainerStyle: CSSProperties = { - alignItems: 'center', - display: 'flex', - margin: '0px 16px', - position: 'relative', - textAlign: isMetric ? 'right' : 'left', - }; - - const FullCellBackgroundWrapper = ({ children }: { children: React.ReactNode }) => ( -
-
{children}
-
- ); - - if (isMetric) { - let left = 0; - let width = 0; - const numericValue = value as number; - if (alignPositiveNegative) { - width = Math.abs( - Math.round((numericValue / Math.max(column.maxValue!, Math.abs(column.minValue!))) * 100), - ); - } else { - const posExtent = Math.abs(Math.max(column.maxValue!, 0)); - const negExtent = Math.abs(Math.min(column.minValue!, 0)); - const tot = posExtent + negExtent; - left = Math.round((Math.min(negExtent + numericValue, negExtent) / tot) * 100); - width = Math.round((Math.abs(numericValue) / tot) * 100); - } - const color = colorPositiveNegative && numericValue < 0 ? NEGATIVE_COLOR : POSITIVE_COLOR; - - Parent = ({ children }: { children: React.ReactNode }) => { - const barStyle: CSSProperties = { - background: color, - borderRadius: 3, - height: HEIGHT_TO_PX[heightType] / 2 + 4, - left: `${left}%`, - position: 'absolute', - width: `${width}%`, - }; - - return ( - -
-
{children}
- - ); - }; - } else { - Parent = ({ children }: { children: React.ReactNode }) => ( - {children} - ); - } - - return ( -
- - {column.format ? ( - column.format.format(value as any) - ) : ( - // eslint-disable-next-line react/no-danger -
- )} - -
- ); -};