From 968397bcc91e7030a9863a477b90c302de262cff Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 27 Feb 2023 10:34:07 -0600 Subject: [PATCH] WIP version of table/column stats process & supporting aggregate changes --- src/qqq/pages/records/query/ColumnStats.tsx | 121 +++++++++++++ src/qqq/pages/records/query/RecordQuery.tsx | 45 ++++- src/qqq/utils/DataGridUtils.tsx | 177 +++++++++++--------- 3 files changed, 259 insertions(+), 84 deletions(-) create mode 100644 src/qqq/pages/records/query/ColumnStats.tsx diff --git a/src/qqq/pages/records/query/ColumnStats.tsx b/src/qqq/pages/records/query/ColumnStats.tsx new file mode 100644 index 0000000..3d000b0 --- /dev/null +++ b/src/qqq/pages/records/query/ColumnStats.tsx @@ -0,0 +1,121 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData"; +import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; +import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete"; +import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError"; +import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import {DataGridPro} from "@mui/x-data-grid-pro"; +import {columnsStateInitializer} from "@mui/x-data-grid/internals"; +import React, {useEffect, useState} from "react"; +import DataGridUtils from "qqq/utils/DataGridUtils"; +import Client from "qqq/utils/qqq/Client"; + +interface Props +{ + tableMetaData: QTableMetaData; + fieldMetaData: QFieldMetaData; + closeModalHandler?: (event: object, reason: string) => void; +} + +ColumnStats.defaultProps = { + closeModalHandler: null, +}; + +const qController = Client.getInstance(); + +function ColumnStats({tableMetaData, fieldMetaData, closeModalHandler}: Props): JSX.Element +{ + const [statusString, setStatusString] = useState("Calculating statistics..."); + const [isLoaded, setIsLoaded] = useState(false); + const [valueCounts, setValueCounts] = useState(null as QRecord[]); + const [countDistinct, setCountDistinct] = useState(null as number); + const [rows, setRows] = useState([]); + const [columns, setColumns] = useState([]); + + useEffect(() => + { + (async () => + { + const processResult = await qController.processRun("tableStats", `tableName=${tableMetaData.name}&fieldName=${fieldMetaData.name}`); + setStatusString(null) + if (processResult instanceof QJobError) + { + const jobError = processResult as QJobError; + // todo setErrorAlert(); + console.error("Error fetching column stats" + jobError.error); + } + else + { + const result = processResult as QJobComplete; + setCountDistinct(result.values.countDistinct); + + const valueCounts = [] as QRecord[]; + result.values.valueCounts.forEach((object: any) => + { + valueCounts.push(new QRecord(object)); + }) + setValueCounts(valueCounts); + + const fakeTableMetaData = new QTableMetaData({primaryKeyField: "value", fields: {value: fieldMetaData, count: {label: "Count", type: "INTEGER"}}}); + const {rows, columnsToRender} = DataGridUtils.makeRows(valueCounts, fakeTableMetaData); + const columns = DataGridUtils.setupGridColumns(fakeTableMetaData, columnsToRender); + setRows(rows); + setColumns(columns); + + setIsLoaded(true); + } + })(); + }, []); + + return ( + + + + Column Statistics for {tableMetaData.label} : {fieldMetaData.label} + + {statusString} + + + + { + isLoaded && <> + + Distinct Values: {countDistinct.toLocaleString()} + + (params.indexRelativeToCurrentPage % 2 === 0 ? "even" : "odd")} + /> + + + } + ); +} + +export default ColumnStats; diff --git a/src/qqq/pages/records/query/RecordQuery.tsx b/src/qqq/pages/records/query/RecordQuery.tsx index 35a45fe..0ec7894 100644 --- a/src/qqq/pages/records/query/RecordQuery.tsx +++ b/src/qqq/pages/records/query/RecordQuery.tsx @@ -49,10 +49,11 @@ import FormData from "form-data"; import React, {forwardRef, useContext, useEffect, useReducer, useRef, useState} from "react"; import {useLocation, useNavigate, useSearchParams} from "react-router-dom"; import QContext from "QContext"; -import {QActionsMenuButton, QCreateNewButton} from "qqq/components/buttons/DefaultButtons"; +import {QActionsMenuButton, QCancelButton, QCreateNewButton} from "qqq/components/buttons/DefaultButtons"; import SavedFilters from "qqq/components/misc/SavedFilters"; import BaseLayout from "qqq/layouts/BaseLayout"; import ProcessRun from "qqq/pages/processes/ProcessRun"; +import ColumnStats from "qqq/pages/records/query/ColumnStats"; import DataGridUtils from "qqq/utils/DataGridUtils"; import Client from "qqq/utils/qqq/Client"; import FilterUtils from "qqq/utils/qqq/FilterUtils"; @@ -167,6 +168,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element const [activeModalProcess, setActiveModalProcess] = useState(null as QProcessMetaData); const [launchingProcess, setLaunchingProcess] = useState(launchProcess); const [recordIdsForProcess, setRecordIdsForProcess] = useState(null as string | QQueryFilter); + const [columnStatsFieldName, setColumnStatsFieldName] = useState(null as string) const instance = useRef({timer: null}); @@ -779,6 +781,16 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element closeActionsMenu(); }; + const closeColumnStats = (event: object, reason: string) => + { + if (reason === "backdropClick" || reason === "escapeKeyDown") + { + return; + } + + setColumnStatsFieldName(null); + }; + const closeModalProcess = (event: object, reason: string) => { if (reason === "backdropClick" || reason === "escapeKeyDown") @@ -981,6 +993,11 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element } } + const openColumnStatistics = async (column: GridColDef) => + { + setColumnStatsFieldName(column.field); + } + const CustomColumnMenu = forwardRef( function GridColumnMenu(props: GridColumnMenuProps, ref) { @@ -1023,6 +1040,14 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element */} + + { + hideMenu(e); + openColumnStatistics(currentColumn); + }}> + Column statistics + + ); }); @@ -1358,6 +1383,24 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element } + + { + columnStatsFieldName && + closeColumnStats(event, reason)}> +
+ + + + + + closeColumnStats(null, null)} disabled={false} /> + + + + +
+
+ } ); diff --git a/src/qqq/utils/DataGridUtils.tsx b/src/qqq/utils/DataGridUtils.tsx index 6d6bee0..7c43b21 100644 --- a/src/qqq/utils/DataGridUtils.tsx +++ b/src/qqq/utils/DataGridUtils.tsx @@ -20,6 +20,7 @@ */ import {AdornmentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/AdornmentType"; +import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData"; import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType"; import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; @@ -101,89 +102,7 @@ export default class DataGridUtils sortedKeys.forEach((key) => { const field = tableMetaData.fields.get(key); - - let columnType = "string"; - let columnWidth = 200; - let filterOperators: GridFilterOperator[] = QGridStringOperators; - - if (field.possibleValueSourceName) - { - filterOperators = buildQGridPvsOperators(tableMetaData.name, field); - } - else - { - switch (field.type) - { - case QFieldType.DECIMAL: - case QFieldType.INTEGER: - columnType = "number"; - columnWidth = 100; - - if (key === tableMetaData.primaryKeyField && field.label.length < 3) - { - columnWidth = 75; - } - - filterOperators = QGridNumericOperators; - break; - case QFieldType.DATE: - columnType = "date"; - columnWidth = 100; - filterOperators = getGridDateOperators(); - break; - case QFieldType.DATE_TIME: - columnType = "dateTime"; - columnWidth = 200; - filterOperators = getGridDateOperators(true); - break; - case QFieldType.BOOLEAN: - columnType = "string"; // using boolean gives an odd 'no' for nulls. - columnWidth = 75; - filterOperators = QGridBooleanOperators; - break; - default: - // noop - leave as string - } - } - - if (field.hasAdornment(AdornmentType.SIZE)) - { - const sizeAdornment = field.getAdornment(AdornmentType.SIZE); - const width: string = sizeAdornment.getValue("width"); - const widths: Map = new Map([ - ["small", 100], - ["medium", 200], - ["large", 400], - ["xlarge", 600] - ]); - if (widths.has(width)) - { - columnWidth = widths.get(width); - } - else - { - console.log("Unrecognized size.width adornment value: " + width); - } - } - - const column = { - field: field.name, - type: columnType, - headerName: field.label, - width: columnWidth, - renderCell: null as any, - filterOperators: filterOperators, - }; - - ///////////////////////////////////////////////////////////////////////////////////////// - // looks like, maybe we can just always render all columns, and remove this parameter? // - ///////////////////////////////////////////////////////////////////////////////////////// - if (columnsToRender == null || columnsToRender[field.name]) - { - column.renderCell = (cellValues: any) => ( - (cellValues.value) - ); - } + const column = this.makeColumnFromField(field, tableMetaData, columnsToRender); if (key === tableMetaData.primaryKeyField && linkBase) { @@ -201,5 +120,97 @@ export default class DataGridUtils return (columns); }; + + + /******************************************************************************* + ** + *******************************************************************************/ + public static makeColumnFromField = (field: QFieldMetaData, tableMetaData: QTableMetaData, columnsToRender: any): GridColDef => + { + let columnType = "string"; + let columnWidth = 200; + let filterOperators: GridFilterOperator[] = QGridStringOperators; + + if (field.possibleValueSourceName) + { + filterOperators = buildQGridPvsOperators(tableMetaData.name, field); + } + else + { + switch (field.type) + { + case QFieldType.DECIMAL: + case QFieldType.INTEGER: + columnType = "number"; + columnWidth = 100; + + if (field.name === tableMetaData.primaryKeyField && field.label.length < 3) + { + columnWidth = 75; + } + + filterOperators = QGridNumericOperators; + break; + case QFieldType.DATE: + columnType = "date"; + columnWidth = 100; + filterOperators = getGridDateOperators(); + break; + case QFieldType.DATE_TIME: + columnType = "dateTime"; + columnWidth = 200; + filterOperators = getGridDateOperators(true); + break; + case QFieldType.BOOLEAN: + columnType = "string"; // using boolean gives an odd 'no' for nulls. + columnWidth = 75; + filterOperators = QGridBooleanOperators; + break; + default: + // noop - leave as string + } + } + + if (field.hasAdornment(AdornmentType.SIZE)) + { + const sizeAdornment = field.getAdornment(AdornmentType.SIZE); + const width: string = sizeAdornment.getValue("width"); + const widths: Map = new Map([ + ["small", 100], + ["medium", 200], + ["large", 400], + ["xlarge", 600] + ]); + if (widths.has(width)) + { + columnWidth = widths.get(width); + } + else + { + console.log("Unrecognized size.width adornment value: " + width); + } + } + + const column = { + field: field.name, + type: columnType, + headerName: field.label, + width: columnWidth, + renderCell: null as any, + filterOperators: filterOperators, + }; + + ///////////////////////////////////////////////////////////////////////////////////////// + // looks like, maybe we can just always render all columns, and remove this parameter? // + ///////////////////////////////////////////////////////////////////////////////////////// + if (columnsToRender == null || columnsToRender[field.name]) + { + column.renderCell = (cellValues: any) => ( + (cellValues.value) + ); + } + + return (column); + } }