Add row styling

Summary:
Added styling / coloring to the new logs plugin, to bring it closer to feature completeness. Made the colum headers slightly more compact

Also made the API more foolproof by introducing the `useAssertStableRef` hook, that will protect against accidentally passing in props that would invalidate rendering every time.

Reviewed By: passy

Differential Revision: D26635063

fbshipit-source-id: 60b2af8db3cc3c12d8d25d922cf1735aed91dd2c
This commit is contained in:
Michel Weststrate
2021-03-16 14:54:53 -07:00
committed by Facebook GitHub Bot
parent a3b3df639b
commit dec8e88aeb
9 changed files with 103 additions and 42 deletions

View File

@@ -328,6 +328,16 @@ export class DataSource<
this.rebuildOutput(); this.rebuildOutput();
} }
/**
* Returns a fork of this dataSource, that shares the source data with this dataSource,
* but has it's own FSRW pipeline, to allow multiple views on the same data
*/
fork(): DataSource<T> {
throw new Error(
'Not implemented. Please contact oncall if this feature is needed',
);
}
emitDataEvent(event: DataEvent<T>) { emitDataEvent(event: DataEvent<T>) {
this.dataUpdateQueue.push(event); this.dataUpdateQueue.push(event);
// TODO: schedule // TODO: schedule

View File

@@ -140,7 +140,7 @@ type SplitLayoutProps = {
center?: boolean; center?: boolean;
gap?: Spacing; gap?: Spacing;
children: [React.ReactNode, React.ReactNode]; children: [React.ReactNode, React.ReactNode];
style?: React.HTMLAttributes<HTMLDivElement>['style']; style?: CSSProperties;
}; };
function renderSplitLayout( function renderSplitLayout(

View File

@@ -8,7 +8,6 @@
*/ */
import {useMemo, useState} from 'react'; import {useMemo, useState} from 'react';
import styled from '@emotion/styled';
import React from 'react'; import React from 'react';
import {theme} from '../theme'; import {theme} from '../theme';
import type {DataTableColumn} from './DataTable'; import type {DataTableColumn} from './DataTable';
@@ -19,12 +18,6 @@ import {Layout} from '../Layout';
const {Text} = Typography; const {Text} = Typography;
export const HeaderButton = styled(Button)({
padding: 4,
backgroundColor: theme.backgroundWash,
borderRadius: 0,
});
export type ColumnFilterHandlers = { export type ColumnFilterHandlers = {
onAddColumnFilter(columnId: string, value: string): void; onAddColumnFilter(columnId: string, value: string): void;
onRemoveColumnFilter(columnId: string, index: number): void; onRemoveColumnFilter(columnId: string, index: number): void;
@@ -135,14 +128,17 @@ export function FilterIcon({
return ( return (
<Dropdown overlay={menu} trigger={['click']}> <Dropdown overlay={menu} trigger={['click']}>
<HeaderButton <Button
size="small"
type="text" type="text"
style={{ style={{
backgroundColor: theme.backgroundWash,
borderRadius: 0,
visibility: isActive ? 'visible' : 'hidden', visibility: isActive ? 'visible' : 'hidden',
color: isActive ? theme.primaryColor : theme.disabledColor, color: isActive ? theme.primaryColor : theme.disabledColor,
}}> }}>
<FilterFilled /> <FilterFilled />
</HeaderButton> </Button>
</Dropdown> </Dropdown>
); );
} }

View File

@@ -15,6 +15,7 @@ import React, {
useState, useState,
RefObject, RefObject,
MutableRefObject, MutableRefObject,
CSSProperties,
} from 'react'; } from 'react';
import {TableRow, DEFAULT_ROW_HEIGHT} from './TableRow'; import {TableRow, DEFAULT_ROW_HEIGHT} from './TableRow';
import {DataSource} from '../../state/datasource/DataSource'; import {DataSource} from '../../state/datasource/DataSource';
@@ -29,14 +30,15 @@ import {theme} from '../theme';
import {tableContextMenuFactory} from './TableContextMenu'; import {tableContextMenuFactory} from './TableContextMenu';
import {Typography} from 'antd'; import {Typography} from 'antd';
import {CoffeeOutlined, SearchOutlined} from '@ant-design/icons'; import {CoffeeOutlined, SearchOutlined} from '@ant-design/icons';
import {useAssertStableRef} from '../../utils/useAssertStableRef';
interface DataTableProps<T = any> { interface DataTableProps<T = any> {
columns: DataTableColumn<T>[]; columns: DataTableColumn<T>[];
dataSource: DataSource<T, any, any>; dataSource: DataSource<T, any, any>;
autoScroll?: boolean; autoScroll?: boolean;
extraActions?: React.ReactElement; extraActions?: React.ReactElement;
// custom onSearch(text, row) option? onSelect?(record: T | undefined, records: T[]): void;
onSelect?(item: T | undefined, items: T[]): void; onRowStyle?(record: T): CSSProperties | undefined;
// multiselect?: true // multiselect?: true
tableManagerRef?: RefObject<TableManager>; tableManagerRef?: RefObject<TableManager>;
_testHeight?: number; // exposed for unit testing only _testHeight?: number; // exposed for unit testing only
@@ -76,7 +78,13 @@ export interface RenderContext<T = any> {
export function DataTable<T extends object>( export function DataTable<T extends object>(
props: DataTableProps<T>, props: DataTableProps<T>,
): React.ReactElement { ): React.ReactElement {
const {dataSource} = props; const {dataSource, onRowStyle} = props;
useAssertStableRef(dataSource, 'dataSource');
useAssertStableRef(onRowStyle, 'onRowStyle');
useAssertStableRef(props.onSelect, 'onRowSelect');
useAssertStableRef(props.columns, 'columns');
useAssertStableRef(props._testHeight, '_testHeight');
const virtualizerRef = useRef<DataSourceVirtualizer | undefined>(); const virtualizerRef = useRef<DataSourceVirtualizer | undefined>();
const tableManager = useDataTableManager( const tableManager = useDataTableManager(
dataSource, dataSource,
@@ -135,7 +143,7 @@ export function DataTable<T extends object>(
const itemRenderer = useCallback( const itemRenderer = useCallback(
function itemRenderer( function itemRenderer(
item: any, record: T,
index: number, index: number,
renderContext: RenderContext<T>, renderContext: RenderContext<T>,
) { ) {
@@ -143,15 +151,16 @@ export function DataTable<T extends object>(
<TableRow <TableRow
key={index} key={index}
config={renderContext} config={renderContext}
value={item} record={record}
itemIndex={index} itemIndex={index}
highlighted={ highlighted={
index === selection.current || selection.items.has(index) index === selection.current || selection.items.has(index)
} }
style={onRowStyle?.(record)}
/> />
); );
}, },
[selection], [selection, onRowStyle],
); );
/** /**

View File

@@ -24,7 +24,8 @@ import {Typography} from 'antd';
import {CaretDownFilled, CaretUpFilled} from '@ant-design/icons'; import {CaretDownFilled, CaretUpFilled} from '@ant-design/icons';
import {Layout} from '../Layout'; import {Layout} from '../Layout';
import {Sorting, OnColumnResize, SortDirection} from './useDataTableManager'; import {Sorting, OnColumnResize, SortDirection} from './useDataTableManager';
import {ColumnFilterHandlers, FilterIcon, HeaderButton} from './ColumnFilter'; import {ColumnFilterHandlers, FilterIcon} from './ColumnFilter';
import {DEFAULT_ROW_HEIGHT} from './TableRow';
const {Text} = Typography; const {Text} = Typography;
@@ -90,14 +91,15 @@ TableHeaderColumnInteractive.displayName =
const TableHeadColumnContainer = styled.div<{ const TableHeadColumnContainer = styled.div<{
width: Width; width: Width;
}>((props) => ({ }>((props) => ({
// height: DEFAULT_ROW_HEIGHT,
flexShrink: props.width === undefined ? 1 : 0, flexShrink: props.width === undefined ? 1 : 0,
flexGrow: props.width === undefined ? 1 : 0, flexGrow: props.width === undefined ? 1 : 0,
width: props.width === undefined ? '100%' : props.width, width: props.width === undefined ? '100%' : props.width,
paddingLeft: 4, paddingLeft: 8,
[`:hover ${SortIconsContainer}`]: { [`:hover ${SortIconsContainer}`]: {
visibility: 'visible', visibility: 'visible',
}, },
[`&:hover ${HeaderButton}`]: { [`&:hover button`]: {
visibility: 'visible !important' as any, visibility: 'visible !important' as any,
}, },
})); }));
@@ -172,7 +174,7 @@ function TableHeadColumn({
}} }}
role="button" role="button"
tabIndex={0}> tabIndex={0}>
<Text strong> <Text type="secondary">
{column.title ?? <>&nbsp;</>} {column.title ?? <>&nbsp;</>}
<SortIcons <SortIcons
direction={sorted} direction={sorted}

View File

@@ -7,7 +7,7 @@
* @format * @format
*/ */
import React, {memo} from 'react'; import React, {CSSProperties, memo} from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import {theme} from 'flipper-plugin'; import {theme} from 'flipper-plugin';
import type {RenderContext} from './DataTable'; import type {RenderContext} from './DataTable';
@@ -23,7 +23,7 @@ type TableBodyRowContainerProps = {
const backgroundColor = (props: TableBodyRowContainerProps) => { const backgroundColor = (props: TableBodyRowContainerProps) => {
if (props.highlighted) { if (props.highlighted) {
return theme.backgroundTransparentHover; return theme.backgroundWash;
} }
return undefined; return undefined;
}; };
@@ -47,8 +47,14 @@ const TableBodyRowContainer = styled.div<TableBodyRowContainerProps>(
backgroundColor: backgroundColor(props), backgroundColor: backgroundColor(props),
borderLeft: props.highlighted borderLeft: props.highlighted
? `4px solid ${theme.primaryColor}` ? `4px solid ${theme.primaryColor}`
: `4px solid ${theme.backgroundDefault}`, : `4px solid transparent`,
paddingTop: 1,
borderBottom: `1px solid ${theme.dividerColor}`,
minHeight: DEFAULT_ROW_HEIGHT, minHeight: DEFAULT_ROW_HEIGHT,
lineHeight: `${DEFAULT_ROW_HEIGHT - 2}px`,
'& .anticon': {
lineHeight: `${DEFAULT_ROW_HEIGHT - 2}px`,
},
overflow: 'hidden', overflow: 'hidden',
width: '100%', width: '100%',
flexShrink: 0, flexShrink: 0,
@@ -74,7 +80,6 @@ const TableBodyColumnContainer = styled.div<{
wordWrap: props.multiline ? 'break-word' : 'normal', wordWrap: props.multiline ? 'break-word' : 'normal',
width: props.width, width: props.width,
justifyContent: props.justifyContent, justifyContent: props.justifyContent,
borderBottom: `1px solid ${theme.dividerColor}`,
'&::selection': { '&::selection': {
color: 'inherit', color: 'inherit',
backgroundColor: theme.buttonDefaultBackground, backgroundColor: theme.buttonDefaultBackground,
@@ -85,32 +90,38 @@ TableBodyColumnContainer.displayName = 'TableRow:TableBodyColumnContainer';
type Props = { type Props = {
config: RenderContext<any>; config: RenderContext<any>;
highlighted: boolean; highlighted: boolean;
value: any; record: any;
itemIndex: number; itemIndex: number;
style?: CSSProperties;
}; };
export const TableRow = memo(function TableRow(props: Props) { export const TableRow = memo(function TableRow({
const {config, highlighted, value: row} = props; record,
itemIndex,
highlighted,
style,
config,
}: Props) {
return ( return (
<TableBodyRowContainer <TableBodyRowContainer
highlighted={highlighted} highlighted={highlighted}
data-key={row.key} data-key={record.key}
onMouseDown={(e) => { onMouseDown={(e) => {
props.config.onMouseDown(e, props.value, props.itemIndex); config.onMouseDown(e, record, itemIndex);
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
props.config.onMouseEnter(e, props.value, props.itemIndex); config.onMouseEnter(e, record, itemIndex);
}}> }}
style={style}>
{config.columns {config.columns
.filter((col) => col.visible) .filter((col) => col.visible)
.map((col) => { .map((col) => {
const value = (col as any).onRender const value = (col as any).onRender
? (col as any).onRender(row) ? (col as any).onRender(record)
: normalizeCellValue((row as any)[col.key]); : normalizeCellValue((record as any)[col.key]);
return ( return (
<TableBodyColumnContainer <TableBodyColumnContainer
className="ant-table-cell"
key={col.key as string} key={col.key as string}
multiline={col.wrap} multiline={col.wrap}
justifyContent={col.align ? col.align : 'flex-start'} justifyContent={col.align ? col.align : 'flex-start'}

View File

@@ -55,15 +55,15 @@ test('update and append', async () => {
expect(elem.length).toBe(1); expect(elem.length).toBe(1);
expect(elem[0].parentElement).toMatchInlineSnapshot(` expect(elem[0].parentElement).toMatchInlineSnapshot(`
<div <div
class="css-tihkal-TableBodyRowContainer efe0za01" class="css-8pa5c2-TableBodyRowContainer efe0za01"
> >
<div <div
class="ant-table-cell css-1u65yt0-TableBodyColumnContainer efe0za00" class="css-kkcfb6-TableBodyColumnContainer efe0za00"
> >
test DataTable test DataTable
</div> </div>
<div <div
class="ant-table-cell css-1u65yt0-TableBodyColumnContainer efe0za00" class="css-kkcfb6-TableBodyColumnContainer efe0za00"
> >
true true
</div> </div>
@@ -112,15 +112,15 @@ test('column visibility', async () => {
expect(elem.length).toBe(1); expect(elem.length).toBe(1);
expect(elem[0].parentElement).toMatchInlineSnapshot(` expect(elem[0].parentElement).toMatchInlineSnapshot(`
<div <div
class="css-tihkal-TableBodyRowContainer efe0za01" class="css-8pa5c2-TableBodyRowContainer efe0za01"
> >
<div <div
class="ant-table-cell css-1u65yt0-TableBodyColumnContainer efe0za00" class="css-kkcfb6-TableBodyColumnContainer efe0za00"
> >
test DataTable test DataTable
</div> </div>
<div <div
class="ant-table-cell css-1u65yt0-TableBodyColumnContainer efe0za00" class="css-kkcfb6-TableBodyColumnContainer efe0za00"
> >
true true
</div> </div>
@@ -137,10 +137,10 @@ test('column visibility', async () => {
expect(elem.length).toBe(1); expect(elem.length).toBe(1);
expect(elem[0].parentElement).toMatchInlineSnapshot(` expect(elem[0].parentElement).toMatchInlineSnapshot(`
<div <div
class="css-tihkal-TableBodyRowContainer efe0za01" class="css-8pa5c2-TableBodyRowContainer efe0za01"
> >
<div <div
class="ant-table-cell css-1u65yt0-TableBodyColumnContainer efe0za00" class="css-kkcfb6-TableBodyColumnContainer efe0za00"
> >
test DataTable test DataTable
</div> </div>

View File

@@ -246,6 +246,10 @@ export function useDataTableManager<T>(
[currentFilter, dataSource], [currentFilter, dataSource],
); );
// if the component unmounts, we reset the SFRW pipeline to
// avoid wasting resources in the background
useEffect(() => () => dataSource.reset(), [dataSource]);
return { return {
/** The default columns, but normalized */ /** The default columns, but normalized */
columns, columns,

View File

@@ -0,0 +1,29 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {useRef} from 'react';
/**
* This hook will throw in development builds if the value passed in is stable.
* Use this if to make sure consumers aren't creating or changing certain props over time
* (intentionally or accidentally)
*/
export const useAssertStableRef =
process.env.NODE_ENV === 'development'
? function useAssertStableRef(value: any, prop: string) {
const ref = useRef(value);
if (ref.current !== value) {
throw new Error(
`[useAssertStableRef] An unstable reference was passed to this component as property '${prop}'. For optimization purposes we expect that this prop doesn't change over time. You might want to create the value passed to this prop outside the render closure, store it in useCallback / useMemo / useState, or set a key on the parent component`,
);
}
}
: function (_value: any, _prop: string) {
// no-op
};