mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-18 05:10:45 +00:00
QQQ-26 add export to entity-list. required http-proxy-middleware
This commit is contained in:
@ -34,6 +34,7 @@
|
|||||||
"flatpickr": "4.6.9",
|
"flatpickr": "4.6.9",
|
||||||
"formik": "2.2.9",
|
"formik": "2.2.9",
|
||||||
"html-react-parser": "1.4.8",
|
"html-react-parser": "1.4.8",
|
||||||
|
"http-proxy-middleware": "2.0.6",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-chartjs-2": "3.0.4",
|
"react-chartjs-2": "3.0.4",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
|
@ -20,7 +20,7 @@ import Icon from "@mui/material/Icon";
|
|||||||
import Menu from "@mui/material/Menu";
|
import Menu from "@mui/material/Menu";
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import Link from "@mui/material/Link";
|
import Link from "@mui/material/Link";
|
||||||
import {Alert} from "@mui/material";
|
import {Alert, tableFooterClasses} from "@mui/material";
|
||||||
import {
|
import {
|
||||||
DataGridPro,
|
DataGridPro,
|
||||||
GridCallbackDetails,
|
GridCallbackDetails,
|
||||||
@ -38,7 +38,9 @@ import {
|
|||||||
GridToolbarContainer,
|
GridToolbarContainer,
|
||||||
GridToolbarDensitySelector,
|
GridToolbarDensitySelector,
|
||||||
GridToolbarExport,
|
GridToolbarExport,
|
||||||
|
GridToolbarExportContainer,
|
||||||
GridToolbarFilterButton,
|
GridToolbarFilterButton,
|
||||||
|
GridExportMenuItemProps,
|
||||||
} from "@mui/x-data-grid-pro";
|
} from "@mui/x-data-grid-pro";
|
||||||
|
|
||||||
// Material Dashboard 2 PRO React TS components
|
// Material Dashboard 2 PRO React TS components
|
||||||
@ -96,6 +98,7 @@ function EntityList({table}: Props): JSX.Element
|
|||||||
|
|
||||||
const [buttonText, setButtonText] = useState("");
|
const [buttonText, setButtonText] = useState("");
|
||||||
const [tableState, setTableState] = useState("");
|
const [tableState, setTableState] = useState("");
|
||||||
|
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
|
||||||
const [, setFiltersMenu] = useState(null);
|
const [, setFiltersMenu] = useState(null);
|
||||||
const [actionsMenu, setActionsMenu] = useState(null);
|
const [actionsMenu, setActionsMenu] = useState(null);
|
||||||
const [tableProcesses, setTableProcesses] = useState([] as QProcessMetaData[]);
|
const [tableProcesses, setTableProcesses] = useState([] as QProcessMetaData[]);
|
||||||
@ -189,11 +192,12 @@ function EntityList({table}: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
(async () =>
|
(async () =>
|
||||||
{
|
{
|
||||||
const tableMetaData = await QClient.loadTableMetaData(tableName);
|
const newTableMetaData = await QClient.loadTableMetaData(tableName);
|
||||||
|
setTableMetaData(newTableMetaData);
|
||||||
if (columnSortModel.length === 0)
|
if (columnSortModel.length === 0)
|
||||||
{
|
{
|
||||||
columnSortModel.push({
|
columnSortModel.push({
|
||||||
field: tableMetaData.primaryKeyField,
|
field: newTableMetaData.primaryKeyField,
|
||||||
sort: "desc",
|
sort: "desc",
|
||||||
});
|
});
|
||||||
setColumnSortModel(columnSortModel);
|
setColumnSortModel(columnSortModel);
|
||||||
@ -203,8 +207,8 @@ function EntityList({table}: Props): JSX.Element
|
|||||||
|
|
||||||
const count = await QClient.count(tableName, qFilter);
|
const count = await QClient.count(tableName, qFilter);
|
||||||
setTotalRecords(count);
|
setTotalRecords(count);
|
||||||
setButtonText(`new ${tableMetaData.label}`);
|
setButtonText(`new ${newTableMetaData.label}`);
|
||||||
setTableLabel(tableMetaData.label);
|
setTableLabel(newTableMetaData.label);
|
||||||
|
|
||||||
const columns = [] as GridColDef[];
|
const columns = [] as GridColDef[];
|
||||||
|
|
||||||
@ -233,10 +237,10 @@ function EntityList({table}: Props): JSX.Element
|
|||||||
rows.push(Object.fromEntries(record.values.entries()));
|
rows.push(Object.fromEntries(record.values.entries()));
|
||||||
});
|
});
|
||||||
|
|
||||||
const sortedKeys = [...tableMetaData.fields.keys()].sort();
|
const sortedKeys = [...newTableMetaData.fields.keys()].sort();
|
||||||
sortedKeys.forEach((key) =>
|
sortedKeys.forEach((key) =>
|
||||||
{
|
{
|
||||||
const field = tableMetaData.fields.get(key);
|
const field = newTableMetaData.fields.get(key);
|
||||||
|
|
||||||
let columnType = "string";
|
let columnType = "string";
|
||||||
switch (field.type)
|
switch (field.type)
|
||||||
@ -265,7 +269,7 @@ function EntityList({table}: Props): JSX.Element
|
|||||||
width: 200,
|
width: 200,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (key === tableMetaData.primaryKeyField)
|
if (key === newTableMetaData.primaryKeyField)
|
||||||
{
|
{
|
||||||
column.width = 75;
|
column.width = 75;
|
||||||
columns.splice(0, 0, column);
|
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 (
|
||||||
|
<MenuItem
|
||||||
|
disabled={totalRecords === 0}
|
||||||
|
onClick={() =>
|
||||||
|
{
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// 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()}`}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function CustomToolbar()
|
function CustomToolbar()
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
@ -373,7 +438,10 @@ function EntityList({table}: Props): JSX.Element
|
|||||||
<GridToolbarColumnsButton />
|
<GridToolbarColumnsButton />
|
||||||
<GridToolbarFilterButton />
|
<GridToolbarFilterButton />
|
||||||
<GridToolbarDensitySelector />
|
<GridToolbarDensitySelector />
|
||||||
<GridToolbarExport />
|
<GridToolbarExportContainer>
|
||||||
|
<ExportMenuItem format="csv" />
|
||||||
|
<ExportMenuItem format="xlsx" />
|
||||||
|
</GridToolbarExportContainer>
|
||||||
<div>
|
<div>
|
||||||
{
|
{
|
||||||
selectFullFilterState === "checked" && (
|
selectFullFilterState === "checked" && (
|
||||||
|
@ -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 {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
|
||||||
import {QJobRunning} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobRunning";
|
import {QJobRunning} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobRunning";
|
||||||
import {
|
import {
|
||||||
DataGrid, GridColDef, GridRowParams, GridRowsProp,
|
DataGridPro, GridColDef, GridRowParams, GridRowsProp,
|
||||||
} from "@mui/x-data-grid";
|
} from "@mui/x-data-grid-pro";
|
||||||
import QDynamicForm from "../../components/QDynamicForm";
|
import QDynamicForm from "../../components/QDynamicForm";
|
||||||
import MDTypography from "../../../components/MDTypography";
|
import MDTypography from "../../../components/MDTypography";
|
||||||
|
|
||||||
@ -108,7 +108,7 @@ function getDynamicStepContent(
|
|||||||
{" "}
|
{" "}
|
||||||
<br />
|
<br />
|
||||||
<MDBox height="100%">
|
<MDBox height="100%">
|
||||||
<DataGrid
|
<DataGridPro
|
||||||
page={recordConfig.pageNo}
|
page={recordConfig.pageNo}
|
||||||
disableSelectionOnClick
|
disableSelectionOnClick
|
||||||
autoHeight
|
autoHeight
|
||||||
|
39
src/setupProxy.js
Normal file
39
src/setupProxy.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2022. 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// 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,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
Reference in New Issue
Block a user