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 58309b424eef..49c61e671d6e 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 000000000000..5ee7a41b6c3f --- /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 000000000000..3debfa524363 --- /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 ( + <> + +