From 6d57dc1e3a14032808a058961b0414bd9fe6eacd Mon Sep 17 00:00:00 2001 From: Julianna Langston Date: Wed, 19 Jun 2024 23:30:02 -0400 Subject: [PATCH 1/2] Setup AG-Grid --- packages/demo/src/client/apps/ag-grid.css | 75 ++++ packages/demo/src/client/apps/ag-grid.ts | 433 ++++++++++++++++++++++ packages/demo/static/ag-grid/index.html | 21 ++ 3 files changed, 529 insertions(+) create mode 100644 packages/demo/src/client/apps/ag-grid.css create mode 100644 packages/demo/src/client/apps/ag-grid.ts create mode 100644 packages/demo/static/ag-grid/index.html diff --git a/packages/demo/src/client/apps/ag-grid.css b/packages/demo/src/client/apps/ag-grid.css new file mode 100644 index 000000000..33091afac --- /dev/null +++ b/packages/demo/src/client/apps/ag-grid.css @@ -0,0 +1,75 @@ +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; +} +.custom-tooltip { + position: absolute; + width: 150px; + height: 70px; + border: 1px solid cornflowerblue; + overflow: hidden; + pointer-events: none; + transition: opacity 1s; +} +.custom-tooltip.ag-tooltip-hiding { + opacity: 0; +} +.custom-tooltip p { + margin: 0.5rem; + white-space: nowrap; +} +.custom-tooltip p:first-of-type { + font-weight: bold; +} +.number { + text-align: right; +} +.ag-row-level-0 { + font-weight: bold; +} +.ag-row-level-1 { + color: darkblue; +} +.ag-row-level-2 { + color: darkgreen; +} +#quick-filter-box{ + border-radius: 0.5rem; +} +#quick-filter-box:focus{ + border-radius: 0.25rem; + /* border-color: red!important; */ +} +.container-quick-search { + box-sizing: border-box; + height: 100%; +} +.quick-search{ + padding-bottom: 5px; + padding-left: 5px; + padding-top: 5px; + background-color: var(--ag-background-color,#2d3436); +} +#myGrid { + width: 100vw; + overflow: auto +} +.gain { + color: green +} +.loss { + color: red; +} +.ag-status-bar { + padding-bottom: 0.25rem; +} +.ag-status-bar-right { + color: rgba(245, 245, 245, 0.64); +} +#clear-search { + margin-left: 0.5rem; +} +button { + background: #1c1f20; + border-color: rgba(255, 255, 255, 0.5); + color: rgba(245, 245, 245, 1); +} diff --git a/packages/demo/src/client/apps/ag-grid.ts b/packages/demo/src/client/apps/ag-grid.ts new file mode 100644 index 000000000..39c0aac67 --- /dev/null +++ b/packages/demo/src/client/apps/ag-grid.ts @@ -0,0 +1,433 @@ +import { getClientAPI } from "@kite9/client"; +import "./ag-grid.css"; + +// DATA +const MIN_BOOK_COUNT = 10; +const MAX_BOOK_COUNT = 20; + +const MIN_TRADE_COUNT = 1; +const MAX_TRADE_COUNT = 10; + +let nextBookId = 62472; +let nextTradeId = 24287; + +const symbolList = [ + "MMM", + "AXP", + "TSLA", + "AAPL", + "BA", + "CAT", + "CVX", + "CSCO", + "KO", + "DIS", + "DOW", + "XOM", + "GS", + "HD", + "IBM", + "INTC", + "JNJ", + "JPM", + "MCD", + "MRK", + "MSFT", + "MSFT", + "NKE", + "PFE", + "PG", + "TRV", + "UTX", + "UNH", + "VZ", + "V", + "WMT", + "WBA", +]; +const portfolios = [ + "Aggressive", + "Defensive", + "Income", + "Speculative", + "Hybrid", +]; + +// FDC3 FUNCTIONS + +const cellDoubleClickEventHandler = (event: any) => { + if(event.column.left === 0 && event.value?.trim() !== ""){ + passContext(event.value); + } +} +const passContext = (ticker: string) => { + window.fdc3.broadcast({ + type: "fdc3.instrument", + name: ticker, + id: { + ticker + } + }) +} +const raiseIntent = (intent: string, ticker: string) => { + window.fdc3.raiseIntent(intent, { + type: "fdc3.instrument", + name: ticker, + id: { + ticker + } + }) +} + +const init = async () => { + // Initialize AG-Grid + // @ts-ignore + const {gridOptions} = new agGrid.Grid( + document.querySelector("#myGrid"), // element + setupGridOptions // options (rows of data, interactions, etc) + ); + + // Handle changes to the filter box + const filterBox = document.querySelector("#quick-filter-box")!; + + // Whenever a user types, filter the blotter by the search text + filterBox.addEventListener("input", (e) => { + // @ts-ignore + gridOptions.api.setQuickFilter(e?.target.value); + }); + + // If the user presses ENTER, broadcast the search query as the context + filterBox.addEventListener("keyup", (e) => { + // @ts-ignore + if(e.key === "Enter" && e.target?.value?.trim() !== ""){ + // @ts-ignore + passContext(e.target.value.toUpperCase()); + } + }) + + + // INITIALIZE FDC3 + try { + // LINE CURRENTLY FAILS + window.fdc3 = await getClientAPI(); + console.log(window.fdc3); + + // Listen for contexts + window.fdc3.addContextListener((context) => { + // We only care about type=fdc3.instrument. + // Ignore anything else. + if(context.type !== "fdc3.instrument") return; + + const symbol = context.id.ticker; + // Show the symbol in the search box + filterBox.value = symbol; + // Apply a filter based on the symbol + gridOptions.api.setQuickFilter(symbol); + }); + } catch (err) { + console.log("waiting..."); + console.error(err); + } +}; + +const dollarFormatterRegEx = /(\d)(?=(\d{3})+(?!\d))/g; + +type FormatterParams = { + value: number; +}; +type TradeRecord = { + security: string; + portfolio: string; + book: string; + trade: number; + submitterID: number; + submitterDealID: number; + bidFlag: "Buy" | "Sell"; + current: number; + previous: number; + chng: number; + pctchng: number; + h52: number; + l52: number; + hlrange: number; + pe: number; +} +type onReadyParams = { + api: { + setRowData: (trades: TradeRecord[]) => void; + }; +}; + +// GENERAL + +const createBookName = () => `IQ-${++nextBookId}`; +const createTradeId = () => ++nextTradeId; +const randomBetween = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min; +const numberCellFormatter = ({ value }: FormatterParams) => + `$${Math.floor(value).toString().replace(dollarFormatterRegEx, "$1,")}`; +const gainLossFormatter = ({ value }: FormatterParams) => + value > 0 ? "gain" : "loss"; +// function gainLossFormatter(params) { +// return ` 0 ? "gain" : "loss"}> ${params.value > 0 ? "+" : ""} $${Math.floor(params.value).toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,")}`; +// } +const percentCellFormatter = ({ value }: FormatterParams) => + value > 0 ? "gain" : "loss"; +// function percentCellFormatter(params) { +// return ` 0 ? "gain" : "loss"}>${Math.floor(params.value).toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,")}%`; +// } + +// Create a list of the data, that we modify as we go. if you are using an immutable +// data store (such as Redux) then this would be similar to your store of data. +const createRowData = () => { + let rowData = []; + for (let i = 0; i < symbolList.length; i++) { + let symbol = symbolList[i]; + for (let j = 0; j < portfolios.length; j++) { + let portfolio = portfolios[j]; + + let bookCount = randomBetween(MAX_BOOK_COUNT, MIN_BOOK_COUNT); + + for (let k = 0; k < bookCount; k++) { + let book = createBookName(); + let tradeCount = randomBetween(MAX_TRADE_COUNT, MIN_TRADE_COUNT); + for (let l = 0; l < tradeCount; l++) { + let trade = createTradeRecord(symbol, portfolio, book); + rowData.push(trade); + } + } + } + } + return rowData; +} +const createTradeRecord = (security: string, portfolio: string, book: string): TradeRecord => { + let previous = Math.floor(Math.random() * 100000) + 100; + let current = previous + Math.floor(Math.random() * 10000) - 2000; + let difference = current - previous; + + let l52 = Math.floor(Math.random() * 100000) + 100; + let h52 = l52 + Math.floor(Math.random() * 10000) - 2000; + + return { + security, + portfolio, + book, + trade: createTradeId(), + submitterID: randomBetween(10, 1000), + submitterDealID: randomBetween(10, 1000), + bidFlag: (Math.random() < .5) ? 'Buy' : 'Sell', + current: current, + previous: previous, + chng: difference, + pctchng: (difference / previous) * 100, + h52: h52, + l52: l52, + hlrange: h52 - l52, + pe: randomBetween(385, 100000) / 100 + + }; +} + + +class BtnCellRenderer { + // @ts-ignore + private eGui: HTMLButtonElement; + private params = {}; + + // @ts-ignore + init(params: any){ + this.params = params; + this.eGui = document.createElement("button"); + // @ts-ignore + this.eGui.innerHTML = this.params.title; + this.eGui.addEventListener("click", this.btnClickHandler); + } + + getGui(){ + return this.eGui; + } + + destroy(){ + this.eGui.removeEventListener("click", this.btnClickHandler); + } + + btnClickHandler(){ + // @ts-ignore + this.params.clicked(this.params); + } +} + +const setupGridOptions = { + defaultColDef: { + filter: "true", // set filtering on for all cols + // width: 120, + // sortable: true, + // resizable: true, + }, + floatingFilter: true, + columnDefs: [ + // these are the row groups, so they are all hidden (they are show in the group column) + { + headerName: "Security", + field: "security", + enableRowGroup: true, + enablePivot: true, + rowGroupIndex: 0, + hide: true, + tooltipField: "security", + tooltipComponentParams: { color: "#ececec" }, + }, + { + headerName: "Portfolio", + field: "portfolio", + enableRowGroup: true, + enablePivot: true, + rowGroupIndex: 1, + hide: true, + }, + { + headerName: "Book ID", + field: "book", + enableRowGroup: true, + enablePivot: true, + rowGroupIndex: 2, + hide: true, + }, + + // all the other columns (visible and not grouped) + { + headerName: "Current", + field: "current", + width: 100, + aggFunc: "sum", + enableValue: true, + cellClass: "number", + valueFormatter: numberCellFormatter, + cellRenderer: "agAnimateShowChangeCellRenderer", + filter: "agNumberColumnFilter", + }, + { + headerName: "Previous", + field: "previous", + width: 100, + aggFunc: "sum", + enableValue: true, + cellClass: "number", + valueFormatter: numberCellFormatter, + cellRenderer: "agAnimateShowChangeCellRenderer", + filter: "agNumberColumnFilter", + }, + { + headerName: "Action", + field: "security", + width: 125, + enableValue: true, + cellRenderer: "btnCellRenderer", + cellRendererParams: { + title: "View chart", + clicked: (params: {node: {key: string}}) => { + raiseIntent("ViewChart",params.node.key); + }, + }, + }, + { + headerName: "Gain/Loss", + field: "chng", + width: 125, + aggFunc: "sum", + enableValue: true, + cellClass: "number", + valueFormatter: gainLossFormatter, + cellRenderer: "agAnimateShowChangeCellRenderer", + filter: "agNumberColumnFilter", + }, + { + headerName: "% Change (avg)", + field: "pctchng", + width: 75, + aggFunc: "avg", + enableValue: true, + cellClass: "number", + cellRenderer: "agAnimateShowChangeCellRenderer", + valueFormatter: percentCellFormatter, + filter: "agNumberColumnFilter", + }, + { + headerName: "52 Wk High", + field: "h52", + width: 100, + aggFunc: "sum", + enableValue: true, + cellClass: "number", + valueFormatter: numberCellFormatter, + cellRenderer: "agAnimateShowChangeCellRenderer", + filter: "agNumberColumnFilter", + }, + { + headerName: "52 Wk Low", + field: "l52", + width: 100, + aggFunc: "sum", + enableValue: true, + cellClass: "number", + valueFormatter: numberCellFormatter, + cellRenderer: "agAnimateShowChangeCellRenderer", + filter: "agNumberColumnFilter", + }, + { + headerName: "Hi-Low Range", + field: "hlrange", + width: 100, + aggFunc: "sum", + enableValue: true, + cellClass: "number", + valueFormatter: numberCellFormatter, + cellRenderer: "agAnimateShowChangeCellRenderer", + filter: "agNumberColumnFilter", + }, + { + headerName: "Current P/E", + field: "pe", + width: 100, + aggFunc: "sum", + enableValue: true, + cellClass: "number", + valueFormatter: numberCellFormatter, + cellRenderer: "agAnimateShowChangeCellRenderer", + filter: "agNumberColumnFilter", + }, + + { + headerName: "Trade ID", + field: "trade", + width: 80, + filter: "agTextColumnFilter", + }, + { + headerName: "Bid", + field: "bidFlag", + enableRowGroup: true, + enablePivot: true, + width: 80, + filter: "agTextColumnFilter", + }, + ], + suppressAggFuncInHeader: true, + animateRows: true, + rowGroupPanelShow: "always", + pivotPanelShow: "always", + getRowNodeId: ({ trade }: { trade: string }) => trade, + autoGroupColumnDef: { + width: 200, + }, + onGridReady: (params: onReadyParams) => { + params.api.setRowData(createRowData()); + }, + onCellDoubleClicked: function (event: Event) { + cellDoubleClickEventHandler(event); + }, + components: { + btnCellRenderer: BtnCellRenderer, + }, +}; + +window.addEventListener("load", init); diff --git a/packages/demo/static/ag-grid/index.html b/packages/demo/static/ag-grid/index.html new file mode 100644 index 000000000..cff9df8b5 --- /dev/null +++ b/packages/demo/static/ag-grid/index.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + From bfab08fcf7a624abd876acf5342bcaec4deaba08 Mon Sep 17 00:00:00 2001 From: Julianna Langston Date: Wed, 19 Jun 2024 23:44:31 -0400 Subject: [PATCH 2/2] tidying --- packages/demo/src/client/apps/ag-grid.ts | 51 ++++++++++-------------- packages/demo/static/da/appd.json | 9 +++++ 2 files changed, 29 insertions(+), 31 deletions(-) diff --git a/packages/demo/src/client/apps/ag-grid.ts b/packages/demo/src/client/apps/ag-grid.ts index 39c0aac67..4ac0ff2ed 100644 --- a/packages/demo/src/client/apps/ag-grid.ts +++ b/packages/demo/src/client/apps/ag-grid.ts @@ -167,14 +167,8 @@ const numberCellFormatter = ({ value }: FormatterParams) => `$${Math.floor(value).toString().replace(dollarFormatterRegEx, "$1,")}`; const gainLossFormatter = ({ value }: FormatterParams) => value > 0 ? "gain" : "loss"; -// function gainLossFormatter(params) { -// return ` 0 ? "gain" : "loss"}> ${params.value > 0 ? "+" : ""} $${Math.floor(params.value).toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,")}`; -// } const percentCellFormatter = ({ value }: FormatterParams) => value > 0 ? "gain" : "loss"; -// function percentCellFormatter(params) { -// return ` 0 ? "gain" : "loss"}>${Math.floor(params.value).toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,")}%`; -// } // Create a list of the data, that we modify as we go. if you are using an immutable // data store (such as Redux) then this would be similar to your store of data. @@ -227,33 +221,28 @@ const createTradeRecord = (security: string, portfolio: string, book: string): T }; } +function BtnCellRenderer() {} -class BtnCellRenderer { - // @ts-ignore - private eGui: HTMLButtonElement; - private params = {}; - - // @ts-ignore - init(params: any){ - this.params = params; - this.eGui = document.createElement("button"); - // @ts-ignore - this.eGui.innerHTML = this.params.title; - this.eGui.addEventListener("click", this.btnClickHandler); - } +BtnCellRenderer.prototype.init = function(params: any) { + this.params = params; - getGui(){ - return this.eGui; - } - - destroy(){ - this.eGui.removeEventListener("click", this.btnClickHandler); - } - - btnClickHandler(){ - // @ts-ignore - this.params.clicked(this.params); - } + this.eGui = document.createElement('button'); + this.eGui.innerHTML = params.title; + + this.btnClickedHandler = this.btnClickedHandler.bind(this); + this.eGui.addEventListener('click', this.btnClickedHandler); +} + +BtnCellRenderer.prototype.getGui = function() { + return this.eGui; +} + +BtnCellRenderer.prototype.destroy = function() { + this.eGui.removeEventListener('click', this.btnClickedHandler); +} + +BtnCellRenderer.prototype.btnClickedHandler = function() { + this.params.clicked(this.params); } const setupGridOptions = { diff --git a/packages/demo/static/da/appd.json b/packages/demo/static/da/appd.json index d7d9f8049..6fb1eabec 100644 --- a/packages/demo/static/da/appd.json +++ b/packages/demo/static/da/appd.json @@ -127,6 +127,15 @@ "version": "1.0.0", "publisher": "FINOS", "icons": [] + }, + { + "appId": "grid", + "name": "grid", + "title": "AG-Grid", + "type": "web", + "details": { + "url": "http://localhost:8095/static/ag-grid/index.html" + } } ], "message": "OK"