Implement deeplink, creating pastes, log deduplication
Summary: This diff implements the remaining features in the logs plugin: - deeplinking - merging duplicate rows The logs plugin source code has now been reduced from originally `935` to `285` LoC. All optimisation code has been removed from the plugin: * debouncing data processing * pre-rendering (and storing!) all rows Finally applied some further styling tweaks and applied some renames to DataTable / DataSource + types finetuning. Some more will follow. Fixed a emotion warning in unit tests which was pretty annoying. Reviewed By: passy Differential Revision: D26666190 fbshipit-source-id: e45e289b4422ebeb46cad927cfc0cfcc9566834f
This commit is contained in:
committed by
Facebook GitHub Bot
parent
dec8e88aeb
commit
525e079284
@@ -228,8 +228,6 @@ const persistor = persistStore(store, undefined, () => {
|
|||||||
setPersistor(persistor);
|
setPersistor(persistor);
|
||||||
|
|
||||||
const CodeBlock = styled(Input.TextArea)({
|
const CodeBlock = styled(Input.TextArea)({
|
||||||
fontFamily:
|
...theme.monospace,
|
||||||
'SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace;',
|
|
||||||
fontSize: '0.8em',
|
|
||||||
color: theme.textColorSecondary,
|
color: theme.textColorSecondary,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export function SidebarTitle({
|
|||||||
const LeftMenuTitle = styled(Layout.Horizontal)({
|
const LeftMenuTitle = styled(Layout.Horizontal)({
|
||||||
padding: `0px ${theme.inlinePaddingH}px`,
|
padding: `0px ${theme.inlinePaddingH}px`,
|
||||||
lineHeight: `${theme.space.large}px`,
|
lineHeight: `${theme.space.large}px`,
|
||||||
fontSize: theme.fontSize.smallBody,
|
fontSize: theme.fontSize.small,
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
'> :first-child': {
|
'> :first-child': {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ function ResultTopDialog(props: {status: HealthcheckStatus}) {
|
|||||||
showIcon
|
showIcon
|
||||||
message={messages.message}
|
message={messages.message}
|
||||||
style={{
|
style={{
|
||||||
fontSize: theme.fontSize.smallBody,
|
fontSize: theme.fontSize.small,
|
||||||
lineHeight: '16px',
|
lineHeight: '16px',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
paddingTop: '10px',
|
paddingTop: '10px',
|
||||||
@@ -179,7 +179,7 @@ function SetupDoctorFooter(props: {
|
|||||||
checked={props.acknowledgeCheck}
|
checked={props.acknowledgeCheck}
|
||||||
onChange={(e) => props.onAcknowledgeCheck(e.target.checked)}
|
onChange={(e) => props.onAcknowledgeCheck(e.target.checked)}
|
||||||
style={{display: 'flex', alignItems: 'center'}}>
|
style={{display: 'flex', alignItems: 'center'}}>
|
||||||
<Text style={{fontSize: theme.fontSize.smallBody}}>
|
<Text style={{fontSize: theme.fontSize.small}}>
|
||||||
Do not show warning about these problems at startup
|
Do not show warning about these problems at startup
|
||||||
</Text>
|
</Text>
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ function WelcomeFooter({
|
|||||||
return (
|
return (
|
||||||
<FooterContainer>
|
<FooterContainer>
|
||||||
<Checkbox checked={checked} onChange={(e) => onCheck(e.target.checked)}>
|
<Checkbox checked={checked} onChange={(e) => onCheck(e.target.checked)}>
|
||||||
<Text style={{fontSize: theme.fontSize.smallBody}}>
|
<Text style={{fontSize: theme.fontSize.small}}>
|
||||||
Show this when app opens (or use ? icon on left)
|
Show this when app opens (or use ? icon on left)
|
||||||
</Text>
|
</Text>
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export default function BlocklistSettingButton(props: {
|
|||||||
key={pluginId}
|
key={pluginId}
|
||||||
closable
|
closable
|
||||||
onClose={() => props.onRemovePlugin(pluginId)}>
|
onClose={() => props.onRemovePlugin(pluginId)}>
|
||||||
<Text style={{fontSize: theme.fontSize.smallBody}} ellipsis>
|
<Text style={{fontSize: theme.fontSize.small}} ellipsis>
|
||||||
{pluginId}
|
{pluginId}
|
||||||
</Text>
|
</Text>
|
||||||
</Tag>
|
</Tag>
|
||||||
@@ -54,7 +54,7 @@ export default function BlocklistSettingButton(props: {
|
|||||||
) : (
|
) : (
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
fontSize: theme.fontSize.smallBody,
|
fontSize: theme.fontSize.small,
|
||||||
color: theme.textColorSecondary,
|
color: theme.textColorSecondary,
|
||||||
}}>
|
}}>
|
||||||
No Blocklisted Plugin
|
No Blocklisted Plugin
|
||||||
@@ -70,7 +70,7 @@ export default function BlocklistSettingButton(props: {
|
|||||||
key={category}
|
key={category}
|
||||||
closable
|
closable
|
||||||
onClose={() => props.onRemoveCategory(category)}>
|
onClose={() => props.onRemoveCategory(category)}>
|
||||||
<Text style={{fontSize: theme.fontSize.smallBody}} ellipsis>
|
<Text style={{fontSize: theme.fontSize.small}} ellipsis>
|
||||||
{category}
|
{category}
|
||||||
</Text>
|
</Text>
|
||||||
</Tag>
|
</Tag>
|
||||||
@@ -79,7 +79,7 @@ export default function BlocklistSettingButton(props: {
|
|||||||
) : (
|
) : (
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
fontSize: theme.fontSize.smallBody,
|
fontSize: theme.fontSize.small,
|
||||||
color: theme.textColorSecondary,
|
color: theme.textColorSecondary,
|
||||||
}}>
|
}}>
|
||||||
No Blocklisted Category
|
No Blocklisted Category
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ function DetailCollapse({detail}: {detail: string | React.ReactNode}) {
|
|||||||
<Paragraph
|
<Paragraph
|
||||||
type="secondary"
|
type="secondary"
|
||||||
style={{
|
style={{
|
||||||
fontSize: theme.fontSize.smallBody,
|
fontSize: theme.fontSize.small,
|
||||||
marginBottom: 0,
|
marginBottom: 0,
|
||||||
}}
|
}}
|
||||||
ellipsis={{rows: 3}}>
|
ellipsis={{rows: 3}}>
|
||||||
@@ -92,7 +92,7 @@ function DetailCollapse({detail}: {detail: string | React.ReactNode}) {
|
|||||||
<Collapse.Panel
|
<Collapse.Panel
|
||||||
key="detail"
|
key="detail"
|
||||||
header={
|
header={
|
||||||
<Text type="secondary" style={{fontSize: theme.fontSize.smallBody}}>
|
<Text type="secondary" style={{fontSize: theme.fontSize.small}}>
|
||||||
View detail
|
View detail
|
||||||
</Text>
|
</Text>
|
||||||
}>
|
}>
|
||||||
@@ -149,14 +149,14 @@ function NotificationEntry({notification}: {notification: PluginNotification}) {
|
|||||||
<Layout.Right center>
|
<Layout.Right center>
|
||||||
<Layout.Horizontal gap="tiny" center>
|
<Layout.Horizontal gap="tiny" center>
|
||||||
{icon}
|
{icon}
|
||||||
<Text style={{fontSize: theme.fontSize.smallBody}}>{pluginName}</Text>
|
<Text style={{fontSize: theme.fontSize.small}}>{pluginName}</Text>
|
||||||
</Layout.Horizontal>
|
</Layout.Horizontal>
|
||||||
{actions}
|
{actions}
|
||||||
</Layout.Right>
|
</Layout.Right>
|
||||||
<Title level={4} ellipsis={{rows: 2}}>
|
<Title level={4} ellipsis={{rows: 2}}>
|
||||||
{title}
|
{title}
|
||||||
</Title>
|
</Title>
|
||||||
<Text type="secondary" style={{fontSize: theme.fontSize.smallBody}}>
|
<Text type="secondary" style={{fontSize: theme.fontSize.small}}>
|
||||||
{clientName && appName
|
{clientName && appName
|
||||||
? `${clientName}/${appName}`
|
? `${clientName}/${appName}`
|
||||||
: clientName ?? appName ?? 'Not Connected'}
|
: clientName ?? appName ?? 'Not Connected'}
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ test('Correct top level API exposed', () => {
|
|||||||
Array [
|
Array [
|
||||||
"Atom",
|
"Atom",
|
||||||
"DataTableColumn",
|
"DataTableColumn",
|
||||||
|
"DataTableManager",
|
||||||
"DefaultKeyboardAction",
|
"DefaultKeyboardAction",
|
||||||
"Device",
|
"Device",
|
||||||
"DeviceLogEntry",
|
"DeviceLogEntry",
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export {Idler} from './utils/Idler';
|
|||||||
export {createDataSource, DataSource} from './state/datasource/DataSource';
|
export {createDataSource, DataSource} from './state/datasource/DataSource';
|
||||||
|
|
||||||
export {DataTable, DataTableColumn} from './ui/datatable/DataTable';
|
export {DataTable, DataTableColumn} from './ui/datatable/DataTable';
|
||||||
|
export {DataTableManager} from './ui/datatable/useDataTableManager';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Interactive as _Interactive,
|
Interactive as _Interactive,
|
||||||
|
|||||||
@@ -108,11 +108,8 @@ export class DataSource<
|
|||||||
output: Entry<T>[] = [];
|
output: Entry<T>[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a direct reference to the stored records.
|
* Returns a defensive copy of the stored records.
|
||||||
* The collection should be treated as readonly and mutable;
|
* This is a O(n) operation! Prefer using .size and .get instead!
|
||||||
* the collection might be directly written to by the datasource,
|
|
||||||
* so for an immutable state create a defensive copy:
|
|
||||||
* `datasource.records.slice()`
|
|
||||||
*/
|
*/
|
||||||
get records(): readonly T[] {
|
get records(): readonly T[] {
|
||||||
return this._records.map(unwrap);
|
return this._records.map(unwrap);
|
||||||
@@ -134,6 +131,18 @@ export class DataSource<
|
|||||||
this.setSortBy(undefined);
|
this.setSortBy(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get size() {
|
||||||
|
return this._records.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getRecord(index: number): T {
|
||||||
|
return this._records[index]?.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get outputSize() {
|
||||||
|
return this.output.length;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a defensive copy of the current output.
|
* Returns a defensive copy of the current output.
|
||||||
* Sort, filter, reverse and window are applied, but windowing isn't.
|
* Sort, filter, reverse and window are applied, but windowing isn't.
|
||||||
|
|||||||
@@ -203,11 +203,11 @@ const SandySplitContainer = styled.div<{
|
|||||||
alignItems: props.center ? 'center' : 'stretch',
|
alignItems: props.center ? 'center' : 'stretch',
|
||||||
gap: normalizeSpace(props.gap, theme.space.small),
|
gap: normalizeSpace(props.gap, theme.space.small),
|
||||||
overflow: props.center ? undefined : 'hidden', // only use overflow hidden in container mode, to avoid weird resizing issues
|
overflow: props.center ? undefined : 'hidden', // only use overflow hidden in container mode, to avoid weird resizing issues
|
||||||
'> :nth-child(1)': {
|
'>:nth-of-type(1)': {
|
||||||
flex: props.grow === 1 ? splitGrowStyle : splitFixedStyle,
|
flex: props.grow === 1 ? splitGrowStyle : splitFixedStyle,
|
||||||
minWidth: props.grow === 1 ? 0 : undefined,
|
minWidth: props.grow === 1 ? 0 : undefined,
|
||||||
},
|
},
|
||||||
'> :nth-child(2)': {
|
'>:nth-of-type(2)': {
|
||||||
flex: props.grow === 2 ? splitGrowStyle : splitFixedStyle,
|
flex: props.grow === 2 ? splitGrowStyle : splitFixedStyle,
|
||||||
minWidth: props.grow === 2 ? 0 : undefined,
|
minWidth: props.grow === 2 ? 0 : undefined,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import React, {
|
|||||||
RefObject,
|
RefObject,
|
||||||
MutableRefObject,
|
MutableRefObject,
|
||||||
CSSProperties,
|
CSSProperties,
|
||||||
|
useEffect,
|
||||||
} 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';
|
||||||
@@ -23,7 +24,7 @@ import {Layout} from '../Layout';
|
|||||||
import {TableHead} from './TableHead';
|
import {TableHead} from './TableHead';
|
||||||
import {Percentage} from '../../utils/widthUtils';
|
import {Percentage} from '../../utils/widthUtils';
|
||||||
import {DataSourceRenderer, DataSourceVirtualizer} from './DataSourceRenderer';
|
import {DataSourceRenderer, DataSourceVirtualizer} from './DataSourceRenderer';
|
||||||
import {useDataTableManager, TableManager} from './useDataTableManager';
|
import {useDataTableManager, DataTableManager} from './useDataTableManager';
|
||||||
import {TableSearch} from './TableSearch';
|
import {TableSearch} from './TableSearch';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import {theme} from '../theme';
|
import {theme} from '../theme';
|
||||||
@@ -40,7 +41,7 @@ interface DataTableProps<T = any> {
|
|||||||
onSelect?(record: T | undefined, records: T[]): void;
|
onSelect?(record: T | undefined, records: T[]): void;
|
||||||
onRowStyle?(record: T): CSSProperties | undefined;
|
onRowStyle?(record: T): CSSProperties | undefined;
|
||||||
// multiselect?: true
|
// multiselect?: true
|
||||||
tableManagerRef?: RefObject<TableManager>;
|
tableManagerRef?: RefObject<DataTableManager<T> | undefined>; // Actually we want a MutableRefObject, but that is not what React.createRef() returns, and we don't want to put the burden on the plugin dev to cast it...
|
||||||
_testHeight?: number; // exposed for unit testing only
|
_testHeight?: number; // exposed for unit testing only
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +93,9 @@ export function DataTable<T extends object>(
|
|||||||
props.onSelect,
|
props.onSelect,
|
||||||
);
|
);
|
||||||
if (props.tableManagerRef) {
|
if (props.tableManagerRef) {
|
||||||
(props.tableManagerRef as MutableRefObject<TableManager>).current = tableManager;
|
(props.tableManagerRef as MutableRefObject<
|
||||||
|
DataTableManager<T>
|
||||||
|
>).current = tableManager;
|
||||||
}
|
}
|
||||||
const {
|
const {
|
||||||
visibleColumns,
|
visibleColumns,
|
||||||
@@ -246,6 +249,17 @@ export function DataTable<T extends object>(
|
|||||||
return <EmptyTable dataSource={dataSource} />;
|
return <EmptyTable dataSource={dataSource} />;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
function cleanup() {
|
||||||
|
return () => {
|
||||||
|
if (props.tableManagerRef) {
|
||||||
|
(props.tableManagerRef as MutableRefObject<undefined>).current = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[props.tableManagerRef],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout.Container grow>
|
<Layout.Container grow>
|
||||||
<Layout.Top>
|
<Layout.Top>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
import {CopyOutlined, FilterOutlined} from '@ant-design/icons';
|
import {CopyOutlined, FilterOutlined} from '@ant-design/icons';
|
||||||
import {Checkbox, Menu} from 'antd';
|
import {Checkbox, Menu} from 'antd';
|
||||||
import {TableManager} from './useDataTableManager';
|
import {DataTableManager} from './useDataTableManager';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {normalizeCellValue} from './TableRow';
|
import {normalizeCellValue} from './TableRow';
|
||||||
import {tryGetFlipperLibImplementation} from '../../plugin/FlipperLib';
|
import {tryGetFlipperLibImplementation} from '../../plugin/FlipperLib';
|
||||||
@@ -17,7 +17,7 @@ import {DataTableColumn} from './DataTable';
|
|||||||
|
|
||||||
const {Item, SubMenu} = Menu;
|
const {Item, SubMenu} = Menu;
|
||||||
|
|
||||||
export function tableContextMenuFactory(tableManager: TableManager) {
|
export function tableContextMenuFactory(tableManager: DataTableManager<any>) {
|
||||||
const lib = tryGetFlipperLibImplementation();
|
const lib = tryGetFlipperLibImplementation();
|
||||||
if (!lib) {
|
if (!lib) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ 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} from './ColumnFilter';
|
import {ColumnFilterHandlers, FilterIcon} from './ColumnFilter';
|
||||||
import {DEFAULT_ROW_HEIGHT} from './TableRow';
|
|
||||||
|
|
||||||
const {Text} = Typography;
|
const {Text} = Typography;
|
||||||
|
|
||||||
@@ -41,27 +40,27 @@ function SortIcons({
|
|||||||
<CaretUpFilled
|
<CaretUpFilled
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onSort(direction === 'up' ? undefined : 'up');
|
onSort(direction === 'asc' ? undefined : 'asc');
|
||||||
}}
|
}}
|
||||||
className={
|
className={
|
||||||
'ant-table-column-sorter-up ' + (direction === 'up' ? 'active' : '')
|
'ant-table-column-sorter-up ' + (direction === 'asc' ? 'active' : '')
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<CaretDownFilled
|
<CaretDownFilled
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onSort(direction === 'down' ? undefined : 'down');
|
onSort(direction === 'desc' ? undefined : 'desc');
|
||||||
}}
|
}}
|
||||||
className={
|
className={
|
||||||
'ant-table-column-sorter-down ' +
|
'ant-table-column-sorter-down ' +
|
||||||
(direction === 'down' ? 'active' : '')
|
(direction === 'desc' ? 'active' : '')
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</SortIconsContainer>
|
</SortIconsContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const SortIconsContainer = styled.span<{direction?: 'up' | 'down'}>(
|
const SortIconsContainer = styled.span<{direction?: 'asc' | 'desc'}>(
|
||||||
({direction}) => ({
|
({direction}) => ({
|
||||||
visibility: direction === undefined ? 'hidden' : undefined,
|
visibility: direction === undefined ? 'hidden' : undefined,
|
||||||
display: 'inline-flex',
|
display: 'inline-flex',
|
||||||
@@ -127,7 +126,7 @@ function TableHeadColumn({
|
|||||||
...filterHandlers
|
...filterHandlers
|
||||||
}: {
|
}: {
|
||||||
column: DataTableColumn<any>;
|
column: DataTableColumn<any>;
|
||||||
sorted: 'up' | 'down' | undefined;
|
sorted: SortDirection;
|
||||||
isResizable: boolean;
|
isResizable: boolean;
|
||||||
onSort: (id: string, direction: SortDirection) => void;
|
onSort: (id: string, direction: SortDirection) => void;
|
||||||
sortOrder: undefined | Sorting;
|
sortOrder: undefined | Sorting;
|
||||||
@@ -169,7 +168,7 @@ function TableHeadColumn({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onSort(
|
onSort(
|
||||||
column.key,
|
column.key,
|
||||||
sorted === 'up' ? undefined : sorted === 'down' ? 'up' : 'down',
|
sorted === 'asc' ? 'desc' : sorted === 'desc' ? undefined : 'asc',
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
role="button"
|
role="button"
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ const TableBodyRowContainer = styled.div<TableBodyRowContainerProps>(
|
|||||||
? `4px solid ${theme.primaryColor}`
|
? `4px solid ${theme.primaryColor}`
|
||||||
: `4px solid transparent`,
|
: `4px solid transparent`,
|
||||||
paddingTop: 1,
|
paddingTop: 1,
|
||||||
borderBottom: `1px solid ${theme.dividerColor}`,
|
|
||||||
minHeight: DEFAULT_ROW_HEIGHT,
|
minHeight: DEFAULT_ROW_HEIGHT,
|
||||||
lineHeight: `${DEFAULT_ROW_HEIGHT - 2}px`,
|
lineHeight: `${DEFAULT_ROW_HEIGHT - 2}px`,
|
||||||
'& .anticon': {
|
'& .anticon': {
|
||||||
@@ -75,6 +74,7 @@ const TableBodyColumnContainer = styled.div<{
|
|||||||
flexGrow: props.width === undefined ? 1 : 0,
|
flexGrow: props.width === undefined ? 1 : 0,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
padding: `0 ${theme.space.small}px`,
|
padding: `0 ${theme.space.small}px`,
|
||||||
|
borderBottom: `1px solid ${theme.dividerColor}`,
|
||||||
verticalAlign: 'top',
|
verticalAlign: 'top',
|
||||||
whiteSpace: props.multiline ? 'normal' : 'nowrap',
|
whiteSpace: props.multiline ? 'normal' : 'nowrap',
|
||||||
wordWrap: props.multiline ? 'break-word' : 'normal',
|
wordWrap: props.multiline ? 'break-word' : 'normal',
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import React, {createRef} from 'react';
|
|||||||
import {DataTable, DataTableColumn} from '../DataTable';
|
import {DataTable, DataTableColumn} from '../DataTable';
|
||||||
import {render, act} from '@testing-library/react';
|
import {render, act} from '@testing-library/react';
|
||||||
import {createDataSource} from '../../../state/datasource/DataSource';
|
import {createDataSource} from '../../../state/datasource/DataSource';
|
||||||
import {computeDataTableFilter, TableManager} from '../useDataTableManager';
|
import {computeDataTableFilter, DataTableManager} from '../useDataTableManager';
|
||||||
import {Button} from 'antd';
|
import {Button} from 'antd';
|
||||||
|
|
||||||
type Todo = {
|
type Todo = {
|
||||||
@@ -41,7 +41,7 @@ const columns: DataTableColumn[] = [
|
|||||||
|
|
||||||
test('update and append', async () => {
|
test('update and append', async () => {
|
||||||
const ds = createTestDataSource();
|
const ds = createTestDataSource();
|
||||||
const ref = createRef<TableManager>();
|
const ref = createRef<DataTableManager<Todo>>();
|
||||||
const rendering = render(
|
const rendering = render(
|
||||||
<DataTable
|
<DataTable
|
||||||
dataSource={ds}
|
dataSource={ds}
|
||||||
@@ -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-8pa5c2-TableBodyRowContainer efe0za01"
|
class="css-1b7miqb-TableBodyRowContainer efe0za01"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="css-kkcfb6-TableBodyColumnContainer efe0za00"
|
class="css-bqa56k-TableBodyColumnContainer efe0za00"
|
||||||
>
|
>
|
||||||
test DataTable
|
test DataTable
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="css-kkcfb6-TableBodyColumnContainer efe0za00"
|
class="css-bqa56k-TableBodyColumnContainer efe0za00"
|
||||||
>
|
>
|
||||||
true
|
true
|
||||||
</div>
|
</div>
|
||||||
@@ -98,7 +98,7 @@ test('update and append', async () => {
|
|||||||
|
|
||||||
test('column visibility', async () => {
|
test('column visibility', async () => {
|
||||||
const ds = createTestDataSource();
|
const ds = createTestDataSource();
|
||||||
const ref = createRef<TableManager>();
|
const ref = createRef<DataTableManager<Todo>>();
|
||||||
const rendering = render(
|
const rendering = render(
|
||||||
<DataTable
|
<DataTable
|
||||||
dataSource={ds}
|
dataSource={ds}
|
||||||
@@ -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-8pa5c2-TableBodyRowContainer efe0za01"
|
class="css-1b7miqb-TableBodyRowContainer efe0za01"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="css-kkcfb6-TableBodyColumnContainer efe0za00"
|
class="css-bqa56k-TableBodyColumnContainer efe0za00"
|
||||||
>
|
>
|
||||||
test DataTable
|
test DataTable
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="css-kkcfb6-TableBodyColumnContainer efe0za00"
|
class="css-bqa56k-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-8pa5c2-TableBodyRowContainer efe0za01"
|
class="css-1b7miqb-TableBodyRowContainer efe0za01"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="css-kkcfb6-TableBodyColumnContainer efe0za00"
|
class="css-bqa56k-TableBodyColumnContainer efe0za00"
|
||||||
>
|
>
|
||||||
test DataTable
|
test DataTable
|
||||||
</div>
|
</div>
|
||||||
@@ -174,7 +174,7 @@ test('sorting', async () => {
|
|||||||
title: 'item b',
|
title: 'item b',
|
||||||
done: false,
|
done: false,
|
||||||
});
|
});
|
||||||
const ref = createRef<TableManager>();
|
const ref = createRef<DataTableManager<Todo>>();
|
||||||
const rendering = render(
|
const rendering = render(
|
||||||
<DataTable
|
<DataTable
|
||||||
dataSource={ds}
|
dataSource={ds}
|
||||||
@@ -195,7 +195,7 @@ test('sorting', async () => {
|
|||||||
}
|
}
|
||||||
// sort asc
|
// sort asc
|
||||||
act(() => {
|
act(() => {
|
||||||
ref.current?.sortColumn('title', 'down');
|
ref.current?.sortColumn('title', 'asc');
|
||||||
});
|
});
|
||||||
{
|
{
|
||||||
const elem = await rendering.findAllByText(/item/);
|
const elem = await rendering.findAllByText(/item/);
|
||||||
@@ -208,7 +208,7 @@ test('sorting', async () => {
|
|||||||
}
|
}
|
||||||
// sort desc
|
// sort desc
|
||||||
act(() => {
|
act(() => {
|
||||||
ref.current?.sortColumn('title', 'up');
|
ref.current?.sortColumn('title', 'desc');
|
||||||
});
|
});
|
||||||
{
|
{
|
||||||
const elem = await rendering.findAllByText(/item/);
|
const elem = await rendering.findAllByText(/item/);
|
||||||
@@ -249,7 +249,7 @@ test('search', async () => {
|
|||||||
title: 'item b',
|
title: 'item b',
|
||||||
done: false,
|
done: false,
|
||||||
});
|
});
|
||||||
const ref = createRef<TableManager>();
|
const ref = createRef<DataTableManager<Todo>>();
|
||||||
const rendering = render(
|
const rendering = render(
|
||||||
<DataTable
|
<DataTable
|
||||||
dataSource={ds}
|
dataSource={ds}
|
||||||
@@ -514,7 +514,7 @@ test('compute filters', () => {
|
|||||||
test('onSelect callback fires, and in order', () => {
|
test('onSelect callback fires, and in order', () => {
|
||||||
const events: any[] = [];
|
const events: any[] = [];
|
||||||
const ds = createTestDataSource();
|
const ds = createTestDataSource();
|
||||||
const ref = createRef<TableManager>();
|
const ref = createRef<DataTableManager<Todo>>();
|
||||||
const rendering = render(
|
const rendering = render(
|
||||||
<DataTable
|
<DataTable
|
||||||
dataSource={ds}
|
dataSource={ds}
|
||||||
@@ -566,7 +566,7 @@ test('onSelect callback fires, and in order', () => {
|
|||||||
test('selection always has the latest state', () => {
|
test('selection always has the latest state', () => {
|
||||||
const events: any[] = [];
|
const events: any[] = [];
|
||||||
const ds = createTestDataSource();
|
const ds = createTestDataSource();
|
||||||
const ref = createRef<TableManager>();
|
const ref = createRef<DataTableManager<Todo>>();
|
||||||
const rendering = render(
|
const rendering = render(
|
||||||
<DataTable
|
<DataTable
|
||||||
dataSource={ds}
|
dataSource={ds}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
import {DataTableColumn} from 'flipper-plugin/src/ui/datatable/DataTable';
|
import {DataTableColumn} from 'flipper-plugin/src/ui/datatable/DataTable';
|
||||||
import {Percentage} from '../../utils/widthUtils';
|
import {Percentage} from '../../utils/widthUtils';
|
||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
import {useCallback, useEffect, useMemo, useState} from 'react';
|
||||||
import {DataSource} from '../../state/datasource/DataSource';
|
import {DataSource} from '../../state/datasource/DataSource';
|
||||||
import {useMemoize} from '../../utils/useMemoize';
|
import {useMemoize} from '../../utils/useMemoize';
|
||||||
|
|
||||||
@@ -20,9 +20,45 @@ export type Sorting = {
|
|||||||
direction: Exclude<SortDirection, undefined>;
|
direction: Exclude<SortDirection, undefined>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SortDirection = 'up' | 'down' | undefined;
|
export type SortDirection = 'asc' | 'desc' | undefined;
|
||||||
|
|
||||||
export type TableManager = ReturnType<typeof useDataTableManager>;
|
export interface DataTableManager<T> {
|
||||||
|
/** The default columns, but normalized */
|
||||||
|
columns: DataTableColumn<T>[];
|
||||||
|
/** The effective columns to be rendererd */
|
||||||
|
visibleColumns: DataTableColumn<T>[];
|
||||||
|
/** The currently applicable sorting, if any */
|
||||||
|
sorting: Sorting | undefined;
|
||||||
|
/** Reset the current table preferences, including column widths an visibility, back to the default */
|
||||||
|
reset(): void;
|
||||||
|
/** Resizes the column with the given key to the given width */
|
||||||
|
resizeColumn(column: string, width: number | Percentage): void;
|
||||||
|
/** Sort by the given column. This toggles statefully between ascending, descending, none (insertion order of the data source) */
|
||||||
|
sortColumn(column: string, direction: SortDirection): void;
|
||||||
|
/** Show / hide the given column */
|
||||||
|
toggleColumnVisibility(column: string): void;
|
||||||
|
/** Active search value */
|
||||||
|
setSearchValue(value: string): void;
|
||||||
|
/** current selection, describes the index index in the datasources's current output (not window) */
|
||||||
|
selection: Selection;
|
||||||
|
selectItem(
|
||||||
|
nextIndex: number | ((currentIndex: number) => number),
|
||||||
|
addToSelection?: boolean,
|
||||||
|
): void;
|
||||||
|
addRangeToSelection(
|
||||||
|
start: number,
|
||||||
|
end: number,
|
||||||
|
allowUnselect?: boolean,
|
||||||
|
): void;
|
||||||
|
clearSelection(): void;
|
||||||
|
getSelectedItem(): T | undefined;
|
||||||
|
getSelectedItems(): readonly T[];
|
||||||
|
/** Changing column filters */
|
||||||
|
addColumnFilter(column: string, value: string, disableOthers?: boolean): void;
|
||||||
|
removeColumnFilter(column: string, index: number): void;
|
||||||
|
toggleColumnFilter(column: string, index: number): void;
|
||||||
|
setColumnFilterFromSelection(column: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
type Selection = {items: ReadonlySet<number>; current: number};
|
type Selection = {items: ReadonlySet<number>; current: number};
|
||||||
|
|
||||||
@@ -38,15 +74,13 @@ export function useDataTableManager<T>(
|
|||||||
dataSource: DataSource<T>,
|
dataSource: DataSource<T>,
|
||||||
defaultColumns: DataTableColumn<T>[],
|
defaultColumns: DataTableColumn<T>[],
|
||||||
onSelect?: (item: T | undefined, items: T[]) => void,
|
onSelect?: (item: T | undefined, items: T[]) => void,
|
||||||
) {
|
): DataTableManager<T> {
|
||||||
const [columns, setEffectiveColumns] = useState(
|
const [columns, setEffectiveColumns] = useState(
|
||||||
computeInitialColumns(defaultColumns),
|
computeInitialColumns(defaultColumns),
|
||||||
);
|
);
|
||||||
// TODO: move selection with shifts with index < selection?
|
// TODO: move selection with shifts with index < selection?
|
||||||
// TODO: clear selection if out of range
|
// TODO: clear selection if out of range
|
||||||
const [selection, setSelection] = useState<Selection>(emptySelection);
|
const [selection, setSelection] = useState<Selection>(emptySelection);
|
||||||
const selectionRef = useRef(selection);
|
|
||||||
selectionRef.current = selection; // store last seen selection for fetching it later
|
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<Sorting | undefined>(undefined);
|
const [sorting, setSorting] = useState<Sorting | undefined>(undefined);
|
||||||
const [searchValue, setSearchValue] = useState('');
|
const [searchValue, setSearchValue] = useState('');
|
||||||
@@ -86,21 +120,22 @@ export function useDataTableManager<T>(
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
// N.B: we really want to have stable refs for these functions,
|
const clearSelection = useCallback(() => {
|
||||||
// to avoid that all context menus need re-render for every selection change,
|
setSelection(emptySelection);
|
||||||
// hence the selectionRef hack
|
}, []);
|
||||||
|
|
||||||
const getSelectedItem = useCallback(() => {
|
const getSelectedItem = useCallback(() => {
|
||||||
return selectionRef.current.current < 0
|
return selection.current < 0
|
||||||
? undefined
|
? undefined
|
||||||
: dataSource.getItem(selectionRef.current.current);
|
: dataSource.getItem(selection.current);
|
||||||
}, [dataSource]);
|
}, [dataSource, selection]);
|
||||||
|
|
||||||
const getSelectedItems = useCallback(() => {
|
const getSelectedItems = useCallback(() => {
|
||||||
return [...selectionRef.current.items]
|
return [...selection.items]
|
||||||
.sort()
|
.sort()
|
||||||
.map((i) => dataSource.getItem(i))
|
.map((i) => dataSource.getItem(i))
|
||||||
.filter(Boolean) as any[];
|
.filter(Boolean) as any[];
|
||||||
}, [dataSource]);
|
}, [dataSource, selection]);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
function fireSelection() {
|
function fireSelection() {
|
||||||
@@ -221,7 +256,7 @@ export function useDataTableManager<T>(
|
|||||||
dataSource.setSortBy(key as any);
|
dataSource.setSortBy(key as any);
|
||||||
}
|
}
|
||||||
if (!sorting || sorting.direction !== direction) {
|
if (!sorting || sorting.direction !== direction) {
|
||||||
dataSource.setReversed(direction === 'up');
|
dataSource.setReversed(direction === 'desc');
|
||||||
}
|
}
|
||||||
setSorting({key, direction});
|
setSorting({key, direction});
|
||||||
}
|
}
|
||||||
@@ -271,6 +306,7 @@ export function useDataTableManager<T>(
|
|||||||
selection,
|
selection,
|
||||||
selectItem,
|
selectItem,
|
||||||
addRangeToSelection,
|
addRangeToSelection,
|
||||||
|
clearSelection,
|
||||||
getSelectedItem,
|
getSelectedItem,
|
||||||
getSelectedItems,
|
getSelectedItems,
|
||||||
/** Changing column filters */
|
/** Changing column filters */
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Exposes all the variables defined in themes/base.less:
|
// Exposes all the variables defined in themes/base.less:
|
||||||
|
|
||||||
export const theme = {
|
export const theme = {
|
||||||
white: 'white', // use as counter color for primary
|
white: 'white', // use as counter color for primary
|
||||||
black: 'black',
|
black: 'black',
|
||||||
@@ -38,7 +37,12 @@ export const theme = {
|
|||||||
huge: 24,
|
huge: 24,
|
||||||
} as const,
|
} as const,
|
||||||
fontSize: {
|
fontSize: {
|
||||||
smallBody: '12px',
|
default: '14px',
|
||||||
|
small: '12px',
|
||||||
|
} as const,
|
||||||
|
monospace: {
|
||||||
|
fontFamily: 'SF Mono,Monaco,Andale Mono,monospace',
|
||||||
|
fontSize: '12px',
|
||||||
} as const,
|
} as const,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user