diff --git a/desktop/app/src/init.tsx b/desktop/app/src/init.tsx index bc0549f40..f94b402ed 100644 --- a/desktop/app/src/init.tsx +++ b/desktop/app/src/init.tsx @@ -228,8 +228,6 @@ const persistor = persistStore(store, undefined, () => { setPersistor(persistor); const CodeBlock = styled(Input.TextArea)({ - fontFamily: - 'SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace;', - fontSize: '0.8em', + ...theme.monospace, color: theme.textColorSecondary, }); diff --git a/desktop/app/src/sandy-chrome/LeftSidebar.tsx b/desktop/app/src/sandy-chrome/LeftSidebar.tsx index 371d8863b..e5cf50a00 100644 --- a/desktop/app/src/sandy-chrome/LeftSidebar.tsx +++ b/desktop/app/src/sandy-chrome/LeftSidebar.tsx @@ -42,7 +42,7 @@ export function SidebarTitle({ const LeftMenuTitle = styled(Layout.Horizontal)({ padding: `0px ${theme.inlinePaddingH}px`, lineHeight: `${theme.space.large}px`, - fontSize: theme.fontSize.smallBody, + fontSize: theme.fontSize.small, textTransform: 'uppercase', '> :first-child': { flex: 1, diff --git a/desktop/app/src/sandy-chrome/SetupDoctorScreen.tsx b/desktop/app/src/sandy-chrome/SetupDoctorScreen.tsx index 36705c910..b33c62080 100644 --- a/desktop/app/src/sandy-chrome/SetupDoctorScreen.tsx +++ b/desktop/app/src/sandy-chrome/SetupDoctorScreen.tsx @@ -83,7 +83,7 @@ function ResultTopDialog(props: {status: HealthcheckStatus}) { showIcon message={messages.message} style={{ - fontSize: theme.fontSize.smallBody, + fontSize: theme.fontSize.small, lineHeight: '16px', fontWeight: 'bold', paddingTop: '10px', @@ -179,7 +179,7 @@ function SetupDoctorFooter(props: { checked={props.acknowledgeCheck} onChange={(e) => props.onAcknowledgeCheck(e.target.checked)} style={{display: 'flex', alignItems: 'center'}}> - + Do not show warning about these problems at startup diff --git a/desktop/app/src/sandy-chrome/WelcomeScreen.tsx b/desktop/app/src/sandy-chrome/WelcomeScreen.tsx index 5d52980a0..74909e452 100644 --- a/desktop/app/src/sandy-chrome/WelcomeScreen.tsx +++ b/desktop/app/src/sandy-chrome/WelcomeScreen.tsx @@ -78,7 +78,7 @@ function WelcomeFooter({ return ( onCheck(e.target.checked)}> - + Show this when app opens (or use ? icon on left) diff --git a/desktop/app/src/sandy-chrome/notification/BlocklistSettingButton.tsx b/desktop/app/src/sandy-chrome/notification/BlocklistSettingButton.tsx index 9edaf0647..4b5c5bf28 100644 --- a/desktop/app/src/sandy-chrome/notification/BlocklistSettingButton.tsx +++ b/desktop/app/src/sandy-chrome/notification/BlocklistSettingButton.tsx @@ -45,7 +45,7 @@ export default function BlocklistSettingButton(props: { key={pluginId} closable onClose={() => props.onRemovePlugin(pluginId)}> - + {pluginId} @@ -54,7 +54,7 @@ export default function BlocklistSettingButton(props: { ) : ( No Blocklisted Plugin @@ -70,7 +70,7 @@ export default function BlocklistSettingButton(props: { key={category} closable onClose={() => props.onRemoveCategory(category)}> - + {category} @@ -79,7 +79,7 @@ export default function BlocklistSettingButton(props: { ) : ( No Blocklisted Category diff --git a/desktop/app/src/sandy-chrome/notification/Notification.tsx b/desktop/app/src/sandy-chrome/notification/Notification.tsx index 6c1d9c9c8..04d938996 100644 --- a/desktop/app/src/sandy-chrome/notification/Notification.tsx +++ b/desktop/app/src/sandy-chrome/notification/Notification.tsx @@ -69,7 +69,7 @@ function DetailCollapse({detail}: {detail: string | React.ReactNode}) { @@ -92,7 +92,7 @@ function DetailCollapse({detail}: {detail: string | React.ReactNode}) { + View detail }> @@ -149,14 +149,14 @@ function NotificationEntry({notification}: {notification: PluginNotification}) { {icon} - {pluginName} + {pluginName} {actions} {title} - + {clientName && appName ? `${clientName}/${appName}` : clientName ?? appName ?? 'Not Connected'} diff --git a/desktop/flipper-plugin/src/__tests__/api.node.tsx b/desktop/flipper-plugin/src/__tests__/api.node.tsx index 59dc0f10b..861141be2 100644 --- a/desktop/flipper-plugin/src/__tests__/api.node.tsx +++ b/desktop/flipper-plugin/src/__tests__/api.node.tsx @@ -56,6 +56,7 @@ test('Correct top level API exposed', () => { Array [ "Atom", "DataTableColumn", + "DataTableManager", "DefaultKeyboardAction", "Device", "DeviceLogEntry", diff --git a/desktop/flipper-plugin/src/index.ts b/desktop/flipper-plugin/src/index.ts index 9c363d844..555f7a345 100644 --- a/desktop/flipper-plugin/src/index.ts +++ b/desktop/flipper-plugin/src/index.ts @@ -78,6 +78,7 @@ export {Idler} from './utils/Idler'; export {createDataSource, DataSource} from './state/datasource/DataSource'; export {DataTable, DataTableColumn} from './ui/datatable/DataTable'; +export {DataTableManager} from './ui/datatable/useDataTableManager'; export { Interactive as _Interactive, diff --git a/desktop/flipper-plugin/src/state/datasource/DataSource.tsx b/desktop/flipper-plugin/src/state/datasource/DataSource.tsx index 61ac62ab7..3fb32c547 100644 --- a/desktop/flipper-plugin/src/state/datasource/DataSource.tsx +++ b/desktop/flipper-plugin/src/state/datasource/DataSource.tsx @@ -108,11 +108,8 @@ export class DataSource< output: Entry[] = []; /** - * Returns a direct reference to the stored records. - * The collection should be treated as readonly and mutable; - * the collection might be directly written to by the datasource, - * so for an immutable state create a defensive copy: - * `datasource.records.slice()` + * Returns a defensive copy of the stored records. + * This is a O(n) operation! Prefer using .size and .get instead! */ get records(): readonly T[] { return this._records.map(unwrap); @@ -134,6 +131,18 @@ export class DataSource< 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. * Sort, filter, reverse and window are applied, but windowing isn't. diff --git a/desktop/flipper-plugin/src/ui/Layout.tsx b/desktop/flipper-plugin/src/ui/Layout.tsx index 4f815d0ac..796f4d413 100644 --- a/desktop/flipper-plugin/src/ui/Layout.tsx +++ b/desktop/flipper-plugin/src/ui/Layout.tsx @@ -203,11 +203,11 @@ const SandySplitContainer = styled.div<{ alignItems: props.center ? 'center' : 'stretch', gap: normalizeSpace(props.gap, theme.space.small), 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, minWidth: props.grow === 1 ? 0 : undefined, }, - '> :nth-child(2)': { + '>:nth-of-type(2)': { flex: props.grow === 2 ? splitGrowStyle : splitFixedStyle, minWidth: props.grow === 2 ? 0 : undefined, }, diff --git a/desktop/flipper-plugin/src/ui/datatable/DataTable.tsx b/desktop/flipper-plugin/src/ui/datatable/DataTable.tsx index 0ccd28a05..6a4fb6235 100644 --- a/desktop/flipper-plugin/src/ui/datatable/DataTable.tsx +++ b/desktop/flipper-plugin/src/ui/datatable/DataTable.tsx @@ -16,6 +16,7 @@ import React, { RefObject, MutableRefObject, CSSProperties, + useEffect, } from 'react'; import {TableRow, DEFAULT_ROW_HEIGHT} from './TableRow'; import {DataSource} from '../../state/datasource/DataSource'; @@ -23,7 +24,7 @@ import {Layout} from '../Layout'; import {TableHead} from './TableHead'; import {Percentage} from '../../utils/widthUtils'; import {DataSourceRenderer, DataSourceVirtualizer} from './DataSourceRenderer'; -import {useDataTableManager, TableManager} from './useDataTableManager'; +import {useDataTableManager, DataTableManager} from './useDataTableManager'; import {TableSearch} from './TableSearch'; import styled from '@emotion/styled'; import {theme} from '../theme'; @@ -40,7 +41,7 @@ interface DataTableProps { onSelect?(record: T | undefined, records: T[]): void; onRowStyle?(record: T): CSSProperties | undefined; // multiselect?: true - tableManagerRef?: RefObject; + tableManagerRef?: RefObject | 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 } @@ -92,7 +93,9 @@ export function DataTable( props.onSelect, ); if (props.tableManagerRef) { - (props.tableManagerRef as MutableRefObject).current = tableManager; + (props.tableManagerRef as MutableRefObject< + DataTableManager + >).current = tableManager; } const { visibleColumns, @@ -246,6 +249,17 @@ export function DataTable( return ; }, []); + useEffect( + function cleanup() { + return () => { + if (props.tableManagerRef) { + (props.tableManagerRef as MutableRefObject).current = undefined; + } + }; + }, + [props.tableManagerRef], + ); + return ( diff --git a/desktop/flipper-plugin/src/ui/datatable/TableContextMenu.tsx b/desktop/flipper-plugin/src/ui/datatable/TableContextMenu.tsx index ca1aad6e8..f329a0be4 100644 --- a/desktop/flipper-plugin/src/ui/datatable/TableContextMenu.tsx +++ b/desktop/flipper-plugin/src/ui/datatable/TableContextMenu.tsx @@ -9,7 +9,7 @@ import {CopyOutlined, FilterOutlined} from '@ant-design/icons'; import {Checkbox, Menu} from 'antd'; -import {TableManager} from './useDataTableManager'; +import {DataTableManager} from './useDataTableManager'; import React from 'react'; import {normalizeCellValue} from './TableRow'; import {tryGetFlipperLibImplementation} from '../../plugin/FlipperLib'; @@ -17,7 +17,7 @@ import {DataTableColumn} from './DataTable'; const {Item, SubMenu} = Menu; -export function tableContextMenuFactory(tableManager: TableManager) { +export function tableContextMenuFactory(tableManager: DataTableManager) { const lib = tryGetFlipperLibImplementation(); if (!lib) { return ( diff --git a/desktop/flipper-plugin/src/ui/datatable/TableHead.tsx b/desktop/flipper-plugin/src/ui/datatable/TableHead.tsx index 4c527e0ff..9aceab1cb 100644 --- a/desktop/flipper-plugin/src/ui/datatable/TableHead.tsx +++ b/desktop/flipper-plugin/src/ui/datatable/TableHead.tsx @@ -25,7 +25,6 @@ import {CaretDownFilled, CaretUpFilled} from '@ant-design/icons'; import {Layout} from '../Layout'; import {Sorting, OnColumnResize, SortDirection} from './useDataTableManager'; import {ColumnFilterHandlers, FilterIcon} from './ColumnFilter'; -import {DEFAULT_ROW_HEIGHT} from './TableRow'; const {Text} = Typography; @@ -41,27 +40,27 @@ function SortIcons({ { e.stopPropagation(); - onSort(direction === 'up' ? undefined : 'up'); + onSort(direction === 'asc' ? undefined : 'asc'); }} className={ - 'ant-table-column-sorter-up ' + (direction === 'up' ? 'active' : '') + 'ant-table-column-sorter-up ' + (direction === 'asc' ? 'active' : '') } /> { e.stopPropagation(); - onSort(direction === 'down' ? undefined : 'down'); + onSort(direction === 'desc' ? undefined : 'desc'); }} className={ 'ant-table-column-sorter-down ' + - (direction === 'down' ? 'active' : '') + (direction === 'desc' ? 'active' : '') } /> ); } -const SortIconsContainer = styled.span<{direction?: 'up' | 'down'}>( +const SortIconsContainer = styled.span<{direction?: 'asc' | 'desc'}>( ({direction}) => ({ visibility: direction === undefined ? 'hidden' : undefined, display: 'inline-flex', @@ -127,7 +126,7 @@ function TableHeadColumn({ ...filterHandlers }: { column: DataTableColumn; - sorted: 'up' | 'down' | undefined; + sorted: SortDirection; isResizable: boolean; onSort: (id: string, direction: SortDirection) => void; sortOrder: undefined | Sorting; @@ -169,7 +168,7 @@ function TableHeadColumn({ e.stopPropagation(); onSort( column.key, - sorted === 'up' ? undefined : sorted === 'down' ? 'up' : 'down', + sorted === 'asc' ? 'desc' : sorted === 'desc' ? undefined : 'asc', ); }} role="button" diff --git a/desktop/flipper-plugin/src/ui/datatable/TableRow.tsx b/desktop/flipper-plugin/src/ui/datatable/TableRow.tsx index 4214e1488..fbaba0a5c 100644 --- a/desktop/flipper-plugin/src/ui/datatable/TableRow.tsx +++ b/desktop/flipper-plugin/src/ui/datatable/TableRow.tsx @@ -49,7 +49,6 @@ const TableBodyRowContainer = styled.div( ? `4px solid ${theme.primaryColor}` : `4px solid transparent`, paddingTop: 1, - borderBottom: `1px solid ${theme.dividerColor}`, minHeight: DEFAULT_ROW_HEIGHT, lineHeight: `${DEFAULT_ROW_HEIGHT - 2}px`, '& .anticon': { @@ -75,6 +74,7 @@ const TableBodyColumnContainer = styled.div<{ flexGrow: props.width === undefined ? 1 : 0, overflow: 'hidden', padding: `0 ${theme.space.small}px`, + borderBottom: `1px solid ${theme.dividerColor}`, verticalAlign: 'top', whiteSpace: props.multiline ? 'normal' : 'nowrap', wordWrap: props.multiline ? 'break-word' : 'normal', diff --git a/desktop/flipper-plugin/src/ui/datatable/__tests__/DataTable.node.tsx b/desktop/flipper-plugin/src/ui/datatable/__tests__/DataTable.node.tsx index 265e252a4..4e9c9ba3f 100644 --- a/desktop/flipper-plugin/src/ui/datatable/__tests__/DataTable.node.tsx +++ b/desktop/flipper-plugin/src/ui/datatable/__tests__/DataTable.node.tsx @@ -11,7 +11,7 @@ import React, {createRef} from 'react'; import {DataTable, DataTableColumn} from '../DataTable'; import {render, act} from '@testing-library/react'; import {createDataSource} from '../../../state/datasource/DataSource'; -import {computeDataTableFilter, TableManager} from '../useDataTableManager'; +import {computeDataTableFilter, DataTableManager} from '../useDataTableManager'; import {Button} from 'antd'; type Todo = { @@ -41,7 +41,7 @@ const columns: DataTableColumn[] = [ test('update and append', async () => { const ds = createTestDataSource(); - const ref = createRef(); + const ref = createRef>(); const rendering = render( { expect(elem.length).toBe(1); expect(elem[0].parentElement).toMatchInlineSnapshot(`
test DataTable
true
@@ -98,7 +98,7 @@ test('update and append', async () => { test('column visibility', async () => { const ds = createTestDataSource(); - const ref = createRef(); + const ref = createRef>(); const rendering = render( { expect(elem.length).toBe(1); expect(elem[0].parentElement).toMatchInlineSnapshot(`
test DataTable
true
@@ -137,10 +137,10 @@ test('column visibility', async () => { expect(elem.length).toBe(1); expect(elem[0].parentElement).toMatchInlineSnapshot(`
test DataTable
@@ -174,7 +174,7 @@ test('sorting', async () => { title: 'item b', done: false, }); - const ref = createRef(); + const ref = createRef>(); const rendering = render( { } // sort asc act(() => { - ref.current?.sortColumn('title', 'down'); + ref.current?.sortColumn('title', 'asc'); }); { const elem = await rendering.findAllByText(/item/); @@ -208,7 +208,7 @@ test('sorting', async () => { } // sort desc act(() => { - ref.current?.sortColumn('title', 'up'); + ref.current?.sortColumn('title', 'desc'); }); { const elem = await rendering.findAllByText(/item/); @@ -249,7 +249,7 @@ test('search', async () => { title: 'item b', done: false, }); - const ref = createRef(); + const ref = createRef>(); const rendering = render( { test('onSelect callback fires, and in order', () => { const events: any[] = []; const ds = createTestDataSource(); - const ref = createRef(); + const ref = createRef>(); const rendering = render( { test('selection always has the latest state', () => { const events: any[] = []; const ds = createTestDataSource(); - const ref = createRef(); + const ref = createRef>(); const rendering = render( ; }; -export type SortDirection = 'up' | 'down' | undefined; +export type SortDirection = 'asc' | 'desc' | undefined; -export type TableManager = ReturnType; +export interface DataTableManager { + /** The default columns, but normalized */ + columns: DataTableColumn[]; + /** The effective columns to be rendererd */ + visibleColumns: DataTableColumn[]; + /** 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; current: number}; @@ -38,15 +74,13 @@ export function useDataTableManager( dataSource: DataSource, defaultColumns: DataTableColumn[], onSelect?: (item: T | undefined, items: T[]) => void, -) { +): DataTableManager { const [columns, setEffectiveColumns] = useState( computeInitialColumns(defaultColumns), ); // TODO: move selection with shifts with index < selection? // TODO: clear selection if out of range const [selection, setSelection] = useState(emptySelection); - const selectionRef = useRef(selection); - selectionRef.current = selection; // store last seen selection for fetching it later const [sorting, setSorting] = useState(undefined); const [searchValue, setSearchValue] = useState(''); @@ -86,21 +120,22 @@ export function useDataTableManager( [], ); - // N.B: we really want to have stable refs for these functions, - // to avoid that all context menus need re-render for every selection change, - // hence the selectionRef hack + const clearSelection = useCallback(() => { + setSelection(emptySelection); + }, []); + const getSelectedItem = useCallback(() => { - return selectionRef.current.current < 0 + return selection.current < 0 ? undefined - : dataSource.getItem(selectionRef.current.current); - }, [dataSource]); + : dataSource.getItem(selection.current); + }, [dataSource, selection]); const getSelectedItems = useCallback(() => { - return [...selectionRef.current.items] + return [...selection.items] .sort() .map((i) => dataSource.getItem(i)) .filter(Boolean) as any[]; - }, [dataSource]); + }, [dataSource, selection]); useEffect( function fireSelection() { @@ -221,7 +256,7 @@ export function useDataTableManager( dataSource.setSortBy(key as any); } if (!sorting || sorting.direction !== direction) { - dataSource.setReversed(direction === 'up'); + dataSource.setReversed(direction === 'desc'); } setSorting({key, direction}); } @@ -271,6 +306,7 @@ export function useDataTableManager( selection, selectItem, addRangeToSelection, + clearSelection, getSelectedItem, getSelectedItems, /** Changing column filters */ diff --git a/desktop/flipper-plugin/src/ui/theme.tsx b/desktop/flipper-plugin/src/ui/theme.tsx index d31357528..c427e42c3 100644 --- a/desktop/flipper-plugin/src/ui/theme.tsx +++ b/desktop/flipper-plugin/src/ui/theme.tsx @@ -8,7 +8,6 @@ */ // Exposes all the variables defined in themes/base.less: - export const theme = { white: 'white', // use as counter color for primary black: 'black', @@ -38,7 +37,12 @@ export const theme = { huge: 24, } as const, fontSize: { - smallBody: '12px', + default: '14px', + small: '12px', + } as const, + monospace: { + fontFamily: 'SF Mono,Monaco,Andale Mono,monospace', + fontSize: '12px', } as const, } as const;