go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/projects/nodes/static/_nextjs/src/components/displayValue.tsx (about)

     1  /**
     2   * Copyright (c) 2024 - Present. Will Charczuk. All rights reserved.
     3   * Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository.
     4   */
     5  import * as api from "../api/nodes";
     6  import { Cell, CellRenderer, Column, CoordinateData, Rect, Region, SelectionModes, Table2, Utils } from "@blueprintjs/table";
     7  import { RefObject, useCallback, useEffect, useRef, useState } from "react";
     8  import { Viewport } from "reactflow";
     9  
    10  export interface DisplayValueProps {
    11    stabilizationNum?: number;
    12    viewport?: Viewport;
    13    refreshedAt?: Date;
    14    valueType: string;
    15    value: any;
    16    nodeType: string;
    17  }
    18  
    19  export function DisplayValue(props: DisplayValueProps) {
    20    const haveStabilized = props.stabilizationNum !== undefined && props.stabilizationNum !== 1;
    21    const valueIsSet = props.value !== undefined && props.value != null;
    22    const varIsLoading = !valueIsSet
    23    const nonVarIsLoading = !haveStabilized || !valueIsSet
    24    const isLoading = (props.nodeType === 'var' || props.nodeType === 'table') ? varIsLoading : nonVarIsLoading;
    25  
    26    if (isLoading) {
    27      return (<div className="display-value"><div className="bp5-skeleton">Loading ...</div></div>)
    28    }
    29    if (props.valueType === 'svg') {
    30      return (<div className="display-value"><div className="value-type-svg" dangerouslySetInnerHTML={{ __html: String(props.value.main) }}></div></div>)
    31    }
    32    if (props.valueType === 'table') {
    33      return (<DisplayTable {...props} />)
    34    }
    35    if (props.valueType === 'any') {
    36      return (<pre className="display-value bp5-code-block">{JSON.stringify(props.value, null, 2)}</pre>)
    37    }
    38    if (props.valueType.startsWith('[]')) {
    39      return (<DisplayList {...props} />)
    40    }
    41    return (<pre className="display-value bp5-code-block">{String(props.value)}</pre>)
    42  }
    43  
    44  export function correctTableViewport(tableRef: RefObject<Table2>, props: DisplayValueProps): () => void {
    45    return () => {
    46      if (!tableRef.current) {
    47        return
    48      }
    49      if (!tableRef.current.locator) {
    50        return
    51      }
    52      if (!props.viewport) {
    53        return
    54      }
    55  
    56      const { zoom } = props.viewport;
    57  
    58      function convertPointToColumn(clientX: number, useMidpoint?: boolean): number {
    59        const tableRect = this.getTableRect();
    60        if (this.grid === undefined || !tableRect.containsX(clientX)) {
    61          return -1;
    62        }
    63        const gridX = this.toGridX(clientX) / zoom;
    64        const limit = useMidpoint ? this.grid.numCols : this.grid.numCols - 1;
    65        const lookupFn = useMidpoint ? this.convertCellMidpointToClientX : this.convertCellIndexToClientX;
    66        return Utils.binarySearch(gridX, limit, lookupFn);
    67      }
    68  
    69      function convertPointToRow(clientY: number, useMidpoint?: boolean): number {
    70        const tableRect = this.getTableRect();
    71        if (this.grid === undefined || !tableRect.containsY(clientY)) {
    72          return -1;
    73        }
    74        const gridY = this.toGridY(clientY) / zoom;
    75        const limit = useMidpoint ? this.grid.numRows : this.grid.numRows - 1;
    76        const lookupFn = useMidpoint ? this.convertCellMidpointToClientY : this.convertCellIndexToClientY;
    77        return Utils.binarySearch(gridY, limit, lookupFn);
    78      }
    79  
    80      function convertPointToCell(clientX: number, clientY: number) {
    81        const gridX = this.toGridX(clientX) / zoom;
    82        const gridY = this.toGridY(clientY) / zoom;
    83        const col = Utils.binarySearch(gridX, this.grid!.numCols - 1, this.convertCellIndexToClientX);
    84        const row = Utils.binarySearch(gridY, this.grid!.numRows - 1, this.convertCellIndexToClientY);
    85        return { col, row };
    86      }
    87  
    88      tableRef.current.locator.convertPointToColumn = convertPointToColumn.bind(tableRef.current.locator);
    89      tableRef.current.locator.convertPointToRow = convertPointToRow.bind(tableRef.current.locator);
    90      tableRef.current.locator.convertPointToCell = convertPointToCell.bind(tableRef.current.locator);
    91    }
    92  }
    93  
    94  interface RowIndices {
    95    rowIndexStart: number;
    96    rowIndexEnd: number;
    97  }
    98  interface ColumnIndices {
    99    columnIndexStart: number;
   100    columnIndexEnd: number;
   101  }
   102  
   103  export function DisplayList(props: DisplayValueProps) {
   104    const tableRef = useRef<Table2>(null)
   105    const list = props.value as any[];
   106    const [visibleRegion, setVisibleRegion] = useState<api.TableRange | undefined>()
   107  
   108    useEffect(correctTableViewport(tableRef, props), [tableRef.current, props.viewport])
   109  
   110    const atRow = (rowIndex: number): any => {
   111      // const rowOffset = visibleRegion !== undefined ? visibleRegion.top : 0;
   112      // const effectiveRowIndex = rowIndex - rowOffset;
   113      if (list && list.length > rowIndex) {
   114        return list[rowIndex]
   115      }
   116      return null
   117    }
   118  
   119    const renderCell = useCallback((rowIndex: number) => {
   120      return (<Cell>{atRow(rowIndex)}</Cell>)
   121    }, [props])
   122  
   123    const getCellClipboardData = useCallback((row: number, col: number, cellRenderer: CellRenderer) => {
   124      return list[row]
   125    }, [props])
   126  
   127    const onVisibleCellsChange = useCallback((rowIndices: RowIndices, columnIndices: ColumnIndices) => {
   128      const newRegion = {
   129        top: rowIndices.rowIndexStart,
   130        left: columnIndices.columnIndexStart,
   131        bottom: rowIndices.rowIndexEnd,
   132        right: columnIndices.columnIndexEnd,
   133      }
   134      setVisibleRegion(newRegion)
   135    }, [props])
   136  
   137    return (
   138      <div className="display-value">
   139        <div className="display-table-outer">
   140          <div className="layout-boundary">
   141            <Table2
   142              ref={tableRef}
   143              className="display-table"
   144              numRows={list ? list.length : 0}
   145              cellRendererDependencies={[list, props.stabilizationNum, props.refreshedAt]}
   146              enableColumnHeader={false}
   147              enableColumnInteractionBar={false}
   148              enableColumnResizing={false}
   149              enableColumnReordering={false}
   150              enableMultipleSelection={true}
   151              selectionModes={SelectionModes.ALL}
   152              getCellClipboardData={getCellClipboardData}
   153              onVisibleCellsChange={onVisibleCellsChange}
   154            >
   155              <Column id={0} cellRenderer={renderCell} name="" />
   156            </Table2>
   157          </div>
   158        </div>
   159      </div>
   160    )
   161  }
   162  
   163  export function DisplayTable(props: DisplayValueProps) {
   164    const tableRef = useRef<Table2>(null)
   165    const selectionRef = useRef<any>(null)
   166  
   167    const data = props.value as api.Table
   168    const [visibleRegion, setVisibleRegion] = useState<api.TableRange | undefined>()
   169  
   170    useEffect(correctTableViewport(tableRef, props), [tableRef.current, props.viewport])
   171  
   172    const atRow = (rowIndex: number, columnIndex: number): any => {
   173      //const rowOffset = visibleRegion !== undefined ? visibleRegion.top : 0;
   174      //const effectiveRowIndex = rowIndex - rowOffset;
   175      //const columnOffset = visibleRegion !== undefined ? visibleRegion.left : 0;
   176      //const effectiveColumnIndex = columnIndex - columnOffset;
   177      return api.tableRowCol(data, rowIndex, columnIndex)
   178    }
   179  
   180    const renderCell = (rowIndex: number, columnIndex: number) => {
   181      const value = atRow(rowIndex, columnIndex)
   182      if (value === undefined) {
   183        return (<Cell></Cell>)
   184      }
   185      return (<Cell key={`${rowIndex}-${columnIndex}`}>{value}</Cell>)
   186    }
   187  
   188    const onSelection = (selection) => {
   189      selectionRef.current = selection;
   190    }
   191    const getCellClipboardData = (row: number, col: number, cellRenderer: CellRenderer) => {
   192      return atRow(row, col);
   193    }
   194  
   195    const onVisibleCellsChange = useCallback((rowIndices: RowIndices, columnIndices: ColumnIndices) => {
   196      const newRegion = {
   197        top: rowIndices.rowIndexStart,
   198        left: columnIndices.columnIndexStart,
   199        bottom: rowIndices.rowIndexEnd,
   200        right: columnIndices.columnIndexEnd,
   201      }
   202      setVisibleRegion(newRegion)
   203    }, [props])
   204  
   205    if (!data || !data.columns || data.columns.length === 0) {
   206      return (<div className="display-value"><div className="bp5-skeleton">Loading ...</div></div>)
   207    }
   208  
   209    return (
   210      <div className="display-value">
   211        <div className="display-table-outer">
   212          <div className="layout-boundary">
   213            <Table2
   214              ref={tableRef}
   215              numRows={api.tableMaxRows(data)}
   216              className="display-table"
   217              cellRendererDependencies={[data, props.stabilizationNum, props.refreshedAt]}
   218              selectionModes={SelectionModes.ALL}
   219              getCellClipboardData={getCellClipboardData}
   220              onSelection={onSelection}
   221              onVisibleCellsChange={onVisibleCellsChange}
   222            >
   223              {data && data.columns && data.columns.length > 0 ? data.columns.map((c, i) => (
   224                <Column id={i} key={i} name={c.name} cellRenderer={renderCell} />
   225              )) : (<Column />)}
   226            </Table2>
   227          </div>
   228        </div>
   229      </div>
   230    )
   231  }