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 000079217..c919eccf2 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 @@ -16,6 +16,8 @@ import { buildColumnMap, ColumnMap, EventEmitter, + getAddedItems, + getMissingItems, getSelectionStatus, KeySet, logger, @@ -57,7 +59,6 @@ export interface ArrayDataSourceConstructorProps data: Array; rangeChangeRowset?: "delta" | "full"; } - const { debug } = logger("ArrayDataSource"); const { RENDER_IDX, SELECTED } = metadataKeys; @@ -148,6 +149,10 @@ export class ArrayDataSource }: ArrayDataSourceConstructorProps) { super(); + console.log(`ArrayDataSource`, { + columnDescriptors, + }); + if (!data || !columnDescriptors) { throw Error( "ArrayDataSource constructor called without data or without columnDescriptors" @@ -293,6 +298,10 @@ export class ArrayDataSource return this.processedData ?? this.#data; } + get table() { + return this.tableSchema.table; + } + get config() { return this.#config; } @@ -434,6 +443,17 @@ export class ArrayDataSource } set columns(columns: string[]) { + const addedColumns = getAddedItems(this.config.columns, columns); + if (addedColumns.length > 0) { + const columnsWithoutDescriptors = getMissingItems( + this.columnDescriptors, + addedColumns, + (col) => col.name + ); + console.log(`columnsWithoutDescriptors`, { + columnsWithoutDescriptors, + }); + } this.config = { ...this.#config, columns, diff --git a/vuu-ui/packages/vuu-data/src/data-source.ts b/vuu-ui/packages/vuu-data/src/data-source.ts index ca8ac416b..16b878d1d 100644 --- a/vuu-ui/packages/vuu-data/src/data-source.ts +++ b/vuu-ui/packages/vuu-data/src/data-source.ts @@ -212,7 +212,7 @@ const equivalentColumns: DataConfigPredicate = ( (cols1 === undefined && cols2?.length === 0) || (cols2 === undefined && cols1?.length === 0); -const columnsChanged: DataConfigPredicate = (config, newConfig) => { +export const columnsChanged: DataConfigPredicate = (config, newConfig) => { const { columns: cols1 } = config; const { columns: cols2 } = newConfig; @@ -510,6 +510,7 @@ export interface DataSource extends EventEmitter { props: SubscribeProps, callback: SubscribeCallback ) => Promise; + table?: VuuTable; title?: string; unsubscribe: () => void; viewport?: string; diff --git a/vuu-ui/packages/vuu-data/src/inlined-worker.js b/vuu-ui/packages/vuu-data/src/inlined-worker.js index 461c16348..c9900c37e 100644 --- a/vuu-ui/packages/vuu-data/src/inlined-worker.js +++ b/vuu-ui/packages/vuu-data/src/inlined-worker.js @@ -1,2553 +1,8 @@ export const workerSourceCode = ` -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); - } - 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 +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 keys: \${this.toDebugString()} free : \${this.free.join(",")} - \`); - 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) { - makeConnection( - connection.url, - connection.protocol, - connection[connectionCallback], - connection - ); -} -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.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); - console.log(\`elapsed time = \${elapsedTime}\`); - 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) { - debug4 == null ? void 0 : debug4(\`handleMessageFromClient: \${message.type}\`); - 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_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" }); + \`),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"}); `; \ 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 3f032bea7..b7879cf58 100644 --- a/vuu-ui/packages/vuu-data/src/json-data-source.ts +++ b/vuu-ui/packages/vuu-data/src/json-data-source.ts @@ -92,6 +92,7 @@ export class JsonDataSource } [this.columnDescriptors, this.#data] = jsonToDataSourceRows(data); + this.visibleRows = this.#data .filter((row) => row[DEPTH] === 0) .map((row, index) => @@ -133,8 +134,6 @@ export class JsonDataSource ) { this.clientCallback = callback; - console.log(`subscribe range ${range?.from} ${range?.to}`); - if (aggregations) { this.#aggregations = aggregations; } @@ -320,6 +319,10 @@ export class JsonDataSource this.#aggregations = aggregations; } + set data(data: JsonData) { + console.log(`set JsonDataSource data`); + } + get sort() { return this.#sort; } diff --git a/vuu-ui/packages/vuu-data/src/server-proxy/array-backed-moving-window.ts b/vuu-ui/packages/vuu-data/src/server-proxy/array-backed-moving-window.ts index 682a332a1..40cebe351 100644 --- a/vuu-ui/packages/vuu-data/src/server-proxy/array-backed-moving-window.ts +++ b/vuu-ui/packages/vuu-data/src/server-proxy/array-backed-moving-window.ts @@ -12,6 +12,10 @@ function dataIsUnchanged(newRow: VuuRow, existingRow?: VuuRow) { return false; } + if (existingRow.data.length !== newRow.data.length) { + return false; + } + if (existingRow.sel !== newRow.sel) { return false; } 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 80b7c2a37..3858fcab2 100644 --- a/vuu-ui/packages/vuu-data/src/server-proxy/viewport.ts +++ b/vuu-ui/packages/vuu-data/src/server-proxy/viewport.ts @@ -918,7 +918,6 @@ export class Viewport { // alleviate pressure on UI DataTable. private shouldThrottleMessage = (mode: DataUpdateMode) => { const elapsedTime = this.setLastUpdate(mode); - console.log(`elapsed time = ${elapsedTime}`); return ( mode === "size-only" && elapsedTime > 0 && diff --git a/vuu-ui/packages/vuu-data/src/websocket-connection.ts b/vuu-ui/packages/vuu-data/src/websocket-connection.ts index e0a8a5976..604138cbd 100644 --- a/vuu-ui/packages/vuu-data/src/websocket-connection.ts +++ b/vuu-ui/packages/vuu-data/src/websocket-connection.ts @@ -69,12 +69,14 @@ export async function connect( async function reconnect(connection: WebsocketConnection) { //TODO it's not enough to reconnect with a new websocket, we have to log back in as well - makeConnection( - connection.url, - connection.protocol, - connection[connectionCallback], - connection - ); + // Temp don't try to reconnect at all until better interop with a proxy is implemented + // makeConnection( + // connection.url, + // connection.protocol, + // connection[connectionCallback], + // connection + // ); + throw Error("connection broken"); } async function makeConnection( diff --git a/vuu-ui/packages/vuu-datagrid-types/index.d.ts b/vuu-ui/packages/vuu-datagrid-types/index.d.ts index 503c026b9..1f6cc94dd 100644 --- a/vuu-ui/packages/vuu-datagrid-types/index.d.ts +++ b/vuu-ui/packages/vuu-datagrid-types/index.d.ts @@ -129,7 +129,6 @@ export interface ColumnDescriptor { align?: ColumnAlignment; className?: string; editable?: boolean; - expression?: string; flex?: number; /** Optional additional level(s) of heading to display above label. diff --git a/vuu-ui/packages/vuu-datatable/src/json-table/JsonTable.tsx b/vuu-ui/packages/vuu-datatable/src/json-table/JsonTable.tsx index 373b1459a..5c128e1d2 100644 --- a/vuu-ui/packages/vuu-datatable/src/json-table/JsonTable.tsx +++ b/vuu-ui/packages/vuu-datatable/src/json-table/JsonTable.tsx @@ -2,7 +2,7 @@ import { TableProps } from "@finos/vuu-table"; import { JsonData } from "@finos/vuu-utils"; import { TableNext } from "@finos/vuu-table"; import { JsonDataSource } from "@finos/vuu-data"; -import { useMemo } from "react"; +import { useEffect, useMemo, useRef } from "react"; import { TableConfig } from "@finos/vuu-datagrid-types"; export interface JsonTableProps @@ -16,19 +16,39 @@ export interface JsonTableProps export const JsonTable = ({ config, - source = { "": "" }, + source: sourceProp = { "": "" }, ...tableProps }: JsonTableProps) => { - const [dataSource, tableConfig] = useMemo< - [JsonDataSource, TableConfig] - >(() => { - const ds = new JsonDataSource({ - data: source, + const sourceRef = useRef(sourceProp); + const dataSourceRef = useRef(); + useMemo(() => { + dataSourceRef.current = new JsonDataSource({ + data: sourceRef.current, }); + }, []); + + const tableConfig = useMemo(() => { + return { + ...config, + columns: dataSourceRef.current?.columnDescriptors ?? [], + }; + }, [config]); + + useEffect(() => { + if (dataSourceRef.current) { + dataSourceRef.current.data = sourceProp; + } + }, [sourceProp]); + + if (dataSourceRef.current === undefined) { + return null; + } - return [ds, { ...config, columns: ds.columnDescriptors }]; - }, [config, source]); return ( - + ); }; diff --git a/vuu-ui/packages/vuu-filters/src/filter-input/FilterInput.css b/vuu-ui/packages/vuu-filters/src/filter-input/FilterInput.css index c234603a3..f62bd786f 100644 --- a/vuu-ui/packages/vuu-filters/src/filter-input/FilterInput.css +++ b/vuu-ui/packages/vuu-filters/src/filter-input/FilterInput.css @@ -22,7 +22,7 @@ --vuuFilterEditor-suggestion-selectedBackground: var(--salt-selectable-background-selected); --vuuFilterEditor-suggestion-selectedColor: var(--vuuFilterEditor-color); --vuuFilterEditor-suggestion-height: 24px; - --vuuFilterEditor-variableColor: blue; + --vuuFilterEditor-variableColor: var(--vuu-color_purple-10); align-items: center; border-color: var(--salt-container-secondary-borderColor); diff --git a/vuu-ui/packages/vuu-icons/index.css b/vuu-ui/packages/vuu-icons/index.css index d1fa693fc..a631f00b8 100644 --- a/vuu-ui/packages/vuu-icons/index.css +++ b/vuu-ui/packages/vuu-icons/index.css @@ -55,6 +55,7 @@ --vuu-svg-cross: 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,'); @@ -65,7 +66,9 @@ --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; @@ -176,6 +179,10 @@ span[data-icon] { --vuu-icon-svg: var(--vuu-svg-alert-circle); } +[data-icon='edit'] { + --vuu-icon-svg: var(--vuu-svg-edit); +} + [data-icon='filter'] { --vuu-icon-svg: var(--svg-filter); } 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 54fbf9d54..88371f969 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: Omit, layout: LayoutJSON) => Promise; + createLayout: ( + metadata: Omit, + 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: Omit, layout: LayoutJSON) => Promise; + updateLayout: ( + id: string, + metadata: Omit, + 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/layout-persistence/LocalLayoutPersistenceManager.ts b/vuu-ui/packages/vuu-layout/src/layout-persistence/LocalLayoutPersistenceManager.ts index ec62b70d7..c1754ed1d 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-persistence/LocalLayoutPersistenceManager.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-persistence/LocalLayoutPersistenceManager.ts @@ -9,15 +9,17 @@ const metadataSaveLocation = "layouts/metadata"; const layoutsSaveLocation = "layouts/layouts"; export class LocalLayoutPersistenceManager implements LayoutPersistenceManager { + #urlKey = "api/vui"; + constructor(urlKey?: string) { + if (urlKey) { + this.#urlKey = urlKey; + } + } createLayout( metadata: Omit, layout: LayoutJSON ): Promise { return new Promise((resolve) => { - console.log( - `Saving layout as ${metadata.name} to group ${metadata.group}...` - ); - Promise.all([this.loadLayouts(), this.loadMetadata()]).then( ([existingLayouts, existingMetadata]) => { const id = getUniqueId(); @@ -92,14 +94,11 @@ export class LocalLayoutPersistenceManager implements LayoutPersistenceManager { } loadApplicationLayout(): Promise { - console.log("loadApplicationLAyout"); return new Promise((resolve) => { - const applicationLayout = getLocalEntity("api/vui"); + const applicationLayout = getLocalEntity(this.#urlKey); if (applicationLayout) { - console.log(applicationLayout); resolve(applicationLayout); } else { - console.log(defaultLayout); resolve(defaultLayout); } }); @@ -107,7 +106,7 @@ export class LocalLayoutPersistenceManager implements LayoutPersistenceManager { saveApplicationLayout(layout: LayoutJSON): Promise { return new Promise((resolve, reject) => { - const savedLayout = saveLocalEntity("api/vui", layout); + const savedLayout = saveLocalEntity(this.#urlKey, layout); if (savedLayout) { resolve(); } else { diff --git a/vuu-ui/packages/vuu-shell/src/shell.tsx b/vuu-ui/packages/vuu-shell/src/shell.tsx index ac903cf4b..94cee39dc 100644 --- a/vuu-ui/packages/vuu-shell/src/shell.tsx +++ b/vuu-ui/packages/vuu-shell/src/shell.tsx @@ -67,7 +67,6 @@ export const Shell = ({ const handleLayoutChange = useCallback( (layout, layoutChangeReason) => { try { - console.log(`handle layout changed ${layoutChangeReason}`); saveApplicationLayout(layout); // saveLayoutConfig(layout); } catch { diff --git a/vuu-ui/packages/vuu-table-extras/src/column-expression-input/ColumnExpressionInput.css b/vuu-ui/packages/vuu-table-extras/src/column-expression-input/ColumnExpressionInput.css index 790a289be..1541e5c6b 100644 --- a/vuu-ui/packages/vuu-table-extras/src/column-expression-input/ColumnExpressionInput.css +++ b/vuu-ui/packages/vuu-table-extras/src/column-expression-input/ColumnExpressionInput.css @@ -13,7 +13,7 @@ --vuuFilterEditor-suggestion-selectedColor: var(--salt-text-primary-foreground); --vuuFilterEditor-suggestion-detailColor: var(--salt-text-secondary-foreground-disabled); --vuuFilterEditor-suggestion-height: 24px; - --vuuFilterEditor-variableColor: blue; + --vuuFilterEditor-variableColor: var(--vuu-color-purple-10); align-items: center; box-sizing: border-box; diff --git a/vuu-ui/packages/vuu-table-extras/src/column-expression-input/ColumnExpressionInput.tsx b/vuu-ui/packages/vuu-table-extras/src/column-expression-input/ColumnExpressionInput.tsx index cc48d4862..387dc0c0b 100644 --- a/vuu-ui/packages/vuu-table-extras/src/column-expression-input/ColumnExpressionInput.tsx +++ b/vuu-ui/packages/vuu-table-extras/src/column-expression-input/ColumnExpressionInput.tsx @@ -1,4 +1,4 @@ -import { HTMLAttributes, memo } from "react"; +import { FocusEventHandler, HTMLAttributes, memo, useCallback } from "react"; import { ColumnDefinitionExpression } from "./column-language-parser"; import { ExpressionSuggestionConsumer, @@ -9,14 +9,16 @@ import "./ColumnExpressionInput.css"; const classBase = "vuuColumnExpressionInput"; +export type ColumnExpressionSubmitHandler = ( + source: string, + expression: ColumnDefinitionExpression | undefined +) => void; + export interface ColumnExpressionInputProps extends ExpressionSuggestionConsumer, Omit, "onChange"> { onChange?: (source: string) => void; - onSubmitExpression?: ( - source: string, - expression: ColumnDefinitionExpression | undefined - ) => void; + onSubmitExpression?: ColumnExpressionSubmitHandler; source?: string; } @@ -27,14 +29,14 @@ export const ColumnExpressionInput = memo( source = "", suggestionProvider, }: ColumnExpressionInputProps) => { - const { editorRef } = useColumnExpressionEditor({ + const { editorRef, onBlur } = useColumnExpressionEditor({ onChange, onSubmitExpression, source, suggestionProvider, }); - return
; + return
; }, (prevProps, newProps) => { return prevProps.source === newProps.source; diff --git a/vuu-ui/packages/vuu-table-extras/src/column-expression-input/column-language-parser/ColumnExpressionLanguage.ts b/vuu-ui/packages/vuu-table-extras/src/column-expression-input/column-language-parser/ColumnExpressionLanguage.ts index 9c7964c22..f88df4adf 100644 --- a/vuu-ui/packages/vuu-table-extras/src/column-expression-input/column-language-parser/ColumnExpressionLanguage.ts +++ b/vuu-ui/packages/vuu-table-extras/src/column-expression-input/column-language-parser/ColumnExpressionLanguage.ts @@ -11,6 +11,7 @@ const columnExpressionLanguage = LRLanguage.define({ parser: parser.configure({ props: [ styleTags({ + Column: tag.attributeValue, Function: tag.variableName, String: tag.string, Or: tag.emphasis, diff --git a/vuu-ui/packages/vuu-table-extras/src/column-expression-input/highlighting.ts b/vuu-ui/packages/vuu-table-extras/src/column-expression-input/highlighting.ts index ec4b12899..a2147652d 100644 --- a/vuu-ui/packages/vuu-table-extras/src/column-expression-input/highlighting.ts +++ b/vuu-ui/packages/vuu-table-extras/src/column-expression-input/highlighting.ts @@ -5,6 +5,10 @@ import { } from "@finos/vuu-codemirror"; const myHighlightStyle = HighlightStyle.define([ + { + tag: tags.attributeValue, + color: "var(--vuuFilterEditor-variableColor);font-weight: bold", + }, { tag: tags.variableName, color: "var(--vuuFilterEditor-variableColor)" }, { tag: tags.comment, color: "green", fontStyle: "italic" }, ]); diff --git a/vuu-ui/packages/vuu-table-extras/src/column-expression-input/useColumnAutoComplete.ts b/vuu-ui/packages/vuu-table-extras/src/column-expression-input/useColumnAutoComplete.ts index 5741f80de..0dafb3446 100644 --- a/vuu-ui/packages/vuu-table-extras/src/column-expression-input/useColumnAutoComplete.ts +++ b/vuu-ui/packages/vuu-table-extras/src/column-expression-input/useColumnAutoComplete.ts @@ -139,7 +139,6 @@ const handleConditionalExpression = ( onSubmit?: () => void ) => { const lastChild = getLastChild(node, context); - console.log(`conditional expression last child ${lastChild?.name}`); switch (lastChild?.name) { case "If": return makeSuggestions(context, suggestionProvider, "expression", { @@ -200,7 +199,6 @@ export const useColumnAutoComplete = ( switch (nodeBefore.name) { case "If": { - console.log(`conditional expression If`); return makeSuggestions(context, "expression", { prefix: "( " }); } case "Condition": @@ -218,7 +216,6 @@ export const useColumnAutoComplete = ( // we need the type of the expression on the other side of the operator return makeSuggestions(context, "expression"); } - console.log(`condition last child ${lastChild?.name}`); } break; case "ConditionalExpression": @@ -311,11 +308,16 @@ export const useColumnAutoComplete = ( case "ArgList": { const functionName = getFunctionName(nodeBefore, state); const lastArgument = getLastChild(nodeBefore, context); - const prefix = lastArgument?.name === "OpenBrace" ? undefined : ","; + + const prefix = + lastArgument?.name === "OpenBrace" || lastArgument?.name === "Comma" + ? undefined + : ","; let options = await suggestionProvider.getSuggestions("expression", { functionName, }); options = prefix ? applyPrefix(options, ", ") : options; + // TODO per function check for number of arguments expected if ( lastArgument?.name !== "OpenBrace" && diff --git a/vuu-ui/packages/vuu-table-extras/src/column-expression-input/useColumnExpressionEditor.ts b/vuu-ui/packages/vuu-table-extras/src/column-expression-input/useColumnExpressionEditor.ts index 03d79646a..4b8e2db98 100644 --- a/vuu-ui/packages/vuu-table-extras/src/column-expression-input/useColumnExpressionEditor.ts +++ b/vuu-ui/packages/vuu-table-extras/src/column-expression-input/useColumnExpressionEditor.ts @@ -10,7 +10,14 @@ import { startCompletion, } from "@finos/vuu-codemirror"; import { createEl } from "@finos/vuu-utils"; -import { MutableRefObject, useEffect, useMemo, useRef } from "react"; +import { + FocusEventHandler, + MutableRefObject, + useCallback, + useEffect, + useMemo, + useRef, +} from "react"; import { columnExpressionLanguageSupport } from "./column-language-parser"; import { ColumnDefinitionExpression, @@ -102,7 +109,7 @@ export const useColumnExpressionEditor = ({ const viewRef = useRef(); const completionFn = useColumnAutoComplete(suggestionProvider, onSubmitRef); - const [createState, clearInput] = useMemo(() => { + const [createState, clearInput, submit] = useMemo(() => { const parseExpression = (): | [string, ColumnDefinitionExpression] | ["", undefined] => { @@ -165,9 +172,9 @@ export const useColumnExpressionEditor = ({ } }), // Enforces single line view - // EditorState.transactionFilter.of((tr) => - // tr.newDoc.lines > 1 ? [] : tr - // ), + EditorState.transactionFilter.of((tr) => + tr.newDoc.lines > 1 ? [] : tr + ), vuuTheme, vuuHighlighting, ], @@ -177,7 +184,7 @@ export const useColumnExpressionEditor = ({ submitExpression(); }; - return [createState, clearInput]; + return [createState, clearInput, submitExpression]; }, [completionFn, onChange, onSubmitExpression, source]); useEffect(() => { @@ -195,5 +202,9 @@ export const useColumnExpressionEditor = ({ }; }, [completionFn, createState]); - return { editorRef, clearInput }; + const handleBlur = useCallback(() => { + submit(); + }, [submit]); + + return { editorRef, clearInput, onBlur: handleBlur }; }; diff --git a/vuu-ui/packages/vuu-table-extras/src/column-expression-input/useColumnExpressionSuggestionProvider.ts b/vuu-ui/packages/vuu-table-extras/src/column-expression-input/useColumnExpressionSuggestionProvider.ts index 4aecc02ec..653ca9c1e 100644 --- a/vuu-ui/packages/vuu-table-extras/src/column-expression-input/useColumnExpressionSuggestionProvider.ts +++ b/vuu-ui/packages/vuu-table-extras/src/column-expression-input/useColumnExpressionSuggestionProvider.ts @@ -67,7 +67,7 @@ const getColumns = (columns: ColumnDescriptor[], options: ColumnOptions) => { return validColumns.map((column) => { const label = column.label ?? column.name; return { - apply: options.prefix ? `${options.prefix}${label}` : label, + apply: options.prefix ? `${options.prefix}${column.name}` : column.name, label, boost: 5, type: "column", diff --git a/vuu-ui/packages/vuu-table-extras/src/column-expression-panel/ColumnExpressionPanel.tsx b/vuu-ui/packages/vuu-table-extras/src/column-expression-panel/ColumnExpressionPanel.tsx index 6ba1d15ee..f9eb926a1 100644 --- a/vuu-ui/packages/vuu-table-extras/src/column-expression-panel/ColumnExpressionPanel.tsx +++ b/vuu-ui/packages/vuu-table-extras/src/column-expression-panel/ColumnExpressionPanel.tsx @@ -5,10 +5,11 @@ import { getCalculatedColumnName, getCalculatedColumnType, } from "@finos/vuu-utils"; -import { Button, FormField, FormFieldLabel, Input } from "@salt-ds/core"; +import { FormField, FormFieldLabel, Input } from "@salt-ds/core"; import { HTMLAttributes, useCallback, useRef } from "react"; import { ColumnExpressionInput, + ColumnExpressionSubmitHandler, useColumnExpressionSuggestionProvider, } from "../column-expression-input"; import { ColumnSettingsProps } from "../column-settings"; @@ -20,36 +21,44 @@ export interface ColumnExpressionPanelProps extends HTMLAttributes, Pick { column: ColumnDescriptor; + /** + * Callback prop, invoked on every change to calculated column definition + * @param calculatedColumnName the full calculated column name + */ onChangeName?: (name: string) => void; - onSave: (column: ColumnDescriptor) => void; } export const ColumnExpressionPanel = ({ column: columnProp, onChangeName: onChangeNameProp, - onSave: onSaveProp, tableConfig, vuuTable, }: ColumnExpressionPanelProps) => { const typeRef = useRef(null); - const { column, onChangeExpression, onChangeName, onChangeType, onSave } = + const { column, onChangeExpression, onChangeName, onChangeType } = useColumnExpression({ column: columnProp, onChangeName: onChangeNameProp, - onSave: onSaveProp, }); - const expressionRef = useRef(getCalculatedColumnExpression(column)); + // The initial value to pass into the Expression Input. That is a + // CodeMirror editor and will manage its own state once initialised. + const initialExpressionRef = useRef( + getCalculatedColumnExpression(column) + ); const suggestionProvider = useColumnExpressionSuggestionProvider({ columns: tableConfig.columns, table: vuuTable, }); - const handleSubmitExpression = useCallback(() => { - requestAnimationFrame(() => { - typeRef.current?.querySelector("button")?.focus(); - }); - }, []); + const handleSubmitExpression = + useCallback(() => { + if (typeRef.current) { + ( + typeRef.current?.querySelector("button") as HTMLButtonElement + )?.focus(); + } + }, []); return (
@@ -62,7 +71,6 @@ export const ColumnExpressionPanel = ({ @@ -72,7 +80,7 @@ export const ColumnExpressionPanel = ({ @@ -87,22 +95,6 @@ export const ColumnExpressionPanel = ({ width="100%" /> - -
- - - -
); }; diff --git a/vuu-ui/packages/vuu-table-extras/src/column-expression-panel/useColumnExpression.ts b/vuu-ui/packages/vuu-table-extras/src/column-expression-panel/useColumnExpression.ts index e83f0f1eb..7345c6e75 100644 --- a/vuu-ui/packages/vuu-table-extras/src/column-expression-panel/useColumnExpression.ts +++ b/vuu-ui/packages/vuu-table-extras/src/column-expression-panel/useColumnExpression.ts @@ -10,7 +10,7 @@ import { ColumnExpressionPanelProps } from "./ColumnExpressionPanel"; export type ColumnExpressionHookProps = Pick< ColumnExpressionPanelProps, - "column" | "onChangeName" | "onSave" + "column" | "onChangeName" >; const applyDefaults = (column: ColumnDescriptor) => { @@ -18,7 +18,7 @@ const applyDefaults = (column: ColumnDescriptor) => { if (type === "") { return { ...column, - name: `${name}:${expression}:string`, + name: `${name}:string:${expression}`, }; } else { return column; @@ -28,27 +28,32 @@ const applyDefaults = (column: ColumnDescriptor) => { export const useColumnExpression = ({ column: columnProp, onChangeName: onChangeNameProp, - onSave: onSaveProp, }: ColumnExpressionHookProps) => { - const [column, setColumn] = useState( + const [column, _setColumn] = useState( applyDefaults(columnProp) ); + const columnRef = useRef(columnProp); + const setColumn = useCallback((column: ColumnDescriptor) => { + columnRef.current = column; + _setColumn(column); + }, []); + // We need to track column name in a ref because ColunExpressionInput // is not a pure React component, it hosts a CodeMirror editor. We // do not want to cause it to render mid-edit. Therefore it uses memo // and only renders on initial load. onChangeExpression must be stable. - const columnNameRef = useRef(column.name); - const expressionRef = useRef(getCalculatedColumnDetails(column)[1]); + // const columnNameRef = useRef(column.name); + // const expressionRef = useRef(getCalculatedColumnDetails(column)[1]); const onChangeName = useCallback( (evt) => { const { value } = evt.target as HTMLInputElement; const newColumn = setCalculatedColumnName(column, value); - columnNameRef.current = newColumn.name; + // columnNameRef.current = newColumn.name; setColumn(newColumn); onChangeNameProp?.(newColumn.name); }, - [column, onChangeNameProp] + [column, onChangeNameProp, setColumn] ); const onChangeExpression = useCallback( @@ -56,12 +61,19 @@ export const useColumnExpression = ({ // we do not set state when this changes as the codemirror editor // manages state of the expression for us until complete const expression = value.trim(); - expressionRef.current = expression; - const [name, , type] = columnNameRef.current.split(":"); - columnNameRef.current = `${name}:${expression}:${type}`; - onChangeNameProp?.(columnNameRef.current); + // expressionRef.current = expression; + // const [name, , type] = column.name.split(":"); + // columnNameRef.current = `${name}:${expression}:${type}`; + + const { current: column } = columnRef; + const newColumn = setCalculatedColumnExpression(column, expression); + setColumn(newColumn); + + onChangeNameProp?.(newColumn.name); + + // console.log(`calculatedColumnName ${columnNameRef.current}`); }, - [onChangeNameProp] + [onChangeNameProp, setColumn] ); const onChangeType = useCallback( @@ -72,23 +84,13 @@ export const useColumnExpression = ({ onChangeNameProp?.(newColumn.name); } }, - [column, onChangeNameProp] + [column, onChangeNameProp, setColumn] ); - const onSave = useCallback(() => { - const newColumn = setCalculatedColumnExpression( - column, - expressionRef.current - ); - setColumn(newColumn); - onSaveProp(newColumn); - }, [column, onSaveProp]); - return { column, onChangeExpression, onChangeName, onChangeType, - onSave, }; }; diff --git a/vuu-ui/packages/vuu-table-extras/src/column-list/ColumnList.css b/vuu-ui/packages/vuu-table-extras/src/column-list/ColumnList.css index 7bdb502bf..4c8615080 100644 --- a/vuu-ui/packages/vuu-table-extras/src/column-list/ColumnList.css +++ b/vuu-ui/packages/vuu-table-extras/src/column-list/ColumnList.css @@ -26,6 +26,11 @@ flex: 1 1 auto; } +.vuuColumnList-text:hover { + font-weight: 600; + text-decoration: underline; +} + .vuuColumnList-checkBox { flex: 0 0 20px; } diff --git a/vuu-ui/packages/vuu-table-extras/src/column-list/ColumnList.tsx b/vuu-ui/packages/vuu-table-extras/src/column-list/ColumnList.tsx index e76725906..d70e53484 100644 --- a/vuu-ui/packages/vuu-table-extras/src/column-list/ColumnList.tsx +++ b/vuu-ui/packages/vuu-table-extras/src/column-list/ColumnList.tsx @@ -8,10 +8,16 @@ import { Checkbox } from "@salt-ds/core"; import { Switch } from "@salt-ds/lab"; import cx from "classnames"; import { ColumnDescriptor } from "@finos/vuu-datagrid-types"; -import { HTMLAttributes, SyntheticEvent, useCallback } from "react"; +import { + HTMLAttributes, + MouseEventHandler, + SyntheticEvent, + useCallback, +} from "react"; import { ColumnItem } from "../table-settings"; import "./ColumnList.css"; +import { getColumnLabel } from "@finos/vuu-utils"; const classBase = "vuuColumnList"; const classBaseListItem = "vuuColumnListItem"; @@ -27,6 +33,7 @@ export interface ColumnListProps columnItems: ColumnItem[]; onChange: ColumnChangeHandler; onMoveListItem: ListProps["onMoveListItem"]; + onNavigateToColumn?: (columnName: string) => void; } const ColumnListItem = ({ @@ -40,12 +47,15 @@ const ColumnListItem = ({ className={cx(classNameProp, classBaseListItem)} data-name={item?.name} > + {item?.isCalculated ? ( ) : ( )} - {item?.label ?? item?.name} + + {getColumnLabel(item as ColumnDescriptor)} + { const handleChange = useCallback( @@ -83,6 +94,17 @@ export const ColumnList = ({ }, [onChange] ); + + const handleClick = useCallback((evt) => { + const targetEl = evt.target as HTMLElement; + if (targetEl.classList.contains("vuuColumnList-text")) { + const listItemEl = targetEl.closest(".vuuListItem") as HTMLElement; + if (listItemEl?.dataset.name) { + onNavigateToColumn?.(listItemEl.dataset.name); + } + } + }, []); + return (
@@ -97,6 +119,7 @@ export const ColumnList = ({ allowDragDrop height="100%" onChange={handleChange} + onClick={handleClick} onMoveListItem={onMoveListItem} selectionStrategy="none" source={columnItems} diff --git a/vuu-ui/packages/vuu-table-extras/src/column-settings/ColumnNameLabel.css b/vuu-ui/packages/vuu-table-extras/src/column-settings/ColumnNameLabel.css index c08eb64a6..d13c7d0aa 100644 --- a/vuu-ui/packages/vuu-table-extras/src/column-settings/ColumnNameLabel.css +++ b/vuu-ui/packages/vuu-table-extras/src/column-settings/ColumnNameLabel.css @@ -1,8 +1,13 @@ .vuuColumnNameLabel-calculated { + cursor: pointer; display: flex; gap: 2px; } +.vuuColumnNameLabel-edit { + margin-left: auto; +} + .vuuColumnNameLabel-placeholder { color: var(--vuu-color-gray-35); } \ No newline at end of file diff --git a/vuu-ui/packages/vuu-table-extras/src/column-settings/ColumnNameLabel.tsx b/vuu-ui/packages/vuu-table-extras/src/column-settings/ColumnNameLabel.tsx index 6dd6259d4..95847da2f 100644 --- a/vuu-ui/packages/vuu-table-extras/src/column-settings/ColumnNameLabel.tsx +++ b/vuu-ui/packages/vuu-table-extras/src/column-settings/ColumnNameLabel.tsx @@ -7,30 +7,36 @@ import { } from "@finos/vuu-utils"; import "./ColumnNameLabel.css"; +import { MouseEventHandler } from "react"; const classBase = "vuuColumnNameLabel"; export interface ColumnNameLabelProps { column: ColumnDescriptor; + onClick: MouseEventHandler; } -export const ColumnNameLabel = ({ column }: ColumnNameLabelProps) => { +export const ColumnNameLabel = ({ column, onClick }: ColumnNameLabelProps) => { if (isCalculatedColumn(column.name)) { - const [name, expression, type] = getCalculatedColumnDetails(column); + const [name, type, expression] = getCalculatedColumnDetails(column); const displayName = name || "name"; - const displayExpression = "expression"; + const displayExpression = "=expression"; const nameClass = displayName === "name" ? `${classBase}-placeholder` : undefined; const expressionClass = expression === "" ? `${classBase}-placeholder` : undefined; return ( -
+
{displayName} : - {displayExpression} - : {type || "string"} + : + {displayExpression} +
); } else { diff --git a/vuu-ui/packages/vuu-table-extras/src/column-settings/ColumnSettingsPanel.css b/vuu-ui/packages/vuu-table-extras/src/column-settings/ColumnSettingsPanel.css index 8fb584a56..f3020185e 100644 --- a/vuu-ui/packages/vuu-table-extras/src/column-settings/ColumnSettingsPanel.css +++ b/vuu-ui/packages/vuu-table-extras/src/column-settings/ColumnSettingsPanel.css @@ -67,6 +67,9 @@ --vuu-icon-svg: var(--vuu-svg-pin-right); } + .vuuColumnSettingsPanel-editing .vuuColumnNameLabel-edit { + display: none; + } 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 b07ba98e8..ec43db60c 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 @@ -1,5 +1,6 @@ import { ColumnDescriptor, TableConfig } from "@finos/vuu-datagrid-types"; import { VuuTable } from "@finos/vuu-protocol-types"; +import { VuuInput } from "@finos/vuu-ui-controls"; import { getCalculatedColumnName, getDefaultAlignment, @@ -12,14 +13,14 @@ import { ToggleButton, ToggleButtonGroup, } from "@salt-ds/core"; +import cx from "classnames"; import { HTMLAttributes } from "react"; +import { ColumnExpressionPanel } from "../column-expression-panel"; import { ColumnFormattingPanel } from "../column-formatting-settings"; +import { ColumnNameLabel } from "./ColumnNameLabel"; import { useColumnSettings } from "./useColumnSettings"; -import { ColumnExpressionPanel } from "../column-expression-panel"; -import { VuuInput } from "@finos/vuu-ui-controls"; import "./ColumnSettingsPanel.css"; -import { ColumnNameLabel } from "./ColumnNameLabel"; const classBase = "vuuColumnSettingsPanel"; @@ -35,6 +36,7 @@ const getColumnLabel = (column: ColumnDescriptor) => { export interface ColumnSettingsProps extends HTMLAttributes { column: ColumnDescriptor; onConfigChange: (config: TableConfig) => void; + onCancelCreateColumn: () => void; onCreateCalculatedColumn: (column: ColumnDescriptor) => void; tableConfig: TableConfig; vuuTable: VuuTable; @@ -42,6 +44,7 @@ export interface ColumnSettingsProps extends HTMLAttributes { export const ColumnSettingsPanel = ({ column: columnProp, + onCancelCreateColumn, onConfigChange, onCreateCalculatedColumn, tableConfig, @@ -55,14 +58,17 @@ export const ColumnSettingsPanel = ({ column, navigateNextColumn, navigatePrevColumn, + onCancel, onChange, onChangeCalculatedColumnName, onChangeFormatting, onChangeRenderer, + onEditCalculatedColumn, onInputCommit, onSave, } = useColumnSettings({ column: columnProp, + onCancelCreateColumn, onConfigChange, onCreateCalculatedColumn, tableConfig, @@ -77,16 +83,19 @@ export const ColumnSettingsPanel = ({ } = column; return ( -
+
- +
{editCalculatedColumn ? ( @@ -167,27 +176,46 @@ export const ColumnSettingsPanel = ({ onChangeRenderer={onChangeRenderer} /> -
- - + +
+ ) : ( +
- NEXT - -
+ + +
+ )}
); }; 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 29f39c9ee..7574d6c5a 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 @@ -19,11 +19,13 @@ import { SingleSelectionHandler } from "@finos/vuu-ui-controls"; import { FormEventHandler, useCallback, + useEffect, useMemo, useRef, useState, } from "react"; import { ColumnSettingsProps } from "./ColumnSettingsPanel"; +import { ColumnExpressionSubmitHandler } from "../column-expression-input"; const integerCellRenderers: CellRendererDescriptor[] = [ { @@ -81,7 +83,7 @@ const getCellRendererDescriptor = ( } } } - // retur the appropriate default value for the column + // returm the appropriate default value for the column const typedAvailableRenderers = getAvailableCellRenderers(column); return typedAvailableRenderers[0]; }; @@ -123,6 +125,7 @@ const replaceColumn = ( export const useColumnSettings = ({ column: columnProp, + onCancelCreateColumn, onConfigChange, onCreateCalculatedColumn, tableConfig, @@ -130,9 +133,19 @@ export const useColumnSettings = ({ const [column, setColumn] = useState( getColumn(tableConfig.columns, columnProp) ); - const [editCalculatedColumn, setEditCalculatedColumn] = useState( - column.name === "::" - ); + const columnRef = useRef(column); + const [inEditMode, setEditMode] = useState(column.name === "::"); + + const handleEditCalculatedcolumn = useCallback(() => { + columnRef.current = column; + setEditMode(true); + }, [column]); + + useEffect(() => { + setColumn(columnProp); + setEditMode(columnProp.name === "::"); + }, [columnProp]); + const availableRenderers = useMemo(() => { return getAvailableCellRenderers(column); }, [column]); @@ -239,28 +252,35 @@ export const useColumnSettings = ({ navigateColumn({ moveBy: -1 }); }, [navigateColumn]); - const handleSaveCalculatedColumn = useCallback( - (calculatedColumn: ColumnDescriptor) => { - // TODO validate expression, unique name - onCreateCalculatedColumn({ - ...column, - ...calculatedColumn, - }); - }, - [column, onCreateCalculatedColumn] - ); + const handleSaveCalculatedColumn = useCallback(() => { + // TODO validate expression, unique name + onCreateCalculatedColumn(column); + }, [column, onCreateCalculatedColumn]); + + const handleCancelEdit = useCallback(() => { + if (columnProp.name === "::") { + onCancelCreateColumn(); + } else { + if (columnRef.current !== undefined && columnRef.current !== column) { + setColumn(columnRef.current); + } + setEditMode(false); + } + }, [column, columnProp.name, onCancelCreateColumn]); return { availableRenderers, - editCalculatedColumn, + editCalculatedColumn: inEditMode, selectedCellRenderer: selectedCellRendererRef.current, column, navigateNextColumn, navigatePrevColumn, + onCancel: handleCancelEdit, onChange: handleChange, onChangeCalculatedColumnName: handleChangeCalculatedColumnName, onChangeFormatting: handleChangeFormatting, onChangeRenderer: handleChangeRenderer, + onEditCalculatedColumn: handleEditCalculatedcolumn, onInputCommit: handleInputCommit, onSave: handleSaveCalculatedColumn, }; diff --git a/vuu-ui/packages/vuu-table-extras/src/table-settings/TableSettingsPanel.tsx b/vuu-ui/packages/vuu-table-extras/src/table-settings/TableSettingsPanel.tsx index 7a41e5a31..8db16df6f 100644 --- a/vuu-ui/packages/vuu-table-extras/src/table-settings/TableSettingsPanel.tsx +++ b/vuu-ui/packages/vuu-table-extras/src/table-settings/TableSettingsPanel.tsx @@ -21,6 +21,7 @@ export interface TableSettingsProps extends HTMLAttributes { onAddCalculatedColumn: () => void; onConfigChange: (config: TableConfig) => void; onDataSourceConfigChange: (dataSOurceConfig: DataSourceConfig) => void; + onNavigateToColumn?: (columnName: string) => void; tableConfig: TableConfig; } @@ -34,6 +35,7 @@ export const TableSettingsPanel = ({ onAddCalculatedColumn, onConfigChange, onDataSourceConfigChange, + onNavigateToColumn, tableConfig: tableConfigProp, ...htmlAttributes }: TableSettingsProps) => { @@ -115,6 +117,7 @@ export const TableSettingsPanel = ({ columnItems={columnItems} onChange={onColumnChange} onMoveListItem={onMoveListItem} + onNavigateToColumn={onNavigateToColumn} />
diff --git a/vuu-ui/packages/vuu-table-extras/src/useTableAndColumnSettings.ts b/vuu-ui/packages/vuu-table-extras/src/useTableAndColumnSettings.ts index bf3efe11e..444e7a5f2 100644 --- a/vuu-ui/packages/vuu-table-extras/src/useTableAndColumnSettings.ts +++ b/vuu-ui/packages/vuu-table-extras/src/useTableAndColumnSettings.ts @@ -54,6 +54,12 @@ export const useTableAndColumnSettings = ({ [dispatchLayoutAction] ); + const handleCancelCreateColumn = useCallback(() => { + requestAnimationFrame(() => { + showTableSettingsRef.current?.(); + }); + }, []); + const handleCreateCalculatedColumn = useCallback( (column: ColumnDescriptor) => { const newAvailableColumns = availableColumns.concat({ @@ -74,6 +80,7 @@ export const useTableAndColumnSettings = ({ (action: ColumnActionColumnSettings) => { showContextPanel("ColumnSettings", "Column Settings", { column: action.column, + onCancelCreateColumn: handleCancelCreateColumn, onConfigChange, onCreateCalculatedColumn: handleCreateCalculatedColumn, tableConfig, @@ -81,6 +88,7 @@ export const useTableAndColumnSettings = ({ } as ColumnSettingsProps); }, [ + handleCancelCreateColumn, handleCreateCalculatedColumn, onConfigChange, showContextPanel, @@ -99,6 +107,21 @@ export const useTableAndColumnSettings = ({ }); }, [showColumnSettingsPanel]); + const handleNavigateToColumn = useCallback( + (columnName: string) => { + const column = tableConfig.columns.find((c) => c.name === columnName); + if (column) { + showColumnSettingsPanel({ + type: "columnSettings", + column, + //TODO where do we get this from + vuuTable: { module: "SIMUL", table: "instruments" }, + }); + } + }, + [showColumnSettingsPanel, tableConfig.columns] + ); + showTableSettingsRef.current = useCallback(() => { showContextPanel("TableSettings", "DataGrid Settings", { availableColumns: @@ -110,11 +133,13 @@ export const useTableAndColumnSettings = ({ onAddCalculatedColumn: handleAddCalculatedColumn, onConfigChange, onDataSourceConfigChange, + onNavigateToColumn: handleNavigateToColumn, tableConfig, } as TableSettingsProps); }, [ availableColumns, handleAddCalculatedColumn, + handleNavigateToColumn, onConfigChange, onDataSourceConfigChange, showContextPanel, 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 c4dd66803..6b993ae99 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/useTableModel.ts +++ b/vuu-ui/packages/vuu-table/src/table-next/useTableModel.ts @@ -11,6 +11,7 @@ import { applyGroupByToColumns, applySortToColumns, getCellRenderer, + getColumnLabel, getTableHeadings, getValueFormatter, hasValidationRules, @@ -195,10 +196,8 @@ export type ColumnActionDispatch = (action: GridModelAction) => void; const columnReducer: GridModelReducer = (state, action) => { info?.(`TableModelReducer ${action.type}`); - console.log(`TableModelReducer ${action.type}`); switch (action.type) { case "init": - console.log({ init: action }); return init(action); case "moveColumn": return moveColumn(state, action); @@ -252,9 +251,13 @@ function init({ tableConfig, }: InitialConfig): InternalTableModel { const { columns, ...tableAttributes } = tableConfig; + console.log({ columns, dataSourceConfig: dataSourceConfig?.columns }); + const keyedColumns = columns .filter(subscribedOnly(dataSourceConfig?.columns)) .map(columnDescriptorToKeyedColumDescriptor(tableAttributes)); + + console.log({ keyedColumns }); const maybePinnedColumns = keyedColumns.some(isPinned) ? sortPinnedColumns(keyedColumns) : keyedColumns; @@ -298,7 +301,7 @@ const columnDescriptorToKeyedColumDescriptor = align = getDefaultAlignment(column.serverDataType), key, name, - label = name, + label = getColumnLabel(column), width = columnDefaultWidth, ...rest } = column; 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 3400fca92..c893646cd 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/useTableNext.ts +++ b/vuu-ui/packages/vuu-table/src/table-next/useTableNext.ts @@ -234,6 +234,9 @@ export const useTable = ({ tableConfig: newTableConfig, dataSourceConfig: dataSource.config, }); + console.log(`dispatch onConfigChange`, { + newTableConfig, + }); onConfigChange?.(newTableConfig); }, [dataSource, dispatchColumnAction, onConfigChange, tableConfig] diff --git a/vuu-ui/packages/vuu-table/src/table/useTableModel.ts b/vuu-ui/packages/vuu-table/src/table/useTableModel.ts index bbd5cea1e..a5ad0c328 100644 --- a/vuu-ui/packages/vuu-table/src/table/useTableModel.ts +++ b/vuu-ui/packages/vuu-table/src/table/useTableModel.ts @@ -24,6 +24,8 @@ import { stripFilterFromColumns, moveItemDeprecated, getDefaultAlignment, + isCalculatedColumn, + getCalculatedColumnName, } from "@finos/vuu-utils"; import { Reducer, useReducer } from "react"; @@ -236,6 +238,14 @@ function init({ dataSourceConfig, tableConfig }: InitialConfig): TableModel { } } +const labelFromName = (column: ColumnDescriptor) => { + if (isCalculatedColumn(column.name)) { + return getCalculatedColumnName(column); + } else { + return column.name; + } +}; + const getLabel = ( label: string, columnFormatHeader?: "uppercase" | "capitalize" @@ -264,7 +274,7 @@ const toKeyedColumWithDefaults = align = getDefaultAlignment(serverDataType), key, name, - label = name, + label = labelFromName(column), width = columnDefaultWidth, ...rest } = column; 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 44e5d7295..59bfae0df 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 @@ -1,4 +1,4 @@ -import { RefObject, SyntheticEvent } from "react"; +import { MouseEventHandler, RefObject, SyntheticEvent } from "react"; export type SelectionDisallowed = "none"; export type SingleSelectionStrategy = "default" | "deselectable"; @@ -79,6 +79,7 @@ export interface SelectionHookProps extends SelectionProps { highlightedIdx: number; itemQuery: string; label?: string; + onClick?: MouseEventHandler; selectionKeys?: string[]; tabToSelect?: boolean; } diff --git a/vuu-ui/packages/vuu-ui-controls/src/common-hooks/useSelection.ts b/vuu-ui/packages/vuu-ui-controls/src/common-hooks/useSelection.ts index 4f30d9c40..0678b8942 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/common-hooks/useSelection.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/common-hooks/useSelection.ts @@ -41,6 +41,7 @@ export const useSelection = ({ // groupSelection = GROUP_SELECTION_NONE, highlightedIdx, itemQuery, + onClick, // label, onSelect, onSelectionChange, @@ -239,18 +240,22 @@ export const useSelection = ({ lastActive.current = highlightedIdx; } } + onClick?.(evt); }, [ containerRef, highlightedIdx, disableSelection, + onClick, selectItemAtIndex, isExtendedSelect, ] ); const listHandlers = selectionIsDisallowed(selectionStrategy) - ? NO_SELECTION_HANDLERS + ? { + onClick, + } : { onClick: handleClick, onKeyDown: handleKeyDown, diff --git a/vuu-ui/packages/vuu-ui-controls/src/list/List.tsx b/vuu-ui/packages/vuu-ui-controls/src/list/List.tsx index 8e03994ce..18e56040e 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/list/List.tsx +++ b/vuu-ui/packages/vuu-ui-controls/src/list/List.tsx @@ -69,6 +69,7 @@ export const List = forwardRef(function List< maxWidth, minHeight, minWidth, + onClick: onClickProp, onDragStart, onDrop, onMoveListItem, @@ -164,6 +165,7 @@ export const List = forwardRef(function List< id, label: "List", listHandlers: listHandlersProp, // should this be in context ? + onClick: onClickProp, onDragStart, onDrop, onMoveListItem, diff --git a/vuu-ui/packages/vuu-ui-controls/src/list/listTypes.ts b/vuu-ui/packages/vuu-ui-controls/src/list/listTypes.ts index 3a452f19e..dff81b307 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/list/listTypes.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/list/listTypes.ts @@ -242,6 +242,7 @@ export interface ListHookProps< | "collapsibleHeaders" | "disabled" | "id" + | "onClick" | "onDragStart" | "onDrop" | "onHighlight" diff --git a/vuu-ui/packages/vuu-ui-controls/src/list/useList.ts b/vuu-ui/packages/vuu-ui-controls/src/list/useList.ts index 096153c8d..e396d668e 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/list/useList.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/list/useList.ts @@ -46,6 +46,7 @@ export const useList = ({ id, label = "", listHandlers: listHandlersProp, + onClick: onClickProp, onDragStart, onDrop, onHighlight, @@ -166,6 +167,7 @@ export const useList = ({ highlightedIdx: highlightedIndex, itemQuery: ".vuuListItem", label: `${label}:useList`, + onClick: onClickProp, onSelect: handleSelect, onSelectionChange: handleSelectionChange, selected, diff --git a/vuu-ui/packages/vuu-utils/src/array-utils.ts b/vuu-ui/packages/vuu-utils/src/array-utils.ts index a58182eed..bf083c528 100644 --- a/vuu-ui/packages/vuu-utils/src/array-utils.ts +++ b/vuu-ui/packages/vuu-utils/src/array-utils.ts @@ -97,3 +97,21 @@ export const moveItem = ( } } }; + +export const getAddedItems = (values: undefined | T[], newValues: T[]) => { + const isNew = (v: T) => !values?.includes(v); + if (values === undefined) { + return newValues; + } else if (newValues.some(isNew)) { + return newValues.filter(isNew); + } else { + return [] as T[]; + } +}; + +export const getMissingItems = ( + sourceItems: T[], + items: I[], + identity: (s: T) => I +) => + items.filter((i) => sourceItems.findIndex((s) => identity(s) === i) === -1); diff --git a/vuu-ui/packages/vuu-utils/src/column-utils.ts b/vuu-ui/packages/vuu-utils/src/column-utils.ts index f00bc3f58..c96f62370 100644 --- a/vuu-ui/packages/vuu-utils/src/column-utils.ts +++ b/vuu-ui/packages/vuu-utils/src/column-utils.ts @@ -583,6 +583,16 @@ export const getColumnName = (name: string) => { } }; +export const getColumnLabel = (column: ColumnDescriptor) => { + if (column.label) { + return column.label; + } else if (isCalculatedColumn(column.name)) { + return getCalculatedColumnName(column); + } else { + return column.name; + } +}; + export const findColumn = ( columns: KeyedColumnDescriptor[], columnName: string @@ -877,7 +887,7 @@ export const isCalculatedColumn = (columnName?: string) => export const getCalculatedColumnDetails = (column: ColumnDescriptor) => { if (isCalculatedColumn(column.name)) { - return column.name.split(":"); + return column.name.split(/:=?/); } else { throw Error( `column-utils, getCalculatedColumnDetails column name ${column.name} is not valid calculated column` @@ -887,41 +897,41 @@ export const getCalculatedColumnDetails = (column: ColumnDescriptor) => { export const getCalculatedColumnName = (column: ColumnDescriptor) => getCalculatedColumnDetails(column)[0]; -export const getCalculatedColumnExpression = (column: ColumnDescriptor) => - getCalculatedColumnDetails(column)[1]; export const getCalculatedColumnType = (column: ColumnDescriptor) => - getCalculatedColumnDetails(column)[2] as VuuColumnDataType; + getCalculatedColumnDetails(column)[1] as VuuColumnDataType; +export const getCalculatedColumnExpression = (column: ColumnDescriptor) => + getCalculatedColumnDetails(column)[2]; export const setCalculatedColumnName = ( column: ColumnDescriptor, name: string ): ColumnDescriptor => { - const [, expression, type] = column.name.split(":"); + const [, type, expression] = column.name.split(":"); return { ...column, - name: `${name}:${expression}:${type}`, + name: `${name}:${type}:${expression}`, }; }; -// TODO should we validate the expression here ? -export const setCalculatedColumnExpression = ( +export const setCalculatedColumnType = ( column: ColumnDescriptor, - expression: string + type: string ): ColumnDescriptor => { - const [name, , type] = column.name.split(":"); + const [name, , expression] = column.name.split(":"); return { ...column, - name: `${name}:${expression}:${type}`, + name: `${name}:${type}:${expression}`, }; }; -export const setCalculatedColumnType = ( +// TODO should we validate the expression here ? +export const setCalculatedColumnExpression = ( column: ColumnDescriptor, - type: string + expression: string ): ColumnDescriptor => { - const [name, expression] = column.name.split(":"); + const [name, type] = column.name.split(":"); return { ...column, - name: `${name}:${expression}:${type}`, + name: `${name}:${type}:=${expression}`, }; }; diff --git a/vuu-ui/packages/vuu-utils/src/event-emitter.ts b/vuu-ui/packages/vuu-utils/src/event-emitter.ts index 25b8de9fa..1cd4b6b3c 100644 --- a/vuu-ui/packages/vuu-utils/src/event-emitter.ts +++ b/vuu-ui/packages/vuu-utils/src/event-emitter.ts @@ -89,6 +89,15 @@ export class EventEmitter { this.addListener(event, listener); } + hasListener(event: E, listener: Events[E]) { + const listeners = this.#events.get(event); + if (Array.isArray(listeners)) { + return listeners.includes(listener); + } else { + return listeners === listener; + } + } + private invokeHandler(handler: Listener | Array, args: unknown[]) { if (isArrayOfListeners(handler)) { handler.slice().forEach((listener) => this.invokeHandler(listener, args)); diff --git a/vuu-ui/packages/vuu-utils/test/json-utils.test.ts b/vuu-ui/packages/vuu-utils/test/json-utils.test.ts index f845b0fd2..6fc6aabe0 100644 --- a/vuu-ui/packages/vuu-utils/test/json-utils.test.ts +++ b/vuu-ui/packages/vuu-utils/test/json-utils.test.ts @@ -123,4 +123,91 @@ describe("jsonToDataSourceRows", () => { [5, 5, true, false, 1, 0, "$root|test6|test6.2", 0, "", "test6.2", "test 6.2 value"], ]]); }); + + it("parses a 2 level structure, mixed simple attributes and array (simple values)", () => { + // prettier-ignore + + expect( + jsonToDataSourceRows({ + test1: "value 1", + test2: 12345, + test3: 100.01, + test4: true, + test5: ["test5.1", "test5.2", "test5.3"], + }) + ).toEqual([ + [ + { name: "col 1", type: { name: "json", renderer: { name: "json" } } }, + { name: "col 2", type: { name: "json", renderer: { name: "json" } } }, + { + name: "col 3", + hidden: true, + type: { name: "json", renderer: { name: "json" } }, + }, + ], + [ + [0, 0, true, false, 0, 0, "$root|test1", 0, "test1", "value 1"], + [1, 1, true, false, 0, 0, "$root|test2", 0, "test2", 12345], + [2, 2, true, false, 0, 0, "$root|test3", 0, "test3", 100.01], + [3, 3, true, false, 0, 0, "$root|test4", 0, "test4", true], + [4, 4, false, false, 0, 3, "$root|test5", 0, "test5+", ""], + [5, 5, true, false, 1, 0, "$root|test5|0", 0, "", "0", "test5.1"], + [6, 6, true, false, 1, 0, "$root|test5|1", 0, "", "1", "test5.2"], + [7, 7, true, false, 1, 0, "$root|test5|2", 0, "", "2", "test5.3"], + ], + ]); + }); + + it("parses a 2 level structure, mixed simple attributes and array (json values)", () => { + console.log( + jsonToDataSourceRows({ + test1: "value 1", + test2: 12345, + test3: 100.01, + test4: true, + test5: [ + { "test5.1": "test 5.1 value" }, + { "test5.2": "test 5.2 value" }, + { "test5.3": "test 5.2 value" }, + ], + }) + ); + + // prettier-ignore + expect( + jsonToDataSourceRows({ + test1: "value 1", + test2: 12345, + test3: 100.01, + test4: true, + test5: [ + {"test5.1": "test 5.1 value"}, + {"test5.2": "test 5.2 value"}, + {"test5.3": "test 5.2 value"} + ], + + + }) + // prettier-ignore + ).toEqual([[ + {name: 'col 1', type: {name: "json", "renderer": {name: "json"}}}, + {name: "col 2", type: {name: "json", "renderer": {name: "json"}}}, + {name: "col 3", hidden: true, type: {name: "json", "renderer": {name: "json"}}}, + {name: "col 4", hidden: true, type: {name: "json", "renderer": {name: "json"}}} + ], + [ + [0, 0, true, false, 0, 0, "$root|test1", 0, "test1", "value 1"], + [1, 1, true, false, 0, 0, "$root|test2", 0, "test2", 12345], + [2, 2, true, false, 0, 0, "$root|test3", 0, "test3", 100.01], + [3, 3, true, false, 0, 0, "$root|test4", 0, "test4", true], + [4, 4, false, false, 0, 3, "$root|test5", 0, "test5+", ""], + [5, 5, false, false, 1, 1, '$root|test5|0', 0, '', '0+', '' ], + [6, 6, true, false, 2, 0, '$root|test5|0|test5.1', 0, '', '', 'test5.1', 'test 5.1 value' ], + [7, 7, false, false, 1, 1, '$root|test5|1', 0, '', '1+', '' ], + [8,8, true, false, 2, 0, '$root|test5|1|test5.2', 0, '', '', 'test5.2','test 5.2 value'], + [9, 9, false, false, 1, 1, '$root|test5|2', 0, '', '2+', '' ], + [10,10, true,false, 2, 0, '$root|test5|2|test5.3', 0, '', '', 'test5.3', 'test 5.2 value'] + ] + ]); + }); }); 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 index 927ef41f1..338ee6ea2 100644 --- a/vuu-ui/sample-apps/app-vuu-basket-trader/src/App.css +++ b/vuu-ui/sample-apps/app-vuu-basket-trader/src/App.css @@ -111,7 +111,7 @@ body { z-index: 1; } - .MainTab { + .vuuTab.MainTab { background-color: #F1F2F4; border-color: #D6D7DA; border-radius: 6px 6px 0 0; @@ -149,7 +149,7 @@ body { width: 6px; } - .MainTab .vuuTab-main { + .vuuTab.MainTab .vuuTab-main { background-color: transparent; font-weight: 700; height: 29px; diff --git a/vuu-ui/sample-apps/feature-filter-table/src/VuuFilterTableFeature.tsx b/vuu-ui/sample-apps/feature-filter-table/src/VuuFilterTableFeature.tsx index bf2b74840..57d0e406a 100644 --- a/vuu-ui/sample-apps/feature-filter-table/src/VuuFilterTableFeature.tsx +++ b/vuu-ui/sample-apps/feature-filter-table/src/VuuFilterTableFeature.tsx @@ -1,4 +1,5 @@ import { + configChanged, DataSource, DataSourceConfig, DataSourceVisualLinkCreatedMessage, @@ -96,28 +97,29 @@ const VuuFilterTableFeature = ({ tableSchema }: FilterTableFeatureProps) => { const handleDataSourceConfigChange = useCallback( (config: DataSourceConfig | undefined, confirmed?: boolean) => { - // confirmed / unconfirmed messages are used for UI updates, not state saving - if (confirmed === undefined) { + if ( + // confirmed / unconfirmed messages are used for UI updates, not state saving + confirmed === undefined && + configChanged(dataSourceConfigFromState, config) + ) { save?.(config, "datasource-config"); } }, - [save] + [dataSourceConfigFromState, save] ); const handleAvailableColumnsChange = useCallback( (columns: SchemaColumn[]) => { console.log("save new available columns"); save?.(columns, "available-columns"); - // tableConfigRef.current = config; }, [save] ); const handleTableConfigChange = useCallback( (config: TableConfig) => { - console.log(`tabale config changed`); + console.log(`table config changed`); save?.(config, "table-config"); - // tableConfigRef.current = config; }, [save] ); @@ -142,6 +144,19 @@ const VuuFilterTableFeature = ({ tableSchema }: FilterTableFeatureProps) => { "color:red;font-weight:bold;" ); + // Only required when injecting a dataSource into session + // state in Showcase examples + if (!ds.hasListener("config", handleDataSourceConfigChange)) { + ds.on("config", handleDataSourceConfigChange); + } + + if (dataSourceConfigFromState) { + // this won't do anything if dataSource config already matches this + // This is only really used when injecting a dataSource into session + // state in Showcase examples + ds.config = dataSourceConfigFromState; + } + return ds; } const columns = diff --git a/vuu-ui/scripts/launch-app.mjs b/vuu-ui/scripts/launch-app.mjs index 4d33d6d8d..d442f0164 100644 --- a/vuu-ui/scripts/launch-app.mjs +++ b/vuu-ui/scripts/launch-app.mjs @@ -24,14 +24,13 @@ export const launchApp = async (websocket) => { const url = websocketUrl ? ` --url ${websocketUrl}` : ""; - await execWait("npm run --silent build"); + // await execWait("npm run --silent build"); const buildTarget = isBasket ? "app:basket" : "app"; - // code from cli branch was following line , replacing 2 lined beneath execWait(`serve -p 3010 ./deployed_apps/${appName}`); - await execWait("npm run --silent build"); - await execWait(`npm run --silent build:${buildTarget}${url}`); + // await execWait("npm run --silent build"); + // await execWait(`npm run --silent build:${buildTarget}${url}`); setTimeout(() => { open("http://localhost:3010/demo"); diff --git a/vuu-ui/showcase/src/examples/DataTable/JsonTable.examples.tsx b/vuu-ui/showcase/src/examples/DataTable/JsonTable.examples.tsx index 6ddc256b8..42399ca0e 100644 --- a/vuu-ui/showcase/src/examples/DataTable/JsonTable.examples.tsx +++ b/vuu-ui/showcase/src/examples/DataTable/JsonTable.examples.tsx @@ -67,6 +67,62 @@ export const DefaultJsonTable = () => { }; DefaultJsonTable.displaySequence = displaySequence++; +const jsonArraySimpleData = { + test1: "string 1", + test2: "string 2", + test3: ["test3.1", "test3.2", "test3.3"], +}; + +export const JsonTableArraySimpleData = () => { + return ( + <> + + + ); +}; +JsonTableArraySimpleData.displaySequence = displaySequence++; + +const jsonArrayJsonData = { + test1: "string 1", + test2: "string 2", + test3: [ + { "test3.1": "test value 3.1" }, + { "test3.2": "test value 3.2" }, + { "test3.3": "test value 3.3" }, + ], +}; + +export const JsonTableArrayJsonData = () => { + return ( + <> + + + ); +}; +JsonTableArrayJsonData.displaySequence = displaySequence++; + export const PackageJsonTable = () => { return ( <> diff --git a/vuu-ui/showcase/src/examples/Table/TableNext.examples.tsx b/vuu-ui/showcase/src/examples/Table/TableNext.examples.tsx index 83bbbe215..c43ab7063 100644 --- a/vuu-ui/showcase/src/examples/Table/TableNext.examples.tsx +++ b/vuu-ui/showcase/src/examples/Table/TableNext.examples.tsx @@ -11,11 +11,10 @@ import { ColumnSettingsPanel, TableSettingsPanel, } from "@finos/vuu-table-extras"; -import { GroupColumnDescriptor, TableConfig } from "@finos/vuu-datagrid-types"; +import { ColumnDescriptor, TableConfig } from "@finos/vuu-datagrid-types"; import { CSSProperties, useCallback, useMemo, useState } from "react"; import { useTableConfig, useTestDataSource } from "../utils"; import { GroupHeaderCellNext } from "@finos/vuu-table"; -import { defaultValueFormatter } from "@finos/vuu-utils"; import { getAllSchemas } from "@finos/vuu-data-test"; import "./TableNext.examples.css"; @@ -351,8 +350,6 @@ export const AutoTableNextBasket = () => { setConfig(config); }; - console.log({ config }); - return ( { }; AutoTableNextBasketDefinitions.displaySequence = displaySequence++; +export const VuuTableNextCalculatedColumns = () => { + const calculatedColumns: ColumnDescriptor[] = useMemo( + () => [ + { + name: "notional:double:=price*quantity", + serverDataType: "double", + type: { + name: "number", + formatting: { + decimals: 2, + }, + }, + }, + { + name: 'isBuy:char:=if(side="Sell","N","Y")', + serverDataType: "char", + }, + { + name: 'CcySort:char:=if(ccy="Gbp",1,if(ccy="USD",2,3))', + serverDataType: "char", + width: 60, + }, + { + name: "CcyLower:string:=lower(ccy)", + serverDataType: "string", + width: 60, + }, + { + name: "AccountUpper:string:=upper(account)", + label: "ACCOUNT", + serverDataType: "string", + }, + { + name: 'ExchangeCcy:string:=concatenate("---", exchange,"...",ccy, "---")', + serverDataType: "string", + }, + { + name: 'ExchangeIsNY:boolean:=starts(exchange,"N")', + serverDataType: "boolean", + }, + // { + // name: "Text", + // expression: "=text(quantity)", + // serverDataType: "string", + // }, + ], + [] + ); + + const schemas = getAllSchemas(); + const { config, dataSource, error } = useTestDataSource({ + // bufferSize: 1000, + schemas, + calculatedColumns, + tablename: "parentOrders", + }); + + console.log({ config, dataSource }); + + const [tableConfig] = useState(config); + + if (error) { + return error; + } + + return ( + + ); +}; +VuuTableNextCalculatedColumns.displaySequence = displaySequence++; + export const GroupHeaderCellNextOneColumn = () => { const column: GroupColumnDescriptor = useMemo(() => { const valueFormatter = defaultValueFormatter; diff --git a/vuu-ui/showcase/src/examples/TableExtras/ColumnExpressionInput.examples.tsx b/vuu-ui/showcase/src/examples/TableExtras/ColumnExpressionInput.examples.tsx index a26033ef6..24c1c4ce5 100644 --- a/vuu-ui/showcase/src/examples/TableExtras/ColumnExpressionInput.examples.tsx +++ b/vuu-ui/showcase/src/examples/TableExtras/ColumnExpressionInput.examples.tsx @@ -3,6 +3,7 @@ import { ColumnExpressionInput, ColumnExpressionInputProps, Expression, + isCompleteExpression, useColumnExpressionSuggestionProvider, } from "@finos/vuu-table-extras"; @@ -10,6 +11,7 @@ import { JsonTable } from "@finos/vuu-datatable"; import {} from "@finos/vuu-utils"; import { useAutoLoginToVuuServer } from "../utils"; +import { Input } from "@salt-ds/core"; let displaySequence = 1; @@ -38,7 +40,8 @@ const columns = [ export const DefaultColumnExpressionInput = () => { const [expression, setExpression] = useState(); const [source, setSource] = useState(""); - const [isValid] = useState(false); + const [currentSource, setCurrentSource] = useState(""); + const [isValid, setIsValid] = useState(false); const suggestionProvider = useColumnExpressionSuggestionProvider({ columns, table, @@ -56,27 +59,47 @@ export const DefaultColumnExpressionInput = () => { const handleChange: ColumnExpressionInputProps["onChange"] = useCallback( (source: string) => { console.log(`source ${source}, expression ${expression}`); - // const isValidExpression = isCompleteExpression(source); - // console.log(`is valid ${isValidExpression}`); - // setIsValid(isCompleteExpression(source)); + setCurrentSource(source); + const isValidExpression = isCompleteExpression(source); + console.log(`is valid ${isValidExpression}`); + setIsValid(isValidExpression); }, - [] + [expression] ); return ( - <> +
+
{currentSource}
+ - isValid {isValid} -
-
+ +
+ {JSON.stringify(expression, null, 2)} +
+ {`isValid ${isValid}`} {/*
{source}
*/} - - + +
); }; DefaultColumnExpressionInput.displaySequence = displaySequence++; diff --git a/vuu-ui/showcase/src/examples/TableExtras/ColumnExpressionPanel.examples.tsx b/vuu-ui/showcase/src/examples/TableExtras/ColumnExpressionPanel.examples.tsx index 27c1a1182..0e6abed1b 100644 --- a/vuu-ui/showcase/src/examples/TableExtras/ColumnExpressionPanel.examples.tsx +++ b/vuu-ui/showcase/src/examples/TableExtras/ColumnExpressionPanel.examples.tsx @@ -1,5 +1,7 @@ -import { ColumnExpressionPanel } from "@finos/vuu-table-extras"; -import { ColumnDescriptor } from "@finos/vuu-datagrid-types"; +import { + ColumnExpressionPanel, + ColumnExpressionSubmitHandler, +} from "@finos/vuu-table-extras"; import { useCallback } from "react"; import { useTableConfig } from "../utils"; @@ -13,15 +15,23 @@ export const DefaultColumnExpressionPanel = () => { table: instrumentPrices, }); - const handleSave = useCallback((column: ColumnDescriptor) => { - console.log(JSON.stringify(column)); + const handleChange = useCallback((columnName: string) => { + console.log(columnName); }, []); + const handleSubmitExpression = useCallback( + (columnName: string) => { + console.log(columnName); + }, + [] + ); + return (
diff --git a/vuu-ui/showcase/src/examples/TableExtras/ColumnSettings/ColumnSettings.examples.tsx b/vuu-ui/showcase/src/examples/TableExtras/ColumnSettings/ColumnSettings.examples.tsx index d08b38eff..fd2e73c95 100644 --- a/vuu-ui/showcase/src/examples/TableExtras/ColumnSettings/ColumnSettings.examples.tsx +++ b/vuu-ui/showcase/src/examples/TableExtras/ColumnSettings/ColumnSettings.examples.tsx @@ -1,11 +1,12 @@ import { getSchema } from "@finos/vuu-data-test"; import { ColumnDescriptor, TableConfig } from "@finos/vuu-datagrid-types"; import { + ColumnExpressionSubmitHandler, ColumnFormattingPanel, ColumnSettingsPanel, } from "@finos/vuu-table-extras"; import { CellRendererDescriptor } from "@finos/vuu-utils"; -import { useMemo, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; let displaySequence = 1; @@ -65,16 +66,21 @@ export const NewCalculatedColumnSettingsPanel = () => { const onConfigChange = (config: TableConfig) => { console.log(`config change ${JSON.stringify(config, null, 2)}`); }; - const onCreateCalculatedColumn = (column: ColumnDescriptor) => { - console.log(`create calculated column ${JSON.stringify(column, null, 2)}`); - setState((s) => ({ - tableConfig: { - ...s.tableConfig, - columns: s.tableConfig.columns.concat(column), - }, - column, - })); - }; + const handleCreateCalculatedColumn = useCallback( + (column: ColumnDescriptor) => { + console.log( + `create calculated column ${JSON.stringify(column, null, 2)}` + ); + setState((s) => ({ + tableConfig: { + ...s.tableConfig, + columns: s.tableConfig.columns.concat(column), + }, + column, + })); + }, + [] + ); return (
{ diff --git a/vuu-ui/showcase/src/features/FilterTable.feature.tsx b/vuu-ui/showcase/src/features/FilterTable.feature.tsx index d8499f5f6..b3949428e 100644 --- a/vuu-ui/showcase/src/features/FilterTable.feature.tsx +++ b/vuu-ui/showcase/src/features/FilterTable.feature.tsx @@ -7,7 +7,6 @@ import { useTableConfig } from "../examples/utils"; export const FilterTableFeature = ({ tableSchema, }: FilterTableFeatureProps) => { - console.log({ tableSchema }); const { saveSession } = useViewContext(); const { dataSource } = useTableConfig({ count: 1000,