diff --git a/app/src/components/grid/gridContextMenu.tsx b/app/src/components/grid/gridContextMenu.tsx index 9170bd0..7b18e11 100644 --- a/app/src/components/grid/gridContextMenu.tsx +++ b/app/src/components/grid/gridContextMenu.tsx @@ -1,5 +1,5 @@ import { GridApi, Column } from "ag-grid-community"; -import { ContextMenuItem } from "./gridTypes"; +import { ContextMenuItem } from "./gridInterface"; import { ChartType } from "ag-grid-community"; // region: Filters diff --git a/app/src/components/grid/gridHelper.tsx b/app/src/components/grid/gridHelper.tsx index 12fea2a..bf5a6f6 100644 --- a/app/src/components/grid/gridHelper.tsx +++ b/app/src/components/grid/gridHelper.tsx @@ -1,4 +1,8 @@ -import { ColumnDataType, ColumnDef, PrefetchedColumnValues } from "./gridTypes"; +import { + ColumnDataType, + ColumnDef, + PrefetchedColumnValues, +} from "./gridInterface"; import { Column, RowClassParams, SetFilter } from "ag-grid-enterprise"; import "./style.css"; import db from "../table/duckDB"; diff --git a/app/src/components/grid/gridInterface.ts b/app/src/components/grid/gridInterface.ts new file mode 100644 index 0000000..501b045 --- /dev/null +++ b/app/src/components/grid/gridInterface.ts @@ -0,0 +1,375 @@ +import { Column, RowClassParams, ColDef, GridApi } from "ag-grid-enterprise"; + +// region: Data Types +/* + Based on DuckDB WASM Formats + https://duckdb.org/docs/sql/data_types/overview.html + We didn't enumerate all, but you can add more. +*/ +export type DataType = "VARCHAR" | "DATE" | "INTEGER" | "DOUBLE" | "FLOAT"; + +export type NumericDataType = "INTEGER" | "DOUBLE" | "FLOAT"; +// endregion + +// region: General Interfaces +export interface RowData { + [key: string]: string | number; +} + +export interface StdAgGridProps { + tabName: string; + tableName: string; + darkMode?: boolean | null; +} + +export interface ColumnDataType { + [key: string]: DataType; +} + +export interface ColumnDef extends ColDef { + headerName: string; + field: string; + enableRowGroup: boolean; + enableValue: boolean; + filter: string; + children?: ColumnDef[] | ColumnDef | null; +} + +export interface ContextMenuItem { + name: string; + action: () => void; +} + +export interface CountStatusBarComponentType { + api: T; + params: P; + tableName: string | null; +} + +export interface SingleFilterModel { + filter: string; + filterType: string; + type: string; + filterModels?: SingleFilterModel[]; + values?: string[]; +} + +export interface MultiFilterModel { + filterType: string; + operator: string; + conditions: SingleFilterModel[]; + filterModels?: SingleFilterModel[]; + values?: string[]; +} + +export interface FilterModel { + [key: string]: SingleFilterModel | MultiFilterModel; +} + +export interface PrefetchedColumnValues { + [key: string]: any; +} +// endregion + +// region: Grid States +interface GridPreDestroyedEvent { + // Current state of the grid + state: GridState; + // The grid api. + api: GridApi; + // Application context as set on `gridOptions.context`. + context: TContext; + // Event identifier + type: "gridPreDestroyed"; +} + +export interface GridState { + // Grid version number + version?: string; + // Includes aggregation functions (column state) + aggregation?: AggregationState; + // Includes opened groups + columnGroup?: ColumnGroupState; + // Includes column ordering (column state) + columnOrder?: ColumnOrderState; + // Includes left/right pinned columns (column state) + columnPinning?: ColumnPinningState; + // Includes column width/flex (column state) + columnSizing?: ColumnSizingState; + // Includes hidden columns (column state) + columnVisibility?: ColumnVisibilityState; + // Includes Column Filters and Advanced Filter + filter?: FilterState; + // Includes currently focused cell. Works for Client-Side Row Model only + focusedCell?: FocusedCellState; + // Includes current page + pagination?: PaginationState; + // Includes current pivot mode and pivot columns (column state) + pivot?: PivotState; + // Includes currently selected cell ranges + cellSelection?: CellSelectionState; + // Includes current row group columns (column state) + rowGroup?: RowGroupState; + // Includes currently expanded group rows + rowGroupExpansion?: RowGroupExpansionState; + // Includes currently selected rows. + // For Server-Side Row Model, will be `ServerSideRowSelectionState | ServerSideRowGroupSelectionState`, + // for other row models, will be an array of row IDs + rowSelection?: + | string[] + | ServerSideRowSelectionState + | ServerSideRowGroupSelectionState; + // Includes current scroll position. Works for Client-Side Row Model only + scroll?: ScrollState; + // Includes current Side Bar positioning and opened tool panel + sideBar?: SideBarState; + // Includes current sort columns and direction (column state) + sort?: SortState; + // When providing a partial `initialState` with some but not all column state properties, set this to `true`. + // Not required if passing the whole state object retrieved from the grid. + partialColumnState?: boolean; +} + +interface AggregationState { + aggregationModel: AggregationColumnState[]; +} + +interface AggregationColumnState { + colId: string; + // Only named aggregation functions can be used in state + aggFunc: string; +} + +interface ColumnGroupState { + openColumnGroupIds: string[]; +} + +interface ColumnOrderState { + // All colIds in order + orderedColIds: string[]; +} + +interface ColumnPinningState { + leftColIds: string[]; + rightColIds: string[]; +} + +interface ColumnSizingState { + columnSizingModel: ColumnSizeState[]; +} + +interface ColumnSizeState { + colId: string; + width?: number; + flex?: number; +} + +interface ColumnVisibilityState { + hiddenColIds: string[]; +} + +interface FilterState { + filterModel?: FilterModel; + advancedFilterModel?: AdvancedFilterModel; +} + +type AdvancedFilterModel = JoinAdvancedFilterModel | ColumnAdvancedFilterModel; + +interface JoinAdvancedFilterModel { + filterType: "join"; + // How the conditions are joined together + type: "AND" | "OR"; + // The filter conditions that are joined by the `type` + conditions: AdvancedFilterModel[]; +} + +type ColumnAdvancedFilterModel = + | TextAdvancedFilterModel + | NumberAdvancedFilterModel + | BooleanAdvancedFilterModel + | DateAdvancedFilterModel + | DateStringAdvancedFilterModel + | ObjectAdvancedFilterModel; + +interface TextAdvancedFilterModel { + filterType: "text"; + // The ID of the column being filtered. + colId: string; + // The filter option that is being applied. + type: TextAdvancedFilterModelType; + // The value to filter on. This is the same value as displayed in the input. + filter?: string; +} + +type TextAdvancedFilterModelType = + | "equals" + | "notEqual" + | "contains" + | "notContains" + | "startsWith" + | "endsWith" + | "blank" + | "notBlank"; + +interface NumberAdvancedFilterModel { + filterType: "number"; + // The ID of the column being filtered. + colId: string; + // The filter option that is being applied. + type: ScalarAdvancedFilterModelType; + // The value to filter on. + filter?: number; +} + +type ScalarAdvancedFilterModelType = + | "equals" + | "notEqual" + | "lessThan" + | "lessThanOrEqual" + | "greaterThan" + | "greaterThanOrEqual" + | "blank" + | "notBlank"; + +interface BooleanAdvancedFilterModel { + filterType: "boolean"; + // The ID of the column being filtered. + colId: string; + // The filter option that is being applied. + type: BooleanAdvancedFilterModelType; +} + +type BooleanAdvancedFilterModelType = "true" | "false"; + +interface DateAdvancedFilterModel { + filterType: "date"; + // The ID of the column being filtered. + colId: string; + // The filter option that is being applied. + type: ScalarAdvancedFilterModelType; + // The value to filter on. This is in format `YYYY-MM-DD`. + filter?: string; +} + +interface DateStringAdvancedFilterModel { + filterType: "dateString"; + // The ID of the column being filtered. + colId: string; + // The filter option that is being applied. + type: ScalarAdvancedFilterModelType; + // The value to filter on. This is in format `YYYY-MM-DD`. + filter?: string; +} + +interface ObjectAdvancedFilterModel { + filterType: "object"; + // The ID of the column being filtered. + colId: string; + // The filter option that is being applied. + type: TextAdvancedFilterModelType; + // The value to filter on. This is the same value as displayed in the input. + filter?: string; +} + +interface FocusedCellState { + colId: string; + // A positive number from 0 to n, where n is the last row the grid is rendering + // or -1 if you want to navigate to the grid header + rowIndex: number; + // Either 'top', 'bottom' or null/undefined (for not pinned) + rowPinned: RowPinnedType; +} + +type RowPinnedType = "top" | "bottom" | null | undefined; + +interface PaginationState { + // Current page + page?: number; + // Current page size. Only use when the pageSizeSelector dropdown is visible + pageSize?: number; +} + +interface PivotState { + pivotMode: boolean; + pivotColIds: string[]; +} + +interface CellSelectionState { + cellRanges: CellSelectionCellState[]; +} + +interface CellSelectionCellState { + id?: string; + type?: CellRangeType; + // The start row of the range + startRow?: RowPosition; + // The end row of the range + endRow?: RowPosition; + // The columns in the range + colIds: string[]; + // The start column for the range + startColId: string; +} + +enum CellRangeType { + VALUE, + DIMENSION, +} + +interface RowPosition { + // A positive number from 0 to n, where n is the last row the grid is rendering + // or -1 if you want to navigate to the grid header + rowIndex: number; + // Either 'top', 'bottom' or null/undefined (for not pinned) + rowPinned: RowPinnedType; +} + +interface RowGroupState { + // Grouped columns in order + groupColIds: string[]; +} + +interface RowGroupExpansionState { + expandedRowGroupIds: string[]; +} + +interface ServerSideRowSelectionState { + // Whether the majority of rows are selected or not + selectAll: boolean; + // All rows that have the opposite selection state to `selectAll` + toggledNodes: string[]; +} + +interface ServerSideRowGroupSelectionState { + nodeId?: string; + selectAllChildren?: boolean; + toggledNodes?: ServerSideRowGroupSelectionState[]; +} + +interface ScrollState { + top: number; + left: number; +} + +interface SideBarState { + // Is side bar visible + visible: boolean; + position: "left" | "right"; + // Open tool panel, or null if closed + openToolPanel: string | null; + // State for each tool panel + toolPanels: { [id: string]: any }; +} + +interface SortState { + // Sorted columns and directions in order + sortModel: SortModelItem[]; +} + +interface SortModelItem { + // Column Id to apply the sort to. + colId: string; + // Sort direction + sort: "asc" | "desc"; +} +// endregion diff --git a/app/src/components/grid/gridStates.ts b/app/src/components/grid/gridStates.ts new file mode 100644 index 0000000..63e43ab --- /dev/null +++ b/app/src/components/grid/gridStates.ts @@ -0,0 +1,107 @@ +// ag-grid +import { GridPreDestroyedEvent, GridApi } from "ag-grid-community"; + +// table Folder +import db from "../table/duckDB"; + +export async function initStateTable() { + const connection = await db.connect(); + const query = ` + CREATE TABLE IF NOT EXISTS grid_states_test ( + table_name VARCHAR, + userSaved VARCHAR, -- Added for Memory Store (MS) and Memory Recall (MV) + state VARCHAR, + columnState VARCHAR, + PRIMARY KEY (table_name, userSaved) + ); + `; + await connection.query(query); + await connection.close(); +} + +export function fetchPreviousState(tableName: string) { + return new Promise((resolve) => { + db.connect().then(async (connection) => { + const query = ` + SELECT table_name, state, columnState FROM grid_states_test + WHERE table_name = '${tableName}' + AND userSaved = 'auto'; + `; + const arrowResult = await connection.query(query); + const result = arrowResult.toArray().map((row) => row.toJSON()); + console.log("initial state table displayed", result); + await connection.close(); + resolve(result[0]); + }); + }); +} + +export function saveState( + gridApi: GridApi | null, + tableName: string, + userSaved: string, +) { + if (gridApi) { + const state = gridApi.getState(); + const columnState = gridApi.getColumnState(); + db.connect().then(async (connection) => { + const stateString = JSON.stringify(state); + const columnStateString = JSON.stringify(columnState); + const query = ` + INSERT INTO grid_states_test + VALUES ('${tableName}', '${userSaved}', '${stateString}', '${columnStateString}') + ON CONFLICT (table_name, userSaved) DO UPDATE SET state = EXCLUDED.state, columnState = EXCLUDED.columnState; + `; + await connection.query(query); + await connection.close(); + console.log("leudom inserted successfully"); + }); + } +} + +export async function applySavedState( + gridApi: GridApi | null, + tableName: string, + userSaved: string, +) { + const connection = await db.connect(); + const query = ` + SELECT * FROM grid_states_test + WHERE table_name = '${tableName}' + AND userSaved = '${userSaved}'; + `; + const arrowResult = await connection.query(query); + const result = arrowResult.toArray().map((row) => row.toJSON()); + await connection.close(); + + if (result.length > 0 && gridApi !== null) { + const gridState = JSON.parse(result[0].state); + const columnState = JSON.parse(result[0].columnState); + // Apply column state and wait for it to be applied + await new Promise((resolve) => { + gridApi.applyColumnState({ + state: columnState, + applyOrder: true, + }); + resolve(console.log("leudom node", gridApi.getRenderedNodes())); + }).then(); + console.log("leudom gridState", gridState); + console.log("leudom columnState", columnState); + + // Set State Manually + // Set Filter Model + if (gridState.filter && gridState.filter.filterModel) { + gridApi.setFilterModel(gridState.filter.filterModel); + } + + // Open groups + const openGroups = gridState.rowGroupExpansion.expandedRowGroupIds; + gridApi.forEachNode((node) => { + if (openGroups.includes(node.id)) { + node.setExpanded(true); + } + }); + } +} + +export default initStateTable; diff --git a/app/src/components/grid/gridTypes.ts b/app/src/components/grid/gridTypes.ts deleted file mode 100644 index db66ddc..0000000 --- a/app/src/components/grid/gridTypes.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Column, RowClassParams, ColDef } from "ag-grid-enterprise"; - -/* - Based on DuckDB WASM Formats - https://duckdb.org/docs/sql/data_types/overview.html - We didn't enumerate all, but you can add more. -*/ -export type DataType = "VARCHAR" | "DATE" | "INTEGER" | "DOUBLE" | "FLOAT"; - -export type NumericDataType = "INTEGER" | "DOUBLE" | "FLOAT"; - -export interface RowData { - [key: string]: string | number; -} - -export interface StdAgGridProps { - tabName: string; - tableName: string; - darkMode?: boolean | null; -} - -export interface ColumnDataType { - [key: string]: DataType; -} - -export interface ColumnDef extends ColDef { - headerName: string; - field: string; - enableRowGroup: boolean; - enableValue: boolean; - filter: string; - children?: ColumnDef[] | ColumnDef | null; -} - -export interface ContextMenuItem { - name: string; - action: () => void; -} - -export interface CountStatusBarComponentType { - api: T; - params: P; - tableName: string | null; -} - -export interface SingleFilterModel { - filter: string; - filterType: string; - type: string; - filterModels?: SingleFilterModel[]; - values?: string[]; -} - -export interface MultiFilterModel { - filterType: string; - operator: string; - conditions: SingleFilterModel[]; - filterModels?: SingleFilterModel[]; - values?: string[]; -} - -export interface FilterModel { - [key: string]: SingleFilterModel | MultiFilterModel; -} - -export interface PrefetchedColumnValues { - [key: string]: any; -} diff --git a/app/src/components/grid/stdGrid.tsx b/app/src/components/grid/stdGrid.tsx index c18152c..18d059c 100644 --- a/app/src/components/grid/stdGrid.tsx +++ b/app/src/components/grid/stdGrid.tsx @@ -1,4 +1,10 @@ -import React, { useEffect, useState, useMemo, useRef } from "react"; +import React, { + useEffect, + useState, + useMemo, + useRef, + useCallback, +} from "react"; import { AgGridReact } from "ag-grid-react"; import { Grid2, Button } from "@mui/material"; @@ -10,7 +16,8 @@ import { ColumnDef, CountStatusBarComponentType, PrefetchedColumnValues, -} from "./gridTypes"; + GridState, +} from "./gridInterface"; import handleKeyDown from "./gridShortcuts"; import { onFilterEqual, @@ -25,6 +32,11 @@ import { getLayeredColumnDefs, getGroupedColumnDefs, } from "./gridHelper"; +import initStateTable, { + fetchPreviousState, + saveState, + applySavedState, +} from "./gridStates"; import GridLoadingOverlay from "./gridLoadingOverlay"; import "./style.css"; @@ -39,10 +51,17 @@ import CustomCountBar, { import db from "../table/duckDB"; // AgGrid imports -import { ColDef, StatusPanelDef, GridApi } from "@ag-grid-community/core"; +import { + ColDef, + StatusPanelDef, + GridApi, + StateUpdatedEvent, +} from "@ag-grid-community/core"; +import { GridPreDestroyedEvent } from "ag-grid-community"; import "ag-grid-enterprise"; import "ag-grid-community/styles/ag-grid.css"; import "ag-grid-community/styles/ag-theme-alpine.css"; +import { PartyMode } from "@mui/icons-material"; function arePropsEqual( prevProps: StdAgGridProps, @@ -59,7 +78,6 @@ const StdAgGrid: React.FC = (props) => { const [columnDefs, setColumnDefs] = useState([]); const [gridApi, setGridApi] = useState(null); const startTime = useRef(performance.now()); - const [columnDataTypes, setColumnDataTypes] = useState({}); const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []); useEffect(() => { @@ -247,22 +265,30 @@ const StdAgGrid: React.FC = (props) => { // endregion // region: onModelUpdated / onGridReady / onFirstDataRendered - const onModelUpdated = (params: any) => { - console.log("std onModelUpdated", params); - }; + const onModelUpdated = (params: any) => {}; const onGridReady = (params: any) => { - console.log("std onGridReady"); setGridApi(params.api); }; - const onFirstDataRendered = () => { - const endTime = performance.now(); - const execTime = endTime - startTime.current; - setExecTime(execTime); - setLoading(false); - }; - // endregion + const onFirstDataRendered = useCallback( + async (params: any) => { + const endTime = performance.now(); + const execTime = endTime - startTime.current; + setExecTime(execTime); + setLoading(false); + + // States + initStateTable(); // Create table if not exists. + await applySavedState(gridApi, props.tableName, "auto"); + }, + [gridApi], + ); + + const onGridPreDestroyed = useCallback((params: GridPreDestroyedEvent) => { + saveState(params.api, props.tableName, "auto"); + console.log("leudom gridState saved", params.api); + }, []); // region: Buttons const resetTable = () => { @@ -349,7 +375,7 @@ const StdAgGrid: React.FC = (props) => { }} > - + {/* // region: Buttons */} @@ -372,6 +398,26 @@ const StdAgGrid: React.FC = (props) => { Autosize Columns + + + + + + = (props) => { ? "ag-theme-alpine-dark" : "ag-theme-alpine" : props.darkMode - ? "ag-theme-alpine-dark" - : "ag-theme-alpine" + ? "ag-theme-alpine-dark" + : "ag-theme-alpine" } > = (props) => { onModelUpdated={onModelUpdated} onGridReady={onGridReady} onFirstDataRendered={onFirstDataRendered} + onGridPreDestroyed={onGridPreDestroyed} rowHeight={25} headerHeight={25} suppressMultiSort={false} diff --git a/app/src/components/statusBar/duckCustomBar.tsx b/app/src/components/statusBar/duckCustomBar.tsx index 4aef8d5..128de30 100644 --- a/app/src/components/statusBar/duckCustomBar.tsx +++ b/app/src/components/statusBar/duckCustomBar.tsx @@ -5,7 +5,7 @@ import { FilterModel, SingleFilterModel, MultiFilterModel, -} from "../grid/gridTypes"; +} from "../grid/gridInterface"; // import { AsyncDuckDB } from "@duckdb/duckdb-wasm"; interface CountBarProps extends CustomStatusPanelProps { diff --git a/app/src/components/table/initTable.tsx b/app/src/components/table/initTable.tsx index 8b07ad8..1e09a39 100644 --- a/app/src/components/table/initTable.tsx +++ b/app/src/components/table/initTable.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from "react"; import db from "./duckDB"; -import { ColumnDataType } from "../grid/gridTypes"; +import { ColumnDataType } from "../grid/gridInterface"; /** * This function initializes the table.