Skip to content

Commit

Permalink
feat: DH-14630 useViewportData + supporting utils (#1230)
Browse files Browse the repository at this point in the history
Supports Enterprise DH-14630

This PR contains a number of custom hooks + utils to support windowed
viewports for the ACL Editor.

resolves #1221
  • Loading branch information
bmingles authored Apr 20, 2023
1 parent 38c37a4 commit 2f9c020
Show file tree
Hide file tree
Showing 17 changed files with 1,006 additions and 0 deletions.
46 changes: 46 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions packages/jsapi-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,14 @@
},
"dependencies": {
"@deephaven/components": "file:../components",
"@deephaven/jsapi-bootstrap": "file:../jsapi-bootstrap",
"@deephaven/jsapi-shim": "file:../jsapi-shim",
"@deephaven/jsapi-types": "file:../jsapi-types",
"@deephaven/jsapi-utils": "file:../jsapi-utils",
"@deephaven/log": "file:../log",
"@deephaven/react-hooks": "file:../log",
"@deephaven/utils": "file:../utils",
"@react-stately/data": "^3.9.1",
"classnames": "^2.3.2",
"prop-types": "^15.8.1"
},
Expand Down
4 changes: 4 additions & 0 deletions packages/jsapi-components/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
export { default as TableInput } from './TableInput';
export { default as useInitializeViewportData } from './useInitializeViewportData';
export { default as useTable } from './useTable';
export { default as useTableColumn } from './useTableColumn';
export { default as useTableListener } from './useTableListener';
export { default as useSelectDistinctTable } from './useSelectDistinctTable';
export { default as useSetPaddedViewportCallback } from './useSetPaddedViewportCallback';
export { default as useViewportData } from './useViewportData';
45 changes: 45 additions & 0 deletions packages/jsapi-components/src/useInitializeViewportData.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { act, renderHook } from '@testing-library/react-hooks';
import { Table } from '@deephaven/jsapi-shim';
import { KeyedItem } from '@deephaven/jsapi-utils';
import { TestUtils } from '@deephaven/utils';
import useInitializeViewportData from './useInitializeViewportData';

const tableA = TestUtils.createMockProxy<Table>({ size: 4 });
const expectedInitialA: KeyedItem<unknown>[] = [
{ key: '0' },
{ key: '1' },
{ key: '2' },
{ key: '3' },
];

const tableB = TestUtils.createMockProxy<Table>({ size: 2 });
const expectedInitialB = [{ key: '0' }, { key: '1' }];

it('should initialize a ListData object based on Table size', () => {
const { result } = renderHook(() => useInitializeViewportData(tableA));

expect(result.current.items).toEqual(expectedInitialA);
});

it('should re-initialize a ListData object if Table reference changes', () => {
const { result, rerender } = renderHook(
({ table }) => useInitializeViewportData(table),
{
initialProps: { table: tableA },
}
);

// Update an item
const updatedItem = { key: '0', item: 'mock.item' };
act(() => {
result.current.update(updatedItem.key, updatedItem);
});

const expectedAfterUpdate = [updatedItem, ...expectedInitialA.slice(1)];
expect(result.current.items).toEqual(expectedAfterUpdate);

// Re-render with a new table instance
rerender({ table: tableB });

expect(result.current.items).toEqual(expectedInitialB);
});
46 changes: 46 additions & 0 deletions packages/jsapi-components/src/useInitializeViewportData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useEffect } from 'react';
import { ListData, useListData } from '@react-stately/data';
import { Table, TreeTable } from '@deephaven/jsapi-shim';
import {
KeyedItem,
generateEmptyKeyedItems,
getSize,
} from '@deephaven/jsapi-utils';

/**
* Initializes a ListData instance that can be used for windowed views of a
* Table. The list must always contain a KeyedItem for every record in the table,
* so it is pre-populated with empty items that can be updated with real data as
* the window changes.
*
* IMPORTANT: this will create an empty KeyedItem object for every row in the
* source table. This is intended for "human" sized tables such as those used in
* admin panels. This is not suitable for "machine" scale with millions+ rows.
* @param table The table that will be used to determine the list size.
* @returns
*/
export default function useInitializeViewportData<T>(
table: Table | TreeTable | null
): ListData<KeyedItem<T>> {
const viewportData = useListData<KeyedItem<T>>({});

// We only want this to fire 1x once the table exists. Note that `useListData`
// has no way to respond to a reference change of the `table` instance so we
// have to manually delete any previous keyed items from the list.
useEffect(() => {
if (!table) {
return;
}

if (viewportData.items.length) {
viewportData.remove(...viewportData.items.map(({ key }) => key));
}

viewportData.insert(0, ...generateEmptyKeyedItems<T>(getSize(table)));

// Intentionally excluding viewportData since it changes on every render.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [table]);

return viewportData;
}
54 changes: 54 additions & 0 deletions packages/jsapi-components/src/useSelectDistinctTable.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Table } from '@deephaven/jsapi-shim';
import { TestUtils } from '@deephaven/utils';
import { renderHook } from '@testing-library/react-hooks';
import useSelectDistinctTable from './useSelectDistinctTable';

let table: Table;
let derivedTable: Table;

beforeEach(() => {
jest.clearAllMocks();

table = TestUtils.createMockProxy<Table>();
derivedTable = TestUtils.createMockProxy<Table>();

(table.selectDistinct as jest.Mock).mockResolvedValue(derivedTable);
});

it('should create and subscribe to a `selectDistinct` derivation of a given table', async () => {
const { result, waitForNextUpdate } = renderHook(() =>
useSelectDistinctTable(table)
);

expect(result.current.distinctTable).toBeNull();

await waitForNextUpdate();

expect(result.current.distinctTable).toBe(derivedTable);
});

it('should safely ignore null table', async () => {
const { result, waitForNextUpdate } = renderHook(() =>
useSelectDistinctTable(null)
);

expect(result.current.distinctTable).toBeNull();

await waitForNextUpdate();

expect(result.current.distinctTable).toBeNull();
});

it('should unsubscribe on unmount', async () => {
const { unmount, waitForNextUpdate } = renderHook(() =>
useSelectDistinctTable(table)
);

await waitForNextUpdate();

expect(derivedTable.close).not.toHaveBeenCalled();

unmount();

expect(derivedTable.close).toHaveBeenCalled();
});
35 changes: 35 additions & 0 deletions packages/jsapi-components/src/useSelectDistinctTable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useCallback, useEffect } from 'react';
import { Table, TreeTable } from '@deephaven/jsapi-shim';
import { usePromiseFactory } from '@deephaven/react-hooks';

/**
* Creates and subscribes to a `selectDistinct` derived table and unsubscribes
* on unmount.
* @param table The table to call `selectDistinct` on.
* @param columnNames The list of column names to pass to `selectDistinct`.
*/
export default function useSelectDistinctTable(
table: Table | TreeTable | null,
...columnNames: string[]
) {
const selectDistinct = useCallback(
async () => table?.selectDistinct(table.findColumns(columnNames)) ?? null,
// Disabling the exhaustive checks due to the spreading of `columnNames`
// eslint-disable-next-line react-hooks/exhaustive-deps
[table, ...columnNames]
);

const { data: distinctTable, error, isError, isLoading } = usePromiseFactory(
selectDistinct,
[]
);

useEffect(
() => () => {
distinctTable?.close();
},
[distinctTable]
);

return { distinctTable, error, isError, isLoading };
}
32 changes: 32 additions & 0 deletions packages/jsapi-components/src/useSetPaddedViewportCallback.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { renderHook } from '@testing-library/react-hooks';
import { Table } from '@deephaven/jsapi-shim';
import { TestUtils } from '@deephaven/utils';
import useSetPaddedViewportCallback from './useSetPaddedViewportCallback';

beforeEach(() => {
jest.clearAllMocks();
});

it('should create a callback that sets a padded viewport', () => {
const table = TestUtils.createMockProxy<Table>({ size: 100 });
const viewportSize = 10;
const viewportPadding = 4;

const { result } = renderHook(() =>
useSetPaddedViewportCallback(table, viewportSize, viewportPadding)
);

// Call our `setPaddedViewport` callback.
const firstRow = 30;
result.current(firstRow);

const expected = {
firstRow: firstRow - viewportPadding,
lastRow: firstRow + viewportSize + viewportPadding - 1,
};

expect(table.setViewport).toHaveBeenCalledWith(
expected.firstRow,
expected.lastRow
);
});
32 changes: 32 additions & 0 deletions packages/jsapi-components/src/useSetPaddedViewportCallback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useCallback } from 'react';
import { Table, TreeTable } from '@deephaven/jsapi-shim';
import { getSize, padFirstAndLastRow } from '@deephaven/jsapi-utils';

/**
* Creates a callback function that will set a Table viewport. The callback has
* a closure over the Table, a desired viewport size, and additional padding.
* These will be combined with a first row index passed to the callback to
* calculate the final viewport.
* @param table Table to call `setViewport` on.
* @param viewportSize The desired viewport size.
* @param viewportPadding Padding to add before and after the viewport.
* @returns A callback function for setting the viewport.
*/
export default function useSetPaddedViewportCallback(
table: Table | TreeTable | null,
viewportSize: number,
viewportPadding: number
) {
return useCallback(
function setPaddedViewport(firstRow: number) {
const [first, last] = padFirstAndLastRow(
firstRow,
viewportSize,
viewportPadding,
getSize(table)
);
table?.setViewport(first, last);
},
[table, viewportPadding, viewportSize]
);
}
Loading

0 comments on commit 2f9c020

Please sign in to comment.