diff --git a/.gitignore b/.gitignore index 0f5a6d690..c8d0dab84 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ website/package-lock.json # Generated docs docs/contributing.md +vuu-ui/packages/vuu-data/src/inlined-worker.js diff --git a/docs/rpc/Viewport_rpc.md b/docs/rpc/Viewport_rpc.md new file mode 100644 index 000000000..bd769dcd5 --- /dev/null +++ b/docs/rpc/Viewport_rpc.md @@ -0,0 +1,11 @@ +import { SvgDottySeparator } from "@site/src/components/SvgDottySeparator"; + +# RPC Calls + + + +## Viewport RPC + +```scala +In Progress +``` \ No newline at end of file diff --git a/docs/rpc/rpc.md b/docs/rpc/rpc.md index 5e3f0b529..255e9c57a 100644 --- a/docs/rpc/rpc.md +++ b/docs/rpc/rpc.md @@ -4,7 +4,24 @@ import { SvgDottySeparator } from "@site/src/components/SvgDottySeparator"; -## Menus +## Overview of RPC + +There are two scopes where rpc services can be defined: + +- Global Scope - these are services that can be called without a viewport being created. +- Viewport Scope - these are services that are created when a user creates a viewport + +## Global Scope - RPC Services + +[RPC Services](service.md) allow us to expose server-side functionality to a Vuu client over a low-latency web-socket connection. + +The Vuu client framework can discover and programmatically call these services over the WebSocket. While there is no generic UI for invoking/inspecting REST services, many components (such as the Autocomplete Search) use services as an implementation mechanism. + +## Global Scope - REST Services + +REST Services allow us to expose server-side functionality to a Vuu client. Each service is modeled in REST-ful resource fashion, and can define the following standard verbs: `get_all`, `get`, `post`, `put`, `delete` + +## Viewport Scope - Menu Items [Menu Items](Menu_items.md) act upon a `table`, `selection`, `row` or `cell` (these are called `scope`). @@ -12,12 +29,9 @@ Once a `menu item` is registered by a server side [`provider`](../providers_tabl Menu items may have filter expressions (applied for each individual row) that determine for which rows they are visible. If a menu item is visible, it can be invoked. On invocation, depending on the `scope` the RPC handler will receive context information about what are we acting upon. -## RPC Services - -[RPC Services](service.md) allow us to expose server-side functionality to a Vuu client over a low-latency connection. - -The Vuu client framework can discover and programmatically call these services over the WebSocket. While there is no generic UI for invoking/inspecting REST services, many components (such as the Autocomplete Search) use REST services as an implementation mechanism. +## Viewport Scope - RPC Calls -## REST Services +[Viewport RPC](Viewport_rpc.md) calls are specific service methods that we want to call on a viewport we've created. They are specific i.e. the UI component needs +to understand the type of call that is being called. In that way they should be used in functionally specific UI components. -[REST Services](#) allow us to expose server-side functionality to a Vuu client. Each service is modeled in REST-ful resource fashion, and can define the following standard verbs: `get_all`, `get`, `post`, `put`, `delete` +They implicitly have access to the viewport and its associated tables that they are being called on. diff --git a/vuu-ui/.gitignore b/vuu-ui/.gitignore deleted file mode 100644 index c4a26c374..000000000 --- a/vuu-ui/.gitignore +++ /dev/null @@ -1 +0,0 @@ -inlined-worker.js \ No newline at end of file diff --git a/vuu-ui/package-lock.json b/vuu-ui/package-lock.json index a5edb95de..15c18686b 100644 --- a/vuu-ui/package-lock.json +++ b/vuu-ui/package-lock.json @@ -2986,10 +2986,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/app-vuu-basket-trader": { - "resolved": "sample-apps/app-vuu-basket-trader", - "link": true - }, "node_modules/app-vuu-example": { "resolved": "sample-apps/app-vuu-example", "link": true @@ -11929,6 +11925,7 @@ "version": "0.0.26", "license": "Apache-2.0", "dependencies": { + "@finos/vuu-filters": "0.0.26", "@finos/vuu-popups": "0.0.26", "@finos/vuu-table": "0.0.26", "@finos/vuu-table-extras": "0.0.26", @@ -12089,6 +12086,7 @@ }, "sample-apps/app-vuu-basket-trader": { "version": "0.0.26", + "extraneous": true, "license": "Apache-2.0", "dependencies": { "@finos/vuu-data": "0.0.26", @@ -12123,9 +12121,7 @@ "@finos/vuu-utils": "0.0.26", "@fontsource/open-sans": "^4.5.13", "@salt-ds/core": "1.8.0", - "@salt-ds/icons": "1.5.1", "@salt-ds/lab": "1.0.0-alpha.15", - "@salt-ds/theme": "1.7.1", "classnames": "^2.3.1", "react": "^17.0.2", "react-dom": "^17.0.2" @@ -13219,6 +13215,7 @@ "@finos/vuu-layout": { "version": "file:packages/vuu-layout", "requires": { + "@finos/vuu-filters": "0.0.26", "@finos/vuu-popups": "0.0.26", "@finos/vuu-table": "0.0.26", "@finos/vuu-table-extras": "0.0.26", @@ -14605,24 +14602,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true }, - "app-vuu-basket-trader": { - "version": "file:sample-apps/app-vuu-basket-trader", - "requires": { - "@finos/vuu-data": "0.0.26", - "@finos/vuu-data-react": "0.0.26", - "@finos/vuu-datagrid-types": "0.0.26", - "@finos/vuu-layout": "0.0.26", - "@finos/vuu-popups": "0.0.26", - "@finos/vuu-shell": "0.0.26", - "@finos/vuu-utils": "0.0.26", - "@fontsource/open-sans": "^4.5.13", - "@salt-ds/core": "1.8.0", - "@salt-ds/lab": "1.0.0-alpha.15", - "classnames": "^2.3.1", - "react": "^17.0.2", - "react-dom": "^17.0.2" - } - }, "app-vuu-example": { "version": "file:sample-apps/app-vuu-example", "requires": { @@ -14635,9 +14614,7 @@ "@finos/vuu-utils": "0.0.26", "@fontsource/open-sans": "^4.5.13", "@salt-ds/core": "1.8.0", - "@salt-ds/icons": "1.5.1", "@salt-ds/lab": "1.0.0-alpha.15", - "@salt-ds/theme": "1.7.1", "classnames": "^2.3.1", "react": "^17.0.2", "react-dom": "^17.0.2" diff --git a/vuu-ui/package.json b/vuu-ui/package.json index c471237d9..6511b5ef5 100644 --- a/vuu-ui/package.json +++ b/vuu-ui/package.json @@ -19,12 +19,10 @@ "format": "prettier --write './**/*.{js,,mjs,jsx,css,md,json}' --config ./.prettierrc", "build": "node ./scripts/build-all.mjs", "build:app": "cd sample-apps/app-vuu-example && node scripts/build.mjs", - "build:app:basket": "cd sample-apps/app-vuu-basket-trader && node scripts/build.mjs", "build:packages": "npm run build -- --cjs --license && npm run type-defs", "build:packages:debug": "npm run build -- --cjs --debug && npm run type-defs -- --debug", "build:salt": "node ./scripts/build-salt.mjs", "launch:demo": "node ./scripts/launch-app.mjs", - "launch:demo:basket": "node ./scripts/launch-app.mjs -- --basket", "launch:demo:electron": "cd tools/electron && node ./node_modules/.bin/electron .", "launch:showcase": " cd showcase && node scripts/launch.mjs", "deploy:websocket-test": "node ./tools/deploy-websocket-test.mjs", diff --git a/vuu-ui/packages/vuu-data-ag-grid/src/ViewportRowModelDataSource.ts b/vuu-ui/packages/vuu-data-ag-grid/src/ViewportRowModelDataSource.ts index e5c120594..3bb7f2c4a 100644 --- a/vuu-ui/packages/vuu-data-ag-grid/src/ViewportRowModelDataSource.ts +++ b/vuu-ui/packages/vuu-data-ag-grid/src/ViewportRowModelDataSource.ts @@ -1,10 +1,10 @@ import { DataSource, DataSourceConfig, + isVuuFeatureAction, SubscribeCallback, VuuFeatureMessage, } from "@finos/vuu-data"; -import { isVuuFeatureAction } from "@finos/vuu-data-react"; import { Filter } from "@finos/vuu-filter-types"; import { VuuGroupBy, VuuSort } from "@finos/vuu-protocol-types"; import { diff --git a/vuu-ui/packages/vuu-data-ag-grid/src/useViewportRowModel.ts b/vuu-ui/packages/vuu-data-ag-grid/src/useViewportRowModel.ts index e072af403..e793185d2 100644 --- a/vuu-ui/packages/vuu-data-ag-grid/src/useViewportRowModel.ts +++ b/vuu-ui/packages/vuu-data-ag-grid/src/useViewportRowModel.ts @@ -1,5 +1,7 @@ import { DataSourceConfig, + isViewportMenusAction, + isVisualLinksAction, MenuRpcResponse, RemoteDataSource, VuuFeatureMessage, @@ -8,8 +10,6 @@ import { } from "@finos/vuu-data"; import { - isViewportMenusAction, - isVisualLinksAction, MenuActionConfig, SuggestionFetcher, useTypeaheadSuggestions, diff --git a/vuu-ui/packages/vuu-data-react/src/hooks/index.ts b/vuu-ui/packages/vuu-data-react/src/hooks/index.ts index 14592a438..bc32ca880 100644 --- a/vuu-ui/packages/vuu-data-react/src/hooks/index.ts +++ b/vuu-ui/packages/vuu-data-react/src/hooks/index.ts @@ -1,4 +1,5 @@ export * from "./useDataSource"; +export * from "./useLookupValues"; export * from "./useServerConnectionStatus"; export * from "./useServerConnectionQuality"; export * from "./useTypeaheadSuggestions"; diff --git a/vuu-ui/packages/vuu-data-react/src/hooks/useLookupValues.ts b/vuu-ui/packages/vuu-data-react/src/hooks/useLookupValues.ts new file mode 100644 index 000000000..607e10c5e --- /dev/null +++ b/vuu-ui/packages/vuu-data-react/src/hooks/useLookupValues.ts @@ -0,0 +1,119 @@ +import { + ColumnDescriptor, + ListOption, + LookupTableDetails, +} from "@finos/vuu-datagrid-types"; +import { RemoteDataSource } from "@finos/vuu-data"; +import { useShellContext } from "@finos/vuu-shell"; +import { + buildColumnMap, + isLookupRenderer, + isTypeDescriptor, +} from "@finos/vuu-utils"; +import { useMemo, useState } from "react"; + +const NO_VALUES: ListOption[] = []; + +const lookupValueMap = new Map>(); + +const loadLookupValues = ({ + labelColumn, + table, + valueColumn, +}: LookupTableDetails): Promise => { + const tableKey = `${table.module}:${table.table}`; + const lookupValues = lookupValueMap.get(tableKey); + if (lookupValues) { + return lookupValues; + } else { + const promise: Promise = new Promise((resolve) => { + const columns = [valueColumn, labelColumn]; + const columnMap = buildColumnMap(columns); + const dataSource = new RemoteDataSource({ + bufferSize: 0, + table, + }); + dataSource.subscribe( + { + columns, + range: { from: 0, to: 100 }, + }, + (message) => { + if (message.type === "viewport-update") { + //TODO check we have full dataset + if (message.rows) { + const listOptions = message.rows.map((row) => ({ + value: row[columnMap[valueColumn]] as string | number, + label: row[columnMap[labelColumn]] as string, + })); + resolve(listOptions); + dataSource.unsubscribe(); + } + } + } + ); + }); + lookupValueMap.set(tableKey, promise); + return promise; + } +}; + +type LookupState = { + initialValue: ListOption | null; + values: ListOption[]; +}; + +const getSelectedOption = ( + values: ListOption[], + selectedValue: string | number | undefined +) => { + if (selectedValue === undefined) { + return null; + } + return values.find((option) => option.value === selectedValue) ?? null; +}; + +const getLookupDetails = ({ name, type }: ColumnDescriptor) => { + if (isTypeDescriptor(type) && isLookupRenderer(type.renderer)) { + return type.renderer.lookup; + } else { + throw Error( + `useLookupValues column ${name} is not configured to use lookup values` + ); + } +}; + +export const useLookupValues = ( + column: ColumnDescriptor, + initialValueProp: number | string +) => { + const lookupDetails = getLookupDetails(column); + const { getLookupValues } = useShellContext(); + + const initialState = useMemo(() => { + const values = getLookupValues?.(lookupDetails.table) ?? NO_VALUES; + return { + initialValue: getSelectedOption(values, initialValueProp), + values, + }; + }, [getLookupValues, initialValueProp, lookupDetails.table]); + + const [{ initialValue, values }, setLookupState] = + useState(initialState); + + useMemo(() => { + if (values === NO_VALUES) { + loadLookupValues(lookupDetails).then((values) => + setLookupState({ + initialValue: getSelectedOption(values, initialValueProp), + values, + }) + ); + } + }, [values, lookupDetails, initialValueProp]); + + return { + initialValue, + values, + }; +}; diff --git a/vuu-ui/packages/vuu-data-react/src/hooks/useVuuMenuActions.ts b/vuu-ui/packages/vuu-data-react/src/hooks/useVuuMenuActions.ts index 3780574a6..23b750484 100644 --- a/vuu-ui/packages/vuu-data-react/src/hooks/useVuuMenuActions.ts +++ b/vuu-ui/packages/vuu-data-react/src/hooks/useVuuMenuActions.ts @@ -53,34 +53,6 @@ const { KEY } = metadataKeys; const NO_CONFIG: MenuActionConfig = {}; -export const isVisualLinksAction = ( - action: GridAction -): action is DataSourceVisualLinksMessage => action.type === "vuu-links"; - -export const isVisualLinkCreatedAction = ( - action: GridAction -): action is DataSourceVisualLinkCreatedMessage => - action.type === "vuu-link-created"; - -export const isVisualLinkRemovedAction = ( - action: GridAction -): action is DataSourceVisualLinkRemovedMessage => - action.type === "vuu-link-removed"; - -export const isViewportMenusAction = ( - action: GridAction -): action is DataSourceMenusMessage => action.type === "vuu-menu"; - -export const isVuuFeatureAction = ( - action: GridAction -): action is VuuFeatureMessage => - isViewportMenusAction(action) || isVisualLinksAction(action); - -export const isVuuFeatureInvocation = ( - action: GridAction -): action is VuuFeatureInvocationMessage => - action.type === "vuu-link-created" || action.type === "vuu-link-removed"; - const isMenuItem = (menu: VuuMenuItem | VuuMenu): menu is VuuMenuItem => "rpcName" in menu; @@ -318,11 +290,12 @@ export const useVuuMenuActions = ({ }: VuuMenuActionHookProps): ViewServerHookResult => { const buildViewserverMenuOptions: MenuBuilder = useCallback( (location, options) => { - const { visualLink, visualLinks, vuuMenu } = menuActionConfig; + const { links, menu } = dataSource; + const { visualLink } = menuActionConfig; const descriptors: ContextMenuItemDescriptor[] = []; - if (location === "grid" && visualLinks && !visualLink) { - visualLinks.forEach((linkDescriptor: LinkDescriptorWithLabel) => { + if (location === "grid" && links && !visualLink) { + links.forEach((linkDescriptor: LinkDescriptorWithLabel) => { const { link, label: linkLabel } = linkDescriptor; const label = linkLabel ? linkLabel : link.toTable; descriptors.push({ @@ -333,13 +306,13 @@ export const useVuuMenuActions = ({ }); } - if (vuuMenu && isTableLocation(location)) { + if (menu && isTableLocation(location)) { const menuDescriptor = buildMenuDescriptor( - vuuMenu, + menu, location, options as VuuServerMenuOptions ); - if (isRoot(vuuMenu) && isGroupMenuItemDescriptor(menuDescriptor)) { + if (isRoot(menu) && isGroupMenuItemDescriptor(menuDescriptor)) { descriptors.push(...menuDescriptor.children); } else if (menuDescriptor) { descriptors.push(menuDescriptor); @@ -348,7 +321,7 @@ export const useVuuMenuActions = ({ return descriptors; }, - [menuActionConfig] + [dataSource, menuActionConfig] ); const handleMenuAction = useCallback( 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 f7c0b7213..854091c6c 100644 --- a/vuu-ui/packages/vuu-data-react/src/hooks/useVuuTables.ts +++ b/vuu-ui/packages/vuu-data-react/src/hooks/useVuuTables.ts @@ -18,9 +18,7 @@ export const useVuuTables = () => { const { tables } = await server.getTableList(); const tableSchemas = buildTables( await Promise.all( - tables.map((tableDescriptor) => - server.getTableSchema(tableDescriptor) - ) + tables.map((vuuTable) => server.getTableSchema(vuuTable)) ) ); setTables(tableSchemas); diff --git a/vuu-ui/packages/vuu-data-test/src/Table.ts b/vuu-ui/packages/vuu-data-test/src/Table.ts new file mode 100644 index 000000000..0c76ffdcd --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/Table.ts @@ -0,0 +1,27 @@ +import { TableSchema } from "@finos/vuu-data"; +import { VuuRowDataItemType } from "@finos/vuu-protocol-types"; +import { EventEmitter } from "@finos/vuu-utils"; + +export type TableEvents = { + delete: (row: VuuRowDataItemType[]) => void; + insert: (row: VuuRowDataItemType[]) => void; +}; + +export class Table extends EventEmitter { + #data: VuuRowDataItemType[][]; + #schema: TableSchema; + constructor(schema: TableSchema, data: VuuRowDataItemType[][]) { + super(); + this.#data = data; + this.#schema = schema; + } + + get data() { + return this.#data; + } + + insert(row: VuuRowDataItemType[]) { + this.#data.push(row); + this.emit("insert", row); + } +} diff --git a/vuu-ui/packages/vuu-data-test/src/TickingArrayDataSource.ts b/vuu-ui/packages/vuu-data-test/src/TickingArrayDataSource.ts new file mode 100644 index 000000000..efec2dc88 --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/TickingArrayDataSource.ts @@ -0,0 +1,172 @@ +import { + ArrayDataSource, + ArrayDataSourceConstructorProps, + MenuRpcResponse, + SubscribeCallback, + SubscribeProps, + VuuUIMessageInRPCEditReject, + VuuUIMessageInRPCEditResponse, +} from "@finos/vuu-data"; +import { + UpdateGenerator, + UpdateHandler, +} from "@finos/vuu-data-test/src/rowUpdates"; +import { DataSourceRow } from "@finos/vuu-data-types"; +import { + ClientToServerEditRpc, + ClientToServerMenuRPC, + VuuMenu, + VuuRange, + VuuRowDataItemType, +} from "@finos/vuu-protocol-types"; +import { Table } from "./Table"; + +export type RpcService = { + rpcName: string; + service: (rpcRequest: any) => Promise; +}; + +export interface TickingArrayDataSourceConstructorProps + extends Omit { + data?: Array; + menu?: VuuMenu; + rpcServices?: RpcService[]; + table?: Table; + updateGenerator?: UpdateGenerator; +} + +export class TickingArrayDataSource extends ArrayDataSource { + #rpcServices: RpcService[] | undefined; + #updateGenerator: UpdateGenerator | undefined; + constructor({ + data, + rpcServices, + table, + updateGenerator, + menu, + ...arrayDataSourceProps + }: TickingArrayDataSourceConstructorProps) { + if (data === undefined && table === undefined) { + throw Error("TickingArrayDataSource must be constructed with data"); + } + super({ + ...arrayDataSourceProps, + data: data ?? table?.data ?? [], + }); + this._menu = menu; + this.#rpcServices = rpcServices; + this.#updateGenerator = updateGenerator; + updateGenerator?.setDataSource(this); + updateGenerator?.setUpdateHandler(this.processUpdates); + + if (table) { + table.on("insert", this.insert); + } + } + + async subscribe(subscribeProps: SubscribeProps, callback: SubscribeCallback) { + const subscription = super.subscribe(subscribeProps, callback); + if (subscribeProps.range) { + this.#updateGenerator?.setRange(subscribeProps.range); + } + return subscription; + } + + set range(range: VuuRange) { + super.range = 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].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)) { + selected.forEach((sel) => { + const row = this.data[sel]; + if (row) { + rows.push(row); + } + }); + } else { + const row = this.data[selected]; + if (row) { + rows.push(row); + } + } + return rows; + }, []); + } + + async menuRpcCall( + rpcRequest: Omit | ClientToServerEditRpc + ): Promise< + | MenuRpcResponse + | VuuUIMessageInRPCEditReject + | VuuUIMessageInRPCEditResponse + | undefined + > { + const rpcService = this.#rpcServices?.find( + (service) => + service.rpcName === (rpcRequest as ClientToServerMenuRPC).rpcName + ); + + if (rpcService) { + switch (rpcRequest.type) { + case "VIEW_PORT_MENUS_SELECT_RPC": { + const selectedRows = this.getSelectedRows(); + return rpcService.service({ + ...rpcRequest, + selectedRows, + }); + } + + default: + } + } + return super.menuRpcCall(rpcRequest); + } +} diff --git a/vuu-ui/packages/vuu-data-test/src/UpdateGenerator.ts b/vuu-ui/packages/vuu-data-test/src/UpdateGenerator.ts index f76e03023..31156575d 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 { RowUpdates, UpdateGenerator, UpdateHandler } from "./rowUpdates"; +import type { RowUpdates, UpdateGenerator, UpdateHandler } from "./rowUpdates"; const getNewValue = (value: number) => { const multiplier = random(0, 100) / 1000; @@ -63,7 +63,7 @@ export class BaseUpdateGenerator implements UpdateGenerator { if (shallUpdateRow) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - const rowUpdates: RowUpdates = [rowIndex]; + const rowUpdates: RowUpdates = ["U", rowIndex]; const row = data[rowIndex]; for (const colIdx of this.tickingColumns) { const shallUpdateColumn = random(0, 10) < 5; diff --git a/vuu-ui/packages/vuu-data-test/src/basket/basket-module.ts b/vuu-ui/packages/vuu-data-test/src/basket/basket-module.ts new file mode 100644 index 000000000..6feec1009 --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/basket/basket-module.ts @@ -0,0 +1,292 @@ +import { VuuModule } from "../vuu-modules"; +import { ColumnMap, metadataKeys } from "@finos/vuu-utils"; +import { BasketsTableName } from "./basket-schemas"; +import { TickingArrayDataSource } from "../TickingArrayDataSource"; +import { schemas } from "./basket-schemas"; +import ftse from "./reference-data/ftse100"; +import nasdaq from "./reference-data/nasdaq100"; +import sp500 from "./reference-data/sp500"; +import hsi from "./reference-data/hsi"; +import { VuuMenu } from "@finos/vuu-protocol-types"; +import { Table } from "../Table"; + +// This is a 'local' columnMap +const buildDataColumnMap = (tableName: BasketsTableName) => + Object.values(schemas[tableName].columns).reduce( + (map, col, index) => { + map[col.name] = index; + return map; + }, + {} + ); + +//--------------- +// export const BasketColumnMap = buildColumnMap("basket"); + +const { KEY } = metadataKeys; + +/** + * BasketConstituent + */ + +const basketConstituentData = []; +for (const row of ftse) { + // prettier-ignore + const [ric, name, lastTrade, change, volume] = row; + const basketId = ".FTSE100"; + const side = "BUY"; + const weighting = 1; + // prettier-ignore + basketConstituentData.push([ basketId, change, name, lastTrade, ric, `${ric}-${basketId}`, side, volume, weighting ]); +} + +for (const row of hsi) { + // prettier-ignore + const [name, ric, lastTrade, change, , volume] = row; + const basketId = ".HSI"; + const side = "BUY"; + const weighting = 1; + // prettier-ignore + basketConstituentData.push([basketId,change,name, lastTrade,ric,`${ric}-${basketId}`,side,volume,weighting ]); +} + +for (const row of nasdaq) { + // prettier-ignore + const [name, ric, weighting, lastTrade, change] = row; + const basketId = ".NASDAQ100"; + const side = "BUY"; + const volume = 1000; + // prettier-ignore + basketConstituentData.push([ basketId, change, name, lastTrade, ric, `${ric}-${basketId}`, side, volume, weighting ]); +} + +for (const row of sp500) { + // prettier-ignore + const [name, ric, weighting,,change] = row; + const basketId = ".SP500"; + const side = "BUY"; + const volume = 1000; + const lastTrade = 0; + // prettier-ignore + basketConstituentData.push([ basketId, change, name, lastTrade, ric, `${ric}-${basketId}`, side, volume, weighting ]); +} + +const basketConstituent = new Table( + schemas.basketConstituent, + basketConstituentData +); + +/** + * BasketTrading + */ +const basketTrading = new Table(schemas.basketTrading, []); + +let basketIncrement = 1; +/** + * BasketTradingConstituent + */ +const basketTradingConstituent = new Table( + schemas.basketTradingConstituent, + [] +); +const basketTradingConstituentJoin = new Table( + schemas.basketTradingConstituentJoin, + [] +); + +function createTradingBasket(basketId: string, basketName: string) { + const instanceId = `steve-${basketIncrement++}`; + const basketTradingRow = [ + basketId, + basketName, + 0, + 1.25, + instanceId, + "OFF MARKET", + 1_000_000, + 1_250_000, + 100, + ]; + + basketTrading.insert(basketTradingRow); + + const { basketId: key } = buildDataColumnMap("basketConstituent"); + const constituents = basketConstituent.data.filter( + (c) => c[key] === basketId + ); + + constituents.forEach(([, , description, , ric, , , quantity, weighting]) => { + const algo = undefined; + const algoParams = ""; + const limitPrice = 95; + const notionalLocal = 0; + const notionalUsd = 0; + const pctFilled = 0; + const priceSpread = 0; + const priceStrategyId = undefined; + const side = "buy"; + const venue = "venue"; + + const basketTradingConstituentRow = [ + algo, + algoParams, + basketId, + description, + instanceId, + `${instanceId}-${ric}`, + limitPrice, + notionalLocal, + notionalUsd, + pctFilled, + priceSpread, + priceStrategyId, + quantity, + ric, + side, + venue, + weighting, + ]; + basketTradingConstituent.insert(basketTradingConstituentRow); + + const ask = 0; + const askSize = 0; + const bid = 0; + const bidSize = 0; + const close = 0; + const last = 0; + const open = 0; + const phase = "market"; + const scenario = "scenario"; + + const basketTradingConstituentJoinRow = [ + algo, + algoParams, + ask, + askSize, + basketId, + bid, + bidSize, + close, + description, + instanceId, + `${instanceId}-${ric}`, + last, + limitPrice, + notionalLocal, + notionalUsd, + open, + pctFilled, + phase, + priceSpread, + priceStrategyId, + quantity, + ric, + scenario, + side, + venue, + weighting, + ]; + basketTradingConstituentJoin.insert(basketTradingConstituentJoinRow); + }); +} + +async function createNewBasket(rpcRequest: any) { + const { basketName, selectedRows } = rpcRequest; + if (selectedRows.length === 1) { + const [row] = selectedRows; + const basketId = row[KEY]; + createTradingBasket(basketId, basketName); + } +} + +//------------------- + +const tables: Record = { + algoType: new Table(schemas.algoType, [ + ["Sniper", 0], + ["Dark Liquidity", 1], + ["VWAP", 2], + ["POV", 3], + ["Dynamic Close", 4], + ]), + basket: new Table(schemas.basket, [ + [".NASDAQ100", ".NASDAQ100", 0, 0], + [".HSI", ".HSI", 0, 0], + [".FTSE100", ".FTSE100", 0, 0], + [".SP500", ".SP500", 0, 0], + ]), + basketConstituent, + basketTrading, + basketTradingConstituent, + basketTradingConstituentJoin, + priceStrategyType: new Table(schemas.priceStrategyType, [ + ["Peg to Near Touch", 0], + ["Far Touch", 1], + ["Limit", 2], + ["Algo", 3], + ]), +}; + +const menus: Record = { + algoType: undefined, + basket: { + name: "ROOT", + menus: [ + { + context: "selected-rows", + filter: "", + name: "Add Basket", + rpcName: "CREATE_NEW_BASKET", + }, + ], + }, + basketConstituent: undefined, + basketTrading: undefined, + basketTradingConstituent: undefined, + basketTradingConstituentJoin: undefined, + priceStrategyType: undefined, +}; + +type RpcService = { + rpcName: string; + service: (rpcRequest: any) => Promise; +}; + +const services: Record = { + algoType: undefined, + basket: [ + { + rpcName: "CREATE_NEW_BASKET", + service: createNewBasket, + }, + ], + basketConstituent: undefined, + basketTrading: undefined, + basketTradingConstituent: undefined, + basketTradingConstituentJoin: undefined, + priceStrategyType: undefined, +}; + +const getColumnDescriptors = (tableName: BasketsTableName) => { + const schema = schemas[tableName]; + return schema.columns; +}; + +const createDataSource = (tableName: BasketsTableName) => { + const columnDescriptors = getColumnDescriptors(tableName); + const { key } = schemas[tableName]; + return new TickingArrayDataSource({ + columnDescriptors, + keyColumn: key, + table: tables[tableName], + menu: menus[tableName], + rpcServices: services[tableName], + // updateGenerator: createUpdateGenerator?.(), + }); +}; + +const basketModule: VuuModule = { + createDataSource, +}; + +export default basketModule; diff --git a/vuu-ui/packages/vuu-data-test/src/basket/basket-schemas.ts b/vuu-ui/packages/vuu-data-test/src/basket/basket-schemas.ts index aa1edde74..6141d59d6 100644 --- a/vuu-ui/packages/vuu-data-test/src/basket/basket-schemas.ts +++ b/vuu-ui/packages/vuu-data-test/src/basket/basket-schemas.ts @@ -6,7 +6,7 @@ export type BasketsTableName = | "basketConstituent" | "basketTrading" | "basketTradingConstituent" - | "basketTrdConsPrices" + | "basketTradingConstituentJoin" | "priceStrategyType"; export const schemas: Readonly< @@ -34,6 +34,8 @@ export const schemas: Readonly< columns: [ { name: "basketId", serverDataType: "string" }, { name: "change", serverDataType: "string" }, + // this column doesn't exist on Vuu server + { name: "description", serverDataType: "string" }, { name: "lastTrade", serverDataType: "string" }, { name: "ric", serverDataType: "string" }, { name: "ricBasketId", serverDataType: "string" }, @@ -64,7 +66,6 @@ export const schemas: Readonly< { name: "algo", serverDataType: "string" }, { name: "algoParams", serverDataType: "string" }, { name: "basketId", serverDataType: "string" }, - { name: "bid", serverDataType: "double" }, { name: "description", serverDataType: "string" }, { name: "instanceId", serverDataType: "string" }, { name: "instanceIdRic", serverDataType: "string" }, @@ -83,7 +84,8 @@ export const schemas: Readonly< key: "instanceIdRic", table: { module: "BASKET", table: "basketTradingConstituent" }, }, - basketTrdConsPrices: { + + basketTradingConstituentJoin: { columns: [ { name: "algo", serverDataType: "string" }, { name: "algoParams", serverDataType: "string" }, @@ -107,12 +109,13 @@ export const schemas: Readonly< { name: "priceStrategyId", serverDataType: "int" }, { name: "quantity", serverDataType: "long" }, { name: "ric", serverDataType: "string" }, + { name: "scenario", serverDataType: "string" }, { name: "side", serverDataType: "string" }, { name: "venue", serverDataType: "string" }, { name: "weighting", serverDataType: "double" }, ], key: "instanceIdRic", - table: { module: "BASKET", table: "basketTradingConstituent" }, + table: { module: "BASKET", table: "basketTradingConstituentJoin" }, }, priceStrategyType: { columns: [ diff --git a/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basket-generator.ts b/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basket-generator.ts index 7de4feae6..34aa03b1b 100644 --- a/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basket-generator.ts +++ b/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basket-generator.ts @@ -1,10 +1,10 @@ import { BasketColumnMap, BasketReferenceData } from "../reference-data"; import { getGenerators } from "../../generatorTemplate"; -const [RowGenerator, ColumnGenerator] = getGenerators( +const [rowGenerator] = getGenerators( "basket", BasketColumnMap, BasketReferenceData ); -export { RowGenerator, ColumnGenerator }; +export default rowGenerator; diff --git a/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketConstituent-generator.ts b/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketConstituent-generator.ts index 9db313bcf..279313978 100644 --- a/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketConstituent-generator.ts +++ b/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketConstituent-generator.ts @@ -4,10 +4,10 @@ import { } from "../reference-data"; import { getGenerators } from "../../generatorTemplate"; -const [RowGenerator, ColumnGenerator] = getGenerators( +const [rowGenerator] = getGenerators( "basketConstituent", BasketConstituentColumnMap, BasketConstituentReferenceData ); -export { RowGenerator, ColumnGenerator }; +export default rowGenerator; diff --git a/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketTrading-generator.ts b/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketTrading-generator.ts index 2fe2e2ed8..cbd01998e 100644 --- a/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketTrading-generator.ts +++ b/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketTrading-generator.ts @@ -4,10 +4,10 @@ import { } from "../reference-data"; import { getGenerators } from "../../generatorTemplate"; -const [RowGenerator, ColumnGenerator] = getGenerators( +const [rowGenerator] = getGenerators( "basketTrading", BasketTradingColumnMap, BasketTradingReferenceData ); -export { RowGenerator, ColumnGenerator }; +export default rowGenerator; diff --git a/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketTradingConstituent-generator.ts b/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketTradingConstituent-generator.ts index 90fd547c2..3c8391fd3 100644 --- a/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketTradingConstituent-generator.ts +++ b/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketTradingConstituent-generator.ts @@ -4,10 +4,10 @@ import { } from "../reference-data"; import { getGenerators } from "../../generatorTemplate"; -const [RowGenerator, ColumnGenerator] = getGenerators( +const [rowGenerator] = getGenerators( "basketTradingConstituent", BasketTradingConstituentColumnMap, BasketTradingConstituentReferenceData ); -export { RowGenerator, ColumnGenerator }; +export default rowGenerator; diff --git a/vuu-ui/packages/vuu-data-test/src/basket/data-generators/index.ts b/vuu-ui/packages/vuu-data-test/src/basket/data-generators/index.ts index 7c1a90d42..fb63f576d 100644 --- a/vuu-ui/packages/vuu-data-test/src/basket/data-generators/index.ts +++ b/vuu-ui/packages/vuu-data-test/src/basket/data-generators/index.ts @@ -1,4 +1,15 @@ -export * as basket from "./basket-generator"; -export * as basketConstituent from "./basketConstituent-generator"; -export * as basketTrading from "./basketTrading-generator"; -export * as basketTradingConstituent from "./basketTradingConstituent-generator"; +import { RowGeneratorFactory } from "../.."; +import { BasketsTableName } from "../basket-schemas"; +import basketGenerators from "./basket-generator"; +import basketConstituentGenerators from "./basketConstituent-generator"; +import basketTradingGenerators from "./basketTrading-generator"; +import basketTradingConstituentGenerators from "./basketTradingConstituent-generator"; + +const generators: Record = { + basket: basketGenerators, + basketConstituent: basketConstituentGenerators, + basketTrading: basketTradingGenerators, + basketTradingConstituent: basketTradingConstituentGenerators, +}; + +export default generators; diff --git a/vuu-ui/packages/vuu-data-test/src/basket/reference-data/basketTrading.ts b/vuu-ui/packages/vuu-data-test/src/basket/reference-data/basketTrading.ts index 165531d04..a3a14b0bf 100644 --- a/vuu-ui/packages/vuu-data-test/src/basket/reference-data/basketTrading.ts +++ b/vuu-ui/packages/vuu-data-test/src/basket/reference-data/basketTrading.ts @@ -35,11 +35,11 @@ const createBasket = (basketId: string, basketName: string) => { data.push(basketTradingRow); }; -createBasket(".FTSE", "Steve FTSE 1"); -createBasket(".FTSE", "Steve FTSE 2"); -createBasket(".FTSE", "Steve FTSE 3"); -createBasket(".FTSE", "Steve FTSE 4"); -createBasket(".FTSE", "Steve FTSE 5"); -createBasket(".FTSE", "Steve FTSE 6"); +// createBasket(".FTSE", "Steve FTSE 1"); +// createBasket(".FTSE", "Steve FTSE 2"); +// createBasket(".FTSE", "Steve FTSE 3"); +// createBasket(".FTSE", "Steve FTSE 4"); +// createBasket(".FTSE", "Steve FTSE 5"); +// createBasket(".FTSE", "Steve FTSE 6"); export default data; diff --git a/vuu-ui/packages/vuu-data-test/src/basket/reference-data/hsi.ts b/vuu-ui/packages/vuu-data-test/src/basket/reference-data/hsi.ts new file mode 100644 index 000000000..a059a1d92 --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/basket/reference-data/hsi.ts @@ -0,0 +1,85 @@ +// Name,Symbol,Last,Change,Change %",Volume,Turn.,P/E,P/B",Yield,Market Cap + +// prettier-ignore +export default [ + ["CKH HOLDINGS","00001.HK",41.900,+1.150,"+2.822%","5.15M","215.13M",4.38,0.31,"6.98%","160.48B"], + ["CLP HOLDINGS","00002.HK",57.950,-0.100,"-0.172%","3.32M","193.19M",156.62,1.39,"5.35%","146.41B"], + ["HK & CHINA GAS","00003.HK",5.460,+0.130,"+2.439%","16.16M","88.00M",19.43,1.66,"6.41%","101.88B"], + ["HSBC HOLDINGS,00005.HK",61.700,+1.100,"+1.815%","22.19M","1.37B",10.54,0.86,"4.05%",1,"227.60B"], + ["POWER ASSETS","00006.HK",37.900,-0.250,"-0.655%","4.23M","160.75M",14.30,0.93,"7.44%","80.77B"], + ["HANG SENG BANK","00011.HK",97.450,+2.950,"+3.122%","2.54M","247.44M",19.69,1.01,"4.21%","186.31B"], + ["HENDERSON LAND","00012.HK",20.650,+0.850,"+4.293%","5.07M","103.70M",10.81,0.31,"8.72%","99.97B"], + ["SHK PPT","00016.HK",83.800,+3.250,"+4.035%","8.25M","685.41M",10.16,0.40,"5.91%","242.83B"], + ["NEW WORLD DEV","00017.HK",15.240,+0.800,"+5.540%","12.95M","196.19M",30.48,0.18,"13.52%","38.35B"], + ["GALAXY ENT","00027.HK",47.150,+1.900,"+4.199%","11.97M","560.21M","No Profit",3.22,"0.00%","206.21B"], + ["MTR CORPORATION","00066.HK",31.000,+0.800,"+2.649%","4.68M","144.86M",19.50,1.07,"4.23%","192.64B"], + ["HANG LUNG PPT","00101.HK",10.720,+0.560,"+5.512%","7.68M","81.99M",12.61,0.36,"7.28%","48.23B"], + ["GEELY AUTO","00175.HK",9.240,+0.170,"+1.874%","18.77M","173.30M",16.06,1.15,"2.27%","92.99B"], + ["ALI HEALTH","00241.HK",4.880,+0.470,"+10.658%","53.62M","257.82M",108.09,3.81,"0.00%","66.04B"], + ["CITIC","00267.HK",7.200,+0.200,"+2.857%","11.54M","82.96M",2.78,0.28,"9.04%","209.45B"], + ["WH GROUP","00288.HK",4.110,+0.080,"+1.985%","18.94M","77.65M",4.93,0.70,"7.30%","52.73B"], + ["CHINA RES BEER","00291.HK",42.950,+1.200,"+2.874%","4.83M","207.98M",28.41,4.57,"1.41%","139.34B"], + ["OOIL","00316.HK",104.500,-0.400,"-0.381%","296.54K","30.98M",0.89,0.66,"78.90%","69.01B"], + ["TINGYI","00322.HK",10.940,+0.180,"+1.673%","2.91M","31.81M",20.75,4.09,"9.39%","61.64B"], + ["SINOPEC CORP","00386.HK",4.280,-0.010,"-0.233%","92.43M","396.61M",6.94,0.58,"9.36%","105.92B"], + ["HKEX","00388.HK",292.600,+9.000,"+3.173%","4.61M","1.34B",36.76,7.46,"2.44%","370.97B"], + ["TECHTRONIC IND","00669.HK",76.000,+1.350,"+1.808%","5.47M","414.78M",16.54,3.43,"2.43%","139.45B"], + ["CHINA OVERSEAS","00688.HK",16.240,+0.520,"+3.308%","13.15M","212.60M",6.76,0.44,"4.93%","177.74B"], + ["TENCENT","00700.HK",306.200,+8.800,"+2.959%","11.25M","3.43B",13.74,3.60,"0.78%",2,"926.16B"], + ["CHINA UNICOM","00762.HK",5.680,-0.050,"-0.873%","9.23M","52.77M",9.16,0.45,"5.44%","173.80B"], + ["LINK REIT","00823.HK",38.400,+1.450,"+3.924%","12.04M","460.96M",5.42,0.52,"7.03%","98.38B"], + ["CHINA RES POWER","00836.HK",14.940,+0.320,"+2.189%","9.91M","147.81M",10.23,0.87,"3.92%","71.87B"], + ["PETROCHINA","00857.HK",5.900,0.000,"0.000%","64.23M","380.44M",6.38,0.70,"8.08%","124.48B"], + ["XINYI GLASS","00868.HK",10.140,+0.170,"+1.705%","3.75M","38.02M",8.01,1.30,"6.11%","42.22B"], + ["ZHONGSHENG HLDG","00881.HK",22.050,+1.450,"+7.039%","6.76M","147.71M",7.08,1.07,"4.94%","52.72B"], + ["CNOOC","00883.HK",13.780,-0.020,"-0.145%","26.37M","365.08M",4.03,0.97,"19.09%","655.47B"], + ["CCB","00939.HK",4.420,+0.060,"+1.376%","233.61M","1.03B",3.06,0.35,"9.93%",1,"062.64B"], + ["CHINA MOBILE","00941.HK",65.700,+0.250,"+0.382%","7.82M","516.10M",9.91,0.96,"6.71%",1,"404.46B"], + ["LONGFOR GROUP","00960.HK",14.080,+0.900,"+6.829%","10.88M","150.68M",3.06,0.55,"9.05%","92.81B"], + ["XINYI SOLAR","00968.HK",5.860,+0.280,"+5.018%","15.99M","92.88M",13.64,1.75,"3.41%","52.17B"], + ["SMIC","00981.HK",20.050,+0.250,"+1.263%","13.10M","264.32M",11.17,1.06,"0.00%","159.31B"], + ["LENOVO GROUP","00992.HK",8.070,+0.250,"+3.197%","33.60M","270.50M",7.61,2.23,"4.71%","97.87B"], + ["CKI HOLDINGS","01038.HK",37.050,-0.200,"-0.537%","1.44M","53.73M",12.03,0.78,"6.83%","93.35B"], + ["HENGAN INT'L","01044.HK",24.950,+0.200,"+0.808%","2.62M","64.97M",13.35,1.32,"6.33%","28.99B"], + ["CHINA SHENHUA","01088.HK",25.400,+0.350,"+1.397%","7.98M","203.11M",6.14,1.13,"11.33%","85.79B"], + ["CSPC PHARMA","01093.HK",5.740,+0.050,"+0.879%","29.29M","167.41M",9.96,2.01,"3.66%","68.32B"], + ["SINOPHARM","01099.HK",22.700,+0.200,"+0.889%","2.07M","46.80M",7.37,0.92,"4.08%","30.46B"], + ["CHINA RES LAND","01109.HK",31.200,+1.000,"+3.311%","9.88M","305.88M",7.02,0.81,"5.07%","222.49B"], + ["CK ASSET","01113.HK",41.250,+1.250,"+3.125%","8.57M","352.44M",6.90,0.39,"5.53%","146.47B"], + ["SINO BIOPHARM","01177.HK",2.830,+0.020,"+0.712%","26.30M","74.09M",18.37,1.59,"4.24%","53.21B"], + ["CHINA RES MIXC","01209.HK",31.600,+1.600,"+5.333%","3.07M","96.42M",28.97,4.48,"2.87%","72.13B"], + ["BYD COMPANY","01211.HK",242.000,+7.000,"+2.979%","3.90M","938.50M",37.57,5.63,"0.53%","265.72B"], + ["AIA","01299.HK",63.850,+0.950,"+1.510%","38.75M","2.48B",408.88,2.53,"2.41%","735.40B"], + ["CHINAHONGQIAO","01378.HK",7.670,+0.280,"+3.789%","13.05M","99.58M",7.27,0.76,"6.65%","72.68B"], + ["ICBC","01398.HK",3.770,+0.070,"+1.892%","233.48M","881.67M",3.45,0.36,"9.08%","327.21B"], + ["XIAOMI-W","01810.HK",12.340,+0.620,"+5.290%","78.53M","960.98M",109.39,1.90,"0.00%","308.83B"], + ["BUD APAC","01876.HK",15.460,+0.160,"+1.046%","16.17M","250.45M",28.65,2.44,"1.91%","204.74B"], + ["SANDS CHINA LTD","01928.HK",24.000,+0.950,"+4.121%","19.98M","475.93M","No Profit","N/A","0.00%","194.24B"], + ["CHOW TAI FOOK","01929.HK",11.800,+0.280,"+2.431%","4.88M","57.68M",21.93,3.64,"10.34%","118.00B"], + ["WHARF REIC","01997.HK",30.250,+1.400,"+4.853%","4.23M","127.31M","No Profit",0.48,"4.33%","91.85B"], + ["ANTA SPORTS","02020.HK",88.150,+4.400,"+5.254%","4.82M","422.51M",27.71,6.16,"1.52%","249.70B"], + ["WUXI BIO","02269.HK",45.650,+1.550,"+3.515%","17.91M","811.49M",38.18,4.88,"0.00%","194.01B"], + ["SHENZHOU INTL","02313.HK",75.000,+4.600,"+6.534%","5.52M","410.68M",21.87,3.25,"2.55%","112.74B"], + ["PING AN","02318.HK",44.850,+0.850,"+1.932%","23.05M","1.03B",8.28,0.85,"6.09%","334.02B"], + ["MENGNIU DAIRY","02319.HK",26.250,+1.500,"+6.061%","8.58M","223.15M",17.34,2.30,"1.73%","103.42B"], + ["LI NING","02331.HK",32.950,+1.500,"+4.769%","22.54M","738.37M",18.80,3.16,"1.58%","86.86B"], + ["SUNNY OPTICAL","02382.HK",54.700,+1.050,"+1.957%","5.62M","308.60M",22.01,2.44,"0.91%","60.00B"], + ["BOC HONG KONG","02388.HK",21.450,+0.300,"+1.418%","6.53M","140.32M",8.38,0.75,"6.33%","226.79B"], + ["CHINA LIFE","02628.HK",12.200,+0.240,"+2.007%","13.53M","165.07M",9.49,0.70,"4.53%","90.78B"], + ["ENN ENERGY","02688.HK",65.000,+1.150,"+1.801%","3.06M","198.69M",11.08,1.67,"4.48%","73.53B"], + ["ZIJIN MINING","02899.HK",11.980,+0.160,"+1.354%","16.20M","194.50M",13.97,3.14,"1.88%","68.73B"], + ["MEITUAN-W","03690.HK",114.600,+3.800,"+3.430%","19.17M","2.19B","No Profit",4.89,"0.00%","715.43B"], + ["HANSOH PHARMA","03692.HK",10.660,+0.160,"+1.524%","17.56M","187.72M",21.48,2.47,"0.94%","63.25B"], + ["CM BANK","03968.HK",32.700,+0.100,"+0.307%","11.13M","363.99M",5.51,0.86,"6.00%","150.12B"], + ["BANK OF CHINA","03988.HK",2.740,+0.050,"+1.859%","275.47M","754.54M",3.33,0.33,"9.55%","229.13B"], + ["CG SERVICES","06098.HK",8.080,+0.250,"+3.193%","16.08M","128.87M",12.42,0.65,"5.20%","27.01B"], + ["JD HEALTH","06618.HK",40.600,+2.950,"+7.835%","7.75M","312.36M",299.91,2.56,"0.00%","129.12B"], + ["HAIER SMARTHOME","06690.HK",24.650,+1.150,"+4.894%","7.24M","178.37M",13.83,2.21,"2.59%","70.46B"], + ["HAIDILAO","06862.HK",21.000,+0.600,"+2.941%","6.86M","143.79M",74.46,13.94,"0.55%","117.05B"], + ["JD-SW","09618.HK",115.100,+4.000,"+3.600%","7.43M","852.28M",30.73,1.50,"6.38%","360.24B"], + ["NONGFU SPRING","09633.HK",45.000,+1.500,"+3.448%","3.69M","165.62M",52.49,18.63,"1.71%","226.56B"], + ["BIDU-SW","09888.HK",133.400,+4.600,"+3.571%","5.82M","772.73M",47.30,1.50,"0.00%","377.37B"], + ["TRIP.COM-S","09961.HK",279.200,+7.000,"+2.572%","1.25M","349.59M",114.05,1.40,"0.00%","176.65B"], + ["BABA-SW","09988.HK",85.600,+2.600,"+3.133%","34.61M","2.96B",21.65,1.62,"0.00%",1,"837.13B"], + ["NTES-S","09999.HK",159.500,+8.300,"+5.489%","5.02M","797.10M",22.70,4.63,"1.36%","546.99B"], +] diff --git a/vuu-ui/packages/vuu-data-test/src/basket/reference-data/nasdaq100.ts b/vuu-ui/packages/vuu-data-test/src/basket/reference-data/nasdaq100.ts new file mode 100644 index 000000000..6bcd0863d --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/basket/reference-data/nasdaq100.ts @@ -0,0 +1,105 @@ +//Name,Symbol,Weight,Last Trade,Chg,% Chg +// prettier-ignore +export default [ + ["Apple Inc", "AAPL", 11.007,174.94,0.15, "(0.09%)"], + ["Microsoft Corp", "MSFT", 9.61,316.98,-0.03, "(-0.01%)"], + ["Amazon.com Inc", "AMZN", 5.401,129.31,0.19, "(0.14%)"], + ["NVIDIA Corp", "NVDA", 4.101,415.74,-0.36, "(-0.09%)"], + ["Meta Platforms Inc", "META", 3.729,299.42,0.34, "(0.11%)"], + ["Tesla Inc", "TSLA", 3.285,244.13,-0.75, "(-0.31%)"], + ["Alphabet Inc", "GOOGL", 3.133,130.49,0.24, "(0.18%)"], + ["Alphabet Inc", "GOOG", 3.085,131.49,0.24, "(0.18%)"], + ["Broadcom Inc", "AVGO", 2.896,833.04,3.96, "(0.48%)"], + ["Costco Wholesale Corp", "COST", 2.137,557.85,-0.74, "(-0.13%)"], + ["PepsiCo Inc", "PEP", 2.096,174.84,-0.43, "(-0.25%)"], + ["Adobe Inc", "ADBE", 2.033,511.42,-1.48, "(-0.29%)"], + ["Cisco Systems Inc", "CSCO", 1.887,53.68,0.11, "(0.20%)"], + ["Comcast Corp", "CMCSA", 1.633,45.30,-0.01, "(-0.01%)"], + ["Netflix Inc", "NFLX", 1.478,379.89,0.08, "(0.02%)"], + ["T-Mobile US Inc", "TMUS", 1.43,140.18,0.83, "(0.60%)"], + ["Advanced Micro Devices Inc", "AMD", 1.348,96.09,-0.11, "(-0.12%)"], + ["Texas Instruments Inc", "TXN", 1.264,161.00,0.69, "(0.43%)"], + ["Intel Corp", "INTC", 1.26,34.19,0.01, "(0.01%)"], + ["Amgen Inc", "AMGN", 1.251,271.18,3.48, "(1.30%)"], + ["Intuit Inc", "INTU", 1.226,509.00,0.43, "(0.08%)"], + ["Honeywell International Inc", "HON", 1.103,188.67,-1.12, "(-0.59%)"], + ["QUALCOMM Inc", "QCOM", 1.046,107.53,-0.16, "(-0.14%)"], + ["Applied Materials Inc", "AMAT", 0.982,136.32,0.15, "(0.11%)"], + ["Booking Holdings Inc", "BKNG", 0.941,3, 19.20,-43.30, "(-1.42%)"], + ["Starbucks Corp", "SBUX", 0.926,93.80,0.12, "(0.12%)"], + ["Intuitive Surgical Inc", "ISRG", 0.867,288.50,0.30, "(0.10%)"], + ["Automatic Data Processing Inc", "ADP", 0.854,239.45,0.10, "(0.04%)"], + ["Mondelez International Inc", "MDLZ", 0.835,71.25,0.82, "(1.16%)"], + ["Gilead Sciences Inc", "GILD", 0.814,75.47,0.46, "(0.61%)"], + ["Vertex Pharmaceuticals Inc", "VRTX", 0.794,353.67,4.14, "(1.18%)"], + ["Regeneron Pharmaceuticals Inc", "REGN", 0.763,828.09,3.25, "(0.39%)"], + ["Analog Devices Inc", "ADI", 0.752,176.82,1.34, "(0.76%)"], + ["Lam Research Corp", "LRCX", 0.702,620.00,0.89, "(0.14%)"], + ["Micron Technology Inc", "MU", 0.646,68.90,0.02, "(0.03%)"], + ["Palo Alto Networks Inc", "PANW", 0.604,228.70,0.19, "(0.08%)"], + ["Synopsys Inc", "SNPS", 0.587,447.12,0.27, "(0.06%)"], + ["Charter Communications Inc", "CHTR", 0.581,441.88,-3.33, "(-0.75%)"], + ["MercadoLibre Inc", "MELI", 0.562,1,275.50,-1.47, "(-0.11%)"], + ["PayPal Holdings Inc", "PYPL", 0.559,57.90,0.02, "(0.03%)"], + ["CSX Corp", "CSX", 0.54,31.23,0.03, "(0.09%)"], + ["Cadence Design Systems Inc", "CDNS", 0.539,230.83,0.17, "(0.07%)"], + ["KLA Corp", "KLAC", 0.53,456.11,4.29, "(0.95%)"], + ["PDD Holdings Inc ADR", "PDD", 0.521,95.93,-0.01, "(-0.01%)"], + ["Marriott International Inc/MD", "MAR", 0.505,193.90,-0.46, "(-0.23%)"], + ["Monster Beverage Corp", "MNST", 0.498,54.48,-0.04, "(-0.08%)"], + ["Airbnb Inc", "ABNB", 0.491,132.09,-0.11, "(-0.08%)"], + ["O'Reilly Automotive Inc", "ORLY", 0.485,936.67,0.65, "(0.07%)"], + ["Cintas Corp", "CTAS", 0.446,505.27,0.52, "(0.10%)"], + ["ASML Holding NV", "ASML", 0.438,585.00,-2.10, "(-0.36%)"], + ["NXP Semiconductors NV", "NXPI", 0.434,196.76,-0.04, "(-0.02%)"], + ["Workday Inc", "WDAY", 0.414,235.55,4.73, "(2.05%)"], + ["Lululemon Athletica Inc", "LULU", 0.405,386.36,-1.69, "(-0.44%)"], + ["Keurig Dr Pepper Inc,KDP", 0.404,33.25,0.13, "(0.39%)"], + ["Fortinet Inc", "FTNT", 0.401,58.29,0.05, "(0.09%)"], + ["Marvell Technology Inc", "MRVL", 0.388,52.38,0.08, "(0.14%)"], + ["PACCAR Inc", "PCAR", 0.38,84.97,0.04, "(0.04%)"], + ["Old Dominion Freight Line Inc", "ODFL", 0.38,401.01,0.35, "(0.09%)"], + ["Autodesk Inc", "ADSK", 0.379,204.07,0.03, "(0.01%)"], + ["Kraft Heinz Co/The", "KHC", 0.368,34.27,0.11, "(0.31%)"], + ["Microchip Technology Inc", "MCHP", 0.36,77.00,-0.08, "(-0.10%)"], + ["Copart Inc", "CPRT", 0.358,43.26,0.10, "(0.22%)"], + ["American Electric Power Co Inc", "AEP", 0.357,79.23,0.06, "(0.08%)"], + ["Paychex Inc", "PAYX", 0.355,113.01,0.06, "(0.06%)"], + ["Exelon Corp", "EXC", 0.35,40.28,0.07, "(0.16%)"], + ["ON Semiconductor Corp", "ON", 0.341,93.87,0.06, "(0.06%)"], + ["AstraZeneca PLC ADR", "AZN", 0.339,67.35,-0.49, "(-0.72%)"], + ["Seagen Inc", "SGEN", 0.336,212.18,-1.52, "(-0.71%)"], + ["Ross Stores Inc", "ROST", 0.335,111.71,0.05, "(0.04%)"], + ["Moderna Inc", "MRNA", 0.331,100.32,0.33, "(0.33%)"], + ["Biogen Inc", "BIIB", 0.326,257.93,0.25, "(0.10%)"], + ["Crowdstrike Holdings Inc", "CRWD", 0.319,162.85,0.28, "(0.17%)"], + ["IDEXX Laboratories Inc", "IDXX", 0.316,439.41,3.72, "(0.85%)"], + ["Baker Hughes Co", "BKR", 0.307,35.44,0.04, "(0.10%)"], + ["Constellation Energy Corp", "CEG",0.307,110.41,0.05, "(0.04%)"], + ["Cognizant Technology Solutions Corp","CTSH",0.303,70.19,0.58,"(0.83%)"], + ["Verisk Analytics Inc", "VRSK", 0.303,242.05,0.10, "(0.04%)"], + ["Dexcom Inc", "DXCM", 0.3,88.30,0.80, "(0.92%)"], + ["Trade Desk Inc/The", "TTD", 0.293,76.38,0.03, "(0.03%)"], + ["Xcel Energy Inc", "XEL", 0.284,59.76,0.03, "(0.04%)"], + ["Electronic Arts Inc", "EA", 0.279,118.38,-0.63, "(-0.53%)"], + ["CoStar Group Inc", "CSGP", 0.277,78.18,0.12, "(0.15%)"], + ["GLOBALFOUNDRIES Inc", "GFS", 0.273,57.21,0.38, "(0.67%)"], + ["Fastenal Co", "FAST", 0.268,54.32,0.11, "(0.20%)"], + ["Atlassian Corp", "TEAM", 0.253,196.01,0.55, "(0.28%)"], + ["GE HealthCare Technologies Inc", "GEHC", 0.252,63.78,-0.56, "(-0.86%)"], + ["Warner Bros Discovery Inc", "WBD", 0.244,11.09,-0.02, "(-0.14%)"], + ["Diamondback Energy Inc", "FANG", 0.235,150.70,0.13, "(0.09%)"], + ["Datadog Inc", "DDOG", 0.23,88.81,0.05, "(0.06%)"], + ["ANSYS Inc", "ANSS", 0.227,300.60,-2.31, "(-0.76%)"], + ["eBay Inc", "EBAY", 0.203,43.22,0.09, "(0.20%)"], + ["Dollar Tree Inc", "DLTR", 0.201,104.77,0.27, "(0.25%)"], + ["Align Technology Inc", "ALGN", 0.2,297.25,-1.31, "(-0.44%)"], + ["Zscaler Inc", "ZS", 0.188,151.80,0.09, "(0.06%)"], + ["Illumina Inc", "ILMN", 0.183,132.31,-0.13, "(-0.10%)"], + ["Walgreens Boots Alliance Inc", "WBA", 0.161,21.18,0.06, "(0.28%)"], + ["Zoom Video Communications Inc", "ZM", 0.151,68.57,-0.29, "(-0.41%)"], + ["Enphase Energy Inc", "ENPH", 0.148,120.41,0.49, "(0.41%)"], + ["Sirius XM Holdings Inc", "SIRI", 0.137,4.05,0.02, "(0.50%)"], + ["JD.com Inc ADR", "JD", 0.117,30.43,0.02, "(0.05%)"], + ["Lucid Group Inc", "LCID", 0.102,5.14,0.03, "(0.49%)"], +]; diff --git a/vuu-ui/packages/vuu-data-test/src/basket/reference-data/sp500.ts b/vuu-ui/packages/vuu-data-test/src/basket/reference-data/sp500.ts new file mode 100644 index 000000000..6cf4f13c0 --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/basket/reference-data/sp500.ts @@ -0,0 +1,509 @@ +//Name,Symbol,Weight,Price,Change,Change % +export default [ + ["Apple Inc", "AAPL", 6.992488, 171.9, 1.47, "(0.86%)"], + ["Microsoft Corp", "MSFT", 6.487978, 314.55, 1.76, "(0.56%)"], + ["Amazon.com Inc", "AMZN", 3.193379, 126.25, 0.27, "(0.22%)"], + ["Nvidia Corp", "NVDA", 2.928461, 433.11, 8.43, "(1.99%)"], + ["Alphabet Inc Cl A", "GOOGL", 2.162218, 132.93, 2.39, "(1.83%)"], + ["Tesla Inc", "TSLA", 1.854311, 246.56, 6.06, "(2.52%)"], + ["Alphabet Inc Cl C", "GOOG", 1.85251, 133.81, 2.35, "(1.78%)"], + ["Meta Platforms Inc Class A", "META", 1.847731, 305.93, 8.19, "(2.75%)"], + ["Berkshire Hathaway Inc Cl B", "BRK.B", 1.82184, 358.67, 0.89, "(0.25%)"], + ["Exxon Mobil Corp", "XOM", 1.343563, 118.97, -1.23, "(-1.03%)"], + ["Unitedhealth Group Inc", "UNH", 1.302884, 510.04, 6.31, "(1.25%)"], + ["Eli Lilly & Co", "LLY", 1.224046, 546.65, -3.11, "(-0.57%)"], + ["Jpmorgan Chase & Co", "JPM", 1.182918, 148.23, 2.45, "(1.68%)"], + ["Johnson & Johnson W/d", "JNJ", 1.056344, 156.83, -0.28, "(-0.18%)"], + ["Visa Inc Class a Shares", "V", 1.029636, 231.97, 2.47, "(1.08%)"], + ["Procter & Gamble Co", "PG", 0.969793, 145.75, -1.59, "(-1.08%)"], + ["Broadcom Inc", "AVGO", 0.941177, 835.33, 18.52, "(2.27%)"], + ["Mastercard Inc A", "MA", 0.918779, 400.65, 5.17, "(1.31%)"], + ["Chevron Corp", "CVX", 0.847353, 170.55, -0.49, "(-0.28%)"], + ["Home Depot Inc", "HD", 0.847289, 304.35, 2.53, "(0.84%)"], + ["Abbvie Inc", "BBV", 0.754672, 152.14, -0.99, "(-0.65%)"], + ["Merck & Co. Inc.", "MRK", 0.736434, 104.2, 0.26, "(0.25%)"], + ["Costco Wholesale Corp", "COST", 0.697258, 569.05, 5.52, "(0.98%)"], + ["Pepsico Inc", "PEP", 0.651768, 168.92, -0.65, "(-0.38%)"], + ["Walmart Inc", "WMT", 0.644449, 161.87, 0.15, "(0.09%)"], + ["Adobe Inc", "ADBE", 0.639652, 507.17, 4.57, "(0.91%)"], + ["Coca Cola Co", "KO", 0.60801, 55.82, -0.13, "(-0.23%)"], + ["Cisco Systems Inc", "CSCO", 0.605326, 53.97, 0.77, "(1.44%)"], + ["Accenture Plc Cl A", "ACN", 0.55372, 298.5, -15.9, "(-5.05%)"], + ["Salesforce Inc", "CRM", 0.551342, 204.46, 1.73, "(0.85%)"], + ["Thermo Fisher Scientific Inc", "TMO", 0.536951, 503.47, 5.18, "(1.04%)"], + ["Mcdonald S Corp", "MCD", 0.536457, 265.83, 2.19, "(0.83%)"], + ["Bank of America Corp", "BAC", 0.526405, 27.51, 0.24, "(0.86%)"], + ["Comcast Corp Class A", "CMCSA", 0.512537, 45.17, 0.57, "(1.27%)"], + ["Linde Plc", "LIN", 0.506835, 375.48, 3.47, "(0.93%)"], + ["Pfizer Inc", "PFE", 0.506043, 32.09, -0.02, "(-0.05%)"], + ["Netflix Inc", "NFLX", 0.467192, 377.77, 0.18, "(0.05%)"], + ["Abbott Laboratories", "ABT", 0.462893, 97.52, 1.99, "(2.08%)"], + ["Oracle Corp", "ORCL", 0.459877, 106.29, 1.67, "(1.60%)"], +]; + +/* +41,Danaher Corp,DHR,0.454026,248.24,0.80,(0.32%) +42,Advanced Micro Devices,AMD,0.442473,103.74,5.67,(5.78%) +43,Wells Fargo & Co,WFC,0.417428,41.04,0.18,(0.43%) +44,Conocophillips,COP,0.411569,123.32,0.23,(0.18%) +45,Walt Disney Co,DIS,0.408438,80.35,0.45,(0.56%) +46,Intel Corp,INTC,0.404462,35.50,0.89,(2.56%) +47,Amgen Inc,AMGN,0.401904,270.91,1.93,(0.72%) +48,Texas Instruments Inc,TXN,0.400931,160.98,2.84,(1.80%) +49,Intuit Inc,INTU,0.397406,515.11,6.87,(1.35%) +50,Philip Morris International,PM,0.39143,92.66,2.34,(2.59%) +51,Caterpillar Inc,CAT,0.388444,276.34,3.63,(1.33%) +52,Verizon Communications Inc,VZ,0.380327,32.56,0.16,(0.48%) +53,Intl Business Machines Corp,IBM,0.364246,140.89,-2.28,(-1.59%) +54,Honeywell International Inc,HON,0.348317,188.68,0.80,(0.43%) +55,Union Pacific Corp,UNP,0.344419,203.88,1.48,(0.73%) +56,Qualcomm Inc,QCOM,0.340195,111.55,2.36,(2.16%) +57,Nextera Energy Inc,NEE,0.338809,57.64,-2.32,(-3.88%) +58,Lowe S Cos Inc,LOW,0.338613,209.52,2.56,(1.24%) +59,Bristol Myers Squibb Co,BMY,0.337683,58.30,0.41,(0.70%) +60,General Electric Co,GE,0.336989,112.43,1.54,(1.39%) +61,S&p Global Inc,SPGI,0.32985,369.21,6.15,(1.69%) +62,Applied Materials Inc,AMAT,0.316678,139.15,4.09,(3.03%) +63,Servicenow Inc,NOW,0.3112,552.84,6.46,(1.18%) +64,Boeing Co,BA,0.309673,193.12,-2.34,(-1.20%) +65,United Parcel Service Cl B,UPS,0.307795,154.29,1.88,(1.23%) +66,Booking Holdings Inc,BKNG,0.307126,3,086.26,3.60,(0.12%) +67,Nike Inc Cl B,NKE,0.305871,89.63,0.21,(0.24%) +68,At&t Inc,T,0.296603,14.94,0.07,(0.44%) +69,Goldman Sachs Group Inc,GS,0.296356,325.72,3.77,(1.17%) +70,Rtx Corp,RTX,0.296189,72.54,-0.34,(-0.46%) +71,Elevance Health Inc,ELV,0.292632,448.99,4.23,(0.95%) +72,Deere & Co,DE,0.291602,388.36,4.00,(1.04%) +73,Starbucks Corp,SBUX,0.291579,91.21,0.04,(0.04%) +74,Morgan Stanley,MS,0.29087,82.34,0.69,(0.84%) +75,Medtronic Plc,MDT,0.290688,78.77,0.55,(0.71%) +76,Prologis Inc,PLD,0.286912,112.30,1.05,(0.94%) +77,Intuitive Surgical Inc,ISRG,0.285764,296.31,4.88,(1.67%) +78,Tjx Companies Inc,TJX,0.282733,89.27,1.16,(1.32%) +79,Automatic Data Processing,ADP,0.279113,243.79,1.16,(0.48%) +80,Marsh & Mclennan Cos,MMC,0.266169,193.53,0.55,(0.29%) +81,Mondelez International Inc A,MDLZ,0.26425,69.44,-0.56,(-0.79%) +82,Gilead Sciences Inc,GILD,0.26285,75.96,0.41,(0.54%) +83,Lockheed Martin Corp,LMT,0.255775,410.40,1.68,(0.41%) +84,Blackrock Inc,BLK,0.252096,645.37,1.98,(0.31%) +85,Vertex Pharmaceuticals Inc,VRTX,0.251521,353.91,4.26,(1.22%) +86,Stryker Corp,SYK,0.250789,273.07,6.53,(2.45%) +87,Cvs Health Corp,CVS,0.250255,70.11,0.33,(0.47%) +88,Regeneron Pharmaceuticals,REGN,0.2481,835.67,3.11,(0.37%) +89,American Express Co,AXP,0.243515,150.42,0.52,(0.34%) +90,Chubb Ltd,CB,0.242706,212.92,1.29,(0.61%) +91,Analog Devices Inc,ADI,0.241943,177.01,4.20,(2.43%) +92,Schlumberger Ltd,SLB,0.241232,61.02,0.22,(0.36%) +93,Eaton Corp Plc,ETN,0.238556,216.42,2.06,(0.96%) +94,The Cigna Group,CI,0.238517,291.43,2.82,(0.98%) +95,Progressive Corp,PGR,0.229854,140.76,0.14,(0.10%) +96,Lam Research Corp,LRCX,0.228483,627.46,13.54,(2.20%) +97,Schwab (Charles) Corp,SCHW,0.225784,54.57,0.19,(0.35%) +98,Zoetis Inc,ZTS,0.224522,174.68,-0.00,(-0.00%) +99,Citigroup Inc,C,0.217552,41.24,0.78,(1.92%) +100,Boston Scientific Corp,BSX,0.213003,53.30,1.20,(2.31%) +101,Blackstone Inc,BX,0.211158,108.88,2.33,(2.19%) +102,Eog Resources Inc,EOG,0.21104,130.56,0.75,(0.57%) +103,Becton Dickinson and Co,BDX,0.209627,262.60,3.80,(1.47%) +104,Micron Technology Inc,MU,0.208276,66.25,-1.96,(-2.87%) +105,American Tower Corp,AMT,0.207727,160.73,1.04,(0.65%) +106,Altria Group Inc,MO,0.207666,42.11,0.20,(0.47%) +107,T Mobile Us Inc,TMUS,0.20192,140.30,0.60,(0.43%) +108,Cme Group Inc,CME,0.201368,200.22,-0.14,(-0.07%) +109,Southern Co,SO,0.20116,65.04,-1.10,(-1.67%) +110,Palo Alto Networks Inc,PANW,0.198047,236.42,4.52,(1.95%) +111,Duke Energy Corp,DUK,0.193827,88.87,-1.23,(-1.36%) +112,Fiserv Inc,FI,0.192662,113.67,0.48,(0.42%) +113,Synopsys Inc,SNPS,0.189768,458.88,12.17,(2.72%) +114,Activision Blizzard Inc,ATVI,0.18717,93.71,-0.22,(-0.23%) +115,Aon Plc Class A,AON,0.186896,330.04,0.12,(0.04%) +116,Equinix Inc,EQIX,0.184757,715.84,8.74,(1.24%) +117,Illinois Tool Works,ITW,0.177501,233.17,0.77,(0.33%) +118,Air Products & Chemicals Inc,APD,0.17678,289.15,3.89,(1.36%) +119,Paypal Holdings Inc,PYPL,0.175802,58.30,0.96,(1.67%) +120,Cadence Design Sys Inc,CDNS,0.175052,235.09,4.44,(1.93%) +121,Northrop Grumman Corp,NOC,0.173654,441.30,4.06,(0.93%) +122,Intercontinental Exchange In,ICE,0.172998,109.93,1.66,(1.53%) +123,Humana Inc,HUM,0.170476,495.73,2.16,(0.44%) +124,Marathon Petroleum Corp,MPC,0.170186,155.28,2.85,(1.87%) +125,Kla Corp,KLAC,0.170079,459.61,14.05,(3.15%) +126,Fedex Corp,FDX,0.169747,266.99,4.28,(1.63%) +127,Csx Corp,CSX,0.169632,30.62,0.34,(1.14%) +128,Mckesson Corp,MCK,0.167376,445.58,1.20,(0.27%) +129,Sherwin Williams Co,SHW,0.165389,254.95,4.33,(1.73%) +130,Colgate Palmolive Co,CL,0.164,71.11,0.06,(0.08%) +131,Airbnb Inc Class A,ABNB,0.159553,137.66,3.63,(2.70%) +132,Waste Management Inc,WM,0.15893,154.17,-0.25,(-0.16%) +133,Emerson Electric Co,EMR,0.154857,97.31,0.45,(0.47%) +134,O Reilly Automotive Inc,ORLY,0.154194,918.15,3.98,(0.44%) +135,Pioneer Natural Resources Co,PXD,0.1524,234.43,0.34,(0.14%) +136,Phillips 66,PSX,0.150197,122.81,2.01,(1.66%) +137,Freeport Mcmoran Inc,FCX,0.146185,37.34,0.79,(2.17%) +138,3m Co W/d,MMM,0.144312,93.48,0.02,(0.02%) +139,Roper Technologies Inc,ROP,0.144165,486.61,-0.02,(-0.00%) +140,Valero Energy Corp,VLO,0.141937,147.59,3.64,(2.53%) +141,Nxp Semiconductors Nv,NXPI,0.141583,202.19,5.49,(2.79%) +142,Target Corp,TGT,0.141106,109.31,-0.44,(-0.40%) +143,Parker Hannifin Corp,PH,0.140358,396.29,5.18,(1.32%) +144,Us Bancorp,USB,0.139291,32.59,0.07,(0.20%) +145,General Dynamics Corp,GD,0.139193,223.44,2.44,(1.10%) +146,Chipotle Mexican Grill Inc,CMG,0.139055,1,828.59,22.51,(1.25%) +147,Hca Healthcare Inc,HCA,0.138966,251.59,4.33,(1.75%) +148,Arthur J Gallagher & Co,AJG,0.138231,231.77,1.44,(0.62%) +149,Moody S Corp,MCO,0.138162,319.07,4.56,(1.45%) +150,Amphenol Corp Cl A,APH,0.136941,84.40,2.04,(2.47%) +151,Ford Motor Co,F,0.1362,12.58,0.19,(1.49%) +152,Marriott International Cl A,MAR,0.135619,199.53,5.65,(2.92%) +153,Pnc Financial Services Group,PNC,0.135457,123.09,1.28,(1.05%) +154,Transdigm Group Inc,TDG,0.131273,860.46,8.64,(1.01%) +155,Carrier Global Corp,CARR,0.129065,56.83,1.75,(3.17%) +156,Autozone Inc,AZO,0.128857,2,556.17,15.27,(0.60%) +157,Trane Technologies Plc,TT,0.12759,207.07,7.01,(3.50%) +158,Motorola Solutions Inc,MSI,0.127284,275.18,2.20,(0.81%) +159,Arista Networks Inc,ANET,0.12715,184.23,2.63,(1.45%) +160,Norfolk Southern Corp,NSC,0.124378,197.30,1.08,(0.55%) +161,General Motors Co,GM,0.124282,33.25,0.90,(2.77%) +162,Paccar Inc,PCAR,0.123613,86.12,1.31,(1.54%) +163,Charter Communications Inc A,CHTR,0.123199,442.28,8.86,(2.04%) +164,Hess Corp,HES,0.121942,157.30,-0.74,(-0.47%) +165,Sempra,SRE,0.121909,68.15,-1.26,(-1.81%) +166,Occidental Petroleum Corp,OXY,0.121531,66.03,0.43,(0.66%) +167,American International Group,AIG,0.121272,61.73,0.72,(1.18%) +168,Autodesk Inc,ADSK,0.120711,208.03,5.75,(2.84%) +169,Edwards Lifesciences Corp,EW,0.119074,69.68,-0.43,(-0.62%) +170,Ecolab Inc,ECL,0.118911,169.50,1.63,(0.97%) +171,Public Storage,PSA,0.117206,266.58,1.60,(0.60%) +172,Microchip Technology Inc,MCHP,0.116275,78.91,2.41,(3.15%) +173,Aflac Inc,AFL,0.11627,77.65,0.62,(0.81%) +174,Cintas Corp,CTAS,0.116227,486.83,5.75,(1.19%) +175,Welltower Inc,WELL,0.116157,81.07,0.87,(1.08%) +176,Williams Cos Inc,WMB,0.115685,34.65,0.55,(1.61%) +177,Kimberly Clark Corp,KMB,0.114902,121.11,-0.55,(-0.45%) +178,Archer Daniels Midland Co,ADM,0.114307,76.74,0.38,(0.50%) +179,Msci Inc,MSCI,0.113047,518.97,7.03,(1.37%) +180,Constellation Brands Inc A,STZ,0.112869,251.62,1.03,(0.41%) +181,On Semiconductor,ON,0.111009,95.12,3.07,(3.33%) +182,Metlife Inc,MET,0.110772,63.06,0.26,(0.41%) +183,Monster Beverage Corp,MNST,0.110311,53.31,0.19,(0.35%) +184,Hilton Worldwide Holdings In,HLT,0.109054,153.24,3.88,(2.60%) +185,American Electric Power,AEP,0.108899,74.71,-1.15,(-1.51%) +186,Crown Castle Inc,CCI,0.108512,91.35,1.65,(1.83%) +187,Exelon Corp,EXC,0.107894,38.11,-0.69,(-1.78%) +188,Nucor Corp,NUE,0.107787,158.31,2.60,(1.67%) +189,Travelers Cos Inc,TRV,0.107342,167.42,-0.51,(-0.30%) +190,Dominion Energy Inc,D,0.106001,44.61,-0.86,(-1.88%) +191,Te Connectivity Ltd,TEL,0.105581,123.84,3.40,(2.82%) +192,Halliburton Co,HAL,0.105449,42.18,0.15,(0.37%) +193,Centene Corp,CNC,0.105443,69.87,0.13,(0.18%) +194,Fortinet Inc,FTNT,0.105315,59.07,0.93,(1.59%) +195,Oneok Inc,OKE,0.104854,64.86,0.35,(0.53%) +196,General Mills Inc,GIS,0.104655,63.79,-0.26,(-0.40%) +197,Copart Inc,CPRT,0.104453,43.66,0.61,(1.41%) +198,Paychex Inc,PAYX,0.104412,117.40,0.89,(0.76%) +199,Biogen Inc,BIIB,0.103916,258.95,1.43,(0.55%) +200,Truist Financial Corp,TFC,0.103679,28.15,0.18,(0.63%) +201,Ross Stores Inc,ROST,0.10361,111.18,2.26,(2.07%) +202,Johnson Controls Internation,JCI,0.103601,53.69,-0.85,(-1.55%) +203,Iqvia Holdings Inc,IQV,0.103397,200.11,-2.10,(-1.04%) +204,Capital One Financial Corp,COF,0.101688,96.63,1.14,(1.20%) +205,Baker Hughes Co,BKR,0.101425,36.62,0.37,(1.02%) +206,Idexx Laboratories Inc,IDXX,0.101263,439.37,2.51,(0.57%) +207,Corteva Inc,CTVA,0.100577,50.77,0.02,(0.05%) +208,Dow Inc,DOW,0.100258,50.87,-0.13,(-0.26%) +209,Old Dominion Freight Line,ODFL,0.100037,406.82,6.93,(1.73%) +210,Constellation Energy,CEG,0.099445,109.94,-0.81,(-0.73%) +211,Dexcom Inc,DXCM,0.099384,95.68,3.59,(3.90%) +212,Simon Property Group Inc,SPG,0.098977,108.45,0.26,(0.24%) +213,Digital Realty Trust Inc,DLR,0.098757,117.60,0.76,(0.65%) +214,Realty Income Corp,O,0.098278,49.83,0.17,(0.33%) +215,Kenvue Inc W/i,KVUE,0.097359,20.20,-0.02,(-0.12%) +216,Verisk Analytics Inc,VRSK,0.097203,241.97,2.34,(0.98%) +217,Cognizant Tech Solutions A,CTSH,0.096172,68.15,-0.05,(-0.07%) +218,P G & E Corp,PCG,0.095995,16.36,-0.09,(-0.52%) +219,Prudential Financial Inc,PRU,0.095804,94.93,0.41,(0.43%) +220,Ametek Inc,AME,0.09567,150.03,1.17,(0.79%) +221,Yum Brands Inc,YUM,0.095443,123.55,1.57,(1.29%) +222,Dupont De Nemours Inc,DD,0.094185,74.58,1.11,(1.51%) +223,Ameriprise Financial Inc,AMP,0.093691,332.11,5.16,(1.58%) +224,L3harris Technologies Inc,LHX,0.092512,173.16,-2.04,(-1.16%) +225,Fidelity National Info Serv,FIS,0.09207,56.06,0.52,(0.94%) +226,Sysco Corp,SYY,0.091968,65.99,0.78,(1.20%) +227,Moderna Inc,MRNA,0.091789,100.61,1.17,(1.18%) +228,Bank of New York Mellon Corp,BK,0.091699,42.76,0.59,(1.39%) +229,Agilent Technologies Inc,A,0.091382,111.91,1.41,(1.28%) +230,Otis Worldwide Corp,OTIS,0.091228,80.55,1.20,(1.51%) +231,Rockwell Automation Inc,ROK,0.091103,288.26,3.46,(1.21%) +232,Dr Horton Inc,DHI,0.090974,108.96,1.95,(1.82%) +233,Cummins Inc,CMI,0.090916,232.98,3.03,(1.32%) +234,Estee Lauder Companies Cl A,EL,0.090788,139.94,-0.10,(-0.07%) +235,Kinder Morgan Inc,KMI,0.090175,16.71,0.05,(0.27%) +236,Keurig Dr Pepper Inc,KDP,0.088358,31.51,-0.16,(-0.51%) +237,Fastenal Co,FAST,0.087952,55.72,0.66,(1.21%) +238,Xcel Energy Inc,XEL,0.087474,56.50,-0.71,(-1.25%) +239,Devon Energy Corp,DVN,0.087359,48.84,0.00,(0.01%) +240,Ww Grainger Inc,GWW,0.086956,704.07,3.96,(0.57%) +241,Costar Group Inc,CSGP,0.086756,77.10,0.75,(0.99%) +242,Cencora Inc,COR,0.086188,183.99,-1.17,(-0.63%) +243,United Rentals Inc,URI,0.085093,454.44,8.22,(1.84%) +244,Hershey Co,HSY,0.084755,201.01,-1.55,(-0.76%) +245,Arch Capital Group Ltd,ACGL,0.083799,81.62,0.54,(0.67%) +246,Ppg Industries Inc,PPG,0.083787,130.49,2.46,(1.92%) +247,Global Payments Inc,GPN,0.083671,116.72,1.47,(1.28%) +248,Consolidated Edison Inc,ED,0.083432,85.06,-1.57,(-1.81%) +249,Newmont Corp,NEM,0.082799,36.78,-0.50,(-1.34%) +250,Republic Services Inc,RSG,0.082765,145.01,0.28,(0.19%) +251,Allstate Corp,ALL,0.08254,112.77,-0.25,(-0.22%) +252,Electronic Arts Inc,EA,0.081213,119.76,1.79,(1.51%) +253,Vici Properties Inc,VICI,0.081127,29.28,0.40,(1.37%) +254,Kroger Co,KR,0.080998,44.96,0.48,(1.07%) +255,Public Service Enterprise Gp,PEG,0.080723,56.88,-1.02,(-1.75%) +256,Lennar Corp A,LEN,0.078759,113.41,1.71,(1.53%) +257,Diamondback Energy Inc,FANG,0.078498,158.01,0.78,(0.50%) +258,West Pharmaceutical Services,WST,0.077362,377.15,1.97,(0.52%) +259,Quanta Services Inc,PWR,0.077074,190.82,0.70,(0.37%) +260,Gartner Inc,IT,0.076911,347.78,-1.41,(-0.40%) +261,Aptiv Plc,APTV,0.0757,99.21,3.35,(3.50%) +262,Vulcan Materials Co,VMC,0.075604,207.60,4.00,(1.97%) +263,Kraft Heinz Co,KHC,0.075468,33.72,-0.31,(-0.90%) +264,Ge Healthcare Technology,GEHC,0.074734,69.81,1.53,(2.24%) +265,Cdw Corp/de,CDW,0.074375,202.53,3.84,(1.93%) +266,Fortive Corp,FTV,0.072323,74.75,1.16,(1.58%) +267,Ingersoll Rand Inc,IR,0.071683,64.52,1.25,(1.97%) +268,Ansys Inc,ANSS,0.071343,299.19,4.85,(1.65%) +269,Extra Space Storage Inc,EXR,0.071269,120.75,0.24,(0.20%) +270,Wec Energy Group Inc,WEC,0.071007,79.67,-0.91,(-1.12%) +271,Martin Marietta Materials,MLM,0.070759,418.00,8.05,(1.96%) +272,Edison International,EIX,0.068887,63.92,-1.38,(-2.11%) +273,American Water Works Co Inc,AWK,0.068233,123.27,-3.07,(-2.43%) +274,Warner Bros Discovery Inc,WBD,0.068207,10.89,-0.15,(-1.31%) +275,Lyondellbasell Indu Cl A,LYB,0.067294,94.86,-0.11,(-0.11%) +276,Mettler Toledo International,MTD,0.067028,1,112.26,14.48,(1.32%) +277,Avalonbay Communities Inc,AVB,0.066762,171.85,-0.60,(-0.35%) +278,Delta Air Lines Inc,DAL,0.065781,37.32,0.66,(1.79%) +279,T Rowe Price Group Inc,TROW,0.064996,104.73,1.19,(1.15%) +280,Keysight Technologies In,KEYS,0.064934,133.51,3.11,(2.38%) +281,Zimmer Biomet Holdings Inc,ZBH,0.064476,112.14,1.38,(1.24%) +282,Dollar General Corp,DG,0.064042,105.04,0.70,(0.67%) +283,Corning Inc,GLW,0.064037,30.35,0.29,(0.98%) +284,Ebay Inc,EBAY,0.063728,43.45,0.56,(1.31%) +285,Cbre Group Inc A,CBRE,0.063578,73.84,0.35,(0.48%) +286,Weyerhaeuser Co,WY,0.063312,30.40,-0.64,(-2.05%) +287,Church & Dwight Co Inc,CHD,0.062765,91.68,-0.29,(-0.31%) +288,Cardinal Health Inc,CAH,0.062583,88.18,0.14,(0.16%) +289,Hp Inc,HPQ,0.062013,25.73,0.09,(0.36%) +290,Equifax Inc,EFX,0.061821,185.40,4.68,(2.59%) +291,Tractor Supply Company,TSCO,0.061683,205.07,2.06,(1.02%) +292,Willis Towers Watson Plc,WTW,0.06168,211.96,0.35,(0.17%) +293,Hewlett Packard Enterprise,HPE,0.061499,17.70,0.63,(3.66%) +294,Fair Isaac Corp,FICO,0.061481,890.19,4.58,(0.52%) +295,Dollar Tree Inc,DLTR,0.061287,106.46,1.11,(1.05%) +296,Hartford Financial Svcs Grp,HIG,0.061236,72.24,0.53,(0.73%) +297,Resmed Inc,RMD,0.060853,149.81,1.16,(0.78%) +298,Take Two Interactive Softwre,TTWO,0.060753,139.63,1.85,(1.34%) +299,Royal Caribbean Cruises Ltd,RCL,0.060515,94.38,2.42,(2.64%) +300,Xylem Inc,XYL,0.060192,91.40,1.09,(1.20%) +301,Align Technology Inc,ALGN,0.060095,309.06,7.38,(2.45%) +302,Steris Plc,STE,0.060013,221.57,3.67,(1.69%) +303,Broadridge Financial Solutio,BR,0.059572,181.18,-0.04,(-0.02%) +304,Discover Financial Services,DFS,0.059507,86.35,1.09,(1.28%) +305,State Street Corp,STT,0.059411,67.81,1.03,(1.54%) +306,Sba Communications Corp,SBAC,0.059216,198.11,2.05,(1.05%) +307,Monolithic Power Systems Inc,MPWR,0.058699,457.69,16.51,(3.74%) +308,Illumina Inc,ILMN,0.058584,133.02,-0.28,(-0.21%) +309,Dte Energy Company,DTE,0.058057,98.82,-1.72,(-1.71%) +310,M & T Bank Corp,MTB,0.057846,126.63,2.09,(1.68%) +311,Coterra Energy Inc,CTRA,0.057589,27.48,0.34,(1.23%) +312,Eversource Energy,ES,0.057281,57.54,-1.23,(-2.09%) +313,Genuine Parts Co,GPC,0.055796,142.94,0.63,(0.45%) +314,Equity Residential,EQR,0.055712,58.67,0.23,(0.40%) +315,Entergy Corp,ETR,0.055241,92.23,-1.02,(-1.10%) +316,Dover Corp,DOV,0.055103,141.68,1.09,(0.77%) +317,Ameren Corporation,AEE,0.054981,74.60,-1.51,(-1.98%) +318,Ulta Beauty Inc,ULTA,0.054585,398.17,5.73,(1.46%) +319,Teledyne Technologies Inc,TDY,0.054228,414.36,0.92,(0.22%) +320,Nvr Inc,NVR,0.054188,6,027.42,86.42,(1.45%) +321,Targa Resources Corp,TRGP,0.05408,87.52,0.93,(1.08%) +322,Molina Healthcare Inc,MOH,0.053618,333.09,2.03,(0.61%) +323,Wabtec Corp,WAB,0.053425,108.18,1.35,(1.26%) +324,Fleetcor Technologies Inc,FLT,0.053345,260.14,1.28,(0.49%) +325,Albemarle Corp,ALB,0.053229,172.99,10.36,(6.37%) +326,Baxter International Inc,BAX,0.05272,37.54,0.21,(0.57%) +327,Raymond James Financial Inc,RJF,0.052247,101.39,1.84,(1.85%) +328,Mccormick & Co Non Vtg Shrs,MKC,0.051871,74.55,0.46,(0.63%) +329,Invitation Homes Inc,INVH,0.051153,31.73,0.03,(0.11%) +330,Firstenergy Corp,FE,0.050777,34.56,-0.69,(-1.94%) +331,Laboratory Crp of Amer Hldgs,LH,0.05072,204.23,-0.62,(-0.30%) +332,Howmet Aerospace Inc,HWM,0.050386,46.99,0.91,(1.97%) +333,Verisign Inc,VRSN,0.050215,202.93,2.46,(1.23%) +334,Ppl Corp,PPL,0.04902,23.56,-0.39,(-1.61%) +335,Iron Mountain Inc,IRM,0.047985,59.55,0.56,(0.94%) +336,Jacobs Solutions Inc,J,0.047882,136.45,0.27,(0.20%) +337,Intl Flavors & Fragrances,IFF,0.047868,67.84,0.64,(0.95%) +338,Centerpoint Energy Inc,CNP,0.047827,26.68,-0.48,(-1.75%) +339,Darden Restaurants Inc,DRI,0.047736,143.44,2.01,(1.42%) +340,Hologic Inc,HOLX,0.047358,69.99,0.75,(1.08%) +341,First Solar Inc,FSLR,0.047324,161.90,3.29,(2.07%) +342,Expeditors Intl Wash Inc,EXPD,0.046936,114.74,1.08,(0.95%) +343,Brown & Brown Inc,BRO,0.046675,71.15,0.19,(0.26%) +344,Factset Research Systems Inc,FDS,0.046601,443.66,8.47,(1.95%) +345,Fifth Third Bancorp,FITB,0.046598,25.05,0.19,(0.74%) +346,Ventas Inc,VTR,0.046351,42.18,0.77,(1.85%) +347,Marathon Oil Corp,MRO,0.046321,27.51,0.12,(0.45%) +348,Steel Dynamics Inc,STLD,0.046096,106.96,0.52,(0.48%) +349,Bunge Ltd,BG,0.046038,110.67,1.10,(1.00%) +350,Ptc Inc,PTC,0.045951,140.25,1.78,(1.28%) +351,Everest Group Ltd,EG,0.04587,381.30,-0.42,(-0.11%) +352,Cincinnati Financial Corp,CINF,0.045852,104.40,-0.30,(-0.29%) +353,Enphase Energy Inc,ENPH,0.045679,121.39,1.28,(1.07%) +354,Nasdaq Inc,NDAQ,0.04562,49.07,0.74,(1.52%) +355,Akamai Technologies Inc,AKAM,0.045578,107.75,0.79,(0.74%) +356,Cboe Global Markets Inc,CBOE,0.045525,155.28,0.51,(0.33%) +357,Cf Industries Holdings Inc,CF,0.04496,84.93,1.48,(1.78%) +358,Waters Corp,WAT,0.044936,274.46,2.12,(0.78%) +359,Pultegroup Inc,PHM,0.044848,74.85,1.65,(2.25%) +360,Tyler Technologies Inc,TYL,0.044831,387.74,2.43,(0.63%) +361,Principal Financial Group,PFG,0.044705,72.74,0.74,(1.02%) +362,Clorox Company,CLX,0.04465,128.98,-0.17,(-0.13%) +363,Southwest Airlines Co,LUV,0.044478,27.30,0.49,(1.81%) +364,Regions Financial Corp,RF,0.044397,17.16,0.22,(1.27%) +365,Garmin Ltd,GRMN,0.044336,104.65,1.42,(1.38%) +366,Atmos Energy Corp,ATO,0.044128,106.32,-0.14,(-0.13%) +367,Netapp Inc,NTAP,0.044116,76.49,1.54,(2.05%) +368,Textron Inc,TXT,0.043659,79.27,0.33,(0.41%) +369,Cooper Cos Inc,COO,0.04361,318.15,2.23,(0.71%) +370,Kellogg Co,K,0.043517,58.90,-0.22,(-0.38%) +371,Idex Corp,IEX,0.043484,210.72,3.28,(1.58%) +372,Cms Energy Corp,CMS,0.043476,52.78,-0.57,(-1.07%) +373,Skyworks Solutions Inc,SWKS,0.042974,98.55,2.25,(2.33%) +374,Alexandria Real Estate Equit,ARE,0.042466,99.48,0.59,(0.60%) +375,Hunt (Jb) Transprt Svcs Inc,JBHT,0.042319,186.54,0.95,(0.51%) +376,Las Vegas Sands Corp,LVS,0.042222,45.63,-0.53,(-1.15%) +377,Ball Corp,BALL,0.042202,48.27,0.04,(0.08%) +378,Walgreens Boots Alliance Inc,WBA,0.04215,20.97,-0.06,(-0.26%) +379,Teradyne Inc,TER,0.041494,100.04,3.56,(3.68%) +380,Mid America Apartment Comm,MAA,0.041385,128.55,0.48,(0.37%) +381,Epam Systems Inc,EPAM,0.041006,257.76,0.96,(0.37%) +382,Avery Dennison Corp,AVY,0.040961,183.42,1.39,(0.76%) +383,Omnicom Group,OMC,0.040483,74.60,1.21,(1.65%) +384,Huntington Bancshares Inc,HBAN,0.040444,10.25,0.13,(1.24%) +385,Eqt Corp,EQT,0.040251,40.00,0.39,(0.99%) +386,Tyson Foods Inc Cl A,TSN,0.040042,49.74,-0.37,(-0.74%) +387,Western Digital Corp,WDC,0.039814,45.50,0.94,(2.11%) +388,Northern Trust Corp,NTRS,0.039557,69.29,0.84,(1.23%) +389,Carnival Corp,CCL,0.039206,14.52,0.54,(3.88%) +390,Expedia Group Inc,EXPE,0.038585,102.75,2.50,(2.49%) +391,United Airlines Holdings Inc,UAL,0.038383,42.84,0.81,(1.94%) +392,Quest Diagnostics Inc,DGX,0.038331,123.71,0.31,(0.25%) +393,Axon Enterprise Inc,AXON,0.03828,195.69,1.04,(0.53%) +394,Packaging Corp of America,PKG,0.038236,152.79,0.90,(0.59%) +395,Revvity Inc,RVTY,0.0378,110.55,1.50,(1.38%) +396,Snap on Inc,SNA,0.037774,257.97,4.00,(1.58%) +397,Pool Corp,POOL,0.037475,353.56,9.86,(2.87%) +398,Essex Property Trust Inc,ESS,0.03733,210.81,0.07,(0.03%) +399,Domino S Pizza Inc,DPZ,0.037258,382.39,2.18,(0.57%) +400,Amcor Plc,AMCR,0.037101,9.09,0.06,(0.61%) +401,Best Buy Co Inc,BBY,0.037071,68.66,0.29,(0.43%) +402,Apa Corp,APA,0.03662,42.49,0.11,(0.27%) +403,Lamb Weston Holdings Inc,LW,0.036553,91.47,0.09,(0.10%) +404,Wr Berkley Corp,WRB,0.036551,65.21,0.86,(1.33%) +405,Conagra Brands Inc,CAG,0.036484,27.44,-0.10,(-0.35%) +406,Lkq Corp,LKQ,0.036449,49.51,0.72,(1.47%) +407,Jm Smucker Co,SJM,0.035767,124.48,-0.96,(-0.77%) +408,Stanley Black & Decker Inc,SWK,0.035567,83.87,0.97,(1.17%) +409,Synchrony Financial,SYF,0.035241,30.73,0.55,(1.84%) +410,Carmax Inc,KMX,0.035202,71.64,-8.05,(-10.10%) +411,Leidos Holdings Inc,LDOS,0.034943,92.20,0.21,(0.23%) +412,Seagate Technology Holdings,STX,0.034918,65.71,1.42,(2.20%) +413,Paycom Software Inc,PAYC,0.034914,259.42,2.34,(0.91%) +414,Celanese Corp,CE,0.034212,127.11,3.81,(3.09%) +415,Trimble Inc,TRMB,0.034136,52.22,3.01,(6.12%) +416,Alliant Energy Corp,LNT,0.03411,48.06,-0.49,(-1.02%) +417,Citizens Financial Group,CFG,0.033972,26.28,0.52,(2.00%) +418,International Paper Co,IP,0.033961,35.10,0.15,(0.41%) +419,Masco Corp,MAS,0.033414,54.58,1.37,(2.58%) +420,Nordson Corp,NDSN,0.033057,223.81,3.73,(1.69%) +421,Loews Corp,L,0.032942,64.16,0.36,(0.56%) +422,Evergy Inc,EVRG,0.032792,50.48,-0.58,(-1.13%) +423,Mosaic Co,MOS,0.03266,35.63,0.43,(1.22%) +424,Molson Coors Beverage Co B,TAP,0.032624,62.29,-0.12,(-0.19%) +425,Zebra Technologies Corp Cl A,ZBRA,0.03257,236.95,11.04,(4.89%) +426,Viatris Inc,VTRS,0.032115,9.80,0.21,(2.14%) +427,Live Nation Entertainment In,LYV,0.032035,83.73,2.71,(3.34%) +428,Host Hotels & Resorts Inc,HST,0.03173,16.38,0.34,(2.11%) +429,Insulet Corp,PODD,0.031493,166.19,4.49,(2.77%) +430,Match Group Inc,MTCH,0.031269,40.07,0.09,(0.23%) +431,Interpublic Group of Cos Inc,IPG,0.031184,29.12,0.39,(1.35%) +432,Hormel Foods Corp,HRL,0.030887,38.15,-0.19,(-0.48%) +433,Incyte Corp,INCY,0.03067,58.93,-0.18,(-0.30%) +434,Udr Inc,UDR,0.030252,35.67,0.28,(0.79%) +435,Jack Henry & Associates Inc,JKHY,0.030211,150.61,2.17,(1.46%) +436,Kimco Realty Corp,KIM,0.029862,17.73,0.24,(1.34%) +437,Aes Corp,AES,0.029775,15.29,-0.65,(-4.05%) +438,Bio Techne Corp,TECH,0.029742,67.67,0.10,(0.15%) +439,Pentair Plc,PNR,0.029592,65.59,0.87,(1.34%) +440,Rollins Inc,ROL,0.029276,37.64,0.20,(0.52%) +441,Mgm Resorts International,MGM,0.028601,36.76,0.27,(0.74%) +442,Ceridian Hcm Holding Inc,CDAY,0.028589,67.38,0.35,(0.52%) +443,Brown Forman Corp Class B,BF.B,0.028542,56.78,0.14,(0.25%) +444,Nisource Inc,NI,0.028365,24.84,-0.35,(-1.37%) +445,Gen Digital Inc,GEN,0.028124,17.83,-0.07,(-0.37%) +446,C.H. Robinson Worldwide Inc,CHRW,0.028048,86.31,0.02,(0.02%) +447,Camden Property Trust,CPT,0.027984,94.56,-0.32,(-0.34%) +448,Charles River Laboratories,CRL,0.027736,195.69,0.75,(0.38%) +449,Healthpeak Properties Inc,PEAK,0.027617,18.15,0.10,(0.58%) +450,Caesars Entertainment Inc,CZR,0.027217,47.52,1.54,(3.36%) +451,Regency Centers Corp,REG,0.027131,59.98,0.85,(1.43%) +452,Keycorp,KEY,0.0269,10.52,0.22,(2.09%) +453,Henry Schein Inc,HSIC,0.026715,73.88,0.62,(0.84%) +454,Globe Life Inc,GL,0.026574,110.04,0.61,(0.56%) +455,Borgwarner Inc,BWA,0.026416,40.72,0.92,(2.31%) +456,F5 Inc,FFIV,0.026087,160.33,2.74,(1.74%) +457,Qorvo Inc,QRVO,0.025849,96.39,1.83,(1.93%) +458,Teleflex Inc,TFX,0.025796,199.42,2.91,(1.48%) +459,Allegion Plc,ALLE,0.025545,104.97,1.68,(1.63%) +460,Westrock Co,WRK,0.025292,35.98,0.23,(0.64%) +461,Eastman Chemical Co,EMN,0.025003,76.99,2.35,(3.15%) +462,Wynn Resorts Ltd,WYNN,0.024938,92.21,-0.00,(-0.00%) +463,Nrg Energy Inc,NRG,0.024681,38.55,-0.03,(-0.08%) +464,Juniper Networks Inc,JNPR,0.024655,27.92,0.42,(1.52%) +465,Pinnacle West Capital,PNW,0.02321,73.45,-0.71,(-0.95%) +466,Hasbro Inc,HAS,0.02308,65.23,0.24,(0.37%) +467,Catalent Inc,CTLT,0.023069,45.85,0.07,(0.15%) +468,American Airlines Group Inc,AAL,0.02296,13.00,0.39,(3.05%) +469,Fmc Corp,FMC,0.022942,66.03,0.02,(0.03%) +470,Campbell Soup Co,CPB,0.022776,41.15,-0.31,(-0.76%) +471,Smith (a.O.) Corp,AOS,0.022769,66.99,1.54,(2.35%) +472,Boston Properties Inc,BXP,0.02269,59.66,0.79,(1.34%) +473,Huntington Ingalls Industrie,HII,0.022502,204.41,1.22,(0.60%) +474,Fox Corp Class A,FOXA,0.021902,31.47,0.55,(1.76%) +475,Robert Half Inc,RHI,0.021872,73.78,0.64,(0.87%) +476,Assurant Inc,AIZ,0.021618,145.63,-0.13,(-0.09%) +477,Universal Health Services B,UHS,0.021551,127.00,2.80,(2.25%) +478,Etsy Inc,ETSY,0.021257,63.53,1.64,(2.65%) +479,Marketaxess Holdings Inc,MKTX,0.021132,205.89,4.11,(2.04%) +480,News Corp Class A,NWSA,0.0209,20.00,0.35,(1.76%) +481,Bio Rad Laboratories A,BIO,0.020548,356.10,3.63,(1.03%) +482,Bath & Body Works Inc,BBWI,0.020412,33.19,1.04,(3.23%) +483,Dentsply Sirona Inc,XRAY,0.020079,33.94,-0.17,(-0.51%) +484,Solaredge Technologies Inc,SEDG,0.020026,132.35,4.34,(3.39%) +485,Whirlpool Corp,WHR,0.019812,131.66,1.03,(0.79%) +486,Franklin Resources Inc,BEN,0.019128,24.55,0.28,(1.14%) +487,Generac Holdings Inc,GNRC,0.018958,110.73,1.84,(1.69%) +488,Norwegian Cruise Line Holdin,NCLH,0.018812,16.96,0.67,(4.08%) +489,Federal Realty Invs Trust,FRT,0.018475,91.45,1.05,(1.16%) +490,Tapestry Inc,TPR,0.01809,28.18,0.23,(0.84%) +491,Invesco Ltd,IVZ,0.017835,14.31,0.22,(1.53%) +492,Paramount Global Class B,PARA,0.017208,12.89,0.11,(0.89%) +493,Vf Corp,VFC,0.015631,16.59,-0.32,(-1.89%) +494,Comerica Inc,CMA,0.014638,41.07,1.12,(2.81%) +495,Davita Inc,DVA,0.014395,95.75,0.01,(0.01%) +496,Zions Bancorp Na,ZION,0.013918,33.96,0.79,(2.39%) +497,Ralph Lauren Corp,RL,0.013127,115.52,0.26,(0.22%) +498,Sealed Air Corp,SEE,0.012794,32.20,0.46,(1.46%) +499,Alaska Air Group Inc,ALK,0.012574,37.04,0.63,(1.73%) +500,Mohawk Industries Inc,MHK,0.011998,85.15,1.23,(1.46%) +501,Organon & Co,OGN,0.011665,16.93,0.34,(2.02%) +502,Dxc Technology Co,DXC,0.011658,20.64,0.29,(1.40%) +503,Fox Corp Class B,FOX,0.010545,29.16,0.53,(1.85%) +504,News Corp Class B,NWS,0.006575,20.75,0.36,(1.74%) +*/ diff --git a/vuu-ui/showcase/src/examples/utils/createArrayDataSource.ts b/vuu-ui/packages/vuu-data-test/src/createArrayDataSource.ts similarity index 100% rename from vuu-ui/showcase/src/examples/utils/createArrayDataSource.ts rename to vuu-ui/packages/vuu-data-test/src/createArrayDataSource.ts index fce1c3185..f095db3f7 100644 --- a/vuu-ui/showcase/src/examples/utils/createArrayDataSource.ts +++ b/vuu-ui/packages/vuu-data-test/src/createArrayDataSource.ts @@ -1,6 +1,6 @@ -import { TickingArrayDataSource } from "./TickingArrayDataSource"; import { getColumnAndRowGenerator, populateArray } from "@finos/vuu-data-test"; import { VuuTable } from "@finos/vuu-protocol-types"; +import { TickingArrayDataSource } from "./TickingArrayDataSource"; export const createArrayDataSource = ({ count = 1000, diff --git a/vuu-ui/packages/vuu-data-test/src/index.ts b/vuu-ui/packages/vuu-data-test/src/index.ts index da4cd3769..a702e7142 100644 --- a/vuu-ui/packages/vuu-data-test/src/index.ts +++ b/vuu-ui/packages/vuu-data-test/src/index.ts @@ -1,2 +1,7 @@ +export * from "./createArrayDataSource"; export * from "./schemas"; +export * from "./TickingArrayDataSource"; export * from "./vuu-row-generator"; +export * from "./vuu-modules"; +export { type BasketsTableName } from "./basket/basket-schemas"; +export { type SimulTableName } from "./simul/simul-schemas"; diff --git a/vuu-ui/packages/vuu-data-test/src/rowUpdates.ts b/vuu-ui/packages/vuu-data-test/src/rowUpdates.ts index d430b0e86..0712ed9fa 100644 --- a/vuu-ui/packages/vuu-data-test/src/rowUpdates.ts +++ b/vuu-ui/packages/vuu-data-test/src/rowUpdates.ts @@ -1,47 +1,33 @@ import { VuuRange, VuuRowDataItemType } from "@finos/vuu-protocol-types"; import { ArrayDataSource } from "@finos/vuu-data"; +export type UpdateHandler = ( + updates: (RowUpdates | RowInsert | RowDelete)[] +) => void; + export interface UpdateGenerator { setDataSource: (dataSource: ArrayDataSource) => void; setRange: (range: VuuRange) => void; setUpdateHandler: (updateHandler: UpdateHandler) => void; } -export type UpdateHandler = (updates: RowUpdates[]) => void; +export type UpdateType = "I" | "D" | "U"; + +// Allow up to 20 updates https://catchts.com/even-length +type MAXIMUM_ALLOWED_BOUNDARY = 20; +type RepeatingTuple< + Tuple extends Array, + Result extends Array = [], + Count extends ReadonlyArray = [] +> = Count["length"] extends MAXIMUM_ALLOWED_BOUNDARY + ? Result + : Tuple extends [] + ? [] + : Result extends [] + ? RepeatingTuple + : RepeatingTuple; -export type RowUpdates = - | [number, number, VuuRowDataItemType] - | [number, number, VuuRowDataItemType, number, VuuRowDataItemType] - | [ - number, - number, - VuuRowDataItemType, - number, - VuuRowDataItemType, - number, - VuuRowDataItemType - ] - | [ - number, - number, - VuuRowDataItemType, - number, - VuuRowDataItemType, - number, - VuuRowDataItemType, - number, - VuuRowDataItemType - ] - | [ - number, - number, - VuuRowDataItemType, - number, - VuuRowDataItemType, - number, - VuuRowDataItemType, - number, - VuuRowDataItemType, - number, - VuuRowDataItemType - ]; +type UpdatePairs = RepeatingTuple<[number, VuuRowDataItemType]>; +export type RowUpdates = ["U", number, ...UpdatePairs]; +export type RowInsert = ["I", ...VuuRowDataItemType[]]; +export type RowDelete = ["D", string]; diff --git a/vuu-ui/packages/vuu-data-test/src/simul/OrderUpdateGenerator.ts b/vuu-ui/packages/vuu-data-test/src/simul/OrderUpdateGenerator.ts new file mode 100644 index 000000000..5dfa16b9d --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/simul/OrderUpdateGenerator.ts @@ -0,0 +1,150 @@ +import { ArrayDataSource } from "@finos/vuu-data"; +import { VuuRange, VuuRowDataItemType } from "@finos/vuu-protocol-types"; +import { buildColumnMap, ColumnMap } from "@finos/vuu-utils"; +import type { + RowDelete, + RowInsert, + RowUpdates, + UpdateGenerator, + UpdateHandler, +} from "../rowUpdates"; +import { random } from "./reference-data"; +import { metadataKeys } from "@finos/vuu-utils"; + +const getNewValue = (value: number) => { + const multiplier = random(0, 100) / 1000; + const direction = random(0, 10) >= 5 ? 1 : -1; + return value + value * multiplier * direction; +}; + +let _orderId = 1; +const orderId = () => `0000000${_orderId++}`.slice(-3); +const createOrder = (): ["I", ...VuuRowDataItemType[]] => { + const createTime = Date.now(); + return [ + "I", + "EUR", + createTime, + 0, + createTime, + orderId(), + 1000, + "AAPL.L", + "buy", + "trader-x", + ]; +}; + +type OrderPhase = "create-order" | "fill-order" | "remove-order"; + +export class OrderUpdateGenerator implements UpdateGenerator { + private dataSource: ArrayDataSource | undefined; + private range: VuuRange | undefined; + private updateHandler: UpdateHandler | undefined; + private updating = false; + private timer: number | undefined; + private phase: OrderPhase = "create-order"; + private orderCount: number; + private columnMap: ColumnMap; + + constructor(orderCount = 20) { + this.orderCount = orderCount; + } + + setRange(range: VuuRange) { + this.range = range; + if (!this.updating && this.updateHandler) { + this.startUpdating(); + } + } + + setDataSource(dataSource: ArrayDataSource) { + this.dataSource = dataSource; + this.columnMap = buildColumnMap(dataSource.columns); + } + + setUpdateHandler(updateHandler: UpdateHandler) { + this.updateHandler = updateHandler; + if (!this.updating && this.range) { + this.startUpdating(); + } + } + + private startUpdating() { + this.updating = true; + this.update(); + } + + private stopUpdating() { + this.updating = false; + if (this.timer) { + window.clearTimeout(this.timer); + this.timer = undefined; + } + } + + update = () => { + const updates: (RowUpdates | RowInsert | RowDelete)[] = []; + + switch (this.phase) { + case "create-order": { + updates.push(createOrder()); + + const data = this.dataSource?.data; + if (data && data.length >= this.orderCount) { + console.log("phase >>> fill"); + this.phase = "fill-order"; + } + + break; + } + + case "fill-order": { + console.log("fill-order"); + const data = this.dataSource?.data; + let filledCount = 0; + if (data) { + const count = data.length; + const { IDX } = metadataKeys; + const { filledQuantity: filledKey, quantity: qtyKey } = + this.columnMap; + for (const order of data) { + const { + [IDX]: rowIdx, + [filledKey]: filledQty, + [qtyKey]: quantity, + } = order; + if (filledQty < quantity) { + const newFilledQty = Math.min( + quantity, + Math.max(100, filledQty * 1.1) + ); + updates.push(["U", rowIdx, filledKey, newFilledQty]); + } else { + filledCount += 1; + // schedule for delete + } + } + if (filledCount === count) { + console.log(">>> remove phase "); + this.phase = "remove-order"; + } + } + + break; + } + + case "remove-order": { + break; + } + } + + if (updates.length > 0) { + this.updateHandler?.(updates); + } + + if (this.updating) { + this.timer = window.setTimeout(this.update, 50); + } + }; +} 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 380281e81..cedad6b0a 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 @@ -69,7 +69,7 @@ for (const char of chars) { currency, description, exchange, - isin, + String(isin), lotSize, ric, price, 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 751df63d6..414ab85c1 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,4 +1,5 @@ -import { InstrumentReferenceData, random } from "."; +import InstrumentReferenceData from "./instruments"; +import { random } from "./utils"; export type ask = number; export type askSize = number; 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 new file mode 100644 index 000000000..eca7f5a2f --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/simul/simul-module.ts @@ -0,0 +1,74 @@ +import { VuuDataRow, VuuRowDataItemType } from "packages/vuu-protocol-types"; +import { buildColumnMap } from "@finos/vuu-utils"; +import { UpdateGenerator } from "../rowUpdates"; +import { TickingArrayDataSource } from "../TickingArrayDataSource"; +import { VuuModule } from "../vuu-modules"; +import instruments from "./reference-data/instruments"; +import prices from "./reference-data/prices"; +import { schemas, SimulTableName } from "./simul-schemas"; +import { BaseUpdateGenerator } from "../UpdateGenerator"; +import { OrderUpdateGenerator } from "./OrderUpdateGenerator"; + +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 tables: Record = { + childOrders, + instruments, + instrumentPrices, + orders, + parentOrders, + prices, +}; + +const updates: Record = { + childOrders: undefined, + instruments: undefined, + instrumentPrices: undefined, + orders: orderUpdateGenerator, + parentOrders: undefined, + prices: pricesUpdateGenerator, +}; + +export const populateArray = (tableName: SimulTableName, count: number) => { + const table = tables[tableName]; + const data: Array = []; + for (let i = 0; i < count; i++) { + if (i >= table.length) { + break; + } + data[i] = table[i]; + } + return data; +}; + +const getColumnDescriptors = (tableName: SimulTableName) => { + const schema = schemas[tableName]; + return schema.columns; +}; + +const createDataSource = (tableName: SimulTableName) => { + const columnDescriptors = getColumnDescriptors(tableName); + const dataArray = populateArray(tableName, 10_000); + return new TickingArrayDataSource({ + columnDescriptors, + data: dataArray, + // menu: menus[tableName], + // rpcServices: services[tableName], + updateGenerator: updates[tableName], + }); +}; + +const simulModule: VuuModule = { + createDataSource, +}; + +export default simulModule; diff --git a/vuu-ui/packages/vuu-data-test/src/vuu-modules.ts b/vuu-ui/packages/vuu-data-test/src/vuu-modules.ts new file mode 100644 index 000000000..06bffa98f --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/vuu-modules.ts @@ -0,0 +1,23 @@ +import { DataSource } from "@finos/vuu-data"; +import basketModule from "./basket/basket-module"; +import { BasketsTableName } from "./basket/basket-schemas"; +import simulModule from "./simul/simul-module"; +import { SimulTableName } from "./simul/simul-schemas"; + +export type VuuModuleName = "BASKET" | "SIMUL"; + +export interface VuuModule { + createDataSource: (tableName: T) => DataSource; +} + +const vuuModules: Record< + VuuModuleName, + VuuModule | VuuModule +> = { + BASKET: basketModule, + SIMUL: simulModule, +}; + +export const vuuModule = ( + moduleName: VuuModuleName +) => vuuModules[moduleName] as VuuModule; 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 82262beec..7012f9505 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 @@ -8,6 +8,7 @@ import { VuuAggregation, VuuColumnDataType, VuuGroupBy, + VuuMenu, VuuRange, VuuRowDataItemType, VuuSort, @@ -33,6 +34,7 @@ import { DataSourceConfig, DataSourceConstructorProps, DataSourceEvents, + DataSourceStatus, groupByChanged, hasFilter, hasGroupBy, @@ -57,35 +59,29 @@ export interface ArrayDataSourceConstructorProps extends Omit { columnDescriptors: ColumnDescriptor[]; data: Array; + keyColumn?: string; rangeChangeRowset?: "delta" | "full"; } const { debug } = logger("ArrayDataSource"); const { RENDER_IDX, SELECTED } = metadataKeys; -const toDataSourceRow = ( - data: VuuRowDataItemType[], - index: number -): DataSourceRow => [ - index, - index, - true, - false, - 1, - 0, - data[0].toString(), - 0, - ...data, -]; - -const buildTableSchema = (columns: ColumnDescriptor[]): TableSchema => { +const toDataSourceRow = + (key: number) => + (data: VuuRowDataItemType[], index: number): DataSourceRow => { + return [index, index, true, false, 1, 0, data[key].toString(), 0, ...data]; + }; + +const buildTableSchema = ( + columns: ColumnDescriptor[], + keyColumn?: string +): TableSchema => { const schema: TableSchema = { columns: columns.map(({ name, serverDataType = "string" }) => ({ name, serverDataType, })), - // how do we identify the key field ? - key: columns[0].name, + key: keyColumn ?? columns[0].name, table: { module: "", table: "Array" }, }; @@ -110,26 +106,30 @@ export class ArrayDataSource { private clientCallback: SubscribeCallback | undefined; private columnDescriptors: ColumnDescriptor[]; - private status = "initialising"; private disabled = false; private groupedData: undefined | DataSourceRow[]; private groupMap: undefined | GroupMap; - private selectedRows: Selection = []; + /** the index of key field within raw data row */ + private key: number; private suspended = false; private tableSchema: TableSchema; private lastRangeServed: VuuRange = { from: 0, to: 0 }; private rangeChangeRowset: "delta" | "full"; private openTreeNodes: string[] = []; - #columns: string[] = []; #columnMap: ColumnMap; #config: WithFullConfig = vanillaConfig; #data: readonly DataSourceRow[]; + #links: LinkDescriptorWithLabel[] | undefined; #range: VuuRange = NULL_RANGE; #selectedRowsCount = 0; #size = 0; + #status: DataSourceStatus = "initialising"; #title: string | undefined; + protected _menu: VuuMenu | undefined; + protected selectedRows: Selection = []; + public viewport: string; private keys = new KeySet(this.#range); @@ -142,6 +142,7 @@ export class ArrayDataSource data, filter, groupBy, + keyColumn, rangeChangeRowset = "delta", sort, title, @@ -149,10 +150,6 @@ export class ArrayDataSource }: ArrayDataSourceConstructorProps) { super(); - console.log(`ArrayDataSource`, { - columnDescriptors, - }); - if (!data || !columnDescriptors) { throw Error( "ArrayDataSource constructor called without data or without columnDescriptors" @@ -160,22 +157,26 @@ export class ArrayDataSource } this.columnDescriptors = columnDescriptors; - this.#columns = columnDescriptors.map((column) => column.name); - this.#columnMap = buildColumnMap(this.#columns); + this.key = keyColumn + ? this.columnDescriptors.findIndex((col) => col.name === keyColumn) + : 0; this.rangeChangeRowset = rangeChangeRowset; - this.tableSchema = buildTableSchema(columnDescriptors); - - this.#data = data.map(toDataSourceRow); + this.tableSchema = buildTableSchema(columnDescriptors, keyColumn); this.viewport = viewport || uuid(); this.#size = data.length; this.#title = title; + const columns = columnDescriptors.map((col) => col.name); + + this.#columnMap = buildColumnMap(columns); + this.#data = data.map(toDataSourceRow(this.key)); + this.config = { ...this.#config, aggregations: aggregations || this.#config.aggregations, - columns: columnDescriptors.map((col) => col.name), + columns, filter: filter || this.#config.filter, groupBy: groupBy || this.#config.groupBy, sort: sort || this.#config.sort, @@ -198,7 +199,7 @@ export class ArrayDataSource ) { this.clientCallback = callback; this.viewport = viewport; - this.status = "subscribed"; + this.#status = "subscribed"; this.lastRangeServed = { from: 0, to: 0 }; let config = this.#config; @@ -270,6 +271,7 @@ export class ArrayDataSource } select(selected: Selection) { + this.#selectedRowsCount = selected.length; debug?.(`select ${JSON.stringify(selected)}`); this.selectedRows = selected; this.setRange(resetRange(this.#range), true); @@ -296,6 +298,18 @@ export class ArrayDataSource } } + get links() { + return this.#links; + } + + get menu() { + return this._menu; + } + + get status() { + return this.#status; + } + get data() { return this.#data; } @@ -405,7 +419,8 @@ export class ArrayDataSource } get size() { - return this.#size; + // return this.#size; + return this.processedData?.length ?? this.#data.length; } get range() { @@ -418,6 +433,21 @@ export class ArrayDataSource } } + protected delete(row: VuuRowDataItemType[]) { + console.log(`delete row ${row.join(",")}`); + } + + protected insert = (row: VuuRowDataItemType[]) => { + // TODO take sorting, filtering. grouping into account + const dataSourceRow = toDataSourceRow(this.key)(row, this.size); + (this.#data as DataSourceRow[]).push(dataSourceRow); + const { from, to } = this.#range; + const [rowIdx] = dataSourceRow; + if (rowIdx >= from && rowIdx < to) { + this.sendRowsToClient(); + } + }; + private setRange(range: VuuRange, forceFullRefresh = false) { this.#range = range; this.keys.reset(range); @@ -442,7 +472,13 @@ export class ArrayDataSource size: data.length, type: "viewport-update", }); - this.lastRangeServed = this.#range; + this.lastRangeServed = { + from: this.#range.from, + to: Math.min( + this.#range.to, + this.#range.from + rowsWithinViewport.length + ), + }; } get columns() { @@ -461,6 +497,10 @@ export class ArrayDataSource columnsWithoutDescriptors, }); } + this.#columnMap = buildColumnMap(columns); + console.log({ + columnMap: this.#columnMap, + }); this.config = { ...this.#config, columns, @@ -498,9 +538,6 @@ export class ArrayDataSource } set sort(sort: VuuSort) { - console.log(`set sort`, { - sort, - }); debug?.(`sort ${JSON.stringify(sort)}`); this.config = { ...this.#config, @@ -577,9 +614,13 @@ export class ArrayDataSource console.log({ row, colName, value }); } - applyEdit(row: DataSourceRow, columnName: string, value: VuuColumnDataType) { + applyEdit( + row: DataSourceRow, + columnName: string, + value: VuuRowDataItemType + ): Promise { console.log(`ArrayDataSource applyEdit ${row[0]} ${columnName} ${value}`); - return true; + return Promise.resolve(true); } async menuRpcCall( diff --git a/vuu-ui/packages/vuu-data/src/connection-manager.ts b/vuu-ui/packages/vuu-data/src/connection-manager.ts index 086cc3763..33ed5c37d 100644 --- a/vuu-ui/packages/vuu-data/src/connection-manager.ts +++ b/vuu-ui/packages/vuu-data/src/connection-manager.ts @@ -235,6 +235,12 @@ export interface ServerAPI { const connectedServerAPI: ServerAPI = { subscribe: (message, callback) => { + if (viewports.get(message.viewport)) { + throw Error( + `ConnectionManager attempting to subscribe with an existing viewport id` + ); + } + // TODO we never use this status viewports.set(message.viewport, { status: "subscribing", request: message, @@ -262,7 +268,7 @@ const connectedServerAPI: ServerAPI = { ) => asyncRequest(message), getTableList: async () => - asyncRequest({ type: Message.GET_TABLE_LIST }), + asyncRequest({ type: "GET_TABLE_LIST" }), getTableSchema: async (table) => asyncRequest({ diff --git a/vuu-ui/packages/vuu-data/src/data-source.ts b/vuu-ui/packages/vuu-data/src/data-source.ts index 6e5b083d4..1a5b6ff1a 100644 --- a/vuu-ui/packages/vuu-data/src/data-source.ts +++ b/vuu-ui/packages/vuu-data/src/data-source.ts @@ -15,6 +15,7 @@ import { VuuLinkDescriptor, VuuMenu, VuuRange, + VuuRowDataItemType, VuuSort, VuuTable, } from "@finos/vuu-protocol-types"; @@ -474,11 +475,35 @@ export type DataSourceEvents = { resize: (size: number) => void; }; +/** + * return Promise indicates success + * return Promise indicates failure + */ export type DataSourceEditHandler = ( row: DataSourceRow, columnName: string, - value: VuuColumnDataType -) => boolean; + value: VuuRowDataItemType +) => Promise; + +export type RpcResponse = + | MenuRpcResponse + | VuuUIMessageInRPCEditReject + | VuuUIMessageInRPCEditResponse; + +export type RpcResponseHandler = (response: RpcResponse) => boolean; + +export type RowSearchPredicate = (row: DataSourceRow) => boolean; + +export type DataSourceStatus = + | "disabled" + | "disabling" + | "enabled" + | "enabling" + | "initialising" + | "subscribing" + | "subscribed" + | "suspended" + | "unsubscribed"; export type RpcResponse = | MenuRpcResponse @@ -493,12 +518,37 @@ export interface DataSource extends EventEmitter { closeTreeNode: (key: string, cascade?: boolean) => void; columns: string[]; config: DataSourceConfig; + status: DataSourceStatus; + /** + * + * Similar to disable but intended for pauses of very short duration (default is 3 seconds). Although + * the dataSource will stop sending messages until resumed, it will not disconnect from a remote server. + * It will preserve subscription to the remote server and continue to apply updates to cached data. It + * just won't send updates through to the UI thread (until resumed). Useful in edge cases such as where a + * component is dragged to a new location. When dropped, the component will be unmounted and very quickly + * remounted by React. For the duration of this operation, we suspend updates . Updating an unmounted + * React component would cause a React error. + * If an suspend is requested and not resumed within 3 seconds, it will automatically be promoted to a disable., + */ suspend?: () => void; resume?: () => void; - enable?: () => void; + /** + * For a dataSource that has been previously disabled and is currently in disabled state , this will restore + * the subscription to active status. Fresh data will be dispatched to client. The enable call optionally + * accepts the same subscribe callback as subscribe. This allows a completely new instance of a component to + * assume ownership of a subscription and receive all messages. + */ + enable?: (callback?: SubscribeCallback) => void; + /** + * Disables this subscription. A datasource will send no further messages until re-enabled. Example usage + * might be for a component displayed within a set of Tabs. If user switches to another tab, the dataSource + * of the component that is no longer visible can be disabled until it is made visible again. + */ disable?: () => void; filter: DataSourceFilter; groupBy: VuuGroupBy; + links?: LinkDescriptorWithLabel[]; + menu?: VuuMenu; menuRpcCall: ( rpcRequest: Omit | ClientToServerEditRpc ) => Promise; diff --git a/vuu-ui/packages/vuu-data/src/inlined-worker.js b/vuu-ui/packages/vuu-data/src/inlined-worker.js index c9900c37e..5b687bae0 100644 --- a/vuu-ui/packages/vuu-data/src/inlined-worker.js +++ b/vuu-ui/packages/vuu-data/src/inlined-worker.js @@ -1,8 +1,2572 @@ export const workerSourceCode = ` -var fe=(s,e,t)=>{if(!e.has(s))throw TypeError("Cannot "+t)};var p=(s,e,t)=>(fe(s,e,"read from private field"),t?t.call(s):e.get(s)),U=(s,e,t)=>{if(e.has(s))throw TypeError("Cannot add the same private member more than once");e instanceof WeakSet?e.add(s):e.set(s,t)},me=(s,e,t,n)=>(fe(s,e,"write to private field"),n?n.call(s,t):e.set(s,t),t);function he(s,e,t=[],n=[]){for(let r=0,o=s.length;r{var e,t;if(((e=globalThis.document)==null?void 0:e.cookie)!==void 0)return(t=globalThis.document.cookie.split("; ").find(n=>n.startsWith(\`\${s}=\`)))==null?void 0:t.split("=")[1]};function Y({from:s,to:e},t=0,n=Number.MAX_SAFE_INTEGER){if(t===0)return ns>=e&&s=this.to||ttypeof s=="string"&&ct.includes(s),dt="error",F=()=>{},gt="error",{loggingLevel:N=gt}=ft(),w=s=>{let e=N==="debug",t=e||N==="info",n=t||N==="warn",r=n||N==="error",o=t?g=>console.info(\`[\${s}] \${g}\`):F,a=n?g=>console.warn(\`[\${s}] \${g}\`):F,u=e?g=>console.debug(\`[\${s}] \${g}\`):F;return{errorEnabled:r,error:r?g=>console.error(\`[\${s}] \${g}\`):F}};function ft(){return typeof loggingSettings<"u"?loggingSettings:{loggingLevel:mt()}}function mt(){let s=be("vuu-logging-level");return pt(s)?s:dt}var{debug:ht,debugEnabled:bt}=w("range-monitor"),W=class{constructor(e){this.source=e;this.range={from:0,to:0};this.timestamp=0}isSet(){return this.timestamp!==0}set({from:e,to:t}){let{timestamp:n}=this;if(this.range.from=e,this.range.to=t,this.timestamp=performance.now(),n)bt&&ht(\`<\${this.source}> [\${e}-\${t}], \${(this.timestamp-n).toFixed(0)} ms elapsed\`);else return 0}};function Ce(s){return Array.isArray(s)}function Ct(s){return!Array.isArray(s)}var y,ye=class{constructor(){U(this,y,new Map)}addListener(e,t){let n=p(this,y).get(e);n?Ce(n)?n.push(t):Ct(n)&&p(this,y).set(e,[n,t]):p(this,y).set(e,t)}removeListener(e,t){if(!p(this,y).has(e))return;let n=p(this,y).get(e),r=-1;if(n===t)p(this,y).delete(e);else if(Array.isArray(n)){for(let o=length;o-- >0;)if(n[o]===t){r=o;break}if(r<0)return;n.length===1?(n.length=0,p(this,y).delete(e)):n.splice(r,1)}}removeAllListeners(e){e&&p(this,y).has(e)?p(this,y).delete(e):e===void 0&&p(this,y).clear()}emit(e,...t){if(p(this,y)){let n=p(this,y).get(e);n&&this.invokeHandler(n,t)}}once(e,t){let n=(...r)=>{this.removeListener(e,n),t(...r)};this.on(e,n)}on(e,t){this.addListener(e,t)}hasListener(e,t){let n=p(this,y).get(e);return Array.isArray(n)?n.includes(t):n===t}invokeHandler(e,t){if(Ce(e))e.slice().forEach(n=>this.invokeHandler(n,t));else switch(t.length){case 0:e();break;case 1:e(t[0]);break;case 2:e(t[0],t[1]);break;default:e.call(null,...t)}}};y=new WeakMap;var \$=String.fromCharCode(8200),m=String.fromCharCode(8199);var wn={DIGIT:m,TWO_DIGITS:m+m,THREE_DIGITS:m+m+m,FULL_PADDING:[null,\$+m,\$+m+m,\$+m+m+m,\$+m+m+m+m]};var En=m+m+m+m+m+m+m+m+m;var{COUNT:Bn}=x;var q=class{constructor(e){this.keys=new Map,this.free=[],this.nextKeyValue=0,this.reset(e)}next(){return this.free.length>0?this.free.pop():this.nextKeyValue++}reset({from:e,to:t}){this.keys.forEach((r,o)=>{(o=t)&&(this.free.push(r),this.keys.delete(o))});let n=t-e;this.keys.size+this.free.length>n&&(this.free.length=Math.max(0,n-this.keys.size));for(let r=e;rthis.keys.size&&(this.nextKeyValue=this.keys.size)}keyFor(e){let t=this.keys.get(e);if(t===void 0)throw console.log(\`key not found +var __accessCheck = (obj, member, msg) => { + if (!member.has(obj)) + throw TypeError("Cannot " + msg); +}; +var __privateGet = (obj, member, getter) => { + __accessCheck(obj, member, "read from private field"); + return getter ? getter.call(obj) : member.get(obj); +}; +var __privateAdd = (obj, member, value) => { + if (member.has(obj)) + throw TypeError("Cannot add the same private member more than once"); + member instanceof WeakSet ? member.add(obj) : member.set(obj, value); +}; +var __privateSet = (obj, member, value, setter) => { + __accessCheck(obj, member, "write to private field"); + setter ? setter.call(obj, value) : member.set(obj, value); + return value; +}; + +// ../vuu-utils/src/array-utils.ts +function partition(array, test, pass = [], fail = []) { + for (let i = 0, len = array.length; i < len; i++) { + (test(array[i], i) ? pass : fail).push(array[i]); + } + return [pass, fail]; +} + +// ../vuu-utils/src/column-utils.ts +var metadataKeys = { + IDX: 0, + RENDER_IDX: 1, + IS_LEAF: 2, + IS_EXPANDED: 3, + DEPTH: 4, + COUNT: 5, + KEY: 6, + SELECTED: 7, + count: 8, + // TODO following only used in datamodel + PARENT_IDX: "parent_idx", + IDX_POINTER: "idx_pointer", + FILTER_COUNT: "filter_count", + NEXT_FILTER_IDX: "next_filter_idx" +}; +var { DEPTH, IS_LEAF } = metadataKeys; + +// ../vuu-utils/src/cookie-utils.ts +var getCookieValue = (name) => { + var _a, _b; + if (((_a = globalThis.document) == null ? void 0 : _a.cookie) !== void 0) { + return (_b = globalThis.document.cookie.split("; ").find((row) => row.startsWith(\`\${name}=\`))) == null ? void 0 : _b.split("=")[1]; + } +}; + +// ../vuu-utils/src/range-utils.ts +function getFullRange({ from, to }, bufferSize = 0, rowCount = Number.MAX_SAFE_INTEGER) { + if (bufferSize === 0) { + if (rowCount < from) { + return { from: 0, to: 0 }; + } else { + return { from, to: Math.min(to, rowCount) }; + } + } else if (from === 0) { + return { from, to: Math.min(to + bufferSize, rowCount) }; + } else { + const rangeSize = to - from; + const buff = Math.round(bufferSize / 2); + const shortfallBefore = from - buff < 0; + const shortFallAfter = rowCount - (to + buff) < 0; + if (shortfallBefore && shortFallAfter) { + return { from: 0, to: rowCount }; + } else if (shortfallBefore) { + return { from: 0, to: rangeSize + bufferSize }; + } else if (shortFallAfter) { + return { + from: Math.max(0, rowCount - (rangeSize + bufferSize)), + to: rowCount + }; + } else { + return { from: from - buff, to: to + buff }; + } + } +} +var withinRange = (value, { from, to }) => value >= from && value < to; +var WindowRange = class { + constructor(from, to) { + this.from = from; + this.to = to; + } + isWithin(index) { + return withinRange(index, this); + } + //find the overlap of this range and a new one + overlap(from, to) { + return from >= this.to || to < this.from ? [0, 0] : [Math.max(from, this.from), Math.min(to, this.to)]; + } + copy() { + return new WindowRange(this.from, this.to); + } +}; + +// ../vuu-utils/src/DataWindow.ts +var { KEY } = metadataKeys; + +// ../vuu-utils/src/logging-utils.ts +var logLevels = ["error", "warn", "info", "debug"]; +var isValidLogLevel = (value) => typeof value === "string" && logLevels.includes(value); +var DEFAULT_LOG_LEVEL = "error"; +var NO_OP = () => void 0; +var DEFAULT_DEBUG_LEVEL = false ? "error" : "info"; +var { loggingLevel = DEFAULT_DEBUG_LEVEL } = getLoggingSettings(); +var logger = (category) => { + const debugEnabled5 = loggingLevel === "debug"; + const infoEnabled5 = debugEnabled5 || loggingLevel === "info"; + const warnEnabled = infoEnabled5 || loggingLevel === "warn"; + const errorEnabled = warnEnabled || loggingLevel === "error"; + const info5 = infoEnabled5 ? (message) => console.info(\`[\${category}] \${message}\`) : NO_OP; + const warn4 = warnEnabled ? (message) => console.warn(\`[\${category}] \${message}\`) : NO_OP; + const debug5 = debugEnabled5 ? (message) => console.debug(\`[\${category}] \${message}\`) : NO_OP; + const error4 = errorEnabled ? (message) => console.error(\`[\${category}] \${message}\`) : NO_OP; + if (false) { + return { + errorEnabled, + error: error4 + }; + } else { + return { + debugEnabled: debugEnabled5, + infoEnabled: infoEnabled5, + warnEnabled, + errorEnabled, + info: info5, + warn: warn4, + debug: debug5, + error: error4 + }; + } +}; +function getLoggingSettings() { + if (typeof loggingSettings !== "undefined") { + return loggingSettings; + } else { + return { + loggingLevel: getLoggingLevelFromCookie() + }; + } +} +function getLoggingLevelFromCookie() { + const value = getCookieValue("vuu-logging-level"); + if (isValidLogLevel(value)) { + return value; + } else { + return DEFAULT_LOG_LEVEL; + } +} + +// ../vuu-utils/src/debug-utils.ts +var { debug, debugEnabled } = logger("range-monitor"); +var RangeMonitor = class { + constructor(source) { + this.source = source; + this.range = { from: 0, to: 0 }; + this.timestamp = 0; + } + isSet() { + return this.timestamp !== 0; + } + set({ from, to }) { + const { timestamp } = this; + this.range.from = from; + this.range.to = to; + this.timestamp = performance.now(); + if (timestamp) { + debugEnabled && debug( + \`<\${this.source}> [\${from}-\${to}], \${(this.timestamp - timestamp).toFixed(0)} ms elapsed\` + ); + } else { + return 0; + } + } +}; + +// ../vuu-utils/src/event-emitter.ts +function isArrayOfListeners(listeners) { + return Array.isArray(listeners); +} +function isOnlyListener(listeners) { + return !Array.isArray(listeners); +} +var _events; +var EventEmitter = class { + constructor() { + __privateAdd(this, _events, /* @__PURE__ */ new Map()); + } + addListener(event, listener) { + const listeners = __privateGet(this, _events).get(event); + if (!listeners) { + __privateGet(this, _events).set(event, listener); + } else if (isArrayOfListeners(listeners)) { + listeners.push(listener); + } else if (isOnlyListener(listeners)) { + __privateGet(this, _events).set(event, [listeners, listener]); + } + } + removeListener(event, listener) { + if (!__privateGet(this, _events).has(event)) { + return; + } + const listenerOrListeners = __privateGet(this, _events).get(event); + let position = -1; + if (listenerOrListeners === listener) { + __privateGet(this, _events).delete(event); + } else if (Array.isArray(listenerOrListeners)) { + for (let i = length; i-- > 0; ) { + if (listenerOrListeners[i] === listener) { + position = i; + break; + } + } + if (position < 0) { + return; + } + if (listenerOrListeners.length === 1) { + listenerOrListeners.length = 0; + __privateGet(this, _events).delete(event); + } else { + listenerOrListeners.splice(position, 1); + } + } + } + removeAllListeners(event) { + if (event && __privateGet(this, _events).has(event)) { + __privateGet(this, _events).delete(event); + } else if (event === void 0) { + __privateGet(this, _events).clear(); + } + } + emit(event, ...args) { + if (__privateGet(this, _events)) { + const handler = __privateGet(this, _events).get(event); + if (handler) { + this.invokeHandler(handler, args); + } + } + } + once(event, listener) { + const handler = (...args) => { + this.removeListener(event, handler); + listener(...args); + }; + this.on(event, handler); + } + on(event, listener) { + this.addListener(event, listener); + } + hasListener(event, listener) { + const listeners = __privateGet(this, _events).get(event); + if (Array.isArray(listeners)) { + return listeners.includes(listener); + } else { + return listeners === listener; + } + } + invokeHandler(handler, args) { + if (isArrayOfListeners(handler)) { + handler.slice().forEach((listener) => this.invokeHandler(listener, args)); + } else { + switch (args.length) { + case 0: + handler(); + break; + case 1: + handler(args[0]); + break; + case 2: + handler(args[0], args[1]); + break; + default: + handler.call(null, ...args); + } + } + } +}; +_events = new WeakMap(); + +// ../vuu-utils/src/round-decimal.ts +var PUNCTUATION_STR = String.fromCharCode(8200); +var DIGIT_STR = String.fromCharCode(8199); +var Space = { + DIGIT: DIGIT_STR, + TWO_DIGITS: DIGIT_STR + DIGIT_STR, + THREE_DIGITS: DIGIT_STR + DIGIT_STR + DIGIT_STR, + FULL_PADDING: [ + null, + PUNCTUATION_STR + DIGIT_STR, + PUNCTUATION_STR + DIGIT_STR + DIGIT_STR, + PUNCTUATION_STR + DIGIT_STR + DIGIT_STR + DIGIT_STR, + PUNCTUATION_STR + DIGIT_STR + DIGIT_STR + DIGIT_STR + DIGIT_STR + ] +}; +var LEADING_FILL = DIGIT_STR + DIGIT_STR + DIGIT_STR + DIGIT_STR + DIGIT_STR + DIGIT_STR + DIGIT_STR + DIGIT_STR + DIGIT_STR; + +// ../vuu-utils/src/json-utils.ts +var { COUNT } = metadataKeys; + +// ../vuu-utils/src/keyset.ts +var KeySet = class { + constructor(range) { + this.keys = /* @__PURE__ */ new Map(); + this.free = []; + this.nextKeyValue = 0; + this.reset(range); + } + next() { + if (this.free.length > 0) { + return this.free.pop(); + } else { + return this.nextKeyValue++; + } + } + reset({ from, to }) { + this.keys.forEach((keyValue, rowIndex) => { + if (rowIndex < from || rowIndex >= to) { + this.free.push(keyValue); + this.keys.delete(rowIndex); + } + }); + const size = to - from; + if (this.keys.size + this.free.length > size) { + this.free.length = Math.max(0, size - this.keys.size); + } + for (let rowIndex = from; rowIndex < to; rowIndex++) { + if (!this.keys.has(rowIndex)) { + const nextKeyValue = this.next(); + this.keys.set(rowIndex, nextKeyValue); + } + } + if (this.nextKeyValue > this.keys.size) { + this.nextKeyValue = this.keys.size; + } + } + keyFor(rowIndex) { + const key = this.keys.get(rowIndex); + if (key === void 0) { + console.log(\`key not found keys: \${this.toDebugString()} free : \${this.free.join(",")} - \`),Error(\`KeySet, no key found for rowIndex \${e}\`);return t}toDebugString(){return Array.from(this.keys.entries()).map((e,t)=>\`\${e}=>\${t}\`).join(",")}};var{IDX:Zn}=x;var{SELECTED:er}=x,I={False:0,True:1,First:2,Last:4};var yt=(s,e)=>e>=s[0]&&e<=s[1],St=I.True+I.First+I.Last,Tt=I.True+I.First,Rt=I.True+I.Last,Z=(s,e)=>{for(let t of s)if(typeof t=="number"){if(t===e)return St}else if(yt(t,e))return e===t[0]?Tt:e===t[1]?Rt:I.True;return I.False};var Se=s=>{if(s.every(t=>typeof t=="number"))return s;let e=[];for(let t of s)if(typeof t=="number")e.push(t);else for(let n=t[0];n<=t[1];n++)e.push(n);return e};var wt=(()=>{let s=0,e=()=>\`0000\${(Math.random()*36**4<<0).toString(36)}\`.slice(-4);return()=>(s+=1,\`u\${e()}\${s}\`)})();var{debug:ks,debugEnabled:As,error:we,info:V,infoEnabled:It,warn:_}=w("websocket-connection"),Ee="ws",vt=s=>s.startsWith(Ee+"://")||s.startsWith(Ee+"s://"),xe={},ee=Symbol("setWebsocket"),B=Symbol("connectionCallback");async function Ie(s,e,t,n=10,r=5){return xe[s]={status:"connecting",connect:{allowed:r,remaining:r},reconnect:{allowed:n,remaining:n}},ve(s,e,t)}async function Q(s){throw Error("connection broken")}async function ve(s,e,t,n){let{status:r,connect:o,reconnect:a}=xe[s],u=r==="connecting"?o:a;try{t({type:"connection-status",status:"connecting"});let c=typeof n<"u",g=await _t(s,e);console.info("%c\u26A1 %cconnected","font-size: 24px;color: green;font-weight: bold;","color:green; font-size: 14px;"),n!==void 0&&n[ee](g);let i=n!=null?n:new te(g,s,e,t),l=c?"reconnected":"connection-open-awaiting-session";return t({type:"connection-status",status:l}),i.status=l,u.remaining=u.allowed,i}catch{let g=--u.remaining>0;if(t({type:"connection-status",status:"disconnected",reason:"failed to connect",retry:g}),g)return Dt(s,e,t,n,2e3);throw Error("Failed to establish connection")}}var Dt=(s,e,t,n,r)=>new Promise(o=>{setTimeout(()=>{o(ve(s,e,t,n))},r)}),_t=(s,e)=>new Promise((t,n)=>{let r=vt(s)?s:\`wss://\${s}\`;It&&e!==void 0&&V(\`WebSocket Protocol \${e==null?void 0:e.toString()}\`);let o=new WebSocket(r,e);o.onopen=()=>t(o),o.onerror=a=>n(a)}),Ve=()=>{_==null||_("Connection cannot be closed, socket not yet opened")},Me=s=>{_==null||_(\`Message cannot be sent, socket closed \${s.body.type}\`)},Pt=s=>{try{return JSON.parse(s)}catch{throw Error(\`Error parsing JSON response from server \${s}\`)}},te=class{constructor(e,t,n,r){this.close=Ve;this.requiresLogin=!0;this.send=Me;this.status="ready";this.messagesCount=0;this.connectionMetricsInterval=null;this.handleWebsocketMessage=e=>{let t=Pt(e.data);this.messagesCount+=1,this[B](t)};this.url=t,this.protocol=n,this[B]=r,this[ee](e)}reconnect(){Q(this)}[(B,ee)](e){let t=this[B];e.onmessage=o=>{this.status="connected",e.onmessage=this.handleWebsocketMessage,this.handleWebsocketMessage(o)},this.connectionMetricsInterval=setInterval(()=>{t({type:"connection-metrics",messagesLength:this.messagesCount}),this.messagesCount=0},2e3),e.onerror=()=>{we("\u26A1 connection error"),t({type:"connection-status",status:"disconnected",reason:"error"}),this.connectionMetricsInterval&&(clearInterval(this.connectionMetricsInterval),this.connectionMetricsInterval=null),this.status==="connection-open-awaiting-session"?we("Websocket connection lost before Vuu session established, check websocket configuration"):this.status!=="closed"&&(Q(this),this.send=r)},e.onclose=()=>{V==null||V("\u26A1 connection close"),t({type:"connection-status",status:"disconnected",reason:"close"}),this.connectionMetricsInterval&&(clearInterval(this.connectionMetricsInterval),this.connectionMetricsInterval=null),this.status!=="closed"&&(Q(this),this.send=r)};let n=o=>{e.send(JSON.stringify(o))},r=o=>{V==null||V(\`TODO queue message until websocket reconnected \${o.body.type}\`)};this.send=n,this.close=()=>{this.status="closed",e.close(),this.close=Ve,this.send=Me,V==null||V("close websocket")}}};var Lt=["VIEW_PORT_MENUS_SELECT_RPC","VIEW_PORT_MENU_TABLE_RPC","VIEW_PORT_MENU_ROW_RPC","VIEW_PORT_MENU_CELL_RPC","VP_EDIT_CELL_RPC","VP_EDIT_ROW_RPC","VP_EDIT_ADD_ROW_RPC","VP_EDIT_DELETE_CELL_RPC","VP_EDIT_DELETE_ROW_RPC","VP_EDIT_SUBMIT_FORM_RPC"],De=s=>Lt.includes(s.type),ne=({requestId:s,...e})=>[s,e],_e=s=>{let e=s.at(0);if(e.updateType==="SIZE"){if(s.length===1)return s;e=s.at(1)}let t=s.at(-1);return[e,t]},Pe=s=>{let e={};for(let t of s)(e[t.viewPortId]||(e[t.viewPortId]=[])).push(t);return e};var re=({columns:s,dataTypes:e,key:t,table:n})=>({table:n,columns:s.map((r,o)=>({name:r,serverDataType:e[o]})),key:t});var Le=s=>s.type==="connection-status",Oe=s=>s.type==="connection-metrics";var ke=s=>"viewport"in s,Ae=s=>s.type==="VIEW_PORT_MENU_RESP"&&s.action!==null&&G(s.action.table),G=s=>s!==null&&typeof s=="object"&&"table"in s&&"module"in s?s.table.startsWith("session"):!1;var Ue="CHANGE_VP_SUCCESS",Fe="CHANGE_VP_RANGE_SUCCESS",Ne="CLOSE_TREE_NODE",We="CLOSE_TREE_SUCCESS";var \$e="CREATE_VP",qe="CREATE_VP_SUCCESS",Be="DISABLE_VP",Ge="DISABLE_VP_SUCCESS";var Ke="ENABLE_VP",He="ENABLE_VP_SUCCESS";var se="GET_VP_VISUAL_LINKS",je="GET_VIEW_PORT_MENUS";var ze="HB",Je="HB_RESP",Ye="LOGIN",Ze="LOGIN_SUCCESS",Xe="OPEN_TREE_NODE",Qe="OPEN_TREE_SUCCESS";var et="REMOVE_VP";var oe="RPC_RESP";var tt="SET_SELECTION_SUCCESS",ie="TABLE_META_RESP",ae="TABLE_LIST_RESP",nt="TABLE_ROW";var st=s=>{switch(s){case"TypeAheadRpcHandler":return"TYPEAHEAD";default:return"SIMUL"}};var ot=[],T=w("array-backed-moving-window");function Ot(s,e){if(!e||e.data.length!==s.data.length||e.sel!==s.sel)return!1;for(let t=0;t{var t;if((t=T.info)==null||t.call(T,\`setRowCount \${e}\`),e{let n=this.bufferSize*.25;return p(this,h).to-t0&&e-p(this,h).from0&&this.clientRange.from+this.rowsWithinRange===this.rowCount}outOfRange(e,t){let{from:n,to:r}=this.range;if(t=r)return!0}setAtIndex(e){let{rowIndex:t}=e,n=t-p(this,h).from;if(Ot(e,this.internalData[n]))return!1;let r=this.isWithinClientRange(t);return(r||this.isWithinRange(t))&&(!this.internalData[n]&&r&&(this.rowsWithinRange+=1),this.internalData[n]=e),r}getAtIndex(e){return p(this,h).isWithin(e)&&this.internalData[e-p(this,h).from]!=null?this.internalData[e-p(this,h).from]:void 0}isWithinRange(e){return p(this,h).isWithin(e)}isWithinClientRange(e){return this.clientRange.isWithin(e)}setClientRange(e,t){var g;(g=T.debug)==null||g.call(T,\`setClientRange \${e} - \${t}\`);let n=this.clientRange.from,r=Math.min(this.clientRange.to,this.rowCount);if(e===n&&t===r)return[!1,ot];let o=this.clientRange.copy();this.clientRange.from=e,this.clientRange.to=t,this.rowsWithinRange=0;for(let i=e;io.to){let i=Math.max(e,o.to);a=this.internalData.slice(i-u,t-u)}else{let i=Math.min(o.from,t);a=this.internalData.slice(e-u,i-u)}return[this.bufferBreakout(e,t),a]}setRange(e,t){var n,r;if(e!==p(this,h).from||t!==p(this,h).to){(n=T.debug)==null||n.call(T,\`setRange \${e} - \${t}\`);let[o,a]=p(this,h).overlap(e,t),u=new Array(t-e);this.rowsWithinRange=0;for(let c=o;c=0;o--)if(e[o]!==void 0){r=e[o];break}return n&&r?[n.rowIndex,r.rowIndex]:[-1,-1]}};h=new WeakMap;var kt=[],{debug:b,debugEnabled:H,error:At,info:d,infoEnabled:Ut,warn:P}=w("viewport"),Ft=({rowKey:s,updateType:e})=>e==="U"&&!s.startsWith("\$root"),j=[void 0,void 0],Nt={count:0,mode:void 0,size:0,ts:0},z=class{constructor({aggregations:e,bufferSize:t=50,columns:n,filter:r,groupBy:o=[],table:a,range:u,sort:c,title:g,viewport:i,visualLink:l},f){this.batchMode=!0;this.hasUpdates=!1;this.pendingUpdates=[];this.pendingOperations=new Map;this.pendingRangeRequests=[];this.rowCountChanged=!1;this.selectedRows=[];this.tableSchema=null;this.useBatchMode=!0;this.lastUpdateStatus=Nt;this.updateThrottleTimer=void 0;this.rangeMonitor=new W("ViewPort");this.disabled=!1;this.isTree=!1;this.status="";this.suspended=!1;this.suspendTimer=null;this.setLastSizeOnlyUpdateSize=e=>{this.lastUpdateStatus.size=e};this.setLastUpdate=e=>{let{ts:t,mode:n}=this.lastUpdateStatus,r=0;if(n===e){let o=Date.now();this.lastUpdateStatus.count+=1,this.lastUpdateStatus.ts=o,r=t===0?0:o-t}else this.lastUpdateStatus.count=1,this.lastUpdateStatus.ts=0,r=0;return this.lastUpdateStatus.mode=e,r};this.rangeRequestAlreadyPending=e=>{let{bufferSize:t}=this,n=t*.25,{from:r}=e;for(let{from:o,to:a}of this.pendingRangeRequests)if(r>=o&&r{this.updateThrottleTimer=void 0,this.lastUpdateStatus.count=3,this.postMessageToClient({clientViewportId:this.clientViewportId,mode:"size-only",size:this.lastUpdateStatus.size,type:"viewport-update"})};this.shouldThrottleMessage=e=>{let t=this.setLastUpdate(e);return e==="size-only"&&t>0&&t<500&&this.lastUpdateStatus.count>3};this.throttleMessage=e=>this.shouldThrottleMessage(e)?(d==null||d("throttling updates setTimeout to 2000"),this.updateThrottleTimer===void 0&&(this.updateThrottleTimer=setTimeout(this.sendThrottledSizeMessage,2e3)),!0):(this.updateThrottleTimer!==void 0&&(clearTimeout(this.updateThrottleTimer),this.updateThrottleTimer=void 0),!1);this.getNewRowCount=()=>{if(this.rowCountChanged&&this.dataWindow)return this.rowCountChanged=!1,this.dataWindow.rowCount};this.aggregations=e,this.bufferSize=t,this.clientRange=u,this.clientViewportId=i,this.columns=n,this.filter=r,this.groupBy=o,this.keys=new q(u),this.pendingLinkedParent=l,this.table=a,this.sort=c,this.title=g,Ut&&(d==null||d(\`constructor #\${i} \${a.table} bufferSize=\${t}\`)),this.dataWindow=new K(this.clientRange,u,this.bufferSize),this.postMessageToClient=f}get hasUpdatesToProcess(){return this.suspended?!1:this.rowCountChanged||this.hasUpdates}get size(){var e;return(e=this.dataWindow.rowCount)!=null?e:0}subscribe(){let{filter:e}=this.filter;return this.status=this.status==="subscribed"?"resubscribing":"subscribing",{type:\$e,table:this.table,range:Y(this.clientRange,this.bufferSize),aggregations:this.aggregations,columns:this.columns,sort:this.sort,groupBy:this.groupBy,filterSpec:{filter:e}}}handleSubscribed({viewPortId:e,aggregations:t,columns:n,filterSpec:r,range:o,sort:a,groupBy:u}){return this.serverViewportId=e,this.status="subscribed",this.aggregations=t,this.columns=n,this.groupBy=u,this.isTree=u&&u.length>0,this.dataWindow.setRange(o.from,o.to),{aggregations:t,type:"subscribed",clientViewportId:this.clientViewportId,columns:n,filter:r,groupBy:u,range:o,sort:a,tableSchema:this.tableSchema}}awaitOperation(e,t){this.pendingOperations.set(e,t)}completeOperation(e,...t){var u;let{clientViewportId:n,pendingOperations:r}=this,o=r.get(e);if(!o){At("no matching operation found to complete");return}let{type:a}=o;if(d==null||d(\`completeOperation \${a}\`),r.delete(e),a==="CHANGE_VP_RANGE"){let[c,g]=t;(u=this.dataWindow)==null||u.setRange(c,g);for(let i=this.pendingRangeRequests.length-1;i>=0;i--){let l=this.pendingRangeRequests[i];if(l.requestId===e){l.acked=!0;break}else P==null||P("range requests sent faster than they are being ACKed")}}else if(a==="config"){let{aggregations:c,columns:g,filter:i,groupBy:l,sort:f}=o.data;return this.aggregations=c,this.columns=g,this.filter=i,this.groupBy=l,this.sort=f,l.length>0?this.isTree=!0:this.isTree&&(this.isTree=!1),b==null||b(\`config change confirmed, isTree : \${this.isTree}\`),{clientViewportId:n,type:a,config:o.data}}else{if(a==="groupBy")return this.isTree=o.data.length>0,this.groupBy=o.data,b==null||b(\`groupBy change confirmed, isTree : \${this.isTree}\`),{clientViewportId:n,type:a,groupBy:o.data};if(a==="columns")return this.columns=o.data,{clientViewportId:n,type:a,columns:o.data};if(a==="filter")return this.filter=o.data,{clientViewportId:n,type:a,filter:o.data};if(a==="aggregate")return this.aggregations=o.data,{clientViewportId:n,type:"aggregate",aggregations:this.aggregations};if(a==="sort")return this.sort=o.data,{clientViewportId:n,type:a,sort:this.sort};if(a!=="selection"){if(a==="disable")return this.disabled=!0,{type:"disabled",clientViewportId:n};if(a==="enable")return this.disabled=!1,{type:"enabled",clientViewportId:n};if(a==="CREATE_VISUAL_LINK"){let[c,g,i]=t;return this.linkedParent={colName:c,parentViewportId:g,parentColName:i},this.pendingLinkedParent=void 0,{type:"vuu-link-created",clientViewportId:n,colName:c,parentViewportId:g,parentColName:i}}else if(a==="REMOVE_VISUAL_LINK")return this.linkedParent=void 0,{type:"vuu-link-removed",clientViewportId:n}}}}rangeRequest(e,t){H&&this.rangeMonitor.set(t);let n="CHANGE_VP_RANGE";if(this.dataWindow){let[r,o]=this.dataWindow.setClientRange(t.from,t.to),a,u=this.dataWindow.rowCount||void 0,c=r&&!this.rangeRequestAlreadyPending(t)?{type:n,viewPortId:this.serverViewportId,...Y(t,this.bufferSize,u)}:null;if(c){H&&(b==null||b(\`create CHANGE_VP_RANGE: [\${c.from} - \${c.to}]\`)),this.awaitOperation(e,{type:n});let i=this.pendingRangeRequests.at(-1);if(i)if(i.acked)console.warn("Range Request before previous request is filled");else{let{from:l,to:f}=i;this.dataWindow.outOfRange(l,f)?a={clientViewportId:this.clientViewportId,type:"debounce-begin"}:P==null||P("Range Request before previous request is acked")}this.pendingRangeRequests.push({...c,requestId:e}),this.useBatchMode&&(this.batchMode=!0)}else o.length>0&&(this.batchMode=!1);this.keys.reset(this.dataWindow.clientRange);let g=this.isTree?le:ue;return o.length?[c,o.map(i=>g(i,this.keys,this.selectedRows))]:a?[c,void 0,a]:[c]}else return[null]}setLinks(e){return this.links=e,[{type:"vuu-links",links:e,clientViewportId:this.clientViewportId},this.pendingLinkedParent]}setMenu(e){return{type:"vuu-menu",menu:e,clientViewportId:this.clientViewportId}}setTableSchema(e){this.tableSchema=e}openTreeNode(e,t){return this.useBatchMode&&(this.batchMode=!0),{type:Xe,vpId:this.serverViewportId,treeKey:t.key}}closeTreeNode(e,t){return this.useBatchMode&&(this.batchMode=!0),{type:Ne,vpId:this.serverViewportId,treeKey:t.key}}createLink(e,t,n,r){let o={type:"CREATE_VISUAL_LINK",parentVpId:n,childVpId:this.serverViewportId,parentColumnName:r,childColumnName:t};return this.awaitOperation(e,o),this.useBatchMode&&(this.batchMode=!0),o}removeLink(e){let t={type:"REMOVE_VISUAL_LINK",childVpId:this.serverViewportId};return this.awaitOperation(e,t),t}suspend(){this.suspended=!0,d==null||d("suspend")}resume(){return this.suspended=!1,H&&(b==null||b(\`resume: \${this.currentData()}\`)),this.currentData()}currentData(){let e=[];if(this.dataWindow){let t=this.dataWindow.getData(),{keys:n}=this,r=this.isTree?le:ue;for(let o of t)o&&e.push(r(o,n,this.selectedRows))}return e}enable(e){return this.awaitOperation(e,{type:"enable"}),d==null||d(\`enable: \${this.serverViewportId}\`),{type:Ke,viewPortId:this.serverViewportId}}disable(e){return this.awaitOperation(e,{type:"disable"}),d==null||d(\`disable: \${this.serverViewportId}\`),this.suspended=!1,{type:Be,viewPortId:this.serverViewportId}}columnRequest(e,t){return this.awaitOperation(e,{type:"columns",data:t}),b==null||b(\`columnRequest: \${t}\`),this.createRequest({columns:t})}filterRequest(e,t){this.awaitOperation(e,{type:"filter",data:t}),this.useBatchMode&&(this.batchMode=!0);let{filter:n}=t;return d==null||d(\`filterRequest: \${n}\`),this.createRequest({filterSpec:{filter:n}})}setConfig(e,t){this.awaitOperation(e,{type:"config",data:t});let{filter:n,...r}=t;return this.useBatchMode&&(this.batchMode=!0),H?b==null||b(\`setConfig \${JSON.stringify(t)}\`):d==null||d("setConfig"),this.createRequest({...r,filterSpec:typeof(n==null?void 0:n.filter)=="string"?{filter:n.filter}:{filter:""}},!0)}aggregateRequest(e,t){return this.awaitOperation(e,{type:"aggregate",data:t}),d==null||d(\`aggregateRequest: \${t}\`),this.createRequest({aggregations:t})}sortRequest(e,t){return this.awaitOperation(e,{type:"sort",data:t}),d==null||d(\`sortRequest: \${JSON.stringify(t.sortDefs)}\`),this.createRequest({sort:t})}groupByRequest(e,t=kt){var n;return this.awaitOperation(e,{type:"groupBy",data:t}),this.useBatchMode&&(this.batchMode=!0),this.isTree||(n=this.dataWindow)==null||n.clear(),this.createRequest({groupBy:t})}selectRequest(e,t){return this.selectedRows=t,this.awaitOperation(e,{type:"selection",data:t}),d==null||d(\`selectRequest: \${t}\`),{type:"SET_SELECTION",vpId:this.serverViewportId,selection:Se(t)}}removePendingRangeRequest(e,t){for(let n=this.pendingRangeRequests.length-1;n>=0;n--){let{from:r,to:o}=this.pendingRangeRequests[n],a=!0;if(e>=r&&er&&t0){e=[],t="update";for(let a of this.pendingUpdates)e.push(o(a,n,r));this.pendingUpdates.length=0}else{let a=this.dataWindow.getData();if(this.dataWindow.hasAllRowsWithinRange){e=[],t="batch";for(let u of a)e.push(o(u,n,r));this.batchMode=!1}}this.hasUpdates=!1}return this.throttleMessage(t)?j:[e,t]}createRequest(e,t=!1){return t?{type:"CHANGE_VP",viewPortId:this.serverViewportId,...e}:{type:"CHANGE_VP",viewPortId:this.serverViewportId,aggregations:this.aggregations,columns:this.columns,sort:this.sort,groupBy:this.groupBy,filterSpec:{filter:this.filter.filter},...e}}},ue=({rowIndex:s,rowKey:e,sel:t,data:n},r,o)=>[s,r.keyFor(s),!0,!1,0,0,e,t?Z(o,s):0].concat(n),le=({rowIndex:s,rowKey:e,sel:t,data:n},r,o)=>{let[a,u,,c,,g,...i]=n;return[s,r.keyFor(s),c,u,a,g,e,t?Z(o,s):0].concat(i)};var it=1;var{debug:E,debugEnabled:L,error:O,info:S,infoEnabled:Wt,warn:k}=w("server-proxy"),C=()=>\`\${it++}\`,\$t={},qt=s=>s.disabled!==!0&&s.suspended!==!0,Bt={type:"NO_ACTION"},Gt=(s,e,t)=>s.map(n=>n.parentVpId===e?{...n,label:t}:n);function Kt(s,e){return s.map(t=>{let{parentVpId:n}=t,r=e.get(n);if(r)return{...t,parentClientVpId:r.clientViewportId,label:r.title};throw Error("addLabelsToLinks viewport not found")})}var J=class{constructor(e,t){this.authToken="";this.user="user";this.pendingTableMetaRequests=new Map;this.pendingRequests=new Map;this.queuedRequests=[];this.cachedTableSchemas=new Map;this.connection=e,this.postMessageToClient=t,this.viewports=new Map,this.mapClientToServerViewport=new Map}async reconnect(){await this.login(this.authToken);let[e,t]=he(Array.from(this.viewports.values()),qt);this.viewports.clear(),this.mapClientToServerViewport.clear();let n=r=>{r.forEach(o=>{let{clientViewportId:a}=o;this.viewports.set(a,o),this.sendMessageToServer(o.subscribe(),a)})};n(e),setTimeout(()=>{n(t)},2e3)}async login(e,t="user"){if(e)return this.authToken=e,this.user=t,new Promise((n,r)=>{this.sendMessageToServer({type:Ye,token:this.authToken,user:t},""),this.pendingLogin={resolve:n,reject:r}});this.authToken===""&&O("login, cannot login until auth token has been obtained")}subscribe(e){if(this.mapClientToServerViewport.has(e.viewport))O(\`spurious subscribe call \${e.viewport}\`);else{if(!this.hasSchemaForTable(e.table)&&!G(e.table)){S==null||S(\`subscribe to \${e.table.table}, no metadata yet, request metadata\`);let n=C();this.sendMessageToServer({type:"GET_TABLE_META",table:e.table},n),this.pendingTableMetaRequests.set(n,e.viewport)}let t=new z(e,this.postMessageToClient);this.viewports.set(e.viewport,t),this.sendIfReady(t.subscribe(),e.viewport,this.sessionId!=="")}}unsubscribe(e){let t=this.mapClientToServerViewport.get(e);t?(S==null||S(\`Unsubscribe Message (Client to Server): - \${t}\`),this.sendMessageToServer({type:et,viewPortId:t})):O(\`failed to unsubscribe client viewport \${e}, viewport not found\`)}getViewportForClient(e,t=!0){let n=this.mapClientToServerViewport.get(e);if(n){let r=this.viewports.get(n);if(r)return r;if(t)throw Error(\`Viewport not found for client viewport \${e}\`);return null}else{if(this.viewports.has(e))return this.viewports.get(e);if(t)throw Error(\`Viewport server id not found for client viewport \${e}\`);return null}}setViewRange(e,t){let n=C(),[r,o,a]=e.rangeRequest(n,t.range);S==null||S(\`setViewRange \${t.range.from} - \${t.range.to}\`),r&&this.sendIfReady(r,n,e.status==="subscribed"),o?(S==null||S(\`setViewRange \${o.length} rows returned from cache\`),this.postMessageToClient({mode:"batch",type:"viewport-update",clientViewportId:e.clientViewportId,rows:o})):a&&this.postMessageToClient(a)}setConfig(e,t){let n=C(),r=e.setConfig(n,t.config);this.sendIfReady(r,n,e.status==="subscribed")}aggregate(e,t){let n=C(),r=e.aggregateRequest(n,t.aggregations);this.sendIfReady(r,n,e.status==="subscribed")}sort(e,t){let n=C(),r=e.sortRequest(n,t.sort);this.sendIfReady(r,n,e.status==="subscribed")}groupBy(e,t){let n=C(),r=e.groupByRequest(n,t.groupBy);this.sendIfReady(r,n,e.status==="subscribed")}filter(e,t){let n=C(),{filter:r}=t,o=e.filterRequest(n,r);this.sendIfReady(o,n,e.status==="subscribed")}setColumns(e,t){let n=C(),{columns:r}=t,o=e.columnRequest(n,r);this.sendIfReady(o,n,e.status==="subscribed")}setTitle(e,t){e&&(e.title=t.title,this.updateTitleOnVisualLinks(e))}select(e,t){let n=C(),{selected:r}=t,o=e.selectRequest(n,r);this.sendIfReady(o,n,e.status==="subscribed")}disableViewport(e){let t=C(),n=e.disable(t);this.sendIfReady(n,t,e.status==="subscribed")}enableViewport(e){if(e.disabled){let t=C(),n=e.enable(t);this.sendIfReady(n,t,e.status==="subscribed")}}suspendViewport(e){e.suspend(),e.suspendTimer=setTimeout(()=>{S==null||S("suspendTimer expired, escalate suspend to disable"),this.disableViewport(e)},3e3)}resumeViewport(e){e.suspendTimer&&(E==null||E("clear suspend timer"),clearTimeout(e.suspendTimer),e.suspendTimer=null);let t=e.resume();this.postMessageToClient({clientViewportId:e.clientViewportId,mode:"batch",rows:t,type:"viewport-update"})}openTreeNode(e,t){if(e.serverViewportId){let n=C();this.sendIfReady(e.openTreeNode(n,t),n,e.status==="subscribed")}}closeTreeNode(e,t){if(e.serverViewportId){let n=C();this.sendIfReady(e.closeTreeNode(n,t),n,e.status==="subscribed")}}createLink(e,t){let{parentClientVpId:n,parentColumnName:r,childColumnName:o}=t,a=C(),u=this.mapClientToServerViewport.get(n);if(u){let c=e.createLink(a,o,u,r);this.sendMessageToServer(c,a)}else O("ServerProxy unable to create link, viewport not found")}removeLink(e){let t=C(),n=e.removeLink(t);this.sendMessageToServer(n,t)}updateTitleOnVisualLinks(e){var r;let{serverViewportId:t,title:n}=e;for(let o of this.viewports.values())if(o!==e&&o.links&&t&&n&&(r=o.links)!=null&&r.some(a=>a.parentVpId===t)){let[a]=o.setLinks(Gt(o.links,t,n));this.postMessageToClient(a)}}removeViewportFromVisualLinks(e){var t;for(let n of this.viewports.values())if((t=n.links)!=null&&t.some(({parentVpId:r})=>r===e)){let[r]=n.setLinks(n.links.filter(({parentVpId:o})=>o!==e));this.postMessageToClient(r)}}menuRpcCall(e){let t=this.getViewportForClient(e.vpId,!1);if(t!=null&&t.serverViewportId){let[n,r]=ne(e);this.sendMessageToServer({...r,vpId:t.serverViewportId},n)}}rpcCall(e){let[t,n]=ne(e),r=st(n.service);this.sendMessageToServer(n,t,{module:r})}handleMessageFromClient(e){if(E==null||E(\`handleMessageFromClient: \${e.type}\`),ke(e))if(e.type==="disable"){let t=this.getViewportForClient(e.viewport,!1);return t!==null?this.disableViewport(t):void 0}else{let t=this.getViewportForClient(e.viewport);switch(e.type){case"setViewRange":return this.setViewRange(t,e);case"config":return this.setConfig(t,e);case"aggregate":return this.aggregate(t,e);case"sort":return this.sort(t,e);case"groupBy":return this.groupBy(t,e);case"filter":return this.filter(t,e);case"select":return this.select(t,e);case"suspend":return this.suspendViewport(t);case"resume":return this.resumeViewport(t);case"enable":return this.enableViewport(t);case"openTreeNode":return this.openTreeNode(t,e);case"closeTreeNode":return this.closeTreeNode(t,e);case"createLink":return this.createLink(t,e);case"removeLink":return this.removeLink(t);case"setColumns":return this.setColumns(t,e);case"setTitle":return this.setTitle(t,e);default:}}else{if(De(e))return this.menuRpcCall(e);{let{type:t,requestId:n}=e;switch(t){case"GET_TABLE_LIST":return this.sendMessageToServer({type:t},n);case"GET_TABLE_META":return this.sendMessageToServer({type:t,table:e.table},n);case"RPC_CALL":return this.rpcCall(e);default:}}}O(\`Vuu ServerProxy Unexpected message from client \${JSON.stringify(e)}\`)}awaitResponseToMessage(e){return new Promise((t,n)=>{let r=C();this.sendMessageToServer(e,r),this.pendingRequests.set(r,{reject:n,resolve:t})})}sendIfReady(e,t,n=!0){return n?this.sendMessageToServer(e,t):this.queuedRequests.push(e),n}sendMessageToServer(e,t=\`\${it++}\`,n=\$t){let{module:r="CORE"}=n;this.authToken&&this.connection.send({requestId:t,sessionId:this.sessionId,token:this.authToken,user:this.user,module:r,body:e})}handleMessageFromServer(e){var u;let{body:t,requestId:n,sessionId:r}=e,o=this.pendingRequests.get(n);if(o){let{resolve:i}=o;this.pendingRequests.delete(n),i(t);return}let{viewports:a}=this;switch(t.type){case ze:this.sendMessageToServer({type:Je,ts:+new Date},"NA");break;case Ze:if(r)this.sessionId=r,(u=this.pendingLogin)==null||u.resolve(r),this.pendingLogin=void 0;else throw Error("LOGIN_SUCCESS did not provide sessionId");break;case qe:{let i=a.get(n);if(i){let{status:l}=i,{viewPortId:f}=t;n!==f&&(a.delete(n),a.set(f,i)),this.mapClientToServerViewport.set(n,f);let R=i.handleSubscribed(t);R&&(this.postMessageToClient(R),L&&E(\`post DataSourceSubscribedMessage to client: \${JSON.stringify(R)}\`)),i.disabled&&this.disableViewport(i),l==="subscribing"&&!G(i.table)&&(this.sendMessageToServer({type:se,vpId:f}),this.sendMessageToServer({type:je,vpId:f}),Array.from(a.entries()).filter(([M,{disabled:A}])=>M!==f&&!A).forEach(([M])=>{this.sendMessageToServer({type:se,vpId:M})}))}}break;case"REMOVE_VP_SUCCESS":{let i=a.get(t.viewPortId);i&&(this.mapClientToServerViewport.delete(i.clientViewportId),a.delete(t.viewPortId),this.removeViewportFromVisualLinks(t.viewPortId))}break;case tt:{let i=this.viewports.get(t.vpId);i&&i.completeOperation(n)}break;case Ue:case Ge:if(a.has(t.viewPortId)){let i=this.viewports.get(t.viewPortId);if(i){let l=i.completeOperation(n);l!==void 0&&(this.postMessageToClient(l),L&&E(\`postMessageToClient \${JSON.stringify(l)}\`))}}break;case He:{let i=this.viewports.get(t.viewPortId);if(i){let l=i.completeOperation(n);if(l){this.postMessageToClient(l);let f=i.currentData();L&&E(\`Enable Response (ServerProxy to Client): \${JSON.stringify(l)}\`),i.size===0?L&&E("Viewport Enabled but size 0, resend to server"):(this.postMessageToClient({clientViewportId:i.clientViewportId,mode:"batch",rows:f,size:i.size,type:"viewport-update"}),L&&E(\`Enable Response (ServerProxy to Client): send size \${i.size} \${f.length} rows from cache\`))}}}break;case nt:{let i=Pe(t.rows);for(let[l,f]of Object.entries(i)){let R=a.get(l);R?R.updateRows(f):k==null||k(\`TABLE_ROW message received for non registered viewport \${l}\`)}this.processUpdates()}break;case Fe:{let i=this.viewports.get(t.viewPortId);if(i){let{from:l,to:f}=t;i.completeOperation(n,l,f)}}break;case Qe:case We:break;case"CREATE_VISUAL_LINK_SUCCESS":{let i=this.viewports.get(t.childVpId),l=this.viewports.get(t.parentVpId);if(i&&l){let{childColumnName:f,parentColumnName:R}=t,M=i.completeOperation(n,f,l.clientViewportId,R);M&&this.postMessageToClient(M)}}break;case"REMOVE_VISUAL_LINK_SUCCESS":{let i=this.viewports.get(t.childVpId);if(i){let l=i.completeOperation(n);l&&this.postMessageToClient(l)}}break;case ae:this.postMessageToClient({type:ae,tables:t.tables,requestId:n});break;case ie:{let i=this.cacheTableMeta(t),l=this.pendingTableMetaRequests.get(n);if(l){this.pendingTableMetaRequests.delete(n);let f=this.viewports.get(l);f?f.setTableSchema(i):k==null||k("Message has come back AFTER CREATE_VP_SUCCESS, what do we do now")}else this.postMessageToClient({type:ie,tableSchema:i,requestId:n})}break;case"VP_VISUAL_LINKS_RESP":{let i=this.getActiveLinks(t.links),l=this.viewports.get(t.vpId);if(i.length&&l){let f=Kt(i,this.viewports),[R,M]=l.setLinks(f);if(this.postMessageToClient(R),M){let{link:A,parentClientVpId:at}=M,de=C(),ge=this.mapClientToServerViewport.get(at);if(ge){let ut=l.createLink(de,A.fromColumn,ge,A.toColumn);this.sendMessageToServer(ut,de)}}}}break;case"VIEW_PORT_MENUS_RESP":if(t.menu.name){let i=this.viewports.get(t.vpId);if(i){let l=i.setMenu(t.menu);this.postMessageToClient(l)}}break;case"VP_EDIT_RPC_RESPONSE":this.postMessageToClient({action:t.action,requestId:n,rpcName:t.rpcName,type:"VP_EDIT_RPC_RESPONSE"});break;case"VP_EDIT_RPC_REJECT":this.viewports.get(t.vpId)&&this.postMessageToClient({requestId:n,type:"VP_EDIT_RPC_REJECT",error:t.error});break;case"VIEW_PORT_MENU_RESP":if(Ae(t)){let{action:i,rpcName:l}=t;this.awaitResponseToMessage({type:"GET_TABLE_META",table:i.table}).then(f=>{let R=re(f);this.postMessageToClient({rpcName:l,type:"VIEW_PORT_MENU_RESP",action:{...i,tableSchema:R},tableAlreadyOpen:this.isTableOpen(i.table),requestId:n})})}else{let{action:i}=t;this.postMessageToClient({type:"VIEW_PORT_MENU_RESP",action:i||Bt,tableAlreadyOpen:i!==null&&this.isTableOpen(i.table),requestId:n})}break;case oe:{let{method:i,result:l}=t;this.postMessageToClient({type:oe,method:i,result:l,requestId:n})}break;case"ERROR":O(t.msg);break;default:Wt&&S(\`handleMessageFromServer \${t.type}.\`)}}hasSchemaForTable(e){return this.cachedTableSchemas.has(\`\${e.module}:\${e.table}\`)}cacheTableMeta(e){let{module:t,table:n}=e.table,r=\`\${t}:\${n}\`,o=this.cachedTableSchemas.get(r);return o||(o=re(e),this.cachedTableSchemas.set(r,o)),o}isTableOpen(e){if(e){let t=e.table;for(let n of this.viewports.values())if(!n.suspended&&n.table.table===t)return!0}}getActiveLinks(e){return e.filter(t=>{let n=this.viewports.get(t.parentVpId);return n&&!n.suspended})}processUpdates(){this.viewports.forEach(e=>{var t;if(e.hasUpdatesToProcess){let n=e.getClientRows();if(n!==j){let[r,o]=n,a=e.getNewRowCount();(a!==void 0||r&&r.length>0)&&(L&&E(\`postMessageToClient #\${e.clientViewportId} viewport-update \${o}, \${(t=r==null?void 0:r.length)!=null?t:"no"} rows, size \${a}\`),o&&this.postMessageToClient({clientViewportId:e.clientViewportId,mode:o,rows:r,size:a,type:"viewport-update"}))}}})}};var D,{info:ce,infoEnabled:pe}=w("worker");async function Ht(s,e,t,n,r,o,a){let u=await Ie(s,e,c=>{Oe(c)?(console.log("post connection metrics"),postMessage({type:"connection-metrics",messages:c})):Le(c)?(r(c),c.status==="reconnected"&&D.reconnect()):D.handleMessageFromServer(c)},o,a);D=new J(u,c=>jt(c)),u.requiresLogin&&await D.login(t,n)}function jt(s){postMessage(s)}var zt=async({data:s})=>{switch(s.type){case"connect":await Ht(s.url,s.protocol,s.token,s.username,postMessage,s.retryLimitDisconnect,s.retryLimitStartup),postMessage({type:"connected"});break;case"subscribe":pe&&ce(\`client subscribe: \${JSON.stringify(s)}\`),D.subscribe(s);break;case"unsubscribe":pe&&ce(\`client unsubscribe: \${JSON.stringify(s)}\`),D.unsubscribe(s.viewport);break;default:pe&&ce(\`client message: \${JSON.stringify(s)}\`),D.handleMessageFromClient(s)}};self.addEventListener("message",zt);postMessage({type:"ready"}); + \`); + throw Error(\`KeySet, no key found for rowIndex \${rowIndex}\`); + } + return key; + } + toDebugString() { + return Array.from(this.keys.entries()).map((k, v) => \`\${k}=>\${v}\`).join(","); + } +}; + +// ../vuu-utils/src/row-utils.ts +var { IDX } = metadataKeys; + +// ../vuu-utils/src/selection-utils.ts +var { SELECTED } = metadataKeys; +var RowSelected = { + False: 0, + True: 1, + First: 2, + Last: 4 +}; +var rangeIncludes = (range, index) => index >= range[0] && index <= range[1]; +var SINGLE_SELECTED_ROW = RowSelected.True + RowSelected.First + RowSelected.Last; +var FIRST_SELECTED_ROW_OF_BLOCK = RowSelected.True + RowSelected.First; +var LAST_SELECTED_ROW_OF_BLOCK = RowSelected.True + RowSelected.Last; +var getSelectionStatus = (selected, itemIndex) => { + for (const item of selected) { + if (typeof item === "number") { + if (item === itemIndex) { + return SINGLE_SELECTED_ROW; + } + } else if (rangeIncludes(item, itemIndex)) { + if (itemIndex === item[0]) { + return FIRST_SELECTED_ROW_OF_BLOCK; + } else if (itemIndex === item[1]) { + return LAST_SELECTED_ROW_OF_BLOCK; + } else { + return RowSelected.True; + } + } + } + return RowSelected.False; +}; +var expandSelection = (selected) => { + if (selected.every((selectedItem) => typeof selectedItem === "number")) { + return selected; + } + const expandedSelected = []; + for (const selectedItem of selected) { + if (typeof selectedItem === "number") { + expandedSelected.push(selectedItem); + } else { + for (let i = selectedItem[0]; i <= selectedItem[1]; i++) { + expandedSelected.push(i); + } + } + } + return expandedSelected; +}; + +// ../../node_modules/html-to-image/es/util.js +var uuid = (() => { + let counter = 0; + const random = () => ( + // eslint-disable-next-line no-bitwise + \`0000\${(Math.random() * 36 ** 4 << 0).toString(36)}\`.slice(-4) + ); + return () => { + counter += 1; + return \`u\${random()}\${counter}\`; + }; +})(); + +// src/websocket-connection.ts +var { debug: debug2, debugEnabled: debugEnabled2, error, info, infoEnabled, warn } = logger( + "websocket-connection" +); +var WS = "ws"; +var isWebsocketUrl = (url) => url.startsWith(WS + "://") || url.startsWith(WS + "s://"); +var connectionAttemptStatus = {}; +var setWebsocket = Symbol("setWebsocket"); +var connectionCallback = Symbol("connectionCallback"); +async function connect(connectionString, protocol, callback, retryLimitDisconnect = 10, retryLimitStartup = 5) { + connectionAttemptStatus[connectionString] = { + status: "connecting", + connect: { + allowed: retryLimitStartup, + remaining: retryLimitStartup + }, + reconnect: { + allowed: retryLimitDisconnect, + remaining: retryLimitDisconnect + } + }; + return makeConnection(connectionString, protocol, callback); +} +async function reconnect(connection) { + throw Error("connection broken"); +} +async function makeConnection(url, protocol, callback, connection) { + const { + status: currentStatus, + connect: connectStatus, + reconnect: reconnectStatus + } = connectionAttemptStatus[url]; + const trackedStatus = currentStatus === "connecting" ? connectStatus : reconnectStatus; + try { + callback({ type: "connection-status", status: "connecting" }); + const reconnecting = typeof connection !== "undefined"; + const ws = await createWebsocket(url, protocol); + console.info( + "%c\u26A1 %cconnected", + "font-size: 24px;color: green;font-weight: bold;", + "color:green; font-size: 14px;" + ); + if (connection !== void 0) { + connection[setWebsocket](ws); + } + const websocketConnection = connection != null ? connection : new WebsocketConnection(ws, url, protocol, callback); + const status = reconnecting ? "reconnected" : "connection-open-awaiting-session"; + callback({ type: "connection-status", status }); + websocketConnection.status = status; + trackedStatus.remaining = trackedStatus.allowed; + return websocketConnection; + } catch (err) { + const retry = --trackedStatus.remaining > 0; + callback({ + type: "connection-status", + status: "disconnected", + reason: "failed to connect", + retry + }); + if (retry) { + return makeConnectionIn(url, protocol, callback, connection, 2e3); + } else { + throw Error("Failed to establish connection"); + } + } +} +var makeConnectionIn = (url, protocol, callback, connection, delay) => new Promise((resolve) => { + setTimeout(() => { + resolve(makeConnection(url, protocol, callback, connection)); + }, delay); +}); +var createWebsocket = (connectionString, protocol) => new Promise((resolve, reject) => { + const websocketUrl = isWebsocketUrl(connectionString) ? connectionString : \`wss://\${connectionString}\`; + if (infoEnabled && protocol !== void 0) { + info(\`WebSocket Protocol \${protocol == null ? void 0 : protocol.toString()}\`); + } + const ws = new WebSocket(websocketUrl, protocol); + ws.onopen = () => resolve(ws); + ws.onerror = (evt) => reject(evt); +}); +var closeWarn = () => { + warn == null ? void 0 : warn(\`Connection cannot be closed, socket not yet opened\`); +}; +var sendWarn = (msg) => { + warn == null ? void 0 : warn(\`Message cannot be sent, socket closed \${msg.body.type}\`); +}; +var parseMessage = (message) => { + try { + return JSON.parse(message); + } catch (e) { + throw Error(\`Error parsing JSON response from server \${message}\`); + } +}; +var WebsocketConnection = class { + constructor(ws, url, protocol, callback) { + this.close = closeWarn; + this.requiresLogin = true; + this.send = sendWarn; + this.status = "ready"; + this.messagesCount = 0; + this.connectionMetricsInterval = null; + this.handleWebsocketMessage = (evt) => { + const vuuMessageFromServer = parseMessage(evt.data); + this.messagesCount += 1; + if (true) { + if (debugEnabled2 && vuuMessageFromServer.body.type !== "HB") { + debug2 == null ? void 0 : debug2(\`<<< \${vuuMessageFromServer.body.type}\`); + } + } + this[connectionCallback](vuuMessageFromServer); + }; + this.url = url; + this.protocol = protocol; + this[connectionCallback] = callback; + this[setWebsocket](ws); + } + reconnect() { + reconnect(this); + } + [(connectionCallback, setWebsocket)](ws) { + const callback = this[connectionCallback]; + ws.onmessage = (evt) => { + this.status = "connected"; + ws.onmessage = this.handleWebsocketMessage; + this.handleWebsocketMessage(evt); + }; + this.connectionMetricsInterval = setInterval(() => { + callback({ + type: "connection-metrics", + messagesLength: this.messagesCount + }); + this.messagesCount = 0; + }, 2e3); + ws.onerror = () => { + error(\`\u26A1 connection error\`); + callback({ + type: "connection-status", + status: "disconnected", + reason: "error" + }); + if (this.connectionMetricsInterval) { + clearInterval(this.connectionMetricsInterval); + this.connectionMetricsInterval = null; + } + if (this.status === "connection-open-awaiting-session") { + error( + \`Websocket connection lost before Vuu session established, check websocket configuration\` + ); + } else if (this.status !== "closed") { + reconnect(this); + this.send = queue; + } + }; + ws.onclose = () => { + info == null ? void 0 : info(\`\u26A1 connection close\`); + callback({ + type: "connection-status", + status: "disconnected", + reason: "close" + }); + if (this.connectionMetricsInterval) { + clearInterval(this.connectionMetricsInterval); + this.connectionMetricsInterval = null; + } + if (this.status !== "closed") { + reconnect(this); + this.send = queue; + } + }; + const send = (msg) => { + if (true) { + if (debugEnabled2 && msg.body.type !== "HB_RESP") { + debug2 == null ? void 0 : debug2(\`>>> \${msg.body.type}\`); + } + } + ws.send(JSON.stringify(msg)); + }; + const queue = (msg) => { + info == null ? void 0 : info(\`TODO queue message until websocket reconnected \${msg.body.type}\`); + }; + this.send = send; + this.close = () => { + this.status = "closed"; + ws.close(); + this.close = closeWarn; + this.send = sendWarn; + info == null ? void 0 : info("close websocket"); + }; + } +}; + +// src/message-utils.ts +var MENU_RPC_TYPES = [ + "VIEW_PORT_MENUS_SELECT_RPC", + "VIEW_PORT_MENU_TABLE_RPC", + "VIEW_PORT_MENU_ROW_RPC", + "VIEW_PORT_MENU_CELL_RPC", + "VP_EDIT_CELL_RPC", + "VP_EDIT_ROW_RPC", + "VP_EDIT_ADD_ROW_RPC", + "VP_EDIT_DELETE_CELL_RPC", + "VP_EDIT_DELETE_ROW_RPC", + "VP_EDIT_SUBMIT_FORM_RPC" +]; +var isVuuMenuRpcRequest = (message) => MENU_RPC_TYPES.includes(message["type"]); +var stripRequestId = ({ + requestId, + ...rest +}) => [requestId, rest]; +var getFirstAndLastRows = (rows) => { + let firstRow = rows.at(0); + if (firstRow.updateType === "SIZE") { + if (rows.length === 1) { + return rows; + } else { + firstRow = rows.at(1); + } + } + const lastRow = rows.at(-1); + return [firstRow, lastRow]; +}; +var groupRowsByViewport = (rows) => { + const result = {}; + for (const row of rows) { + const rowsForViewport = result[row.viewPortId] || (result[row.viewPortId] = []); + rowsForViewport.push(row); + } + return result; +}; +var createSchemaFromTableMetadata = ({ + columns, + dataTypes, + key, + table +}) => { + return { + table, + columns: columns.map((col, idx) => ({ + name: col, + serverDataType: dataTypes[idx] + })), + key + }; +}; + +// src/vuuUIMessageTypes.ts +var isConnectionStatusMessage = (msg) => msg.type === "connection-status"; +var isConnectionQualityMetrics = (msg) => msg.type === "connection-metrics"; +var isViewporttMessage = (msg) => "viewport" in msg; +var isSessionTableActionMessage = (messageBody) => messageBody.type === "VIEW_PORT_MENU_RESP" && messageBody.action !== null && isSessionTable(messageBody.action.table); +var isSessionTable = (table) => { + if (table !== null && typeof table === "object" && "table" in table && "module" in table) { + return table.table.startsWith("session"); + } + return false; +}; + +// src/server-proxy/messages.ts +var CHANGE_VP_SUCCESS = "CHANGE_VP_SUCCESS"; +var CHANGE_VP_RANGE_SUCCESS = "CHANGE_VP_RANGE_SUCCESS"; +var CLOSE_TREE_NODE = "CLOSE_TREE_NODE"; +var CLOSE_TREE_SUCCESS = "CLOSE_TREE_SUCCESS"; +var CREATE_VP = "CREATE_VP"; +var CREATE_VP_SUCCESS = "CREATE_VP_SUCCESS"; +var DISABLE_VP = "DISABLE_VP"; +var DISABLE_VP_SUCCESS = "DISABLE_VP_SUCCESS"; +var ENABLE_VP = "ENABLE_VP"; +var ENABLE_VP_SUCCESS = "ENABLE_VP_SUCCESS"; +var GET_VP_VISUAL_LINKS = "GET_VP_VISUAL_LINKS"; +var GET_VIEW_PORT_MENUS = "GET_VIEW_PORT_MENUS"; +var HB = "HB"; +var HB_RESP = "HB_RESP"; +var LOGIN = "LOGIN"; +var LOGIN_SUCCESS = "LOGIN_SUCCESS"; +var OPEN_TREE_NODE = "OPEN_TREE_NODE"; +var OPEN_TREE_SUCCESS = "OPEN_TREE_SUCCESS"; +var REMOVE_VP = "REMOVE_VP"; +var RPC_RESP = "RPC_RESP"; +var SET_SELECTION_SUCCESS = "SET_SELECTION_SUCCESS"; +var TABLE_META_RESP = "TABLE_META_RESP"; +var TABLE_LIST_RESP = "TABLE_LIST_RESP"; +var TABLE_ROW = "TABLE_ROW"; + +// src/server-proxy/rpc-services.ts +var getRpcServiceModule = (service) => { + switch (service) { + case "TypeAheadRpcHandler": + return "TYPEAHEAD"; + default: + return "SIMUL"; + } +}; + +// src/server-proxy/array-backed-moving-window.ts +var EMPTY_ARRAY = []; +var log = logger("array-backed-moving-window"); +function dataIsUnchanged(newRow, existingRow) { + if (!existingRow) { + return false; + } + if (existingRow.data.length !== newRow.data.length) { + return false; + } + if (existingRow.sel !== newRow.sel) { + return false; + } + for (let i = 0; i < existingRow.data.length; i++) { + if (existingRow.data[i] !== newRow.data[i]) { + return false; + } + } + return true; +} +var _range; +var ArrayBackedMovingWindow = class { + // Note, the buffer is already accounted for in the range passed in here + constructor({ from: clientFrom, to: clientTo }, { from, to }, bufferSize) { + __privateAdd(this, _range, void 0); + this.setRowCount = (rowCount) => { + var _a; + (_a = log.info) == null ? void 0 : _a.call(log, \`setRowCount \${rowCount}\`); + if (rowCount < this.internalData.length) { + this.internalData.length = rowCount; + } + if (rowCount < this.rowCount) { + this.rowsWithinRange = 0; + const end = Math.min(rowCount, this.clientRange.to); + for (let i = this.clientRange.from; i < end; i++) { + const rowIndex = i - __privateGet(this, _range).from; + if (this.internalData[rowIndex] !== void 0) { + this.rowsWithinRange += 1; + } + } + } + this.rowCount = rowCount; + }; + this.bufferBreakout = (from, to) => { + const bufferPerimeter = this.bufferSize * 0.25; + if (__privateGet(this, _range).to - to < bufferPerimeter) { + return true; + } else if (__privateGet(this, _range).from > 0 && from - __privateGet(this, _range).from < bufferPerimeter) { + return true; + } else { + return false; + } + }; + this.bufferSize = bufferSize; + this.clientRange = new WindowRange(clientFrom, clientTo); + __privateSet(this, _range, new WindowRange(from, to)); + this.internalData = new Array(bufferSize); + this.rowsWithinRange = 0; + this.rowCount = 0; + } + get range() { + return __privateGet(this, _range); + } + // TODO we shpuld probably have a hasAllClientRowsWithinRange + get hasAllRowsWithinRange() { + return this.rowsWithinRange === this.clientRange.to - this.clientRange.from || // this.rowsWithinRange === this.range.to - this.range.from || + this.rowCount > 0 && this.clientRange.from + this.rowsWithinRange === this.rowCount; + } + // Check to see if set of rows is outside the current viewport range, indicating + // that veiwport is being scrolled quickly and server is not able to keep up. + outOfRange(firstIndex, lastIndex) { + const { from, to } = this.range; + if (lastIndex < from) { + return true; + } + if (firstIndex >= to) { + return true; + } + } + setAtIndex(row) { + const { rowIndex: index } = row; + const internalIndex = index - __privateGet(this, _range).from; + if (dataIsUnchanged(row, this.internalData[internalIndex])) { + return false; + } + const isWithinClientRange = this.isWithinClientRange(index); + if (isWithinClientRange || this.isWithinRange(index)) { + if (!this.internalData[internalIndex] && isWithinClientRange) { + this.rowsWithinRange += 1; + } + this.internalData[internalIndex] = row; + } + return isWithinClientRange; + } + getAtIndex(index) { + return __privateGet(this, _range).isWithin(index) && this.internalData[index - __privateGet(this, _range).from] != null ? this.internalData[index - __privateGet(this, _range).from] : void 0; + } + isWithinRange(index) { + return __privateGet(this, _range).isWithin(index); + } + isWithinClientRange(index) { + return this.clientRange.isWithin(index); + } + // Returns [false] or [serverDataRequired, clientRows, holdingRows] + setClientRange(from, to) { + var _a; + (_a = log.debug) == null ? void 0 : _a.call(log, \`setClientRange \${from} - \${to}\`); + const currentFrom = this.clientRange.from; + const currentTo = Math.min(this.clientRange.to, this.rowCount); + if (from === currentFrom && to === currentTo) { + return [ + false, + EMPTY_ARRAY + /*, EMPTY_ARRAY*/ + ]; + } + const originalRange = this.clientRange.copy(); + this.clientRange.from = from; + this.clientRange.to = to; + this.rowsWithinRange = 0; + for (let i = from; i < to; i++) { + const internalIndex = i - __privateGet(this, _range).from; + if (this.internalData[internalIndex]) { + this.rowsWithinRange += 1; + } + } + let clientRows = EMPTY_ARRAY; + const offset = __privateGet(this, _range).from; + if (this.hasAllRowsWithinRange) { + if (to > originalRange.to) { + const start = Math.max(from, originalRange.to); + clientRows = this.internalData.slice(start - offset, to - offset); + } else { + const end = Math.min(originalRange.from, to); + clientRows = this.internalData.slice(from - offset, end - offset); + } + } + const serverDataRequired = this.bufferBreakout(from, to); + return [serverDataRequired, clientRows]; + } + setRange(from, to) { + var _a, _b; + if (from !== __privateGet(this, _range).from || to !== __privateGet(this, _range).to) { + (_a = log.debug) == null ? void 0 : _a.call(log, \`setRange \${from} - \${to}\`); + const [overlapFrom, overlapTo] = __privateGet(this, _range).overlap(from, to); + const newData = new Array(to - from); + this.rowsWithinRange = 0; + for (let i = overlapFrom; i < overlapTo; i++) { + const data = this.getAtIndex(i); + if (data) { + const index = i - from; + newData[index] = data; + if (this.isWithinClientRange(i)) { + this.rowsWithinRange += 1; + } + } + } + this.internalData = newData; + __privateGet(this, _range).from = from; + __privateGet(this, _range).to = to; + } else { + (_b = log.debug) == null ? void 0 : _b.call(log, \`setRange \${from} - \${to} IGNORED because not changed\`); + } + } + //TODO temp + get data() { + return this.internalData; + } + getData() { + var _a; + const { from, to } = __privateGet(this, _range); + const { from: clientFrom, to: clientTo } = this.clientRange; + const startOffset = Math.max(0, clientFrom - from); + const endOffset = Math.min( + to - from, + to, + clientTo - from, + (_a = this.rowCount) != null ? _a : to + ); + return this.internalData.slice(startOffset, endOffset); + } + clear() { + var _a; + (_a = log.debug) == null ? void 0 : _a.call(log, "clear"); + this.internalData.length = 0; + this.rowsWithinRange = 0; + this.setRowCount(0); + } + // used only for debugging + getCurrentDataRange() { + const rows = this.internalData; + const len = rows.length; + let [firstRow] = this.internalData; + let lastRow = this.internalData[len - 1]; + if (firstRow && lastRow) { + return [firstRow.rowIndex, lastRow.rowIndex]; + } else { + for (let i = 0; i < len; i++) { + if (rows[i] !== void 0) { + firstRow = rows[i]; + break; + } + } + for (let i = len - 1; i >= 0; i--) { + if (rows[i] !== void 0) { + lastRow = rows[i]; + break; + } + } + if (firstRow && lastRow) { + return [firstRow.rowIndex, lastRow.rowIndex]; + } else { + return [-1, -1]; + } + } + } +}; +_range = new WeakMap(); + +// src/server-proxy/viewport.ts +var EMPTY_GROUPBY = []; +var { debug: debug3, debugEnabled: debugEnabled3, error: error2, info: info2, infoEnabled: infoEnabled2, warn: warn2 } = logger("viewport"); +var isLeafUpdate = ({ rowKey, updateType }) => updateType === "U" && !rowKey.startsWith("\$root"); +var NO_DATA_UPDATE = [ + void 0, + void 0 +]; +var NO_UPDATE_STATUS = { + count: 0, + mode: void 0, + size: 0, + ts: 0 +}; +var Viewport = class { + constructor({ + aggregations, + bufferSize = 50, + columns, + filter, + groupBy = [], + table, + range, + sort, + title, + viewport, + visualLink + }, postMessageToClient) { + /** batchMode is irrelevant for Vuu Table, it was introduced to try and improve rendering performance of AgGrid */ + this.batchMode = true; + this.hasUpdates = false; + this.pendingUpdates = []; + this.pendingOperations = /* @__PURE__ */ new Map(); + this.pendingRangeRequests = []; + this.rowCountChanged = false; + this.selectedRows = []; + this.tableSchema = null; + this.useBatchMode = true; + this.lastUpdateStatus = NO_UPDATE_STATUS; + this.updateThrottleTimer = void 0; + this.rangeMonitor = new RangeMonitor("ViewPort"); + this.disabled = false; + this.isTree = false; + // TODO roll disabled/suspended into status + this.status = ""; + this.suspended = false; + this.suspendTimer = null; + // Records SIZE only updates + this.setLastSizeOnlyUpdateSize = (size) => { + this.lastUpdateStatus.size = size; + }; + this.setLastUpdate = (mode) => { + const { ts: lastTS, mode: lastMode } = this.lastUpdateStatus; + let elapsedTime = 0; + if (lastMode === mode) { + const ts = Date.now(); + this.lastUpdateStatus.count += 1; + this.lastUpdateStatus.ts = ts; + elapsedTime = lastTS === 0 ? 0 : ts - lastTS; + } else { + this.lastUpdateStatus.count = 1; + this.lastUpdateStatus.ts = 0; + elapsedTime = 0; + } + this.lastUpdateStatus.mode = mode; + return elapsedTime; + }; + this.rangeRequestAlreadyPending = (range) => { + const { bufferSize } = this; + const bufferThreshold = bufferSize * 0.25; + let { from: stillPendingFrom } = range; + for (const { from, to } of this.pendingRangeRequests) { + if (stillPendingFrom >= from && stillPendingFrom < to) { + if (range.to + bufferThreshold <= to) { + return true; + } else { + stillPendingFrom = to; + } + } + } + return false; + }; + this.sendThrottledSizeMessage = () => { + this.updateThrottleTimer = void 0; + this.lastUpdateStatus.count = 3; + this.postMessageToClient({ + clientViewportId: this.clientViewportId, + mode: "size-only", + size: this.lastUpdateStatus.size, + type: "viewport-update" + }); + }; + // If we are receiving multiple SIZE updates but no data, table is loading rows + // outside of our viewport. We can safely throttle these requests. Doing so will + // alleviate pressure on UI DataTable. + this.shouldThrottleMessage = (mode) => { + const elapsedTime = this.setLastUpdate(mode); + return mode === "size-only" && elapsedTime > 0 && elapsedTime < 500 && this.lastUpdateStatus.count > 3; + }; + this.throttleMessage = (mode) => { + if (this.shouldThrottleMessage(mode)) { + info2 == null ? void 0 : info2("throttling updates setTimeout to 2000"); + if (this.updateThrottleTimer === void 0) { + this.updateThrottleTimer = setTimeout( + this.sendThrottledSizeMessage, + 2e3 + ); + } + return true; + } else if (this.updateThrottleTimer !== void 0) { + clearTimeout(this.updateThrottleTimer); + this.updateThrottleTimer = void 0; + } + return false; + }; + this.getNewRowCount = () => { + if (this.rowCountChanged && this.dataWindow) { + this.rowCountChanged = false; + return this.dataWindow.rowCount; + } + }; + this.aggregations = aggregations; + this.bufferSize = bufferSize; + this.clientRange = range; + this.clientViewportId = viewport; + this.columns = columns; + this.filter = filter; + this.groupBy = groupBy; + this.keys = new KeySet(range); + this.pendingLinkedParent = visualLink; + this.table = table; + this.sort = sort; + this.title = title; + infoEnabled2 && (info2 == null ? void 0 : info2( + \`constructor #\${viewport} \${table.table} bufferSize=\${bufferSize}\` + )); + this.dataWindow = new ArrayBackedMovingWindow( + this.clientRange, + range, + this.bufferSize + ); + this.postMessageToClient = postMessageToClient; + } + get hasUpdatesToProcess() { + if (this.suspended) { + return false; + } + return this.rowCountChanged || this.hasUpdates; + } + get size() { + var _a; + return (_a = this.dataWindow.rowCount) != null ? _a : 0; + } + subscribe() { + const { filter } = this.filter; + this.status = this.status === "subscribed" ? "resubscribing" : "subscribing"; + return { + type: CREATE_VP, + table: this.table, + range: getFullRange(this.clientRange, this.bufferSize), + aggregations: this.aggregations, + columns: this.columns, + sort: this.sort, + groupBy: this.groupBy, + filterSpec: { filter } + }; + } + handleSubscribed({ + viewPortId, + aggregations, + columns, + filterSpec: filter, + range, + sort, + groupBy + }) { + this.serverViewportId = viewPortId; + this.status = "subscribed"; + this.aggregations = aggregations; + this.columns = columns; + this.groupBy = groupBy; + this.isTree = groupBy && groupBy.length > 0; + this.dataWindow.setRange(range.from, range.to); + return { + aggregations, + type: "subscribed", + clientViewportId: this.clientViewportId, + columns, + filter, + groupBy, + range, + sort, + tableSchema: this.tableSchema + }; + } + awaitOperation(requestId, msg) { + this.pendingOperations.set(requestId, msg); + } + // Return a message if we need to communicate this to client UI + completeOperation(requestId, ...params) { + var _a; + const { clientViewportId, pendingOperations } = this; + const pendingOperation = pendingOperations.get(requestId); + if (!pendingOperation) { + error2("no matching operation found to complete"); + return; + } + const { type } = pendingOperation; + info2 == null ? void 0 : info2(\`completeOperation \${type}\`); + pendingOperations.delete(requestId); + if (type === "CHANGE_VP_RANGE") { + const [from, to] = params; + (_a = this.dataWindow) == null ? void 0 : _a.setRange(from, to); + for (let i = this.pendingRangeRequests.length - 1; i >= 0; i--) { + const pendingRangeRequest = this.pendingRangeRequests[i]; + if (pendingRangeRequest.requestId === requestId) { + pendingRangeRequest.acked = true; + break; + } else { + warn2 == null ? void 0 : warn2("range requests sent faster than they are being ACKed"); + } + } + } else if (type === "config") { + const { aggregations, columns, filter, groupBy, sort } = pendingOperation.data; + this.aggregations = aggregations; + this.columns = columns; + this.filter = filter; + this.groupBy = groupBy; + this.sort = sort; + if (groupBy.length > 0) { + this.isTree = true; + } else if (this.isTree) { + this.isTree = false; + } + debug3 == null ? void 0 : debug3(\`config change confirmed, isTree : \${this.isTree}\`); + return { + clientViewportId, + type, + config: pendingOperation.data + }; + } else if (type === "groupBy") { + this.isTree = pendingOperation.data.length > 0; + this.groupBy = pendingOperation.data; + debug3 == null ? void 0 : debug3(\`groupBy change confirmed, isTree : \${this.isTree}\`); + return { + clientViewportId, + type, + groupBy: pendingOperation.data + }; + } else if (type === "columns") { + this.columns = pendingOperation.data; + return { + clientViewportId, + type, + columns: pendingOperation.data + }; + } else if (type === "filter") { + this.filter = pendingOperation.data; + return { + clientViewportId, + type, + filter: pendingOperation.data + }; + } else if (type === "aggregate") { + this.aggregations = pendingOperation.data; + return { + clientViewportId, + type: "aggregate", + aggregations: this.aggregations + }; + } else if (type === "sort") { + this.sort = pendingOperation.data; + return { + clientViewportId, + type, + sort: this.sort + }; + } else if (type === "selection") { + } else if (type === "disable") { + this.disabled = true; + return { + type: "disabled", + clientViewportId + }; + } else if (type === "enable") { + this.disabled = false; + return { + type: "enabled", + clientViewportId + }; + } else if (type === "CREATE_VISUAL_LINK") { + const [colName, parentViewportId, parentColName] = params; + this.linkedParent = { + colName, + parentViewportId, + parentColName + }; + this.pendingLinkedParent = void 0; + return { + type: "vuu-link-created", + clientViewportId, + colName, + parentViewportId, + parentColName + }; + } else if (type === "REMOVE_VISUAL_LINK") { + this.linkedParent = void 0; + return { + type: "vuu-link-removed", + clientViewportId + }; + } + } + // TODO when a range request arrives, consider the viewport to be scrolling + // until data arrives and we have the full range. + // When not scrolling, any server data is an update + // When scrolling, we are in batch mode + rangeRequest(requestId, range) { + if (debugEnabled3) { + this.rangeMonitor.set(range); + } + const type = "CHANGE_VP_RANGE"; + if (this.dataWindow) { + const [serverDataRequired, clientRows] = this.dataWindow.setClientRange( + range.from, + range.to + ); + let debounceRequest; + const maxRange = this.dataWindow.rowCount || void 0; + const serverRequest = serverDataRequired && !this.rangeRequestAlreadyPending(range) ? { + type, + viewPortId: this.serverViewportId, + ...getFullRange(range, this.bufferSize, maxRange) + } : null; + if (serverRequest) { + debugEnabled3 && (debug3 == null ? void 0 : debug3( + \`create CHANGE_VP_RANGE: [\${serverRequest.from} - \${serverRequest.to}]\` + )); + this.awaitOperation(requestId, { type }); + const pendingRequest = this.pendingRangeRequests.at(-1); + if (pendingRequest) { + if (pendingRequest.acked) { + console.warn("Range Request before previous request is filled"); + } else { + const { from, to } = pendingRequest; + if (this.dataWindow.outOfRange(from, to)) { + debounceRequest = { + clientViewportId: this.clientViewportId, + type: "debounce-begin" + }; + } else { + warn2 == null ? void 0 : warn2("Range Request before previous request is acked"); + } + } + } + this.pendingRangeRequests.push({ ...serverRequest, requestId }); + if (this.useBatchMode) { + this.batchMode = true; + } + } else if (clientRows.length > 0) { + this.batchMode = false; + } + this.keys.reset(this.dataWindow.clientRange); + const toClient = this.isTree ? toClientRowTree : toClientRow; + if (clientRows.length) { + return [ + serverRequest, + clientRows.map((row) => { + return toClient(row, this.keys, this.selectedRows); + }) + ]; + } else if (debounceRequest) { + return [serverRequest, void 0, debounceRequest]; + } else { + return [serverRequest]; + } + } else { + return [null]; + } + } + setLinks(links) { + this.links = links; + return [ + { + type: "vuu-links", + links, + clientViewportId: this.clientViewportId + }, + this.pendingLinkedParent + ]; + } + setMenu(menu) { + return { + type: "vuu-menu", + menu, + clientViewportId: this.clientViewportId + }; + } + setTableSchema(tableSchema) { + this.tableSchema = tableSchema; + } + openTreeNode(requestId, message) { + if (this.useBatchMode) { + this.batchMode = true; + } + return { + type: OPEN_TREE_NODE, + vpId: this.serverViewportId, + treeKey: message.key + }; + } + closeTreeNode(requestId, message) { + if (this.useBatchMode) { + this.batchMode = true; + } + return { + type: CLOSE_TREE_NODE, + vpId: this.serverViewportId, + treeKey: message.key + }; + } + createLink(requestId, colName, parentVpId, parentColumnName) { + const message = { + type: "CREATE_VISUAL_LINK", + parentVpId, + childVpId: this.serverViewportId, + parentColumnName, + childColumnName: colName + }; + this.awaitOperation(requestId, message); + if (this.useBatchMode) { + this.batchMode = true; + } + return message; + } + removeLink(requestId) { + const message = { + type: "REMOVE_VISUAL_LINK", + childVpId: this.serverViewportId + }; + this.awaitOperation(requestId, message); + return message; + } + suspend() { + this.suspended = true; + info2 == null ? void 0 : info2("suspend"); + } + resume() { + this.suspended = false; + if (debugEnabled3) { + debug3 == null ? void 0 : debug3(\`resume: \${this.currentData()}\`); + } + return this.currentData(); + } + currentData() { + const out = []; + if (this.dataWindow) { + const records = this.dataWindow.getData(); + const { keys } = this; + const toClient = this.isTree ? toClientRowTree : toClientRow; + for (const row of records) { + if (row) { + out.push(toClient(row, keys, this.selectedRows)); + } + } + } + return out; + } + enable(requestId) { + this.awaitOperation(requestId, { type: "enable" }); + info2 == null ? void 0 : info2(\`enable: \${this.serverViewportId}\`); + return { + type: ENABLE_VP, + viewPortId: this.serverViewportId + }; + } + disable(requestId) { + this.awaitOperation(requestId, { type: "disable" }); + info2 == null ? void 0 : info2(\`disable: \${this.serverViewportId}\`); + this.suspended = false; + return { + type: DISABLE_VP, + viewPortId: this.serverViewportId + }; + } + columnRequest(requestId, columns) { + this.awaitOperation(requestId, { + type: "columns", + data: columns + }); + debug3 == null ? void 0 : debug3(\`columnRequest: \${columns}\`); + return this.createRequest({ columns }); + } + filterRequest(requestId, dataSourceFilter) { + this.awaitOperation(requestId, { + type: "filter", + data: dataSourceFilter + }); + if (this.useBatchMode) { + this.batchMode = true; + } + const { filter } = dataSourceFilter; + info2 == null ? void 0 : info2(\`filterRequest: \${filter}\`); + return this.createRequest({ filterSpec: { filter } }); + } + setConfig(requestId, config) { + this.awaitOperation(requestId, { type: "config", data: config }); + const { filter, ...remainingConfig } = config; + if (this.useBatchMode) { + this.batchMode = true; + } + debugEnabled3 ? debug3 == null ? void 0 : debug3(\`setConfig \${JSON.stringify(config)}\`) : info2 == null ? void 0 : info2(\`setConfig\`); + return this.createRequest( + { + ...remainingConfig, + filterSpec: typeof (filter == null ? void 0 : filter.filter) === "string" ? { + filter: filter.filter + } : { + filter: "" + } + }, + true + ); + } + aggregateRequest(requestId, aggregations) { + this.awaitOperation(requestId, { type: "aggregate", data: aggregations }); + info2 == null ? void 0 : info2(\`aggregateRequest: \${aggregations}\`); + return this.createRequest({ aggregations }); + } + sortRequest(requestId, sort) { + this.awaitOperation(requestId, { type: "sort", data: sort }); + info2 == null ? void 0 : info2(\`sortRequest: \${JSON.stringify(sort.sortDefs)}\`); + return this.createRequest({ sort }); + } + groupByRequest(requestId, groupBy = EMPTY_GROUPBY) { + var _a; + this.awaitOperation(requestId, { type: "groupBy", data: groupBy }); + if (this.useBatchMode) { + this.batchMode = true; + } + if (!this.isTree) { + (_a = this.dataWindow) == null ? void 0 : _a.clear(); + } + return this.createRequest({ groupBy }); + } + selectRequest(requestId, selected) { + this.selectedRows = selected; + this.awaitOperation(requestId, { type: "selection", data: selected }); + info2 == null ? void 0 : info2(\`selectRequest: \${selected}\`); + return { + type: "SET_SELECTION", + vpId: this.serverViewportId, + selection: expandSelection(selected) + }; + } + removePendingRangeRequest(firstIndex, lastIndex) { + for (let i = this.pendingRangeRequests.length - 1; i >= 0; i--) { + const { from, to } = this.pendingRangeRequests[i]; + let isLast = true; + if (firstIndex >= from && firstIndex < to || lastIndex > from && lastIndex < to) { + if (!isLast) { + console.warn( + "removePendingRangeRequest TABLE_ROWS are not for latest request" + ); + } + this.pendingRangeRequests.splice(i, 1); + break; + } else { + isLast = false; + } + } + } + updateRows(rows) { + var _a, _b, _c; + const [firstRow, lastRow] = getFirstAndLastRows(rows); + if (firstRow && lastRow) { + this.removePendingRangeRequest(firstRow.rowIndex, lastRow.rowIndex); + } + if (rows.length === 1) { + if (firstRow.vpSize === 0 && this.disabled) { + debug3 == null ? void 0 : debug3( + \`ignore a SIZE=0 message on disabled viewport (\${rows.length} rows)\` + ); + return; + } else if (firstRow.updateType === "SIZE") { + this.setLastSizeOnlyUpdateSize(firstRow.vpSize); + } + } + for (const row of rows) { + if (this.isTree && isLeafUpdate(row)) { + continue; + } else { + if (row.updateType === "SIZE" || ((_a = this.dataWindow) == null ? void 0 : _a.rowCount) !== row.vpSize) { + (_b = this.dataWindow) == null ? void 0 : _b.setRowCount(row.vpSize); + this.rowCountChanged = true; + } + if (row.updateType === "U") { + if ((_c = this.dataWindow) == null ? void 0 : _c.setAtIndex(row)) { + this.hasUpdates = true; + if (!this.batchMode) { + this.pendingUpdates.push(row); + } + } + } + } + } + } + // This is called only after new data has been received from server - data + // returned direcly from buffer does not use this. + getClientRows() { + let out = void 0; + let mode = "size-only"; + if (!this.hasUpdates && !this.rowCountChanged) { + return NO_DATA_UPDATE; + } + if (this.hasUpdates) { + const { keys, selectedRows } = this; + const toClient = this.isTree ? toClientRowTree : toClientRow; + if (this.updateThrottleTimer) { + self.clearTimeout(this.updateThrottleTimer); + this.updateThrottleTimer = void 0; + } + if (this.pendingUpdates.length > 0) { + out = []; + mode = "update"; + for (const row of this.pendingUpdates) { + out.push(toClient(row, keys, selectedRows)); + } + this.pendingUpdates.length = 0; + } else { + const records = this.dataWindow.getData(); + if (this.dataWindow.hasAllRowsWithinRange) { + out = []; + mode = "batch"; + for (const row of records) { + out.push(toClient(row, keys, selectedRows)); + } + this.batchMode = false; + } + } + this.hasUpdates = false; + } + if (this.throttleMessage(mode)) { + return NO_DATA_UPDATE; + } else { + return [out, mode]; + } + } + createRequest(params, overWrite = false) { + if (overWrite) { + return { + type: "CHANGE_VP", + viewPortId: this.serverViewportId, + ...params + }; + } else { + return { + type: "CHANGE_VP", + viewPortId: this.serverViewportId, + aggregations: this.aggregations, + columns: this.columns, + sort: this.sort, + groupBy: this.groupBy, + filterSpec: { + filter: this.filter.filter + }, + ...params + }; + } + } +}; +var toClientRow = ({ rowIndex, rowKey, sel: isSelected, data }, keys, selectedRows) => { + return [ + rowIndex, + keys.keyFor(rowIndex), + true, + false, + 0, + 0, + rowKey, + isSelected ? getSelectionStatus(selectedRows, rowIndex) : 0 + ].concat(data); +}; +var toClientRowTree = ({ rowIndex, rowKey, sel: isSelected, data }, keys, selectedRows) => { + const [depth, isExpanded, , isLeaf, , count, ...rest] = data; + return [ + rowIndex, + keys.keyFor(rowIndex), + isLeaf, + isExpanded, + depth, + count, + rowKey, + isSelected ? getSelectionStatus(selectedRows, rowIndex) : 0 + ].concat(rest); +}; + +// src/server-proxy/server-proxy.ts +var _requestId = 1; +var { debug: debug4, debugEnabled: debugEnabled4, error: error3, info: info3, infoEnabled: infoEnabled3, warn: warn3 } = logger("server-proxy"); +var nextRequestId = () => \`\${_requestId++}\`; +var DEFAULT_OPTIONS = {}; +var isActiveViewport = (viewPort) => viewPort.disabled !== true && viewPort.suspended !== true; +var NO_ACTION = { + type: "NO_ACTION" +}; +var addTitleToLinks = (links, serverViewportId, label) => links.map( + (link) => link.parentVpId === serverViewportId ? { ...link, label } : link +); +function addLabelsToLinks(links, viewports) { + return links.map((linkDescriptor) => { + const { parentVpId } = linkDescriptor; + const viewport = viewports.get(parentVpId); + if (viewport) { + return { + ...linkDescriptor, + parentClientVpId: viewport.clientViewportId, + label: viewport.title + }; + } else { + throw Error("addLabelsToLinks viewport not found"); + } + }); +} +var ServerProxy = class { + constructor(connection, callback) { + this.authToken = ""; + this.user = "user"; + this.pendingTableMetaRequests = /* @__PURE__ */ new Map(); + this.pendingRequests = /* @__PURE__ */ new Map(); + this.queuedRequests = []; + this.cachedTableSchemas = /* @__PURE__ */ new Map(); + this.connection = connection; + this.postMessageToClient = callback; + this.viewports = /* @__PURE__ */ new Map(); + this.mapClientToServerViewport = /* @__PURE__ */ new Map(); + } + async reconnect() { + await this.login(this.authToken); + const [activeViewports, inactiveViewports] = partition( + Array.from(this.viewports.values()), + isActiveViewport + ); + this.viewports.clear(); + this.mapClientToServerViewport.clear(); + const reconnectViewports = (viewports) => { + viewports.forEach((viewport) => { + const { clientViewportId } = viewport; + this.viewports.set(clientViewportId, viewport); + this.sendMessageToServer(viewport.subscribe(), clientViewportId); + }); + }; + reconnectViewports(activeViewports); + setTimeout(() => { + reconnectViewports(inactiveViewports); + }, 2e3); + } + async login(authToken, user = "user") { + if (authToken) { + this.authToken = authToken; + this.user = user; + return new Promise((resolve, reject) => { + this.sendMessageToServer( + { type: LOGIN, token: this.authToken, user }, + "" + ); + this.pendingLogin = { resolve, reject }; + }); + } else if (this.authToken === "") { + error3("login, cannot login until auth token has been obtained"); + } + } + subscribe(message) { + if (!this.mapClientToServerViewport.has(message.viewport)) { + if (!this.hasSchemaForTable(message.table) && // A Session table is never cached - it is limited to a single workflow interaction + // The metadata for a session table is requested even before the subscribe call. + !isSessionTable(message.table)) { + info3 == null ? void 0 : info3( + \`subscribe to \${message.table.table}, no metadata yet, request metadata\` + ); + const requestId = nextRequestId(); + this.sendMessageToServer( + { type: "GET_TABLE_META", table: message.table }, + requestId + ); + this.pendingTableMetaRequests.set(requestId, message.viewport); + } + const viewport = new Viewport(message, this.postMessageToClient); + this.viewports.set(message.viewport, viewport); + this.sendIfReady( + viewport.subscribe(), + message.viewport, + this.sessionId !== "" + ); + } else { + error3(\`spurious subscribe call \${message.viewport}\`); + } + } + unsubscribe(clientViewportId) { + const serverViewportId = this.mapClientToServerViewport.get(clientViewportId); + if (serverViewportId) { + info3 == null ? void 0 : info3( + \`Unsubscribe Message (Client to Server): + \${serverViewportId}\` + ); + this.sendMessageToServer({ + type: REMOVE_VP, + viewPortId: serverViewportId + }); + } else { + error3( + \`failed to unsubscribe client viewport \${clientViewportId}, viewport not found\` + ); + } + } + getViewportForClient(clientViewportId, throws = true) { + const serverViewportId = this.mapClientToServerViewport.get(clientViewportId); + if (serverViewportId) { + const viewport = this.viewports.get(serverViewportId); + if (viewport) { + return viewport; + } else if (throws) { + throw Error( + \`Viewport not found for client viewport \${clientViewportId}\` + ); + } else { + return null; + } + } else if (this.viewports.has(clientViewportId)) { + return this.viewports.get(clientViewportId); + } else if (throws) { + throw Error( + \`Viewport server id not found for client viewport \${clientViewportId}\` + ); + } else { + return null; + } + } + /**********************************************************************/ + /* Handle messages from client */ + /**********************************************************************/ + setViewRange(viewport, message) { + const requestId = nextRequestId(); + const [serverRequest, rows, debounceRequest] = viewport.rangeRequest( + requestId, + message.range + ); + info3 == null ? void 0 : info3(\`setViewRange \${message.range.from} - \${message.range.to}\`); + if (serverRequest) { + if (true) { + info3 == null ? void 0 : info3( + \`CHANGE_VP_RANGE [\${message.range.from}-\${message.range.to}] => [\${serverRequest.from}-\${serverRequest.to}]\` + ); + } + this.sendIfReady( + serverRequest, + requestId, + viewport.status === "subscribed" + ); + } + if (rows) { + info3 == null ? void 0 : info3(\`setViewRange \${rows.length} rows returned from cache\`); + this.postMessageToClient({ + mode: "batch", + type: "viewport-update", + clientViewportId: viewport.clientViewportId, + rows + }); + } else if (debounceRequest) { + this.postMessageToClient(debounceRequest); + } + } + setConfig(viewport, message) { + const requestId = nextRequestId(); + const request = viewport.setConfig(requestId, message.config); + this.sendIfReady(request, requestId, viewport.status === "subscribed"); + } + aggregate(viewport, message) { + const requestId = nextRequestId(); + const request = viewport.aggregateRequest(requestId, message.aggregations); + this.sendIfReady(request, requestId, viewport.status === "subscribed"); + } + sort(viewport, message) { + const requestId = nextRequestId(); + const request = viewport.sortRequest(requestId, message.sort); + this.sendIfReady(request, requestId, viewport.status === "subscribed"); + } + groupBy(viewport, message) { + const requestId = nextRequestId(); + const request = viewport.groupByRequest(requestId, message.groupBy); + this.sendIfReady(request, requestId, viewport.status === "subscribed"); + } + filter(viewport, message) { + const requestId = nextRequestId(); + const { filter } = message; + const request = viewport.filterRequest(requestId, filter); + this.sendIfReady(request, requestId, viewport.status === "subscribed"); + } + setColumns(viewport, message) { + const requestId = nextRequestId(); + const { columns } = message; + const request = viewport.columnRequest(requestId, columns); + this.sendIfReady(request, requestId, viewport.status === "subscribed"); + } + setTitle(viewport, message) { + if (viewport) { + viewport.title = message.title; + this.updateTitleOnVisualLinks(viewport); + } + } + select(viewport, message) { + const requestId = nextRequestId(); + const { selected } = message; + const request = viewport.selectRequest(requestId, selected); + this.sendIfReady(request, requestId, viewport.status === "subscribed"); + } + disableViewport(viewport) { + const requestId = nextRequestId(); + const request = viewport.disable(requestId); + this.sendIfReady(request, requestId, viewport.status === "subscribed"); + } + enableViewport(viewport) { + if (viewport.disabled) { + const requestId = nextRequestId(); + const request = viewport.enable(requestId); + this.sendIfReady(request, requestId, viewport.status === "subscribed"); + } + } + suspendViewport(viewport) { + viewport.suspend(); + viewport.suspendTimer = setTimeout(() => { + info3 == null ? void 0 : info3("suspendTimer expired, escalate suspend to disable"); + this.disableViewport(viewport); + }, 3e3); + } + resumeViewport(viewport) { + if (viewport.suspendTimer) { + debug4 == null ? void 0 : debug4("clear suspend timer"); + clearTimeout(viewport.suspendTimer); + viewport.suspendTimer = null; + } + const rows = viewport.resume(); + this.postMessageToClient({ + clientViewportId: viewport.clientViewportId, + mode: "batch", + rows, + type: "viewport-update" + }); + } + openTreeNode(viewport, message) { + if (viewport.serverViewportId) { + const requestId = nextRequestId(); + this.sendIfReady( + viewport.openTreeNode(requestId, message), + requestId, + viewport.status === "subscribed" + ); + } + } + closeTreeNode(viewport, message) { + if (viewport.serverViewportId) { + const requestId = nextRequestId(); + this.sendIfReady( + viewport.closeTreeNode(requestId, message), + requestId, + viewport.status === "subscribed" + ); + } + } + createLink(viewport, message) { + const { parentClientVpId, parentColumnName, childColumnName } = message; + const requestId = nextRequestId(); + const parentVpId = this.mapClientToServerViewport.get(parentClientVpId); + if (parentVpId) { + const request = viewport.createLink( + requestId, + childColumnName, + parentVpId, + parentColumnName + ); + this.sendMessageToServer(request, requestId); + } else { + error3("ServerProxy unable to create link, viewport not found"); + } + } + removeLink(viewport) { + const requestId = nextRequestId(); + const request = viewport.removeLink(requestId); + this.sendMessageToServer(request, requestId); + } + updateTitleOnVisualLinks(viewport) { + var _a; + const { serverViewportId, title } = viewport; + for (const vp of this.viewports.values()) { + if (vp !== viewport && vp.links && serverViewportId && title) { + if ((_a = vp.links) == null ? void 0 : _a.some((link) => link.parentVpId === serverViewportId)) { + const [messageToClient] = vp.setLinks( + addTitleToLinks(vp.links, serverViewportId, title) + ); + this.postMessageToClient(messageToClient); + } + } + } + } + removeViewportFromVisualLinks(serverViewportId) { + var _a; + for (const vp of this.viewports.values()) { + if ((_a = vp.links) == null ? void 0 : _a.some(({ parentVpId }) => parentVpId === serverViewportId)) { + const [messageToClient] = vp.setLinks( + vp.links.filter(({ parentVpId }) => parentVpId !== serverViewportId) + ); + this.postMessageToClient(messageToClient); + } + } + } + menuRpcCall(message) { + const viewport = this.getViewportForClient(message.vpId, false); + if (viewport == null ? void 0 : viewport.serverViewportId) { + const [requestId, rpcRequest] = stripRequestId(message); + this.sendMessageToServer( + { + ...rpcRequest, + vpId: viewport.serverViewportId + }, + requestId + ); + } + } + rpcCall(message) { + const [requestId, rpcRequest] = stripRequestId(message); + const module = getRpcServiceModule(rpcRequest.service); + this.sendMessageToServer(rpcRequest, requestId, { module }); + } + handleMessageFromClient(message) { + if (isViewporttMessage(message)) { + if (message.type === "disable") { + const viewport = this.getViewportForClient(message.viewport, false); + if (viewport !== null) { + return this.disableViewport(viewport); + } else { + return; + } + } else { + const viewport = this.getViewportForClient(message.viewport); + switch (message.type) { + case "setViewRange": + return this.setViewRange(viewport, message); + case "config": + return this.setConfig(viewport, message); + case "aggregate": + return this.aggregate(viewport, message); + case "sort": + return this.sort(viewport, message); + case "groupBy": + return this.groupBy(viewport, message); + case "filter": + return this.filter(viewport, message); + case "select": + return this.select(viewport, message); + case "suspend": + return this.suspendViewport(viewport); + case "resume": + return this.resumeViewport(viewport); + case "enable": + return this.enableViewport(viewport); + case "openTreeNode": + return this.openTreeNode(viewport, message); + case "closeTreeNode": + return this.closeTreeNode(viewport, message); + case "createLink": + return this.createLink(viewport, message); + case "removeLink": + return this.removeLink(viewport); + case "setColumns": + return this.setColumns(viewport, message); + case "setTitle": + return this.setTitle(viewport, message); + default: + } + } + } else if (isVuuMenuRpcRequest(message)) { + return this.menuRpcCall(message); + } else { + const { type, requestId } = message; + switch (type) { + case "GET_TABLE_LIST": + return this.sendMessageToServer({ type }, requestId); + case "GET_TABLE_META": + return this.sendMessageToServer( + { type, table: message.table }, + requestId + ); + case "RPC_CALL": + return this.rpcCall(message); + default: + } + } + error3( + \`Vuu ServerProxy Unexpected message from client \${JSON.stringify( + message + )}\` + ); + } + awaitResponseToMessage(message) { + return new Promise((resolve, reject) => { + const requestId = nextRequestId(); + this.sendMessageToServer(message, requestId); + this.pendingRequests.set(requestId, { reject, resolve }); + }); + } + sendIfReady(message, requestId, isReady = true) { + if (isReady) { + this.sendMessageToServer(message, requestId); + } else { + this.queuedRequests.push(message); + } + return isReady; + } + sendMessageToServer(body, requestId = \`\${_requestId++}\`, options = DEFAULT_OPTIONS) { + const { module = "CORE" } = options; + if (this.authToken) { + this.connection.send({ + requestId, + sessionId: this.sessionId, + token: this.authToken, + user: this.user, + module, + body + }); + } + } + handleMessageFromServer(message) { + var _a, _b, _c; + const { body, requestId, sessionId } = message; + const pendingRequest = this.pendingRequests.get(requestId); + if (pendingRequest) { + const { resolve } = pendingRequest; + this.pendingRequests.delete(requestId); + resolve(body); + return; + } + const { viewports } = this; + switch (body.type) { + case HB: + this.sendMessageToServer( + { type: HB_RESP, ts: +/* @__PURE__ */ new Date() }, + "NA" + ); + break; + case LOGIN_SUCCESS: + if (sessionId) { + this.sessionId = sessionId; + (_a = this.pendingLogin) == null ? void 0 : _a.resolve(sessionId); + this.pendingLogin = void 0; + } else { + throw Error("LOGIN_SUCCESS did not provide sessionId"); + } + break; + case CREATE_VP_SUCCESS: + { + const viewport = viewports.get(requestId); + if (viewport) { + const { status: viewportStatus } = viewport; + const { viewPortId: serverViewportId } = body; + if (requestId !== serverViewportId) { + viewports.delete(requestId); + viewports.set(serverViewportId, viewport); + } + this.mapClientToServerViewport.set(requestId, serverViewportId); + const response = viewport.handleSubscribed(body); + if (response) { + this.postMessageToClient(response); + if (debugEnabled4) { + debug4( + \`post DataSourceSubscribedMessage to client: \${JSON.stringify( + response + )}\` + ); + } + } + if (viewport.disabled) { + this.disableViewport(viewport); + } + if (viewportStatus === "subscribing" && // A session table will never have Visual Links, nor Context Menus + !isSessionTable(viewport.table)) { + this.sendMessageToServer({ + type: GET_VP_VISUAL_LINKS, + vpId: serverViewportId + }); + this.sendMessageToServer({ + type: GET_VIEW_PORT_MENUS, + vpId: serverViewportId + }); + Array.from(viewports.entries()).filter( + ([id, { disabled }]) => id !== serverViewportId && !disabled + ).forEach(([vpId]) => { + this.sendMessageToServer({ + type: GET_VP_VISUAL_LINKS, + vpId + }); + }); + } + } + } + break; + case "REMOVE_VP_SUCCESS": + { + const viewport = viewports.get(body.viewPortId); + if (viewport) { + this.mapClientToServerViewport.delete(viewport.clientViewportId); + viewports.delete(body.viewPortId); + this.removeViewportFromVisualLinks(body.viewPortId); + } + } + break; + case SET_SELECTION_SUCCESS: + { + const viewport = this.viewports.get(body.vpId); + if (viewport) { + viewport.completeOperation(requestId); + } + } + break; + case CHANGE_VP_SUCCESS: + case DISABLE_VP_SUCCESS: + if (viewports.has(body.viewPortId)) { + const viewport = this.viewports.get(body.viewPortId); + if (viewport) { + const response = viewport.completeOperation(requestId); + if (response !== void 0) { + this.postMessageToClient(response); + if (debugEnabled4) { + debug4(\`postMessageToClient \${JSON.stringify(response)}\`); + } + } + } + } + break; + case ENABLE_VP_SUCCESS: + { + const viewport = this.viewports.get(body.viewPortId); + if (viewport) { + const response = viewport.completeOperation(requestId); + if (response) { + this.postMessageToClient(response); + const rows = viewport.currentData(); + debugEnabled4 && debug4( + \`Enable Response (ServerProxy to Client): \${JSON.stringify( + response + )}\` + ); + if (viewport.size === 0) { + debugEnabled4 && debug4(\`Viewport Enabled but size 0, resend to server\`); + } else { + this.postMessageToClient({ + clientViewportId: viewport.clientViewportId, + mode: "batch", + rows, + size: viewport.size, + type: "viewport-update" + }); + debugEnabled4 && debug4( + \`Enable Response (ServerProxy to Client): send size \${viewport.size} \${rows.length} rows from cache\` + ); + } + } + } + } + break; + case TABLE_ROW: + { + const viewportRowMap = groupRowsByViewport(body.rows); + if (debugEnabled4) { + const [firstRow, secondRow] = body.rows; + if (body.rows.length === 0) { + debug4("handleMessageFromServer TABLE_ROW 0 rows"); + } else if ((firstRow == null ? void 0 : firstRow.rowIndex) === -1) { + if (body.rows.length === 1) { + if (firstRow.updateType === "SIZE") { + debug4( + \`handleMessageFromServer [\${firstRow.viewPortId}] TABLE_ROW SIZE ONLY \${firstRow.vpSize}\` + ); + } else { + debug4( + \`handleMessageFromServer [\${firstRow.viewPortId}] TABLE_ROW SIZE \${firstRow.vpSize} rowIdx \${firstRow.rowIndex}\` + ); + } + } else { + debug4( + \`handleMessageFromServer TABLE_ROW \${body.rows.length} rows, SIZE \${firstRow.vpSize}, [\${secondRow == null ? void 0 : secondRow.rowIndex}] - [\${(_b = body.rows[body.rows.length - 1]) == null ? void 0 : _b.rowIndex}]\` + ); + } + } else { + debug4( + \`handleMessageFromServer TABLE_ROW \${body.rows.length} rows [\${firstRow == null ? void 0 : firstRow.rowIndex}] - [\${(_c = body.rows[body.rows.length - 1]) == null ? void 0 : _c.rowIndex}]\` + ); + } + } + for (const [viewportId, rows] of Object.entries(viewportRowMap)) { + const viewport = viewports.get(viewportId); + if (viewport) { + viewport.updateRows(rows); + } else { + warn3 == null ? void 0 : warn3( + \`TABLE_ROW message received for non registered viewport \${viewportId}\` + ); + } + } + this.processUpdates(); + } + break; + case CHANGE_VP_RANGE_SUCCESS: + { + const viewport = this.viewports.get(body.viewPortId); + if (viewport) { + const { from, to } = body; + if (true) { + info3 == null ? void 0 : info3(\`CHANGE_VP_RANGE_SUCCESS \${from} - \${to}\`); + } + viewport.completeOperation(requestId, from, to); + } + } + break; + case OPEN_TREE_SUCCESS: + case CLOSE_TREE_SUCCESS: + break; + case "CREATE_VISUAL_LINK_SUCCESS": + { + const viewport = this.viewports.get(body.childVpId); + const parentViewport = this.viewports.get(body.parentVpId); + if (viewport && parentViewport) { + const { childColumnName, parentColumnName } = body; + const response = viewport.completeOperation( + requestId, + childColumnName, + parentViewport.clientViewportId, + parentColumnName + ); + if (response) { + this.postMessageToClient(response); + } + } + } + break; + case "REMOVE_VISUAL_LINK_SUCCESS": + { + const viewport = this.viewports.get(body.childVpId); + if (viewport) { + const response = viewport.completeOperation( + requestId + ); + if (response) { + this.postMessageToClient(response); + } + } + } + break; + case TABLE_LIST_RESP: + this.postMessageToClient({ + type: TABLE_LIST_RESP, + tables: body.tables, + requestId + }); + break; + case TABLE_META_RESP: + { + const tableSchema = this.cacheTableMeta(body); + const clientViewportId = this.pendingTableMetaRequests.get(requestId); + if (clientViewportId) { + this.pendingTableMetaRequests.delete(requestId); + const viewport = this.viewports.get(clientViewportId); + if (viewport) { + viewport.setTableSchema(tableSchema); + } else { + warn3 == null ? void 0 : warn3( + "Message has come back AFTER CREATE_VP_SUCCESS, what do we do now" + ); + } + } else { + this.postMessageToClient({ + type: TABLE_META_RESP, + tableSchema, + requestId + }); + } + } + break; + case "VP_VISUAL_LINKS_RESP": + { + const activeLinkDescriptors = this.getActiveLinks(body.links); + const viewport = this.viewports.get(body.vpId); + if (activeLinkDescriptors.length && viewport) { + const linkDescriptorsWithLabels = addLabelsToLinks( + activeLinkDescriptors, + this.viewports + ); + const [clientMessage, pendingLink] = viewport.setLinks( + linkDescriptorsWithLabels + ); + this.postMessageToClient(clientMessage); + if (pendingLink) { + const { link, parentClientVpId } = pendingLink; + const requestId2 = nextRequestId(); + const serverViewportId = this.mapClientToServerViewport.get(parentClientVpId); + if (serverViewportId) { + const message2 = viewport.createLink( + requestId2, + link.fromColumn, + serverViewportId, + link.toColumn + ); + this.sendMessageToServer(message2, requestId2); + } + } + } + } + break; + case "VIEW_PORT_MENUS_RESP": + if (body.menu.name) { + const viewport = this.viewports.get(body.vpId); + if (viewport) { + const clientMessage = viewport.setMenu(body.menu); + this.postMessageToClient(clientMessage); + } + } + break; + case "VP_EDIT_RPC_RESPONSE": + { + this.postMessageToClient({ + action: body.action, + requestId, + rpcName: body.rpcName, + type: "VP_EDIT_RPC_RESPONSE" + }); + } + break; + case "VP_EDIT_RPC_REJECT": + { + const viewport = this.viewports.get(body.vpId); + if (viewport) { + this.postMessageToClient({ + requestId, + type: "VP_EDIT_RPC_REJECT", + error: body.error + }); + } + } + break; + case "VIEW_PORT_MENU_REJ": { + console.log(\`send menu error back to client\`); + const { error: error4, rpcName, vpId } = body; + const viewport = this.viewports.get(vpId); + if (viewport) { + this.postMessageToClient({ + clientViewportId: viewport.clientViewportId, + error: error4, + rpcName, + type: "VIEW_PORT_MENU_REJ", + requestId + }); + } + break; + } + case "VIEW_PORT_MENU_RESP": + { + if (isSessionTableActionMessage(body)) { + const { action, rpcName } = body; + this.awaitResponseToMessage({ + type: "GET_TABLE_META", + table: action.table + }).then((response) => { + const tableSchema = createSchemaFromTableMetadata( + response + ); + this.postMessageToClient({ + rpcName, + type: "VIEW_PORT_MENU_RESP", + action: { + ...action, + tableSchema + }, + tableAlreadyOpen: this.isTableOpen(action.table), + requestId + }); + }); + } else { + const { action } = body; + this.postMessageToClient({ + type: "VIEW_PORT_MENU_RESP", + action: action || NO_ACTION, + tableAlreadyOpen: action !== null && this.isTableOpen(action.table), + requestId + }); + } + } + break; + case RPC_RESP: + { + const { method, result } = body; + this.postMessageToClient({ + type: RPC_RESP, + method, + result, + requestId + }); + } + break; + case "ERROR": + error3(body.msg); + break; + default: + infoEnabled3 && info3(\`handleMessageFromServer \${body["type"]}.\`); + } + } + hasSchemaForTable(table) { + return this.cachedTableSchemas.has(\`\${table.module}:\${table.table}\`); + } + cacheTableMeta(messageBody) { + const { module, table } = messageBody.table; + const key = \`\${module}:\${table}\`; + let tableSchema = this.cachedTableSchemas.get(key); + if (!tableSchema) { + tableSchema = createSchemaFromTableMetadata(messageBody); + this.cachedTableSchemas.set(key, tableSchema); + } + return tableSchema; + } + isTableOpen(table) { + if (table) { + const tableName = table.table; + for (const viewport of this.viewports.values()) { + if (!viewport.suspended && viewport.table.table === tableName) { + return true; + } + } + } + } + // Eliminate links to suspended viewports + getActiveLinks(linkDescriptors) { + return linkDescriptors.filter((linkDescriptor) => { + const viewport = this.viewports.get(linkDescriptor.parentVpId); + return viewport && !viewport.suspended; + }); + } + processUpdates() { + this.viewports.forEach((viewport) => { + var _a; + if (viewport.hasUpdatesToProcess) { + const result = viewport.getClientRows(); + if (result !== NO_DATA_UPDATE) { + const [rows, mode] = result; + const size = viewport.getNewRowCount(); + if (size !== void 0 || rows && rows.length > 0) { + debugEnabled4 && debug4( + \`postMessageToClient #\${viewport.clientViewportId} viewport-update \${mode}, \${(_a = rows == null ? void 0 : rows.length) != null ? _a : "no"} rows, size \${size}\` + ); + if (mode) { + this.postMessageToClient({ + clientViewportId: viewport.clientViewportId, + mode, + rows, + size, + type: "viewport-update" + }); + } + } + } + } + }); + } +}; + +// src/worker.ts +var server; +var { info: info4, infoEnabled: infoEnabled4 } = logger("worker"); +async function connectToServer(url, protocol, token, username, onConnectionStatusChange, retryLimitDisconnect, retryLimitStartup) { + const connection = await connect( + url, + protocol, + // if this was called during connect, we would get a ReferenceError, but it will + // never be called until subscriptions have been made, so this is safe. + //TODO do we need to listen in to the connection messages here so we can lock back in, in the event of a reconnenct ? + (msg) => { + if (isConnectionQualityMetrics(msg)) { + console.log("post connection metrics"); + postMessage({ type: "connection-metrics", messages: msg }); + } else if (isConnectionStatusMessage(msg)) { + onConnectionStatusChange(msg); + if (msg.status === "reconnected") { + server.reconnect(); + } + } else { + server.handleMessageFromServer(msg); + } + }, + retryLimitDisconnect, + retryLimitStartup + ); + server = new ServerProxy(connection, (msg) => sendMessageToClient(msg)); + if (connection.requiresLogin) { + await server.login(token, username); + } +} +function sendMessageToClient(message) { + postMessage(message); +} +var handleMessageFromClient = async ({ + data: message +}) => { + switch (message.type) { + case "connect": + await connectToServer( + message.url, + message.protocol, + message.token, + message.username, + postMessage, + message.retryLimitDisconnect, + message.retryLimitStartup + ); + postMessage({ type: "connected" }); + break; + case "subscribe": + infoEnabled4 && info4(\`client subscribe: \${JSON.stringify(message)}\`); + server.subscribe(message); + break; + case "unsubscribe": + infoEnabled4 && info4(\`client unsubscribe: \${JSON.stringify(message)}\`); + server.unsubscribe(message.viewport); + break; + default: + infoEnabled4 && info4(\`client message: \${JSON.stringify(message)}\`); + server.handleMessageFromClient(message); + } +}; +self.addEventListener("message", handleMessageFromClient); +postMessage({ type: "ready" }); `; \ No newline at end of file diff --git a/vuu-ui/packages/vuu-data/src/json-data-source.ts b/vuu-ui/packages/vuu-data/src/json-data-source.ts index b7879cf58..c50b74078 100644 --- a/vuu-ui/packages/vuu-data/src/json-data-source.ts +++ b/vuu-ui/packages/vuu-data/src/json-data-source.ts @@ -8,6 +8,7 @@ import { ClientToServerMenuRPC, ClientToServerEditRpc, VuuColumnDataType, + VuuRowDataItemType, } from "@finos/vuu-protocol-types"; import { DataSourceFilter, DataSourceRow } from "@finos/vuu-data-types"; import { @@ -23,6 +24,7 @@ import type { DataSource, DataSourceConstructorProps, DataSourceEvents, + DataSourceStatus, SubscribeCallback, SubscribeProps, WithFullConfig, @@ -54,7 +56,6 @@ export class JsonDataSource extends EventEmitter implements DataSource { - private status = "initialising"; public columnDescriptors: ColumnDescriptor[]; private clientCallback: SubscribeCallback | undefined; private expandedRows = new Set(); @@ -69,6 +70,7 @@ export class JsonDataSource #selectedRowsCount = 0; #size = 0; #sort: VuuSort = { sortDefs: [] }; + #status: DataSourceStatus = "initialising"; #title: string | undefined; public rowCount: number | undefined; @@ -156,14 +158,14 @@ export class JsonDataSource this.#sort = sort; } - if (this.status !== "initialising") { + if (this.#status !== "initialising") { //TODO check if subscription details are still the same return; } this.viewport = viewport; - this.status = "subscribed"; + this.#status = "subscribed"; this.clientCallback?.({ aggregations: this.#aggregations, @@ -208,6 +210,19 @@ export class JsonDataSource console.log("noop"); return this; } + set data(data: JsonData) { + console.log(`set JsonDataSource data`); + [this.columnDescriptors, this.#data] = jsonToDataSourceRows(data); + this.visibleRows = this.#data + .filter((row) => row[DEPTH] === 0) + .map((row, index) => + ([index, index] as Partial).concat(row.slice(2)) + ) as DataSourceRow[]; + + requestAnimationFrame(() => { + this.sendRowsToClient(); + }); + } select(selected: Selection) { const updatedRows: DataSourceRow[] = []; @@ -262,6 +277,10 @@ export class JsonDataSource this.sendRowsToClient(); } + get status() { + return this.#status; + } + get config() { return this.#config; } @@ -386,9 +405,15 @@ export class JsonDataSource return undefined; } - applyEdit(rowIndex: number, columnName: string, value: VuuColumnDataType) { - console.log(`ArrayDataSource applyEdit ${rowIndex} ${columnName} ${value}`); - return true; + applyEdit( + row: DataSourceRow, + columnName: string, + value: VuuRowDataItemType + ): Promise { + console.log( + `ArrayDataSource applyEdit ${row.join(",")} ${columnName} ${value}` + ); + return Promise.resolve(true); } getChildRows(rowKey: 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 1d81b5807..4400783db 100644 --- a/vuu-ui/packages/vuu-data/src/remote-data-source.ts +++ b/vuu-ui/packages/vuu-data/src/remote-data-source.ts @@ -5,9 +5,10 @@ import { ClientToServerMenuRPC, LinkDescriptorWithLabel, VuuAggregation, - VuuColumnDataType, VuuGroupBy, + VuuMenu, VuuRange, + VuuRowDataItemType, VuuSort, VuuTable, } from "@finos/vuu-protocol-types"; @@ -30,6 +31,7 @@ import { DataSourceConfig, DataSourceConstructorProps, DataSourceEvents, + DataSourceStatus, isDataSourceConfigMessage, OptimizeStrategy, SubscribeCallback, @@ -38,7 +40,12 @@ import { withConfigDefaults, WithFullConfig, } from "./data-source"; -import { MenuRpcResponse } from "./vuuUIMessageTypes"; + +import { + isViewportMenusAction, + isVisualLinksAction, + MenuRpcResponse, +} from "./vuuUIMessageTypes"; type RangeRequest = (range: VuuRange) => void; @@ -46,17 +53,6 @@ const { info } = logger("RemoteDataSource"); const { KEY } = metadataKeys; -type DataSourceStatus = - | "disabled" - | "disabling" - | "enabled" - | "enabling" - | "initialising" - | "subscribing" - | "subscribed" - | "suspended" - | "unsubscribed"; - /*----------------------------------------------------------------- A RemoteDataSource manages a single subscription via the ServerProxy ----------------------------------------------------------------*/ @@ -66,17 +62,20 @@ export class RemoteDataSource { private bufferSize: number; private server: ServerAPI | null = null; - private status: DataSourceStatus = "initialising"; private clientCallback: SubscribeCallback | undefined; private configChangePending: DataSourceConfig | undefined; private rangeRequest: RangeRequest; #config: WithFullConfig = vanillaConfig; #groupBy: VuuGroupBy = []; + #links: LinkDescriptorWithLabel[] | undefined; + #menu: VuuMenu | undefined; #optimize: OptimizeStrategy = "throttle"; #range: VuuRange = { from: 0, to: 0 }; #selectedRowsCount = 0; #size = 0; + #status: DataSourceStatus = "initialising"; + #title: string | undefined; public table: VuuTable; @@ -119,7 +118,7 @@ export class RemoteDataSource async subscribe( { - viewport = this.viewport ?? uuid(), + viewport = this.viewport ?? (this.viewport = uuid()), columns, aggregations, range, @@ -130,7 +129,6 @@ export class RemoteDataSource callback: SubscribeCallback ) { this.clientCallback = callback; - if (aggregations || columns || filter || groupBy || sort) { this.#config = { ...this.#config, @@ -149,11 +147,18 @@ export class RemoteDataSource this.#range = range; } - if (this.status !== "initialising" && this.status !== "unsubscribed") { + if ( + this.#status !== "initialising" && + this.#status !== "unsubscribed" && + // We can subscribe to a disabled dataSource. No request will be + // sent to server to create a new VP, just to enable the existing one. + // The current subscribing client becomes the subscription owner + this.#status !== "disabled" + ) { return; } - this.status = "subscribing"; + this.#status = "subscribing"; this.viewport = viewport; this.server = await getServerAPI(); @@ -175,12 +180,12 @@ export class RemoteDataSource handleMessageFromServer = (message: DataSourceCallbackMessage) => { if (message.type === "subscribed") { - this.status = "subscribed"; + this.#status = "subscribed"; this.clientCallback?.(message); } else if (message.type === "disabled") { - this.status = "disabled"; + this.#status = "disabled"; } else if (message.type === "enabled") { - this.status = "enabled"; + this.#status = "enabled"; } else if (isDataSourceConfigMessage(message)) { // This is an ACK for a CHANGE_VP message. Nothing to do here. We need // to wait for data to be returned before we can consider the change @@ -204,7 +209,15 @@ export class RemoteDataSource if (this.configChangePending) { this.setConfigPending(); } - this.clientCallback?.(message); + + if (isViewportMenusAction(message)) { + this.#menu = message.menu; + } else if (isVisualLinksAction(message)) { + this.#links = message.links as LinkDescriptorWithLabel[]; + } else { + this.clientCallback?.(message); + } + if (this.optimize === "debounce") { this.revertDebounce(); } @@ -219,15 +232,15 @@ export class RemoteDataSource this.server?.destroy(this.viewport); this.server = null; this.removeAllListeners(); - this.status = "unsubscribed"; + this.#status = "unsubscribed"; this.viewport = undefined; this.range = { from: 0, to: 0 }; } suspend() { - info?.(`suspend #${this.viewport}, current status ${this.status}`); + info?.(`suspend #${this.viewport}, current status ${this.#status}`); if (this.viewport) { - this.status = "suspended"; + this.#status = "suspended"; this.server?.send({ type: "suspend", viewport: this.viewport, @@ -237,25 +250,25 @@ export class RemoteDataSource } resume() { - info?.(`resume #${this.viewport}, current status ${this.status}`); + info?.(`resume #${this.viewport}, current status ${this.#status}`); if (this.viewport) { - if (this.status === "disabled" || this.status === "disabling") { + if (this.#status === "disabled" || this.#status === "disabling") { this.enable(); - } else if (this.status === "suspended") { + } else if (this.#status === "suspended") { this.server?.send({ type: "resume", viewport: this.viewport, }); - this.status = "subscribed"; + this.#status = "subscribed"; } } return this; } disable() { - info?.(`disable #${this.viewport}, current status ${this.status}`); + info?.(`disable #${this.viewport}, current status ${this.#status}`); if (this.viewport) { - this.status = "disabling"; + this.#status = "disabling"; this.server?.send({ viewport: this.viewport, type: "disable", @@ -264,13 +277,16 @@ export class RemoteDataSource return this; } - enable() { - info?.(`enable #${this.viewport}, current status ${this.status}`); + enable(callback?: SubscribeCallback) { + info?.(`enable #${this.viewport}, current status ${this.#status}`); if ( this.viewport && - (this.status === "disabled" || this.status === "disabling") + (this.#status === "disabled" || this.#status === "disabling") ) { - this.status = "enabling"; + this.#status = "enabling"; + if (callback) { + this.clientCallback = callback; + } this.server?.send({ viewport: this.viewport, type: "enable", @@ -280,6 +296,8 @@ export class RemoteDataSource } select(selected: Selection) { + //TODO this isn't always going to be correct - need to count + // selection block items this.#selectedRowsCount = selected.length; if (this.viewport) { this.server?.send({ @@ -310,6 +328,18 @@ export class RemoteDataSource } } + get links() { + return this.#links; + } + + get menu() { + return this.#menu; + } + + get status() { + return this.#status; + } + get optimize() { return this.#optimize; } @@ -616,15 +646,18 @@ export class RemoteDataSource } } - applyEdit(row: DataSourceRow, columnName: string, value: VuuColumnDataType) { - this.menuRpcCall({ + applyEdit(row: DataSourceRow, columnName: string, value: VuuRowDataItemType) { + return this.menuRpcCall({ rowKey: row[KEY], field: columnName, - value: parseInt(value), + value: value, type: "VP_EDIT_CELL_RPC", - }).then(() => { - console.log("response"); + }).then((response) => { + if (response?.error) { + return response.error; + } else { + return true; + } }); - return true; } } diff --git a/vuu-ui/packages/vuu-data/src/server-proxy/messages.ts b/vuu-ui/packages/vuu-data/src/server-proxy/messages.ts index 6c98788bc..0fc1f7e4a 100644 --- a/vuu-ui/packages/vuu-data/src/server-proxy/messages.ts +++ b/vuu-ui/packages/vuu-data/src/server-proxy/messages.ts @@ -5,14 +5,12 @@ export const CLOSE_TREE_SUCCESS = "CLOSE_TREE_SUCCESS"; export const CLOSE_TREE_REJECT = "CLOSE_TREE_REJECT"; export const CREATE_VISUAL_LINK = "CREATE_VISUAL_LINK"; export const CREATE_VP = "CREATE_VP"; -export const CREATE_VP_SUCCESS = "CREATE_VP_SUCCESS"; export const DISABLE_VP = "DISABLE_VP"; export const DISABLE_VP_SUCCESS = "DISABLE_VP_SUCCESS"; export const DISABLE_VP_REJECT = "DISABLE_VP_REJECT"; export const ENABLE_VP = "ENABLE_VP"; export const ENABLE_VP_SUCCESS = "ENABLE_VP_SUCCESS"; export const ENABLE_VP_REJECT = "ENABLE_VP_REJECT"; -export const GET_TABLE_LIST = "GET_TABLE_LIST"; export const GET_TABLE_META = "GET_TABLE_META"; export const GET_VP_VISUAL_LINKS = "GET_VP_VISUAL_LINKS"; export const GET_VIEW_PORT_MENUS = "GET_VIEW_PORT_MENUS"; @@ -25,7 +23,6 @@ export const VIEW_PORT_MENU_REJ = "VIEW_PORT_MENU_REJ"; export const HB = "HB"; export const HB_RESP = "HB_RESP"; export const LOGIN = "LOGIN"; -export const LOGIN_SUCCESS = "LOGIN_SUCCESS"; export const OPEN_TREE_NODE = "OPEN_TREE_NODE"; export const OPEN_TREE_SUCCESS = "OPEN_TREE_SUCCESS"; export const OPEN_TREE_REJECT = "OPEN_TREE_REJECT"; @@ -36,8 +33,6 @@ export const RPC_RESP = "RPC_RESP"; export const MENU_RPC_RESP = "MENU_RPC_RESP"; export const SET_SELECTION = "SET_SELECTION"; export const SET_SELECTION_SUCCESS = "SET_SELECTION_SUCCESS"; -export const TABLE_META_RESP = "TABLE_META_RESP"; -export const TABLE_LIST_RESP = "TABLE_LIST_RESP"; export const TABLE_ROW = "TABLE_ROW"; export const SIZE = "SIZE"; 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 34a8f09f3..58c7d2d1b 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 @@ -3,7 +3,9 @@ import { ClientToServerMenuRPC, ClientToServerMessage, LinkDescriptorWithLabel, + ServerToClientCreateViewPortSuccess, ServerToClientMessage, + ServerToClientTableList, ServerToClientTableMeta, VuuLinkDescriptor, VuuRpcRequest, @@ -33,8 +35,6 @@ import { OpenDialogAction, ServerProxySubscribeMessage, VuuUIMessageIn, - VuuUIMessageInTableList, - VuuUIMessageInTableMeta, VuuUIMessageOut, VuuUIMessageOutAggregate, VuuUIMessageOutCloseTreeNode, @@ -125,11 +125,15 @@ export class ServerProxy { private authToken = ""; private user = "user"; private pendingLogin?: PendingLogin; - private pendingTableMetaRequests = new Map(); private pendingRequests = new Map(); private sessionId?: string; private queuedRequests: Array = []; - private cachedTableSchemas: Map> = new Map(); + private cachedTableMetaRequests: Map< + string, + Promise + > = new Map(); + private cachedTableSchemas: Map = new Map(); + private tableList: Promise | undefined; constructor(connection: Connection, callback: PostMessageToClientCallback) { this.connection = connection; @@ -188,32 +192,84 @@ export class ServerProxy { public subscribe(message: ServerProxySubscribeMessage) { // guard against subscribe message when a viewport is already subscribed if (!this.mapClientToServerViewport.has(message.viewport)) { - if ( - !this.hasSchemaForTable(message.table) && - // A Session table is never cached - it is limited to a single workflow interaction - // The metadata for a session table is requested even before the subscribe call. - !isSessionTable(message.table) - ) { - info?.( - `subscribe to ${message.table.table}, no metadata yet, request metadata` - ); - const requestId = nextRequestId(); - this.sendMessageToServer( - { type: "GET_TABLE_META", table: message.table }, - requestId - ); - this.pendingTableMetaRequests.set(requestId, 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, + // 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 - this.sendIfReady( + //TODO handle CREATE_VP error, but server does not send it at the moment + const pendingSubscription = this.awaitResponseToMessage( viewport.subscribe(), - message.viewport, - this.sessionId !== "" + message.viewport ); + const awaitPendingReponses = Promise.all([ + pendingSubscription, + pendingTableSchema, + ]) as Promise< + [ServerToClientCreateViewPortSuccess, TableSchema | undefined] + >; + awaitPendingReponses.then(([subscribeResponse, tableSchema]) => { + const { viewPortId: serverViewportId } = subscribeResponse; + const { status: viewportStatus } = viewport; + + // switch storage key from client viewportId to server viewportId + if (message.viewport !== serverViewportId) { + this.viewports.delete(message.viewport); + this.viewports.set(serverViewportId, viewport); + } + this.mapClientToServerViewport.set(message.viewport, serverViewportId); + + const clientResponse = viewport.handleSubscribed( + subscribeResponse, + tableSchema + ); + if (clientResponse) { + this.postMessageToClient(clientResponse); + if (debugEnabled) { + debug( + `post DataSourceSubscribedMessage to client: ${JSON.stringify( + clientResponse + )}` + ); + } + } + + // In the case of a reconnect, we may have resubscribed a disabled viewport, + // reset the disabled state on server + if (viewport.disabled) { + this.disableViewport(viewport); + } + + if ( + viewportStatus === "subscribing" && + // A session table will never have Visual Links, nor Context Menus + !isSessionTable(viewport.table) + ) { + // If status is "resubscribing", the following is unnecessary + this.sendMessageToServer({ + type: Message.GET_VP_VISUAL_LINKS, + vpId: serverViewportId, + }); + this.sendMessageToServer({ + type: Message.GET_VIEW_PORT_MENUS, + vpId: serverViewportId, + }); + + // Resend requests for links from other viewports already on page, they may be linkable to this viewport + Array.from(this.viewports.entries()) + .filter( + ([id, { disabled }]) => id !== serverViewportId && !disabled + ) + .forEach(([vpId]) => { + this.sendMessageToServer({ + type: Message.GET_VP_VISUAL_LINKS, + vpId, + }); + }); + } + }); } else { error(`spurious subscribe call ${message.viewport}`); } @@ -558,13 +614,33 @@ export class ServerProxy { } else { const { type, requestId } = message; switch (type) { - case "GET_TABLE_LIST": - return this.sendMessageToServer({ type }, requestId); - case "GET_TABLE_META": - return this.sendMessageToServer( - { type, table: message.table }, + case "GET_TABLE_LIST": { + this.tableList ??= this.awaitResponseToMessage( + { type }, requestId - ); + ) as Promise; + this.tableList.then((response) => { + this.postMessageToClient({ + type: "TABLE_LIST_RESP", + tables: response.tables, + requestId, + }); + }); + return; + } + + case "GET_TABLE_META": { + this.getTableMeta(message.table, requestId).then((tableSchema) => { + if (tableSchema) { + this.postMessageToClient({ + type: "TABLE_META_RESP", + tableSchema, + requestId, + }); + } + }); + return; + } case "RPC_CALL": return this.rpcCall(message); default: @@ -577,11 +653,27 @@ export class ServerProxy { ); } + private getTableMeta(table: VuuTable, requestId = nextRequestId()) { + if (isSessionTable(table)) { + return Promise.resolve(undefined); + } + const key = `${table.module}:${table.table}`; + let tableMetaRequest = this.cachedTableMetaRequests.get(key); + if (!tableMetaRequest) { + tableMetaRequest = this.awaitResponseToMessage( + { type: "GET_TABLE_META", table }, + requestId + ) as Promise; + this.cachedTableMetaRequests.set(key, tableMetaRequest); + } + return tableMetaRequest?.then((response) => this.cacheTableMeta(response)); + } + private awaitResponseToMessage( - message: ClientToServerBody + message: ClientToServerBody, + requestId = nextRequestId() ): Promise { return new Promise((resolve, reject) => { - const requestId = nextRequestId(); this.sendMessageToServer(message, requestId); this.pendingRequests.set(requestId, { reject, resolve }); }); @@ -642,7 +734,7 @@ export class ServerProxy { ); break; - case Message.LOGIN_SUCCESS: + case "LOGIN_SUCCESS": if (sessionId) { this.sessionId = sessionId; // we should tear down the pending Login now @@ -654,68 +746,6 @@ export class ServerProxy { break; // TODO login rejected - case Message.CREATE_VP_SUCCESS: - { - const viewport = viewports.get(requestId); - // The clientViewportId was used as requestId for CREATE_VP message. From this point, - // we will key viewports using serverViewPortId and maintain a mapping between client - // and server viewport ids. - if (viewport) { - const { status: viewportStatus } = viewport; - const { viewPortId: serverViewportId } = body; - - if (requestId !== serverViewportId) { - viewports.delete(requestId); - viewports.set(serverViewportId, viewport); - } - this.mapClientToServerViewport.set(requestId, serverViewportId); - const response = viewport.handleSubscribed(body); - if (response) { - this.postMessageToClient(response); - if (debugEnabled) { - debug( - `post DataSourceSubscribedMessage to client: ${JSON.stringify( - response - )}` - ); - } - } - // In the case of a reconnect, we may have resubscribed a disabled viewport, - // reset the disabled state on server - if (viewport.disabled) { - this.disableViewport(viewport); - } - if ( - viewportStatus === "subscribing" && - // A session table will never have Visual Links, nor Context Menus - !isSessionTable(viewport.table) - ) { - // If status is "resubscribing", the following is unnecessary - this.sendMessageToServer({ - type: Message.GET_VP_VISUAL_LINKS, - vpId: serverViewportId, - }); - this.sendMessageToServer({ - type: Message.GET_VIEW_PORT_MENUS, - vpId: serverViewportId, - }); - - // Resend requests for links from other viewports already on page, they may be linkable to this viewport - Array.from(viewports.entries()) - .filter( - ([id, { disabled }]) => id !== serverViewportId && !disabled - ) - .forEach(([vpId]) => { - this.sendMessageToServer({ - type: Message.GET_VP_VISUAL_LINKS, - vpId, - }); - }); - } - } - } - break; - case "REMOVE_VP_SUCCESS": { const viewport = viewports.get(body.viewPortId); @@ -755,6 +785,7 @@ export class ServerProxy { case Message.ENABLE_VP_SUCCESS: { + //TODO resend menus, links etc to client const viewport = this.viewports.get(body.viewPortId); if (viewport) { const response = viewport.completeOperation(requestId); @@ -890,43 +921,6 @@ export class ServerProxy { } break; - case Message.TABLE_LIST_RESP: - this.postMessageToClient({ - type: Message.TABLE_LIST_RESP, - tables: body.tables, - requestId, - } as VuuUIMessageInTableList); - break; - - case Message.TABLE_META_RESP: - // This request may have originated from client or may have been made by - // ServerProxy whilst creating a new subscription - { - const tableSchema = this.cacheTableMeta(body); - const clientViewportId = this.pendingTableMetaRequests.get(requestId); - if (clientViewportId) { - this.pendingTableMetaRequests.delete(requestId); - // If the viewport is still stored under clientViewportId, the subscription has not - // yet been acknowledged and client not informed. If the subscription has already - // been acknowledged, the viewport will be stored under serverViewportId; - const viewport = this.viewports.get(clientViewportId); - if (viewport) { - viewport.setTableSchema(tableSchema); - } else { - warn?.( - "Message has come back AFTER CREATE_VP_SUCCESS, what do we do now" - ); - } - } else { - this.postMessageToClient({ - type: Message.TABLE_META_RESP, - tableSchema, - requestId, - } as VuuUIMessageInTableMeta); - } - } - break; - case "VP_VISUAL_LINKS_RESP": { const activeLinkDescriptors = this.getActiveLinks(body.links); @@ -993,6 +987,23 @@ export class ServerProxy { } break; + case "VIEW_PORT_MENU_REJ": { + console.log(`send menu error back to client`); + const { error, rpcName, vpId } = body; + const viewport = this.viewports.get(vpId); + if (viewport) { + this.postMessageToClient({ + clientViewportId: viewport.clientViewportId, + error, + rpcName, + type: "VIEW_PORT_MENU_REJ", + requestId, + }); + } + + break; + } + case "VIEW_PORT_MENU_RESP": { if (isSessionTableActionMessage(body)) { @@ -1055,10 +1066,6 @@ export class ServerProxy { } } - private hasSchemaForTable(table: VuuTable) { - return this.cachedTableSchemas.has(`${table.module}:${table.table}`); - } - private cacheTableMeta(messageBody: ServerToClientTableMeta): TableSchema { const { module, table } = messageBody.table; const key = `${module}:${table}`; 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 3858fcab2..ec0678bab 100644 --- a/vuu-ui/packages/vuu-data/src/server-proxy/viewport.ts +++ b/vuu-ui/packages/vuu-data/src/server-proxy/viewport.ts @@ -182,7 +182,6 @@ export class Viewport { private postMessageToClient: (message: DataSourceCallbackMessage) => void; private rowCountChanged = false; private selectedRows: Selection = []; - private tableSchema: TableSchema | null = null; private useBatchMode = true; private lastUpdateStatus: LastUpdateStatus = NO_UPDATE_STATUS; private updateThrottleTimer: number | undefined = undefined; @@ -299,15 +298,18 @@ export class Viewport { } as ClientToServerCreateViewPort; } - handleSubscribed({ - viewPortId, - aggregations, - columns, - filterSpec: filter, - range, - sort, - groupBy, - }: ServerToClientCreateViewPortSuccess) { + handleSubscribed( + { + viewPortId, + aggregations, + columns, + filterSpec: filter, + range, + sort, + groupBy, + }: ServerToClientCreateViewPortSuccess, + tableSchema?: TableSchema + ) { this.serverViewportId = viewPortId; this.status = "subscribed"; this.aggregations = aggregations; @@ -330,7 +332,7 @@ export class Viewport { groupBy, range, sort, - tableSchema: this.tableSchema, + tableSchema, } as DataSourceSubscribedMessage; } @@ -572,10 +574,6 @@ export class Viewport { }; } - setTableSchema(tableSchema: TableSchema) { - this.tableSchema = tableSchema; - } - openTreeNode(requestId: string, message: VuuUIMessageOutOpenTreeNode) { if (this.useBatchMode) { this.batchMode = true; diff --git a/vuu-ui/packages/vuu-data/src/vuuUIMessageTypes.ts b/vuu-ui/packages/vuu-data/src/vuuUIMessageTypes.ts index c416bc5b6..29ed6e16a 100644 --- a/vuu-ui/packages/vuu-data/src/vuuUIMessageTypes.ts +++ b/vuu-ui/packages/vuu-data/src/vuuUIMessageTypes.ts @@ -14,8 +14,16 @@ import { } from "@finos/vuu-protocol-types"; import { DataSourceFilter } from "@finos/vuu-data-types"; import { TableSchema, WithRequestId } from "./message-utils"; -import { WithFullConfig } from "./data-source"; -import { Selection } from "@finos/vuu-datagrid-types"; +import { + DataSourceMenusMessage, + DataSourceVisualLinkCreatedMessage, + DataSourceVisualLinkRemovedMessage, + DataSourceVisualLinksMessage, + VuuFeatureInvocationMessage, + VuuFeatureMessage, + WithFullConfig, +} from "./data-source"; +import { GridAction, Selection } from "@finos/vuu-datagrid-types"; import { WebSocketProtocol } from "./websocket-connection"; export interface OpenDialogAction { @@ -134,12 +142,26 @@ export interface MenuRpcResponse { tableAlreadyOpen?: boolean; type: "VIEW_PORT_MENU_RESP"; } +export interface MenuRpcReject extends ViewportMessageIn { + error?: string; + requestId: string; + rpcName?: string; + type: "VIEW_PORT_MENU_REJ"; +} + +export interface VuuUIMessageInMenuRej { + error: string; + requestId: string; + rpcName: string; + type: "VIEW_PORT_MENU_REJ"; +} export type VuuUIMessageIn = | VuuUIMessageInConnected | VuuUIMessageInWorkerReady | VuuUIMessageInRPC | MenuRpcResponse + | MenuRpcReject | VuuUIMessageInTableList | VuuUIMessageInTableMeta | VuuUIMessageInRPCEditReject @@ -333,3 +355,31 @@ export const isSessionTable = (table?: unknown) => { } return false; }; + +export const isVisualLinksAction = ( + action: GridAction +): action is DataSourceVisualLinksMessage => action.type === "vuu-links"; + +export const isVisualLinkCreatedAction = ( + action: GridAction +): action is DataSourceVisualLinkCreatedMessage => + action.type === "vuu-link-created"; + +export const isVisualLinkRemovedAction = ( + action: GridAction +): action is DataSourceVisualLinkRemovedMessage => + action.type === "vuu-link-removed"; + +export const isViewportMenusAction = ( + action: GridAction +): action is DataSourceMenusMessage => action.type === "vuu-menu"; + +export const isVuuFeatureAction = ( + action: GridAction +): action is VuuFeatureMessage => + isViewportMenusAction(action) || isVisualLinksAction(action); + +export const isVuuFeatureInvocation = ( + action: GridAction +): action is VuuFeatureInvocationMessage => + action.type === "vuu-link-created" || action.type === "vuu-link-removed"; diff --git a/vuu-ui/packages/vuu-data/test/server-proxy-throttle.test.ts b/vuu-ui/packages/vuu-data/test/server-proxy-throttle.test.ts index 0b611d7b9..f7e7b7e7f 100644 --- a/vuu-ui/packages/vuu-data/test/server-proxy-throttle.test.ts +++ b/vuu-ui/packages/vuu-data/test/server-proxy-throttle.test.ts @@ -36,9 +36,9 @@ const mockConnection = { }; describe("ServerProxy 'size-only throttling'", () => { - it("passes a size only message through to UI client", () => { + it("passes a size only message through to UI client", async () => { const postMessageToClient = vi.fn(); - const serverProxy = createServerProxyAndSubscribeToViewport( + const serverProxy = await createServerProxyAndSubscribeToViewport( postMessageToClient, { connection: mockConnection, 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 0824c8771..bd3d6972a 100644 --- a/vuu-ui/packages/vuu-data/test/server-proxy.test.ts +++ b/vuu-ui/packages/vuu-data/test/server-proxy.test.ts @@ -1,5 +1,5 @@ import "./global-mocks"; -import { describe, expect, vi, it } from "vitest"; +import { beforeEach, describe, expect, vi, it } from "vitest"; import { ServerProxy, TEST_setRequestId, @@ -14,6 +14,8 @@ import { createTableGroupRows, createSubscription, sizeRow, + subscribe, + testSchema, updateTableRow, } from "./test-utils"; import { @@ -30,32 +32,72 @@ const SERVER_MESSAGE_CONSTANTS = { user: "user", }; -const mockConnection = { - send: vi.fn(), - status: "ready" as const, -}; - -const noop = () => undefined; - describe("ServerProxy", () => { + beforeEach(() => { + TEST_setRequestId(1); + }); + describe("subscription", () => { - it("creates Viewport on client subscribe", () => { - const [clientSubscription] = createSubscription(); - const serverProxy = new ServerProxy(mockConnection, noop); - serverProxy.subscribe(clientSubscription); - expect(serverProxy["viewports"].size).toEqual(1); - const { clientViewportId, status } = serverProxy["viewports"].get( - "client-vp-1" - ) as Viewport; - expect(clientViewportId).toEqual("client-vp-1"); - expect(status).toEqual("subscribing"); + it("sends server requests for metadata, links and menus along with subscription", async () => { + const postMessageToClient = vi.fn(); + const connection = { + send: vi.fn(), + status: "ready" as const, + }; + + await createServerProxyAndSubscribeToViewport(postMessageToClient, { + connection, + }); + + expect(connection.send).toBeCalledTimes(4); + + expect(connection.send).toHaveBeenNthCalledWith(1, { + body: { + table: { module: "TEST", table: "test-table" }, + type: "GET_TABLE_META", + }, + requestId: "1", + ...SERVER_MESSAGE_CONSTANTS, + }); + + expect(connection.send).toHaveBeenNthCalledWith(2, { + body: { + aggregations: [], + columns: ["col-1"], + type: "CREATE_VP", + table: { module: "TEST", table: "test-table" }, + range: { from: 0, to: 10 }, + sort: { sortDefs: [] }, + filterSpec: { filter: "" }, + groupBy: [], + }, + requestId: "client-vp-1", + ...SERVER_MESSAGE_CONSTANTS, + }); + + expect(connection.send).toHaveBeenNthCalledWith(3, { + body: { + type: "GET_VP_VISUAL_LINKS", + vpId: "server-vp-1", + }, + requestId: "2", + ...SERVER_MESSAGE_CONSTANTS, + }); + expect(connection.send).toHaveBeenNthCalledWith(4, { + body: { + type: "GET_VIEW_PORT_MENUS", + vpId: "server-vp-1", + }, + requestId: "3", + ...SERVER_MESSAGE_CONSTANTS, + }); }); - it("initialises Viewport when server ACKS subscription", () => { + it("initialises Viewport when server ACKS subscription", async () => { const postMessageToClient = vi.fn(); - const serverProxy = - createServerProxyAndSubscribeToViewport(postMessageToClient); - + const serverProxy = await createServerProxyAndSubscribeToViewport( + postMessageToClient + ); expect(serverProxy["viewports"].size).toEqual(1); expect( serverProxy["mapClientToServerViewport"].get("client-vp-1") @@ -68,15 +110,12 @@ describe("ServerProxy", () => { expect(status).toEqual("subscribed"); }); - it("sends message to client once subscribed", () => { - const callback = vi.fn(); - const [clientSubscription, serverSubscription] = createSubscription(); - const serverProxy = new ServerProxy(mockConnection, callback); - serverProxy.subscribe(clientSubscription); - serverProxy.handleMessageFromServer(serverSubscription); - //TODO cover tableSchema in test - expect(callback).toHaveBeenCalledTimes(1); - expect(callback).toHaveBeenCalledWith({ + it("sends message to client once subscribed", async () => { + const postMessageToClient = vi.fn(); + await createServerProxyAndSubscribeToViewport(postMessageToClient); + + expect(postMessageToClient).toHaveBeenCalledTimes(1); + expect(postMessageToClient).toHaveBeenCalledWith({ aggregations: [], clientViewportId: "client-vp-1", columns: ["col-1", "col-2", "col-3", "col-4"], @@ -89,17 +128,20 @@ describe("ServerProxy", () => { sort: { sortDefs: [], }, - tableSchema: null, + tableSchema: testSchema, type: "subscribed", }); }); }); describe("Data Handling", () => { - it("sends data to client when initial full dataset is received", () => { + it("sends data to client when initial full dataset is received", async () => { const postMessageToClient = vi.fn(); - const serverProxy = - createServerProxyAndSubscribeToViewport(postMessageToClient); + const serverProxy = await createServerProxyAndSubscribeToViewport( + postMessageToClient + ); + + postMessageToClient.mockClear(); serverProxy.handleMessageFromServer({ ...COMMON_ATTRS, @@ -133,10 +175,13 @@ describe("ServerProxy", () => { ); }); - it("sends data to client once all data for client range is available", () => { + it("sends data to client once all data for client range is available", async () => { const postMessageToClient = vi.fn(); - const serverProxy = - createServerProxyAndSubscribeToViewport(postMessageToClient); + const serverProxy = await createServerProxyAndSubscribeToViewport( + postMessageToClient + ); + + postMessageToClient.mockClear(); serverProxy.handleMessageFromServer({ ...COMMON_ATTRS, @@ -191,10 +236,11 @@ describe("ServerProxy", () => { }); describe("Scrolling, no buffer", () => { - it("scrolls forward, partial viewport", () => { + it("scrolls forward, partial viewport", async () => { const postMessageToClient = vi.fn(); - const serverProxy = - createServerProxyAndSubscribeToViewport(postMessageToClient); + const serverProxy = await createServerProxyAndSubscribeToViewport( + postMessageToClient + ); serverProxy.handleMessageFromServer({ ...COMMON_ATTRS, @@ -255,10 +301,11 @@ describe("ServerProxy", () => { }); }); - it("scrolls forward, discrete viewport", () => { + it("scrolls forward, discrete viewport", async () => { const postMessageToClient = vi.fn(); - const serverProxy = - createServerProxyAndSubscribeToViewport(postMessageToClient); + const serverProxy = await createServerProxyAndSubscribeToViewport( + postMessageToClient + ); serverProxy.handleMessageFromServer({ ...COMMON_ATTRS, @@ -320,10 +367,11 @@ describe("ServerProxy", () => { }); describe("Updates", () => { - it("Updates, no scrolling, only sends updated rows to client", () => { + it("Updates, no scrolling, only sends updated rows to client", async () => { const postMessageToClient = vi.fn(); - const serverProxy = - createServerProxyAndSubscribeToViewport(postMessageToClient); + const serverProxy = await createServerProxyAndSubscribeToViewport( + postMessageToClient + ); serverProxy.handleMessageFromServer({ ...COMMON_ATTRS, @@ -357,34 +405,21 @@ describe("ServerProxy", () => { }); describe("Buffering data", () => { - it("buffers 10 rows, server sends entire buffer set", () => { - const [clientSubscription1, serverSubscriptionAck1] = createSubscription({ - bufferSize: 10, - }); - + it("buffers 10 rows, server sends entire buffer set", async () => { const postMessageToClient = vi.fn(); + const connection = { + send: vi.fn(), + status: "ready" as const, + }; + const serverProxy = await createServerProxyAndSubscribeToViewport( + postMessageToClient, + { + bufferSize: 10, + connection, + } + ); - const serverProxy = new ServerProxy(mockConnection, postMessageToClient); - serverProxy["sessionId"] = "dsdsd"; - serverProxy["authToken"] = "test"; - - mockConnection.send.mockClear(); - TEST_setRequestId(1); - - serverProxy.subscribe(clientSubscription1); - - expect(mockConnection.send).toBeCalledTimes(2); - - expect(mockConnection.send).toHaveBeenNthCalledWith(1, { - body: { - table: clientSubscription1.table, - type: "GET_TABLE_META", - }, - requestId: "1", - ...SERVER_MESSAGE_CONSTANTS, - }); - - expect(mockConnection.send).toHaveBeenNthCalledWith(2, { + expect(connection.send).toHaveBeenNthCalledWith(2, { body: { aggregations: [], columns: ["col-1"], @@ -399,8 +434,6 @@ describe("ServerProxy", () => { ...SERVER_MESSAGE_CONSTANTS, }); - serverProxy.handleMessageFromServer(serverSubscriptionAck1); - postMessageToClient.mockClear(); serverProxy.handleMessageFromServer({ @@ -433,20 +466,22 @@ describe("ServerProxy", () => { }); }); - it("10 rows in grid, so 11 requested, (render buffer 0), 10 rows in Viewport buffer, page down, narrowing of range by 1 row", () => { - const [clientSubscription1, serverSubscriptionAck1] = createSubscription({ - bufferSize: 10, - to: 11, - }); - + it("10 rows in grid, so 11 requested, (render buffer 0), 10 rows in Viewport buffer, page down, narrowing of range by 1 row", async () => { const postMessageToClient = vi.fn(); - const serverProxy = new ServerProxy(mockConnection, postMessageToClient); - serverProxy["sessionId"] = "dsdsd"; - serverProxy["authToken"] = "test"; - - serverProxy.subscribe(clientSubscription1); + const connection = { + send: vi.fn(), + status: "ready" as const, + }; + const serverProxy = await createServerProxyAndSubscribeToViewport( + postMessageToClient, + { + bufferSize: 10, + connection, + to: 11, + } + ); - expect(mockConnection.send).toHaveBeenCalledWith({ + expect(connection.send).toHaveBeenNthCalledWith(2, { body: { aggregations: [], columns: ["col-1"], @@ -464,8 +499,6 @@ describe("ServerProxy", () => { user: "user", }); - serverProxy.handleMessageFromServer(serverSubscriptionAck1); - postMessageToClient.mockClear(); serverProxy.handleMessageFromServer({ @@ -526,15 +559,17 @@ describe("ServerProxy", () => { }); }); - it("buffers 10 rows, server sends partial buffer set, enough to fulfill client request, followed by rest", () => { + it("buffers 10 rows, server sends partial buffer set, enough to fulfill client request, followed by rest", async () => { const postMessageToClient = vi.fn(); - const serverProxy = createServerProxyAndSubscribeToViewport( + const serverProxy = await createServerProxyAndSubscribeToViewport( postMessageToClient, { bufferSize: 10, } ); + postMessageToClient.mockClear(); + serverProxy.handleMessageFromServer({ ...COMMON_ATTRS, body: { @@ -577,15 +612,17 @@ describe("ServerProxy", () => { expect(postMessageToClient).toHaveBeenCalledTimes(0); }); - it("buffers 10 rows, server sends partial buffer set, not enough to fulfill client request, followed by rest", () => { + it("buffers 10 rows, server sends partial buffer set, not enough to fulfill client request, followed by rest", async () => { const postMessageToClient = vi.fn(); - const serverProxy = createServerProxyAndSubscribeToViewport( + const serverProxy = await createServerProxyAndSubscribeToViewport( postMessageToClient, { bufferSize: 10, } ); + postMessageToClient.mockClear(); + serverProxy.handleMessageFromServer({ ...COMMON_ATTRS, body: { @@ -636,7 +673,7 @@ describe("ServerProxy", () => { postMessageToClient.mockClear(); - // This will be a buffer top-up only, so no callback + // This will be a buffer top-up only, so no postMessageToClient serverProxy.handleMessageFromServer({ ...COMMON_ATTRS, body: { @@ -650,19 +687,23 @@ describe("ServerProxy", () => { }); describe("scrolling, with buffer", () => { - it("scroll to end", () => { - const [clientSubscription1, serverSubscriptionAck1] = createSubscription({ - to: 20, - bufferSize: 100, - }); + it("scroll to end", async () => { + const postMessageToClient = vi.fn(); + const connection = { + send: vi.fn(), + status: "ready" as const, + }; - const callback = vi.fn(); - const serverProxy = new ServerProxy(mockConnection, callback); - serverProxy["sessionId"] = "dsdsd"; - serverProxy["authToken"] = "test"; + const serverProxy = await createServerProxyAndSubscribeToViewport( + postMessageToClient, + { + bufferSize: 100, + connection, + to: 20, + } + ); - serverProxy.subscribe(clientSubscription1); - serverProxy.handleMessageFromServer(serverSubscriptionAck1); + postMessageToClient.mockClear(); serverProxy.handleMessageFromServer({ ...COMMON_ATTRS, @@ -675,8 +716,8 @@ describe("ServerProxy", () => { }, }); - callback.mockClear(); - mockConnection.send.mockClear(); + postMessageToClient.mockClear(); + connection.send.mockClear(); TEST_setRequestId(1); serverProxy.handleMessageFromClient({ @@ -685,9 +726,9 @@ describe("ServerProxy", () => { range: { from: 4975, to: 5000 }, }); - expect(callback).toHaveBeenCalledTimes(0); - expect(mockConnection.send).toHaveBeenCalledTimes(1); - expect(mockConnection.send).toHaveBeenCalledWith({ + expect(postMessageToClient).toHaveBeenCalledTimes(0); + expect(connection.send).toHaveBeenCalledTimes(1); + expect(connection.send).toHaveBeenCalledWith({ body: { viewPortId: "server-vp-1", type: "CHANGE_VP_RANGE", @@ -712,7 +753,7 @@ describe("ServerProxy", () => { }, }); - expect(callback).toHaveBeenCalledTimes(0); + expect(postMessageToClient).toHaveBeenCalledTimes(0); serverProxy.handleMessageFromServer({ ...COMMON_ATTRS, @@ -722,10 +763,10 @@ describe("ServerProxy", () => { }, }); - expect(callback).toHaveBeenCalledTimes(1); + expect(postMessageToClient).toHaveBeenCalledTimes(1); // prettier-ignore - expect(callback).toHaveBeenCalledWith({ + expect(postMessageToClient).toHaveBeenCalledWith({ mode: "batch", type: "viewport-update", clientViewportId: "client-vp-1", @@ -759,17 +800,22 @@ describe("ServerProxy", () => { }); }); - it("returns client range requests from buffer, if available. Calls server when end of buffer is approached", () => { - const [clientSubscription1, serverSubscriptionAck1] = createSubscription({ - bufferSize: 10, - }); - const callback = vi.fn(); - const serverProxy = new ServerProxy(mockConnection, callback); - serverProxy["sessionId"] = "dsdsd"; - serverProxy["authToken"] = "test"; + it("returns client range requests from buffer, if available. Calls server when end of buffer is approached", async () => { + const postMessageToClient = vi.fn(); + const connection = { + send: vi.fn(), + status: "ready" as const, + }; + + const serverProxy = await createServerProxyAndSubscribeToViewport( + postMessageToClient, + { + bufferSize: 10, + connection, + } + ); - serverProxy.subscribe(clientSubscription1); - serverProxy.handleMessageFromServer(serverSubscriptionAck1); + postMessageToClient.mockClear(); serverProxy.handleMessageFromServer({ ...COMMON_ATTRS, @@ -779,8 +825,8 @@ describe("ServerProxy", () => { }, }); - callback.mockClear(); - mockConnection.send.mockClear(); + postMessageToClient.mockClear(); + connection.send.mockClear(); serverProxy.handleMessageFromClient({ viewport: "client-vp-1", @@ -788,11 +834,11 @@ describe("ServerProxy", () => { range: { from: 2, to: 12 }, }); - expect(mockConnection.send).toHaveBeenCalledTimes(0); - expect(callback).toHaveBeenCalledTimes(1); + expect(connection.send).toHaveBeenCalledTimes(0); + expect(postMessageToClient).toHaveBeenCalledTimes(1); // prettier-ignore - expect(callback).toHaveBeenCalledWith({ + expect(postMessageToClient).toHaveBeenCalledWith({ mode: "batch", type: "viewport-update", clientViewportId: "client-vp-1", @@ -802,8 +848,8 @@ describe("ServerProxy", () => { ], }); - callback.mockClear(); - mockConnection.send.mockClear(); + postMessageToClient.mockClear(); + connection.send.mockClear(); serverProxy.handleMessageFromClient({ viewport: "client-vp-1", @@ -811,11 +857,11 @@ describe("ServerProxy", () => { range: { from: 5, to: 15 }, }); - expect(mockConnection.send).toHaveBeenCalledTimes(0); - expect(callback).toHaveBeenCalledTimes(1); + expect(connection.send).toHaveBeenCalledTimes(0); + expect(postMessageToClient).toHaveBeenCalledTimes(1); // prettier-ignore - expect(callback).toHaveBeenCalledWith({ + expect(postMessageToClient).toHaveBeenCalledWith({ mode: "batch", type: "viewport-update", clientViewportId: "client-vp-1", @@ -826,8 +872,8 @@ describe("ServerProxy", () => { ], }); - callback.mockClear(); - mockConnection.send.mockClear(); + postMessageToClient.mockClear(); + connection.send.mockClear(); TEST_setRequestId(1); serverProxy.handleMessageFromClient({ @@ -836,11 +882,11 @@ describe("ServerProxy", () => { range: { from: 8, to: 18 }, }); - expect(mockConnection.send).toHaveBeenCalledTimes(1); - expect(callback).toHaveBeenCalledTimes(1); + expect(connection.send).toHaveBeenCalledTimes(1); + expect(postMessageToClient).toHaveBeenCalledTimes(1); // prettier-ignore - expect(callback).toHaveBeenCalledWith({ + expect(postMessageToClient).toHaveBeenCalledWith({ mode: "batch", type: "viewport-update", clientViewportId: "client-vp-1", @@ -851,7 +897,7 @@ describe("ServerProxy", () => { ], }); - expect(mockConnection.send).toHaveBeenCalledWith({ + expect(connection.send).toHaveBeenCalledWith({ user: "user", body: { viewPortId: "server-vp-1", @@ -866,18 +912,17 @@ describe("ServerProxy", () => { }); }); - it("records sent to client when enough data available, client scrolls before initial rows rendered", () => { - const [clientSubscription1, serverSubscriptionAck1] = createSubscription({ - bufferSize: 10, - }); + it("records sent to client when enough data available, client scrolls before initial rows rendered", async () => { + const postMessageToClient = vi.fn(); - // 1) subscribe for rows [0,10] - const callback = vi.fn(); - const serverProxy = new ServerProxy(mockConnection, callback); - serverProxy.subscribe(clientSubscription1); - serverProxy.handleMessageFromServer(serverSubscriptionAck1); + const serverProxy = await createServerProxyAndSubscribeToViewport( + postMessageToClient, + { + bufferSize: 10, + } + ); - callback.mockClear(); + postMessageToClient.mockClear(); // 2) server with responds with just rows [0 ... 4] serverProxy.handleMessageFromServer({ @@ -889,15 +934,15 @@ describe("ServerProxy", () => { }); // 3) Do not have entire set requested by user, so only size is initially returned - expect(callback).toHaveBeenCalledTimes(1); - expect(callback).toHaveBeenCalledWith({ + expect(postMessageToClient).toHaveBeenCalledTimes(1); + expect(postMessageToClient).toHaveBeenCalledWith({ mode: "size-only", type: "viewport-update", clientViewportId: "client-vp-1", size: 100, }); - callback.mockClear(); + postMessageToClient.mockClear(); // 4) now client scrolls, before initial data sent serverProxy.handleMessageFromClient({ @@ -906,7 +951,7 @@ describe("ServerProxy", () => { range: { from: 2, to: 12 }, }); - expect(callback).toHaveBeenCalledTimes(0); + expect(postMessageToClient).toHaveBeenCalledTimes(0); serverProxy.handleMessageFromServer({ ...COMMON_ATTRS, @@ -916,9 +961,9 @@ describe("ServerProxy", () => { }, }); - expect(callback).toHaveBeenCalledTimes(0); + expect(postMessageToClient).toHaveBeenCalledTimes(0); - callback.mockClear(); + postMessageToClient.mockClear(); serverProxy.handleMessageFromServer({ ...COMMON_ATTRS, @@ -928,10 +973,10 @@ describe("ServerProxy", () => { }, }); - expect(callback).toHaveBeenCalledTimes(1); + expect(postMessageToClient).toHaveBeenCalledTimes(1); // prettier-ignore - expect(callback).toHaveBeenCalledWith({ + expect(postMessageToClient).toHaveBeenCalledWith({ mode: "batch", type: "viewport-update", clientViewportId: "client-vp-1", @@ -950,19 +995,17 @@ describe("ServerProxy", () => { }); }); - it("data sequence is correct when scrolling backward, data arrives from server in multiple batches", () => { - const [clientSubscription1, serverSubscriptionAck1] = createSubscription({ - bufferSize: 10, - }); - // Client requests rows 0..10 with viewport buffersize of 10 - const callback = vi.fn(); - const serverProxy = new ServerProxy(mockConnection, callback); + it("data sequence is correct when scrolling backward, data arrives from server in multiple batches", async () => { + const postMessageToClient = vi.fn(); - serverProxy["sessionId"] = "dsdsd"; - serverProxy["authToken"] = "test"; + const serverProxy = await createServerProxyAndSubscribeToViewport( + postMessageToClient, + { + bufferSize: 10, + } + ); - serverProxy.subscribe(clientSubscription1); - serverProxy.handleMessageFromServer(serverSubscriptionAck1); + postMessageToClient.mockClear(); // This translates into server call for rows 0..20 these are all stored in Viewport cache // and rows 0..10 returned to client @@ -974,7 +1017,7 @@ describe("ServerProxy", () => { }, }); - callback.mockClear(); + postMessageToClient.mockClear(); // Client now requests 20..30, with the buffer this translates to 15..35. // We have 0..20 in Viewport cache, so 0..15 will be discarded and 20..30 @@ -1003,9 +1046,9 @@ describe("ServerProxy", () => { rows: [...createTableRows("server-vp-1", 20, 35, 100, 2)], }, }); - expect(callback).toHaveBeenCalledTimes(1); + expect(postMessageToClient).toHaveBeenCalledTimes(1); - callback.mockClear(); + postMessageToClient.mockClear(); // Client now requests 12..22 (scrolled backwards) which expands to 7..27 Viewport cache // contains 15..35 so we discard 27..35 and keep 15..27. We can expect 7..15 from server. @@ -1018,7 +1061,7 @@ describe("ServerProxy", () => { type: "setViewRange", range: { from: 12, to: 22 }, }); - expect(callback).toHaveBeenCalledTimes(0); + expect(postMessageToClient).toHaveBeenCalledTimes(0); serverProxy.handleMessageFromServer({ ...COMMON_ATTRS, @@ -1041,9 +1084,9 @@ describe("ServerProxy", () => { rows: [...createTableRows("server-vp-1", 13, 15, 100, 3)], }, }); - expect(callback).toHaveBeenCalledTimes(0); + expect(postMessageToClient).toHaveBeenCalledTimes(0); - callback.mockClear(); + postMessageToClient.mockClear(); // We get the remaining rows we requested. Viewport cache now contains full 7..27 // and we have all the rows from the client range, so we can take this together with @@ -1055,10 +1098,10 @@ describe("ServerProxy", () => { rows: [...createTableRows("server-vp-1", 7, 13, 100, 4)], }, }); - expect(callback).toHaveBeenCalledTimes(1); + expect(postMessageToClient).toHaveBeenCalledTimes(1); // prettier-ignore - expect(callback).toHaveBeenCalledWith({ + expect(postMessageToClient).toHaveBeenCalledWith({ mode: "batch", type: "viewport-update", clientViewportId: "client-vp-1", @@ -1077,15 +1120,22 @@ describe("ServerProxy", () => { }); }); - it("Scrolling with large buffer. Keys are recomputed on each scroll. Calls server when end of buffer is approached", () => { - const [clientSubscription1, serverSubscriptionAck1] = createSubscription({ - bufferSize: 100, - }); + it("Scrolling with large buffer. Keys are recomputed on each scroll. Calls server when end of buffer is approached", async () => { + const postMessageToClient = vi.fn(); + const connection = { + send: vi.fn(), + status: "ready" as const, + }; + + const serverProxy = await createServerProxyAndSubscribeToViewport( + postMessageToClient, + { + bufferSize: 100, + connection, + } + ); - const callback = vi.fn(); - const serverProxy = new ServerProxy(mockConnection, callback); - serverProxy.subscribe(clientSubscription1); - serverProxy.handleMessageFromServer(serverSubscriptionAck1); + postMessageToClient.mockClear(); serverProxy.handleMessageFromServer({ ...COMMON_ATTRS, @@ -1110,8 +1160,8 @@ describe("ServerProxy", () => { to: 10, }); - callback.mockClear(); - mockConnection.send.mockClear(); + postMessageToClient.mockClear(); + connection.send.mockClear(); serverProxy.handleMessageFromClient({ viewport: "client-vp-1", @@ -1119,11 +1169,11 @@ describe("ServerProxy", () => { range: { from: 12, to: 23 }, }); - expect(mockConnection.send).toHaveBeenCalledTimes(0); - expect(callback).toHaveBeenCalledTimes(1); + expect(connection.send).toHaveBeenCalledTimes(0); + expect(postMessageToClient).toHaveBeenCalledTimes(1); // prettier-ignore - expect(callback).toHaveBeenCalledWith({ + expect(postMessageToClient).toHaveBeenCalledWith({ mode: "batch", type: "viewport-update", clientViewportId: "client-vp-1", @@ -1142,8 +1192,8 @@ describe("ServerProxy", () => { ], }); - callback.mockClear(); - mockConnection.send.mockClear(); + postMessageToClient.mockClear(); + connection.send.mockClear(); serverProxy.handleMessageFromClient({ viewport: "client-vp-1", @@ -1151,11 +1201,11 @@ describe("ServerProxy", () => { range: { from: 30, to: 40 }, }); - expect(mockConnection.send).toHaveBeenCalledTimes(0); - expect(callback).toHaveBeenCalledTimes(1); + expect(connection.send).toHaveBeenCalledTimes(0); + expect(postMessageToClient).toHaveBeenCalledTimes(1); // prettier-ignore - expect(callback).toHaveBeenCalledWith({ + expect(postMessageToClient).toHaveBeenCalledWith({ mode: "batch", type: "viewport-update", clientViewportId: "client-vp-1", @@ -1176,13 +1226,17 @@ describe("ServerProxy", () => { }); describe("synchronising with server", () => { - it("does not spam server when buffer limit reached and server request already in-flight", () => { - TEST_setRequestId(1); + it("does not spam server when buffer limit reached and server request already in-flight", async () => { + const connection = { + send: vi.fn(), + status: "ready" as const, + }; + const postMessageToClient = vi.fn(); // prettier-ignore - const serverProxy = createServerProxyAndSubscribeToViewport( + const serverProxy = await createServerProxyAndSubscribeToViewport( postMessageToClient, - { bufferSize: 20, connection: mockConnection } + { bufferSize: 20, connection } ); TEST_setRequestId(1); @@ -1197,7 +1251,7 @@ describe("ServerProxy", () => { }); postMessageToClient.mockClear(); - mockConnection.send.mockClear(); + connection.send.mockClear(); // 2) Client requests rows 16..26 . although non-contiguous with previous request, we already have // full client range in viewport buffer. We need to read ahead from server, because we're close to @@ -1208,11 +1262,11 @@ describe("ServerProxy", () => { range: { from: 16, to: 26 }, }); - expect(mockConnection.send).toHaveBeenCalledTimes(1); + expect(connection.send).toHaveBeenCalledTimes(1); expect(postMessageToClient).toHaveBeenCalledTimes(1); // TODO test for the call to get nmetadata as well - expect(mockConnection.send).toHaveBeenCalledWith({ + expect(connection.send).toHaveBeenCalledWith({ user: "user", body: { viewPortId: "server-vp-1", @@ -1227,7 +1281,7 @@ describe("ServerProxy", () => { }); postMessageToClient.mockClear(); - mockConnection.send.mockClear(); + connection.send.mockClear(); // Client requests 17..27 before we have received response to previous request. The // request in-flight already covers this range. We have the data in cache to satisfy @@ -1238,11 +1292,11 @@ describe("ServerProxy", () => { range: { from: 17, to: 27 }, }); - expect(mockConnection.send).toHaveBeenCalledTimes(0); + expect(connection.send).toHaveBeenCalledTimes(0); expect(postMessageToClient).toHaveBeenCalledTimes(1); postMessageToClient.mockClear(); - mockConnection.send.mockClear(); + connection.send.mockClear(); // client requests 18..28 same deal as above serverProxy.handleMessageFromClient({ @@ -1251,18 +1305,21 @@ describe("ServerProxy", () => { range: { from: 18, to: 28 }, }); - expect(mockConnection.send).toHaveBeenCalledTimes(0); + expect(connection.send).toHaveBeenCalledTimes(0); expect(postMessageToClient).toHaveBeenCalledTimes(1); }); - it("re-requests data from server even before receiving results", () => { - TEST_setRequestId(1); + it("re-requests data from server even before receiving results", async () => { const postMessageToClient = vi.fn(); - const serverProxy = createServerProxyAndSubscribeToViewport( + const connection = { + send: vi.fn(), + status: "ready" as const, + }; + const serverProxy = await createServerProxyAndSubscribeToViewport( postMessageToClient, { bufferSize: 20, - connection: mockConnection, + connection, } ); @@ -1276,7 +1333,7 @@ describe("ServerProxy", () => { }); postMessageToClient.mockClear(); - mockConnection.send.mockClear(); + connection.send.mockClear(); TEST_setRequestId(1); @@ -1288,11 +1345,11 @@ describe("ServerProxy", () => { range: { from: 16, to: 26 }, }); - expect(mockConnection.send).toHaveBeenCalledTimes(1); + expect(connection.send).toHaveBeenCalledTimes(1); expect(postMessageToClient).toHaveBeenCalledTimes(1); // buffer size is 20 so we will have requested +/- 10 around the client range - expect(mockConnection.send).toHaveBeenCalledWith({ + expect(connection.send).toHaveBeenCalledWith({ user: "user", body: { viewPortId: "server-vp-1", @@ -1319,7 +1376,7 @@ describe("ServerProxy", () => { }); postMessageToClient.mockClear(); - mockConnection.send.mockClear(); + connection.send.mockClear(); // 4) client scrolls forward again, before we have received previously requested rows. We already have // a request in flight, so we don't send another. We have all rows client needs. @@ -1329,11 +1386,11 @@ describe("ServerProxy", () => { range: { from: 17, to: 27 }, }); - expect(mockConnection.send).toHaveBeenCalledTimes(0); + expect(connection.send).toHaveBeenCalledTimes(0); expect(postMessageToClient).toHaveBeenCalledTimes(1); postMessageToClient.mockClear(); - mockConnection.send.mockClear(); + connection.send.mockClear(); TEST_setRequestId(1); @@ -1345,10 +1402,10 @@ describe("ServerProxy", () => { range: { from: 24, to: 34 }, }); - expect(mockConnection.send).toHaveBeenCalledTimes(1); + expect(connection.send).toHaveBeenCalledTimes(1); expect(postMessageToClient).toHaveBeenCalledTimes(0); - expect(mockConnection.send).toHaveBeenCalledWith({ + expect(connection.send).toHaveBeenCalledWith({ user: "user", body: { viewPortId: "server-vp-1", @@ -1365,15 +1422,16 @@ describe("ServerProxy", () => { }); describe("growing and shrinking rowset (Orders)", () => { - it("initializes with rowset that does not fill client viewport", () => { - const [clientSubscription1, serverSubscriptionAck1] = createSubscription({ - to: 20, - bufferSize: 100, - }); + it("initializes with rowset that does not fill client viewport", async () => { const postMessageToClient = vi.fn(); - const serverProxy = new ServerProxy(mockConnection, postMessageToClient); - serverProxy.subscribe(clientSubscription1); - serverProxy.handleMessageFromServer(serverSubscriptionAck1); + + const serverProxy = await createServerProxyAndSubscribeToViewport( + postMessageToClient, + { + bufferSize: 100, + to: 20, + } + ); postMessageToClient.mockClear(); @@ -1410,15 +1468,17 @@ describe("ServerProxy", () => { }); }); - it("gradually reduces, then grows viewport", () => { - const [clientSubscription1, serverSubscriptionAck1] = createSubscription({ - to: 20, - bufferSize: 100, - }); - const callback = vi.fn(); - const serverProxy = new ServerProxy(mockConnection, callback); - serverProxy.subscribe(clientSubscription1); - serverProxy.handleMessageFromServer(serverSubscriptionAck1); + it("gradually reduces, then grows viewport", async () => { + const postMessageToClient = vi.fn(); + const serverProxy = await createServerProxyAndSubscribeToViewport( + postMessageToClient, + { + bufferSize: 100, + to: 20, + } + ); + + postMessageToClient.mockClear(); const timeNow = Date.now(); console.log(`time now ${timeNow}`); @@ -1435,7 +1495,7 @@ describe("ServerProxy", () => { }, }); - callback.mockClear(); + postMessageToClient.mockClear(); serverProxy.handleMessageFromServer({ ...COMMON_ATTRS, @@ -1456,9 +1516,9 @@ describe("ServerProxy", () => { }, }); - // callbacks will be size only - expect(callback).toHaveBeenCalledTimes(1); - expect(callback).toHaveBeenCalledWith({ + // postMessageToClients will be size only + expect(postMessageToClient).toHaveBeenCalledTimes(1); + expect(postMessageToClient).toHaveBeenCalledWith({ mode: "size-only", type: "viewport-update", clientViewportId: "client-vp-1", @@ -1467,7 +1527,7 @@ describe("ServerProxy", () => { vi.setSystemTime(timeNow + 10); - callback.mockClear(); + postMessageToClient.mockClear(); serverProxy.handleMessageFromServer({ ...COMMON_ATTRS, body: { @@ -1475,8 +1535,8 @@ describe("ServerProxy", () => { rows: [sizeRow("server-vp-1", 8)], }, }); - expect(callback).toHaveBeenCalledTimes(1); - expect(callback).toHaveBeenCalledWith({ + expect(postMessageToClient).toHaveBeenCalledTimes(1); + expect(postMessageToClient).toHaveBeenCalledWith({ type: "viewport-update", mode: "size-only", clientViewportId: "client-vp-1", @@ -1485,7 +1545,7 @@ describe("ServerProxy", () => { vi.setSystemTime(timeNow + 20); - callback.mockClear(); + postMessageToClient.mockClear(); serverProxy.handleMessageFromServer({ ...COMMON_ATTRS, body: { @@ -1493,15 +1553,15 @@ describe("ServerProxy", () => { rows: [sizeRow("server-vp-1", 1)], }, }); - expect(callback).toHaveBeenCalledTimes(1); - expect(callback).toHaveBeenCalledWith({ + expect(postMessageToClient).toHaveBeenCalledTimes(1); + expect(postMessageToClient).toHaveBeenCalledWith({ mode: "size-only", type: "viewport-update", clientViewportId: "client-vp-1", size: 1, }); - callback.mockClear(); + postMessageToClient.mockClear(); serverProxy.handleMessageFromServer({ ...COMMON_ATTRS, body: { @@ -1510,15 +1570,15 @@ describe("ServerProxy", () => { }, }); // fails intermittent;y with 0 - expect(callback).toHaveBeenCalledTimes(1); - expect(callback).toHaveBeenCalledWith({ + expect(postMessageToClient).toHaveBeenCalledTimes(1); + expect(postMessageToClient).toHaveBeenCalledWith({ mode: "size-only", type: "viewport-update", clientViewportId: "client-vp-1", size: 0, }); - callback.mockClear(); + postMessageToClient.mockClear(); serverProxy.handleMessageFromServer({ ...COMMON_ATTRS, body: { @@ -1529,9 +1589,9 @@ describe("ServerProxy", () => { ], }, }); - expect(callback).toHaveBeenCalledTimes(1); + expect(postMessageToClient).toHaveBeenCalledTimes(1); // prettier-ignore - expect(callback).toHaveBeenCalledWith({ + expect(postMessageToClient).toHaveBeenCalledWith({ mode: "update", // WRONG type: "viewport-update", clientViewportId: "client-vp-1", @@ -1541,7 +1601,7 @@ describe("ServerProxy", () => { ], }); - callback.mockClear(); + postMessageToClient.mockClear(); serverProxy.handleMessageFromServer({ ...COMMON_ATTRS, body: { @@ -1552,9 +1612,9 @@ describe("ServerProxy", () => { ], }, }); - expect(callback).toHaveBeenCalledTimes(1); + expect(postMessageToClient).toHaveBeenCalledTimes(1); // prettier-ignore - expect(callback).toHaveBeenCalledWith({ + expect(postMessageToClient).toHaveBeenCalledWith({ mode: "update", // WRONG type: "viewport-update", clientViewportId: "client-vp-1", @@ -1564,7 +1624,7 @@ describe("ServerProxy", () => { ], }); - callback.mockClear(); + postMessageToClient.mockClear(); serverProxy.handleMessageFromServer({ ...COMMON_ATTRS, body: { @@ -1575,9 +1635,9 @@ describe("ServerProxy", () => { ], }, }); - expect(callback).toHaveBeenCalledTimes(1); + expect(postMessageToClient).toHaveBeenCalledTimes(1); // prettier-ignore - expect(callback).toHaveBeenCalledWith({ + expect(postMessageToClient).toHaveBeenCalledWith({ mode: "update", // WRONG type: "viewport-update", clientViewportId: "client-vp-1", @@ -1593,17 +1653,22 @@ describe("ServerProxy", () => { }); describe("selection", () => { - it("single select", () => { - const [clientSubscription1, serverSubscriptionAck1] = createSubscription({ - to: 20, - }); + it("single select", async () => { const postMessageToClient = vi.fn(); - const serverProxy = new ServerProxy(mockConnection, postMessageToClient); - serverProxy["sessionId"] = "dsdsd"; - serverProxy["authToken"] = "test"; + const connection = { + send: vi.fn(), + status: "ready" as const, + }; - serverProxy.subscribe(clientSubscription1); - serverProxy.handleMessageFromServer(serverSubscriptionAck1); + const serverProxy = await createServerProxyAndSubscribeToViewport( + postMessageToClient, + { + connection, + to: 20, + } + ); + + postMessageToClient.mockClear(); serverProxy.handleMessageFromServer({ ...COMMON_ATTRS, @@ -1618,7 +1683,7 @@ describe("ServerProxy", () => { TEST_setRequestId(1); postMessageToClient.mockClear(); - mockConnection.send.mockClear(); + connection.send.mockClear(); // prettier-ignore serverProxy.handleMessageFromClient({ @@ -1629,8 +1694,8 @@ describe("ServerProxy", () => { expect(postMessageToClient).toHaveBeenCalledTimes(0); - expect(mockConnection.send).toHaveBeenCalledTimes(1); - expect(mockConnection.send).toHaveBeenCalledWith({ + expect(connection.send).toHaveBeenCalledTimes(1); + expect(connection.send).toHaveBeenCalledWith({ body: { vpId: "server-vp-1", type: "SET_SELECTION", @@ -1645,7 +1710,7 @@ describe("ServerProxy", () => { TEST_setRequestId(1); postMessageToClient.mockClear(); - mockConnection.send.mockClear(); + connection.send.mockClear(); // prettier-ignore serverProxy.handleMessageFromClient({ @@ -1655,9 +1720,9 @@ describe("ServerProxy", () => { }); expect(postMessageToClient).toHaveBeenCalledTimes(0); - expect(mockConnection.send).toHaveBeenCalledTimes(1); + expect(connection.send).toHaveBeenCalledTimes(1); - expect(mockConnection.send).toHaveBeenCalledWith({ + expect(connection.send).toHaveBeenCalledWith({ body: { vpId: "server-vp-1", type: "SET_SELECTION", @@ -1673,10 +1738,19 @@ describe("ServerProxy", () => { }); describe("filtering", () => { - it("invokes filter on viewport, which stores current filter criteria", () => { + it("invokes filter on viewport, which stores current filter criteria", async () => { const postMessageToClient = vi.fn(); - const serverProxy = - createServerProxyAndSubscribeToViewport(postMessageToClient); + const connection = { + send: vi.fn(), + status: "ready" as const, + }; + + const serverProxy = await createServerProxyAndSubscribeToViewport( + postMessageToClient, + { + connection, + } + ); serverProxy.handleMessageFromServer({ ...COMMON_ATTRS, @@ -1688,7 +1762,7 @@ describe("ServerProxy", () => { TEST_setRequestId(1); postMessageToClient.mockClear(); - mockConnection.send.mockClear(); + connection.send.mockClear(); serverProxy.handleMessageFromClient({ viewport: "client-vp-1", @@ -1715,10 +1789,19 @@ describe("ServerProxy", () => { }); }); - it("sets batch mode when a filter has been applied", () => { + it("sets batch mode when a filter has been applied", async () => { const postMessageToClient = vi.fn(); - const serverProxy = - createServerProxyAndSubscribeToViewport(postMessageToClient); + const connection = { + send: vi.fn(), + status: "ready" as const, + }; + + const serverProxy = await createServerProxyAndSubscribeToViewport( + postMessageToClient, + { + connection, + } + ); serverProxy.handleMessageFromServer({ ...COMMON_ATTRS, @@ -1730,7 +1813,7 @@ describe("ServerProxy", () => { TEST_setRequestId(1); postMessageToClient.mockClear(); - mockConnection.send.mockClear(); + connection.send.mockClear(); serverProxy.handleMessageFromClient({ viewport: "client-vp-1", @@ -1794,10 +1877,20 @@ describe("ServerProxy", () => { size: 43 }); }); - it("handles TABLE_ROWS that preceed filter request together with filtered rows, in same batch", () => { + + it("handles TABLE_ROWS that preceed filter request together with filtered rows, in same batch", async () => { const postMessageToClient = vi.fn(); - const serverProxy = - createServerProxyAndSubscribeToViewport(postMessageToClient); + const connection = { + send: vi.fn(), + status: "ready" as const, + }; + + const serverProxy = await createServerProxyAndSubscribeToViewport( + postMessageToClient, + { + connection, + } + ); serverProxy.handleMessageFromServer({ ...COMMON_ATTRS, @@ -1809,7 +1902,7 @@ describe("ServerProxy", () => { TEST_setRequestId(1); postMessageToClient.mockClear(); - mockConnection.send.mockClear(); + connection.send.mockClear(); serverProxy.handleMessageFromClient({ viewport: "client-vp-1", @@ -1886,16 +1979,19 @@ describe("ServerProxy", () => { }); describe("GroupBy", () => { - const [clientSubscription1, serverSubscriptionAck1] = createSubscription(); + it("sets viewport isTree when groupby in place", async () => { + const postMessageToClient = vi.fn(); + const connection = { + send: vi.fn(), + status: "ready" as const, + }; - it("sets viewport isTree when groupby in place", () => { - const callback = vi.fn(); - const serverProxy = new ServerProxy(mockConnection, callback); - serverProxy["sessionId"] = "dsdsd"; - serverProxy["authToken"] = "test"; + const serverProxy = await createServerProxyAndSubscribeToViewport( + postMessageToClient, + { connection } + ); - serverProxy.subscribe(clientSubscription1); - serverProxy.handleMessageFromServer(serverSubscriptionAck1); + postMessageToClient.mockClear(); serverProxy.handleMessageFromServer({ ...COMMON_ATTRS, @@ -1906,8 +2002,8 @@ describe("ServerProxy", () => { }); TEST_setRequestId(1); - callback.mockClear(); - mockConnection.send.mockClear(); + postMessageToClient.mockClear(); + connection.send.mockClear(); serverProxy.handleMessageFromClient({ viewport: "client-vp-1", @@ -1915,10 +2011,10 @@ describe("ServerProxy", () => { groupBy: ["col-4"], }); - expect(callback).toHaveBeenCalledTimes(0); - expect(mockConnection.send).toHaveBeenCalledTimes(1); + expect(postMessageToClient).toHaveBeenCalledTimes(0); + expect(connection.send).toHaveBeenCalledTimes(1); - expect(mockConnection.send).toHaveBeenCalledWith({ + expect(connection.send).toHaveBeenCalledWith({ body: { aggregations: [], viewPortId: "server-vp-1", @@ -1952,14 +2048,21 @@ describe("ServerProxy", () => { expect(serverProxy["viewports"].get("server-vp-1")?.isTree).toBe(true); }); - it("on changing group, sends grouped records as batch, with SIZE record", () => { - const callback = vi.fn(); - const serverProxy = new ServerProxy(mockConnection, callback); - serverProxy["sessionId"] = "dsdsd"; - serverProxy["authToken"] = "test"; + it("on changing group, sends grouped records as batch, with SIZE record", async () => { + const postMessageToClient = vi.fn(); + const connection = { + send: vi.fn(), + status: "ready" as const, + }; + + const serverProxy = await createServerProxyAndSubscribeToViewport( + postMessageToClient, + { + connection, + } + ); - serverProxy.subscribe(clientSubscription1); - serverProxy.handleMessageFromServer(serverSubscriptionAck1); + postMessageToClient.mockClear(); serverProxy.handleMessageFromServer({ ...COMMON_ATTRS, @@ -1970,8 +2073,8 @@ describe("ServerProxy", () => { }); TEST_setRequestId(1); - callback.mockClear(); - mockConnection.send.mockClear(); + postMessageToClient.mockClear(); + connection.send.mockClear(); serverProxy.handleMessageFromClient({ viewport: "client-vp-1", @@ -1979,9 +2082,9 @@ describe("ServerProxy", () => { groupBy: ["col-4"], }); - expect(callback).toHaveBeenCalledTimes(0); - expect(mockConnection.send).toHaveBeenCalledTimes(1); - expect(mockConnection.send).toHaveBeenCalledWith({ + expect(postMessageToClient).toHaveBeenCalledTimes(0); + expect(connection.send).toHaveBeenCalledTimes(1); + expect(connection.send).toHaveBeenCalledWith({ body: { aggregations: [], viewPortId: "server-vp-1", @@ -2012,16 +2115,16 @@ describe("ServerProxy", () => { }, }); - callback.mockClear(); + postMessageToClient.mockClear(); serverProxy.handleMessageFromServer(createTableGroupRows()); - expect(callback).toHaveBeenCalledTimes(1); + expect(postMessageToClient).toHaveBeenCalledTimes(1); expect( serverProxy["viewports"].get("server-vp-1")?.["dataWindow"]?.[ "internalData" ] ).toHaveLength(4); // prettier-ignore - expect(callback).toHaveBeenCalledWith({ + expect(postMessageToClient).toHaveBeenCalledWith({ mode: "batch", type: "viewport-update", clientViewportId: "client-vp-1", @@ -2035,14 +2138,21 @@ describe("ServerProxy", () => { }); }); - it("on changing group, sends grouped records as batch, without SIZE record", () => { - const callback = vi.fn(); - const serverProxy = new ServerProxy(mockConnection, callback); - serverProxy["sessionId"] = "dsdsd"; - serverProxy["authToken"] = "test"; + it("on changing group, sends grouped records as batch, without SIZE record", async () => { + const postMessageToClient = vi.fn(); + const connection = { + send: vi.fn(), + status: "ready" as const, + }; + + const serverProxy = await createServerProxyAndSubscribeToViewport( + postMessageToClient, + { + connection, + } + ); - serverProxy.subscribe(clientSubscription1); - serverProxy.handleMessageFromServer(serverSubscriptionAck1); + postMessageToClient.mockClear(); serverProxy.handleMessageFromServer({ ...COMMON_ATTRS, @@ -2053,8 +2163,8 @@ describe("ServerProxy", () => { }); TEST_setRequestId(1); - callback.mockClear(); - mockConnection.send.mockClear(); + postMessageToClient.mockClear(); + connection.send.mockClear(); serverProxy.handleMessageFromClient({ viewport: "client-vp-1", @@ -2062,9 +2172,9 @@ describe("ServerProxy", () => { groupBy: ["col-4"], }); - expect(callback).toHaveBeenCalledTimes(0); - expect(mockConnection.send).toHaveBeenCalledTimes(1); - expect(mockConnection.send).toHaveBeenCalledWith({ + expect(postMessageToClient).toHaveBeenCalledTimes(0); + expect(connection.send).toHaveBeenCalledTimes(1); + expect(connection.send).toHaveBeenCalledWith({ body: { aggregations: [], viewPortId: "server-vp-1", @@ -2095,16 +2205,16 @@ describe("ServerProxy", () => { }, }); - callback.mockClear(); + postMessageToClient.mockClear(); serverProxy.handleMessageFromServer(createTableGroupRows(false)); - expect(callback).toHaveBeenCalledTimes(1); + expect(postMessageToClient).toHaveBeenCalledTimes(1); expect( serverProxy["viewports"].get("server-vp-1")?.["dataWindow"]?.[ "internalData" ] ).toHaveLength(4); // prettier-ignore - expect(callback).toHaveBeenCalledWith({ + expect(postMessageToClient).toHaveBeenCalledWith({ mode: "batch", type: "viewport-update", clientViewportId: "client-vp-1", @@ -2118,14 +2228,22 @@ describe("ServerProxy", () => { }); }); - it("on changing group, it may receive group records in multiple batches", () => { - const callback = vi.fn(); - const serverProxy = new ServerProxy(mockConnection, callback); - serverProxy["sessionId"] = "dsdsd"; - serverProxy["authToken"] = "test"; + it("on changing group, it may receive group records in multiple batches", async () => { + const postMessageToClient = vi.fn(); + const connection = { + send: vi.fn(), + status: "ready" as const, + }; - serverProxy.subscribe(clientSubscription1); - serverProxy.handleMessageFromServer(serverSubscriptionAck1); + const serverProxy = await createServerProxyAndSubscribeToViewport( + postMessageToClient, + { + bufferSize: 10, + connection, + } + ); + + postMessageToClient.mockClear(); serverProxy.handleMessageFromServer({ ...COMMON_ATTRS, @@ -2136,8 +2254,8 @@ describe("ServerProxy", () => { }); TEST_setRequestId(1); - callback.mockClear(); - mockConnection.send.mockClear(); + postMessageToClient.mockClear(); + connection.send.mockClear(); serverProxy.handleMessageFromClient({ viewport: "client-vp-1", @@ -2145,9 +2263,9 @@ describe("ServerProxy", () => { groupBy: ["col-4"], }); - expect(callback).toHaveBeenCalledTimes(0); - expect(mockConnection.send).toHaveBeenCalledTimes(1); - expect(mockConnection.send).toHaveBeenCalledWith({ + expect(postMessageToClient).toHaveBeenCalledTimes(0); + expect(connection.send).toHaveBeenCalledTimes(1); + expect(connection.send).toHaveBeenCalledWith({ body: { aggregations: [], viewPortId: "server-vp-1", @@ -2178,7 +2296,7 @@ describe("ServerProxy", () => { }, }); - callback.mockClear(); + postMessageToClient.mockClear(); const groupRows = createTableGroupRows(false); const group1 = { @@ -2207,7 +2325,7 @@ describe("ServerProxy", () => { serverProxy.handleMessageFromServer(group1); serverProxy.handleMessageFromServer(group2); - expect(callback).toHaveBeenCalledTimes(2); + expect(postMessageToClient).toHaveBeenCalledTimes(2); expect( serverProxy["viewports"].get("server-vp-1")?.["dataWindow"]?.[ "internalData" @@ -2215,7 +2333,7 @@ describe("ServerProxy", () => { ).toHaveLength(4); // prettier-ignore - expect(callback).toHaveBeenNthCalledWith(1, { + expect(postMessageToClient).toHaveBeenNthCalledWith(1, { mode: "size-only", type: "viewport-update", clientViewportId: "client-vp-1", @@ -2223,7 +2341,7 @@ describe("ServerProxy", () => { }) // prettier-ignore - expect(callback).toHaveBeenNthCalledWith(2, { + expect(postMessageToClient).toHaveBeenNthCalledWith(2, { mode: "batch", type: "viewport-update", clientViewportId: "client-vp-1", @@ -2236,11 +2354,21 @@ describe("ServerProxy", () => { }); }); - it("ignores regular row updates after grouping is in place", () => { - const callback = vi.fn(); - const serverProxy = new ServerProxy(mockConnection, callback); - serverProxy.subscribe(clientSubscription1); - serverProxy.handleMessageFromServer(serverSubscriptionAck1); + it("ignores regular row updates after grouping is in place", async () => { + const postMessageToClient = vi.fn(); + const connection = { + send: vi.fn(), + status: "ready" as const, + }; + + const serverProxy = await createServerProxyAndSubscribeToViewport( + postMessageToClient, + { + connection, + } + ); + + postMessageToClient.mockClear(); serverProxy.handleMessageFromServer({ ...COMMON_ATTRS, @@ -2251,8 +2379,8 @@ describe("ServerProxy", () => { }); TEST_setRequestId(1); - callback.mockClear(); - mockConnection.send.mockClear(); + postMessageToClient.mockClear(); + connection.send.mockClear(); serverProxy.handleMessageFromClient({ viewport: "client-vp-1", @@ -2274,7 +2402,7 @@ describe("ServerProxy", () => { }, }); - callback.mockClear(); + postMessageToClient.mockClear(); serverProxy.handleMessageFromServer({ ...COMMON_ATTRS, @@ -2284,14 +2412,25 @@ describe("ServerProxy", () => { }, }); - expect(callback).toHaveBeenCalledTimes(0); + expect(postMessageToClient).toHaveBeenCalledTimes(0); }); - it("processes group row updates", () => { - const callback = vi.fn(); - const serverProxy = new ServerProxy(mockConnection, callback); - serverProxy.subscribe(clientSubscription1); - serverProxy.handleMessageFromServer(serverSubscriptionAck1); + it("processes group row updates", async () => { + const postMessageToClient = vi.fn(); + const connection = { + send: vi.fn(), + status: "ready" as const, + }; + + const serverProxy = await createServerProxyAndSubscribeToViewport( + postMessageToClient, + { + bufferSize: 10, + connection, + } + ); + + postMessageToClient.mockClear(); serverProxy.handleMessageFromServer({ ...COMMON_ATTRS, @@ -2302,8 +2441,8 @@ describe("ServerProxy", () => { }); TEST_setRequestId(1); - callback.mockClear(); - mockConnection.send.mockClear(); + postMessageToClient.mockClear(); + connection.send.mockClear(); serverProxy.handleMessageFromClient({ viewport: "client-vp-1", @@ -2325,18 +2464,18 @@ describe("ServerProxy", () => { }, }); - callback.mockClear(); + postMessageToClient.mockClear(); serverProxy.handleMessageFromServer(createTableGroupRows()); - expect(callback).toHaveBeenCalledTimes(1); + expect(postMessageToClient).toHaveBeenCalledTimes(1); expect( serverProxy["viewports"].get("server-vp-1")?.["dataWindow"]?.[ "internalData" ] ).toHaveLength(4); // prettier-ignore - expect(callback).toHaveBeenCalledWith({ + expect(postMessageToClient).toHaveBeenCalledWith({ mode: "batch", type: "viewport-update", clientViewportId: "client-vp-1", @@ -2349,7 +2488,7 @@ describe("ServerProxy", () => { size: 4, }); - callback.mockClear(); + postMessageToClient.mockClear(); // prettier-ignore serverProxy.handleMessageFromServer({ @@ -2370,14 +2509,14 @@ describe("ServerProxy", () => { }, }); - expect(callback).toHaveBeenCalledTimes(1); + expect(postMessageToClient).toHaveBeenCalledTimes(1); expect( serverProxy["viewports"].get("server-vp-1")?.["dataWindow"]?.[ "internalData" ] ).toHaveLength(4); // prettier-ignore - expect(callback).toHaveBeenCalledWith({ + expect(postMessageToClient).toHaveBeenCalledWith({ mode: "update", type: "viewport-update", clientViewportId: "client-vp-1", @@ -2389,17 +2528,11 @@ describe("ServerProxy", () => { }); describe("SIZE records", () => { - const [clientSubscription1, serverSubscriptionAck1] = createSubscription(); - - it("subscribe whilst table is loading", () => { + it("subscribe whilst table is loading", async () => { const postMessageToClient = vi.fn(); - const serverProxy = new ServerProxy(mockConnection, postMessageToClient); - //TODO we shouldn't be able to bypass checks like this - serverProxy["sessionId"] = "dsdsd"; - serverProxy["authToken"] = "test"; - - serverProxy.subscribe(clientSubscription1); - serverProxy.handleMessageFromServer(serverSubscriptionAck1); + const serverProxy = await createServerProxyAndSubscribeToViewport( + postMessageToClient + ); postMessageToClient.mockClear(); @@ -2520,25 +2653,16 @@ describe("ServerProxy", () => { }); }); - describe("on visual linking", () => { - it("returns link table rows", () => { - const [clientSubscription1, serverSubscriptionAck1] = - createSubscription(); - const [clientSubscription2, serverSubscriptionAck2] = createSubscription({ - key: "2", - }); - + describe("on visual linking", async () => { + it("returns link table rows", async () => { const postMessageToClient = vi.fn(); - const serverProxy = new ServerProxy(mockConnection, postMessageToClient); - //TODO we shouldn't be able to bypass checks like this - serverProxy["sessionId"] = "dsdsd"; - serverProxy["authToken"] = "test"; + const serverProxy = await createServerProxyAndSubscribeToViewport( + postMessageToClient + ); - serverProxy.subscribe(clientSubscription1); - serverProxy.handleMessageFromServer(serverSubscriptionAck1); + await subscribe(serverProxy, { key: "2" }); - serverProxy.subscribe(clientSubscription2); - serverProxy.handleMessageFromServer(serverSubscriptionAck2); + postMessageToClient.mockClear(); serverProxy.handleMessageFromServer({ ...COMMON_ATTRS, @@ -2618,10 +2742,11 @@ describe("ServerProxy", () => { }); describe("debounce mode", () => { - it("clears pending range request when request is filled", () => { + it("clears pending range request when request is filled", async () => { const postMessageToClient = vi.fn(); - const serverProxy = - createServerProxyAndSubscribeToViewport(postMessageToClient); + const serverProxy = await createServerProxyAndSubscribeToViewport( + postMessageToClient + ); const viewport = serverProxy["viewports"].get("server-vp-1") as Viewport; expect(viewport["pendingRangeRequests"]).toHaveLength(0); @@ -2665,10 +2790,11 @@ describe("ServerProxy", () => { expect(viewport["pendingRangeRequests"]).toHaveLength(0); }); - it("clears pending range request when only partial set of rows received", () => { + it("clears pending range request when only partial set of rows received", async () => { const postMessageToClient = vi.fn(); - const serverProxy = - createServerProxyAndSubscribeToViewport(postMessageToClient); + const serverProxy = await createServerProxyAndSubscribeToViewport( + postMessageToClient + ); const viewport = serverProxy["viewports"].get("server-vp-1") as Viewport; expect(viewport["pendingRangeRequests"]).toHaveLength(0); @@ -2708,10 +2834,11 @@ describe("ServerProxy", () => { expect(viewport["pendingRangeRequests"]).toHaveLength(0); }); - it("queues pending range requests, until filled, no message to client until current client range filled", () => { + it("queues pending range requests, until filled, no message to client until current client range filled", async () => { const postMessageToClient = vi.fn(); - const serverProxy = - createServerProxyAndSubscribeToViewport(postMessageToClient); + const serverProxy = await createServerProxyAndSubscribeToViewport( + postMessageToClient + ); const viewport = serverProxy["viewports"].get("server-vp-1") as Viewport; expect(viewport["pendingRangeRequests"]).toHaveLength(0); @@ -2792,10 +2919,11 @@ describe("ServerProxy", () => { expect(viewport["pendingRangeRequests"]).toHaveLength(0); }); - it("sends debounce request to client when rows requested before previous request acked", () => { + it("sends debounce request to client when rows requested before previous request acked", async () => { const postMessageToClient = vi.fn(); - const serverProxy = - createServerProxyAndSubscribeToViewport(postMessageToClient); + const serverProxy = await createServerProxyAndSubscribeToViewport( + postMessageToClient + ); // prettier-ignore serverProxy.handleMessageFromServer({ @@ -2836,16 +2964,19 @@ describe("ServerProxy", () => { }); describe("config", () => { - const [clientSubscription1, serverSubscriptionAck1] = createSubscription(); - - it("sets viewport isTree when config includes groupby", () => { - const callback = vi.fn(); - const serverProxy = new ServerProxy(mockConnection, callback); - serverProxy["sessionId"] = "dsdsd"; - serverProxy["authToken"] = "test"; + it("sets viewport isTree when config includes groupby", async () => { + const postMessageToClient = vi.fn(); + const connection = { + send: vi.fn(), + status: "ready" as const, + }; - serverProxy.subscribe(clientSubscription1); - serverProxy.handleMessageFromServer(serverSubscriptionAck1); + const serverProxy = await createServerProxyAndSubscribeToViewport( + postMessageToClient, + { + connection, + } + ); serverProxy.handleMessageFromServer({ ...COMMON_ATTRS, @@ -2856,8 +2987,8 @@ describe("ServerProxy", () => { }); TEST_setRequestId(1); - callback.mockClear(); - mockConnection.send.mockClear(); + postMessageToClient.mockClear(); + connection.send.mockClear(); serverProxy.handleMessageFromClient({ viewport: "client-vp-1", @@ -2871,10 +3002,10 @@ describe("ServerProxy", () => { }, }); - expect(callback).toHaveBeenCalledTimes(0); - expect(mockConnection.send).toHaveBeenCalledTimes(1); + expect(postMessageToClient).toHaveBeenCalledTimes(0); + expect(connection.send).toHaveBeenCalledTimes(1); - expect(mockConnection.send).toHaveBeenCalledWith({ + expect(connection.send).toHaveBeenCalledWith({ body: { aggregations: [], viewPortId: "server-vp-1", @@ -2910,75 +3041,57 @@ describe("ServerProxy", () => { }); describe("multiple subscriptions", () => { - it("sends messages to correct clients once subscribed", () => { - const callback = vi.fn(); - const [clientSubscription1, serverSubscription1] = createSubscription({ - key: "1", - }); - const [clientSubscription2, serverSubscription2] = createSubscription({ - key: "2", - }); - const serverProxy = new ServerProxy(mockConnection, callback); - serverProxy.subscribe(clientSubscription1); - serverProxy.subscribe(clientSubscription2); - serverProxy.handleMessageFromServer(serverSubscription1); - serverProxy.handleMessageFromServer(serverSubscription2); - //TODO cover tableSchema in test - expect(callback).toHaveBeenCalledTimes(2); - expect(callback).toHaveBeenNthCalledWith<[DataSourceSubscribedMessage]>( - 1, - { - aggregations: [], - clientViewportId: "client-vp-1", - columns: ["col-1", "col-2", "col-3", "col-4"], - filter: { filter: "" }, - groupBy: [], - range: { - from: 0, - to: 10, - }, - sort: { - sortDefs: [], - }, - tableSchema: null, - type: "subscribed", - } - ); - expect(callback).toHaveBeenNthCalledWith<[DataSourceSubscribedMessage]>( - 2, - { - aggregations: [], - clientViewportId: "client-vp-2", - columns: ["col-1", "col-2", "col-3", "col-4"], - filter: { filter: "" }, - groupBy: [], - range: { - from: 0, - to: 10, - }, - sort: { - sortDefs: [], - }, - tableSchema: null, - type: "subscribed", - } + it("sends messages to correct clients once subscribed", async () => { + const postMessageToClient = vi.fn(); + const serverProxy = await createServerProxyAndSubscribeToViewport( + postMessageToClient ); + + await subscribe(serverProxy, { key: "2" }); + + expect(postMessageToClient).toHaveBeenCalledTimes(2); + expect(postMessageToClient).toHaveBeenNthCalledWith(1, { + aggregations: [], + clientViewportId: "client-vp-1", + columns: ["col-1", "col-2", "col-3", "col-4"], + filter: { filter: "" }, + groupBy: [], + range: { + from: 0, + to: 10, + }, + sort: { + sortDefs: [], + }, + tableSchema: testSchema, + type: "subscribed", + }); + expect(postMessageToClient).toHaveBeenNthCalledWith(2, { + aggregations: [], + clientViewportId: "client-vp-2", + columns: ["col-1", "col-2", "col-3", "col-4"], + filter: { filter: "" }, + groupBy: [], + range: { + from: 0, + to: 10, + }, + sort: { + sortDefs: [], + }, + tableSchema: testSchema, + type: "subscribed", + }); expect(serverProxy["viewports"].size).toEqual(2); }); - it("sends data to each client when initial full datasets are received as separate batches", () => { + it("sends data to each client when initial full datasets are received as separate batches", async () => { const postMessageToClient = vi.fn(); - const [clientSubscription1, serverSubscription1] = createSubscription({ - key: "1", - }); - const [clientSubscription2, serverSubscription2] = createSubscription({ - key: "2", - }); - const serverProxy = new ServerProxy(mockConnection, postMessageToClient); - serverProxy.subscribe(clientSubscription1); - serverProxy.subscribe(clientSubscription2); - serverProxy.handleMessageFromServer(serverSubscription1); - serverProxy.handleMessageFromServer(serverSubscription2); + const serverProxy = await createServerProxyAndSubscribeToViewport( + postMessageToClient + ); + + await subscribe(serverProxy, { key: "2" }); postMessageToClient.mockClear(); @@ -3050,19 +3163,13 @@ describe("ServerProxy", () => { ); }); - it("sends data to each client when initial full datasets are received interleaved", () => { + it("sends data to each client when initial full datasets are received interleaved", async () => { const postMessageToClient = vi.fn(); - const [clientSubscription1, serverSubscription1] = createSubscription({ - key: "1", - }); - const [clientSubscription2, serverSubscription2] = createSubscription({ - key: "2", - }); - const serverProxy = new ServerProxy(mockConnection, postMessageToClient); - serverProxy.subscribe(clientSubscription1); - serverProxy.subscribe(clientSubscription2); - serverProxy.handleMessageFromServer(serverSubscription1); - serverProxy.handleMessageFromServer(serverSubscription2); + const serverProxy = await createServerProxyAndSubscribeToViewport( + postMessageToClient + ); + + await subscribe(serverProxy, { key: "2" }); postMessageToClient.mockClear(); @@ -3127,19 +3234,13 @@ describe("ServerProxy", () => { ); }); - it("sends data to each client followed by mixed updates", () => { + it("sends data to each client followed by mixed updates", async () => { const postMessageToClient = vi.fn(); - const [clientSubscription1, serverSubscription1] = createSubscription({ - key: "1", - }); - const [clientSubscription2, serverSubscription2] = createSubscription({ - key: "2", - }); - const serverProxy = new ServerProxy(mockConnection, postMessageToClient); - serverProxy.subscribe(clientSubscription1); - serverProxy.subscribe(clientSubscription2); - serverProxy.handleMessageFromServer(serverSubscription1); - serverProxy.handleMessageFromServer(serverSubscription2); + const serverProxy = await createServerProxyAndSubscribeToViewport( + postMessageToClient + ); + + await subscribe(serverProxy, { key: "2" }); postMessageToClient.mockClear(); @@ -3205,19 +3306,13 @@ describe("ServerProxy", () => { }); }); - it("sends mixed updates, including size change for one vp", () => { + it("sends mixed updates, including size change for one vp", async () => { const postMessageToClient = vi.fn(); - const [clientSubscription1, serverSubscription1] = createSubscription({ - key: "1", - }); - const [clientSubscription2, serverSubscription2] = createSubscription({ - key: "2", - }); - const serverProxy = new ServerProxy(mockConnection, postMessageToClient); - serverProxy.subscribe(clientSubscription1); - serverProxy.subscribe(clientSubscription2); - serverProxy.handleMessageFromServer(serverSubscription1); - serverProxy.handleMessageFromServer(serverSubscription2); + const serverProxy = await createServerProxyAndSubscribeToViewport( + postMessageToClient + ); + + await subscribe(serverProxy, { key: "2" }); postMessageToClient.mockClear(); @@ -3330,12 +3425,17 @@ describe("ServerProxy", () => { }); describe("disable and enable", () => { - it("sends a message to server when client calls disable", () => { + it("sends a message to server when client calls disable", async () => { const postMessageToClient = vi.fn(); - const serverProxy = createServerProxyAndSubscribeToViewport( + const connection = { + send: vi.fn(), + status: "ready" as const, + }; + + const serverProxy = await createServerProxyAndSubscribeToViewport( postMessageToClient, { - connection: mockConnection, + connection, } ); // prettier-ignore @@ -3347,7 +3447,7 @@ describe("ServerProxy", () => { }, }); - mockConnection.send.mockClear(); + connection.send.mockClear(); postMessageToClient.mockClear(); expect(serverProxy["viewports"].get("server-vp-1")?.disabled).toBe(false); @@ -3358,10 +3458,10 @@ describe("ServerProxy", () => { type: "disable", viewport: "client-vp-1", }); - expect(mockConnection.send).toBeCalledTimes(1); + expect(connection.send).toBeCalledTimes(1); expect(postMessageToClient).toHaveBeenCalledTimes(0); - expect(mockConnection.send).toHaveBeenCalledWith({ + expect(connection.send).toHaveBeenCalledWith({ body: { type: "DISABLE_VP", viewPortId: "server-vp-1", @@ -3384,12 +3484,16 @@ describe("ServerProxy", () => { expect(serverProxy["viewports"].get("server-vp-1")?.disabled).toBe(true); }); - it("sends a message to server when client calls enable, re-sends data from cache to client", () => { + it("sends a message to server when client calls enable, re-sends data from cache to client", async () => { const postMessageToClient = vi.fn(); - const serverProxy = createServerProxyAndSubscribeToViewport( + const connection = { + send: vi.fn(), + status: "ready" as const, + }; + const serverProxy = await createServerProxyAndSubscribeToViewport( postMessageToClient, { - connection: mockConnection, + connection, } ); // prettier-ignore @@ -3417,7 +3521,7 @@ describe("ServerProxy", () => { }, }); - mockConnection.send.mockClear(); + connection.send.mockClear(); postMessageToClient.mockClear(); TEST_setRequestId(1); @@ -3428,10 +3532,10 @@ describe("ServerProxy", () => { type: "enable", viewport: "client-vp-1", }); - expect(mockConnection.send).toBeCalledTimes(1); + expect(connection.send).toBeCalledTimes(1); expect(postMessageToClient).toHaveBeenCalledTimes(0); - expect(mockConnection.send).toHaveBeenCalledWith({ + expect(connection.send).toHaveBeenCalledWith({ body: { type: "ENABLE_VP", viewPortId: "server-vp-1", @@ -3483,12 +3587,17 @@ describe("ServerProxy", () => { }); }); - it("does nothing if client calls enable for a viewport which has not been disabled", () => { + it("does nothing if client calls enable for a viewport which has not been disabled", async () => { const postMessageToClient = vi.fn(); - const serverProxy = createServerProxyAndSubscribeToViewport( + const connection = { + send: vi.fn(), + status: "ready" as const, + }; + + const serverProxy = await createServerProxyAndSubscribeToViewport( postMessageToClient, { - connection: mockConnection, + connection, } ); // prettier-ignore @@ -3500,7 +3609,7 @@ describe("ServerProxy", () => { }, }); - mockConnection.send.mockClear(); + connection.send.mockClear(); postMessageToClient.mockClear(); TEST_setRequestId(1); @@ -3509,7 +3618,7 @@ describe("ServerProxy", () => { type: "enable", viewport: "client-vp-1", }); - expect(mockConnection.send).toBeCalledTimes(0); + expect(connection.send).toBeCalledTimes(0); expect(postMessageToClient).toHaveBeenCalledTimes(0); }); }); diff --git a/vuu-ui/packages/vuu-data/test/test-utils.ts b/vuu-ui/packages/vuu-data/test/test-utils.ts index 43a4d2e0c..14abfc90c 100644 --- a/vuu-ui/packages/vuu-data/test/test-utils.ts +++ b/vuu-ui/packages/vuu-data/test/test-utils.ts @@ -2,6 +2,7 @@ import { vi } from "vitest"; import { ServerToClientCreateViewPortSuccess, ServerToClientMessage, + ServerToClientTableMeta, ServerToClientTableRows, VuuRow, } from "@finos/vuu-protocol-types"; @@ -156,6 +157,17 @@ export const updateTableRow = ( }; }; +export const testSchema = { + columns: [ + { name: "col-1", serverDataType: "string" }, + { name: "col-2", serverDataType: "string" }, + { name: "col-3", serverDataType: "string" }, + { name: "col-4", serverDataType: "string" }, + ], + key: "col-1", + table: { module: "TEST", table: "test-table" }, +}; + // prettier-ignore export const createSubscription = ({ aggregations = [], @@ -170,7 +182,8 @@ export const createSubscription = ({ viewport = `client-vp-${key}` } = {}): [ ServerProxySubscribeMessage, - ServerToClientMessage + ServerToClientMessage, + ServerToClientMessage ] => [ { aggregations, @@ -199,6 +212,19 @@ export const createSubscription = ({ }, token: "", user: "user" + }, { + module: "TEST", + requestId: `1`, + body: { + columns: ['col-1', 'col-2', 'col-3', 'col-4'], + key: 'col-1', + dataTypes: ['string','string','string','string'], + table: {module: "TEST", table: "test-table"}, + type: "TABLE_META_RESP" + }, + token: "", + user: "user" + } ]; @@ -207,26 +233,46 @@ const mockConnection = { status: "ready" as const, }; -export const createServerProxyAndSubscribeToViewport = ( +export const subscribe = async ( + serverProxy: ServerProxy, + { bufferSize = 0, key = "1", to = 10 }: SubscriptionDetails +) => { + const [clientSubscription, serverSubscriptionAck, tableMetaResponse] = + createSubscription({ + bufferSize, + key, + to, + }); + + serverProxy.subscribe(clientSubscription); + serverProxy.handleMessageFromServer(serverSubscriptionAck); + serverProxy.handleMessageFromServer(tableMetaResponse); + + // allow the promises pending for the subscription and ,etadata to resolve + await new Promise((resolve) => window.setTimeout(resolve, 0)); +}; + +export type SubscriptionDetails = { + bufferSize?: number; + key?: string; + to?: number; +}; + +export const createServerProxyAndSubscribeToViewport = async ( postMessageToClient: any, { bufferSize = 0, connection = mockConnection, - }: { bufferSize?: number; connection?: any } = {} + key = "1", + to = 10, + }: { bufferSize?: number; connection?: any; key?: string; to?: number } = {} ) => { const serverProxy = new ServerProxy(connection, postMessageToClient); //TODO we shouldn't be able to bypass checks like this serverProxy["sessionId"] = "dsdsd"; serverProxy["authToken"] = "test"; - const [clientSubscription, serverSubscriptionAck] = createSubscription({ - bufferSize, - }); - - serverProxy.subscribe(clientSubscription); - serverProxy.handleMessageFromServer(serverSubscriptionAck); - - postMessageToClient.mockClear(); + await subscribe(serverProxy, { bufferSize, key, to }); return serverProxy; }; diff --git a/vuu-ui/packages/vuu-data/test/viewport.test.ts b/vuu-ui/packages/vuu-data/test/viewport.test.ts index 0607ef858..2e7b54be8 100644 --- a/vuu-ui/packages/vuu-data/test/viewport.test.ts +++ b/vuu-ui/packages/vuu-data/test/viewport.test.ts @@ -3,8 +3,12 @@ import { ServerToClientCreateViewPortSuccess } from "@finos/vuu-protocol-types"; import { describe, expect, it } from "vitest"; import { ServerProxySubscribeMessage } from "../src"; import { Viewport } from "../src/server-proxy/viewport"; -import { createSubscription, createTableRows, sizeRow } from "./test-utils"; -import { TableSchema } from "../src/message-utils"; +import { + createSubscription, + createTableRows, + sizeRow, + testSchema, +} from "./test-utils"; const config_options = { aggregations: [], @@ -22,6 +26,8 @@ const vuu_config_options = { sort: { sortDefs: [] }, }; +const noop = () => undefined; + const vuu_table = { module: "TEST", table: "test-table" }; const constructor_options = { @@ -34,14 +40,14 @@ const constructor_options = { describe("Viewport", () => { describe("constructor", () => { it("initial status is empty", () => { - const vp = new Viewport(constructor_options); + const vp = new Viewport(constructor_options, noop); expect(vp.status).toEqual(""); }); }); describe("subscribe", () => { it("uses constructor params to construct subscribe message", () => { - const vp = new Viewport(constructor_options); + const vp = new Viewport(constructor_options, noop); const message = vp.subscribe(); const { filter: { filter }, @@ -59,16 +65,19 @@ describe("Viewport", () => { }); }); it("sets status to subscribing", () => { - const vp = new Viewport(constructor_options); + const vp = new Viewport(constructor_options, noop); vp.subscribe(); expect(vp.status).toEqual("subscribing"); }); it("uses bufferSize when constructing range", () => { - const vp = new Viewport({ - bufferSize: 100, - ...constructor_options, - }); + const vp = new Viewport( + { + bufferSize: 100, + ...constructor_options, + }, + noop + ); const message = vp.subscribe(); const { filter: { filter }, @@ -86,11 +95,14 @@ describe("Viewport", () => { }); }); it("applies bufferSize to existing range", () => { - const vp = new Viewport({ - ...constructor_options, - bufferSize: 100, - range: { from: 0, to: 100 }, - }); + const vp = new Viewport( + { + ...constructor_options, + bufferSize: 100, + range: { from: 0, to: 100 }, + }, + noop + ); const message = vp.subscribe(); const { filter: { filter }, @@ -107,11 +119,14 @@ describe("Viewport", () => { }); }); it("splits bufferSize around existing range", () => { - const vp = new Viewport({ - ...constructor_options, - bufferSize: 100, - range: { from: 100, to: 200 }, - }); + const vp = new Viewport( + { + ...constructor_options, + bufferSize: 100, + range: { from: 100, to: 200 }, + }, + noop + ); const message = vp.subscribe(); const { filter: { filter }, @@ -131,7 +146,7 @@ describe("Viewport", () => { describe("subscribed", () => { it("sets status to subscribed", () => { - const vp = new Viewport(constructor_options); + const vp = new Viewport(constructor_options, noop); const vuuMessageBody: ServerToClientCreateViewPortSuccess = { ...vuu_config_options, range: { from: 0, to: 50 }, @@ -144,7 +159,7 @@ describe("Viewport", () => { }); it("echos back subscription details, enriching values sent by server", () => { - const vp = new Viewport(constructor_options); + const vp = new Viewport(constructor_options, noop); const vuuMessageBody: ServerToClientCreateViewPortSuccess = { ...vuu_config_options, range: { from: 0, to: 50 }, @@ -152,41 +167,13 @@ describe("Viewport", () => { table: vuu_table.table, viewPortId: "server-vp1", }; - const message = vp.handleSubscribed(vuuMessageBody); + const message = vp.handleSubscribed(vuuMessageBody, testSchema); expect(message).toEqual({ ...config_options, clientViewportId: constructor_options.viewport, range: { from: 0, to: 50 }, - tableSchema: null, - type: "subscribed", - }); - }); - it("includes tableSchema, when this has been received", () => { - const vp = new Viewport(constructor_options); - const vuuMessageBody: ServerToClientCreateViewPortSuccess = { - ...vuu_config_options, - range: { from: 0, to: 50 }, - type: "CREATE_VP_SUCCESS", - table: vuu_table.table, - viewPortId: "server-vp1", - }; - - const tableSchema: TableSchema = { - table: { module: "TEST", table: "testTable" }, - key: "col1", - columns: [{ name: "col1", serverDataType: "string" }], - }; - - vp.setTableSchema(tableSchema); - - const message = vp.handleSubscribed(vuuMessageBody); - - expect(message).toEqual({ - ...config_options, - clientViewportId: constructor_options.viewport, - range: { from: 0, to: 50 }, - tableSchema, + tableSchema: testSchema, type: "subscribed", }); }); @@ -194,11 +181,14 @@ describe("Viewport", () => { describe("pending range requests", () => { it("holds requests in pending queue, marking them when acked, until first rows received", () => { - const vp = new Viewport({ - ...constructor_options, - bufferSize: 10, - range: { from: 0, to: 10 }, - }); + const vp = new Viewport( + { + ...constructor_options, + bufferSize: 10, + range: { from: 0, to: 10 }, + }, + noop + ); const [, serverSubscription] = createSubscription(); vp.handleSubscribed(serverSubscription.body); @@ -223,11 +213,14 @@ describe("Viewport", () => { describe("rangeRequestAlreadyPending", () => { it("tests range requests against last pending server request", () => { - const vp = new Viewport({ - ...constructor_options, - bufferSize: 10, - range: { from: 0, to: 10 }, - }); + const vp = new Viewport( + { + ...constructor_options, + bufferSize: 10, + range: { from: 0, to: 10 }, + }, + noop + ); const [, serverSubscription] = createSubscription(); vp.handleSubscribed(serverSubscription.body); @@ -258,11 +251,14 @@ describe("Viewport", () => { }); it("tests range requests against multiple pending server requests", () => { - const vp = new Viewport({ - ...constructor_options, - bufferSize: 10, - range: { from: 0, to: 10 }, - }); + const vp = new Viewport( + { + ...constructor_options, + bufferSize: 10, + range: { from: 0, to: 10 }, + }, + noop + ); const [, serverSubscription] = createSubscription(); vp.handleSubscribed(serverSubscription.body); @@ -299,11 +295,14 @@ describe("Viewport", () => { describe("groupBy", () => { it("clears dataWindow when a groupBy request is received on a non grouped viewport", () => { - const vp = new Viewport({ - ...constructor_options, - bufferSize: 10, - range: { from: 0, to: 10 }, - }); + const vp = new Viewport( + { + ...constructor_options, + bufferSize: 10, + range: { from: 0, to: 10 }, + }, + noop + ); const [, serverSubscription] = createSubscription(); vp.handleSubscribed(serverSubscription.body); diff --git a/vuu-ui/packages/vuu-datagrid-types/index.d.ts b/vuu-ui/packages/vuu-datagrid-types/index.d.ts index 92b7b7ea7..65fc21298 100644 --- a/vuu-ui/packages/vuu-datagrid-types/index.d.ts +++ b/vuu-ui/packages/vuu-datagrid-types/index.d.ts @@ -3,7 +3,9 @@ import type { Filter } from "@finos/vuu-filter-types"; import type { VuuAggType, VuuColumnDataType, + VuuRowDataItemType, VuuSortType, + VuuTable, } from "@finos/vuu-protocol-types"; import type { FunctionComponent, MouseEvent } from "react"; import type { ClientSideValidationChecker } from "@finos/vuu-ui-controls"; @@ -22,8 +24,8 @@ export type TableHeadings = TableHeading[][]; export type DataCellEditHandler = ( row: DataSourceRow, columnName: string, - value: VuuColumnDataType -) => boolean; + value: VuuRowDataItemType +) => Promise; export interface TableCellProps { className?: string; @@ -34,9 +36,15 @@ export interface TableCellProps { row: DataSourceRow; } +export type CommitResponse = Promise; + +export type DataItemCommitHandler = ( + value: VuuRowDataItemType +) => CommitResponse; + export interface TableCellRendererProps extends Omit { - onCommit?: (value: VuuColumnDataType) => boolean; + onCommit?: DataItemCommitHandler; } export interface TableAttributes { @@ -62,7 +70,7 @@ export interface GridConfig extends TableConfig { selectionBookendWidth?: number; } -export declare type TypeFormatting = { +export declare type ColumnTypeFormatting = { alignOnDecimals?: boolean; decimals?: number; pattern?: string; @@ -77,19 +85,45 @@ export interface EditValidationRule { value?: string; } -export interface ColumnTypeRenderer { +export type ListOption = { + label: string; + value: number | string; +}; + +/** + * Descibes a custom cell renderer for a Table column + */ +export interface ColumnTypeRendering { associatedField?: string; // specific to Background renderer flashStyle?: "bg-only" | "arrow-bg" | "arrow"; name: string; rules?: EditValidationRule[]; - // These are for the dropdown-input - how do we type parameters for custom renderers ? - values?: ReadonlyArray; } export interface MappedValueTypeRenderer { map: ColumnTypeValueMap; } +export type LookupTableDetails = { + labelColumn: string; + table: VuuTable; + valueColumn: string; +}; +/** + * This describes a serverside lookup table which will be bound to the edit control + * for this column. The lookup table will typically have two columns, mapping a + * numeric value to a User friendly display string. + */ +export interface LookupRenderer { + name: string; + lookup: LookupTableDetails; +} + +export interface ValueListRenderer { + name: string; + values: string[]; +} + export declare type ColumnTypeSimple = | "string" | "number" @@ -100,13 +134,23 @@ export declare type ColumnTypeSimple = | "checkbox"; export declare type ColumnTypeDescriptor = { - formatting?: TypeFormatting; + formatting?: ColumnTypeFormatting; name: ColumnTypeSimple; - renderer?: ColumnTypeRenderer | MappedValueTypeRenderer; + renderer?: + | ColumnTypeRendering + | LookupRenderer + | MappedValueTypeRenderer + | ValueListRenderer; +}; + +export declare type ColumnTypeDescriptorCustomRenderer = { + formatting?: ColumnTypeFormatting; + name: ColumnTypeSimple; + renderer: ColumnTypeRendering; }; export interface ColumnTypeRendererWithValidationRules - extends ColumnTypeRenderer { + extends ColumnTypeRendering { rules: EditValidationRule[]; } @@ -151,6 +195,11 @@ export interface ColumnDescriptor { width?: number; } +export interface ColumnDescriptorCustomRenderer + extends Omit { + type: ColumnTypeDescriptorCustomRenderer; +} + /** This is an internal description of a Column that extends the public * definitin with internal state values. */ export interface KeyedColumnDescriptor extends ColumnDescriptor { diff --git a/vuu-ui/packages/vuu-datagrid/src/cell-renderers/progress-cell.tsx b/vuu-ui/packages/vuu-datagrid/src/cell-renderers/progress-cell.tsx index 863b9cd12..923695046 100644 --- a/vuu-ui/packages/vuu-datagrid/src/cell-renderers/progress-cell.tsx +++ b/vuu-ui/packages/vuu-datagrid/src/cell-renderers/progress-cell.tsx @@ -5,7 +5,7 @@ import React from "react"; import { isTypeDescriptor } from "@finos/vuu-utils"; import { GridCellProps } from "../grid-cells"; import "./progress-cell.css"; -import { ColumnTypeRenderer } from "@finos/vuu-datagrid-types"; +import { ColumnTypeRendering } from "@finos/vuu-datagrid-types"; const ProgressCell = React.memo(function ProgressCell({ column, @@ -18,9 +18,9 @@ const ProgressCell = React.memo(function ProgressCell({ const value = row[column.key]; if ( isTypeDescriptor(type) && - (type.renderer as ColumnTypeRenderer)?.associatedField + (type.renderer as ColumnTypeRendering)?.associatedField ) { - const associatedField = (type.renderer as ColumnTypeRenderer) + const associatedField = (type.renderer as ColumnTypeRendering) .associatedField as string; const associatedValue = row[columnMap[associatedField]]; if (typeof value === "number" && typeof associatedValue === "number") { diff --git a/vuu-ui/packages/vuu-datagrid/src/grid-cells/GridCell.tsx b/vuu-ui/packages/vuu-datagrid/src/grid-cells/GridCell.tsx index e1f6c52c1..00471a5a3 100644 --- a/vuu-ui/packages/vuu-datagrid/src/grid-cells/GridCell.tsx +++ b/vuu-ui/packages/vuu-datagrid/src/grid-cells/GridCell.tsx @@ -1,5 +1,5 @@ import { - ColumnTypeRenderer, + ColumnTypeRendering, KeyedColumnDescriptor, } from "@finos/vuu-datagrid-types"; import { ColumnMap, DataRow, isTypeDescriptor } from "@finos/vuu-utils"; @@ -55,7 +55,7 @@ export const GridCell = React.memo(function GridCell({ const className = useGridCellClassName(column); const { type } = column; const rendererName = isTypeDescriptor(type) - ? (type?.renderer as ColumnTypeRenderer)?.name + ? (type?.renderer as ColumnTypeRendering)?.name : null; const Cell = rendererName && diff --git a/vuu-ui/packages/vuu-datagrid/src/grid-cells/useCellFormatter.ts b/vuu-ui/packages/vuu-datagrid/src/grid-cells/useCellFormatter.ts index 1f8577143..0abefbe50 100644 --- a/vuu-ui/packages/vuu-datagrid/src/grid-cells/useCellFormatter.ts +++ b/vuu-ui/packages/vuu-datagrid/src/grid-cells/useCellFormatter.ts @@ -1,7 +1,7 @@ import { isTypeDescriptor, roundDecimal } from "@finos/vuu-utils"; import { KeyedColumnDescriptor, - TypeFormatting, + ColumnTypeFormatting, } from "@finos/vuu-datagrid-types"; import { createElement, useRef } from "react"; @@ -30,7 +30,7 @@ function numericFormatter( alignOnDecimals = false, decimals = 4, zeroPad = false, - }: TypeFormatting = DEFAULT_NUMERIC_FORMATTING, + }: ColumnTypeFormatting = DEFAULT_NUMERIC_FORMATTING, align: "left" | "right" = "right" ) { const props = { className: "num" }; diff --git a/vuu-ui/packages/vuu-filters/src/filter-clause/ExpandoCombobox.tsx b/vuu-ui/packages/vuu-filters/src/filter-clause/ExpandoCombobox.tsx index 1b1aa3933..03f201514 100644 --- a/vuu-ui/packages/vuu-filters/src/filter-clause/ExpandoCombobox.tsx +++ b/vuu-ui/packages/vuu-filters/src/filter-clause/ExpandoCombobox.tsx @@ -103,8 +103,8 @@ export const ExpandoCombobox = forwardRef(function ExpandoCombobox< displayedItemCount: 10, itemHeight: 22, maxWidth: 300, - minWidth: 100, - width: "auto", + minWidth: 80, + width: "content-width", }, ]; }, [InputPropsProp, handleInputChange, ListPropsProp]); @@ -128,7 +128,7 @@ export const ExpandoCombobox = forwardRef(function ExpandoCombobox< ); const popupProps = { - minWidth: 102, + minWidth: "fit-content", }; return props.source?.length === 0 ? null : ( diff --git a/vuu-ui/packages/vuu-icons/index.css b/vuu-ui/packages/vuu-icons/index.css index a631f00b8..df0ae24f9 100644 --- a/vuu-ui/packages/vuu-icons/index.css +++ b/vuu-ui/packages/vuu-icons/index.css @@ -21,7 +21,6 @@ --svg-link: url('data:image/svg+xml;utf8,'); --svg-logout: url('data:image/svg+xml;utf8,'); --svg-rings: url('data:image/svg+xml;utf8,'); - --svg-settings: url('data:image/svg+xml;utf8,'); --svg-sort-order-down: url('data:image/svg+xml;utf8,'); --svg-sorted-asc: url('data:image/svg+xml;utf8,'); --svg-sorted-dsc: url('data:image/svg+xml;utf8,'); @@ -53,22 +52,27 @@ --vuu-svg-chevron-right: url('data:image/svg+xml;utf8,'); --vuu-svg-chevron-up: url('data:image/svg+xml;utf8,'); --vuu-svg-cross: url('data:image/svg+xml;utf8,'); + --vuu-svg-edit: url('data:image/svg+xml;utf8,'); + --vuu-svg-eye: url('data:image/svg+xml;utf8,'); + --vuu-svg-history: url('data:image/svg+xml;utf8,'); --vuu-svg-more-horiz: url('data:image/svg+xml;utf8,'); --vuu-svg-more-vert: url('data:image/svg+xml;utf8,'); - --vuu-svg-edit: url('data:image/svg+xml;utf8,'); --vuu-svg-plus: url('data:image/svg+xml;utf8,'); --vuu-svg-price-arrow: url('data:image/svg+xml;utf8,'); --vuu-svg-radio: url('data:image/svg+xml;utf8,'); --vuu-svg-search: url('data:image/svg+xml;utf8,'); + --vuu-svg-settings: url('data:image/svg+xml;utf8,'); --vuu-svg-tick: url('data:image/svg+xml;utf8,'); --vuu-svg-triangle-right: url('data:image/svg+xml;utf8,'); --vuu-svg-info-circle: url('data:image/svg+xml;utf8, '); --vuu-svg-warn-triangle: url('data:image/svg+xml;utf8,'); } - - span[data-icon] { + + + +span[data-icon] { display: inline-block; height: var(--vuu-icon-height, var(--vuu-icon-size, 18px)); position: relative; @@ -173,6 +177,9 @@ [data-icon='draggable'] { --vuu-icon-svg: var(--vuu-svg-draggable); } +[data-icon='eye'] { + --vuu-icon-svg: var(--vuu-svg-eye); +} [data-icon='error'] { --vuu-icon-color: var(--vuuIcon-color, var(--salt-status-error-foreground)); @@ -186,6 +193,9 @@ [data-icon='filter'] { --vuu-icon-svg: var(--svg-filter); } +[data-icon='history'] { + --vuu-icon-svg: var(--vuu-svg-history); +} [data-icon='link'] { --vuu-icon-svg: var(--svg-link); @@ -203,7 +213,6 @@ --vuu-icon-svg: var(--svg-rings); } - [data-icon='column-2A'] { --vuu-icon-svg: var(--svg-column-2A); } @@ -237,7 +246,7 @@ } [data-icon='settings'] { - --vuu-icon-svg: var(--svg-settings); + --vuu-icon-svg: var(--vuu-svg-settings); } [data-icon='sort-up'] { diff --git a/vuu-ui/packages/vuu-layout/package.json b/vuu-ui/packages/vuu-layout/package.json index 3d7e6e074..62d410a7a 100644 --- a/vuu-ui/packages/vuu-layout/package.json +++ b/vuu-ui/packages/vuu-layout/package.json @@ -12,6 +12,7 @@ "types": "src/index.ts", "dependencies": { "@salt-ds/core": "1.8.0", + "@finos/vuu-filters": "0.0.26", "@finos/vuu-popups": "0.0.26", "@finos/vuu-table": "0.0.26", "@finos/vuu-table-extras": "0.0.26", diff --git a/vuu-ui/packages/vuu-layout/src/layout-persistence/LayoutPersistenceManager.ts b/vuu-ui/packages/vuu-layout/src/layout-persistence/LayoutPersistenceManager.ts index 75abba111..88ca5bf96 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-persistence/LayoutPersistenceManager.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-persistence/LayoutPersistenceManager.ts @@ -10,7 +10,10 @@ export interface LayoutPersistenceManager { * * @returns Unique identifier assigned to the saved layout */ - createLayout: (metadata: LayoutMetadataDto, layout: LayoutJSON) => Promise; + createLayout: ( + metadata: LayoutMetadataDto, + layout: LayoutJSON + ) => Promise; /** * Overwrites an existing layout and its corresponding metadata with the provided information @@ -19,7 +22,11 @@ export interface LayoutPersistenceManager { * @param metadata - Metadata describing the new layout to overwrite with * @param layout - Full JSON representation of the new layout to overwrite with */ - updateLayout: (id: string, metadata: LayoutMetadataDto, layout: LayoutJSON) => Promise; + updateLayout: ( + id: string, + metadata: LayoutMetadataDto, + layout: LayoutJSON + ) => Promise; /** * Deletes an existing layout and its corresponding metadata @@ -52,9 +59,9 @@ export interface LayoutPersistenceManager { loadApplicationLayout: () => Promise; /** - * Saves the application layout which includes all layouts on screen - * - * @param layout - Full JSON representation of the application layout to be saved - */ - saveApplicationLayout: (layout: LayoutJSON) => Promise; + * Saves the application layout which includes all layouts on screen + * + * @param layout - Full JSON representation of the application layout to be saved + */ + saveApplicationLayout: (layout: LayoutJSON) => Promise; } diff --git a/vuu-ui/packages/vuu-layout/src/overflow-container/OverflowContainer.css b/vuu-ui/packages/vuu-layout/src/overflow-container/OverflowContainer.css index fe8a7f901..b3d0d67ad 100644 --- a/vuu-ui/packages/vuu-layout/src/overflow-container/OverflowContainer.css +++ b/vuu-ui/packages/vuu-layout/src/overflow-container/OverflowContainer.css @@ -1,21 +1,37 @@ .vuuOverflowContainer { - --item-gap : 2px; + --overflow-borderColor: var(--vuuOverflowContainer-borderColor, none); + --overflow-borderStyle: var(--vuuOverflowContainer-borderStyle, none); + --overflow-borderWidth: var(--vuuOverflowContainer-borderWidth, 0px); + + --overflow-border-topWidth: var(--vuuOverflowContainer-borderTopWidth, var(--overflow-borderWidth)); + --overflow-border-rightWidth: var(--vuuOverflowContainer-borderRightWidth, var(--overflow-borderWidth)); + --overflow-border-bottomWidth: var(--vuuOverflowContainer-borderBottomWidth, var(--overflow-borderWidth)); + --overflow-border-leftWidth: var(--vuuOverflowContainer-borderLeftWidth, var(--overflow-borderWidth)); + + --overflow-item-gap : var(--vuuOverflowContainer-gap,2px); --overflow-direction: row; - --overflow-height: var(--item-height); + --overflow-height: var(--overflow-item-height); --overflow-top:top:0; --overflow-width: 0px; + background-color: var(--vuuOverflowContainer-background); - height: var(--overflow-container-height); + border-color: var(--overflow-borderColor); + border-style: var(--overflow-borderStyle); + border-top-width: var(--overflow-border-topWidth); + border-right-width: var(--overflow-border-rightWidth); + border-bottom-width: var(--overflow-border-bottomWidth); + border-left-width: var(--overflow-border-leftWidth); + height: var(--overflow-container-heightWidth); } .vuuOverflowContainer-horizontal { --item-align: center; - --item-margin: 0 var(--item-gap) 0 var(--item-gap); + --item-margin: 0 var(--overflow-item-gap) 0 var(--overflow-item-gap); } .vuuOverflowContainer-vertical { --item-align: stretch; - --item-height: auto; - --item-margin: var(--item-gap) 0 var(--item-gap) 0; + --overflow-item-height: auto; + --item-margin: var(--overflow-item-gap) 0 var(--overflow-item-gap) 0; --overflow-direction: column; --overflow-height: 0; --overflow-left: 0; @@ -25,12 +41,14 @@ } .vuuOverflowContainer-wrapContainer { - --item-height: var(--overflow-container-height); + --border: calc(var(--overflow-border-topWidth) + var(--overflow-border-bottomWidth)); + --overflow-item-height: calc(var(--overflow-container-height) - var(--border)); align-items: var(--item-align); display: flex; flex-direction: var(--overflow-direction); flex-wrap: wrap; - height: var(--item-height); + height: var(--overflow-item-height); + justify-content: var(--vuuOverflowContainer-justifyContent, flex-start); min-width: var(--vuuOverflowContainer-minWidth, 44px); overflow: hidden; position: relative; @@ -54,7 +72,7 @@ display: flex; order: 1; position: relative; - height: var(--item-height); + height: var(--overflow-item-height); margin: var(--item-margin); } diff --git a/vuu-ui/packages/vuu-layout/src/overflow-container/OverflowContainer.tsx b/vuu-ui/packages/vuu-layout/src/overflow-container/OverflowContainer.tsx index 9d66ed388..da47264c6 100644 --- a/vuu-ui/packages/vuu-layout/src/overflow-container/OverflowContainer.tsx +++ b/vuu-ui/packages/vuu-layout/src/overflow-container/OverflowContainer.tsx @@ -146,7 +146,7 @@ export const OverflowContainer = forwardRef(function OverflowContainer( return (
diff --git a/vuu-ui/packages/vuu-layout/src/toolbar/Toolbar.css b/vuu-ui/packages/vuu-layout/src/toolbar/Toolbar.css index 5a5455d3f..126111b3c 100644 --- a/vuu-ui/packages/vuu-layout/src/toolbar/Toolbar.css +++ b/vuu-ui/packages/vuu-layout/src/toolbar/Toolbar.css @@ -1,10 +1,34 @@ .vuuToolbar { - background: var(--vuuToolbar-background, inherit); - height: var(--vuuToolbar-height, 36px); + --toolbar-height: var(--vuuToolbar-height, 36px); + --toolbar-item-height: var(--vuuToolbarItem-height, 100%); + --vuuOverflowContainer-background: var(--vuuToolbar-background); + --vuuOverflowContainer-borderColor: var(--vuuToolbar-borderColor); + --vuuOverflowContainer-borderStyle: var(--vuuToolbar-borderStyle); + --vuuOverflowContainer-borderWidth: var(--vuuToolbar-borderWidth); + height: var(--toolbar-height); +} + +.vuuToolbar-alignCenter { + --vuuOverflowContainer-justifyContent: center; +} + +.vuuToolbar-alignEnd { + --vuuOverflowContainer-justifyContent: flex-end; } .vuuToolbarItem { - height: 100%; + height: var(--toolbar-item-height); +} + +.vuuToolbar-withSeparators .vuuOverflowContainer-item:not(:first-child):before { + content: ''; + position: absolute; + left: calc(-1 * var(--overflow-item-gap)); + top: calc((var(--toolbar-height) - var(--toolbar-item-height)) /2); + width: 1px; + /* height: calc(var(--basket-selector-height) - 16px); */ + height: var(--toolbar-item-height); + background-color: var(--vuu-color-gray-05); } diff --git a/vuu-ui/packages/vuu-layout/src/toolbar/Toolbar.tsx b/vuu-ui/packages/vuu-layout/src/toolbar/Toolbar.tsx index 1585b654b..312627fd2 100644 --- a/vuu-ui/packages/vuu-layout/src/toolbar/Toolbar.tsx +++ b/vuu-ui/packages/vuu-layout/src/toolbar/Toolbar.tsx @@ -21,6 +21,7 @@ export type ActiveItemChangeHandler = (itemIndex: number[]) => void; export type NavigationOutOfBoundsHandler = (direction: "start" | "end") => void; export interface ToolbarProps extends OverflowContainerProps { activeItemIndex?: number[]; + alignItems?: "start" | "center" | "end"; defaultActiveItemIndex?: number[]; onActiveChange?: ActiveItemChangeHandler; /** @@ -30,18 +31,21 @@ export interface ToolbarProps extends OverflowContainerProps { */ onNavigateOutOfBounds?: NavigationOutOfBoundsHandler; selectionStrategy?: SelectionStrategy | SpecialKeyMultipleSelection; + showSeparators?: boolean; } export const Toolbar = ({ activeItemIndex: activeItemIndexProp, + alignItems = "start", defaultActiveItemIndex, children, - className: classNameProp, + className, id: idProp, onActiveChange, onNavigateOutOfBounds, orientation = "horizontal", selectionStrategy = "none", + showSeparators = false, ...props }: ToolbarProps) => { const rootRef = useRef(null); @@ -62,7 +66,6 @@ export const Toolbar = ({ }); const id = useId(idProp); - const className = cx(classBase, `${classBase}-${orientation}`, classNameProp); const items = useMemo( () => @@ -90,7 +93,11 @@ export const Toolbar = ({ diff --git a/vuu-ui/packages/vuu-popups/src/popup/Popup.tsx b/vuu-ui/packages/vuu-popups/src/popup/Popup.tsx index 893b7273e..88192586d 100644 --- a/vuu-ui/packages/vuu-popups/src/popup/Popup.tsx +++ b/vuu-ui/packages/vuu-popups/src/popup/Popup.tsx @@ -14,7 +14,7 @@ export type PopupPlacement = export interface PopupComponentProps extends HTMLAttributes { anchorElement: RefObject; - minWidth?: number; + minWidth?: number | string; offsetLeft?: number; offsetTop?: number; placement: PopupPlacement; diff --git a/vuu-ui/packages/vuu-popups/src/popup/useAnchoredPosition.ts b/vuu-ui/packages/vuu-popups/src/popup/useAnchoredPosition.ts index f6fa8f635..9ca9cc747 100644 --- a/vuu-ui/packages/vuu-popups/src/popup/useAnchoredPosition.ts +++ b/vuu-ui/packages/vuu-popups/src/popup/useAnchoredPosition.ts @@ -18,11 +18,11 @@ const getPositionRelativeToAnchor = ( placement: PopupPlacement, offsetLeft: number, offsetTop: number, - minWidth?: number, + minWidth?: number | string, dimensions?: { height: number; width: number } ): { left: number; - minWidth?: number; + minWidth?: number | string; top: number; visibility?: Visibility; width?: number; @@ -66,7 +66,7 @@ const getPositionRelativeToAnchor = ( export type Position = { left: number; - minWidth?: number; + minWidth?: number | string; top: number; visibility?: Visibility; width?: number; diff --git a/vuu-ui/packages/vuu-popups/src/tooltip/Tooltip.css b/vuu-ui/packages/vuu-popups/src/tooltip/Tooltip.css index d085d3c91..b981d4436 100644 --- a/vuu-ui/packages/vuu-popups/src/tooltip/Tooltip.css +++ b/vuu-ui/packages/vuu-popups/src/tooltip/Tooltip.css @@ -1,5 +1,7 @@ .vuuTooltip { --tooltip-align: flex-start; + --tooltip-background: white; + --tooltip-color: var(--vuu-color-gray-80); --tooltip-justify: flex-start; --tooltip-top: auto; --tooltip-right: auto; @@ -34,9 +36,9 @@ } .vuuTooltip-content { - background-color: var(--salt-color-blue-500); + background-color: var(--tooltip-background); border-radius: 4px; - color: white; + color: var(--tooltip-color); line-height: 24px; padding: 2px 6px; position: absolute; @@ -48,14 +50,14 @@ } .vuuTooltip::before { - background-color: var(--salt-color-blue-500); + background-color: var(--tooltip-background); /* background-color: red; */ content: ' '; display: block; position: absolute; width: 12px; height: 12px; - z-index: -1; + /* z-index: -1; */ } .vuuTooltip[data-align='above']::before { @@ -72,4 +74,10 @@ .vuuTooltip[data-align='left']::before { transform: translate(-19px, 0) rotate(45deg); +} + +.vuuTooltip-error { + --tooltip-background: var(--vuu-color-red-50); + --tooltip-color: white; + color: white; } \ No newline at end of file diff --git a/vuu-ui/packages/vuu-popups/src/tooltip/Tooltip.tsx b/vuu-ui/packages/vuu-popups/src/tooltip/Tooltip.tsx index d52dbfdb6..d8f694d89 100644 --- a/vuu-ui/packages/vuu-popups/src/tooltip/Tooltip.tsx +++ b/vuu-ui/packages/vuu-popups/src/tooltip/Tooltip.tsx @@ -1,11 +1,13 @@ import { MouseEventHandler, ReactNode, RefObject } from "react"; import { Portal } from "../portal"; import { TooltipPlacement, useAnchoredPosition } from "./useAnchoredPosition"; +import cx from "classnames"; import "./Tooltip.css"; const classBase = "vuuTooltip"; +export type TooltipStatus = "warning" | "error" | "info"; export interface TooltipProps { anchorElement: RefObject; children: ReactNode; @@ -13,6 +15,7 @@ export interface TooltipProps { onMouseEnter: MouseEventHandler; onMouseLeave: MouseEventHandler; placement: TooltipPlacement; + status?: TooltipStatus; } export const Tooltip = ({ @@ -22,6 +25,7 @@ export const Tooltip = ({ onMouseEnter, onMouseLeave, placement, + status, }: TooltipProps) => { const position = useAnchoredPosition({ anchorElement, placement }); if (position === undefined) { @@ -30,7 +34,9 @@ export const Tooltip = ({ return (
ListOption[]; + +export type DefaultColumnConfiguration = ( + tableName: T, + columnName: string +) => Partial | undefined; export interface ShellContextProps { - getDefaultColumnConfig?: ( - tableName: string, - columnName: string - ) => Partial; + getDefaultColumnConfig?: DefaultColumnConfiguration; + getLookupValues?: LookupTableProvider; handleRpcResponse?: RpcResponseHandler; } diff --git a/vuu-ui/packages/vuu-shell/src/app-header/AppHeader.css b/vuu-ui/packages/vuu-shell/src/app-header/AppHeader.css index a79df4825..35fa260ef 100644 --- a/vuu-ui/packages/vuu-shell/src/app-header/AppHeader.css +++ b/vuu-ui/packages/vuu-shell/src/app-header/AppHeader.css @@ -1,15 +1,24 @@ .vuuAppHeader { + --saltButton-borderRadius: 6px; + --saltButton-text-color: var(--vuu-color-gray-50); + --saltButton-padding: 12px; + --vuuToolbarItem-height: 26px; + --vuuOverflowContainer-gap: 8px; + --vuu-icon-color: var(--vuu-color-gray-45); + --vuu-icon-size: 16px; + --vuuToolbar-background: var(--vuuAppHeader-background, var(--vuu-color-gray-28)); + --vuuToolbar-borderWidth: 1px; + --vuuToolbar-borderStyle: solid; + --vuuToolbar-borderColor: var(--vuu-color-gray-30); + align-items: center; - background: var(--vuuAppHeader-background, var(--salt-container-primary-background)); - border-bottom: solid 1px var(--salt-container-secondary-borderColor); display: flex; - height: 40px; justify-content: flex-end; } .vuu-theme .vuuAppHeader { border-radius: 8px; - border: 1px solid #D6D7DA; - height: 44px; + margin-bottom: 8px; -} \ No newline at end of file +} + diff --git a/vuu-ui/packages/vuu-shell/src/app-header/AppHeader.tsx b/vuu-ui/packages/vuu-shell/src/app-header/AppHeader.tsx index 56058b421..79b59c25a 100644 --- a/vuu-ui/packages/vuu-shell/src/app-header/AppHeader.tsx +++ b/vuu-ui/packages/vuu-shell/src/app-header/AppHeader.tsx @@ -1,11 +1,14 @@ import { HTMLAttributes, useCallback } from "react"; import { VuuUser } from "../shell"; -import { UserProfile } from "../user-profile"; -import { ThemeSwitch } from "../theme-switch"; +// import { UserProfile } from "../user-profile"; +// import { ThemeSwitch } from "../theme-switch"; import { ThemeMode } from "../theme-provider"; import cx from "classnames"; +import { Toolbar } from "@finos/vuu-layout"; +import { logout } from "../login"; import "./AppHeader.css"; +import { Button } from "@salt-ds/core"; const classBase = "vuuAppHeader"; export interface AppHeaderProps extends HTMLAttributes { @@ -28,19 +31,50 @@ export const AppHeader = ({ ...htmlAttributes }: AppHeaderProps) => { const className = cx(classBase, classNameProp); - const handleSwitchTheme = useCallback( - (mode: ThemeMode) => onSwitchTheme?.(mode), - [onSwitchTheme] - ); + // const handleSwitchTheme = useCallback( + // (mode: ThemeMode) => onSwitchTheme?.(mode), + // [onSwitchTheme] + // ); + + const handleLogout = useCallback(() => { + logout(loginUrl); + }, [loginUrl]); + return ( -
- - + + + + + {/* */} + {/* -
+ /> */} + ); }; diff --git a/vuu-ui/packages/vuu-shell/src/index.ts b/vuu-ui/packages/vuu-shell/src/index.ts index c6e19420d..954fed7b5 100644 --- a/vuu-ui/packages/vuu-shell/src/index.ts +++ b/vuu-ui/packages/vuu-shell/src/index.ts @@ -1,3 +1,4 @@ +export * from "./app-header"; export * from "./connection-status"; export * from "./density-switch"; export * from "./feature"; diff --git a/vuu-ui/packages/vuu-shell/src/layout-management/useLayoutManager.tsx b/vuu-ui/packages/vuu-shell/src/layout-management/useLayoutManager.tsx index 8e517fc7b..1c83e8f47 100644 --- a/vuu-ui/packages/vuu-shell/src/layout-management/useLayoutManager.tsx +++ b/vuu-ui/packages/vuu-shell/src/layout-management/useLayoutManager.tsx @@ -6,22 +6,28 @@ import React, { useState, } from "react"; import { + defaultLayout, LayoutJSON, + LayoutPersistenceManager, LocalLayoutPersistenceManager, RemoteLayoutPersistenceManager, resolveJSONPath, } from "@finos/vuu-layout"; -import { - LayoutMetadata, - LayoutMetadataDto -} from "./layoutTypes"; -import { defaultLayout } from "@finos/vuu-layout/"; +import { LayoutMetadata, LayoutMetadataDto } from "./layoutTypes"; const local = process.env.LOCAL ?? true; -const persistenceManager = local - ? new LocalLayoutPersistenceManager() - : new RemoteLayoutPersistenceManager(); +let _persistenceManager: LayoutPersistenceManager; + +const getPersistenceManager = () => { + if (_persistenceManager == undefined) { + _persistenceManager = local + ? new LocalLayoutPersistenceManager() + : new RemoteLayoutPersistenceManager(); + } + + return _persistenceManager; +}; export const LayoutManagementContext = React.createContext<{ layoutMetadata: LayoutMetadata[]; @@ -61,7 +67,8 @@ export const LayoutManagementProvider = ( ); useEffect(() => { - persistenceManager.loadMetadata() + getPersistenceManager() + .loadMetadata() .then((metadata) => { setLayoutMetadata(metadata); }) @@ -70,20 +77,24 @@ export const LayoutManagementProvider = ( console.error("Error occurred while retrieving metadata", error); }); - persistenceManager.loadApplicationLayout() + getPersistenceManager() + .loadApplicationLayout() .then((layout: LayoutJSON) => { setApplicationLayout(layout); }) .catch((error: Error) => { //TODO: Show error toaster - console.error("Error occurred while retrieving application layout", error); + console.error( + "Error occurred while retrieving application layout", + error + ); }); }, [setApplicationLayout]); const saveApplicationLayout = useCallback( (layout: LayoutJSON) => { setApplicationLayout(layout, false); - persistenceManager.saveApplicationLayout(layout); + getPersistenceManager().saveApplicationLayout(layout); }, [setApplicationLayout] ); @@ -95,7 +106,7 @@ export const LayoutManagementProvider = ( ); if (layoutToSave) { - persistenceManager + getPersistenceManager() .createLayout(metadata, layoutToSave) .then((metadata) => { //TODO: Show success toast @@ -111,14 +122,16 @@ export const LayoutManagementProvider = ( const loadLayoutById = useCallback( (id: string) => { - persistenceManager.loadLayout(id).then((layoutJson) => { - const { current: prev } = applicationLayoutRef; - setApplicationLayout({ - ...prev, - active: prev.children?.length ?? 0, - children: [...(prev.children || []), layoutJson], + getPersistenceManager() + .loadLayout(id) + .then((layoutJson) => { + const { current: prev } = applicationLayoutRef; + setApplicationLayout({ + ...prev, + active: prev.children?.length ?? 0, + children: [...(prev.children || []), layoutJson], + }); }); - }); }, [setApplicationLayout] ); diff --git a/vuu-ui/packages/vuu-shell/src/left-nav/LeftNav.css b/vuu-ui/packages/vuu-shell/src/left-nav/LeftNav.css index 7cef09867..e05b72301 100644 --- a/vuu-ui/packages/vuu-shell/src/left-nav/LeftNav.css +++ b/vuu-ui/packages/vuu-shell/src/left-nav/LeftNav.css @@ -14,11 +14,16 @@ --svg-layouts: url('data:image/svg+xml;utf8,'); --vuu-light-text-primary: #15171b; + --menu-level-2-width: 0px; + box-shadow: 3px 4px 4px 0px rgba(0, 0, 0, 0.15); + display: flex; margin-bottom: 4px; + overflow: hidden; position: relative; - transition: width .3s ease-in-out; + transition: width .2s ease-out; z-index: 0; + width: calc(var(--menu-width) + var(--menu-level-2-width)); } @@ -32,10 +37,12 @@ .vuuLeftNav-menu-icons-content { --menu-width: var(--nav-menu-collapsed-width); + --menu-level-2-width: var(--nav-menu-content-width); } .vuuLeftNav-menu-full-content { --menu-width: var(--nav-menu-expanded-width); + --menu-level-2-width: var(--nav-menu-content-width); } .vuuLeftNav-menu-icons-content .vuuLeftNav-menu-secondary, @@ -50,18 +57,22 @@ flex-direction: column; height: 100%; padding: 32px 16px; - transition: width ease .3s; - width: var(--menu-width, 100%); + transition: flex-basis ease-out .2s; + flex-grow:0; + flex-shrink:0; + flex-basis: var(--menu-width); } .vuuLeftNav-menu-secondary { flex: 1 1 auto; height: 100%; display: none; - position: absolute; + /* position: absolute; */ top:0; right: 0; - width: var(--nav-menu-content-width, 240px); + flex-grow:0; + flex-shrink:0; + flex-basis: var(--nav-menu-content-width, 240px); z-index: -1; } @@ -98,9 +109,6 @@ padding: 0; } -/* .vuuLeftNav [data-icon]:after { - transition: left ease .3s; -} */ .vuuLeftNav [data-icon='demo'] { --vuu-icon-svg: var(--svg-demo); diff --git a/vuu-ui/packages/vuu-shell/src/left-nav/LeftNav.tsx b/vuu-ui/packages/vuu-shell/src/left-nav/LeftNav.tsx index 722287349..9e80f8060 100644 --- a/vuu-ui/packages/vuu-shell/src/left-nav/LeftNav.tsx +++ b/vuu-ui/packages/vuu-shell/src/left-nav/LeftNav.tsx @@ -13,30 +13,39 @@ import "./LeftNav.css"; const classBase = "vuuLeftNav"; +export type NavDisplayStatus = + | "menu-full" + | "menu-icons" + | "menu-full-content" + | "menu-icons-content"; + +export type NavDisplayStatusHandler = ( + navDisplayStatus: NavDisplayStatus +) => void; interface LeftNavProps extends HTMLAttributes { "data-path"?: string; + defaultActiveTabIndex?: number; + defaultDisplayStatus?: NavDisplayStatus; features: FeatureProps[]; tableFeatures: FeatureProps[]; + onChangeDisplayStatus?: NavDisplayStatusHandler; onResize?: (size: number) => void; sizeCollapsed?: number; sizeContent?: number; sizeExpanded?: number; } -type NavStatus = - | "menu-full" - | "menu-icons" - | "menu-full-content" - | "menu-icons-content"; - type NavState = { activeTabIndex: number; - navStatus: NavStatus; + navStatus: NavDisplayStatus; }; export const LeftNav = ({ "data-path": path, + defaultDisplayStatus = "menu-full", + defaultActiveTabIndex = 0, features, + onChangeDisplayStatus, onResize, sizeCollapsed = 80, sizeContent = 300, @@ -47,13 +56,13 @@ export const LeftNav = ({ }: LeftNavProps) => { const dispatch = useLayoutProviderDispatch(); const [navState, setNavState] = useState({ - activeTabIndex: 0, - navStatus: "menu-full", + activeTabIndex: defaultActiveTabIndex, + navStatus: defaultDisplayStatus, }); const [themeClass] = useThemeAttributes(); const toggleNavWidth = useCallback( - (navStatus: NavStatus) => { + (navStatus: NavDisplayStatus) => { switch (navStatus) { case "menu-icons": return sizeExpanded; @@ -68,7 +77,7 @@ export const LeftNav = ({ [sizeCollapsed, sizeContent, sizeExpanded] ); - const toggleNavStatus = (navStatus: NavStatus) => { + const toggleNavStatus = (navStatus: NavDisplayStatus) => { switch (navStatus) { case "menu-icons": return "menu-full"; @@ -82,7 +91,10 @@ export const LeftNav = ({ }; const getWidthAndStatus = useCallback( - (navState: NavStatus, tabIndex: number): [number, NavStatus] => { + ( + navState: NavDisplayStatus, + tabIndex: number + ): [number, NavDisplayStatus] => { if (tabIndex === 0) { const newNavState = navState === "menu-full-content" @@ -127,16 +139,18 @@ export const LeftNav = ({ const toggleSize = useCallback(() => { const { activeTabIndex, navStatus: currentNavStatus } = navState; + const newNavStatus = toggleNavStatus(currentNavStatus); setNavState({ activeTabIndex, - navStatus: toggleNavStatus(currentNavStatus), + navStatus: newNavStatus, }); dispatch({ type: Action.LAYOUT_RESIZE, path, size: toggleNavWidth(currentNavStatus), } as LayoutResizeAction); - }, [dispatch, navState, path, toggleNavWidth]); + onChangeDisplayStatus?.(newNavStatus); + }, [dispatch, navState, onChangeDisplayStatus, path, toggleNavWidth]); const style = { ...styleProp, diff --git a/vuu-ui/packages/vuu-shell/src/login/LoginPanel.css b/vuu-ui/packages/vuu-shell/src/login/LoginPanel.css index 9aaa5f776..6a04d73d7 100644 --- a/vuu-ui/packages/vuu-shell/src/login/LoginPanel.css +++ b/vuu-ui/packages/vuu-shell/src/login/LoginPanel.css @@ -1,24 +1,70 @@ .vuuLoginPanel { + --saltInput-paddingLeft: 8px; + --login-width: 856px; + --login-height: 550px; --hwTextInput-border: solid 1px #ccc; --hwTextInput-height: 28px; --hwTextInput-padding: 0 12px; --hwTextInput-width: 100%; --login-row-height: 60px; - align-content: center; + border-radius: 16px; + box-shadow: 0px 24px 44px 0px rgba(0, 0, 0, 0.25); + display: flex; + height: var(--login-height); + margin: 0 auto; + width: var(--login-width); +} + +.vuuLoginPanel .saltInput-primary { +--saltInput-height: 36px; +} + +.vuuLoginPanel-title { + justify-content: center; +font-size: 28px; +font-weight: 400; +display: flex; +} + + +.vuuLoginPanel-branding { align-items: center; - border: solid 1px lightgray; + background-color: var(--vuu-color-purple-50); + border-radius: 16px 0 0 16px; display: flex; + flex: 0 0 368px; flex-direction: column; - gap: 24px; + gap: 40px; justify-content: center; - justify-items: center; - margin: 0 auto; - padding: 48px 48px 24px 48px; - width: fit-content; + padding: 40px 100px; +} + +.vuuLoginPanel-form { + background-color: white; + border-radius: 0 16px 16px 0px; + display: flex; + flex: 1 1 auto; + flex-direction: column; + gap: 32px; + justify-content: center; + padding: 0 40px; +} + +.vuuLoginPanel-password { + --vuu-icon-size: 16px; +} + +.vuuLoginPanel-appName { + color: white; + font-size: 18px; + font-weight: 700; + text-transform: uppercase; } .vuuLoginPanel-login { - grid-column: 2/3; - align-self: end; - justify-self: end; + --saltButton-height: 36px; + --saltButton-borderRadius: 6px; + width: 100% } + + diff --git a/vuu-ui/packages/vuu-shell/src/login/LoginPanel.tsx b/vuu-ui/packages/vuu-shell/src/login/LoginPanel.tsx index e909d4894..b01ffacfa 100644 --- a/vuu-ui/packages/vuu-shell/src/login/LoginPanel.tsx +++ b/vuu-ui/packages/vuu-shell/src/login/LoginPanel.tsx @@ -1,5 +1,15 @@ -import { ChangeEvent, HTMLAttributes, useState } from "react"; -import { Button, FormField, FormFieldLabel, Input } from "@salt-ds/core"; +import { + ChangeEvent, + HTMLAttributes, + useCallback, + useEffect, + useRef, + useState, +} from "react"; +import { Button, FormField, FormFieldLabel } from "@salt-ds/core"; +import { VuuInput } from "@finos/vuu-ui-controls"; +import { VuuLogo } from "./VuuLogo"; +import cx from "classnames"; import "./LoginPanel.css"; @@ -7,16 +17,21 @@ const classBase = "vuuLoginPanel"; export interface LoginPanelProps extends Omit, "onSubmit"> { - onSubmit: (username: string, password: string) => void; + appName?: string; + onSubmit: (username: string, password?: string) => void; requirePassword?: boolean; } export const LoginPanel = ({ + appName = "Demo App", + className, requirePassword = true, onSubmit, + ...htmlAttributes }: LoginPanelProps) => { const [username, setUserName] = useState(""); const [password, setPassword] = useState(""); + const inputRef = useRef(null); const login = () => { onSubmit(username, password); @@ -30,39 +45,76 @@ export const LoginPanel = ({ setPassword(evt.target.value); }; + const handleCommitName = useCallback(() => { + if (!requirePassword) { + onSubmit(username); + } + }, [onSubmit, requirePassword, username]); + + const handleCommitPassword = useCallback(() => { + if (username) { + onSubmit(username, password); + } + }, [onSubmit, password, username]); + const dataIsValid = username.trim() !== "" && (requirePassword === false || password.trim() !== ""); - return ( -
- - Username - - + useEffect(() => { + console.log(`inputRef`, { + input: inputRef.current, + }); + inputRef.current?.focus(); + }, []); - {requirePassword ? ( - - Password - +
+ +
{appName}
+
+
+
Welcome Back
+ + Username + - ) : null} - + {requirePassword ? ( + + Password + + } + /> + + ) : null} + + +
); }; diff --git a/vuu-ui/packages/vuu-shell/src/login/VuuLogo.tsx b/vuu-ui/packages/vuu-shell/src/login/VuuLogo.tsx new file mode 100644 index 000000000..87665de04 --- /dev/null +++ b/vuu-ui/packages/vuu-shell/src/login/VuuLogo.tsx @@ -0,0 +1,91 @@ +import { useMemo } from "react"; + +export const VuuLogo = () => { + const logo = useMemo( + () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ), + [] + ); + + return logo; +}; diff --git a/vuu-ui/packages/vuu-table-extras/src/cell-renderers-next/background-cell/BackgroundCell.css b/vuu-ui/packages/vuu-table-extras/src/cell-renderers-next/background-cell/BackgroundCell.css index ee6105aaf..3c4efd0f3 100644 --- a/vuu-ui/packages/vuu-table-extras/src/cell-renderers-next/background-cell/BackgroundCell.css +++ b/vuu-ui/packages/vuu-table-extras/src/cell-renderers-next/background-cell/BackgroundCell.css @@ -34,22 +34,30 @@ padding-right: 8px; } -.vuuBackgroundCell.up1 { +.vuuBackgroundCell-arrowOnly > .vuuBackgroundCell-flasher { + background-color: transparent; +} + +.vuuBackgroundCell-backgroundOnly.up1, +.vuuBackgroundCell-backgroundArrow.up1 { animation-duration: 30s; animation-name: reactbgup1; } -.vuuBackgroundCell.up2 { +.vuuBackgroundCell-backgroundOnly.up2, +.vuuBackgroundCell-backgroundArrow.up2 { animation-duration: 30s; animation-name: reactbgup2; } -.vuuBackgroundCell.down1 { +.vuuBackgroundCell-backgroundOnly.down1, +.vuuBackgroundCell-backgroundArrow.down1 { animation-duration: 30s; animation-name: reactbgdown1; } -.vuuBackgroundCell.down2 { +.vuuBackgroundCell-backgroundOnly.down2, +.vuuBackgroundCell-backgroundArrow.down2 { animation-duration: 30s; animation-name: reactbgdown2; } diff --git a/vuu-ui/packages/vuu-table-extras/src/cell-renderers-next/background-cell/BackgroundCell.tsx b/vuu-ui/packages/vuu-table-extras/src/cell-renderers-next/background-cell/BackgroundCell.tsx index cc3c58c5a..24b2b65a4 100644 --- a/vuu-ui/packages/vuu-table-extras/src/cell-renderers-next/background-cell/BackgroundCell.tsx +++ b/vuu-ui/packages/vuu-table-extras/src/cell-renderers-next/background-cell/BackgroundCell.tsx @@ -58,6 +58,7 @@ export const BackgroundCell = ({ column, row }: TableCellProps) => { const dirClass = direction ? ` ` + direction : ""; const className = cx(classBase, dirClass, { + [`${classBase}-backgroundOnly`]: flashStyle === FlashStyle.BackgroundOnly, [`${classBase}-arrowOnly`]: flashStyle === FlashStyle.ArrowOnly, [`${classBase}-arrowBackground`]: flashStyle === FlashStyle.ArrowBackground, }); @@ -70,8 +71,14 @@ export const BackgroundCell = ({ column, row }: TableCellProps) => { ); }; -registerComponent("background-next", BackgroundCell, "cell-renderer", { - description: "Change background color of cell when value changes", - label: "Background Flash", - serverDataType: ["long", "int", "double"], -}); +registerComponent( + "vuu.price-move-background", + BackgroundCell, + "cell-renderer", + { + description: "Change background color of cell when value changes", + configEditor: "BackgroundCellConfigurationEditor", + label: "Background Flash", + serverDataType: ["long", "int", "double"], + } +); diff --git a/vuu-ui/packages/vuu-table-extras/src/cell-renderers-next/background-cell/BackgroundCellConfigurationEditor.css b/vuu-ui/packages/vuu-table-extras/src/cell-renderers-next/background-cell/BackgroundCellConfigurationEditor.css new file mode 100644 index 000000000..73ace7396 --- /dev/null +++ b/vuu-ui/packages/vuu-table-extras/src/cell-renderers-next/background-cell/BackgroundCellConfigurationEditor.css @@ -0,0 +1,4 @@ +.vuuBackgroundCellConfiguration { + height: 50px; + background-color: red; +} \ No newline at end of file diff --git a/vuu-ui/packages/vuu-table-extras/src/cell-renderers-next/background-cell/BackgroundCellConfigurationEditor.tsx b/vuu-ui/packages/vuu-table-extras/src/cell-renderers-next/background-cell/BackgroundCellConfigurationEditor.tsx new file mode 100644 index 000000000..2f98442d0 --- /dev/null +++ b/vuu-ui/packages/vuu-table-extras/src/cell-renderers-next/background-cell/BackgroundCellConfigurationEditor.tsx @@ -0,0 +1,70 @@ +import { + ConfigurationEditorProps, + registerConfigurationEditor, +} from "@finos/vuu-utils"; +import { FormField, FormFieldLabel } from "@salt-ds/core"; +import { Dropdown, SingleSelectionHandler } from "@finos/vuu-ui-controls"; + +import "./BackgroundCellConfigurationEditor.css"; +import { useCallback, useState } from "react"; +import { ColumnDescriptorCustomRenderer } from "packages/vuu-datagrid-types"; + +const classBase = "vuuBackgroundCellConfiguration"; + +type FlashOption = { + label: string; + value: "arrow" | "bg-only" | "arrow-bg"; +}; + +const flashOptions: FlashOption[] = [ + { label: "Background Only", value: "bg-only" }, + { label: "Background and Arrow", value: "arrow-bg" }, + { label: "Arrow Only", value: "arrow" }, +]; + +const [defaultFlashOption] = flashOptions; + +const valueFromColumn = (column: ColumnDescriptorCustomRenderer) => { + const { flashStyle } = column.type.renderer; + return flashOptions.find((o) => o.value === flashStyle) || defaultFlashOption; +}; + +export const BackgroundCellConfigurationEditor = ({ + column, + onChangeRendering, +}: ConfigurationEditorProps) => { + const [flashStyle, setFlashStyle] = useState( + valueFromColumn(column) + ); + const handleSelectionChange = useCallback< + SingleSelectionHandler + >( + (_, flashOption) => { + setFlashStyle(flashOption); + const renderProps = column.type.renderer; + onChangeRendering({ + ...renderProps, + flashStyle: flashOption?.value ?? defaultFlashOption.value, + }); + }, + [column.type, onChangeRendering] + ); + + return ( + + Flash Style + + className={`${classBase}-flashStyle`} + onSelectionChange={handleSelectionChange} + selected={flashStyle} + source={flashOptions} + width="100%" + /> + + ); +}; + +registerConfigurationEditor( + "BackgroundCellConfigurationEditor", + BackgroundCellConfigurationEditor +); diff --git a/vuu-ui/packages/vuu-table-extras/src/cell-renderers-next/background-cell/FlashingBackground.css b/vuu-ui/packages/vuu-table-extras/src/cell-renderers-next/background-cell/FlashingBackground.css index afe889aa0..39d4277fc 100644 --- a/vuu-ui/packages/vuu-table-extras/src/cell-renderers-next/background-cell/FlashingBackground.css +++ b/vuu-ui/packages/vuu-table-extras/src/cell-renderers-next/background-cell/FlashingBackground.css @@ -1,57 +1,57 @@ -@property --background-cell-background { - syntax: ''; /* <- defined as type number for the transition to work */ +/* @property --background-cell-background { + syntax: ''; initial-value: transparent; inherits: false; -} +} */ -@property --background-cell-color { - syntax: ''; /* <- defined as type number for the transition to work */ +/* @property color { + syntax: ''; initial-value: #15171B; inherits: false; -} +} */ @keyframes reactbgup1 { from { - --background-cell-background: var(--vuu-color-green-50); - --background-cell-color: white; + background-color: var(--vuu-color-green-50); + color: white; } to { - --background-cell-background: transparent; - --background-cell-color: var(--vuu-color-gray-80); + background-color: transparent; + color: var(--vuu-color-gray-80); } } @keyframes reactbgup2 { from { - --background-cell-background: var(--vuu-color-green-50); - --background-cell-color: #ffffff; + background-color: var(--vuu-color-green-50); + color: #ffffff; } to { - --background-cell-background: transparent; - --background-cell-color: var(--vuu-color-gray-80); + background-color: transparent; + color: var(--vuu-color-gray-80); } } @keyframes reactbgdown1 { from { - --background-cell-background: var(--vuu-color-red-50); - --background-cell-color: white; + background-color: var(--vuu-color-red-50); + color: white; } to { - --background-cell-background: transparent; - --background-cell-color: var(--vuu-color-gray-80); + background-color: transparent; + color: var(--vuu-color-gray-80); } } @keyframes reactbgdown2 { from { - --background-cell-background: var(--vuu-color-red-50); - --background-cell-color: white; + background-color: var(--vuu-color-red-50); + color: white; } to { - --background-cell-background: transparent; - --background-cell-color: var(--vuu-color-gray-80); + background-color: transparent; + color: var(--vuu-color-gray-80); } } diff --git a/vuu-ui/packages/vuu-table-extras/src/cell-renderers-next/background-cell/index.ts b/vuu-ui/packages/vuu-table-extras/src/cell-renderers-next/background-cell/index.ts index 6003e7c50..2096217f6 100644 --- a/vuu-ui/packages/vuu-table-extras/src/cell-renderers-next/background-cell/index.ts +++ b/vuu-ui/packages/vuu-table-extras/src/cell-renderers-next/background-cell/index.ts @@ -1 +1,2 @@ export * from "./BackgroundCell"; +export * from "./BackgroundCellConfigurationEditor"; diff --git a/vuu-ui/packages/vuu-table-extras/src/cell-renderers/background-cell/BackgroundCell.css b/vuu-ui/packages/vuu-table-extras/src/cell-renderers/background-cell/BackgroundCell.css deleted file mode 100644 index 8f14fd7c6..000000000 --- a/vuu-ui/packages/vuu-table-extras/src/cell-renderers/background-cell/BackgroundCell.css +++ /dev/null @@ -1,94 +0,0 @@ -.vuuTable td:has(> .vuuBackgroundCellDeprecated){ - padding: 0; - text-align: right; -} - -.vuuBackgroundCellDeprecated { - padding-right: var(--salt-size-unit); - position: relative; - z-index: -1; -} - -.vuuBackgroundCellDeprecated-flasher { - color: transparent; - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: -1; -} - -.vuuBackgroundCellDeprecated-flasher { - text-align: left; -} -.vuuBackgroundCellDeprecated-flasher + .num { - padding-left: 8px; -} - -.right .vuuBackgroundCellDeprecated-flasher { - text-align: right; -} -.right .vuuBackgroundCellDeprecated-flasher + .num { - padding-right: 8px; -} - -.up1 > .vuuBackgroundCellDeprecated-flasher { - animation-duration: 30s; - animation-name: reactbgup1; -} - -.up2 > .vuuBackgroundCellDeprecated-flasher { - animation-duration: 30s; - animation-name: reactbgup2; -} - -.down1 > .vuuBackgroundCellDeprecated-flasher { - animation-duration: 30s; - animation-name: reactbgdown1; -} - -.down2 > .vuuBackgroundCellDeprecated-flasher { - animation-duration: 30s; - animation-name: reactbgdown2; -} - -.up1.vuuBackgroundCellDeprecated-arrowOnly > .vuuBackgroundCellDeprecated-flasher { - animation-duration: 30s; - animation-name: reactarrowup1; -} - -.up2.vuuBackgroundCellDeprecated-arrowOnly > .vuuBackgroundCellDeprecated-flasher { - animation-duration: 30s; - animation-name: reactarrowup2; -} - -.down1.vuuBackgroundCellDeprecated-arrowOnly > .vuuBackgroundCellDeprecated-flasher { - animation-duration: 30s; - animation-name: reactarrowdown1; -} - -.down2.vuuBackgroundCellDeprecated-arrowOnly > .vuuBackgroundCellDeprecated-flasher { - animation-duration: 30s; - animation-name: reactarrowdown2; -} - -.up1.vuuBackgroundCellDeprecated-arrowBackground > .vuuBackgroundCellDeprecated-flasher { - animation-duration: 30s; - animation-name: reactbgarrowup1; -} - -.up2.vuuBackgroundCellDeprecated-arrowBackground > .vuuBackgroundCellDeprecated-flasher { - animation-duration: 30s; - animation-name: reactbgarrowup2; -} - -.down1.vuuBackgroundCellDeprecated-arrowBackground > .vuuBackgroundCellDeprecated-flasher { - animation-duration: 30s; - animation-name: reactbgarrowdown1; -} - -.down2.vuuBackgroundCellDeprecated-arrowBackground > .vuuBackgroundCellDeprecated-flasher { - animation-duration: 30s; - animation-name: reactbgarrowdown2; -} diff --git a/vuu-ui/packages/vuu-table-extras/src/cell-renderers/background-cell/BackgroundCell.tsx b/vuu-ui/packages/vuu-table-extras/src/cell-renderers/background-cell/BackgroundCell.tsx deleted file mode 100644 index a1af89b0b..000000000 --- a/vuu-ui/packages/vuu-table-extras/src/cell-renderers/background-cell/BackgroundCell.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { ColumnType, TableCellProps } from "@finos/vuu-datagrid-types"; -import { - DOWN1, - DOWN2, - isTypeDescriptor, - metadataKeys, - registerComponent, - UP1, - UP2, -} from "@finos/vuu-utils"; -import cx from "classnames"; -import { useDirection } from "./useDirection"; - -import "./BackgroundCell.css"; -import "./FlashingBackground.css"; - -const CHAR_ARROW_UP = String.fromCharCode(11014); -const CHAR_ARROW_DOWN = String.fromCharCode(11015); - -const { KEY } = metadataKeys; - -const classBase = "vuuBackgroundCellDeprecated"; - -// TODO these sre repeated from PriceFormatter - where shoud they live ? -const FlashStyle = { - ArrowOnly: "arrow", - BackgroundOnly: "bg-only", - ArrowBackground: "arrow-bg", -}; - -const getFlashStyle = (colType?: ColumnType) => { - if (isTypeDescriptor(colType) && colType.renderer) { - if ("flashStyle" in colType.renderer) { - return colType.renderer["flashStyle"]; - } - } - return FlashStyle.BackgroundOnly; -}; - -const BackgroundCell = ({ column, row }: TableCellProps) => { - //TODO what about click handling - - const { key, type, valueFormatter } = column; - const value = row[key]; - const flashStyle = getFlashStyle(type); - const direction = useDirection(row[KEY], value, column); - const arrow = - flashStyle === FlashStyle.ArrowOnly || - flashStyle === FlashStyle.ArrowBackground - ? direction === UP1 || direction === UP2 - ? CHAR_ARROW_UP - : direction === DOWN1 || direction === DOWN2 - ? CHAR_ARROW_DOWN - : null - : null; - - const dirClass = direction ? ` ` + direction : ""; - - const className = cx(classBase, dirClass, { - [`${classBase}-arrowOnly`]: flashStyle === FlashStyle.ArrowOnly, - [`${classBase}-arrowBackground`]: flashStyle === FlashStyle.ArrowBackground, - }); - - return ( -
-
{arrow}
- {valueFormatter(row[column.key])} -
- ); -}; - -registerComponent("background", BackgroundCell, "cell-renderer", { - description: "Background Cell renderer for VuuTable", - label: "Background Cell (deprecated)", - serverDataType: ["long", "int", "double"], -}); diff --git a/vuu-ui/packages/vuu-table-extras/src/cell-renderers/background-cell/FlashingBackground.css b/vuu-ui/packages/vuu-table-extras/src/cell-renderers/background-cell/FlashingBackground.css deleted file mode 100644 index cf4a729c7..000000000 --- a/vuu-ui/packages/vuu-table-extras/src/cell-renderers/background-cell/FlashingBackground.css +++ /dev/null @@ -1,132 +0,0 @@ -@keyframes reactbgup1 { - from { - background-color: green; - } - to { - background-color: transparent; - } - } - - @keyframes reactbgup2 { - from { - background-color: green; - } - to { - background-color: transparent; - } - } - - @keyframes reactbgdown1 { - from { - background-color: red; - } - to { - background-color: transparent; - } - } - - @keyframes reactbgdown2 { - from { - background-color: red; - } - to { - background-color: transparent; - } - } - - @keyframes reactarrowup1 { - from { - color: white; - } - to { - color: transparent; - } - } - - @keyframes reactarrowup2 { - from { - color: white; - } - to { - color: transparent; - } - } - - @keyframes reactarrowdown1 { - from { - color: white; - } - to { - color: transparent; - } - } - - @keyframes reactarrowdown2 { - from { - color: white; - } - to { - color: transparent; - } - } - - @keyframes reactbgarrowup1 { - 0% { - color: green; - background-color: green; - } - 20% { - color: green; - background-color: transparent; - } - 100% { - color: transparent; - background-color: transparent; - } - } - - @keyframes reactbgarrowup2 { - 0% { - color: green; - background-color: green; - } - 20% { - color: green; - background-color: transparent; - } - 100% { - color: transparent; - background-color: transparent; - } - } - - @keyframes reactbgarrowdown1 { - 0% { - color: red; - background-color: red; - } - 20% { - color: red; - background-color: transparent; - } - 100% { - color: transparent; - background-color: transparent; - } - } - - @keyframes reactbgarrowdown2 { - 0% { - color: red; - background-color: red; - } - 20% { - color: red; - background-color: transparent; - } - 100% { - color: transparent; - background-color: transparent; - } - } - \ No newline at end of file diff --git a/vuu-ui/packages/vuu-table-extras/src/cell-renderers/background-cell/index.ts b/vuu-ui/packages/vuu-table-extras/src/cell-renderers/background-cell/index.ts deleted file mode 100644 index 6003e7c50..000000000 --- a/vuu-ui/packages/vuu-table-extras/src/cell-renderers/background-cell/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./BackgroundCell"; diff --git a/vuu-ui/packages/vuu-table-extras/src/cell-renderers/background-cell/useDirection.ts b/vuu-ui/packages/vuu-table-extras/src/cell-renderers/background-cell/useDirection.ts deleted file mode 100644 index b604a67d9..000000000 --- a/vuu-ui/packages/vuu-table-extras/src/cell-renderers/background-cell/useDirection.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; -import { - getMovingValueDirection, - isTypeDescriptor, - isValidNumber, - valueChangeDirection, -} from "@finos/vuu-utils"; -import { useEffect, useRef } from "react"; - -const INITIAL_VALUE = [undefined, undefined, undefined, undefined]; - -type State = [string, unknown, KeyedColumnDescriptor, valueChangeDirection]; - -export function useDirection( - key: string, - value: unknown, - column: KeyedColumnDescriptor -) { - const ref = useRef(); - const [prevKey, prevValue, prevColumn, prevDirection] = - ref.current || INITIAL_VALUE; - - const { type: dataType } = column; - const decimals = isTypeDescriptor(dataType) - ? dataType.formatting?.decimals - : undefined; - - const direction = - key === prevKey && - isValidNumber(value) && - isValidNumber(prevValue) && - column === prevColumn - ? getMovingValueDirection(value, prevDirection, prevValue, decimals) - : ""; - - useEffect(() => { - ref.current = [key, value, column, direction]; - }); - - return direction; -} diff --git a/vuu-ui/packages/vuu-table-extras/src/cell-renderers/index.ts b/vuu-ui/packages/vuu-table-extras/src/cell-renderers/index.ts index e6346037f..f3fd0c349 100644 --- a/vuu-ui/packages/vuu-table-extras/src/cell-renderers/index.ts +++ b/vuu-ui/packages/vuu-table-extras/src/cell-renderers/index.ts @@ -1,2 +1 @@ -export * from "./background-cell"; export * from "./progress-cell"; diff --git a/vuu-ui/packages/vuu-table-extras/src/column-formatting-settings/ColumnFormattingPanel.tsx b/vuu-ui/packages/vuu-table-extras/src/column-formatting-settings/ColumnFormattingPanel.tsx index c6fa92193..efd4d2a43 100644 --- a/vuu-ui/packages/vuu-table-extras/src/column-formatting-settings/ColumnFormattingPanel.tsx +++ b/vuu-ui/packages/vuu-table-extras/src/column-formatting-settings/ColumnFormattingPanel.tsx @@ -1,36 +1,44 @@ -import { ColumnDescriptor, TypeFormatting } from "@finos/vuu-datagrid-types"; +import { + ColumnDescriptor, + ColumnDescriptorCustomRenderer, + ColumnTypeFormatting, + ColumnTypeRendering, +} from "@finos/vuu-datagrid-types"; import { Dropdown, SingleSelectionHandler } from "@finos/vuu-ui-controls"; -import { CellRendererDescriptor } from "@finos/vuu-utils"; +import { + CellRendererDescriptor, + ConfigurationEditorProps, + getCellRendererOptions, + getConfigurationEditor, + isColumnTypeRenderer, + isTypeDescriptor, +} from "@finos/vuu-utils"; import { FormField, FormFieldLabel } from "@salt-ds/core"; import cx from "classnames"; -import { HTMLAttributes, useMemo } from "react"; +import { HTMLAttributes, useCallback, useMemo } from "react"; import { NumericFormattingSettings } from "./NumericFormattingSettings"; -// import "./ColumnTypePanel.css"; - const classBase = "vuuColumnFormattingPanel"; export interface ColumnFormattingPanelProps extends HTMLAttributes { availableRenderers: CellRendererDescriptor[]; - selectedCellRenderer?: CellRendererDescriptor; column: ColumnDescriptor; - onChangeFormatting: (formatting: TypeFormatting) => void; - onChangeRenderer: SingleSelectionHandler; + onChangeFormatting: (formatting: ColumnTypeFormatting) => void; + onChangeRendering: (renderProps: ColumnTypeRendering) => void; } const itemToString = (item: CellRendererDescriptor) => item.label ?? item.name; export const ColumnFormattingPanel = ({ availableRenderers, - selectedCellRenderer, className, column, onChangeFormatting, - onChangeRenderer, + onChangeRendering, ...htmlAttributes }: ColumnFormattingPanelProps) => { - const content = useMemo(() => { + const contentForType = useMemo(() => { switch (column.serverDataType) { case "double": case "int": @@ -46,6 +54,42 @@ export const ColumnFormattingPanel = ({ } }, [column, onChangeFormatting]); + const ConfigEditor = useMemo< + React.FC | undefined + >(() => { + const { type } = column; + if (isTypeDescriptor(type) && isColumnTypeRenderer(type.renderer)) { + const cellRendererOptions = getCellRendererOptions(type.renderer.name); + return getConfigurationEditor(cellRendererOptions?.configEditor); + } + return undefined; + }, [column]); + + const selectedCellRenderer = useMemo(() => { + const { type } = column; + const [defaultRenderer] = availableRenderers; + const rendererName = + isTypeDescriptor(type) && isColumnTypeRenderer(type.renderer) + ? type.renderer.name + : undefined; + const configuredRenderer = availableRenderers.find( + (renderer) => renderer.name === rendererName + ); + return configuredRenderer ?? defaultRenderer; + }, [availableRenderers, column]); + + const handleChangeRenderer = useCallback< + SingleSelectionHandler + >( + (evt, cellRendererDescriptor) => { + const renderProps: ColumnTypeRendering = { + name: cellRendererDescriptor.name, + }; + onChangeRendering?.(renderProps); + }, + [onChangeRendering] + ); + const { serverDataType = "string" } = column; return ( @@ -59,7 +103,7 @@ export const ColumnFormattingPanel = ({ className={cx(`${classBase}-renderer`)} itemToString={itemToString} - onSelectionChange={onChangeRenderer} + onSelectionChange={handleChangeRenderer} selected={selectedCellRenderer} source={availableRenderers} width="100%" @@ -68,7 +112,13 @@ export const ColumnFormattingPanel = ({
- {content} + {contentForType} + {ConfigEditor ? ( + + ) : null}
); diff --git a/vuu-ui/packages/vuu-table-extras/src/column-formatting-settings/NumericFormattingSettings.tsx b/vuu-ui/packages/vuu-table-extras/src/column-formatting-settings/NumericFormattingSettings.tsx index 5936c7140..12286c388 100644 --- a/vuu-ui/packages/vuu-table-extras/src/column-formatting-settings/NumericFormattingSettings.tsx +++ b/vuu-ui/packages/vuu-table-extras/src/column-formatting-settings/NumericFormattingSettings.tsx @@ -1,7 +1,10 @@ import { FormField, FormFieldLabel, Input } from "@salt-ds/core"; import { Switch } from "@salt-ds/lab"; -import { ColumnDescriptor, TypeFormatting } from "@finos/vuu-datagrid-types"; -import { getTypeSettingsFromColumn } from "@finos/vuu-utils"; +import { + ColumnDescriptor, + ColumnTypeFormatting, +} from "@finos/vuu-datagrid-types"; +import { getTypeFormattingFromColumn } from "@finos/vuu-utils"; import { ChangeEvent, KeyboardEvent, @@ -14,16 +17,15 @@ const classBase = "vuuFormattingSettings"; export interface NumericFormattingSettingsProps { column: ColumnDescriptor; - onChange: (formatting: TypeFormatting) => void; + onChange: (formatting: ColumnTypeFormatting) => void; } export const NumericFormattingSettings = ({ column, onChange, }: NumericFormattingSettingsProps) => { - const [formattingSettings, setFormattingSettings] = useState( - getTypeSettingsFromColumn(column) - ); + const [formattingSettings, setFormattingSettings] = + useState(getTypeFormattingFromColumn(column)); const handleInputKeyDown = useCallback( (evt: KeyboardEvent) => { diff --git a/vuu-ui/packages/vuu-table-extras/src/column-settings/ColumnSettingsPanel.tsx b/vuu-ui/packages/vuu-table-extras/src/column-settings/ColumnSettingsPanel.tsx index ec43db60c..a23638de1 100644 --- a/vuu-ui/packages/vuu-table-extras/src/column-settings/ColumnSettingsPanel.tsx +++ b/vuu-ui/packages/vuu-table-extras/src/column-settings/ColumnSettingsPanel.tsx @@ -54,7 +54,6 @@ export const ColumnSettingsPanel = ({ const { availableRenderers, editCalculatedColumn, - selectedCellRenderer, column, navigateNextColumn, navigatePrevColumn, @@ -62,7 +61,7 @@ export const ColumnSettingsPanel = ({ onChange, onChangeCalculatedColumnName, onChangeFormatting, - onChangeRenderer, + onChangeRendering, onEditCalculatedColumn, onInputCommit, onSave, @@ -170,10 +169,9 @@ export const ColumnSettingsPanel = ({ {editCalculatedColumn ? ( diff --git a/vuu-ui/packages/vuu-table-extras/src/column-settings/useColumnSettings.ts b/vuu-ui/packages/vuu-table-extras/src/column-settings/useColumnSettings.ts index 7574d6c5a..14eb667fa 100644 --- a/vuu-ui/packages/vuu-table-extras/src/column-settings/useColumnSettings.ts +++ b/vuu-ui/packages/vuu-table-extras/src/column-settings/useColumnSettings.ts @@ -1,21 +1,19 @@ import { ColumnDescriptor, TableConfig, - TypeFormatting, + ColumnTypeFormatting, } from "@finos/vuu-datagrid-types"; import { CellRendererDescriptor, + ColumnRenderPropsChangeHandler, getRegisteredCellRenderers, - isColumnTypeRenderer, - isTypeDescriptor, isValidColumnAlignment, isValidPinLocation, setCalculatedColumnName, - updateColumnRenderer, + updateColumnRenderProps, updateColumnType, } from "@finos/vuu-utils"; -import { SingleSelectionHandler } from "@finos/vuu-ui-controls"; import { FormEventHandler, useCallback, @@ -25,7 +23,6 @@ import { useState, } from "react"; import { ColumnSettingsProps } from "./ColumnSettingsPanel"; -import { ColumnExpressionSubmitHandler } from "../column-expression-input"; const integerCellRenderers: CellRendererDescriptor[] = [ { @@ -33,6 +30,7 @@ const integerCellRenderers: CellRendererDescriptor[] = [ label: "Default Renderer (int, long)", name: "default-int", }, + ...getRegisteredCellRenderers("int"), ]; const doubleCellRenderers: CellRendererDescriptor[] = [ { @@ -49,6 +47,7 @@ const stringCellRenderers: CellRendererDescriptor[] = [ label: "Default Renderer (string)", name: "default-string", }, + ...getRegisteredCellRenderers("string"), ]; const getAvailableCellRenderers = ( @@ -68,26 +67,6 @@ const getAvailableCellRenderers = ( } }; -const getCellRendererDescriptor = ( - availableRenderers: CellRendererDescriptor[], - column: ColumnDescriptor -) => { - if (isTypeDescriptor(column.type)) { - const { renderer } = column.type; - if (isColumnTypeRenderer(renderer)) { - const cellRenderer = availableRenderers.find( - (r) => r.name === renderer.name - ); - if (cellRenderer) { - return cellRenderer; - } - } - } - // returm the appropriate default value for the column - const typedAvailableRenderers = getAvailableCellRenderers(column); - return typedAvailableRenderers[0]; -}; - const getFieldName = (input: HTMLInputElement): string => { const saltFormField = input.closest(".saltFormField") as HTMLElement; if (saltFormField && saltFormField.dataset.field) { @@ -150,10 +129,6 @@ export const useColumnSettings = ({ return getAvailableCellRenderers(column); }, [column]); - const selectedCellRendererRef = useRef( - getCellRendererDescriptor(availableRenderers, column) - ); - const handleInputCommit = useCallback(() => { onConfigChange(replaceColumn(tableConfig, column)); }, [column, onConfigChange, tableConfig]); @@ -203,16 +178,22 @@ export const useColumnSettings = ({ setColumn((state) => ({ ...state, name })); }, []); - const handleChangeRenderer = useCallback< - SingleSelectionHandler - >( - (evt, cellRenderer) => { - if (cellRenderer) { - const newColumn: ColumnDescriptor = updateColumnRenderer( + const handleChangeFormatting = useCallback( + (formatting: ColumnTypeFormatting) => { + const newColumn: ColumnDescriptor = updateColumnType(column, formatting); + setColumn(newColumn); + onConfigChange(replaceColumn(tableConfig, newColumn)); + }, + [column, onConfigChange, tableConfig] + ); + + const handleChangeRendering = useCallback( + (renderProps) => { + if (renderProps) { + const newColumn: ColumnDescriptor = updateColumnRenderProps( column, - cellRenderer + renderProps ); - selectedCellRendererRef.current = cellRenderer; setColumn(newColumn); onConfigChange(replaceColumn(tableConfig, newColumn)); } @@ -220,29 +201,16 @@ export const useColumnSettings = ({ [column, onConfigChange, tableConfig] ); - const handleChangeFormatting = useCallback( - (formatting: TypeFormatting) => { - const newColumn: ColumnDescriptor = updateColumnType(column, formatting); - setColumn(newColumn); - onConfigChange(replaceColumn(tableConfig, newColumn)); - }, - [column, onConfigChange, tableConfig] - ); - const navigateColumn = useCallback( ({ moveBy }: { moveBy: number }) => { const { columns } = tableConfig; const index = columns.indexOf(column) + moveBy; const newColumn = columns[index]; if (newColumn) { - selectedCellRendererRef.current = getCellRendererDescriptor( - availableRenderers, - newColumn - ); setColumn(newColumn); } }, - [availableRenderers, column, tableConfig] + [column, tableConfig] ); const navigateNextColumn = useCallback(() => { navigateColumn({ moveBy: 1 }); @@ -271,7 +239,6 @@ export const useColumnSettings = ({ return { availableRenderers, editCalculatedColumn: inEditMode, - selectedCellRenderer: selectedCellRendererRef.current, column, navigateNextColumn, navigatePrevColumn, @@ -279,7 +246,7 @@ export const useColumnSettings = ({ onChange: handleChange, onChangeCalculatedColumnName: handleChangeCalculatedColumnName, onChangeFormatting: handleChangeFormatting, - onChangeRenderer: handleChangeRenderer, + onChangeRendering: handleChangeRendering, onEditCalculatedColumn: handleEditCalculatedcolumn, onInputCommit: handleInputCommit, onSave: handleSaveCalculatedColumn, 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 41e919df7..7c4a4b06f 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 @@ -27,8 +27,8 @@ export const DataSourceStats = ({ }, [dataSource]); const className = cx(classBase, classNameProp); - const from = numberFormatter.format(range.from); - const to = numberFormatter.format(range.to - 1); + const from = numberFormatter.format(range.from + 1); + const to = numberFormatter.format(Math.min(range.to - 1, 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 f585ad55c..3f2726f22 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/TableNext.tsx +++ b/vuu-ui/packages/vuu-table/src/table-next/TableNext.tsx @@ -29,7 +29,6 @@ export const TableNext = forwardRef(function TableNext( navigationStyle = "cell", onAvailableColumnsChange, onConfigChange, - onFeatureEnabled, onFeatureInvocation, onRowClick: onRowClickProp, onSelect, @@ -73,7 +72,6 @@ export const TableNext = forwardRef(function TableNext( navigationStyle, onAvailableColumnsChange, onConfigChange, - onFeatureEnabled, onFeatureInvocation, onRowClick: onRowClickProp, onSelect, diff --git a/vuu-ui/packages/vuu-table/src/table-next/cell-renderers/cell-utils.ts b/vuu-ui/packages/vuu-table/src/table-next/cell-renderers/cell-utils.ts new file mode 100644 index 000000000..fee46d456 --- /dev/null +++ b/vuu-ui/packages/vuu-table/src/table-next/cell-renderers/cell-utils.ts @@ -0,0 +1,12 @@ +import { TableCellRendererProps } from "packages/vuu-datagrid-types"; +/** + * A memo compare function for cell renderers. Can be used to suppress + * render where column and data are both unchanged. Avoids render + * when row changes, where changes in row are unrelated to this cell. + */ +export const dataAndColumnUnchanged = ( + p: TableCellRendererProps, + p1: TableCellRendererProps +) => + p.column === p1.column && + p.row[p.columnMap[p.column.name]] === p1.row[p1.columnMap[p1.column.name]]; diff --git a/vuu-ui/packages/vuu-table/src/table-next/cell-renderers/dropdown-cell/DropdownCell.tsx b/vuu-ui/packages/vuu-table/src/table-next/cell-renderers/dropdown-cell/DropdownCell.tsx index 88cf3efb7..52c3b975f 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/cell-renderers/dropdown-cell/DropdownCell.tsx +++ b/vuu-ui/packages/vuu-table/src/table-next/cell-renderers/dropdown-cell/DropdownCell.tsx @@ -1,52 +1,60 @@ +import { useLookupValues } from "@finos/vuu-data-react"; +import { ListOption, TableCellRendererProps } from "@finos/vuu-datagrid-types"; import { + dispatchCommitEvent, Dropdown, DropdownOpenKey, SingleSelectionHandler, + WarnCommit, } from "@finos/vuu-ui-controls"; -import { - isColumnTypeRenderer, - isTypeDescriptor, - registerComponent, -} from "@finos/vuu-utils"; -import { TableCellProps } from "@finos/vuu-datagrid-types"; -// import { dispatchCommitEvent } from "@finos/vuu-ui-controls"; +import { registerComponent } from "@finos/vuu-utils"; +import { VuuColumnDataType } from "@finos/vuu-protocol-types"; +import { memo, useCallback, useState } from "react"; +import { dataAndColumnUnchanged } from "../cell-utils"; import "./DropdownCell.css"; -import { useCallback, useState } from "react"; const classBase = "vuuTableDropdownCell"; const openKeys: DropdownOpenKey[] = ["Enter", " "]; -export const DropdownCell = ({ column, columnMap, row }: TableCellProps) => { - const values = - isTypeDescriptor(column.type) && isColumnTypeRenderer(column.type?.renderer) - ? column.type?.renderer?.values - : []; - +export const DropdownCell = memo(function DropdownCell({ + column, + columnMap, + onCommit = WarnCommit, + row, +}: TableCellRendererProps) { const dataIdx = columnMap[column.name]; - const [value, setValue] = useState(row[dataIdx]); - const handleSelectionChange = useCallback( - (evt, selectedItem) => { - if (selectedItem) { - setValue(selectedItem); - // dispatchCommitEvent(evt.target as HTMLElement); + const { initialValue, values } = useLookupValues(column, row[dataIdx]); + + const [value, setValue] = useState(null); + + const handleSelectionChange = useCallback>( + (evt, selectedOption) => { + if (selectedOption) { + setValue(selectedOption); + onCommit(selectedOption.value as VuuColumnDataType).then((response) => { + if (response === true && evt) { + dispatchCommitEvent(evt.target as HTMLElement); + } + }); } }, - [] + [onCommit] ); return ( - className={classBase} onSelectionChange={handleSelectionChange} openKeys={openKeys} - selected={value} + selected={value ?? initialValue} source={values} width={column.width - 17} // temp hack /> ); -}; +}, +dataAndColumnUnchanged); registerComponent("dropdown-cell", DropdownCell, "cell-renderer", {}); diff --git a/vuu-ui/packages/vuu-table/src/table-next/cell-renderers/index.ts b/vuu-ui/packages/vuu-table/src/table-next/cell-renderers/index.ts index b602eec50..593451edf 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/cell-renderers/index.ts +++ b/vuu-ui/packages/vuu-table/src/table-next/cell-renderers/index.ts @@ -1,2 +1,3 @@ export * from "./dropdown-cell"; export * from "./input-cell"; +export * from "./toggle-cell"; diff --git a/vuu-ui/packages/vuu-table/src/table-next/cell-renderers/input-cell/InputCell.tsx b/vuu-ui/packages/vuu-table/src/table-next/cell-renderers/input-cell/InputCell.tsx index cd0b8a890..b1add91f7 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/cell-renderers/input-cell/InputCell.tsx +++ b/vuu-ui/packages/vuu-table/src/table-next/cell-renderers/input-cell/InputCell.tsx @@ -11,11 +11,11 @@ import "./InputCell.css"; const classBase = "vuuTableInputCell"; -const WarnCommit = () => { +const WarnCommit = (): Promise => { console.warn( "onCommit handler has not been provided to InputCell cell renderer" ); - return true; + return Promise.resolve(true); }; export const InputCell = ({ column, diff --git a/vuu-ui/packages/vuu-table/src/table-next/cell-renderers/toggle-cell/ToggleCell.css b/vuu-ui/packages/vuu-table/src/table-next/cell-renderers/toggle-cell/ToggleCell.css new file mode 100644 index 000000000..cffcda938 --- /dev/null +++ b/vuu-ui/packages/vuu-table/src/table-next/cell-renderers/toggle-cell/ToggleCell.css @@ -0,0 +1,32 @@ + +.vuuTableToggleCell { + --saltButton-borderRadius: 4px; + --saltButton-height: 16px; + font-weight: 500; + position: relative; + top: 1px;; +} +.vuuTableToggleCell-side { + --saltButton-minWidth: 40px; +} +.vuuTableToggleCell.vuuCycleStateButton-buy { + background-color: var(--vuu-color-green-50); +} + +.vuuTableToggleCell.vuuCycleStateButton-sell { + background-color: var(--vuu-color-red-50); + +} + +.vuuTableNextCell .vuuTableToggleCell:focus { + /* TODO fix use of important */ + border: solid 2px var(--vuu-color-purple-10) !important; + color: white !important; + height: 18px !important; + top: 0px !important; +} + + + + + diff --git a/vuu-ui/packages/vuu-table/src/table-next/cell-renderers/toggle-cell/ToggleCell.tsx b/vuu-ui/packages/vuu-table/src/table-next/cell-renderers/toggle-cell/ToggleCell.tsx new file mode 100644 index 000000000..4c5bbe9d1 --- /dev/null +++ b/vuu-ui/packages/vuu-table/src/table-next/cell-renderers/toggle-cell/ToggleCell.tsx @@ -0,0 +1,71 @@ +import { + ColumnDescriptor, + TableCellRendererProps, +} from "@finos/vuu-datagrid-types"; +import { + CycleStateButtonProps, + dispatchCommitEvent, + WarnCommit, +} from "@finos/vuu-ui-controls"; +import { + isTypeDescriptor, + isValueListRenderer, + registerComponent, +} from "@finos/vuu-utils"; +import cx from "classnames"; + +import { memo, useCallback } from "react"; +import { dataAndColumnUnchanged } from "../cell-utils"; +import { CycleStateButton } from "@finos/vuu-ui-controls"; + +import "./ToggleCell.css"; + +const classBase = "vuuTableToggleCell"; + +const getValueList = ({ name, type }: ColumnDescriptor) => { + if (isTypeDescriptor(type) && isValueListRenderer(type.renderer)) { + return type.renderer.values; + } else { + throw Error( + `useLookupValues column ${name} has not been configured with a values list` + ); + } +}; + +export const ToggleCell = memo(function ToggleCell({ + column, + columnMap, + onCommit = WarnCommit, + row, +}: TableCellRendererProps) { + const values = getValueList(column); + const dataIdx = columnMap[column.name]; + const value = row[dataIdx]; + + const handleCommit = useCallback( + (evt, value) => { + return onCommit(value).then((response) => { + if (response === true) { + dispatchCommitEvent(evt.target as HTMLElement); + } + return response; + }); + }, + [onCommit] + ); + + return ( + + {value} + + ); +}, +dataAndColumnUnchanged); + +registerComponent("toggle-cell", ToggleCell, "cell-renderer", {}); diff --git a/vuu-ui/packages/vuu-table/src/table-next/cell-renderers/toggle-cell/index.ts b/vuu-ui/packages/vuu-table/src/table-next/cell-renderers/toggle-cell/index.ts new file mode 100644 index 000000000..3d7185458 --- /dev/null +++ b/vuu-ui/packages/vuu-table/src/table-next/cell-renderers/toggle-cell/index.ts @@ -0,0 +1 @@ +export * from "./ToggleCell"; diff --git a/vuu-ui/packages/vuu-table/src/table-next/context-menu/index.ts b/vuu-ui/packages/vuu-table/src/table-next/context-menu/index.ts index 8c6db9865..9c7d25131 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/context-menu/index.ts +++ b/vuu-ui/packages/vuu-table/src/table-next/context-menu/index.ts @@ -1,2 +1,2 @@ export * from "./buildContextMenuDescriptors"; -export * from "./useTableContextMenu"; +export * from "./useHandleTableContextMenu"; diff --git a/vuu-ui/packages/vuu-table/src/table-next/context-menu/useTableContextMenu.ts b/vuu-ui/packages/vuu-table/src/table-next/context-menu/useHandleTableContextMenu.ts similarity index 99% rename from vuu-ui/packages/vuu-table/src/table-next/context-menu/useTableContextMenu.ts rename to vuu-ui/packages/vuu-table/src/table-next/context-menu/useHandleTableContextMenu.ts index fc3813a85..70b84b051 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/context-menu/useTableContextMenu.ts +++ b/vuu-ui/packages/vuu-table/src/table-next/context-menu/useHandleTableContextMenu.ts @@ -49,7 +49,7 @@ const removeFilterColumn = ( const { Average, Count, Distinct, High, Low, Sum } = AggregationType; -export const useTableContextMenu = ({ +export const useHandleTableContextMenu = ({ dataSource, onPersistentColumnOperation, }: ContextMenuHookProps) => { diff --git a/vuu-ui/packages/vuu-table/src/table-next/header-cell/HeaderCell.css b/vuu-ui/packages/vuu-table/src/table-next/header-cell/HeaderCell.css index 8522a0901..e9a9a58cb 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/header-cell/HeaderCell.css +++ b/vuu-ui/packages/vuu-table/src/table-next/header-cell/HeaderCell.css @@ -34,6 +34,7 @@ line-height: calc(var(--header-height) - 1px); overflow: hidden; text-overflow: ellipsis; + white-space: nowrap; } .vuuTableNextHeaderCell-right .vuuTableNextHeaderCell-label { diff --git a/vuu-ui/packages/vuu-table/src/table-next/table-cell/TableCell.tsx b/vuu-ui/packages/vuu-table/src/table-next/table-cell/TableCell.tsx index 91dbea8b6..c6c3ecf61 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/table-cell/TableCell.tsx +++ b/vuu-ui/packages/vuu-table/src/table-next/table-cell/TableCell.tsx @@ -1,12 +1,17 @@ -import { TableCellProps } from "@finos/vuu-datagrid-types"; -import { metadataKeys } from "@finos/vuu-utils"; -import { VuuColumnDataType } from "@finos/vuu-protocol-types"; +import { + DataItemCommitHandler, + TableCellProps, +} from "@finos/vuu-datagrid-types"; +import { + VuuColumnDataType, + VuuRowDataItemType, +} from "@finos/vuu-protocol-types"; +import { isNumericColumn } from "@finos/vuu-utils"; import { MouseEventHandler, useCallback } from "react"; import { useCell } from "../useCell"; import "./TableCell.css"; -const { IDX } = metadataKeys; const classBase = "vuuTableNextCell"; export const TableCell = ({ @@ -20,13 +25,24 @@ export const TableCell = ({ const { CellRenderer, name, valueFormatter } = column; const dataIdx = columnMap[name]; - const handleDataItemEdited = useCallback( - (value: VuuColumnDataType) => { - onDataEdited?.(row, name, value); - // TODO will only return false in case of server rejection - return true; + const handleDataItemEdited = useCallback( + (value) => { + if (onDataEdited) { + let typedValue = value; + if (isNumericColumn(column) && typeof value === "string") { + typedValue = + column.serverDataType === "double" + ? parseFloat(value) + : parseInt(value); + } + return onDataEdited?.(row, name, typedValue); + } else { + throw Error( + "TableCell onDataEdited prop not supplied for an editable cell" + ); + } }, - [name, onDataEdited, row] + [column, name, onDataEdited, row] ); const handleClick = useCallback( diff --git a/vuu-ui/packages/vuu-table/src/table-next/table-dom-utils.ts b/vuu-ui/packages/vuu-table/src/table-next/table-dom-utils.ts index 8976179b8..b0006e1a3 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/table-dom-utils.ts +++ b/vuu-ui/packages/vuu-table/src/table-next/table-dom-utils.ts @@ -33,5 +33,5 @@ export const getTableCell = ( export const cellIsEditable = (cell: HTMLDivElement) => cell.classList.contains("vuuTableNextCell-editable"); -export const cellIsTextInput = (cell: HTMLDivElement) => +export const cellIsTextInput = (cell: HTMLElement) => cell.querySelector(".vuuTableInputCell") !== null; diff --git a/vuu-ui/packages/vuu-table/src/table-next/useCellEditing.ts b/vuu-ui/packages/vuu-table/src/table-next/useCellEditing.ts index f50a7d320..bb24595a3 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/useCellEditing.ts +++ b/vuu-ui/packages/vuu-table/src/table-next/useCellEditing.ts @@ -1,5 +1,9 @@ import { isCharacterKey } from "@finos/vuu-utils"; -import { KeyboardEvent as ReactKeyboardEvent, useCallback } from "react"; +import { + FocusEventHandler, + KeyboardEvent as ReactKeyboardEvent, + useCallback, +} from "react"; import { cellIsTextInput } from "./table-dom-utils"; export interface CellEditingHookProps { @@ -11,39 +15,28 @@ export const useCellEditing = ({ navigate }: CellEditingHookProps) => { navigate(); }, [navigate]); - const editInput = useCallback( - (evt: ReactKeyboardEvent) => { - const cellEl = evt.target as HTMLDivElement; - const input = cellEl.querySelector("input"); - if (input) { - input.focus(); - input.select(); - } - // TODO dergister on blur - // TODO need a custom event - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore commit is a custom event fired by vuu inputs - cellEl.addEventListener("vuu-commit", commitHandler, true); - }, - [commitHandler] - ); + const editInput = useCallback((evt: ReactKeyboardEvent) => { + const cellEl = evt.target as HTMLDivElement; + const input = cellEl.querySelector("input"); + if (input) { + input.focus(); + input.select(); + } + }, []); - const focusInput = useCallback( - (evt: ReactKeyboardEvent) => { - const cellEl = evt.target as HTMLDivElement; - const input = cellEl.querySelector("input"); - if (input) { - input.focus(); - input.select(); - } - cellEl.addEventListener("vuu-commit", commitHandler, true); - }, - [commitHandler] - ); + const focusInput = useCallback((evt: ReactKeyboardEvent) => { + const cellEl = evt.target as HTMLDivElement; + const input = cellEl.querySelector("input"); + if (input) { + input.focus(); + input.select(); + } + }, []); const handleKeyDown = useCallback( (e: ReactKeyboardEvent) => { - if (cellIsTextInput(e.target as HTMLDivElement)) { + const el = e.target as HTMLElement; + if (cellIsTextInput(el)) { if (isCharacterKey(e.key)) { editInput(e); } else if (e.key === "Enter") { @@ -54,7 +47,25 @@ export const useCellEditing = ({ navigate }: CellEditingHookProps) => { [editInput, focusInput] ); + const handleBlur = useCallback( + (e) => { + const el = e.target as HTMLElement; + el.removeEventListener("vuu-commit", commitHandler, true); + }, + [commitHandler] + ); + + const handleFocus = useCallback( + (e) => { + const el = e.target as HTMLElement; + el.addEventListener("vuu-commit", commitHandler, true); + }, + [commitHandler] + ); + return { + onBlur: handleBlur, + onFocus: handleFocus, onKeyDown: handleKeyDown, }; }; diff --git a/vuu-ui/packages/vuu-table/src/table-next/useDataSource.ts b/vuu-ui/packages/vuu-table/src/table-next/useDataSource.ts index cb901d82a..7a02d8af8 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/useDataSource.ts +++ b/vuu-ui/packages/vuu-table/src/table-next/useDataSource.ts @@ -1,14 +1,10 @@ import { DataSource, DataSourceSubscribedMessage, + isVuuFeatureInvocation, SubscribeCallback, VuuFeatureInvocationMessage, - VuuFeatureMessage, } from "@finos/vuu-data"; -import { - isVuuFeatureAction, - isVuuFeatureInvocation, -} from "@finos/vuu-data-react/src"; import { DataSourceRow } from "@finos/vuu-data-types"; import { VuuRange } from "@finos/vuu-protocol-types"; import { getFullRange, NULL_RANGE } from "@finos/vuu-utils"; @@ -18,7 +14,6 @@ import { MovingWindow } from "./moving-window"; export interface DataSourceHookProps { dataSource: DataSource; // onConfigChange?: (message: DataSourceConfigMessage) => void; - onFeatureEnabled?: (message: VuuFeatureMessage) => void; onFeatureInvocation?: (message: VuuFeatureInvocationMessage) => void; onSizeChange: (size: number) => void; onSubscribed: (subscription: DataSourceSubscribedMessage) => void; @@ -28,7 +23,6 @@ export interface DataSourceHookProps { export const useDataSource = ({ dataSource, - onFeatureEnabled, onFeatureInvocation, onSizeChange, onSubscribed, @@ -77,22 +71,13 @@ export const useDataSource = ({ data.current = dataWindow.data; hasUpdated.current = true; } - } else if (isVuuFeatureAction(message)) { - onFeatureEnabled?.(message); } else if (isVuuFeatureInvocation(message)) { onFeatureInvocation?.(message); } else { console.log(`useDataSource unexpected message ${message.type}`); } }, - [ - dataWindow, - onFeatureEnabled, - onFeatureInvocation, - onSizeChange, - onSubscribed, - setData, - ] + [dataWindow, onFeatureInvocation, onSizeChange, onSubscribed, setData] ); const getSelectedRows = useCallback(() => { @@ -127,11 +112,15 @@ export const useDataSource = ({ // }, [refreshIfUpdated]); useEffect(() => { - //TODO could we improve this by using a ref for range ? - dataSource?.subscribe( - { range: getFullRange(range, renderBufferSize) }, - datasourceMessageHandler - ); + if (dataSource.status === "disabled") { + dataSource.enable?.(datasourceMessageHandler); + } else { + //TODO could we improve this by using a ref for range ? + dataSource?.subscribe( + { range: getFullRange(range, renderBufferSize) }, + datasourceMessageHandler + ); + } }, [dataSource, datasourceMessageHandler, range, renderBufferSize]); const setRange = useCallback( diff --git a/vuu-ui/packages/vuu-table/src/table-next/useKeyboardNavigation.ts b/vuu-ui/packages/vuu-table/src/table-next/useKeyboardNavigation.ts index e1b4ddf24..426c3c73a 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/useKeyboardNavigation.ts +++ b/vuu-ui/packages/vuu-table/src/table-next/useKeyboardNavigation.ts @@ -60,19 +60,17 @@ const howFarIsCellOutsideViewport = ( cellEl: HTMLElement ): readonly [ScrollDirection | undefined, number | undefined] => { //TODO lots of scope for optimisation here - const scrollbarContainer = cellEl - .closest(".vuuTableNext") - ?.querySelector(".vuuTableNext-scrollbarContainer"); - if (scrollbarContainer) { - const viewport = scrollbarContainer?.getBoundingClientRect(); + const contentContainer = cellEl.closest(".vuuTableNext-contentContainer"); + if (contentContainer) { + const viewport = contentContainer?.getBoundingClientRect(); const cell = cellEl.closest(".vuuTableNextCell")?.getBoundingClientRect(); if (cell) { if (cell.bottom > viewport.bottom) { return ["down", cell.bottom - viewport.bottom]; } else if (cell.top < viewport.top) { return ["up", cell.top - viewport.top]; - } else if (cell.right < viewport.right) { - return ["right", cell.right - viewport.right]; + } else if (cell.right > viewport.right) { + return ["right", cell.right + 6 - viewport.right]; } else if (cell.left < viewport.left) { return ["left", cell.left - viewport.left]; } else { @@ -107,7 +105,8 @@ function nextCellPos( return [rowIdx + 1, colIdx]; } } else if (key === "ArrowRight") { - if (colIdx < columnCount - 1) { + // The colIdx is 1 based, because of the selection decorator + if (colIdx < columnCount) { return [rowIdx, colIdx + 1]; } else { return [rowIdx, colIdx]; @@ -186,7 +185,7 @@ NavigationHookProps) => { if (direction && distance) { requestScroll?.({ type: "scroll-distance", distance, direction }); } - activeCell.focus(); + activeCell.focus({ preventScroll: true }); } } }, @@ -259,7 +258,6 @@ NavigationHookProps) => { const [nextRowIdx, nextColIdx] = isPagingKey(key) ? await nextPageItemIdx(key, activeCellPos.current) : nextCellPos(key, activeCellPos.current, columnCount, rowCount); - console.log(`nextRowIdx ${nextRowIdx} nextColIdx ${nextColIdx}`); const [rowIdx, colIdx] = activeCellPos.current; if (nextRowIdx !== rowIdx || nextColIdx !== colIdx) { diff --git a/vuu-ui/packages/vuu-table/src/table-next/useTableContextMenu.ts b/vuu-ui/packages/vuu-table/src/table-next/useTableContextMenu.ts index 339539b61..945e98938 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/useTableContextMenu.ts +++ b/vuu-ui/packages/vuu-table/src/table-next/useTableContextMenu.ts @@ -45,7 +45,7 @@ export const useTableContextMenu = ({ }); } }, - [columns, data, dataSource, showContextMenu] + [columns, data, dataSource, getSelectedRows, showContextMenu] ); return onContextMenu; diff --git a/vuu-ui/packages/vuu-table/src/table-next/useTableModel.ts b/vuu-ui/packages/vuu-table/src/table-next/useTableModel.ts index 4b963d86b..d44b9dc4f 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/useTableModel.ts +++ b/vuu-ui/packages/vuu-table/src/table-next/useTableModel.ts @@ -18,10 +18,9 @@ import { isFilteredColumn, isGroupColumn, isPinned, - isTypeDescriptor, logger, metadataKeys, - moveItem, + replaceColumn, sortPinnedColumns, stripFilterFromColumns, subscribedOnly, @@ -41,12 +40,6 @@ const KEY_OFFSET = metadataKeys.count; const columnWithoutDataType = ({ serverDataType }: ColumnDescriptor) => serverDataType === undefined; -const getCellRendererForColumn = (column: ColumnDescriptor) => { - if (isTypeDescriptor(column.type)) { - return getCellRenderer(column.type?.renderer); - } -}; - const getDataType = ( column: ColumnDescriptor, tableSchema: TableSchema @@ -107,7 +100,6 @@ export interface ColumnActionMove { type: "moveColumn"; column: KeyedColumnDescriptor; moveBy?: 1 | -1; - moveTo?: number; } export interface ColumnActionPin { @@ -307,7 +299,7 @@ const columnDescriptorToKeyedColumDescriptor = const keyedColumnWithDefaults = { ...rest, align, - CellRenderer: getCellRendererForColumn(column), + CellRenderer: getCellRenderer(column), clientSideEditValidationCheck: hasValidationRules(column.type) ? buildValidationChecker(column.type.renderer.rules) : undefined, @@ -331,7 +323,8 @@ const columnDescriptorToKeyedColumDescriptor = function moveColumn( state: InternalTableModel, - { column, moveBy, moveTo }: ColumnActionMove + // TODO do we ever use this ? + { column, moveBy }: ColumnActionMove ) { const { columns } = state; if (typeof moveBy === "number") { @@ -343,12 +336,6 @@ function moveColumn( ...state, columns: newColumns, }; - } else if (typeof moveTo === "number") { - const index = columns.indexOf(column); - return { - ...state, - columns: moveItem(columns, index, moveTo), - }; } return state; } @@ -513,10 +500,3 @@ function updateTableConfig( return result; } - -function replaceColumn( - state: KeyedColumnDescriptor[], - column: KeyedColumnDescriptor -) { - return state.map((col) => (col.name === column.name ? column : col)); -} 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 aa8266cc1..4c37b7d4a 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/useTableNext.ts +++ b/vuu-ui/packages/vuu-table/src/table-next/useTableNext.ts @@ -24,10 +24,12 @@ import { isJsonGroup, isValidNumber, metadataKeys, + moveColumnTo, updateColumn, visibleColumnAtIndex, } from "@finos/vuu-utils"; import { + FocusEvent, KeyboardEvent, MouseEvent, RefObject, @@ -38,17 +40,19 @@ import { } from "react"; import { buildContextMenuDescriptors, + ColumnActionHide, + ColumnActionPin, MeasuredProps, RowClickHandler, TableProps, useSelection, - useTableContextMenu, } from "../table"; import { TableColumnResizeHandler } from "./column-resizing"; import { updateTableConfig } from "./table-config"; import { useDataSource } from "./useDataSource"; import { useInitialValue } from "./useInitialValue"; -import { useTableContextMenu as useTableContextMenuNext } from "./useTableContextMenu"; +import { useTableContextMenu } from "./useTableContextMenu"; +import { useHandleTableContextMenu } from "./context-menu"; import { useCellEditing } from "./useCellEditing"; import { isShowColumnSettings, @@ -70,7 +74,6 @@ export interface TableHookProps | "navigationStyle" | "onAvailableColumnsChange" | "onConfigChange" - | "onFeatureEnabled" | "onFeatureInvocation" | "onSelect" | "onSelectionChange" @@ -102,7 +105,6 @@ export const useTable = ({ navigationStyle = "cell", onAvailableColumnsChange, onConfigChange, - onFeatureEnabled, onFeatureInvocation, onRowClick: onRowClickProp, onSelect, @@ -146,6 +148,21 @@ export const useTable = ({ }); }, [tableConfig, dataSource.config, dispatchColumnAction]); + const applyTableConfigChange = useCallback( + (config: TableConfig) => { + dispatchColumnAction({ + type: "init", + tableConfig: config, + dataSourceConfig: dataSource.config, + }); + onConfigChange?.(config); + }, + [dataSource.config, dispatchColumnAction, onConfigChange] + ); + + /** + * These stateColumns are required only for the duration of a column resize operation + */ const [stateColumns, setStateColumns] = useState(); const [columns, setColumnSize] = useMemo(() => { const setSize = (columnName: string, width: number) => { @@ -182,30 +199,33 @@ export const useTable = ({ const onSubscribed = useCallback( ({ tableSchema }: DataSourceSubscribedMessage) => { if (tableSchema) { - // dispatchColumnAction({ - // type: "setTableSchema", - // tableSchema, - // }); + dispatchColumnAction({ + type: "setTableSchema", + tableSchema, + }); } else { console.log("subscription message with no schema"); } }, - [] + [dispatchColumnAction] ); - const { data, getSelectedRows, onEditTableData, range, setRange } = - useDataSource({ - dataSource, - onFeatureEnabled, - onFeatureInvocation, - renderBufferSize, - onSizeChange: onDataRowcountChange, - onSubscribed, - range: initialRange, - }); + const { data, getSelectedRows, range, setRange } = useDataSource({ + dataSource, + onFeatureInvocation, + renderBufferSize, + onSizeChange: onDataRowcountChange, + onSubscribed, + range: initialRange, + }); const handleConfigChanged = useCallback( (tableConfig: TableConfig) => { + // console.log( + // `useTableNext handleConfigChanged`, + // JSON.stringify(tableConfig, null, 2) + // ); + dispatchColumnAction({ type: "init", tableConfig, @@ -226,23 +246,6 @@ export const useTable = ({ [dataSource] ); - const handleCreateCalculatedColumn = useCallback( - (column: ColumnDescriptor) => { - dataSource.columns = dataSource.columns.concat(column.name); - const newTableConfig = addColumn(tableConfig, column); - dispatchColumnAction({ - type: "init", - tableConfig: newTableConfig, - dataSourceConfig: dataSource.config, - }); - console.log(`dispatch onConfigChange`, { - newTableConfig, - }); - onConfigChange?.(newTableConfig); - }, - [dataSource, dispatchColumnAction, onConfigChange, tableConfig] - ); - useEffect(() => { dataSource.on("config", (config, confirmed) => { dispatchColumnAction({ @@ -253,6 +256,42 @@ export const useTable = ({ }); }, [dataSource, dispatchColumnAction]); + const handleCreateCalculatedColumn = useCallback( + (column: ColumnDescriptor) => { + dataSource.columns = dataSource.columns.concat(column.name); + applyTableConfigChange(addColumn(tableConfig, column)); + }, + [dataSource, tableConfig, applyTableConfigChange] + ); + + const hideColumns = useCallback( + (action: ColumnActionHide) => { + const { columns } = action; + const hiddenColumns = columns.map((c) => c.name); + const newTableConfig = { + ...tableConfig, + columns: tableConfig.columns.map((col) => + hiddenColumns.includes(col.name) ? { ...col, hidden: true } : col + ), + }; + applyTableConfigChange(newTableConfig); + }, + [tableConfig, applyTableConfigChange] + ); + + const pinColumn = useCallback( + (action: ColumnActionPin) => { + applyTableConfigChange({ + ...tableConfig, + columns: updateColumn(tableConfig.columns, { + ...action.column, + pin: action.pin, + }), + }); + }, + [tableConfig, applyTableConfigChange] + ); + const { showColumnSettingsPanel, showTableSettingsPanel } = useTableAndColumnSettings({ availableColumns: @@ -275,14 +314,26 @@ export const useTable = ({ } else if (isShowTableSettings(action)) { showTableSettingsPanel(); } else { - // expectConfigChangeRef.current = true; - dispatchColumnAction(action); + switch (action.type) { + case "hideColumns": + return hideColumns(action); + case "pinColumn": + return pinColumn(action); + default: + dispatchColumnAction(action); + } } }, - [dispatchColumnAction, showColumnSettingsPanel, showTableSettingsPanel] + [ + dispatchColumnAction, + hideColumns, + pinColumn, + showColumnSettingsPanel, + showTableSettingsPanel, + ] ); - const handleContextMenuAction = useTableContextMenu({ + const handleContextMenuAction = useHandleTableContextMenu({ dataSource, onPersistentColumnOperation, }); @@ -321,6 +372,7 @@ export const useTable = ({ column, width, }); + setStateColumns(undefined); onConfigChange?.( updateTableConfig(tableConfig, { type: "col-size", @@ -409,6 +461,7 @@ export const useTable = ({ const { navigate, + onFocus: navigationFocus, onKeyDown: navigationKeyDown, ...containerProps } = useKeyboardNavigation({ @@ -421,7 +474,23 @@ export const useTable = ({ viewportRowCount: viewportMeasurements.rowCount, }); - const { onKeyDown: editingKeyDown } = useCellEditing({ navigate }); + const { + onBlur: editingBlur, + onKeyDown: editingKeyDown, + onFocus: editingFocus, + } = useCellEditing({ + navigate, + }); + + const handleFocus = useCallback( + (e: FocusEvent) => { + navigationFocus(); + if (!e.defaultPrevented) { + editingFocus(e); + } + }, + [editingFocus, navigationFocus] + ); const handleKeyDown = useCallback( (e: KeyboardEvent) => { @@ -433,7 +502,7 @@ export const useTable = ({ [navigationKeyDown, editingKeyDown] ); - const onContextMenu = useTableContextMenuNext({ + const onContextMenu = useTableContextMenu({ columns, data, dataSource, @@ -504,22 +573,26 @@ export const useTable = ({ const handleDrop = useCallback( (moveFrom: number, moveTo: number) => { - // onMoveColumn?.(fromIndex, toIndex); - const column = columns[moveFrom]; + const column = tableConfig.columns[moveFrom]; + + const newTableConfig = { + ...tableConfig, + columns: moveColumnTo(tableConfig.columns, column, moveTo), + }; dispatchColumnAction({ - type: "moveColumn", - column, - moveTo, + type: "init", + tableConfig: newTableConfig, + dataSourceConfig: dataSource.config, }); + onConfigChange?.(newTableConfig); }, - [columns, dispatchColumnAction] + [dataSource.config, dispatchColumnAction, onConfigChange, tableConfig] ); const handleDataEdited = useCallback( - (rowIndex, columnName, value) => { - return dataSource.applyEdit(rowIndex, columnName, value); - }, + async (row, columnName, value) => + dataSource.applyEdit(row, columnName, value), [dataSource] ); @@ -543,6 +616,8 @@ export const useTable = ({ return { ...containerProps, + onBlur: editingBlur, + onFocus: handleFocus, onKeyDown: handleKeyDown, columnMap, columns, diff --git a/vuu-ui/packages/vuu-table/src/table-next/useTableScroll.ts b/vuu-ui/packages/vuu-table/src/table-next/useTableScroll.ts index 00a6a8853..ef47a7469 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/useTableScroll.ts +++ b/vuu-ui/packages/vuu-table/src/table-next/useTableScroll.ts @@ -201,7 +201,7 @@ export const useTableScroll = ({ scrollbarContainer.scrollTo({ top: newScrollTop, left: newScrollLeft, - behavior: "auto", + behavior: "smooth", }); } else if (scrollRequest.type === "scroll-page") { const { direction } = scrollRequest; diff --git a/vuu-ui/packages/vuu-table/src/table/Table.tsx b/vuu-ui/packages/vuu-table/src/table/Table.tsx index 49be16bda..2b996ae13 100644 --- a/vuu-ui/packages/vuu-table/src/table/Table.tsx +++ b/vuu-ui/packages/vuu-table/src/table/Table.tsx @@ -30,7 +30,6 @@ export const Table = ({ height, id: idProp, onConfigChange, - onFeatureEnabled, onFeatureInvocation, onSelect, onSelectionChange, @@ -60,7 +59,6 @@ export const Table = ({ headerHeight, height, onConfigChange, - onFeatureEnabled, onFeatureInvocation, onSelectionChange, rowHeight, diff --git a/vuu-ui/packages/vuu-table/src/table/context-menu/useTableContextMenu.ts b/vuu-ui/packages/vuu-table/src/table/context-menu/useTableContextMenu.ts index db4ec0320..fc3813a85 100644 --- a/vuu-ui/packages/vuu-table/src/table/context-menu/useTableContextMenu.ts +++ b/vuu-ui/packages/vuu-table/src/table/context-menu/useTableContextMenu.ts @@ -21,6 +21,11 @@ export interface ContextMenuOptions { } export interface ContextMenuHookProps { dataSource?: DataSource; + /** + * A persistent Column Operation is any manipulation of a table column that should be + * persisted across user sessions. e.g. if user pins a column, column should still be + * pinned next time user opens app. + */ onPersistentColumnOperation: (action: PersistentColumnAction) => void; } diff --git a/vuu-ui/packages/vuu-table/src/table/dataTableTypes.ts b/vuu-ui/packages/vuu-table/src/table/dataTableTypes.ts index ec8c2be16..4faded45f 100644 --- a/vuu-ui/packages/vuu-table/src/table/dataTableTypes.ts +++ b/vuu-ui/packages/vuu-table/src/table/dataTableTypes.ts @@ -13,7 +13,7 @@ import { TableSelectionModel, } from "@finos/vuu-datagrid-types"; import { VuuDataRow } from "@finos/vuu-protocol-types"; -import { MeasuredContainerProps } from "packages/vuu-layout/src"; +import { MeasuredContainerProps } from "@finos/vuu-layout"; import { FC, MouseEvent } from "react"; import { RowProps } from "../table-next/Row"; @@ -50,12 +50,6 @@ export interface TableProps extends Omit { * prop, table state can be persisted across sessions. */ onConfigChange?: (config: TableConfig) => void; - /** - * Features like context menu actions and visual links are enabled by the Vuu server. - * This callback allows us to receive a notification when such a feature is available. - * The options provided must then be used to configure appropriate UI affordances. - */ - onFeatureEnabled?: (message: VuuFeatureMessage) => void; /** * When a Vuu feature e.g. context menu action, has been invoked, the Vuu server * response must be handled. This callback provides that response. diff --git a/vuu-ui/packages/vuu-table/src/table/useDataSource.ts b/vuu-ui/packages/vuu-table/src/table/useDataSource.ts index d018100e3..8599987bf 100644 --- a/vuu-ui/packages/vuu-table/src/table/useDataSource.ts +++ b/vuu-ui/packages/vuu-table/src/table/useDataSource.ts @@ -2,22 +2,19 @@ import { DataSource, DataSourceConfigMessage, DataSourceSubscribedMessage, + isVuuFeatureAction, + isVuuFeatureInvocation, SubscribeCallback, VuuFeatureInvocationMessage, VuuFeatureMessage, } from "@finos/vuu-data"; import { DataSourceRow } from "@finos/vuu-data-types"; -import { - isVuuFeatureAction, - isVuuFeatureInvocation, -} from "@finos/vuu-data-react"; import { VuuRange, VuuSortCol } from "@finos/vuu-protocol-types"; import { getFullRange, isRowSelectedLast, metadataKeys, - RowSelected, WindowRange, } from "@finos/vuu-utils"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; diff --git a/vuu-ui/packages/vuu-table/src/table/useTableModel.ts b/vuu-ui/packages/vuu-table/src/table/useTableModel.ts index a5ad0c328..e2193e310 100644 --- a/vuu-ui/packages/vuu-table/src/table/useTableModel.ts +++ b/vuu-ui/packages/vuu-table/src/table/useTableModel.ts @@ -30,8 +30,7 @@ import { import { Reducer, useReducer } from "react"; import { VuuColumnDataType } from "@finos/vuu-protocol-types"; -import { DataSourceConfig } from "@finos/vuu-data"; -import { TableSchema } from "@finos/vuu-data/src/message-utils"; +import { DataSourceConfig, TableSchema } from "@finos/vuu-data"; const DEFAULT_COLUMN_WIDTH = 100; const KEY_OFFSET = metadataKeys.count; @@ -41,7 +40,7 @@ const columnWithoutDataType = ({ serverDataType }: ColumnDescriptor) => const getCellRendererForColumn = (column: ColumnDescriptor) => { if (isTypeDescriptor(column.type)) { - return getCellRenderer(column.type?.renderer); + return getCellRenderer(column); } }; diff --git a/vuu-ui/packages/vuu-theme/css/components/button.css b/vuu-ui/packages/vuu-theme/css/components/button.css index d201a23da..d35d0a9ab 100644 --- a/vuu-ui/packages/vuu-theme/css/components/button.css +++ b/vuu-ui/packages/vuu-theme/css/components/button.css @@ -1,11 +1,11 @@ .saltButton { white-space: nowrap;; + --saltButton-borderRadius: 6px; } .saltButton-primary { --saltButton-borderColor: var(--salt-actionable-primary-foreground); --saltButton-borderWidth: 1px; - --saltButton-borderRadius: 6px; --saltButton-borderStyle: solid; --vuu-icon-color: var(--saltIcon-color); } diff --git a/vuu-ui/packages/vuu-theme/css/components/input.css b/vuu-ui/packages/vuu-theme/css/components/input.css index 58d2edd44..3e89014aa 100644 --- a/vuu-ui/packages/vuu-theme/css/components/input.css +++ b/vuu-ui/packages/vuu-theme/css/components/input.css @@ -2,6 +2,10 @@ display: none; } +.saltInput:hover { + background: inherit;; +} + .saltInput-primary { --saltInput-height: 24px; border: solid 1px var(--input-borderColor, var(--salt-editable-borderColor)); diff --git a/vuu-ui/packages/vuu-theme/css/components/toggle-button.css b/vuu-ui/packages/vuu-theme/css/components/toggle-button.css index 3bae4e002..fbfdbad5a 100644 --- a/vuu-ui/packages/vuu-theme/css/components/toggle-button.css +++ b/vuu-ui/packages/vuu-theme/css/components/toggle-button.css @@ -5,7 +5,7 @@ } .saltToggleButtonGroup-horizontal .saltToggleButton { - height: 24px; + height: var(--vuuToggleButton-height, 24px); } .vuuIconToggleButton { diff --git a/vuu-ui/packages/vuu-theme/css/foundations/color.css b/vuu-ui/packages/vuu-theme/css/foundations/color.css index ea52e17e8..e058e225f 100644 --- a/vuu-ui/packages/vuu-theme/css/foundations/color.css +++ b/vuu-ui/packages/vuu-theme/css/foundations/color.css @@ -5,12 +5,15 @@ --vuu-color-transparent: transparent; --vuu-color-black: black; --vuu-color-white: white; + --vuu-color-white-fade-70: rgba(255,255,255,.7); /** text-selection */ --vuu-color-blue-40: rgb(164, 213, 244); /* #A4D5F4 */ --vuu-color-purple-10: rgb(109,24,189); /* #6D18BD */ + --vuu-color-purple-20-fade-40 :rgba(197, 163, 229, .4); /* #C5A3E566 */ + --vuu-color-purple-50: rgb(42, 1, 95); /* #2A015F */ --vuu-color-purple-10-fade-light: rgba(109,24,189, var(--vuu-fade-light)); --vuu-color-pink-10: rgb(234, 120, 128); /* #F37880 */ diff --git a/vuu-ui/packages/vuu-theme/css/palette/interact.css b/vuu-ui/packages/vuu-theme/css/palette/interact.css index 1c1b5fd6c..456a9c140 100644 --- a/vuu-ui/packages/vuu-theme/css/palette/interact.css +++ b/vuu-ui/packages/vuu-theme/css/palette/interact.css @@ -21,12 +21,12 @@ --salt-palette-interact-cta-background: var(--vuu-color-purple-10); --salt-palette-interact-cta-background-active: var(--vuu-color-purple-10); --salt-palette-interact-cta-background-activeDisabled: var(--salt-color-blue-700-fade-background); - --salt-palette-interact-cta-background-disabled: var(--salt-color-blue-600-fade-background); + --salt-palette-interact-cta-background-disabled: var(--vuu-color-purple-20-fade-40); --salt-palette-interact-cta-background-hover: var(--vuu-color-pink-10); --salt-palette-interact-cta-foreground: var(--salt-color-white); --salt-palette-interact-cta-foreground-active: var(--salt-color-white); --salt-palette-interact-cta-foreground-activeDisabled: var(--salt-color-white-fade-foreground); - --salt-palette-interact-cta-foreground-disabled: var(--salt-color-white-fade-foreground); + --salt-palette-interact-cta-foreground-disabled: var(--vuu-color-white-fade-70); --salt-palette-interact-cta-foreground-hover: var(--vuu-color-gray-80); --salt-palette-interact-primary-background: var(--vuu-color-white); --salt-palette-interact-primary-background-active: var(--vuu-color-gray-50); @@ -73,7 +73,7 @@ --salt-palette-interact-cta-background: var(--salt-color-blue-600); --salt-palette-interact-cta-background-active: var(--salt-color-blue-700); --salt-palette-interact-cta-background-activeDisabled: var(--salt-color-blue-700-fade-background); - --salt-palette-interact-cta-background-disabled: var(--salt-color-blue-600-fade-background); + --salt-palette-interact-cta-background-disabled: var(--vuu-color-purple-20-fade-40); --salt-palette-interact-cta-background-hover: var(--salt-color-blue-500); --salt-palette-interact-cta-foreground: var(--salt-color-white); --salt-palette-interact-cta-foreground-active: var(--salt-color-white); diff --git a/vuu-ui/packages/vuu-ui-controls/src/common-hooks/selectionTypes.ts b/vuu-ui/packages/vuu-ui-controls/src/common-hooks/selectionTypes.ts index 59bfae0df..3196773bc 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/common-hooks/selectionTypes.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/common-hooks/selectionTypes.ts @@ -90,10 +90,17 @@ export interface SelectionHookResult { setSelected: (selected: string[]) => void; } +/** + * evt is only null in the special case of a selection fired from a multi-select + * host on tab or selection based on freeform text in combobox + */ export type MultiSelectionHandler = ( event: SyntheticEvent | null, selected: Item[] ) => void; +/** + * evt is only null in the special case of freeform text in combobox + */ export type SingleSelectionHandler = ( event: SyntheticEvent | null, selected: Item diff --git a/vuu-ui/packages/vuu-ui-controls/src/cycle-state-button/CycleStateButton.css b/vuu-ui/packages/vuu-ui-controls/src/cycle-state-button/CycleStateButton.css new file mode 100644 index 000000000..e69de29bb diff --git a/vuu-ui/packages/vuu-ui-controls/src/cycle-state-button/CycleStateButton.tsx b/vuu-ui/packages/vuu-ui-controls/src/cycle-state-button/CycleStateButton.tsx new file mode 100644 index 000000000..7615b6306 --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/cycle-state-button/CycleStateButton.tsx @@ -0,0 +1,57 @@ +import { Button, ButtonProps } from "@salt-ds/core"; +import cx from "classnames"; +import { CommitResponse } from "packages/vuu-datagrid-types"; +import { + VuuColumnDataType, + VuuRowDataItemType, +} from "packages/vuu-protocol-types"; +import { ForwardedRef, forwardRef, SyntheticEvent, useCallback } from "react"; + +const classBase = "vuuCycleStateButton"; + +export interface CycleStateButtonProps extends ButtonProps { + onCommit: (evt: SyntheticEvent, value: VuuRowDataItemType) => CommitResponse; + values: string[]; + value: string; +} + +const getNextValue = (value: string, valueList: string[]) => { + const index = valueList.indexOf(value); + if (index === valueList.length - 1) { + return valueList[0]; + } else { + return valueList[index + 1]; + } +}; + +export const CycleStateButton = forwardRef(function CycleStateButton( + { className, onCommit, value, values, ...buttonProps }: CycleStateButtonProps, + forwardedRef: ForwardedRef +) { + const handleClick = useCallback( + (evt: SyntheticEvent) => { + const nextValue = getNextValue(value, values); + onCommit(evt, nextValue as VuuColumnDataType).then((response) => { + if (response !== true) { + console.error(response); + } + }); + }, + [onCommit, value, values] + ); + + return ( + + ); +}); diff --git a/vuu-ui/packages/vuu-ui-controls/src/cycle-state-button/index.ts b/vuu-ui/packages/vuu-ui-controls/src/cycle-state-button/index.ts new file mode 100644 index 000000000..6fd693266 --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/cycle-state-button/index.ts @@ -0,0 +1 @@ +export * from "./CycleStateButton"; diff --git a/vuu-ui/packages/vuu-ui-controls/src/dropdown/Dropdown.css b/vuu-ui/packages/vuu-ui-controls/src/dropdown/Dropdown.css index c5187606c..4782939ba 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/dropdown/Dropdown.css +++ b/vuu-ui/packages/vuu-ui-controls/src/dropdown/Dropdown.css @@ -1,10 +1,5 @@ .vuuDropdown { --saltIcon-margin: 2px 0 0 8px; - --saltButton-borderStyle: solid; - --saltButton-borderColor: var(--salt-editable-borderColor); - --saltButton-borderWidth: 1px; - --saltButton-borderRadius: 6px; - --saltButton-height: var(--vuuDropdown-height, auto); display: inline-block; line-height: 0; @@ -12,6 +7,14 @@ width: var(--vuuDropdown-width, auto); } +.vuuDropdownButton.saltButton-secondary { + --saltButton-borderStyle: solid; + --saltButton-borderColor: var(--salt-editable-borderColor); + --saltButton-borderWidth: 1px; + --saltButton-borderRadius: 6px; + --saltButton-height: var(--vuuDropdown-height, auto); +} + .vuuDropdown-fullWidth { width: 100%; } diff --git a/vuu-ui/packages/vuu-ui-controls/src/dropdown/dropdownTypes.ts b/vuu-ui/packages/vuu-ui-controls/src/dropdown/dropdownTypes.ts index ad862944c..5a5096d05 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/dropdown/dropdownTypes.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/dropdown/dropdownTypes.ts @@ -14,6 +14,7 @@ export type CloseReason = | "Escape" | "click-away" | "select" + | "script" | "Tab" | "toggle"; export type OpenChangeHandler = ( diff --git a/vuu-ui/packages/vuu-ui-controls/src/dropdown/useDropdown.ts b/vuu-ui/packages/vuu-ui-controls/src/dropdown/useDropdown.ts index e350b861a..8a2e6a2b2 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/dropdown/useDropdown.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/dropdown/useDropdown.ts @@ -53,21 +53,18 @@ export const useDropdown = ({ const handleSelectionChange = useCallback( (evt, selected) => { - console.log(`useDropdown onSelectionChange`, { - selected, - }); if (!isMultiSelect) { setIsOpen(false); onOpenChange?.(false); } if (Array.isArray(selected)) { (onSelectionChange as MultiSelectionHandler)?.( - null, + evt, selected as Item[] ); } else if (selected) { (onSelectionChange as SingleSelectionHandler)?.( - null, + evt, selected as Item ); } diff --git a/vuu-ui/packages/vuu-ui-controls/src/editable/useEditableText.ts b/vuu-ui/packages/vuu-ui-controls/src/editable/useEditableText.ts index f9cc2aaf3..4e1038d6e 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/editable/useEditableText.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/editable/useEditableText.ts @@ -1,4 +1,5 @@ -import { VuuColumnDataType } from "@finos/vuu-protocol-types"; +import { VuuRowDataItemType } from "@finos/vuu-protocol-types"; +import { DataItemCommitHandler } from "packages/vuu-datagrid-types"; import { FormEventHandler, KeyboardEvent, @@ -8,12 +9,19 @@ import { } from "react"; import { ClientSideValidationChecker } from "./editable-utils"; +export const WarnCommit = (): Promise => { + console.warn( + "onCommit handler has not been provided to InputCell cell renderer" + ); + return Promise.resolve(true); +}; + export interface EditableTextHookProps< - T extends VuuColumnDataType = VuuColumnDataType + T extends VuuRowDataItemType = VuuRowDataItemType > { clientSideEditValidationCheck?: ClientSideValidationChecker; initialValue: T; - onCommit: (value: T) => boolean; + onCommit: DataItemCommitHandler; } export const dispatchCommitEvent = (el: HTMLElement) => { @@ -22,7 +30,7 @@ export const dispatchCommitEvent = (el: HTMLElement) => { }; export const useEditableText = < - T extends VuuColumnDataType = VuuColumnDataType + T extends VuuRowDataItemType = VuuRowDataItemType >({ clientSideEditValidationCheck, initialValue, @@ -34,10 +42,6 @@ export const useEditableText = < const isDirtyRef = useRef(false); const hasCommittedRef = useRef(false); - const handleBlur = useCallback(() => { - console.log("blur"); - }, []); - const handleKeyDown = useCallback( (evt: KeyboardEvent) => { if (evt.key === "Enter") { @@ -49,11 +53,14 @@ export const useEditableText = < setMessage(warningMessage); } else { setMessage(undefined); - // if we want to potentially await server ACK here, need async - if (onCommit(value)) { - isDirtyRef.current = false; - dispatchCommitEvent(evt.target as HTMLInputElement); - } + onCommit(value).then((response) => { + if (response === true) { + isDirtyRef.current = false; + dispatchCommitEvent(evt.target as HTMLInputElement); + } else { + setMessage(response); + } + }); } } else { dispatchCommitEvent(evt.target as HTMLInputElement); @@ -82,10 +89,8 @@ export const useEditableText = < const { value } = evt.target as HTMLInputElement; isDirtyRef.current = value !== initialValueRef.current; setValue(value as T); - console.log(`value changes to ${value} message ${message}`); if (hasCommittedRef.current) { const warningMessage = clientSideEditValidationCheck?.(value); - console.log({ warningMessage }); if (warningMessage !== message && warningMessage !== false) { setMessage(warningMessage); } @@ -95,7 +100,6 @@ export const useEditableText = < ); return { - onBlur: handleBlur, onChange: handleChange, onKeyDown: handleKeyDown, value, diff --git a/vuu-ui/packages/vuu-ui-controls/src/expando-input/ExpandoInput.css b/vuu-ui/packages/vuu-ui-controls/src/expando-input/ExpandoInput.css index 794f43fde..5c10aab65 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/expando-input/ExpandoInput.css +++ b/vuu-ui/packages/vuu-ui-controls/src/expando-input/ExpandoInput.css @@ -28,7 +28,10 @@ visibility: hidden; white-space: pre-wrap; } - + .vuuExpandoInput-error { + border-color: var(--vuu-color-red-50); + } + .vuuExpandoInput .saltInput { font-weight: var(--salt-text-fontWeight); left: var(--expandoInput-padding, 0); diff --git a/vuu-ui/packages/vuu-ui-controls/src/expando-input/ExpandoInput.tsx b/vuu-ui/packages/vuu-ui-controls/src/expando-input/ExpandoInput.tsx index 9ab9341d8..5b8bfd0e3 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/expando-input/ExpandoInput.tsx +++ b/vuu-ui/packages/vuu-ui-controls/src/expando-input/ExpandoInput.tsx @@ -1,4 +1,4 @@ -import { Input, InputProps } from "@salt-ds/core"; +import { VuuInput, VuuInputProps } from "@finos/vuu-ui-controls"; import cx from "classnames"; import { ForwardedRef, forwardRef } from "react"; @@ -6,15 +6,35 @@ import "./ExpandoInput.css"; const classBase = "vuuExpandoInput"; +const noop = () => undefined; + +export interface ExpandoInputProps extends Omit { + onCommit?: VuuInputProps["onCommit"]; +} + export const ExpandoInput = forwardRef(function ExpandoInput( - { className: classNameProp, value, inputProps, ...InputProps }: InputProps, + { + className: classNameProp, + errorMessage, + value, + inputProps, + onCommit = noop, + ...InputProps + }: ExpandoInputProps, forwardedRef: ForwardedRef ) { return ( -
- + { setIsOpen(open); onOpenChange?.(open, closeReason); - if (open === false) { - dataSource.unsubscribe(); - } + // if (open === false) { + // dataSource.unsubscribe(); + // } }, - [dataSource, onOpenChange, setIsOpen] + [onOpenChange, setIsOpen] ); const handleInputChange = useCallback( @@ -92,6 +92,9 @@ export const useInstrumentPicker = ({ ); const inputProps = { + inputProps: { + autoComplete: "off", + }, onChange: handleInputChange, }; const controlProps = {}; diff --git a/vuu-ui/packages/vuu-ui-controls/src/tabstrip/Tabstrip.css b/vuu-ui/packages/vuu-ui-controls/src/tabstrip/Tabstrip.css index e0a111663..5178a0301 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/tabstrip/Tabstrip.css +++ b/vuu-ui/packages/vuu-ui-controls/src/tabstrip/Tabstrip.css @@ -18,6 +18,10 @@ /* Tabstrip orientation is horizontal */ .vuuTabstrip-horizontal { + --vuuOverflowContainer-borderColor: var(--salt-container-primary-borderColor); + --vuuOverflowContainer-borderStyle: none none solid none; + --vuuOverflowContainer-borderBottomWidth: 1px; + --tabstrip-height: var(--vuuTabstrip-height, 28px); --tabstrip-width: var(--vuuTabstrip-width, 100%); --tab-height: var(--tabstrip-height); @@ -27,7 +31,7 @@ --tab-thumb-top: auto; --tab-thumb-width: var(--tab-thumb-size, 100%); align-items: flex-start; - border-bottom: var(--vuuTabstrip-borderBottom, solid 1px var(--salt-container-primary-borderColor)); + /* border-bottom: var(--vuuTabstrip-borderBottom, solid 1px var(--salt-container-primary-borderColor)); */ } /* Tabstrip orientation is vertical */ @@ -107,7 +111,7 @@ } .vuuDraggable-tabstrip-horizontal { - --item-height: var(--tabstrip-height); + --overflow-item-height: var(--tabstrip-height); --tab-thumb-height: 2px; --tab-thumb-left: 0px; --tabstrip-display: inline-flex; diff --git a/vuu-ui/packages/vuu-ui-controls/src/tabstrip/Tabstrip.tsx b/vuu-ui/packages/vuu-ui-controls/src/tabstrip/Tabstrip.tsx index bf7ee27c1..1ebb1a1f8 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/tabstrip/Tabstrip.tsx +++ b/vuu-ui/packages/vuu-ui-controls/src/tabstrip/Tabstrip.tsx @@ -136,7 +136,7 @@ export const Tabstrip = ({ {...htmlAttributes} {...tabstripHook.containerProps} className={className} - height={28} + height={29} id={id} orientation={orientation} overflowIcon="more-horiz" diff --git a/vuu-ui/packages/vuu-ui-controls/src/tabstrip/useAnimatedSelectionThumb.ts b/vuu-ui/packages/vuu-ui-controls/src/tabstrip/useAnimatedSelectionThumb.ts index beefb4049..03161598e 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/tabstrip/useAnimatedSelectionThumb.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/tabstrip/useAnimatedSelectionThumb.ts @@ -40,7 +40,6 @@ export const useAnimatedSelectionThumb = ( isValidNumber(newPosition) && isValidNumber(oldSize) ) { - console.log({ orientation, positionProp, oldPosition, newPosition }); offset = oldPosition - newPosition; size = oldSize; const speed = orientation === "horizontal" ? 1100 : 700; diff --git a/vuu-ui/packages/vuu-ui-controls/src/vuu-input/VuuInput.css b/vuu-ui/packages/vuu-ui-controls/src/vuu-input/VuuInput.css index e69de29bb..af090a2a2 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/vuu-input/VuuInput.css +++ b/vuu-ui/packages/vuu-ui-controls/src/vuu-input/VuuInput.css @@ -0,0 +1,7 @@ +.vuuInput { + --vuu-icon-size: 16px; +} + +.vuuInput-errorIcon:after { + cursor: pointer; +} \ No newline at end of file diff --git a/vuu-ui/packages/vuu-ui-controls/src/vuu-input/VuuInput.tsx b/vuu-ui/packages/vuu-ui-controls/src/vuu-input/VuuInput.tsx index fdf95e3d3..81cd31a31 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/vuu-input/VuuInput.tsx +++ b/vuu-ui/packages/vuu-ui-controls/src/vuu-input/VuuInput.tsx @@ -4,13 +4,23 @@ import { Input, InputProps } from "@salt-ds/core"; import cx from "classnames"; import { FocusEventHandler, + ForwardedRef, + forwardRef, KeyboardEventHandler, SyntheticEvent, useCallback, } from "react"; +import { Tooltip, useTooltip } from "@finos/vuu-popups"; +import { useId } from "@finos/vuu-layout"; + +import "./VuuInput.css"; const classBase = "vuuInput"; +const constantInputProps = { + autoComplete: "off", +}; + export type Commithandler = ( evt: SyntheticEvent, value: T @@ -18,6 +28,7 @@ export type Commithandler = ( export interface VuuInputProps< T extends VuuRowDataItemType = VuuRowDataItemType > extends InputProps { + errorMessage?: string; onCommit: Commithandler; type?: T; } @@ -26,13 +37,26 @@ export interface VuuInputProps< * A variant of Salt Input that provides a commit callback prop, * TODO along with cancel behaviour ? */ -export const VuuInput = ({ - className, - onCommit, - onKeyDown, - type, - ...props -}: VuuInputProps) => { +export const VuuInput = forwardRef(function VuuInput< + T extends VuuRowDataItemType = string +>( + { + className, + errorMessage, + id: idProp, + onCommit, + onKeyDown, + type, + ...props + }: VuuInputProps, + forwardedRef: ForwardedRef +) { + const id = useId(idProp); + const { anchorProps, tooltipProps } = useTooltip({ + id, + tooltipContent: errorMessage, + }); + const commitValue = useCallback>( (evt, value) => { if (type === "number") { @@ -73,12 +97,32 @@ export const VuuInput = ({ [commitValue] ); - return ( - + ) : undefined; + + return ( + <> + + {tooltipProps ? : null} + ); -}; +}); diff --git a/vuu-ui/packages/vuu-utils/src/column-utils.ts b/vuu-ui/packages/vuu-utils/src/column-utils.ts index 3bb469015..89eb46036 100644 --- a/vuu-ui/packages/vuu-utils/src/column-utils.ts +++ b/vuu-ui/packages/vuu-utils/src/column-utils.ts @@ -1,10 +1,11 @@ +import type { SchemaColumn, TableSchema } from "@finos/vuu-data"; import type { DataSourceFilter, DataSourceRow } from "@finos/vuu-data-types"; import type { ColumnAlignment, ColumnDescriptor, ColumnType, ColumnTypeDescriptor, - ColumnTypeRenderer, + ColumnTypeRendering, ColumnTypeWithValidationRules, GroupColumnDescriptor, KeyedColumnDescriptor, @@ -12,7 +13,9 @@ import type { PinLocation, TableHeading, TableHeadings, - TypeFormatting, + ColumnTypeFormatting, + LookupRenderer, + ValueListRenderer, } from "@finos/vuu-datagrid-types"; import type { Filter, MultiClauseFilter } from "@finos/vuu-filter-types"; import type { @@ -24,9 +27,9 @@ import type { VuuRowRecord, VuuSort, } from "@finos/vuu-protocol-types"; -import type { SchemaColumn } from "@finos/vuu-data"; +import { DefaultColumnConfiguration } from "@finos/vuu-shell"; import type { CSSProperties } from "react"; -import type { CellRendererDescriptor } from "./component-registry"; +import { moveItem } from "./array-utils"; import { isFilterClause, isMultiClauseFilter } from "./filter-utils"; /** @@ -164,6 +167,9 @@ export declare type ColumnTypeSimple = | "time" | "checkbox"; +/** + * + */ export const isTypeDescriptor = ( type?: ColumnType ): type is ColumnTypeDescriptor => @@ -173,8 +179,20 @@ const EMPTY_COLUMN_MAP = {} as const; export const isColumnTypeRenderer = ( renderer?: unknown -): renderer is ColumnTypeRenderer => - typeof (renderer as ColumnTypeRenderer)?.name !== "undefined"; +): renderer is ColumnTypeRendering => + typeof (renderer as ColumnTypeRendering)?.name !== "undefined"; + +export const isLookupRenderer = ( + renderer?: unknown +): renderer is LookupRenderer => + typeof (renderer as LookupRenderer)?.name !== "undefined" && + "lookup" in (renderer as LookupRenderer); + +export const isValueListRenderer = ( + renderer?: unknown +): renderer is ValueListRenderer => + typeof (renderer as ValueListRenderer)?.name !== "undefined" && + Array.isArray((renderer as ValueListRenderer).values); export const hasValidationRules = ( type?: ColumnType @@ -614,10 +632,10 @@ export const findColumn = ( } }; -export function updateColumn( - columns: KeyedColumnDescriptor[], - column: KeyedColumnDescriptor -): KeyedColumnDescriptor[]; +export function updateColumn( + columns: T[], + column: T +): T[]; export function updateColumn( columns: KeyedColumnDescriptor[], column: string, @@ -747,7 +765,7 @@ export const getDefaultColumnType = ( export const updateColumnType = ( column: T, - formatting: TypeFormatting + formatting: ColumnTypeFormatting ): T => { const { serverDataType, type = getDefaultColumnType(serverDataType) } = column; @@ -771,11 +789,11 @@ export const updateColumnType = ( } }; -export const updateColumnRenderer = < +export const updateColumnRenderProps = < T extends ColumnDescriptor = ColumnDescriptor >( column: T, - cellRenderer: CellRendererDescriptor + renderer: ColumnTypeRendering ): T => { const { serverDataType, type } = column; if (type === undefined) { @@ -783,9 +801,7 @@ export const updateColumnRenderer = < ...column, type: { name: getDefaultColumnType(serverDataType), - renderer: { - name: cellRenderer.name, - }, + renderer, }, }; } else if (isSimpleColumnType(type)) { @@ -793,9 +809,7 @@ export const updateColumnRenderer = < ...column, type: { name: type, - renderer: { - name: cellRenderer.name, - }, + renderer, }, }; } else { @@ -803,18 +817,17 @@ export const updateColumnRenderer = < ...column, type: { ...type, - renderer: { - name: cellRenderer.name, - }, + // TODO do we need to preserve any existing attributes from renderer ? + renderer, }, }; } }; const NO_TYPE_SETTINGS = {}; -export const getTypeSettingsFromColumn = ( +export const getTypeFormattingFromColumn = ( column: ColumnDescriptor -): TypeFormatting => { +): ColumnTypeFormatting => { if (isTypeDescriptor(column.type)) { return column.type.formatting ?? NO_TYPE_SETTINGS; } else { @@ -936,3 +949,40 @@ export const setCalculatedColumnExpression = ( name: `${name}:${type}:=${expression}`, }; }; + +export const moveColumnTo = ( + columns: ColumnDescriptor[], + column: ColumnDescriptor, + newIndex: number +) => { + const index = columns.indexOf(column); + return moveItem(columns, index, newIndex); +}; + +export function replaceColumn( + state: KeyedColumnDescriptor[], + column: KeyedColumnDescriptor +) { + return state.map((col) => (col.name === column.name ? column : col)); +} + +export const applyDefaultColumnConfig = ( + { columns, table }: TableSchema, + getDefaultColumnConfig?: DefaultColumnConfiguration +) => { + if (typeof getDefaultColumnConfig === "function") { + return columns.map((column) => { + const config = getDefaultColumnConfig(table.table, column.name); + if (config) { + return { + ...column, + ...config, + }; + } else { + return column; + } + }); + } else { + return columns; + } +}; diff --git a/vuu-ui/packages/vuu-utils/src/component-registry.ts b/vuu-ui/packages/vuu-utils/src/component-registry.ts index 388bf41e0..99b2dc3f6 100644 --- a/vuu-ui/packages/vuu-utils/src/component-registry.ts +++ b/vuu-ui/packages/vuu-utils/src/component-registry.ts @@ -1,6 +1,8 @@ import { FunctionComponent as FC, HTMLAttributes } from "react"; import { - ColumnTypeRenderer, + ColumnDescriptor, + ColumnDescriptorCustomRenderer, + ColumnTypeRendering, EditValidationRule, MappedValueTypeRenderer, TableCellRendererProps, @@ -9,13 +11,30 @@ import { VuuColumnDataType, VuuRowDataItemType, } from "@finos/vuu-protocol-types"; +import { isTypeDescriptor, isColumnTypeRenderer } from "./column-utils"; export interface CellConfigPanelProps extends HTMLAttributes { onConfigChange: () => void; } +export type PropertyChangeHandler = ( + propertyName: string, + propertyValue: string | number | boolean +) => void; + +export type ColumnRenderPropsChangeHandler = ( + renderProps: ColumnTypeRendering +) => void; +export interface ConfigurationEditorProps { + column: ColumnDescriptorCustomRenderer; + onChangeRendering: ColumnRenderPropsChangeHandler; +} + +export type ConfigEditorComponent = FC; + const cellRenderersMap = new Map>(); -const cellConfigPanelsMap = new Map>(); +const configEditorsMap = new Map>(); +const cellConfigPanelsMap = new Map(); const editRuleValidatorsMap = new Map(); const optionsMap = new Map(); @@ -30,7 +49,8 @@ export type ComponentType = | "data-edit-validator"; type CellRendererOptions = { - [key: string]: unknown; + // [key: string]: unknown; + configEditor?: string; description?: string; label?: string; serverDataType?: VuuColumnDataType | VuuColumnDataType[] | "json" | "private"; @@ -96,6 +116,13 @@ export function registerComponent< } } +export const registerConfigurationEditor = ( + componentName: string, + configurationEditor: FC +) => { + configEditorsMap.set(componentName, configurationEditor); +}; + export const getRegisteredCellRenderers = ( serverDataType?: VuuColumnDataType | "json" ): CellRendererDescriptor[] => { @@ -113,12 +140,26 @@ export const getRegisteredCellRenderers = ( } }; -export function getCellRenderer( - renderer?: ColumnTypeRenderer | MappedValueTypeRenderer -) { - if (renderer && "name" in renderer) { - return cellRenderersMap.get(renderer.name); +export const getCellRendererOptions = (renderName: string) => + optionsMap.get(renderName); + +export function getCellRenderer(column: ColumnDescriptor) { + if (isTypeDescriptor(column.type)) { + const { renderer } = column.type; + if (isColumnTypeRenderer(renderer)) { + return cellRenderersMap.get(renderer.name); + } } + if (column.editable) { + // we can only offer a text input edit as a generic editor. + // If a more specialised editor is required, user must configure + // it in column config. + return cellRenderersMap.get("input-cell"); + } +} + +export function getConfigurationEditor(configEditor = "") { + return configEditorsMap.get(configEditor); } export function getCellConfigPanelRenderer(name: string) { diff --git a/vuu-ui/packages/vuu-utils/src/formatting-utils.ts b/vuu-ui/packages/vuu-utils/src/formatting-utils.ts index 687adb639..c9dac851c 100644 --- a/vuu-ui/packages/vuu-utils/src/formatting-utils.ts +++ b/vuu-ui/packages/vuu-utils/src/formatting-utils.ts @@ -1,7 +1,7 @@ import { ColumnDescriptor, ColumnTypeValueMap, - TypeFormatting, + ColumnTypeFormatting, } from "@finos/vuu-datagrid-types"; import { roundDecimal } from "./round-decimal"; import { @@ -16,7 +16,7 @@ export type ValueFormatters = { [key: string]: ValueFormatter; }; -const DEFAULT_NUMERIC_FORMAT: TypeFormatting = {}; +const DEFAULT_NUMERIC_FORMAT: ColumnTypeFormatting = {}; export const defaultValueFormatter = (value: unknown) => value == null ? "" : typeof value === "string" ? value : value.toString(); diff --git a/vuu-ui/packages/vuu-utils/src/json-utils.ts b/vuu-ui/packages/vuu-utils/src/json-utils.ts index 71e505675..0ddc5f1ac 100644 --- a/vuu-ui/packages/vuu-utils/src/json-utils.ts +++ b/vuu-ui/packages/vuu-utils/src/json-utils.ts @@ -41,6 +41,12 @@ const getCellValue = ( ): CellValue => { if (isJsonData(attributeValue)) { return { attribute: `${attribute}+`, attributeValue: "", type: "json" }; + } else if (attributeValue === undefined) { + return { + attribute, + attributeValue: "undefined", + type: "string", + }; } else if (isVuuRowDataItem(attributeValue)) { return { attribute, diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/config/localhost.config.json b/vuu-ui/sample-apps/app-vuu-basket-trader/config/localhost.config.json deleted file mode 100644 index 0967ef424..000000000 --- a/vuu-ui/sample-apps/app-vuu-basket-trader/config/localhost.config.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/demo.tsx b/vuu-ui/sample-apps/app-vuu-basket-trader/demo.tsx deleted file mode 100644 index b732a2f03..000000000 --- a/vuu-ui/sample-apps/app-vuu-basket-trader/demo.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from "react"; -import ReactDOM from "react-dom"; -import { LoginPanel } from "@finos/vuu-shell"; -import { SaltProvider } from "@salt-ds/core"; -import { uuid } from "@finos/vuu-utils"; - -import "./login.css"; - -async function login(username: string) { - try { - const authToken = uuid(); - const date = new Date(); - const days = 1; - date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000); - document.cookie = `vuu-username=${username};expires=${date.toUTCString()};path=/`; - document.cookie = `vuu-auth-token=${authToken};expires=${date.toUTCString()};path=/`; - document.cookie = `vuu-auth-mode=demo`; - - window.location.href = "index.html"; - } catch (err) { - console.error(err); - } -} - -ReactDOM.render( - - - , - document.getElementById("root") -); diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/index.tsx b/vuu-ui/sample-apps/app-vuu-basket-trader/index.tsx deleted file mode 100644 index e2cd5be6f..000000000 --- a/vuu-ui/sample-apps/app-vuu-basket-trader/index.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { - getAuthDetailsFromCookies, - LayoutManagementProvider, - redirectToLogin, -} from "@finos/vuu-shell"; -import React from "react"; -import ReactDOM from "react-dom"; -import { App } from "./src/App"; - -import "@finos/vuu-icons/index.css"; -import "@finos/vuu-theme/index.css"; - -const [username, token] = getAuthDetailsFromCookies(); -if (!username || !token) { - // This won't be needed with serverside protection - redirectToLogin(); -} else { - ReactDOM.render( - - - , - document.getElementById("root") - ); -} diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/layout-loadind.md b/vuu-ui/sample-apps/app-vuu-basket-trader/layout-loadind.md deleted file mode 100644 index 17298ff83..000000000 --- a/vuu-ui/sample-apps/app-vuu-basket-trader/layout-loadind.md +++ /dev/null @@ -1,215 +0,0 @@ -1. client logs in for first time - /api/vui/{username} - -1A) existing service responds with 404, so we display defaultLayout, with placeholder - -**defaultLayout JSON is hardcoded in the app** - -```JSON -{ - -type: "Stack", -props: { - style: { - width: "100%", - height: "100%", - }, - enableAddTab: true, - enableRemoveTab: true, - preserve: true, - active: 0, - TabstripProps: { - allowAddTab: true, - allowCloseTab: true, - allowRenameTab: true, - }, -}, -children: [ - { - type: "Placeholder", - title: "Page 1", - }, -] -} -``` - -1B) new service returns defaultLayout, with placeholder "vuu.layout.version": 2 - -Note: we wrap the layout structure with an enclosing envelope that carries the version. We might -eventually want to record additional metadata here or user settings etc. Maybe application id - -Note also: the 'placeholder' is defined inline. I think the new system should equally support -inline layouts and dynamically loaded layouts. The former should be considered readonly - -```JSON -{ -"vuu.layout.version": 2 -layout: - { - - type: "Stack", - props: { - style: { - width: "100%", - height: "100%", - }, - enableAddTab: true, - enableRemoveTab: true, - preserve: true, - active: 0, - TabstripProps: { - allowAddTab: true, - allowCloseTab: true, - allowRenameTab: true, - }, - }, - children: [ - { - type: "Placeholder", - title: "Page 1", - }, - ] - } -} -``` - -2. UI renders defaultLayout, with placeholder. In either of the scenarios above we reach this point. - -3. User drags content onto layout to replace or displace placeholder - -3A) existing service POSTS to /api/vui/{username} the full persisted JSON structure, as above but with new content in open tab - -```JSON -{ - -type: "Stack", -props: { - style: { - width: "100%", - height: "100%", - }, - enableAddTab: true, - enableRemoveTab: true, - preserve: true, - active: 0, - TabstripProps: { - allowAddTab: true, - allowCloseTab: true, - allowRenameTab: true, - }, -}, -children: [ - { - type: "View", - id: 'xzy', - title: "Page 1", - props: { - closeable: true, - header: true, - label: "SIMUL Instruments", - resize: "defer" - }, - state: { - "table-config": { - columns: [ - {name: "ric", label: "RIC", pin: "left" }, - ... - ] - } - }, - children: [ - { - type: "Feature", - props: { - url: "./feature-vuu-table/index.js" - css: "./feature-vuu-table/index.css" - } - - } - ] - }, -] -} -``` - -3B) new service makes 2 POST requests - -Save the application-level JSON ... - -- /api/vui/{username} - -```JSON -{ -"vuu.layout.version": 2 -layout: - { - - type: "Stack", - props: { - style: { - width: "100%", - height: "100%", - }, - enableAddTab: true, - enableRemoveTab: true, - preserve: true, - active: 0, - TabstripProps: { - allowAddTab: true, - allowCloseTab: true, - allowRenameTab: true, - }, - }, - children: [ - { - type: "layout", - title: "Page 1", - url: "/api/vui/{username}/xyz" - }, - ] - } -} -``` - -save the new layout ... - -- /api/vui/layouts/xyz - -```JSON -{ - id: "xyz" - createdBy: "", - createdTime: "", - lastUpdatedTime: "", - layout: { - type: "View", - id: 'xzy', - title: "Page 1", - props: { - closeable: true, - header: true, - label: "SIMUL Instruments", - resize: "defer" - }, - state: { - "table-config": { - columns: [ - {name: "ric", label: "RIC", pin: "left" }, - ... - ] - } - }, - children: [ - { - type: "Feature", - props: { - url: "./feature-vuu-table/index.js" - css: "./feature-vuu-table/index.css" - } - - } - ] - } -} - -``` diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/login.css b/vuu-ui/sample-apps/app-vuu-basket-trader/login.css deleted file mode 100644 index 56812898f..000000000 --- a/vuu-ui/sample-apps/app-vuu-basket-trader/login.css +++ /dev/null @@ -1,16 +0,0 @@ -body { - align-items: center; - display: flex; - flex-direction: column; - justify-content: center; - margin: 0; -} - -#root { - width: 100vw; - height: 100vh; - display: flex; - align-items: center; - justify-content: center; - -} diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/login.tsx b/vuu-ui/sample-apps/app-vuu-basket-trader/login.tsx deleted file mode 100644 index f2e0e3cee..000000000 --- a/vuu-ui/sample-apps/app-vuu-basket-trader/login.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from "react"; -import ReactDOM from "react-dom"; -import { LoginPanel, ThemeProvider } from "@finos/vuu-shell"; -import { authenticate } from "@finos/vuu-data"; - -import "@finos/vuu-theme/index.css"; -import "./login.css"; - -async function login(username: string, password: string) { - try { - const { authUrl } = await vuuConfig; - const authToken = await authenticate(username, password, authUrl); - const date = new Date(); - const days = 1; - date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000); - document.cookie = `vuu-username=${username};expires=${date.toUTCString()};path=/`; - document.cookie = `vuu-auth-token=${authToken};expires=${date.toUTCString()};path=/`; - document.cookie = `vuu-auth-mode=login`; - window.location.href = "index.html"; - } catch (err) { - console.error(err); - } -} - -ReactDOM.render( - - - , - document.getElementById("root") -); diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/package.json b/vuu-ui/sample-apps/app-vuu-basket-trader/package.json deleted file mode 100644 index 8baea4e7b..000000000 --- a/vuu-ui/sample-apps/app-vuu-basket-trader/package.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "name": "app-vuu-basket-trader", - "version": "0.0.26", - "description": "", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "build": "node ./scripts/build.mjs", - "build:with-ag-grid": "node ./scripts/build.mjs --features feature-vuu-blotter,feature-ag-grid", - "start": "serve -p 5002 ../../deployed_apps/app-vuu-example" - }, - "keywords": [], - "author": "heswell", - "license": "Apache-2.0", - "sideEffects": [ - "**/*.css" - ], - "devDependencies": {}, - "dependencies": { - "@fontsource/open-sans": "^4.5.13", - "@salt-ds/core": "1.8.0", - "@salt-ds/lab": "1.0.0-alpha.15", - "@finos/vuu-data": "0.0.26", - "@finos/vuu-datagrid-types": "0.0.26", - "@finos/vuu-data-react": "0.0.26", - "@finos/vuu-layout": "0.0.26", - "@finos/vuu-popups": "0.0.26", - "@finos/vuu-shell": "0.0.26", - "@finos/vuu-utils": "0.0.26", - "classnames": "^2.3.1", - "react": "^17.0.2", - "react-dom": "^17.0.2" - }, - "engines": { - "node": ">=16.0.0" - } -} diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/public/about.txt b/vuu-ui/sample-apps/app-vuu-basket-trader/public/about.txt deleted file mode 100644 index dc353a4a5..000000000 --- a/vuu-ui/sample-apps/app-vuu-basket-trader/public/about.txt +++ /dev/null @@ -1,6 +0,0 @@ -This favicon was generated using the following font: - -- Font Title: Roboto Condensed -- Font Author: Copyright 2011 Google Inc. All Rights Reserved. -- Font Source: http://fonts.gstatic.com/s/robotocondensed/v19/ieVl2ZhZI2eCN5jzbjEETS9weq8-59WxDCs5cvI.ttf -- Font License: Apache License, version 2.0 (http://www.apache.org/licenses/LICENSE-2.0.html)) diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/public/apple-touch-icon.png b/vuu-ui/sample-apps/app-vuu-basket-trader/public/apple-touch-icon.png deleted file mode 100644 index 74bdb1c5c..000000000 Binary files a/vuu-ui/sample-apps/app-vuu-basket-trader/public/apple-touch-icon.png and /dev/null differ diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/public/demo.html b/vuu-ui/sample-apps/app-vuu-basket-trader/public/demo.html deleted file mode 100644 index 5e53d03ff..000000000 --- a/vuu-ui/sample-apps/app-vuu-basket-trader/public/demo.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - VUU App - - -
- - - diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/public/favicon.ico b/vuu-ui/sample-apps/app-vuu-basket-trader/public/favicon.ico deleted file mode 100644 index cc26fb39d..000000000 Binary files a/vuu-ui/sample-apps/app-vuu-basket-trader/public/favicon.ico and /dev/null differ diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/public/index.html b/vuu-ui/sample-apps/app-vuu-basket-trader/public/index.html deleted file mode 100644 index 155bead1d..000000000 --- a/vuu-ui/sample-apps/app-vuu-basket-trader/public/index.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - VUU App - - -
- - - diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/public/login.html b/vuu-ui/sample-apps/app-vuu-basket-trader/public/login.html deleted file mode 100644 index d9fcdf50c..000000000 --- a/vuu-ui/sample-apps/app-vuu-basket-trader/public/login.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - VUU App - - -
- - - diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/public/manifest.json b/vuu-ui/sample-apps/app-vuu-basket-trader/public/manifest.json deleted file mode 100644 index 17ddafb82..000000000 --- a/vuu-ui/sample-apps/app-vuu-basket-trader/public/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "", - "short_name": "", - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" -} diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/scripts/.eslintrc.json b/vuu-ui/sample-apps/app-vuu-basket-trader/scripts/.eslintrc.json deleted file mode 100644 index 17388657e..000000000 --- a/vuu-ui/sample-apps/app-vuu-basket-trader/scripts/.eslintrc.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "env": { - "node": true, - "es2021": true - }, - "extends": ["eslint:recommended", "plugin:react/recommended"], - "parserOptions": { - "ecmaFeatures": { - "jsx": true - }, - "ecmaVersion": "latest", - "sourceType": "module" - }, - "rules": {} -} diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/scripts/build.mjs b/vuu-ui/sample-apps/app-vuu-basket-trader/scripts/build.mjs deleted file mode 100644 index 3e83b8134..000000000 --- a/vuu-ui/sample-apps/app-vuu-basket-trader/scripts/build.mjs +++ /dev/null @@ -1,191 +0,0 @@ -import { - assertFileExists, - byFileName, - copyFolderSync, - formatBytes, - formatDuration, - getCommandLineArg, - padRight, - readJson, - readPackageJson, - writeMetaFile, -} from "../../../scripts/utils.mjs"; -import { build } from "../../../scripts/esbuild.mjs"; -import fs from "fs"; -import path from "path"; - -const entryPoints = ["index.tsx", "login.tsx", "demo.tsx"]; - -const outdir = "../../deployed_apps/app-vuu-basket-trader"; -let configFile = "./config/localhost.config.json"; - -const websocketUrl = getCommandLineArg("--url", true); -const watch = getCommandLineArg("--watch"); -const development = watch || getCommandLineArg("--dev"); -const configPath = getCommandLineArg("--config", true); -const features = getCommandLineArg( - "--features", - true, - "feature-filter-table,feature-instrument-tiles,feature-basket-trading" -); -if (configPath) { - configFile = configPath; -} - -const featureEntryPoints = features - .split(",") - .map((featureName) => `../${featureName}/index.ts`); - -assertFileExists(configFile, true); - -const { name: projectName } = readPackageJson(); - -const esbuildConfig = { - entryPoints: entryPoints.concat(featureEntryPoints), - env: development ? "development" : "production", - name: "app-vuu-basket-trader", - outdir, - splitting: true, - target: "esnext", -}; - -async function writeFeatureEntriesToConfigJson(featureBundles) { - return new Promise((resolve, reject) => { - console.log("[DEPLOY config]"); - const configJson = readJson(configFile); - if (websocketUrl) { - configJson.websocketUrl = websocketUrl; - } - let { features } = configJson; - if (features === undefined) { - features = configJson.features = {}; - } - - const featureFilePath = (featureName, files, matchPattern) => { - const file = files.find(({ fileName }) => - fileName.endsWith(matchPattern) - ); - if (file) { - return `./feature-${featureName}/${file.fileName}`; - } - }; - - featureBundles.forEach(({ name, files }) => { - const { description = name, vuu } = readJson( - path.resolve(`../feature-${name}/package.json`) - ); - features[name] = { - title: description, - name, - url: featureFilePath(name, files, ".js"), - css: featureFilePath(name, files, ".css"), - ...vuu, - }; - }); - - fs.writeFile( - path.resolve(outdir, "config.json"), - JSON.stringify(configJson, null, 2), - (err) => { - if (err) { - reject(err); - } else { - resolve(); - } - } - ); - }); -} - -async function main() { - function createDeployFolder() { - fs.rmSync(outdir, { recursive: true, force: true }); - fs.mkdirSync(outdir, { recursive: true }); - } - - console.log("[CLEAN]"); - createDeployFolder(); - - console.log("[BUILD]"); - const [ - { - result: { metafile }, - duration, - }, - ] = await Promise.all([build(esbuildConfig)]).catch((e) => { - console.error(e); - process.exit(1); - }); - - await writeMetaFile(metafile, outdir); - - console.log("[DEPLOY public assets]"); - const publicContent = fs.readdirSync(`./public`); - publicContent.forEach((file) => { - if (file !== ".DS_Store") { - if (typeof fs.cp === "function") { - // node v16.7 + - fs.cp( - path.resolve("public", file), - path.resolve(outdir, file), - { recursive: true }, - (err) => { - if (err) throw err; - } - ); - } else { - // delete once we no longer need to support node16 < .7 - copyFolderSync( - path.resolve("public", file), - path.resolve(outdir, file) - ); - } - } - }); - - const outputs = { - core: [], - common: [], - features: [], - }; - for (const [file, { bytes }] of Object.entries(metafile.outputs)) { - if (file.endsWith("js") || file.endsWith("css")) { - const fileName = file.replace(`${outdir}/`, ""); - if (fileName.startsWith(projectName)) { - outputs.core.push({ fileName, bytes }); - } else if (fileName.startsWith("feature")) { - const [name, featureFileName] = fileName.split("/"); - const featureName = name.replace("feature-", ""); - let feature = outputs.features.find((f) => f.name === featureName); - if (feature === undefined) { - feature = { name: featureName, files: [] }; - outputs.features.push(feature); - } - feature.files.push({ fileName: featureFileName, bytes }); - } else { - outputs.common.push({ fileName, bytes }); - } - } - } - - console.log("\ncore"); - outputs.core.sort(byFileName).forEach(({ fileName, bytes }) => { - console.log(`${padRight(fileName, 30)} ${formatBytes(bytes)}`); - }); - console.log("\ncommon"); - outputs.common.forEach(({ fileName, bytes }) => { - console.log(`${padRight(fileName, 30)} ${formatBytes(bytes)}`); - }); - outputs.features.forEach(({ name, files }) => { - console.log(`\nfeature: ${name}`); - files.forEach(({ fileName, bytes }) => { - console.log(`${padRight(fileName, 30)} ${formatBytes(bytes)}`); - }); - }); - - console.log(`\nbuild took ${formatDuration(duration)}`); - - await writeFeatureEntriesToConfigJson(outputs.features); -} - -main(); diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/src/App.css b/vuu-ui/sample-apps/app-vuu-basket-trader/src/App.css deleted file mode 100644 index 52c514dcd..000000000 --- a/vuu-ui/sample-apps/app-vuu-basket-trader/src/App.css +++ /dev/null @@ -1,157 +0,0 @@ -body { - padding: 0; - margin: 0; - height: 100vh; - width: 100vw; - overflow: hidden; -} - -.ToolbarField .Input { - border: solid 1px #ccc; - height: 28px; - padding: 0 8px; -} - -.DragIcon.dragging { - background-color: orange !important; -} - -.vuuApp { - background-color: green; -} - -.vuuShell-content { - padding: 8px; -} - - -.ToolbarField > svg { - fill: var(--spectrum-global-color-static-gray-600); - width: 18px; - height: 18px; -} - -.vuHeaderCell { - text-transform: uppercase; -} - -.vuDialog { - margin-top: 20%; -} - -.vuDialog { - --vuuView-flex-direction: row; - --vuuView-flex-wrap: wrap; -} - -.vuDialog .Grid { - --hw-grid-flex-size: 1 1 auto; -} - -/** Temp, until we create Toolbar */ -.vuuToolbarProxy { - align-items: center; - display: flex; - gap: 12px; - height: var(--vuuToolbarProxy-height, 36px); -} - -.vuuToolbarProxy > [data-align="end"]{ - margin-left: auto; -} - -.vuuToolbarProxy-vertical { - flex-direction: column; -} - -.vuuShell-mainTabs { - --vuuTab-height: 28px; - border: solid 1px #D6D7DA; - border-top: none !important; - border-radius: 6px; - height: 100%; - padding: 36px 8px 8px 8px; - position: relative; - width: 100%; -} - -.vuuShellMainTabstrip > .vuuOverflowContainer-wrapContainer { - background: var(--vuu-color-gray-25); - -} - - .vuuShell-mainTabs > .vuuTabstrip { - --vuuTabstrip-height: 28px; - --saltTabs-tabstrip-height: 29px; - --tabstrip-height: 29px; - left:-1px; - padding-bottom: 7px; - position: absolute !important; - right: 1px; - top: 0; - width: calc(100% + 2px) !important; - } - - .vuuShell-mainTabs > .vuuTabHeader { - border-bottom: none; - } - - .vuuShell-mainTabs > .vuuTabstrip:before { - background-color: transparent; - border-radius: 0 6px 0 0; - border-left: solid 1px #D6D7DA; - border-right: solid 1px #D6D7DA; - border-top: solid 1px #D6D7DA; - content: ''; - position: absolute; - bottom: 0; - left:0; - right:0; - height: 8px; - z-index: 1; - } - - .vuuTab.MainTab { - background-color: #F1F2F4; - border-color: #D6D7DA; - border-radius: 6px 6px 0 0; - border-width: 1px; - border-style: solid; - position: relative; - } - - .MainTab.vuuTab-selected { - background-color: white; - border-bottom-color: white; - z-index: 1; - - } - - .MainTab.vuuTab-selected:before{ - background-color: #6d188b;; - content: ''; - position: absolute; - height: 100%; - left:0; - top:0; - border-radius: 6px 0 0 0; - width: 6px; - } - - .MainTab.vuuTab:hover:not(.vuuTab-selected):before{ - background-color: #F37880; - content: ''; - position: absolute; - height: 100%; - left:0; - top:0; - border-radius: 6px 0 0 0; - width: 6px; - } - - .vuuTab.MainTab .vuuTab-main { - background-color: transparent; - font-weight: 700; - height: 29px; - padding: 0 24px; - } diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/src/App.tsx b/vuu-ui/sample-apps/app-vuu-basket-trader/src/App.tsx deleted file mode 100644 index 332e584be..000000000 --- a/vuu-ui/sample-apps/app-vuu-basket-trader/src/App.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { ContextMenuProvider, useDialog } from "@finos/vuu-popups"; -import { - LeftNav, - Shell, - ShellContextProvider, - ShellProps, - VuuUser, -} from "@finos/vuu-shell"; -import { getDefaultColumnConfig } from "./columnMetaData"; -import { createPlaceholder } from "./createPlaceholder"; -import { useFeatures } from "./useFeatures"; -import { - ColumnSettingsPanel, - TableSettingsPanel, -} from "@finos/vuu-table-extras"; -import { - registerComponent, - useLayoutContextMenuItems, -} from "@finos/vuu-layout"; - -import "./App.css"; -import { useRpcResponseHandler } from "./useRpcResponseHandler"; - -registerComponent("ColumnSettings", ColumnSettingsPanel, "view"); -registerComponent("TableSettings", TableSettingsPanel, "view"); - -// createNewChild is used when we add a new Tab to Stack -const layoutProps: ShellProps["LayoutProps"] = { - createNewChild: createPlaceholder, - pathToDropTarget: "#main-tabs.ACTIVE_CHILD", -}; - -const defaultWebsocketUrl = `wss://${location.hostname}:8090/websocket`; -const { - websocketUrl: serverUrl = defaultWebsocketUrl, - features: configuredFeatures, -} = - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - await vuuConfig; - -export const App = ({ user }: { user: VuuUser }) => { - const [features, tableFeatures] = useFeatures({ - features: configuredFeatures, - }); - - const { dialog, setDialogState } = useDialog(); - const { handleRpcResponse } = useRpcResponseHandler(setDialogState); - const { buildMenuOptions, handleMenuAction } = - useLayoutContextMenuItems(setDialogState); - - // TODO get Context from Shell - return ( - - - - } - saveUrl="https://localhost:8443/api/vui" - serverUrl={serverUrl} - user={user} - > - {dialog} - - - - ); -}; diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/src/Layouts.jsx b/vuu-ui/sample-apps/app-vuu-basket-trader/src/Layouts.jsx deleted file mode 100644 index 4e95fc3f7..000000000 --- a/vuu-ui/sample-apps/app-vuu-basket-trader/src/Layouts.jsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from "react"; -import { FlexboxLayout as Flexbox, Placeholder } from "@finos/vuu-layout"; - -export const twoColumns = ( - - - - -); diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/src/columnMetaData.ts b/vuu-ui/sample-apps/app-vuu-basket-trader/src/columnMetaData.ts deleted file mode 100644 index 7a8016780..000000000 --- a/vuu-ui/sample-apps/app-vuu-basket-trader/src/columnMetaData.ts +++ /dev/null @@ -1,399 +0,0 @@ -import { ColumnDescriptor } from "@finos/vuu-datagrid-types"; - -const Average = 2; - -const ccy: Partial = { - name: "ccy", - label: "CCY", - width: 60, -}; - -const filledQuantity: Partial = { - label: "Filled Qty", - name: "filledQuantity", - minWidth: 150, - type: { - name: "number", - renderer: { name: "progress", associatedField: "quantity" }, - formatting: { decimals: 0 }, - }, -}; - -const ric: Partial = { - name: "ric", - label: "RIC", - type: { - name: "string", - }, - width: 60, -}; - -const side: Partial = { - label: "Side", - name: "side", - type: { - name: "string", - }, - width: 60, -}; - -const columnMetaData: { [key: string]: Partial } = { - account: { - label: "Account", - name: "account", - type: { - name: "string", - }, - }, - algo: { - label: "Algo", - name: "algo", - type: { - name: "string", - }, - }, - ask: { - name: "ask", - label: "Ask", - type: { - name: "number", - renderer: { name: "background", flashStyle: "arrow-bg" }, - formatting: { decimals: 2, zeroPad: true }, - }, - aggregate: Average, - }, - askSize: { - name: "askSize", - label: "Ask Size", - type: { - name: "number", - }, - aggregate: Average, - }, - averagePrice: { - label: "Average Price", - name: "averagePrice", - type: { - name: "number", - }, - aggregate: Average, - }, - bbg: { - name: "bbg", - label: "BBG", - type: { - name: "string", - }, - }, - bid: { - label: "Bid", - name: "bid", - type: { - name: "number", - renderer: { name: "background", flashStyle: "arrow-bg" }, - formatting: { decimals: 2, zeroPad: true }, - }, - aggregate: Average, - }, - bidSize: { - label: "Bid Size", - name: "bidSize", - type: { - name: "number", - }, - aggregate: Average, - }, - childCount: { - label: "Child Count", - name: "childCount", - type: { - name: "number", - }, - aggregate: Average, - }, - - close: { - label: "Close", - name: "close", - type: { - name: "number", - formatting: { decimals: 2, zeroPad: true }, - }, - aggregate: Average, - }, - clOrderId: { - label: "Child Order ID", - name: "clOrderId", - width: 60, - }, - created: { - label: "Created", - name: "created", - type: { - name: "time", - }, - }, - currency: { - name: "currency", - label: "CCY", - width: 60, - }, - description: { - name: "description", - label: "Description", - type: { - name: "string", - }, - }, - exchange: { - name: "exchange", - label: "Exchange", - type: { - name: "string", - }, - }, - filledQty: { - label: "Filled Qty", - name: "filledQty", - width: 150, - type: { - name: "number", - }, - }, - id: { - name: "id", - label: "ID", - type: { - name: "string", - }, - }, - idAsInt: { - label: "ID (int)", - name: "idAsInt", - type: { - name: "string", - }, - }, - isin: { - name: "isin", - label: "ISIN", - type: { - name: "string", - }, - }, - last: { - label: "Last", - name: "last", - type: { - name: "number", - formatting: { decimals: 2, zeroPad: true }, - }, - aggregate: Average, - }, - lastUpdate: { - label: "Last Update", - name: "lastUpdate", - type: { - name: "time", - }, - }, - lotSize: { - label: "Lot Size", - name: "lotSize", - width: 80, - type: { - name: "number", - }, - }, - max: { - label: "Max", - name: "max", - width: 80, - type: { - name: "number", - }, - }, - mean: { - label: "Mean", - name: "mean", - width: 80, - type: { - name: "number", - }, - }, - open: { - label: "Open", - name: "open", - type: { - name: "number", - formatting: { decimals: 2, zeroPad: true }, - }, - aggregate: Average, - }, - openQty: { - label: "Open Qty", - name: "openQty", - width: 80, - type: { - name: "number", - formatting: { decimals: 0 }, - }, - }, - orderId: { - label: "Order ID", - name: "orderId", - width: 60, - }, - - phase: { - label: "Phase", - name: "phase", - type: { - name: "string", - }, - }, - parentOrderId: { - label: "Parent Order Id", - name: "parentOrderId", - width: 80, - type: { - name: "number", - }, - }, - orderType: { - label: "Order Type", - name: "orderType", - type: { - name: "string", - }, - }, - price: { - label: "Price", - name: "price", - type: { - name: "number", - formatting: { decimals: 2, zeroPad: true }, - }, - aggregate: Average, - }, - priceLevel: { - label: "Price Level", - name: "priceLevel", - type: { - name: "string", - }, - }, - quantity: { - label: "Quantity", - name: "quantity", - width: 80, - type: { - name: "number", - }, - }, - scenario: { - label: "Scenario", - name: "scenario", - type: { - name: "string", - }, - }, - size: { - label: "Size", - name: "size", - width: 80, - type: { - name: "number", - }, - }, - status: { - label: "Status", - name: "status", - type: { - name: "string", - }, - }, - strategy: { - label: "Strategy", - name: "strategy", - type: { - name: "string", - }, - }, - table: { - label: "Table", - name: "table", - type: { - name: "string", - }, - }, - trader: { - label: "Trader", - name: "trader", - type: { - name: "string", - }, - }, - uniqueId: { - label: "Unique ID", - name: "uniqueId", - type: { - name: "string", - }, - }, - updateCount: { - label: "Update Count", - name: "updateCount", - width: 80, - type: { - name: "number", - }, - }, - updatesPerSecond: { - label: "Updates Per Second", - name: "updatesPerSecond", - width: 80, - type: { - name: "number", - }, - }, - user: { - label: "User", - name: "user", - type: { - name: "string", - }, - }, - volLimit: { - label: "Vol Limit", - name: "volLimit", - width: 80, - type: { - name: "number", - }, - }, -}; - -type TableColDefs = { [key: string]: Partial }; - -const tables: { [key: string]: TableColDefs } = { - orders: { - ccy, - filledQuantity, - ric, - side, - }, - ordersPrices: { - ccy, - filledQuantity, - ric, - side, - }, -}; - -export const getDefaultColumnConfig = ( - tableName: string, - columnName: string -) => { - return tables[tableName]?.[columnName] ?? columnMetaData[columnName]; -}; diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/src/createPlaceholder.tsx b/vuu-ui/sample-apps/app-vuu-basket-trader/src/createPlaceholder.tsx deleted file mode 100644 index 0bfc5cb22..000000000 --- a/vuu-ui/sample-apps/app-vuu-basket-trader/src/createPlaceholder.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Placeholder, View } from "@finos/vuu-layout"; - -export const createPlaceholder = (index?: number) => ( - // Note make this width 100% and height 100% and we get a weird error where view continually resizes - growing - - - -); diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/src/session-editing/index.ts b/vuu-ui/sample-apps/app-vuu-basket-trader/src/session-editing/index.ts deleted file mode 100644 index 8e92f0ebb..000000000 --- a/vuu-ui/sample-apps/app-vuu-basket-trader/src/session-editing/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./session-table-config"; diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/src/session-editing/session-table-config.ts b/vuu-ui/sample-apps/app-vuu-basket-trader/src/session-editing/session-table-config.ts deleted file mode 100644 index 91dc3cb01..000000000 --- a/vuu-ui/sample-apps/app-vuu-basket-trader/src/session-editing/session-table-config.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { - MenuRpcResponse, - OpenDialogAction, - TableSchema, -} from "@finos/vuu-data"; -import { FormConfig, FormFieldDescriptor } from "@finos/vuu-shell"; - -const static_config: { [key: string]: Partial } = { - OPEN_EDIT_RESET_FIX: { - title: "Reset the Sequence Number", - fields: [ - { - label: "Process Id", - description: "Process Id", - name: "process-id", - type: "string", - }, - { - description: "Sequence Number", - label: "Sequence Number", - name: "sequenceNumber", - type: "long", - }, - ], - }, -}; - -const mergeFields = ( - fields: FormFieldDescriptor[], - staticFields?: FormFieldDescriptor[] -) => { - if (Array.isArray(staticFields)) { - return fields.map((field) => { - const { name } = field; - const staticField = staticFields.find((f) => f.name === name); - if (staticField) { - return { - ...field, - ...staticField, - }; - } else { - return field; - } - }); - } else { - return fields; - } -}; - -const getStaticConfig = (rpcName: string, formConfig: FormConfig) => { - const staticConfig = static_config[rpcName]; - if (staticConfig) { - return { - ...formConfig, - ...staticConfig, - fields: mergeFields(formConfig.fields, staticConfig.fields), - }; - } else { - return formConfig; - } -}; - -const defaultFormConfig = { - fields: [], - key: "", - title: "", -}; - -const keyFirst = (c1: FormFieldDescriptor, c2: FormFieldDescriptor) => - c1.isKeyField ? -1 : c2.isKeyField ? 1 : 0; - -const configFromSchema = (schema?: TableSchema): FormConfig | undefined => { - if (schema) { - const { columns, key } = schema; - return { - key, - title: `Parameters for command`, - fields: columns - .map((col) => ({ - description: col.name, - label: col.name, - name: col.name, - type: col.serverDataType, - isKeyField: col.name === key, - })) - .sort(keyFirst), - }; - } -}; - -export const getFormConfig = ({ action, rpcName }: MenuRpcResponse) => { - const { tableSchema: schema } = action as OpenDialogAction; - const config = configFromSchema(schema) ?? defaultFormConfig; - - if (rpcName !== undefined && rpcName in static_config) { - return { - config: getStaticConfig(rpcName, config), - schema, - }; - } - - return { - config, - schema, - }; -}; diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/tsconfig.json b/vuu-ui/sample-apps/app-vuu-basket-trader/tsconfig.json deleted file mode 100644 index 409f9488e..000000000 --- a/vuu-ui/sample-apps/app-vuu-basket-trader/tsconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "target": "esnext", - } -} diff --git a/vuu-ui/sample-apps/app-vuu-example/demo.tsx b/vuu-ui/sample-apps/app-vuu-example/demo.tsx index b732a2f03..f82aa7fc9 100644 --- a/vuu-ui/sample-apps/app-vuu-example/demo.tsx +++ b/vuu-ui/sample-apps/app-vuu-example/demo.tsx @@ -1,9 +1,11 @@ import React from "react"; import ReactDOM from "react-dom"; -import { LoginPanel } from "@finos/vuu-shell"; -import { SaltProvider } from "@salt-ds/core"; +import { LoginPanel, ThemeProvider } from "@finos/vuu-shell"; import { uuid } from "@finos/vuu-utils"; +import "@finos/vuu-icons/index.css"; +import "@finos/vuu-theme/index.css"; + import "./login.css"; async function login(username: string) { @@ -23,8 +25,8 @@ async function login(username: string) { } ReactDOM.render( - + - , + , document.getElementById("root") ); diff --git a/vuu-ui/sample-apps/app-vuu-example/index.tsx b/vuu-ui/sample-apps/app-vuu-example/index.tsx index 770db3202..e2cd5be6f 100644 --- a/vuu-ui/sample-apps/app-vuu-example/index.tsx +++ b/vuu-ui/sample-apps/app-vuu-example/index.tsx @@ -1,14 +1,14 @@ -import React from "react"; -import ReactDOM from "react-dom"; -import { App } from "./src/App"; import { getAuthDetailsFromCookies, LayoutManagementProvider, redirectToLogin, } from "@finos/vuu-shell"; +import React from "react"; +import ReactDOM from "react-dom"; +import { App } from "./src/App"; -import "@salt-ds/theme/index.css"; import "@finos/vuu-icons/index.css"; +import "@finos/vuu-theme/index.css"; const [username, token] = getAuthDetailsFromCookies(); if (!username || !token) { diff --git a/vuu-ui/sample-apps/app-vuu-example/login.tsx b/vuu-ui/sample-apps/app-vuu-example/login.tsx index 878194ee0..925da5457 100644 --- a/vuu-ui/sample-apps/app-vuu-example/login.tsx +++ b/vuu-ui/sample-apps/app-vuu-example/login.tsx @@ -1,13 +1,14 @@ import React from "react"; import ReactDOM from "react-dom"; -import { LoginPanel } from "@finos/vuu-shell"; +import { LoginPanel, ThemeProvider } from "@finos/vuu-shell"; import { authenticate } from "@finos/vuu-data"; -import { SaltProvider } from "@salt-ds/core"; +import "@finos/vuu-icons/index.css"; import "@finos/vuu-theme/index.css"; + import "./login.css"; -async function login(username: string, password: string) { +async function login(username: string, password = "password") { try { const { authUrl } = await vuuConfig; const authToken = await authenticate(username, password, authUrl); @@ -24,8 +25,8 @@ async function login(username: string, password: string) { } ReactDOM.render( - + - , + , document.getElementById("root") ); diff --git a/vuu-ui/sample-apps/app-vuu-example/package.json b/vuu-ui/sample-apps/app-vuu-example/package.json index c6a74eb97..4367ff480 100644 --- a/vuu-ui/sample-apps/app-vuu-example/package.json +++ b/vuu-ui/sample-apps/app-vuu-example/package.json @@ -18,9 +18,7 @@ "dependencies": { "@fontsource/open-sans": "^4.5.13", "@salt-ds/core": "1.8.0", - "@salt-ds/icons": "1.5.1", "@salt-ds/lab": "1.0.0-alpha.15", - "@salt-ds/theme": "1.7.1", "@finos/vuu-data": "0.0.26", "@finos/vuu-datagrid-types": "0.0.26", "@finos/vuu-data-react": "0.0.26", diff --git a/vuu-ui/sample-apps/app-vuu-example/public/.DS_Store b/vuu-ui/sample-apps/app-vuu-example/public/.DS_Store deleted file mode 100644 index f636ba78d..000000000 Binary files a/vuu-ui/sample-apps/app-vuu-example/public/.DS_Store and /dev/null differ diff --git a/vuu-ui/sample-apps/app-vuu-example/public/demo.html b/vuu-ui/sample-apps/app-vuu-example/public/demo.html index 8054d3bf6..9abfbcef2 100644 --- a/vuu-ui/sample-apps/app-vuu-example/public/demo.html +++ b/vuu-ui/sample-apps/app-vuu-example/public/demo.html @@ -9,7 +9,6 @@ - VUU App diff --git a/vuu-ui/sample-apps/app-vuu-example/public/favicon-16x16.png b/vuu-ui/sample-apps/app-vuu-example/public/favicon-16x16.png deleted file mode 100644 index f85891bba..000000000 Binary files a/vuu-ui/sample-apps/app-vuu-example/public/favicon-16x16.png and /dev/null differ diff --git a/vuu-ui/sample-apps/app-vuu-example/public/favicon-32x32.png b/vuu-ui/sample-apps/app-vuu-example/public/favicon-32x32.png deleted file mode 100644 index 84d627f7a..000000000 Binary files a/vuu-ui/sample-apps/app-vuu-example/public/favicon-32x32.png and /dev/null differ diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/public/favicon.png b/vuu-ui/sample-apps/app-vuu-example/public/favicon.png similarity index 100% rename from vuu-ui/sample-apps/app-vuu-basket-trader/public/favicon.png rename to vuu-ui/sample-apps/app-vuu-example/public/favicon.png diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/public/favicon.svg b/vuu-ui/sample-apps/app-vuu-example/public/favicon.svg similarity index 100% rename from vuu-ui/sample-apps/app-vuu-basket-trader/public/favicon.svg rename to vuu-ui/sample-apps/app-vuu-example/public/favicon.svg diff --git a/vuu-ui/sample-apps/app-vuu-example/public/index.html b/vuu-ui/sample-apps/app-vuu-example/public/index.html index 69b362da4..a6cd74925 100644 --- a/vuu-ui/sample-apps/app-vuu-example/public/index.html +++ b/vuu-ui/sample-apps/app-vuu-example/public/index.html @@ -2,9 +2,7 @@ - - - + @@ -12,8 +10,7 @@ - - + VUU App diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/public/vuu-icon.svg b/vuu-ui/sample-apps/app-vuu-example/public/vuu-icon.svg similarity index 100% rename from vuu-ui/sample-apps/app-vuu-basket-trader/public/vuu-icon.svg rename to vuu-ui/sample-apps/app-vuu-example/public/vuu-icon.svg diff --git a/vuu-ui/sample-apps/app-vuu-example/scripts/build.mjs b/vuu-ui/sample-apps/app-vuu-example/scripts/build.mjs index b91fb1bbf..4a4bec219 100644 --- a/vuu-ui/sample-apps/app-vuu-example/scripts/build.mjs +++ b/vuu-ui/sample-apps/app-vuu-example/scripts/build.mjs @@ -20,12 +20,14 @@ const outdir = "../../deployed_apps/app-vuu-example"; let configFile = "./config/localhost.config.json"; const websocketUrl = getCommandLineArg("--url", true); -console.log(`websocket URL ${websocketUrl} type ${typeof websocketUrl}`); const watch = getCommandLineArg("--watch"); const development = watch || getCommandLineArg("--dev"); const configPath = getCommandLineArg("--config", true); -const features = getCommandLineArg("--features", true, "feature-vuu-table"); -console.log({ features }); +const features = getCommandLineArg( + "--features", + true, + "feature-filter-table,feature-instrument-tiles,feature-basket-trading" +); if (configPath) { configFile = configPath; } @@ -69,7 +71,7 @@ async function writeFeatureEntriesToConfigJson(featureBundles) { }; featureBundles.forEach(({ name, files }) => { - const { description = name } = readJson( + const { description = name, vuu } = readJson( path.resolve(`../feature-${name}/package.json`) ); features[name] = { @@ -77,6 +79,7 @@ async function writeFeatureEntriesToConfigJson(featureBundles) { name, url: featureFilePath(name, files, ".js"), css: featureFilePath(name, files, ".css"), + ...vuu, }; }); diff --git a/vuu-ui/sample-apps/app-vuu-example/src/App.css b/vuu-ui/sample-apps/app-vuu-example/src/App.css index 048d3429c..52c514dcd 100644 --- a/vuu-ui/sample-apps/app-vuu-example/src/App.css +++ b/vuu-ui/sample-apps/app-vuu-example/src/App.css @@ -6,12 +6,6 @@ body { overflow: hidden; } -.App { - - --vuuView-borderStyle: none solid solid none; - -} - .ToolbarField .Input { border: solid 1px #ccc; height: 28px; @@ -26,6 +20,11 @@ body { background-color: green; } +.vuuShell-content { + padding: 8px; +} + + .ToolbarField > svg { fill: var(--spectrum-global-color-static-gray-600); width: 18px; @@ -54,7 +53,7 @@ body { align-items: center; display: flex; gap: 12px; - height: 36px; + height: var(--vuuToolbarProxy-height, 36px); } .vuuToolbarProxy > [data-align="end"]{ @@ -66,8 +65,93 @@ body { } .vuuShell-mainTabs { + --vuuTab-height: 28px; + border: solid 1px #D6D7DA; + border-top: none !important; + border-radius: 6px; height: 100%; + padding: 36px 8px 8px 8px; position: relative; width: 100%; } +.vuuShellMainTabstrip > .vuuOverflowContainer-wrapContainer { + background: var(--vuu-color-gray-25); + +} + + .vuuShell-mainTabs > .vuuTabstrip { + --vuuTabstrip-height: 28px; + --saltTabs-tabstrip-height: 29px; + --tabstrip-height: 29px; + left:-1px; + padding-bottom: 7px; + position: absolute !important; + right: 1px; + top: 0; + width: calc(100% + 2px) !important; + } + + .vuuShell-mainTabs > .vuuTabHeader { + border-bottom: none; + } + + .vuuShell-mainTabs > .vuuTabstrip:before { + background-color: transparent; + border-radius: 0 6px 0 0; + border-left: solid 1px #D6D7DA; + border-right: solid 1px #D6D7DA; + border-top: solid 1px #D6D7DA; + content: ''; + position: absolute; + bottom: 0; + left:0; + right:0; + height: 8px; + z-index: 1; + } + + .vuuTab.MainTab { + background-color: #F1F2F4; + border-color: #D6D7DA; + border-radius: 6px 6px 0 0; + border-width: 1px; + border-style: solid; + position: relative; + } + + .MainTab.vuuTab-selected { + background-color: white; + border-bottom-color: white; + z-index: 1; + + } + + .MainTab.vuuTab-selected:before{ + background-color: #6d188b;; + content: ''; + position: absolute; + height: 100%; + left:0; + top:0; + border-radius: 6px 0 0 0; + width: 6px; + } + + .MainTab.vuuTab:hover:not(.vuuTab-selected):before{ + background-color: #F37880; + content: ''; + position: absolute; + height: 100%; + left:0; + top:0; + border-radius: 6px 0 0 0; + width: 6px; + } + + .vuuTab.MainTab .vuuTab-main { + background-color: transparent; + font-weight: 700; + height: 29px; + padding: 0 24px; + } diff --git a/vuu-ui/sample-apps/app-vuu-example/src/App.tsx b/vuu-ui/sample-apps/app-vuu-example/src/App.tsx index 91562ab06..332e584be 100644 --- a/vuu-ui/sample-apps/app-vuu-example/src/App.tsx +++ b/vuu-ui/sample-apps/app-vuu-example/src/App.tsx @@ -1,35 +1,28 @@ -import { hasAction, MenuRpcResponse, TableSchema } from "@finos/vuu-data"; -import { RpcResponseHandler, useVuuTables } from "@finos/vuu-data-react"; -import { Dialog } from "@finos/vuu-popups"; +import { ContextMenuProvider, useDialog } from "@finos/vuu-popups"; import { - Feature, - SessionEditingForm, + LeftNav, Shell, ShellContextProvider, ShellProps, - ThemeProvider, VuuUser, } from "@finos/vuu-shell"; -import { ReactElement, useCallback, useRef, useState } from "react"; -import { AppSidePanel } from "./app-sidepanel"; import { getDefaultColumnConfig } from "./columnMetaData"; -import { getFormConfig } from "./session-editing"; import { createPlaceholder } from "./createPlaceholder"; +import { useFeatures } from "./useFeatures"; +import { + ColumnSettingsPanel, + TableSettingsPanel, +} from "@finos/vuu-table-extras"; +import { + registerComponent, + useLayoutContextMenuItems, +} from "@finos/vuu-layout"; import "./App.css"; -// Because we do not render the AppSidePanel directly, the css will not be included in bundle. -import "./app-sidepanel/AppSidePanel.css"; -import { VuuTable } from "@finos/vuu-protocol-types"; - -const defaultWebsocketUrl = `wss://${location.hostname}:8090/websocket`; -const { websocketUrl: serverUrl = defaultWebsocketUrl, features } = - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - await vuuConfig; +import { useRpcResponseHandler } from "./useRpcResponseHandler"; -//TODO how do we separate this from the feature -const vuuBlotterUrl = "./feature-vuu-table/index.js"; -// const vuuBlotterUrl = "./feature-vuu-table/index.js"; +registerComponent("ColumnSettings", ColumnSettingsPanel, "view"); +registerComponent("TableSettings", TableSettingsPanel, "view"); // createNewChild is used when we add a new Tab to Stack const layoutProps: ShellProps["LayoutProps"] = { @@ -37,83 +30,52 @@ const layoutProps: ShellProps["LayoutProps"] = { pathToDropTarget: "#main-tabs.ACTIVE_CHILD", }; -const withTable = (action: unknown): action is { table: VuuTable } => - action !== null && typeof action === "object" && "table" in action; +const defaultWebsocketUrl = `wss://${location.hostname}:8090/websocket`; +const { + websocketUrl: serverUrl = defaultWebsocketUrl, + features: configuredFeatures, +} = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + await vuuConfig; export const App = ({ user }: { user: VuuUser }) => { - const dialogTitleRef = useRef(""); - const [dialogContent, setDialogContent] = useState(); - const handleClose = () => { - setDialogContent(undefined); - }; + const [features, tableFeatures] = useFeatures({ + features: configuredFeatures, + }); - const tables = useVuuTables(); - - const handleRpcResponse: RpcResponseHandler = useCallback( - (response) => { - if ( - hasAction(response) && - typeof response.action === "object" && - response.action !== null && - "type" in response.action && - response.action?.type === "OPEN_DIALOG_ACTION" - ) { - const { tableSchema } = response.action as unknown as { - tableSchema: TableSchema; - }; - if (tableSchema) { - const formConfig = getFormConfig(response as MenuRpcResponse); - dialogTitleRef.current = formConfig.config.title; - setDialogContent( - - ); - } else if ( - withTable(response.action) && - tables && - response.action.table - ) { - const schema = tables.get(response.action.table.table); - if (schema) { - // If we already have this table open in this viewport, ignore - setDialogContent( - - ); - } - } - } else { - console.warn(`App, handleServiceRequest ${JSON.stringify(response)}`); - } - }, - [tables] - ); + const { dialog, setDialogState } = useDialog(); + const { handleRpcResponse } = useRpcResponseHandler(setDialogState); + const { buildMenuOptions, handleMenuAction } = + useLayoutContextMenuItems(setDialogState); // TODO get Context from Shell return ( - - + + } + leftSidePanelLayout="full-height" + leftSidePanel={ + + } + saveUrl="https://localhost:8443/api/vui" serverUrl={serverUrl} user={user} > - - {dialogContent} - + {dialog} - - + + ); }; diff --git a/vuu-ui/sample-apps/app-vuu-example/src/app-sidepanel/AppSidePanel.css b/vuu-ui/sample-apps/app-vuu-example/src/app-sidepanel/AppSidePanel.css deleted file mode 100644 index c6028d563..000000000 --- a/vuu-ui/sample-apps/app-vuu-example/src/app-sidepanel/AppSidePanel.css +++ /dev/null @@ -1,5 +0,0 @@ - -.vuuFeatureDropdown { - border-bottom: solid 1px var(--salt-container-primary-borderColor); - margin-bottom: var(--salt-space-unit); -} \ No newline at end of file diff --git a/vuu-ui/sample-apps/app-vuu-example/src/app-sidepanel/AppSidePanel.tsx b/vuu-ui/sample-apps/app-vuu-example/src/app-sidepanel/AppSidePanel.tsx deleted file mode 100644 index 32e884ba2..000000000 --- a/vuu-ui/sample-apps/app-vuu-example/src/app-sidepanel/AppSidePanel.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import { byModule, TableSchema } from "@finos/vuu-data"; -import { Palette, PaletteItem, ViewProps } from "@finos/vuu-layout"; -import { Feature, Features } from "@finos/vuu-shell"; -import { - Accordion, - AccordionGroup, - AccordionHeader, - AccordionPanel, -} from "@salt-ds/core"; -import { Dropdown, SelectionChangeHandler } from "@salt-ds/lab"; -import cx from "classnames"; -import { ReactElement, useMemo, useState } from "react"; - -import "./AppSidePanel.css"; - -const NO_FEATURES: Features = {}; -const NULL_FEATURE = {}; -export interface AppSidePanelProps { - features?: Features; - tables?: Map; - ViewProps?: Partial; -} - -type FeatureDescriptor = { - className: string; - css: string; - js: string; - name: string; - title: string; -}; - -const capitalize = (text: string) => - text.length === 0 ? "" : text[0].toUpperCase() + text.slice(1); - -const regexp_worfify = /(? { - const [firstWord, ...rest] = text.split(regexp_worfify); - return `${capitalize(firstWord)} ${rest.join(" ")}`; -}; - -const classBase = "vuuAppSidePanel"; - -export const AppSidePanel = ({ - features = NO_FEATURES, - tables, - ViewProps, -}: AppSidePanelProps) => { - const gridFeatures = useMemo( - () => - Object.entries(features).map(([featureName, { title, url, css }]) => { - return { - className: featureName, - css, - js: url, - name: featureName, - title, - } as FeatureDescriptor; - }), - [features] - ); - - const [selectedFeature, setSelectedFeature] = useState( - gridFeatures[0] ?? NULL_FEATURE - ); - const handleSelectFeature: SelectionChangeHandler = (event, item) => { - const feature = gridFeatures.find((f) => f.title === item); - if (feature) { - setSelectedFeature(feature); - } - }; - - const paletteItems = useMemo(() => { - return tables === undefined - ? [] - : Array.from(tables.values()) - .sort(byModule) - .map((schema) => { - const { className, css, js } = selectedFeature; - return { - component: ( - - ), - id: schema.table.table, - label: `${schema.table.module} ${wordify(schema.table.table)}`, - }; - }); - }, [selectedFeature, tables]); - - const featureSelection = (): ReactElement => { - const featureNames = gridFeatures.map((f) => f.title); - if (featureNames.length === 1) { - return
{featureNames[0]}
; - } else { - return ( - - className="vuuFeatureDropdown" - fullWidth - onSelectionChange={handleSelectFeature} - selected={selectedFeature?.title} - source={featureNames} - /> - ); - } - }; - - return ( -
- - - My Layouts - - - - Vuu Tables - - <> - {featureSelection()} - - {paletteItems.map((spec) => ( - - {spec.component} - - ))} - - - - - - Layout Templates - - - -
- ); -}; diff --git a/vuu-ui/sample-apps/app-vuu-example/src/app-sidepanel/index.ts b/vuu-ui/sample-apps/app-vuu-example/src/app-sidepanel/index.ts deleted file mode 100644 index fa3d1a25a..000000000 --- a/vuu-ui/sample-apps/app-vuu-example/src/app-sidepanel/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./AppSidePanel"; diff --git a/vuu-ui/sample-apps/app-vuu-example/src/columnMetaData.ts b/vuu-ui/sample-apps/app-vuu-example/src/columnMetaData.ts index 7a8016780..9fe83b886 100644 --- a/vuu-ui/sample-apps/app-vuu-example/src/columnMetaData.ts +++ b/vuu-ui/sample-apps/app-vuu-example/src/columnMetaData.ts @@ -57,7 +57,7 @@ const columnMetaData: { [key: string]: Partial } = { label: "Ask", type: { name: "number", - renderer: { name: "background", flashStyle: "arrow-bg" }, + renderer: { name: "vuu.price-move-background", flashStyle: "arrow-bg" }, formatting: { decimals: 2, zeroPad: true }, }, aggregate: Average, @@ -90,7 +90,7 @@ const columnMetaData: { [key: string]: Partial } = { name: "bid", type: { name: "number", - renderer: { name: "background", flashStyle: "arrow-bg" }, + renderer: { name: "vuu.price-move-background", flashStyle: "arrow-bg" }, formatting: { decimals: 2, zeroPad: true }, }, aggregate: Average, @@ -100,6 +100,8 @@ const columnMetaData: { [key: string]: Partial } = { name: "bidSize", type: { name: "number", + renderer: { name: "vuu.price-move-background", flashStyle: "bg-only" }, + formatting: { decimals: 2, zeroPad: true }, }, aggregate: Average, }, diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/src/useFeatures.ts b/vuu-ui/sample-apps/app-vuu-example/src/useFeatures.ts similarity index 100% rename from vuu-ui/sample-apps/app-vuu-basket-trader/src/useFeatures.ts rename to vuu-ui/sample-apps/app-vuu-example/src/useFeatures.ts diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/src/useRpcResponseHandler.tsx b/vuu-ui/sample-apps/app-vuu-example/src/useRpcResponseHandler.tsx similarity index 97% rename from vuu-ui/sample-apps/app-vuu-basket-trader/src/useRpcResponseHandler.tsx rename to vuu-ui/sample-apps/app-vuu-example/src/useRpcResponseHandler.tsx index f21679dcd..15b6292b0 100644 --- a/vuu-ui/sample-apps/app-vuu-basket-trader/src/useRpcResponseHandler.tsx +++ b/vuu-ui/sample-apps/app-vuu-example/src/useRpcResponseHandler.tsx @@ -2,7 +2,7 @@ import { RpcResponseHandler, useVuuTables } from "@finos/vuu-data-react"; import { hasAction, MenuRpcResponse, TableSchema } from "@finos/vuu-data"; import { useCallback } from "react"; import { getFormConfig } from "./session-editing"; -import { Feature, SessionEditingForm } from "packages/vuu-shell/src"; +import { Feature, SessionEditingForm } from "@finos/vuu-shell"; import { VuuTable } from "@finos/vuu-protocol-types"; import { SetDialog } from "@finos/vuu-popups"; diff --git a/vuu-ui/sample-apps/feature-basket-trading/index.ts b/vuu-ui/sample-apps/feature-basket-trading/index.ts index 683bbe19a..959fa2995 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/index.ts +++ b/vuu-ui/sample-apps/feature-basket-trading/index.ts @@ -6,3 +6,5 @@ export type { basketDataSourceKey } from "./src/useBasketTradingDatasources"; export { BasketSelector } from "./src/basket-selector"; export { BasketToolbar } from "./src/basket-toolbar"; export { NewBasketPanel } from "./src/new-basket-panel"; + +export { type Basket } from "./src/useBasketTrading"; diff --git a/vuu-ui/sample-apps/feature-basket-trading/package.json b/vuu-ui/sample-apps/feature-basket-trading/package.json index e8c343ac7..1048ffa0d 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/package.json +++ b/vuu-ui/sample-apps/feature-basket-trading/package.json @@ -5,7 +5,7 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "node ../../scripts/build-feature.mjs", - "start": "serve -p 5002 ../../deployed_apps/app-vuu-basket-trader" + "start": "serve -p 5002 ../../deployed_apps/app-vuu-example" }, "private": true, "keywords": [], @@ -53,7 +53,7 @@ }, { "module": "BASKET", - "table": "basketTradingConstituent" + "table": "basketTradingConstituentJoin" }, { "module": "SIMUL", 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 5e69cf41c..d40c26a70 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/VuuBasketTradingFeature.tsx +++ b/vuu-ui/sample-apps/feature-basket-trading/src/VuuBasketTradingFeature.tsx @@ -1,13 +1,9 @@ import { TableSchema } from "@finos/vuu-data"; import { FlexboxLayout, Stack } from "@finos/vuu-layout"; import { ContextMenuProvider } from "@finos/vuu-popups"; -import { useEffect, useMemo, useState } from "react"; -import { BasketSelectorProps } from "./basket-selector"; import { BasketTableEdit } from "./basket-table-edit"; import { BasketTableLive } from "./basket-table-live"; import { BasketToolbar } from "./basket-toolbar"; -import { useBasketTabMenu } from "./useBasketTabMenu"; -import { useBasketTradingDataSources } from "./useBasketTradingDatasources"; import "./VuuBasketTradingFeature.css"; import { EmptyBasketsPanel } from "./empty-baskets-panel"; @@ -21,7 +17,7 @@ const basketStatus: [BasketStatus, BasketStatus] = ["design", "on-market"]; export interface BasketTradingFeatureProps { basketSchema: TableSchema; basketTradingSchema: TableSchema; - basketTradingConstituentSchema: TableSchema; + basketTradingConstituentJoinSchema: TableSchema; instrumentsSchema: TableSchema; } @@ -29,111 +25,52 @@ const VuuBasketTradingFeature = (props: BasketTradingFeatureProps) => { const { basketSchema, basketTradingSchema, - basketTradingConstituentSchema, + basketTradingConstituentJoinSchema, instrumentsSchema, } = props; - const basketInstanceId = "steve-00001"; - const { activeTabIndex, - dataSourceBasket, - dataSourceBasketTrading, - dataSourceBasketTradingControl, - dataSourceBasketTradingSearch, - dataSourceBasketTradingConstituent, - dataSourceInstruments, + basket, + basketCount, + basketSelectorProps, + contextMenuProps, + dataSourceBasketTradingConstituentJoin, + dialog, + onClickAddBasket, + onCommitBasketChange, onSendToMarket, onTakeOffMarket, - } = useBasketTradingDataSources({ - basketInstanceId, + } = useBasketTrading({ basketSchema, basketTradingSchema, - basketTradingConstituentSchema, + basketTradingConstituentJoinSchema, instrumentsSchema, }); - const [basketCount, setBasketCount] = useState(-1); - useMemo(() => { - dataSourceBasketTradingControl.subscribe( - { - range: { from: 0, to: 100 }, - }, - (message) => { - console.log("message from dataSourceTradingControl", { - message, - }); - if (message.size) { - setBasketCount(message.size); - } - if (message.rows) { - console.table(message.rows); - } - } - ); - - // TEMP server is notsending TABLE_ROWS if size is zero - setTimeout(() => { - setBasketCount((count) => (count === -1 ? 0 : count)); - }, 1000); - }, [dataSourceBasketTradingControl]); - useEffect(() => { - // dataSourceBasketDesign.resume?.(); - return () => { - dataSourceBasketTradingControl.unsubscribe?.(); - }; - }, [dataSourceBasketTradingControl]); - - const [buildMenuOptions, handleMenuAction] = useBasketTabMenu({ - dataSourceInstruments, - }); - - const { dialog, handleAddBasket } = useBasketTrading({ - basketSchema, - dataSourceBasket, - }); - - // useMemo(() => { - // dataSourceBasketTrading.filter = { - // filter: `basketId = "${basketId}"`, - // }; - // }, [basketId, dataSourceBasketTrading]); - - const basketSelectorProps = useMemo( - () => ({ - basketInstanceId, - dataSourceBasketTrading, - dataSourceBasketTradingSearch: dataSourceBasketTradingSearch, - onClickAddBasket: handleAddBasket, - }), - [dataSourceBasketTrading, dataSourceBasketTradingSearch, handleAddBasket] - ); - if (basketCount === -1) { // TODO loading return null; } else if (basketCount === 0) { return ( <> - + {dialog} ); } return ( - + @@ -146,13 +83,13 @@ const VuuBasketTradingFeature = (props: BasketTradingFeatureProps) => { diff --git a/vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/BasketSelector.tsx b/vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/BasketSelector.tsx index 81b5381a3..15bc914ec 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/BasketSelector.tsx +++ b/vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/BasketSelector.tsx @@ -3,47 +3,48 @@ import { Button } from "@salt-ds/core"; import { DataSource } from "@finos/vuu-data"; import { useId } from "@finos/vuu-layout"; import { DropdownBaseProps, InstrumentSearch } from "@finos/vuu-ui-controls"; -import { HTMLAttributes, useCallback, useRef } from "react"; +import { HTMLAttributes, useRef } from "react"; import "./BasketSelector.css"; import { useBasketSelector } from "./useBasketSelector"; +import { Basket } from "../useBasketTrading"; const classBase = "vuuBasketSelector"; export interface BasketSelectorProps extends Pick, HTMLAttributes { + basket?: Basket; basketInstanceId?: string; - dataSourceBasketTrading: DataSource; dataSourceBasketTradingSearch: DataSource; label?: string; onClickAddBasket: () => void; + onSelectBasket: (basketInstanceId: string) => void; } export const BasketSelector = ({ + basket, basketInstanceId, - dataSourceBasketTrading, dataSourceBasketTradingSearch, id: idProp, isOpen: isOpenProp, - onClickAddBasket, + onClickAddBasket: onClickAddBasketProp, onOpenChange: onOpenChangeProp, + onSelectBasket, ...htmlAttributes }: BasketSelectorProps) => { const rootRef = useRef(null); const id = useId(idProp); - const { basket, isOpen, onOpenChange, tableProps } = useBasketSelector({ - basketInstanceId, - dataSourceBasketTrading, - dataSourceBasketTradingSearch, - isOpen: isOpenProp, - onOpenChange: onOpenChangeProp, - }); - - const handleClickAddBasket = useCallback(() => { - onClickAddBasket(); - }, [onClickAddBasket]); + const { isOpen, onClickAddBasket, onOpenChange, tableProps } = + useBasketSelector({ + basketInstanceId, + dataSourceBasketTradingSearch, + isOpen: isOpenProp, + onClickAddBasket: onClickAddBasketProp, + onOpenChange: onOpenChangeProp, + onSelectBasket, + }); return (
-
diff --git a/vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/useBasketSelector.ts b/vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/useBasketSelector.ts index e1734e666..805f8569d 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/useBasketSelector.ts +++ b/vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/useBasketSelector.ts @@ -1,41 +1,29 @@ import { TableProps, TableRowClickHandler } from "@finos/vuu-table"; -import { buildColumnMap, ColumnMap } from "@finos/vuu-utils"; -import { SubscribeCallback } from "@finos/vuu-data"; -import { VuuDataRow } from "@finos/vuu-protocol-types"; +import { buildColumnMap } from "@finos/vuu-utils"; import { OpenChangeHandler, useControlled } from "@finos/vuu-ui-controls"; -import { useCallback, useMemo, useRef, useState } from "react"; +import { useCallback, useMemo } from "react"; import { BasketSelectorProps } from "./BasketSelector"; import { BasketSelectorRow } from "./BasketSelectorRow"; -export class Basket { - basketId: string; - basketName: string; - fxRateToUsd: number; - - constructor(data: VuuDataRow, columnMap: ColumnMap) { - this.basketId = data[columnMap.basketId] as string; - this.basketName = data[columnMap.basketName] as string; - this.fxRateToUsd = data[columnMap.fxRateToUsd] as number; - } -} - export type BasketSelectorHookProps = Pick< BasketSelectorProps, | "basketInstanceId" - | "dataSourceBasketTrading" | "dataSourceBasketTradingSearch" | "defaultIsOpen" | "isOpen" | "onOpenChange" + | "onClickAddBasket" + | "onSelectBasket" >; export const useBasketSelector = ({ - basketInstanceId: basketInstanceIdProp, - dataSourceBasketTrading, + basketInstanceId, dataSourceBasketTradingSearch, defaultIsOpen, isOpen: isOpenProp, + onClickAddBasket, onOpenChange, + onSelectBasket, }: BasketSelectorHookProps) => { const [isOpen, setIsOpen] = useControlled({ controlled: isOpenProp, @@ -43,14 +31,9 @@ export const useBasketSelector = ({ name: "useDropdownList", }); - const [basketInstanceId, setBasketInstanceId] = useState( - basketInstanceIdProp - ); - const [basket, setBasket] = useState(); - const columnMap = useMemo( - () => buildColumnMap(dataSourceBasketTrading.columns), - [dataSourceBasketTrading.columns] + () => buildColumnMap(dataSourceBasketTradingSearch.columns), + [dataSourceBasketTradingSearch.columns] ); const handleOpenChange = useCallback( @@ -58,7 +41,8 @@ export const useBasketSelector = ({ setIsOpen(open); onOpenChange?.(open, closeReason); if (open === false) { - dataSourceBasketTradingSearch.unsubscribe(); + console.log(`%cdisable basketSearch`, "color:red;font-weight:bold;"); + dataSourceBasketTradingSearch.disable?.(); } }, [dataSourceBasketTradingSearch, onOpenChange, setIsOpen] @@ -66,34 +50,17 @@ export const useBasketSelector = ({ const handleRowClick = useCallback( (row) => { - const instanceIdId = row[columnMap.instanceId] as string; - setBasketInstanceId(instanceIdId); - setIsOpen(false); - dataSourceBasketTrading.filter = { - filter: `instanceId = "${basketInstanceId}"`, - }; + const instanceId = row[columnMap.instanceId] as string; + handleOpenChange(false, "select"); + onSelectBasket?.(instanceId); }, - [basketInstanceId, columnMap.instanceId, dataSourceBasketTrading, setIsOpen] + [columnMap.instanceId, handleOpenChange, onSelectBasket] ); - const handleData = useCallback( - (message) => { - if (message.type === "viewport-update" && message.rows?.length === 1) { - setBasket(new Basket(message.rows[0], columnMap)); - } - }, - [columnMap] - ); - - useMemo(() => { - dataSourceBasketTrading.subscribe( - { - range: { from: 0, to: 1 }, - filter: { filter: `instanceId = "${basketInstanceId}"` }, - }, - handleData - ); - }, [dataSourceBasketTrading, basketInstanceId, handleData]); + const handleClickAddBasket = useCallback(() => { + handleOpenChange(false, "script"); + onClickAddBasket(); + }, [handleOpenChange, onClickAddBasket]); const tableProps: Partial = useMemo( () => ({ @@ -128,13 +95,14 @@ export const useBasketSelector = ({ }, onRowClick: handleRowClick, rowHeight: 47, + selectedKeys: basketInstanceId ? [basketInstanceId] : undefined, }), - [handleRowClick] + [basketInstanceId, handleRowClick] ); return { - basket, isOpen, + onClickAddBasket: handleClickAddBasket, onOpenChange: handleOpenChange, tableProps, }; 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 1b3652a0f..d12b1b48b 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 @@ -2,6 +2,7 @@ import { TableSchema } from "@finos/vuu-data"; import { ColumnDescriptor, TableConfig } from "@finos/vuu-datagrid-types"; import { TableNext, TableProps } from "@finos/vuu-table"; import { useMemo } from "react"; +import columns from "./basketConstituentEditColumns"; import "./BasketTableEdit.css"; @@ -23,27 +24,6 @@ const labels: { [key: string]: string } = { const applyColumnDefaults = (tableSchema: TableSchema) => tableSchema.columns.map((column) => { switch (column.name) { - case "ric": - return { - ...column, - label: "Ticker", - pin: "left", - }; - case "ask": - case "bid": - case "last": - return { - ...column, - label: labels[column.name] ?? column.name, - type: { - name: "number", - formatting: { - alignOnDecimals: true, - decimals: 2, - zeroPad: true, - }, - }, - } as ColumnDescriptor; case "limitPrice": return { ...column, @@ -61,26 +41,6 @@ const applyColumnDefaults = (tableSchema: TableSchema) => }, }, } as ColumnDescriptor; - case "priceStrategy": - return { - ...column, - editable: true, - label: labels[column.name] ?? column.name, - type: { - name: "string", - renderer: { - name: "dropdown-cell", - // TODO how do we get these - values: [ - "Peg to near touch", - "Limit", - "Strategy 3", - "Strategy 4", - "Strategy 5", - ], - }, - }, - }; case "quantity": return { ...column, @@ -119,20 +79,26 @@ const applyColumnDefaults = (tableSchema: TableSchema) => }); export const BasketTableEdit = ({ + dataSource, tableSchema, ...props }: BasketTableEditProps) => { + useMemo(() => { + dataSource.columns = columns.map((col) => col.name); + }, [dataSource]); + const tableConfig = useMemo( () => ({ - columns: applyColumnDefaults(tableSchema), + columns, rowSeparators: true, }), - [tableSchema] + [] ); return (