From aa372a670bcf00341797e4e8f62a9006927ad3a9 Mon Sep 17 00:00:00 2001 From: heswell Date: Mon, 4 Dec 2023 10:32:52 +0000 Subject: [PATCH] Feature docs (#1037) * convert test tables in showcase to Table instances * refine showcase Tables * GridLayout experiments * table loading fix, add instrument fix intermittent bug with table loading move add instrument to col header for basket trading * responsive basket toolbar. on order status --- docs/ui/vuu_ui_features.md | 14 +- .../vuu-data-react/src/hooks/useVuuTables.ts | 7 +- vuu-ui/packages/vuu-data-test/src/Table.ts | 132 ++++++- .../src/TickingArrayDataSource.ts | 59 +--- .../vuu-data-test/src/UpdateGenerator.ts | 41 +-- .../src/basket/reference-data/basket.ts | 22 -- .../reference-data/basketConstituent.ts | 43 --- .../src/basket/reference-data/index.ts | 5 - .../packages/vuu-data-test/src/rowUpdates.ts | 5 +- .../src/simul/reference-data/instruments.ts | 10 +- .../src/simul/reference-data/prices.ts | 61 +++- .../vuu-data-test/src/simul/simul-module.ts | 62 ++-- .../array-data-source/array-data-source.ts | 101 +++--- .../vuu-data/src/remote-data-source.ts | 4 + .../vuu-data/src/server-proxy/server-proxy.ts | 85 ++++- .../vuu-data/src/server-proxy/viewport.ts | 2 - .../vuu-data/test/server-proxy.test.ts | 50 ++- vuu-ui/packages/vuu-data/test/test-utils.ts | 10 +- .../src/datasource-stats/DatasourceStats.tsx | 2 +- .../vuu-table/src/table-next/TableNext.tsx | 321 +++++++++++------- .../vuu-table/src/table-next/index.ts | 2 + .../vuu-table/src/table-next/moving-window.ts | 1 + .../vuu-table/src/table-next/useTableNext.ts | 13 +- .../vuu-theme/css/components/button.css | 4 + .../src/drag-drop/useDragDropNext.tsx | 6 + .../instrument-search/InstrumentSearch.tsx | 85 ++--- .../instrument-search/useInstrumentSearch.ts | 72 ++++ .../vuu-utils/src/component-registry.ts | 2 +- .../src/VuuBasketTradingFeature.tsx | 10 +- .../src/basket-table-edit/BasketTableEdit.tsx | 5 + .../basketConstituentEditColumns.ts | 6 +- .../src/basket-table-live/BasketTableLive.tsx | 2 +- .../src/basket-toolbar/BasketMenu.tsx | 4 +- .../src/basket-toolbar/BasketToolbar.css | 184 +++++++++- .../src/basket-toolbar/BasketToolbar.tsx | 35 +- .../ColHeaderAddSymbol.css | 7 + .../ColHeaderAddSymbol.tsx | 50 +++ .../src/cell-renderers/index.ts | 1 + .../src/useBasketTrading.tsx | 23 +- .../src/useBasketTradingDatasources.ts | 15 +- .../src/VuuInstrumentTilesFeature.tsx | 1 + .../src/examples/Table/SIMUL.examples.tsx | 77 +++-- .../src/examples/Table/TableNext.examples.tsx | 24 +- .../InstrumentTilesFeature.examples.tsx | 29 +- .../src/examples/html/GridLayout.examples.css | 177 ++++++++++ .../src/examples/html/GridLayout.examples.tsx | 43 +++ .../examples/html/components/GridLayout.css | 3 + .../examples/html/components/GridLayout.tsx | 44 +++ .../html/components/grid-dom-utils.ts | 16 + .../html/components/useSplitterResizing.ts | 89 +++++ vuu-ui/showcase/src/examples/html/index.ts | 1 + .../src/features/InstrumentTiles.feature.tsx | 26 +- 52 files changed, 1510 insertions(+), 583 deletions(-) delete mode 100644 vuu-ui/packages/vuu-data-test/src/basket/reference-data/basket.ts delete mode 100644 vuu-ui/packages/vuu-data-test/src/basket/reference-data/basketConstituent.ts delete mode 100644 vuu-ui/packages/vuu-data-test/src/basket/reference-data/index.ts create mode 100644 vuu-ui/packages/vuu-ui-controls/src/instrument-search/useInstrumentSearch.ts create mode 100644 vuu-ui/sample-apps/feature-basket-trading/src/cell-renderers/col-header-add-symbol/ColHeaderAddSymbol.css create mode 100644 vuu-ui/sample-apps/feature-basket-trading/src/cell-renderers/col-header-add-symbol/ColHeaderAddSymbol.tsx create mode 100644 vuu-ui/showcase/src/examples/html/GridLayout.examples.css create mode 100644 vuu-ui/showcase/src/examples/html/GridLayout.examples.tsx create mode 100644 vuu-ui/showcase/src/examples/html/components/GridLayout.css create mode 100644 vuu-ui/showcase/src/examples/html/components/GridLayout.tsx create mode 100644 vuu-ui/showcase/src/examples/html/components/grid-dom-utils.ts create mode 100644 vuu-ui/showcase/src/examples/html/components/useSplitterResizing.ts diff --git a/docs/ui/vuu_ui_features.md b/docs/ui/vuu_ui_features.md index 2e87e951b..f7057d24b 100644 --- a/docs/ui/vuu_ui_features.md +++ b/docs/ui/vuu_ui_features.md @@ -46,10 +46,17 @@ It is not ideal that all bundles must be created, together with the runtime shel ## How does a Feature manage data communication with the Vuu Server ? +Data from a remote Vuu server instance is managed using one or more client side DataSource instances. A single DataSource represents a subscription to a Vuu table. A Feature may need to create more than one dataSource. The basket trading feature is an example of a complex feature that creates five dataSources. The Filter Table feature creates a single dataSource. There is a pattern for dataSource management that all features should use and that can be seen in all the existing Features provided with the sample app. Once created, a dataSource should be saved using the session storage mechanism provided by Vuu. When loading, a Feature should first try to load dataSources from session state. If not found, they can be instantiated then saved into session state. THe reason for this is that there are some runtime scenarios that will result in a React component being unmounted, then remounted. When this happens, we want to try and avoid tearing down then recreating the server subscription(s). By storing the dataSource(s) in session state , they persist across React component remount events and can be reloaded back into the original owning component. In the future, as the Feature API is evolved, we will look at baking this behaviour into `feature datasources`. Note: if users do not do this and build Features that always create new dataSource instances whenever mounted, they will work as expected, if less efficient in terms of server resources. + ## How does a Vuu app know which Feature(s) to load ? +The sample app provided with Vuu does not load any features by default. It initially renders only the Shell. It does provide a mechanism to allow a user to add features to the app at runtime - from the palettes available in the Left Nav. The `Vuu Features` palette offers the Basket Trading and Instrument Tiles features. The `Vuu Tables` palette hosts the Filter Table feature, which can be dragged onto the main content area of the app, using any one of the listed Vuu tables. +In a real world application, the app would just as likely be built with one or more features built-in and preloaded by default. + ## Getting started - how do I create a Feature ? +TBC + ## Does the Vuu Showcase support Features ? Yes it does. The Vuu showcase is a developer tool that allows components to be rendered in isolation, with hot module reloading for a convenient developer experience. Features are a little more complex to render here because of the injection of props by the Vuu Shell @@ -57,5 +64,8 @@ and the fact that many features will create Vuu datasources. When running in the ### dataSource creation/injection in Showcase features -Most features will create Vuu dataSource(s) internally. However there is a pattern for dataSource creation, descibed above. Features should use the session state service provided by the Vuu shell to manage any dataSources created. The Showcase can take advantage of this pattern by pre-populating the session store with dataSources. The Feature, when loaded will then use these dataSources rather than creating. -In the existing Showcase examples, there is a `features` folder. The components in here are wrappers round actual VuuFeatures implemented in sample-apps. These render the actual underlying feature but only after creating local versions of the dataSource(s) required by those features and storing them in session state. WHen these features are rendered in SHowcase examples, they will be using the local test data. +Most features will create Vuu dataSource(s) internally. However there is a pattern for dataSource creation, described above, which helps us here. Features should use the session state service provided by the Vuu shell to manage any dataSources created. The Showcase can take advantage of this pattern by pre-populating the session store with one or more dataSources. The Feature, when loaded will then use these dataSources rather than creating new dataSource instances. +In the existing Showcase examples, there is a `features` folder. The components in here are wrappers round actual VuuFeatures implemented in `sample-apps`. These render the actual underlying feature but only after creating local versions of the dataSource(s) required by those features and storing them in session state. When these features are rendered in Showcase examples, they will be using the local test data. + +Within the `examples` folder of Showcase, the `VuuFeatures` folder has examples of usage of the BasketTrading, InstrumentTiles and FilterTable features. +Each of these has at least two exported examples. One exports the example feature directly, using it as a regular React component. The other exports the example using the actual `Feature` dynamic loader component, which loads the feature from a url. This mimics almost exactly the way features are loaded into a Vuu app. The actual urls employed will vary depending on whether the Showcase is being run in dev mode (with hot module reloading) or with a built version of Showcase. Both sets of urls can be seen in the examples and the set approprtate to the current environment will be used. The Showcase build is set up to define bundle examples as separate entry points so feature bundles are created. diff --git a/vuu-ui/packages/vuu-data-react/src/hooks/useVuuTables.ts b/vuu-ui/packages/vuu-data-react/src/hooks/useVuuTables.ts index 854091c6c..fa8f4c33f 100644 --- a/vuu-ui/packages/vuu-data-react/src/hooks/useVuuTables.ts +++ b/vuu-ui/packages/vuu-data-react/src/hooks/useVuuTables.ts @@ -1,5 +1,6 @@ -import { useCallback, useEffect, useState } from "react"; import { getServerAPI, TableSchema } from "@finos/vuu-data"; +import { VuuTable } from "@finos/vuu-protocol-types"; +import { useCallback, useEffect, useMemo, useState } from "react"; export const useVuuTables = () => { const [tables, setTables] = useState | undefined>(); @@ -14,6 +15,7 @@ export const useVuuTables = () => { useEffect(() => { async function fetchTableMetadata() { + console.log("GET TABLE LIST"); const server = await getServerAPI(); const { tables } = await server.getTableList(); const tableSchemas = buildTables( @@ -29,3 +31,6 @@ export const useVuuTables = () => { return tables; }; + +export const getVuuTableSchema = (table: VuuTable) => + getServerAPI().then((server) => server.getTableSchema(table)); diff --git a/vuu-ui/packages/vuu-data-test/src/Table.ts b/vuu-ui/packages/vuu-data-test/src/Table.ts index 9b645b45f..122d6bcdd 100644 --- a/vuu-ui/packages/vuu-data-test/src/Table.ts +++ b/vuu-ui/packages/vuu-data-test/src/Table.ts @@ -1,11 +1,12 @@ -import { TableSchema } from "@finos/vuu-data"; -import { VuuRowDataItemType } from "@finos/vuu-protocol-types"; +import { SchemaColumn, TableSchema } from "@finos/vuu-data"; +import { VuuRowDataItemType, VuuTable } from "@finos/vuu-protocol-types"; import { ColumnMap, EventEmitter } from "@finos/vuu-utils"; +import { UpdateGenerator } from "./rowUpdates"; export type TableEvents = { delete: (row: VuuRowDataItemType[]) => void; insert: (row: VuuRowDataItemType[]) => void; - update: (row: VuuRowDataItemType[], columnName: string) => void; + update: (row: VuuRowDataItemType[], columnName?: string) => void; }; export class Table extends EventEmitter { @@ -13,27 +14,43 @@ export class Table extends EventEmitter { #dataMap: ColumnMap; #indexOfKey: number; #schema: TableSchema; + constructor( schema: TableSchema, data: VuuRowDataItemType[][], - dataMap: ColumnMap + dataMap: ColumnMap, + updateGenerator?: UpdateGenerator ) { super(); this.#data = data; this.#dataMap = dataMap; this.#schema = schema; this.#indexOfKey = dataMap[schema.key]; + updateGenerator?.setTable(this); + updateGenerator?.setRange({ from: 0, to: 20 }); } get data() { return this.#data; } + get map() { + return this.#dataMap; + } + + get schema() { + return this.#schema; + } + insert(row: VuuRowDataItemType[]) { this.#data.push(row); this.emit("insert", row); } + findByKey(key: string) { + return this.#data.find((d) => (d[this.#indexOfKey] = key)); + } + update(key: string, columnName: string, value: VuuRowDataItemType) { const rowIndex = this.#data.findIndex( (row) => row[this.#indexOfKey] === key @@ -47,4 +64,111 @@ export class Table extends EventEmitter { this.emit("update", newRow, columnName); } } + updateRow(row: VuuRowDataItemType[]) { + const key = row[this.#indexOfKey]; + const rowIndex = this.#data.findIndex( + (row) => row[this.#indexOfKey] === key + ); + if (rowIndex !== -1) { + this.#data[rowIndex] = row; + this.emit("update", row); + } + } } + +export const buildDataColumnMap = (schema: TableSchema) => + Object.values(schema.columns).reduce((map, col, index) => { + map[col.name] = index; + return map; + }, {}); + +const getServerDataType = ( + columnName: string, + { columns: cols1, table: t1 }: TableSchema, + { columns: cols2, table: t2 }: TableSchema +) => { + const col1 = cols1.find((col) => col.name === columnName); + const col2 = cols2.find((col) => col.name === columnName); + if (col1 && col2) { + if (col1.serverDataType === col2.serverDataType) { + return col1.serverDataType; + } else { + throw Error( + `both tables ${t1.table} and ${t2.table} implement column ${columnName}, but with types differ` + ); + } + } else if (col1) { + return col1.serverDataType; + } else if (col2) { + return col2.serverDataType; + } else { + throw Error(`WTF how is this possible`); + } +}; + +// Just copies source tables, then registers update listeners. +// Not terribly efficient, but good enough for showcase +export const joinTables = ( + joinTable: VuuTable, + table1: Table, + table2: Table, + joinColumn: string +) => { + const { map: m1, schema: schema1 } = table1; + const { map: m2, schema: schema2 } = table2; + const k1 = m1[joinColumn]; + const k2 = m2[joinColumn]; + + const combinedColumns = new Set( + [...schema1.columns, ...schema2.columns].map((col) => col.name).sort() + ); + + const combinedSchema: TableSchema = { + key: joinColumn, + table: joinTable, + columns: Array.from(combinedColumns).map((columnName) => ({ + name: columnName, + serverDataType: getServerDataType(columnName, schema1, schema2), + })), + }; + + const data: VuuRowDataItemType[][] = []; + const combinedColumnMap = buildDataColumnMap(combinedSchema); + const start = performance.now(); + for (const row of table1.data) { + const row2 = table2.findByKey(String(row[k1])); + if (row2) { + const out = []; + for (const column of table1.schema.columns) { + const value = row[m1[column.name]]; + out[combinedColumnMap[column.name]] = value; + } + for (const column of table2.schema.columns) { + const value = row2[m2[column.name]]; + out[combinedColumnMap[column.name]] = value; + } + + data.push(out); + } + } + const end = performance.now(); + console.log(`took ${end - start} ms to create join table ${joinTable.table}`); + + const newTable = new Table(combinedSchema, data, combinedColumnMap); + + table2.on("update", (row) => { + const keyValue = row[k2] as string; + const targetRow = newTable.findByKey(keyValue); + if (targetRow) { + const updatedRow = targetRow.slice(); + for (const { name } of table2.schema.columns) { + if (row[m2[name]] !== updatedRow[combinedColumnMap[name]]) { + updatedRow[combinedColumnMap[name]] = row[m2[name]]; + } + } + newTable.updateRow(updatedRow); + } + }); + + return newTable; +}; diff --git a/vuu-ui/packages/vuu-data-test/src/TickingArrayDataSource.ts b/vuu-ui/packages/vuu-data-test/src/TickingArrayDataSource.ts index 32677f3bf..d629af727 100644 --- a/vuu-ui/packages/vuu-data-test/src/TickingArrayDataSource.ts +++ b/vuu-ui/packages/vuu-data-test/src/TickingArrayDataSource.ts @@ -17,7 +17,6 @@ import { VuuRowDataItemType, } from "@finos/vuu-protocol-types"; import { metadataKeys } from "@finos/vuu-utils"; -import { UpdateGenerator, UpdateHandler } from "./rowUpdates"; import { Table } from "./Table"; export type RpcService = { @@ -32,13 +31,11 @@ export interface TickingArrayDataSourceConstructorProps menuRpcServices?: RpcService[]; rpcServices?: RpcService[]; table?: Table; - updateGenerator?: UpdateGenerator; } export class TickingArrayDataSource extends ArrayDataSource { #menuRpcServices: RpcService[] | undefined; #rpcServices: RpcService[] | undefined; - #updateGenerator: UpdateGenerator | undefined; #table?: Table; constructor({ @@ -46,7 +43,6 @@ export class TickingArrayDataSource extends ArrayDataSource { menuRpcServices, rpcServices, table, - updateGenerator, menu, ...arrayDataSourceProps }: TickingArrayDataSourceConstructorProps) { @@ -60,75 +56,30 @@ export class TickingArrayDataSource extends ArrayDataSource { this._menu = menu; this.#menuRpcServices = menuRpcServices; this.#rpcServices = rpcServices; - this.#updateGenerator = updateGenerator; this.#table = table; - updateGenerator?.setDataSource(this); - updateGenerator?.setUpdateHandler(this.processUpdates); if (table) { table.on("insert", this.insert); - table.on("update", this.update); + table.on("update", this.updateRow); } } async subscribe(subscribeProps: SubscribeProps, callback: SubscribeCallback) { const subscription = super.subscribe(subscribeProps, callback); - if (subscribeProps.range) { - this.#updateGenerator?.setRange(subscribeProps.range); - } + // if (subscribeProps.range) { + // this.#updateGenerator?.setRange(subscribeProps.range); + // } return subscription; } set range(range: VuuRange) { super.range = range; - this.#updateGenerator?.setRange(range); + // this.#updateGenerator?.setRange(range); } get range() { return super.range; } - private processUpdates: UpdateHandler = (rowUpdates) => { - const updatedRows: DataSourceRow[] = []; - const data = super.currentData; - for (const [updateType, ...updateRecord] of rowUpdates) { - switch (updateType) { - case "U": { - const [rowIndex, ...updates] = updateRecord; - const row = data[rowIndex as number].slice() as DataSourceRow; - if (row) { - for (let i = 0; i < updates.length; i += 2) { - const colIdx = updates[i] as number; - const colVal = updates[i + 1]; - row[colIdx] = colVal; - } - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - // TODO this is problematic if we're filtered - // we need to update the correct underlying row - data[rowIndex] = row; - updatedRows.push(row); - } - break; - } - case "I": { - this.insert(updateRecord); - - break; - } - case "D": { - console.log(`delete row`); - break; - } - } - } - super._clientCallback?.({ - clientViewportId: super.viewport, - mode: "update", - rows: updatedRows, - type: "viewport-update", - }); - }; - private getSelectedRows() { return this.selectedRows.reduce((rows, selected) => { if (Array.isArray(selected)) { diff --git a/vuu-ui/packages/vuu-data-test/src/UpdateGenerator.ts b/vuu-ui/packages/vuu-data-test/src/UpdateGenerator.ts index 31156575d..48a78ae9a 100644 --- a/vuu-ui/packages/vuu-data-test/src/UpdateGenerator.ts +++ b/vuu-ui/packages/vuu-data-test/src/UpdateGenerator.ts @@ -1,7 +1,7 @@ -import { ArrayDataSource } from "@finos/vuu-data"; import { VuuRange } from "@finos/vuu-protocol-types"; import { random } from "./simul/reference-data"; -import type { RowUpdates, UpdateGenerator, UpdateHandler } from "./rowUpdates"; +import type { UpdateGenerator } from "./rowUpdates"; +import { Table } from "./Table"; const getNewValue = (value: number) => { const multiplier = random(0, 100) / 1000; @@ -10,9 +10,8 @@ const getNewValue = (value: number) => { }; export class BaseUpdateGenerator implements UpdateGenerator { - private dataSource: ArrayDataSource | undefined; + private table: Table | undefined; private range: VuuRange | undefined; - private updateHandler: UpdateHandler | undefined; private updating = false; private timer: number | undefined; private tickingColumns: number[]; @@ -23,20 +22,13 @@ export class BaseUpdateGenerator implements UpdateGenerator { setRange(range: VuuRange) { this.range = range; - if (!this.updating && this.updateHandler) { + if (!this.updating && this.table) { this.startUpdating(); } } - setDataSource(dataSource: ArrayDataSource) { - this.dataSource = dataSource; - } - - setUpdateHandler(updateHandler: UpdateHandler) { - this.updateHandler = updateHandler; - if (!this.updating && this.range) { - this.startUpdating(); - } + setTable(table: Table) { + this.table = table; } private startUpdating() { @@ -53,32 +45,33 @@ export class BaseUpdateGenerator implements UpdateGenerator { } update = () => { - if (this.range && this.updateHandler) { - const updates: RowUpdates[] = []; - const data = this.dataSource?.currentData; + if (this.range && this.table) { + const data = this.table?.data; if (data && data?.length > 0) { const maxRange = Math.min(this.range.to, data.length); for (let rowIndex = this.range.from; rowIndex < maxRange; rowIndex++) { + let updateCount = 0; const shallUpdateRow = random(0, 10) < 2; if (shallUpdateRow) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - const rowUpdates: RowUpdates = ["U", rowIndex]; + const rowUpdates = this.table.data[rowIndex]; const row = data[rowIndex]; for (const colIdx of this.tickingColumns) { const shallUpdateColumn = random(0, 10) < 5; if (shallUpdateColumn) { - rowUpdates.push(colIdx, getNewValue(row[colIdx] as number)); + updateCount += 1; + const newValue = getNewValue(row[colIdx] as number); + if (this.table) { + rowUpdates[colIdx] = newValue; + } } } - if (rowUpdates.length > 1) { - updates.push(rowUpdates); + if (this.table && updateCount > 0) { + this.table.updateRow(rowUpdates); } } } - if (updates.length > 0) { - this.updateHandler(updates); - } } } diff --git a/vuu-ui/packages/vuu-data-test/src/basket/reference-data/basket.ts b/vuu-ui/packages/vuu-data-test/src/basket/reference-data/basket.ts deleted file mode 100644 index 5dbf416cc..000000000 --- a/vuu-ui/packages/vuu-data-test/src/basket/reference-data/basket.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { VuuDataRow } from "@finos/vuu-protocol-types"; -import { ColumnMap } from "@finos/vuu-utils"; -import { getSchema } from "../../schemas"; - -const schema = getSchema("basket"); - -export const BasketColumnMap = Object.values(schema.columns).reduce( - (map, col, index) => { - map[col.name] = index; - return map; - }, - {} -); - -const data: VuuDataRow[] = [ - [".NASDAQ100", ".NASDAQ100", 0, 0], - [".HSI", ".HSI", 0, 0], - [".FTSE100", ".FTSE100", 0, 0], - [".SP500", ".SP500", 0, 0], -]; - -export default data; diff --git a/vuu-ui/packages/vuu-data-test/src/basket/reference-data/basketConstituent.ts b/vuu-ui/packages/vuu-data-test/src/basket/reference-data/basketConstituent.ts deleted file mode 100644 index 933435dd6..000000000 --- a/vuu-ui/packages/vuu-data-test/src/basket/reference-data/basketConstituent.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { VuuDataRow } from "@finos/vuu-protocol-types"; -import { getSchema } from "../../schemas"; -import { ColumnMap } from "@finos/vuu-utils"; -import ftse from "./ftse100"; - -const schema = getSchema("basketConstituent"); - -export const BasketConstituentColumnMap = Object.values( - schema.columns -).reduce((map, col, index) => { - map[col.name] = index; - return map; -}, {}); - -const data: VuuDataRow[] = []; - -// const start = performance.now(); -// Create 100_000 Instruments - -for (const row of ftse) { - // prettier-ignore - const [ric, name, lastTrade, change, volume] = row; - - const basketId = ".FTSE100"; - const side = "BUY"; - const weighting = 1; - - data.push([ - basketId, - change, - lastTrade, - ric, - `${ric}-${basketId}`, - side, - volume, - weighting, - ]); -} - -// const end = performance.now(); -// console.log(`generating 100,000 instrumentPrices took ${end - start} ms`); - -export default data; diff --git a/vuu-ui/packages/vuu-data-test/src/basket/reference-data/index.ts b/vuu-ui/packages/vuu-data-test/src/basket/reference-data/index.ts deleted file mode 100644 index b430b4240..000000000 --- a/vuu-ui/packages/vuu-data-test/src/basket/reference-data/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { default as BasketReferenceData, BasketColumnMap } from "./basket"; -export { - default as BasketConstituentReferenceData, - BasketConstituentColumnMap, -} from "./basketConstituent"; diff --git a/vuu-ui/packages/vuu-data-test/src/rowUpdates.ts b/vuu-ui/packages/vuu-data-test/src/rowUpdates.ts index 0712ed9fa..b1d5305b1 100644 --- a/vuu-ui/packages/vuu-data-test/src/rowUpdates.ts +++ b/vuu-ui/packages/vuu-data-test/src/rowUpdates.ts @@ -1,14 +1,13 @@ import { VuuRange, VuuRowDataItemType } from "@finos/vuu-protocol-types"; -import { ArrayDataSource } from "@finos/vuu-data"; +import { Table } from "./Table"; export type UpdateHandler = ( updates: (RowUpdates | RowInsert | RowDelete)[] ) => void; export interface UpdateGenerator { - setDataSource: (dataSource: ArrayDataSource) => void; + setTable: (table: Table) => void; setRange: (range: VuuRange) => void; - setUpdateHandler: (updateHandler: UpdateHandler) => void; } export type UpdateType = "I" | "D" | "U"; diff --git a/vuu-ui/packages/vuu-data-test/src/simul/reference-data/instruments.ts b/vuu-ui/packages/vuu-data-test/src/simul/reference-data/instruments.ts index cedad6b0a..0fc014959 100644 --- a/vuu-ui/packages/vuu-data-test/src/simul/reference-data/instruments.ts +++ b/vuu-ui/packages/vuu-data-test/src/simul/reference-data/instruments.ts @@ -3,6 +3,8 @@ import { currencies } from "./currencies"; import { locations, suffixes } from "./locations"; import { lotsizes } from "./lotsizes"; import { random } from "./utils"; +import { buildDataColumnMap, Table } from "../../Table"; +import { schemas } from "../simul-schemas"; export type bbg = string; export type currency = string; @@ -79,4 +81,10 @@ for (const char of chars) { const end = performance.now(); console.log(`generating 100,000 instruments took ${end - start} ms`); -export default instruments; +const instrumentsTable = new Table( + schemas.instruments, + instruments, + buildDataColumnMap(schemas.instruments) +); + +export default instrumentsTable; diff --git a/vuu-ui/packages/vuu-data-test/src/simul/reference-data/prices.ts b/vuu-ui/packages/vuu-data-test/src/simul/reference-data/prices.ts index 414ab85c1..c6abb5d35 100644 --- a/vuu-ui/packages/vuu-data-test/src/simul/reference-data/prices.ts +++ b/vuu-ui/packages/vuu-data-test/src/simul/reference-data/prices.ts @@ -1,5 +1,10 @@ -import InstrumentReferenceData from "./instruments"; +import { buildDataColumnMap, Table } from "../../Table"; +import { BaseUpdateGenerator } from "../../UpdateGenerator"; +import { schemas } from "../simul-schemas"; +import instrumentTable, { InstrumentsDataRow } from "./instruments"; +import { tables as basketTables } from "../../basket/basket-module"; import { random } from "./utils"; +import { VuuRowDataItemType } from "packages/vuu-protocol-types"; export type ask = number; export type askSize = number; @@ -25,12 +30,25 @@ export type PricesDataRow = [ scenario ]; +const { basketConstituent } = basketTables; + +const { bid, bidSize, ask, askSize } = buildDataColumnMap(schemas.prices); +const pricesUpdateGenerator = new BaseUpdateGenerator([ + bid, + bidSize, + ask, + askSize, +]); + const prices: PricesDataRow[] = []; const start = performance.now(); // Create 100_000 Instruments -for (const [, , , , , , ric, priceSeed] of InstrumentReferenceData) { +// prettier-ignore +for (const [,,,,,,ric, + priceSeed, +] of instrumentTable.data as InstrumentsDataRow[]) { const spread = random(0, 10); const ask = priceSeed + spread / 2; @@ -56,7 +74,44 @@ for (const [, , , , , , ric, priceSeed] of InstrumentReferenceData) { ]); } +// prettier-ignore +for (const [,,,lastTrade,ric] of basketConstituent.data as VuuRowDataItemType[][]) { + const priceSeed = parseFloat(String(lastTrade)); + if (!isNaN(priceSeed)){ + const spread = random(0, 10); + const ask = priceSeed + spread / 2; + const askSize = random(1000, 3000); + const bid = priceSeed - spread / 2; + const bidSize = random(1000, 3000); + const close = priceSeed + random(0, 1) / 10; + const last = priceSeed + random(0, 1) / 10; + const open = priceSeed + random(0, 1) / 10; + const phase = "C"; + const scenario = "close"; + prices.push([ + ask, + askSize, + bid, + bidSize, + close, + last, + open, + phase, + ric, + scenario, + ]); + + } +} + const end = performance.now(); console.log(`generating 100,000 prices took ${end - start} ms`); -export default prices; +const pricesTable = new Table( + schemas.prices, + prices, + buildDataColumnMap(schemas.prices), + pricesUpdateGenerator +); + +export default pricesTable; diff --git a/vuu-ui/packages/vuu-data-test/src/simul/simul-module.ts b/vuu-ui/packages/vuu-data-test/src/simul/simul-module.ts index 7c4401de8..781fb0608 100644 --- a/vuu-ui/packages/vuu-data-test/src/simul/simul-module.ts +++ b/vuu-ui/packages/vuu-data-test/src/simul/simul-module.ts @@ -1,41 +1,36 @@ -import { VuuDataRow, VuuRowDataItemType } from "packages/vuu-protocol-types"; -import { buildColumnMap } from "@finos/vuu-utils"; -import { UpdateGenerator } from "../rowUpdates"; +import { VuuRowDataItemType } from "packages/vuu-protocol-types"; import { TickingArrayDataSource } from "../TickingArrayDataSource"; import { VuuModule } from "../vuu-modules"; -import instruments from "./reference-data/instruments"; -import prices from "./reference-data/prices"; +import instrumentsTable from "./reference-data/instruments"; +import pricesTable from "./reference-data/prices"; import { schemas, SimulTableName } from "./simul-schemas"; -import { BaseUpdateGenerator } from "../UpdateGenerator"; -import { OrderUpdateGenerator } from "./OrderUpdateGenerator"; +import { buildDataColumnMap, joinTables, Table } from "../Table"; -const childOrders: VuuDataRow[] = []; -const instrumentPrices: VuuDataRow[] = []; -const orders: VuuDataRow[] = []; -const parentOrders: VuuDataRow[] = []; - -const { bid, bidSize, ask, askSize } = buildColumnMap(schemas.prices.columns); // prettier-ignore -const pricesUpdateGenerator = new BaseUpdateGenerator([bid, bidSize, ask, askSize]); - -const orderUpdateGenerator = new OrderUpdateGenerator(); +// const pricesUpdateGenerator = new BaseUpdateGenerator([bid, bidSize, ask, askSize]); -const tables: Record = { - childOrders, - instruments, - instrumentPrices, - orders, - parentOrders, - prices, -}; +// const orderUpdateGenerator = new OrderUpdateGenerator(); -const updates: Record = { - childOrders: undefined, - instruments: undefined, - instrumentPrices: undefined, - orders: orderUpdateGenerator, - parentOrders: undefined, - prices: pricesUpdateGenerator, +const tables: Record = { + childOrders: new Table( + schemas.childOrders, + [], + buildDataColumnMap(schemas.childOrders) + ), + instruments: instrumentsTable, + instrumentPrices: joinTables( + { module: "SIMUL", table: "instrumentPrices" }, + instrumentsTable, + pricesTable, + "ric" + ), + orders: new Table(schemas.orders, [], buildDataColumnMap(schemas.orders)), + parentOrders: new Table( + schemas.parentOrders, + [], + buildDataColumnMap(schemas.parentOrders) + ), + prices: pricesTable, }; export const populateArray = (tableName: SimulTableName, count: number) => { @@ -61,13 +56,12 @@ const getColumnDescriptors = (tableName: SimulTableName) => { const createDataSource = (tableName: SimulTableName) => { const columnDescriptors = getColumnDescriptors(tableName); - const dataArray = populateArray(tableName, 10_000); return new TickingArrayDataSource({ columnDescriptors, - data: dataArray, + keyColumn: schemas[tableName].key, + table: tables[tableName], // menu: menus[tableName], // rpcServices: services[tableName], - updateGenerator: updates[tableName], }); }; diff --git a/vuu-ui/packages/vuu-data/src/array-data-source/array-data-source.ts b/vuu-ui/packages/vuu-data/src/array-data-source/array-data-source.ts index 3aa67fe47..3cd1bc07d 100644 --- a/vuu-ui/packages/vuu-data/src/array-data-source/array-data-source.ts +++ b/vuu-ui/packages/vuu-data/src/array-data-source/array-data-source.ts @@ -54,6 +54,8 @@ import { collapseGroup, expandGroup, GroupMap, groupRows } from "./group-utils"; import { sortRows } from "./sort-utils"; import { buildDataToClientMap, toClientRow } from "./array-data-utils"; +const { KEY } = metadataKeys; + export interface ArrayDataSourceConstructorProps extends Omit { columnDescriptors: ColumnDescriptor[]; @@ -67,7 +69,7 @@ const { debug } = logger("ArrayDataSource"); const toDataSourceRow = (key: number) => (data: VuuRowDataItemType[], index: number): DataSourceRow => { - return [index, index, true, false, 1, 0, data[key].toString(), 0, ...data]; + return [index, index, true, false, 1, 0, String(data[key]), 0, ...data]; }; const buildTableSchema = ( @@ -108,7 +110,7 @@ export class ArrayDataSource /** Map reflecting positions of columns in client data sent to user */ #columnMap: ColumnMap; #config: WithFullConfig = vanillaConfig; - #data: readonly DataSourceRow[]; + #data: Readonly[]; #links: LinkDescriptorWithLabel[] | undefined; #range: VuuRange = NULL_RANGE; #selectedRowsCount = 0; @@ -438,10 +440,9 @@ export class ArrayDataSource } }; - protected update = (row: VuuRowDataItemType[], columnName: string) => { + protected update = (row: VuuRowDataItemType[], columnName?: string) => { // TODO take sorting, filtering. grouping into account const keyValue = row[this.key]; - const { KEY } = metadataKeys; const colIndex = this.#columnMap[columnName]; const dataColIndex = this.dataMap?.[columnName]; const dataIndex = this.#data.findIndex((row) => row[KEY] === keyValue); @@ -451,7 +452,22 @@ export class ArrayDataSource const { from, to } = this.#range; const [rowIdx] = dataSourceRow; if (rowIdx >= from && rowIdx < to) { - this.sendRowsToClient(true); + this.sendRowsToClient(false, dataSourceRow); + } + } + }; + + protected updateRow = (row: VuuRowDataItemType[]) => { + // TODO take sorting, filtering. grouping into account + const keyValue = row[this.key]; + const dataIndex = this.#data.findIndex((row) => row[KEY] === keyValue); + if (dataIndex !== -1) { + const dataSourceRow = toDataSourceRow(this.key)(row, dataIndex); + // maybe update this in place + this.#data[dataIndex] = dataSourceRow; + const { from, to } = this.#range; + if (dataIndex >= from && dataIndex < to) { + this.sendRowsToClient(false, dataSourceRow); } } }; @@ -462,33 +478,42 @@ export class ArrayDataSource this.sendRowsToClient(forceFullRefresh); } - sendRowsToClient(forceFullRefresh = false) { - const rowRange = - this.rangeChangeRowset === "delta" && !forceFullRefresh - ? rangeNewItems(this.lastRangeServed, this.#range) - : this.#range; - const data = this.processedData ?? this.#data; - - const rowsWithinViewport = data - .slice(rowRange.from, rowRange.to) - .map((row) => - toClientRow(row, this.keys, this.selectedRows, this.dataIndices) - ); - - this.clientCallback?.({ - clientViewportId: this.viewport, - mode: "batch", - rows: rowsWithinViewport, - size: data.length, - type: "viewport-update", - }); - this.lastRangeServed = { - from: this.#range.from, - to: Math.min( - this.#range.to, - this.#range.from + rowsWithinViewport.length - ), - }; + sendRowsToClient(forceFullRefresh = false, row?: DataSourceRow) { + if (row) { + this.clientCallback?.({ + clientViewportId: this.viewport, + mode: "update", + rows: [row], + type: "viewport-update", + }); + } else { + const rowRange = + this.rangeChangeRowset === "delta" && !forceFullRefresh + ? rangeNewItems(this.lastRangeServed, this.#range) + : this.#range; + const data = this.processedData ?? this.#data; + + const rowsWithinViewport = data + .slice(rowRange.from, rowRange.to) + .map((row) => + toClientRow(row, this.keys, this.selectedRows, this.dataIndices) + ); + + this.clientCallback?.({ + clientViewportId: this.viewport, + mode: "batch", + rows: rowsWithinViewport, + size: data.length, + type: "viewport-update", + }); + this.lastRangeServed = { + from: this.#range.from, + to: Math.min( + this.#range.to, + this.#range.from + rowsWithinViewport.length + ), + }; + } } get columns() { @@ -510,9 +535,6 @@ export class ArrayDataSource this.#columnMap = buildColumnMap(columns); this.dataIndices = buildDataToClientMap(this.#columnMap, this.dataMap); - const dataToClientMap = buildDataToClientMap(this.#columnMap, this.dataMap); - console.log({ dataToClientMap }); - this.config = { ...this.#config, columns, @@ -617,15 +639,6 @@ export class ArrayDataSource } } - private updateRow( - rowKey: string, - colName: string, - value: VuuRowDataItemType - ) { - const row = this.findRow(parseInt(rowKey)); - console.log({ row, colName, value }); - } - applyEdit( row: DataSourceRow, columnName: string, diff --git a/vuu-ui/packages/vuu-data/src/remote-data-source.ts b/vuu-ui/packages/vuu-data/src/remote-data-source.ts index c95913826..589fbd4e9 100644 --- a/vuu-ui/packages/vuu-data/src/remote-data-source.ts +++ b/vuu-ui/packages/vuu-data/src/remote-data-source.ts @@ -236,6 +236,7 @@ export class RemoteDataSource }; unsubscribe() { + console.log(`unsubscribe #${this.viewport}`); info?.(`unsubscribe #${this.viewport}`); if (this.viewport) { this.server?.unsubscribe(this.viewport); @@ -249,6 +250,7 @@ export class RemoteDataSource } suspend() { + console.log(`suspend #${this.viewport}, current status ${this.#status}`); info?.(`suspend #${this.viewport}, current status ${this.#status}`); if (this.viewport) { this.#status = "suspended"; @@ -261,6 +263,8 @@ export class RemoteDataSource } resume() { + console.log(`resume #${this.viewport}, current status ${this.#status}`); + const isDisabled = this.#status.startsWith("disabl"); const isSuspended = this.#status === "suspended"; info?.(`resume #${this.viewport}, current status ${this.#status}`); diff --git a/vuu-ui/packages/vuu-data/src/server-proxy/server-proxy.ts b/vuu-ui/packages/vuu-data/src/server-proxy/server-proxy.ts index 1945afbd5..0af1dc051 100644 --- a/vuu-ui/packages/vuu-data/src/server-proxy/server-proxy.ts +++ b/vuu-ui/packages/vuu-data/src/server-proxy/server-proxy.ts @@ -118,6 +118,13 @@ interface PendingLogin { resolve: (value: string) => void; // TODO reject: () => void; } + +type QueuedRequest = { + clientViewportId: string; + message: ClientToServerMessage["body"]; + requestId: string; +}; + export class ServerProxy { private connection: Connection; private postMessageToClient: PostMessageToClientCallback; @@ -128,7 +135,7 @@ export class ServerProxy { private pendingLogin?: PendingLogin; private pendingRequests = new Map(); private sessionId?: string; - private queuedRequests: Array = []; + private queuedRequests: Array = []; private cachedTableMetaRequests: Map< string, Promise @@ -194,12 +201,10 @@ export class ServerProxy { // guard against subscribe message when a viewport is already subscribed if (!this.mapClientToServerViewport.has(message.viewport)) { const pendingTableSchema = this.getTableMeta(message.table); - // if ( const viewport = new Viewport(message, this.postMessageToClient); this.viewports.set(message.viewport, viewport); - // Use client side viewport id as request id, so that when we process the response, - // which will provide the serverside viewport id, we can establish a mapping between - // the two + // Use client side viewport id as request id, so that when we process the response, which + // will provide the serverside viewport id, we can establish a mapping between the two. //TODO handle CREATE_VP error, but server does not send it at the moment const pendingSubscription = this.awaitResponseToMessage( viewport.subscribe(), @@ -211,8 +216,7 @@ export class ServerProxy { ]) as Promise<[ServerToClientCreateViewPortSuccess, TableSchema]>; awaitPendingReponses.then(([subscribeResponse, tableSchema]) => { const { viewPortId: serverViewportId } = subscribeResponse; - const { status: viewportStatus } = viewport; - + const { status: previousViewportStatus } = viewport; // switch storage key from client viewportId to server viewportId if (message.viewport !== serverViewportId) { this.viewports.delete(message.viewport); @@ -241,8 +245,12 @@ export class ServerProxy { this.disableViewport(viewport); } + if (this.queuedRequests.length > 0) { + this.processQueuedRequests(); + } + if ( - viewportStatus === "subscribing" && + previousViewportStatus === "subscribing" && // A session table will never have Visual Links, nor Context Menus !isSessionTable(viewport.table) ) { @@ -274,6 +282,33 @@ export class ServerProxy { } } + private processQueuedRequests() { + const messageTypesProcessed: { [key: string]: true } = {}; + while (this.queuedRequests.length) { + const queuedRequest = this.queuedRequests.pop(); + if (queuedRequest) { + const { clientViewportId, message, requestId } = queuedRequest; + if (message.type === "CHANGE_VP_RANGE") { + if (messageTypesProcessed.CHANGE_VP_RANGE) { + continue; + } + messageTypesProcessed.CHANGE_VP_RANGE = true; + const serverViewportId = + this.mapClientToServerViewport.get(clientViewportId); + if (serverViewportId) { + this.sendMessageToServer( + { + ...message, + viewPortId: serverViewportId, + }, + requestId + ); + } + } + } + } + } + public unsubscribe(clientViewportId: string) { const serverViewportId = this.mapClientToServerViewport.get(clientViewportId); @@ -344,11 +379,18 @@ export class ServerProxy { `CHANGE_VP_RANGE [${message.range.from}-${message.range.to}] => [${serverRequest.from}-${serverRequest.to}]` ); } - this.sendIfReady( + const sentToServer = this.sendIfReady( serverRequest, requestId, viewport.status === "subscribed" ); + if (!sentToServer) { + this.queuedRequests.push({ + clientViewportId: message.viewport, + message: serverRequest, + requestId, + }); + } } if (rows) { info?.(`setViewRange ${rows.length} rows returned from cache`); @@ -707,9 +749,6 @@ export class ServerProxy { // TODO implement the message queuing in remote data view if (isReady) { this.sendMessageToServer(message, requestId); - } else { - // TODO need to make sure we keep the requestId - this.queuedRequests.push(message); } return isReady; } @@ -721,6 +760,19 @@ export class ServerProxy { ) { const { module = "CORE" } = options; if (this.authToken) { + // if (body.type === "HB_RESP") { + // // do nothing; + // } else if (body.type === "CREATE_VP" || body.type === "CHANGE_VP_RANGE") { + // console.log( + // `%c >>> ${JSON.stringify(body, null, 2)}`, + // "background-color:green;color:white;font-weight:bold;" + // ); + // } else { + // console.log( + // `%c >>> ${body.type}`, + // "background-color:green;color:white;font-weight:bold;" + // ); + // } this.connection.send({ requestId, sessionId: this.sessionId, @@ -735,7 +787,14 @@ export class ServerProxy { public handleMessageFromServer(message: ServerToClientMessage) { const { body, requestId, sessionId } = message; - // onsole.log(`%c<<< [${new Date().toISOString().slice(11,23)}] (ServerProxy) ${message.type || JSON.stringify(message)}`,"color:white;background-color:blue;font-weight:bold;"); + // if (message.body.type !== "HB") { + // console.log( + // `%c<<< [${new Date().toISOString().slice(11, 23)}] (ServerProxy) ${ + // message.body.type || JSON.stringify(message) + // }`, + // "color:white;background-color:blue;font-weight:bold;" + // ); + // } const pendingRequest = this.pendingRequests.get(requestId); if (pendingRequest) { diff --git a/vuu-ui/packages/vuu-data/src/server-proxy/viewport.ts b/vuu-ui/packages/vuu-data/src/server-proxy/viewport.ts index 4cb9a6465..de2995210 100644 --- a/vuu-ui/packages/vuu-data/src/server-proxy/viewport.ts +++ b/vuu-ui/packages/vuu-data/src/server-proxy/viewport.ts @@ -484,7 +484,6 @@ export class Viewport { ); let debounceRequest: DataSourceDebounceRequest | undefined; - // Don't use zero as a range cap, it's is likely a transient count reported immediately // following a groupBy operation. const maxRange = this.dataWindow.rowCount || undefined; @@ -533,7 +532,6 @@ export class Viewport { this.keys.reset(this.dataWindow.clientRange); const toClient = this.isTree ? toClientRowTree : toClientRow; - if (clientRows.length) { return [ serverRequest, diff --git a/vuu-ui/packages/vuu-data/test/server-proxy.test.ts b/vuu-ui/packages/vuu-data/test/server-proxy.test.ts index 050f0e755..692dbbe45 100644 --- a/vuu-ui/packages/vuu-data/test/server-proxy.test.ts +++ b/vuu-ui/packages/vuu-data/test/server-proxy.test.ts @@ -1,6 +1,9 @@ import "./global-mocks"; import { beforeEach, describe, expect, vi, it } from "vitest"; -import { TEST_setRequestId } from "../src/server-proxy/server-proxy"; +import { + ServerProxy, + TEST_setRequestId, +} from "../src/server-proxy/server-proxy"; import { Viewport } from "../src/server-proxy/viewport"; import { COMMON_ATTRS, @@ -13,6 +16,7 @@ import { subscribe, testSchema, updateTableRow, + createSubscription, } from "./test-utils"; import { DataSourceDataMessage, DataSourceEnabledMessage } from "../src"; import { VuuRow } from "@finos/vuu-protocol-types"; @@ -3675,4 +3679,48 @@ describe("ServerProxy", () => { }); }); }); + + describe("request queuing", () => { + it("queue is empty in normal operation", async () => { + const [serverProxy] = await createFixtures(); + expect(serverProxy["queuedRequests"].length).toEqual(0); + }); + + it("queues range requests sent before subscription completes, sends to server after subscription completes", async () => { + const connection = { send: vi.fn(), status: "ready" as const }; + const postMessageToClient = vi.fn(); + const serverProxy = new ServerProxy(connection, postMessageToClient); + serverProxy["authToken"] = "test"; + serverProxy["sessionId"] = "dsdsd"; + + const [clientSubscription, serverSubscriptionAck, tableMetaResponse] = + createSubscription(); + serverProxy.subscribe(clientSubscription); + serverProxy.handleMessageFromClient({ + type: "setViewRange", + viewport: "client-vp-1", + range: { from: 0, to: 20 }, + }); + expect(serverProxy["queuedRequests"].length).toEqual(1); + connection.send.mockClear(); + TEST_setRequestId(1); + serverProxy.handleMessageFromServer(serverSubscriptionAck); + serverProxy.handleMessageFromServer(tableMetaResponse); + // allow the promises pending for the subscription and metadata to resolve + await new Promise((resolve) => window.setTimeout(resolve, 0)); + // expect(serverProxy["queuedRequests"].length).toEqual(0); + console.log(`test messages sent`); + expect(connection.send).toHaveBeenCalledTimes(3); + expect(connection.send).toHaveBeenNthCalledWith(1, { + body: { + from: 0, + to: 20, + type: "CHANGE_VP_RANGE", + viewPortId: "server-vp-1", + }, + requestId: "2", + ...SERVER_MESSAGE_CONSTANTS, + }); + }); + }); }); diff --git a/vuu-ui/packages/vuu-data/test/test-utils.ts b/vuu-ui/packages/vuu-data/test/test-utils.ts index dee9cb397..589293e13 100644 --- a/vuu-ui/packages/vuu-data/test/test-utils.ts +++ b/vuu-ui/packages/vuu-data/test/test-utils.ts @@ -242,17 +242,11 @@ export const subscribe = async ( { bufferSize = 0, key = "1", to = 10 }: SubscriptionDetails ) => { const [clientSubscription, serverSubscriptionAck, tableMetaResponse] = - createSubscription({ - bufferSize, - key, - to, - }); - + createSubscription({ bufferSize, key, to }); serverProxy.subscribe(clientSubscription); serverProxy.handleMessageFromServer(serverSubscriptionAck); serverProxy.handleMessageFromServer(tableMetaResponse); - - // allow the promises pending for the subscription and ,etadata to resolve + // allow the promises pending for the subscription and metadata to resolve await new Promise((resolve) => window.setTimeout(resolve, 0)); }; diff --git a/vuu-ui/packages/vuu-table-extras/src/datasource-stats/DatasourceStats.tsx b/vuu-ui/packages/vuu-table-extras/src/datasource-stats/DatasourceStats.tsx index 7c4a4b06f..655db1446 100644 --- a/vuu-ui/packages/vuu-table-extras/src/datasource-stats/DatasourceStats.tsx +++ b/vuu-ui/packages/vuu-table-extras/src/datasource-stats/DatasourceStats.tsx @@ -28,7 +28,7 @@ export const DataSourceStats = ({ const className = cx(classBase, classNameProp); const from = numberFormatter.format(range.from + 1); - const to = numberFormatter.format(Math.min(range.to - 1, size)); + const to = numberFormatter.format(Math.min(range.to, size)); const value = numberFormatter.format(size); return (
diff --git a/vuu-ui/packages/vuu-table/src/table-next/TableNext.tsx b/vuu-ui/packages/vuu-table/src/table-next/TableNext.tsx index 552e7eea7..3a3e1a6fa 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/TableNext.tsx +++ b/vuu-ui/packages/vuu-table/src/table-next/TableNext.tsx @@ -13,6 +13,7 @@ import { DataSourceRow } from "@finos/vuu-data-types"; import { MeasuredContainer, MeasuredContainerProps, + MeasuredSize, useId, } from "@finos/vuu-layout"; import { ContextMenuProvider } from "@finos/vuu-popups"; @@ -20,7 +21,15 @@ import { DragStartHandler, dragStrategy } from "@finos/vuu-ui-controls"; import { isGroupColumn, metadataKeys, notHidden } from "@finos/vuu-utils"; import { useForkRef } from "@salt-ds/core"; import cx from "classnames"; -import { CSSProperties, FC, ForwardedRef, forwardRef, useRef } from "react"; +import { + CSSProperties, + FC, + ForwardedRef, + forwardRef, + RefObject, + useRef, + useState, +} from "react"; import { GroupHeaderCellNext as GroupHeaderCell, HeaderCell, @@ -84,7 +93,6 @@ export interface TableProps * via mouse click or keyboard (default ENTER); */ onRowClick?: TableRowClickHandler; - onShowConfigEditor?: () => void; onSelect?: TableRowSelectHandler; onSelectionChange?: SelectionChangeHandler; renderBufferSize?: number; @@ -103,40 +111,37 @@ export interface TableProps showColumnHeaders?: boolean; } -export const TableNext = forwardRef(function TableNext( - { - Row = DefaultRow, - allowDragDrop, - availableColumns, - className: classNameProp, - config, - dataSource, - disableFocus = false, - highlightedIndex: highlightedIndexProp, - id: idProp, - navigationStyle = "cell", - onAvailableColumnsChange, - onConfigChange, - onDragStart, - onDrop, - onFeatureInvocation, - onHighlight, - onRowClick: onRowClickProp, - onSelect, - onSelectionChange, - onShowConfigEditor: onShowSettings, - renderBufferSize = 0, - rowHeight = 20, - selectionModel = "extended", - showColumnHeaders = true, - headerHeight = showColumnHeaders ? 25 : 0, - style: styleProp, - ...htmlAttributes - }: TableProps, - forwardedRef: ForwardedRef -) { +const TableCore = ({ + Row = DefaultRow, + allowDragDrop, + availableColumns, + config, + containerRef, + dataSource, + disableFocus = false, + highlightedIndex: highlightedIndexProp, + id: idProp, + navigationStyle = "cell", + onAvailableColumnsChange, + onConfigChange, + onDragStart, + onDrop, + onFeatureInvocation, + onHighlight, + onRowClick: onRowClickProp, + onSelect, + onSelectionChange, + renderBufferSize = 0, + rowHeight = 20, + selectionModel = "extended", + showColumnHeaders = true, + headerHeight = showColumnHeaders ? 25 : 0, + size, +}: TableProps & { + containerRef: RefObject; + size: MeasuredSize; +}) => { const id = useId(idProp); - const containerRef = useRef(null); const { columnMap, columns, @@ -180,109 +185,179 @@ export const TableNext = forwardRef(function TableNext( renderBufferSize, rowHeight, selectionModel, + size, }); - const getStyle = () => { - return { - ...styleProp, - "--content-height": `${viewportMeasurements.contentHeight}px`, - "--horizontal-scrollbar-height": `${viewportMeasurements.horizontalScrollbarHeight}px`, - "--content-width": `${viewportMeasurements.contentWidth}px`, - "--pinned-width-left": `${viewportMeasurements.pinnedWidthLeft}px`, - "--pinned-width-right": `${viewportMeasurements.pinnedWidthRight}px`, - "--header-height": `${headerHeight}px`, - "--row-height": `${rowHeight}px`, - "--total-header-height": `${viewportMeasurements.totalHeaderHeight}px`, - "--vertical-scrollbar-width": `${viewportMeasurements.verticalScrollbarWidth}px`, - "--viewport-body-height": `${viewportMeasurements.viewportBodyHeight}px`, - } as CSSProperties; - }; - const className = cx(classBase, classNameProp, { - [`${classBase}-colLines`]: tableAttributes.columnSeparators, - [`${classBase}-rowLines`]: tableAttributes.rowSeparators, - // [`${classBase}-highlight`]: tableAttributes.showHighlightedRow, - [`${classBase}-zebra`]: tableAttributes.zebraStripes, - // [`${classBase}-loading`]: isDataLoading(tableProps.columns), - }); + const cssVariables = { + "--content-height": `${viewportMeasurements.contentHeight}px`, + "--content-width": `${viewportMeasurements.contentWidth}px`, + "--horizontal-scrollbar-height": `${viewportMeasurements.horizontalScrollbarHeight}px`, + "--pinned-width-left": `${viewportMeasurements.pinnedWidthLeft}px`, + "--pinned-width-right": `${viewportMeasurements.pinnedWidthRight}px`, + "--header-height": `${headerHeight}px`, + "--row-height": `${rowHeight}px`, + "--total-header-height": `${viewportMeasurements.totalHeaderHeight}px`, + "--vertical-scrollbar-width": `${viewportMeasurements.verticalScrollbarWidth}px`, + "--viewport-body-height": `${viewportMeasurements.viewportBodyHeight}px`, + } as CSSProperties; + + console.log(`TableNext render ${data.length} rows`); return ( - +
+
+
-
-
-
-
- {showColumnHeaders ? ( -
-
- {columns.filter(notHidden).map((col, i) => - isGroupColumn(col) ? ( - - ) : ( - - ) - )} - {draggableColumn} -
+ {showColumnHeaders ? ( +
+
+ {columns.filter(notHidden).map((col, i) => + isGroupColumn(col) ? ( + + ) : ( + + ) + )} + {draggableColumn}
- ) : null} -
- {data.map((data) => ( - - ))}
+ ) : null} +
+ {data.map((data) => ( + + ))}
- {draggableRow} - +
+ {draggableRow} ); +}; + +export const TableNext = forwardRef(function TableNext( + { + Row, + allowDragDrop, + availableColumns, + className: classNameProp, + config, + dataSource, + disableFocus, + highlightedIndex, + id, + navigationStyle, + onAvailableColumnsChange, + onConfigChange, + onDragStart, + onDrop, + onFeatureInvocation, + onHighlight, + onRowClick, + onSelect, + onSelectionChange, + renderBufferSize, + rowHeight, + selectionModel, + showColumnHeaders, + headerHeight, + style: styleProp, + ...htmlAttributes + }: TableProps, + forwardedRef: ForwardedRef +) { + const containerRef = useRef(null); + + const [size, setSize] = useState(); + + const className = cx(classBase, classNameProp, { + [`${classBase}-colLines`]: config.columnSeparators, + [`${classBase}-rowLines`]: config.rowSeparators, + // [`${classBase}-highlight`]: tableAttributes.showHighlightedRow, + [`${classBase}-zebra`]: config.zebraStripes, + // [`${classBase}-loading`]: isDataLoading(tableProps.columns), + }); + + return ( + + {size ? ( + + ) : null} + + ); }); diff --git a/vuu-ui/packages/vuu-table/src/table-next/index.ts b/vuu-ui/packages/vuu-table/src/table-next/index.ts index eac8a51bf..e58244362 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/index.ts +++ b/vuu-ui/packages/vuu-table/src/table-next/index.ts @@ -1,7 +1,9 @@ export { GroupHeaderCellNext } from "./header-cell"; +export * from "./header-cell"; export * from "./TableNext"; export * from "./table-config"; export * from "./cell-renderers"; export type { RowProps } from "./Row"; export * from "./useControlledTableNavigation"; export * from "./useTableModel"; +export * from "./useTableViewport"; diff --git a/vuu-ui/packages/vuu-table/src/table-next/moving-window.ts b/vuu-ui/packages/vuu-table/src/table-next/moving-window.ts index 7b3e07254..02cd417a4 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/moving-window.ts +++ b/vuu-ui/packages/vuu-table/src/table-next/moving-window.ts @@ -59,6 +59,7 @@ export class MovingWindow { } setRange({ from, to }: VuuRange) { + console.log(`dataWindow setRange ${from} ${to}`); if (from !== this.range.from || to !== this.range.to) { const [overlapFrom, overlapTo] = this.range.overlap(from, to); const newData = new Array(Math.max(0, to - from)); diff --git a/vuu-ui/packages/vuu-table/src/table-next/useTableNext.ts b/vuu-ui/packages/vuu-table/src/table-next/useTableNext.ts index b6a6a8693..03be9ae52 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/useTableNext.ts +++ b/vuu-ui/packages/vuu-table/src/table-next/useTableNext.ts @@ -97,6 +97,7 @@ export interface TableHookProps headerHeight: number; rowHeight: number; selectionModel: TableSelectionModel; + size: MeasuredSize; } const { KEY, IS_EXPANDED, IS_LEAF } = metadataKeys; @@ -138,6 +139,7 @@ export const useTable = ({ renderBufferSize = 0, rowHeight = 20, selectionModel, + size, }: TableHookProps) => { const [rowCount, setRowCount] = useState(dataSource.size); if (dataSource === undefined) { @@ -150,11 +152,6 @@ export const useTable = ({ const useRowDragDrop = allowDragDrop ? useDragDrop : useNullDragDrop; - const [size, setSize] = useState(); - const handleResize = useCallback((size: MeasuredSize) => { - setSize(size); - }, []); - const menuBuilder = useMemo( () => buildContextMenuDescriptors(dataSource), [dataSource] @@ -220,14 +217,17 @@ export const useTable = ({ headings, rowCount, rowHeight, - size, + size: size, }); + console.log(JSON.stringify(viewportMeasurements, null, 2)); const initialRange = useInitialValue({ from: 0, to: viewportMeasurements.rowCount, }); + console.log({ initialRange }); + const onSubscribed = useCallback( ({ tableSchema }: DataSourceSubscribedMessage) => { if (tableSchema) { @@ -729,7 +729,6 @@ export const useTable = ({ onContextMenu, onDataEdited: handleDataEdited, onRemoveGroupColumn, - onResize: handleResize, onRowClick: handleRowClick, onToggleGroup, scrollProps, diff --git a/vuu-ui/packages/vuu-theme/css/components/button.css b/vuu-ui/packages/vuu-theme/css/components/button.css index d35d0a9ab..4063fd8be 100644 --- a/vuu-ui/packages/vuu-theme/css/components/button.css +++ b/vuu-ui/packages/vuu-theme/css/components/button.css @@ -24,3 +24,7 @@ outline-width: 1px; outline-offset: -1px; } + +.saltButton[data-icon]{ + min-width: 20px; +} \ No newline at end of file diff --git a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropNext.tsx b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropNext.tsx index 8fef54eec..603f6d8a4 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropNext.tsx +++ b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropNext.tsx @@ -314,6 +314,11 @@ export const useDragDropNext: DragDropHook = ({ ? Math.abs(lastClientContraPos - clientContraPos) : 0; + if (allowDragDrop === true && !isDragSource && !isDropTarget) { + //This is a simple internal drag + return false; + } + // If isDropTarget is false, there are configured dropTargets in context // but this is not one, so drag will be handed straight over to DragProvider // (global drag). If isDropTarget is undefined, we have no DragProvider @@ -334,6 +339,7 @@ export const useDragDropNext: DragDropHook = ({ } }, [ + allowDragDrop, id, isDragSource, isDropTarget, diff --git a/vuu-ui/packages/vuu-ui-controls/src/instrument-search/InstrumentSearch.tsx b/vuu-ui/packages/vuu-ui-controls/src/instrument-search/InstrumentSearch.tsx index 89a749e0a..aaf299021 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/instrument-search/InstrumentSearch.tsx +++ b/vuu-ui/packages/vuu-ui-controls/src/instrument-search/InstrumentSearch.tsx @@ -8,17 +8,12 @@ import { } from "@finos/vuu-table"; import { Input } from "@salt-ds/core"; import cx from "classnames"; -import { - FormEvent, - HTMLAttributes, - RefCallback, - useCallback, - useMemo, - useState, -} from "react"; +import { HTMLAttributes, RefCallback, useCallback } from "react"; import "./SearchCell"; import "./InstrumentSearch.css"; +import { VuuTable } from "packages/vuu-protocol-types"; +import { useInstrumentSearch } from "./useInstrumentSearch"; const classBase = "vuuInstrumentSearch"; @@ -42,9 +37,10 @@ const defaultTableConfig: TableConfig = { export interface InstrumentSearchProps extends HTMLAttributes { TableProps?: Partial; autoFocus?: boolean; - dataSource: DataSource; + dataSource?: DataSource; placeHolder?: string; searchColumns?: string[]; + table?: VuuTable; } const searchIcon = ; @@ -53,39 +49,20 @@ export const InstrumentSearch = ({ TableProps, autoFocus = false, className, - dataSource, + dataSource: dataSourceProp, placeHolder, - searchColumns = ["description"], + searchColumns, + table, ...htmlAttributes }: InstrumentSearchProps) => { - const baseFilterPattern = useMemo( - // TODO make this contains once server supports it - () => searchColumns.map((col) => `${col} starts "__VALUE__"`).join(" or "), - [searchColumns] - ); + const { dataSource, onChange, searchState } = useInstrumentSearch({ + dataSource: dataSourceProp, + searchColumns, + table, + }); const { highlightedIndexRef, onHighlight, onKeyDown, tableRef } = - useControlledTableNavigation(-1, dataSource.size); - - const [searchState, setSearchState] = useState<{ - searchText: string; - filter: string; - }>({ searchText: "", filter: "" }); - - const handleChange = useCallback( - (evt: FormEvent) => { - const { value } = evt.target as HTMLInputElement; - const filter = baseFilterPattern.replaceAll("__VALUE__", value); - setSearchState({ - searchText: value, - filter, - }); - dataSource.filter = { - filter, - }; - }, - [baseFilterPattern, dataSource] - ); + useControlledTableNavigation(-1, dataSource?.size ?? 0); const searchCallbackRef = useCallback>((el) => { setTimeout(() => { @@ -102,25 +79,27 @@ export const InstrumentSearch = ({ placeholder={placeHolder} ref={autoFocus ? searchCallbackRef : null} value={searchState.searchText} - onChange={handleChange} + onChange={onChange} />
- + {dataSource ? ( + + ) : null}
); }; diff --git a/vuu-ui/packages/vuu-ui-controls/src/instrument-search/useInstrumentSearch.ts b/vuu-ui/packages/vuu-ui-controls/src/instrument-search/useInstrumentSearch.ts new file mode 100644 index 000000000..e384f4961 --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/instrument-search/useInstrumentSearch.ts @@ -0,0 +1,72 @@ +import { RemoteDataSource } from "@finos/vuu-data"; +import { getVuuTableSchema } from "@finos/vuu-data-react"; +import { FormEventHandler, useCallback, useMemo, useState } from "react"; +import { InstrumentSearchProps } from "./InstrumentSearch"; + +export interface InstrumentSearchHookProps + extends Pick< + InstrumentSearchProps, + "dataSource" | "searchColumns" | "table" + > { + label?: string; +} + +export const useInstrumentSearch = ({ + dataSource: dataSourceProp, + searchColumns = ["description"], + table, +}: InstrumentSearchHookProps) => { + const [dataSource, setDataSource] = useState(dataSourceProp); + const [searchState, setSearchState] = useState<{ + searchText: string; + filter: string; + }>({ searchText: "", filter: "" }); + + const baseFilterPattern = useMemo( + // TODO make this contains once server supports it + () => searchColumns.map((col) => `${col} starts "__VALUE__"`).join(" or "), + [searchColumns] + ); + + useMemo(() => { + if (dataSourceProp === undefined) { + if (table) { + getVuuTableSchema(table).then((tableSchema) => { + setDataSource( + new RemoteDataSource({ + table: tableSchema.table, + columns: tableSchema.columns.map((col) => col.name), + }) + ); + }); + } else { + throw Error( + `useInstrumentSearch, if dataSource ismnot provided as prop, Vuu table must be provided` + ); + } + } + }, [dataSourceProp, table]); + + const handleChange = useCallback( + (evt) => { + const { value } = evt.target as HTMLInputElement; + const filter = baseFilterPattern.replaceAll("__VALUE__", value); + setSearchState({ + searchText: value, + filter, + }); + if (dataSource) { + dataSource.filter = { + filter, + }; + } + }, + [baseFilterPattern, dataSource] + ); + + return { + dataSource, + onChange: handleChange, + searchState, + }; +}; diff --git a/vuu-ui/packages/vuu-utils/src/component-registry.ts b/vuu-ui/packages/vuu-utils/src/component-registry.ts index 9fcb13646..cbb98a52c 100644 --- a/vuu-ui/packages/vuu-utils/src/component-registry.ts +++ b/vuu-ui/packages/vuu-utils/src/component-registry.ts @@ -11,7 +11,7 @@ import { VuuRowDataItemType, } from "@finos/vuu-protocol-types"; import { isTypeDescriptor, isColumnTypeRenderer } from "./column-utils"; -import { HeaderCellProps } from "packages/vuu-datagrid/src"; +import { HeaderCellProps } from "@finos/vuu-table"; export interface CellConfigPanelProps extends HTMLAttributes { onConfigChange: () => void; diff --git a/vuu-ui/sample-apps/feature-basket-trading/src/VuuBasketTradingFeature.tsx b/vuu-ui/sample-apps/feature-basket-trading/src/VuuBasketTradingFeature.tsx index b677fd5a6..fce92cd13 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/VuuBasketTradingFeature.tsx +++ b/vuu-ui/sample-apps/feature-basket-trading/src/VuuBasketTradingFeature.tsx @@ -1,6 +1,5 @@ import { TableSchema } from "@finos/vuu-data"; import { FlexboxLayout, Stack } from "@finos/vuu-layout"; -import { ContextMenuProvider } from "@finos/vuu-popups"; import { BasketTableEdit } from "./basket-table-edit"; import { BasketTableLive } from "./basket-table-live"; import { BasketToolbar } from "./basket-toolbar"; @@ -18,7 +17,6 @@ export interface BasketTradingFeatureProps { basketSchema: TableSchema; basketTradingSchema: TableSchema; basketTradingConstituentJoinSchema: TableSchema; - basketConstituentSchema: TableSchema; } const VuuBasketTradingFeature = (props: BasketTradingFeatureProps) => { @@ -26,7 +24,6 @@ const VuuBasketTradingFeature = (props: BasketTradingFeatureProps) => { basketSchema, basketTradingSchema, basketTradingConstituentJoinSchema, - basketConstituentSchema, } = props; const { @@ -34,7 +31,6 @@ const VuuBasketTradingFeature = (props: BasketTradingFeatureProps) => { basketCount, basketDesignContextMenuConfig, basketSelectorProps, - contextMenuProps, dataSourceBasketTradingConstituentJoin, dialog, onClickAddBasket, @@ -46,7 +42,6 @@ const VuuBasketTradingFeature = (props: BasketTradingFeatureProps) => { basketSchema, basketTradingSchema, basketTradingConstituentJoinSchema, - basketConstituentSchema, }); if (basketCount === -1) { @@ -64,7 +59,7 @@ const VuuBasketTradingFeature = (props: BasketTradingFeatureProps) => { const activeTabIndex = basket?.status === "ON_MARKET" ? 1 : 0; return ( - + <> { style={{ flex: 1 }} > { {dialog} - + ); }; diff --git a/vuu-ui/sample-apps/feature-basket-trading/src/basket-table-edit/BasketTableEdit.tsx b/vuu-ui/sample-apps/feature-basket-trading/src/basket-table-edit/BasketTableEdit.tsx index 9f1f188dd..523168192 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/basket-table-edit/BasketTableEdit.tsx +++ b/vuu-ui/sample-apps/feature-basket-trading/src/basket-table-edit/BasketTableEdit.tsx @@ -7,11 +7,16 @@ import { } from "@finos/vuu-popups"; import { useMemo } from "react"; import columns from "./basketConstituentEditColumns"; +import { ColHeaderAddSymbol } from "../cell-renderers"; import "./BasketTableEdit.css"; const classBase = "vuuBasketTableEdit"; +if (typeof ColHeaderAddSymbol !== "function") { + console.warn("BasketTableEdit not all custom cell renderers are available"); +} + export interface BasketTableEditProps extends Omit { contextMenuConfig: ContextMenuConfiguration; tableSchema: TableSchema; diff --git a/vuu-ui/sample-apps/feature-basket-trading/src/basket-table-edit/basketConstituentEditColumns.ts b/vuu-ui/sample-apps/feature-basket-trading/src/basket-table-edit/basketConstituentEditColumns.ts index 702af2900..e107b636d 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/basket-table-edit/basketConstituentEditColumns.ts +++ b/vuu-ui/sample-apps/feature-basket-trading/src/basket-table-edit/basketConstituentEditColumns.ts @@ -27,7 +27,11 @@ export default [ }, width: 60, }, - { name: "ric", pin: "left" }, + { + name: "ric", + pin: "left", + colHeaderContentRenderer: "col-header-add-symbol", + }, { name: "description", label: "Name", width: 220 }, { name: "quantity" }, { name: "weighting", editable }, diff --git a/vuu-ui/sample-apps/feature-basket-trading/src/basket-table-live/BasketTableLive.tsx b/vuu-ui/sample-apps/feature-basket-trading/src/basket-table-live/BasketTableLive.tsx index feb0fd86f..dfac2f0cb 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/basket-table-live/BasketTableLive.tsx +++ b/vuu-ui/sample-apps/feature-basket-trading/src/basket-table-live/BasketTableLive.tsx @@ -10,7 +10,7 @@ if ( typeof SpreadCell !== "function" || typeof StatusCell !== "function" ) { - console.warn("BasketTableLive not all cusatom cell renderers are available"); + console.warn("BasketTableLive not all custom cell renderers are available"); } import "./BasketTableLive.css"; diff --git a/vuu-ui/sample-apps/feature-basket-trading/src/basket-toolbar/BasketMenu.tsx b/vuu-ui/sample-apps/feature-basket-trading/src/basket-toolbar/BasketMenu.tsx index 780a9eba7..a63ff956f 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/basket-toolbar/BasketMenu.tsx +++ b/vuu-ui/sample-apps/feature-basket-trading/src/basket-toolbar/BasketMenu.tsx @@ -12,6 +12,7 @@ import "./BasketMenu.css"; const classBase = "vuuBasketMenu"; export interface BasketMenuProps { + className?: string; location?: string; onMenuAction: MenuActionHandler; onMenuClose?: () => void; @@ -26,6 +27,7 @@ export interface BasketMenuProps { } export const BasketMenu = ({ + className, location, onMenuAction, onMenuClose, @@ -60,7 +62,7 @@ export const BasketMenu = ({ return ( ); - const statusIndicator = ( - + const readOnlyStatus = ( + + Status + ON MARKET + ); + const inputSide = ( Side @@ -112,9 +117,9 @@ export const BasketToolbar = ({ ); const readOnlySide = ( - - Units - {basket?.side ?? ""} + + Side + {basket?.side ?? ""} ); @@ -130,7 +135,7 @@ export const BasketToolbar = ({ ); const readOnlyUnits = ( - + Units {basket?.units ?? ""} @@ -154,13 +159,21 @@ export const BasketToolbar = ({ ); const pctFilled = ( - + % Filled - {basket?.filledPct ?? ""} + + {basket?.pctFilled ?? ""} + ); - const basketMenu = ; + const basketMenu = ( + + ); const sendToMarket = (