From 8a33207966c6ab5967a873b5cffb93247e8b7939 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 19 Jul 2022 13:04:40 -0500 Subject: [PATCH 1/2] QQQ-26 add export to entity-list. required http-proxy-middleware --- package.json | 1 + src/page.routes.tsx | 6 +- src/qqq/pages/entity-list/index.tsx | 86 ++++++++++++++++++++++++++--- src/qqq/pages/process-run/index.tsx | 6 +- src/setupProxy.js | 39 +++++++++++++ 5 files changed, 123 insertions(+), 15 deletions(-) create mode 100644 src/setupProxy.js diff --git a/package.json b/package.json index 660b569..8d987fc 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "flatpickr": "4.6.9", "formik": "2.2.9", "html-react-parser": "1.4.8", + "http-proxy-middleware": "2.0.6", "react": "17.0.2", "react-chartjs-2": "3.0.4", "react-dom": "17.0.2", diff --git a/src/page.routes.tsx b/src/page.routes.tsx index 3974ada..93ca6ce 100644 --- a/src/page.routes.tsx +++ b/src/page.routes.tsx @@ -76,9 +76,9 @@ const pageRoutes = [ name: "pricing page", route: "/pages/pricing-page", }, - { name: "RTL", route: "/pages/rtl" }, - { name: "widgets", route: "/pages/widgets" }, - { name: "charts", route: "/pages/charts" }, + {name: "RTL", route: "/pages/rtl"}, + {name: "widgets", route: "/pages/widgets"}, + {name: "charts", route: "/pages/charts"}, { name: "notfications", route: "/pages/notifications", diff --git a/src/qqq/pages/entity-list/index.tsx b/src/qqq/pages/entity-list/index.tsx index 876fb81..11303d9 100644 --- a/src/qqq/pages/entity-list/index.tsx +++ b/src/qqq/pages/entity-list/index.tsx @@ -20,7 +20,7 @@ import Icon from "@mui/material/Icon"; import Menu from "@mui/material/Menu"; import MenuItem from "@mui/material/MenuItem"; import Link from "@mui/material/Link"; -import {Alert} from "@mui/material"; +import {Alert, tableFooterClasses} from "@mui/material"; import { DataGridPro, GridCallbackDetails, @@ -38,7 +38,9 @@ import { GridToolbarContainer, GridToolbarDensitySelector, GridToolbarExport, + GridToolbarExportContainer, GridToolbarFilterButton, + GridExportMenuItemProps, } from "@mui/x-data-grid-pro"; // Material Dashboard 2 PRO React TS components @@ -96,6 +98,7 @@ function EntityList({table}: Props): JSX.Element const [buttonText, setButtonText] = useState(""); const [tableState, setTableState] = useState(""); + const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData); const [, setFiltersMenu] = useState(null); const [actionsMenu, setActionsMenu] = useState(null); const [tableProcesses, setTableProcesses] = useState([] as QProcessMetaData[]); @@ -189,11 +192,12 @@ function EntityList({table}: Props): JSX.Element { (async () => { - const tableMetaData = await QClient.loadTableMetaData(tableName); + const newTableMetaData = await QClient.loadTableMetaData(tableName); + setTableMetaData(newTableMetaData); if (columnSortModel.length === 0) { columnSortModel.push({ - field: tableMetaData.primaryKeyField, + field: newTableMetaData.primaryKeyField, sort: "desc", }); setColumnSortModel(columnSortModel); @@ -203,8 +207,8 @@ function EntityList({table}: Props): JSX.Element const count = await QClient.count(tableName, qFilter); setTotalRecords(count); - setButtonText(`new ${tableMetaData.label}`); - setTableLabel(tableMetaData.label); + setButtonText(`new ${newTableMetaData.label}`); + setTableLabel(newTableMetaData.label); const columns = [] as GridColDef[]; @@ -233,10 +237,10 @@ function EntityList({table}: Props): JSX.Element rows.push(Object.fromEntries(record.values.entries())); }); - const sortedKeys = [...tableMetaData.fields.keys()].sort(); + const sortedKeys = [...newTableMetaData.fields.keys()].sort(); sortedKeys.forEach((key) => { - const field = tableMetaData.fields.get(key); + const field = newTableMetaData.fields.get(key); let columnType = "string"; switch (field.type) @@ -265,7 +269,7 @@ function EntityList({table}: Props): JSX.Element width: 200, }; - if (key === tableMetaData.primaryKeyField) + if (key === newTableMetaData.primaryKeyField) { column.width = 75; columns.splice(0, 0, column); @@ -366,6 +370,67 @@ function EntityList({table}: Props): JSX.Element })(); } + interface QExportMenuItemProps extends GridExportMenuItemProps<{}> + { + format: string; + } + + function ExportMenuItem(props: QExportMenuItemProps) + { + const {format, hideMenu} = props; + + return ( + + { + /////////////////////////////////////////////////////////////////////////////// + // build the list of visible fields. note, not doing them in-order (in case // + // the user did drag & drop), because column order model isn't right yet // + // so just doing them to match columns (which were pKey, then sorted) // + /////////////////////////////////////////////////////////////////////////////// + const visibleFields: string[] = []; + columns.forEach((gridColumn) => + { + const fieldName = gridColumn.field; + // @ts-ignore + if (columnVisibilityModel[fieldName] !== false) + { + visibleFields.push(fieldName); + } + }); + + const zp = (value: number): string => (value < 10 ? `0${value}` : `${value}`); + + ////////////////////////////////////// + // construct the url for the export // + ////////////////////////////////////// + const d = new Date(); + const dateString = `${d.getFullYear()}-${zp(d.getMonth())}-${zp(d.getDate())} ${zp(d.getHours())}${zp(d.getMinutes())}`; + const filename = `${tableMetaData.label} Export ${dateString}.${format}`; + const url = `/data/${tableMetaData.name}/export/${filename}?filter=${JSON.stringify(buildQFilter())}&fields=${visibleFields.join(",")}`; + + //////////////////////////////////// + // create an 'a' tag and click it // + //////////////////////////////////// + const a = document.createElement("a"); + a.href = url; + a.download = filename; + a.target = "_blank"; + a.click(); + + /////////////////////////////////////////// + // Hide the export menu after the export // + /////////////////////////////////////////// + hideMenu?.(); + }} + > + Export + {` ${format.toUpperCase()}`} + + ); + } + function CustomToolbar() { return ( @@ -373,7 +438,10 @@ function EntityList({table}: Props): JSX.Element - + + + +
{ selectFullFilterState === "checked" && ( diff --git a/src/qqq/pages/process-run/index.tsx b/src/qqq/pages/process-run/index.tsx index 1e7f4c4..a024d50 100644 --- a/src/qqq/pages/process-run/index.tsx +++ b/src/qqq/pages/process-run/index.tsx @@ -46,8 +46,8 @@ import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJo import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError"; import {QJobRunning} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobRunning"; import { - DataGrid, GridColDef, GridRowParams, GridRowsProp, -} from "@mui/x-data-grid"; + DataGridPro, GridColDef, GridRowParams, GridRowsProp, +} from "@mui/x-data-grid-pro"; import QDynamicForm from "../../components/QDynamicForm"; import MDTypography from "../../../components/MDTypography"; @@ -108,7 +108,7 @@ function getDynamicStepContent( {" "}
- . + */ + +///////////////////////////////////////////////////////////////////////////////////// +// this file "magically" works with http-proxy-middleware. // +// Most API calls to the qqq backend (e.g., through QController) do NOT go through // +// the React Router. However, exports do (presumably because they are full- // +// page style requests, not ajax/fetches), so they need specific proxy config. // +///////////////////////////////////////////////////////////////////////////////////// +const {createProxyMiddleware} = require("http-proxy-middleware"); + +module.exports = function (app) +{ + app.use( + "/data/*/export/*", + createProxyMiddleware({ + target: "http://localhost:8000", + changeOrigin: true, + }), + ); +}; From f502d62f7343eea7858589bb7fd5b9fc6cfed684 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 19 Jul 2022 15:57:44 -0500 Subject: [PATCH 2/2] QQQ-26 switch from a-href to open exports, to do a little window.open instead --- src/qqq/pages/entity-list/index.tsx | 34 +++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/src/qqq/pages/entity-list/index.tsx b/src/qqq/pages/entity-list/index.tsx index 11303d9..961ca48 100644 --- a/src/qqq/pages/entity-list/index.tsx +++ b/src/qqq/pages/entity-list/index.tsx @@ -400,6 +400,9 @@ function EntityList({table}: Props): JSX.Element } }); + /////////////////////// + // zero-pad function // + /////////////////////// const zp = (value: number): string => (value < 10 ? `0${value}` : `${value}`); ////////////////////////////////////// @@ -408,16 +411,29 @@ function EntityList({table}: Props): JSX.Element const d = new Date(); const dateString = `${d.getFullYear()}-${zp(d.getMonth())}-${zp(d.getDate())} ${zp(d.getHours())}${zp(d.getMinutes())}`; const filename = `${tableMetaData.label} Export ${dateString}.${format}`; - const url = `/data/${tableMetaData.name}/export/${filename}?filter=${JSON.stringify(buildQFilter())}&fields=${visibleFields.join(",")}`; + const url = `/data/${tableMetaData.name}/export/${filename}?filter=${encodeURIComponent(JSON.stringify(buildQFilter()))}&fields=${visibleFields.join(",")}`; - //////////////////////////////////// - // create an 'a' tag and click it // - //////////////////////////////////// - const a = document.createElement("a"); - a.href = url; - a.download = filename; - a.target = "_blank"; - a.click(); + ////////////////////////////////////////////////////////////////////////////////////// + // open a window (tab) with a little page that says the file is being generated. // + // then have that page load the url for the export. // + // If there's an error, it'll appear in that window. else, the file will download. // + ////////////////////////////////////////////////////////////////////////////////////// + const exportWindow = window.open("", "_blank"); + exportWindow.document.write(` + + + ${filename} + + + Generating file ${filename} with ${totalRecords.toLocaleString()} records... + `); /////////////////////////////////////////// // Hide the export menu after the export //