From 968397bcc91e7030a9863a477b90c302de262cff Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 27 Feb 2023 10:34:07 -0600 Subject: [PATCH 1/6] 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); + } } From f766f17a927ba667062ff05f9aedd8c20bb257be Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 1 Mar 2023 08:38:35 -0600 Subject: [PATCH 2/6] column stats checkpoint --- src/qqq/pages/records/query/ColumnStats.tsx | 111 +++++++++++++++----- src/qqq/pages/records/query/RecordQuery.tsx | 4 +- src/qqq/styles/qqq-override-styles.css | 2 + src/qqq/utils/DataGridUtils.tsx | 2 +- 4 files changed, 92 insertions(+), 27 deletions(-) diff --git a/src/qqq/pages/records/query/ColumnStats.tsx b/src/qqq/pages/records/query/ColumnStats.tsx index 3d000b0..1c94cfd 100644 --- a/src/qqq/pages/records/query/ColumnStats.tsx +++ b/src/qqq/pages/records/query/ColumnStats.tsx @@ -21,13 +21,17 @@ import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData"; import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; +import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection"; 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 {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter"; +import {TablePagination} from "@mui/material"; import Box from "@mui/material/Box"; +import LinearProgress from "@mui/material/LinearProgress"; import Typography from "@mui/material/Typography"; import {DataGridPro} from "@mui/x-data-grid-pro"; -import {columnsStateInitializer} from "@mui/x-data-grid/internals"; +import FormData from "form-data"; import React, {useEffect, useState} from "react"; import DataGridUtils from "qqq/utils/DataGridUtils"; import Client from "qqq/utils/qqq/Client"; @@ -36,16 +40,15 @@ interface Props { tableMetaData: QTableMetaData; fieldMetaData: QFieldMetaData; - closeModalHandler?: (event: object, reason: string) => void; + filter: QQueryFilter; } ColumnStats.defaultProps = { - closeModalHandler: null, }; const qController = Client.getInstance(); -function ColumnStats({tableMetaData, fieldMetaData, closeModalHandler}: Props): JSX.Element +function ColumnStats({tableMetaData, fieldMetaData, filter}: Props): JSX.Element { const [statusString, setStatusString] = useState("Calculating statistics..."); const [isLoaded, setIsLoaded] = useState(false); @@ -58,7 +61,12 @@ function ColumnStats({tableMetaData, fieldMetaData, closeModalHandler}: Props): { (async () => { - const processResult = await qController.processRun("tableStats", `tableName=${tableMetaData.name}&fieldName=${fieldMetaData.name}`); + const formData = new FormData(); + formData.append("tableName", tableMetaData.name); + formData.append("fieldName", fieldMetaData.name); + formData.append("filterJSON", JSON.stringify(filter)); + const processResult = await qController.processRun("tableStats", formData); + setStatusString(null) if (processResult instanceof QJobError) { @@ -72,15 +80,29 @@ function ColumnStats({tableMetaData, fieldMetaData, closeModalHandler}: Props): setCountDistinct(result.values.countDistinct); const valueCounts = [] as QRecord[]; - result.values.valueCounts.forEach((object: any) => + for(let i = 0; i < result.values.valueCounts.length; i++) { - valueCounts.push(new QRecord(object)); - }) + valueCounts.push(new QRecord(result.values.valueCounts[i])); + } setValueCounts(valueCounts); - const fakeTableMetaData = new QTableMetaData({primaryKeyField: "value", fields: {value: fieldMetaData, count: {label: "Count", type: "INTEGER"}}}); + const fakeTableMetaData = new QTableMetaData({primaryKeyField: fieldMetaData.name}); + fakeTableMetaData.fields = new Map(); + fakeTableMetaData.fields.set(fieldMetaData.name, fieldMetaData); + fakeTableMetaData.fields.set("count", new QFieldMetaData({name: "count", label: "Count", type: "INTEGER"})); + fakeTableMetaData.sections = [] as QTableSection[]; + fakeTableMetaData.sections.push(new QTableSection({fieldNames: [fieldMetaData.name, "count"]})); + const {rows, columnsToRender} = DataGridUtils.makeRows(valueCounts, fakeTableMetaData); const columns = DataGridUtils.setupGridColumns(fakeTableMetaData, columnsToRender); + + columns[1].sortComparator = (v1, v2): number => + { + const n1 = parseInt(v1.replaceAll(",", "")); + const n2 = parseInt(v2.replaceAll(",", "")); + return (n1 - n2); + } + setRows(rows); setColumns(columns); @@ -89,32 +111,71 @@ function ColumnStats({tableMetaData, fieldMetaData, closeModalHandler}: Props): })(); }, []); + // @ts-ignore + const defaultLabelDisplayedRows = ({from, to, count}) => + { + // todo - not done + return ("Showing stuff"); + }; + + function CustomPagination() + { + return ( + + ); + } + + function Loading() + { + return ( + + ); + } + return ( - Column Statistics for {tableMetaData.label} : {fieldMetaData.label} + Column Statistics for {tableMetaData.label}: {fieldMetaData.label} {statusString} - { - isLoaded && <> - - Distinct Values: {countDistinct.toLocaleString()} + +
+
Distinct Values:
{Number(countDistinct).toLocaleString()}
+
+
- (params.indexRelativeToCurrentPage % 2 === 0 ? "even" : "odd")} - /> -
- - } + + (params.indexRelativeToCurrentPage % 2 === 0 ? "even" : "odd")} + initialState={{ + sorting: { + sortModel: [ + { + field: "count", + sort: "desc", + }, + ], + }, + }} + /> +
); } diff --git a/src/qqq/pages/records/query/RecordQuery.tsx b/src/qqq/pages/records/query/RecordQuery.tsx index 0ec7894..251a121 100644 --- a/src/qqq/pages/records/query/RecordQuery.tsx +++ b/src/qqq/pages/records/query/RecordQuery.tsx @@ -169,6 +169,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element const [launchingProcess, setLaunchingProcess] = useState(launchProcess); const [recordIdsForProcess, setRecordIdsForProcess] = useState(null as string | QQueryFilter); const [columnStatsFieldName, setColumnStatsFieldName] = useState(null as string) + const [filterForColumnStats, setFilterForColumnStats] = useState(null as QQueryFilter) const instance = useRef({timer: null}); @@ -995,6 +996,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element const openColumnStatistics = async (column: GridColDef) => { + setFilterForColumnStats(buildQFilter(filterModel)); setColumnStatsFieldName(column.field); } @@ -1391,7 +1393,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element - + closeColumnStats(null, null)} disabled={false} /> diff --git a/src/qqq/styles/qqq-override-styles.css b/src/qqq/styles/qqq-override-styles.css index c01fa06..13393ff 100644 --- a/src/qqq/styles/qqq-override-styles.css +++ b/src/qqq/styles/qqq-override-styles.css @@ -350,12 +350,14 @@ input[type="search"]::-webkit-search-results-decoration { display: none; } color: rgb(52, 71, 103); font-weight: 700; padding-right: .5em; + display: inline; } .fieldValue { color: rgb(123, 128, 154); font-weight: 400; + display: inline; } .fullScreenWidget diff --git a/src/qqq/utils/DataGridUtils.tsx b/src/qqq/utils/DataGridUtils.tsx index 7c43b21..319eaca 100644 --- a/src/qqq/utils/DataGridUtils.tsx +++ b/src/qqq/utils/DataGridUtils.tsx @@ -56,7 +56,7 @@ export default class DataGridUtils if(!row["id"]) { - row["id"] = row[tableMetaData.primaryKeyField]; + row["id"] = record.values.get(tableMetaData.primaryKeyField) ?? row[tableMetaData.primaryKeyField]; } rows.push(row); From ed17a311afaeb6ff5d57b6a5c940dcfbd364d125 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 3 Mar 2023 10:07:29 -0600 Subject: [PATCH 3/6] Checkpoint - nearing releasable! --- src/qqq/pages/records/query/ColumnStats.tsx | 167 +++++++++++++------- src/qqq/utils/DataGridUtils.tsx | 7 + 2 files changed, 116 insertions(+), 58 deletions(-) diff --git a/src/qqq/pages/records/query/ColumnStats.tsx b/src/qqq/pages/records/query/ColumnStats.tsx index 1c94cfd..088de5b 100644 --- a/src/qqq/pages/records/query/ColumnStats.tsx +++ b/src/qqq/pages/records/query/ColumnStats.tsx @@ -28,13 +28,16 @@ import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter"; import {TablePagination} from "@mui/material"; import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import Grid from "@mui/material/Grid"; import LinearProgress from "@mui/material/LinearProgress"; import Typography from "@mui/material/Typography"; -import {DataGridPro} from "@mui/x-data-grid-pro"; +import {DataGridPro, GridSortModel} from "@mui/x-data-grid-pro"; import FormData from "form-data"; import React, {useEffect, useState} from "react"; import DataGridUtils from "qqq/utils/DataGridUtils"; import Client from "qqq/utils/qqq/Client"; +import ValueUtils from "qqq/utils/qqq/ValueUtils"; interface Props { @@ -51,33 +54,62 @@ const qController = Client.getInstance(); function ColumnStats({tableMetaData, fieldMetaData, filter}: Props): JSX.Element { const [statusString, setStatusString] = useState("Calculating statistics..."); - const [isLoaded, setIsLoaded] = useState(false); + const [loading, setLoading] = useState(true); const [valueCounts, setValueCounts] = useState(null as QRecord[]); + const [statsRecord, setStatsRecord] = useState(null as QRecord); + const [orderBy, setOrderBy] = useState(null as string); + const [statsFields, setStatsFields] = useState([] as QFieldMetaData[]); const [countDistinct, setCountDistinct] = useState(null as number); const [rows, setRows] = useState([]); const [columns, setColumns] = useState([]); useEffect(() => { + if(!loading) + { + return; + } + (async () => { const formData = new FormData(); formData.append("tableName", tableMetaData.name); formData.append("fieldName", fieldMetaData.name); formData.append("filterJSON", JSON.stringify(filter)); + if(orderBy) + { + formData.append("orderBy", orderBy); + } const processResult = await qController.processRun("tableStats", formData); setStatusString(null) if (processResult instanceof QJobError) { const jobError = processResult as QJobError; - // todo setErrorAlert(); - console.error("Error fetching column stats" + jobError.error); + setStatusString("Error fetching column stats: " + jobError.error); + setLoading(false); } else { const result = processResult as QJobComplete; - setCountDistinct(result.values.countDistinct); + + const statFieldObjects = result.values.statsFields; + if(statFieldObjects && statFieldObjects.length) + { + const newStatsFields = [] as QFieldMetaData[]; + for(let i = 0; i - { - const n1 = parseInt(v1.replaceAll(",", "")); - const n2 = parseInt(v2.replaceAll(",", "")); - return (n1 - n2); - } + columns[0].width = 200; + columns[1].width = 200; setRows(rows); setColumns(columns); - - setIsLoaded(true); + setLoading(false); } })(); - }, []); - - // @ts-ignore - const defaultLabelDisplayedRows = ({from, to, count}) => - { - // todo - not done - return ("Showing stuff"); - }; + }, [loading]); function CustomPagination() { return ( - + + {rows && rows.length && countDistinct && rows.length < countDistinct ? Showing the first {rows.length.toLocaleString()} of {countDistinct.toLocaleString()} values : <>} + {rows && rows.length && countDistinct && rows.length >= countDistinct && rows.length == 1 ? Showing the only value : <>} + {rows && rows.length && countDistinct && rows.length >= countDistinct && rows.length > 1 ? Showing all {rows.length.toLocaleString()} values : <>} + ); } + const refresh = () => + { + setLoading(true) + setStatusString("Refreshing...") + } + function Loading() { return ( @@ -139,43 +161,72 @@ function ColumnStats({tableMetaData, fieldMetaData, filter}: Props): JSX.Element ); } + const handleSortChange = (gridSort: GridSortModel) => + { + if (gridSort && gridSort.length > 0) + { + console.log("Sort: ", gridSort[0]); + setOrderBy(`${gridSort[0].field}.${gridSort[0].sort}`); + refresh(); + } + }; + return ( - Column Statistics for {tableMetaData.label}: {fieldMetaData.label} + Column Statistics for {fieldMetaData.label} - {statusString} + {statusString ?? <> } + - -
-
Distinct Values:
{Number(countDistinct).toLocaleString()}
-
-
- - - (params.indexRelativeToCurrentPage % 2 === 0 ? "even" : "odd")} - initialState={{ - sorting: { - sortModel: [ - { - field: "count", - sort: "desc", + + + + (params.indexRelativeToCurrentPage % 2 === 0 ? "even" : "odd")} + sortingMode={"server"} + onSortModelChange={handleSortChange} + sortingOrder={["desc", "asc"]} + pagination={true} + paginationMode={"server"} + initialState={{ + sorting: { + sortModel: [ + { + field: "count", + sort: "desc", + }, + ], }, - ], - }, - }} - /> - + }} + /> + + + + + { + statsFields && statsFields.map((field) => + ( + +
{field.label}:
+
{ValueUtils.getValueForDisplay(field, statsRecord?.values.get(field.name), statsRecord?.displayValues.get(field.name))}
+
+ )) + } +
+
+
); } diff --git a/src/qqq/utils/DataGridUtils.tsx b/src/qqq/utils/DataGridUtils.tsx index 319eaca..99c5d72 100644 --- a/src/qqq/utils/DataGridUtils.tsx +++ b/src/qqq/utils/DataGridUtils.tsx @@ -57,6 +57,13 @@ export default class DataGridUtils if(!row["id"]) { row["id"] = record.values.get(tableMetaData.primaryKeyField) ?? row[tableMetaData.primaryKeyField]; + if(row["id"] === null || row["id"] === undefined) + { + ///////////////////////////////////////////////////////////////////////////////////////// + // DataGrid gets very upset about a null or undefined here, so, try to make it happier // + ///////////////////////////////////////////////////////////////////////////////////////// + row["id"] = "--"; + } } rows.push(row); From 8f81b2b7642682b3df71ff9a779264237fd1f54d Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 13 Mar 2023 08:28:46 -0500 Subject: [PATCH 4/6] Renamed tableStats to columnStats --- src/qqq/pages/records/query/ColumnStats.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/qqq/pages/records/query/ColumnStats.tsx b/src/qqq/pages/records/query/ColumnStats.tsx index 088de5b..e784cdc 100644 --- a/src/qqq/pages/records/query/ColumnStats.tsx +++ b/src/qqq/pages/records/query/ColumnStats.tsx @@ -80,7 +80,7 @@ function ColumnStats({tableMetaData, fieldMetaData, filter}: Props): JSX.Element { formData.append("orderBy", orderBy); } - const processResult = await qController.processRun("tableStats", formData); + const processResult = await qController.processRun("columnStats", formData); setStatusString(null) if (processResult instanceof QJobError) @@ -127,8 +127,12 @@ function ColumnStats({tableMetaData, fieldMetaData, filter}: Props): JSX.Element const {rows, columnsToRender} = DataGridUtils.makeRows(valueCounts, fakeTableMetaData); const columns = DataGridUtils.setupGridColumns(fakeTableMetaData, columnsToRender); - columns[0].width = 200; - columns[1].width = 200; + columns.forEach((c) => + { + c.width = 200; + c.filterable = false; + c.hideable = false; + }) setRows(rows); setColumns(columns); @@ -192,6 +196,8 @@ function ColumnStats({tableMetaData, fieldMetaData, filter}: Props): JSX.Element rows={rows} disableSelectionOnClick columns={columns} + disableColumnSelector={true} + disableColumnPinning={true} loading={loading} rowBuffer={10} getRowClassName={(params) => (params.indexRelativeToCurrentPage % 2 === 0 ? "even" : "odd")} From b63fb034107b985ac64b2ea6ce13b23a51163b4a Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 16 Mar 2023 12:00:29 -0500 Subject: [PATCH 5/6] fix build error and add icon to refresh --- src/qqq/pages/records/query/ColumnStats.tsx | 3 ++- src/qqq/pages/records/query/RecordQuery.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/qqq/pages/records/query/ColumnStats.tsx b/src/qqq/pages/records/query/ColumnStats.tsx index e784cdc..c42c91e 100644 --- a/src/qqq/pages/records/query/ColumnStats.tsx +++ b/src/qqq/pages/records/query/ColumnStats.tsx @@ -30,6 +30,7 @@ import {TablePagination} from "@mui/material"; import Box from "@mui/material/Box"; import Button from "@mui/material/Button"; import Grid from "@mui/material/Grid"; +import Icon from "@mui/material/Icon"; import LinearProgress from "@mui/material/LinearProgress"; import Typography from "@mui/material/Typography"; import {DataGridPro, GridSortModel} from "@mui/x-data-grid-pro"; @@ -184,7 +185,7 @@ function ColumnStats({tableMetaData, fieldMetaData, filter}: Props): JSX.Element {statusString ?? <> } -
diff --git a/src/qqq/pages/records/query/RecordQuery.tsx b/src/qqq/pages/records/query/RecordQuery.tsx index 251a121..d1edbbb 100644 --- a/src/qqq/pages/records/query/RecordQuery.tsx +++ b/src/qqq/pages/records/query/RecordQuery.tsx @@ -996,7 +996,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element const openColumnStatistics = async (column: GridColDef) => { - setFilterForColumnStats(buildQFilter(filterModel)); + setFilterForColumnStats(buildQFilter(tableMetaData, filterModel)); setColumnStatsFieldName(column.field); } From db3e5ff50760300d3f2b2dc9e90c1d8232f34545 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 16 Mar 2023 12:10:40 -0500 Subject: [PATCH 6/6] add `npm run build` before `npm run start`, so a build failure gives a cleaner error to circleci --- .circleci/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index ce065e1..0ac03b9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -49,6 +49,7 @@ commands: name: Run react app and mvn verify command: | echo "HTTPS=true" >> ./.env + npm run build export REACT_APP_PROXY_LOCALHOST_PORT=8001; export PORT=3001; npm run start & dockerize -wait tcp://localhost:3001 -timeout 3m export QQQ_SELENIUM_HEADLESS=true; mvn verify