mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-22 15:18:44 +00:00
Compare commits
11 Commits
snapshot-f
...
snapshot-f
Author | SHA1 | Date | |
---|---|---|---|
575ffe761f | |||
0949ee9f78 | |||
f41b71d3c7 | |||
57fefe9671 | |||
8a018c34f6 | |||
1b4f70a547 | |||
1f343abbb5 | |||
7ea50dd7bb | |||
b6b7d8d8b3 | |||
7bf515554d | |||
7fa42a6eb5 |
@ -33,6 +33,7 @@
|
|||||||
"html-react-parser": "1.4.8",
|
"html-react-parser": "1.4.8",
|
||||||
"html-to-text": "^9.0.5",
|
"html-to-text": "^9.0.5",
|
||||||
"http-proxy-middleware": "2.0.6",
|
"http-proxy-middleware": "2.0.6",
|
||||||
|
"jwt-decode": "3.1.2",
|
||||||
"rapidoc": "9.3.4",
|
"rapidoc": "9.3.4",
|
||||||
"react": "18.0.0",
|
"react": "18.0.0",
|
||||||
"react-ace": "10.1.0",
|
"react-ace": "10.1.0",
|
||||||
|
121
src/App.tsx
121
src/App.tsx
@ -33,7 +33,8 @@ import CssBaseline from "@mui/material/CssBaseline";
|
|||||||
import Icon from "@mui/material/Icon";
|
import Icon from "@mui/material/Icon";
|
||||||
import {ThemeProvider} from "@mui/material/styles";
|
import {ThemeProvider} from "@mui/material/styles";
|
||||||
import {LicenseInfo} from "@mui/x-license-pro";
|
import {LicenseInfo} from "@mui/x-license-pro";
|
||||||
import React, {JSXElementConstructor, Key, ReactElement, useContext, useEffect, useState,} from "react";
|
import jwt_decode from "jwt-decode";
|
||||||
|
import React, {JSXElementConstructor, Key, ReactElement, useEffect, useState,} from "react";
|
||||||
import {useCookies} from "react-cookie";
|
import {useCookies} from "react-cookie";
|
||||||
import {Navigate, Route, Routes, useLocation,} from "react-router-dom";
|
import {Navigate, Route, Routes, useLocation,} from "react-router-dom";
|
||||||
import {Md5} from "ts-md5/dist/md5";
|
import {Md5} from "ts-md5/dist/md5";
|
||||||
@ -57,11 +58,11 @@ import ProcessUtils from "qqq/utils/qqq/ProcessUtils";
|
|||||||
|
|
||||||
|
|
||||||
const qController = Client.getInstance();
|
const qController = Client.getInstance();
|
||||||
export const SESSION_ID_COOKIE_NAME = "sessionId";
|
export const SESSION_UUID_COOKIE_NAME = "sessionUUID";
|
||||||
|
|
||||||
export default function App()
|
export default function App()
|
||||||
{
|
{
|
||||||
const [, setCookie, removeCookie] = useCookies([SESSION_ID_COOKIE_NAME]);
|
const [cookies, setCookie, removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
|
||||||
const {user, getAccessTokenSilently, logout} = useAuth0();
|
const {user, getAccessTokenSilently, logout} = useAuth0();
|
||||||
const [loadingToken, setLoadingToken] = useState(false);
|
const [loadingToken, setLoadingToken] = useState(false);
|
||||||
const [isFullyAuthenticated, setIsFullyAuthenticated] = useState(false);
|
const [isFullyAuthenticated, setIsFullyAuthenticated] = useState(false);
|
||||||
@ -69,8 +70,62 @@ export default function App()
|
|||||||
const [branding, setBranding] = useState({} as QBrandingMetaData);
|
const [branding, setBranding] = useState({} as QBrandingMetaData);
|
||||||
const [metaData, setMetaData] = useState({} as QInstance);
|
const [metaData, setMetaData] = useState({} as QInstance);
|
||||||
const [needLicenseKey, setNeedLicenseKey] = useState(true);
|
const [needLicenseKey, setNeedLicenseKey] = useState(true);
|
||||||
|
const [loggedInUser, setLoggedInUser] = useState({} as { name?: string, email?: string });
|
||||||
const [defaultRoute, setDefaultRoute] = useState("/no-apps");
|
const [defaultRoute, setDefaultRoute] = useState("/no-apps");
|
||||||
|
|
||||||
|
const shouldStoreNewToken = (newToken: string, oldToken: string): boolean =>
|
||||||
|
{
|
||||||
|
if (!cookies[SESSION_UUID_COOKIE_NAME])
|
||||||
|
{
|
||||||
|
console.log("No session uuid cookie - so we should store a new one.");
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!oldToken)
|
||||||
|
{
|
||||||
|
console.log("No accessToken in localStorage - so we should store a new one.");
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const oldJSON: any = jwt_decode(oldToken);
|
||||||
|
const newJSON: any = jwt_decode(newToken);
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if the old (local storage) token is expired, then we need to store the new one //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
const oldExp = oldJSON["exp"];
|
||||||
|
if(oldExp * 1000 < (new Date().getTime()))
|
||||||
|
{
|
||||||
|
console.log("Access token in local storage was expired - so we should store a new one.");
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// remove the exp & iat values from what we compare - as they are always different from auth0 //
|
||||||
|
// note, this is only deleting them from what we compare, not from what we'd store. //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
delete newJSON["exp"]
|
||||||
|
delete newJSON["iat"]
|
||||||
|
delete oldJSON["exp"]
|
||||||
|
delete oldJSON["iat"]
|
||||||
|
|
||||||
|
const different = JSON.stringify(newJSON) !== JSON.stringify(oldJSON);
|
||||||
|
if(different)
|
||||||
|
{
|
||||||
|
console.log("Latest access token from auth0 has changed vs localStorage - so we should store a new one.");
|
||||||
|
}
|
||||||
|
return (different);
|
||||||
|
}
|
||||||
|
catch(e)
|
||||||
|
{
|
||||||
|
console.log("Caught in shouldStoreNewToken: " + e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (true);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
if (loadingToken)
|
if (loadingToken)
|
||||||
@ -92,20 +147,48 @@ export default function App()
|
|||||||
{
|
{
|
||||||
console.log("Loading token from auth0...");
|
console.log("Loading token from auth0...");
|
||||||
const accessToken = await getAccessTokenSilently();
|
const accessToken = await getAccessTokenSilently();
|
||||||
qController.setAuthorizationHeaderValue("Bearer " + accessToken);
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////
|
const lsAccessToken = localStorage.getItem("accessToken");
|
||||||
// we've stopped using session id cook with auth0, so make sure it is not set. //
|
if (shouldStoreNewToken(accessToken, lsAccessToken))
|
||||||
/////////////////////////////////////////////////////////////////////////////////
|
{
|
||||||
removeCookie(SESSION_ID_COOKIE_NAME);
|
console.log("Sending accessToken to backend, requesting a sessionUUID...");
|
||||||
|
const newSessionUuid = await qController.manageSession(accessToken, null);
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// the request to the backend should send a header to set the cookie, so we don't need to do it ourselves. //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// setCookie(SESSION_UUID_COOKIE_NAME, newSessionUuid, {path: "/"});
|
||||||
|
|
||||||
|
localStorage.setItem("accessToken", accessToken);
|
||||||
|
console.log("Got new sessionUUID from backend, and stored new accessToken");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
console.log("Using existing sessionUUID cookie");
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// todo#authHeader - this is our quick rollback plan - if we feel the need to stop using the cookie approach. //
|
||||||
|
// we turn off the shouldStoreNewToken block above, and turn on these 2 lines. //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
||||||
|
localStorage.removeItem("accessToken");
|
||||||
|
*/
|
||||||
|
|
||||||
setIsFullyAuthenticated(true);
|
setIsFullyAuthenticated(true);
|
||||||
|
qController.setGotAuthentication();
|
||||||
|
qController.setAuthorizationHeaderValue("Bearer " + accessToken);
|
||||||
|
|
||||||
|
setLoggedInUser(user);
|
||||||
console.log("Token load complete.");
|
console.log("Token load complete.");
|
||||||
}
|
}
|
||||||
catch (e)
|
catch (e)
|
||||||
{
|
{
|
||||||
console.log(`Error loading token: ${JSON.stringify(e)}`);
|
console.log(`Error loading token: ${JSON.stringify(e)}`);
|
||||||
qController.clearAuthenticationMetaDataLocalStorage();
|
qController.clearAuthenticationMetaDataLocalStorage();
|
||||||
|
localStorage.removeItem("accessToken")
|
||||||
|
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
||||||
logout();
|
logout();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -116,9 +199,9 @@ export default function App()
|
|||||||
// use a random token if anonymous or mock //
|
// use a random token if anonymous or mock //
|
||||||
/////////////////////////////////////////////
|
/////////////////////////////////////////////
|
||||||
console.log("Generating random token...");
|
console.log("Generating random token...");
|
||||||
qController.setAuthorizationHeaderValue(null);
|
qController.setAuthorizationHeaderValue(Md5.hashStr(`${new Date()}`));
|
||||||
setIsFullyAuthenticated(true);
|
setIsFullyAuthenticated(true);
|
||||||
setCookie(SESSION_ID_COOKIE_NAME, Md5.hashStr(`${new Date()}`), {path: "/"});
|
setCookie(SESSION_UUID_COOKIE_NAME, Md5.hashStr(`${new Date()}`), {path: "/"});
|
||||||
console.log("Token generation complete.");
|
console.log("Token generation complete.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -149,7 +232,7 @@ export default function App()
|
|||||||
const [needToLoadRoutes, setNeedToLoadRoutes] = useState(true);
|
const [needToLoadRoutes, setNeedToLoadRoutes] = useState(true);
|
||||||
const [sideNavRoutes, setSideNavRoutes] = useState([]);
|
const [sideNavRoutes, setSideNavRoutes] = useState([]);
|
||||||
const [appRoutes, setAppRoutes] = useState(null as any);
|
const [appRoutes, setAppRoutes] = useState(null as any);
|
||||||
const [pathToLabelMap, setPathToLabelMap] = useState({} as {[path: string]: string});
|
const [pathToLabelMap, setPathToLabelMap] = useState({} as { [path: string]: string });
|
||||||
|
|
||||||
////////////////////////////////////////////
|
////////////////////////////////////////////
|
||||||
// load qqq meta data to make more routes //
|
// load qqq meta data to make more routes //
|
||||||
@ -267,14 +350,14 @@ export default function App()
|
|||||||
name: `${app.label}`,
|
name: `${app.label}`,
|
||||||
key: app.name,
|
key: app.name,
|
||||||
route: path,
|
route: path,
|
||||||
component: <RecordQuery table={table} key={table.name}/>,
|
component: <RecordQuery table={table} key={table.name} />,
|
||||||
});
|
});
|
||||||
|
|
||||||
routeList.push({
|
routeList.push({
|
||||||
name: `${app.label}`,
|
name: `${app.label}`,
|
||||||
key: app.name,
|
key: app.name,
|
||||||
route: `${path}/savedFilter/:id`,
|
route: `${path}/savedFilter/:id`,
|
||||||
component: <RecordQuery table={table} key={table.name}/>,
|
component: <RecordQuery table={table} key={table.name} />,
|
||||||
});
|
});
|
||||||
|
|
||||||
routeList.push({
|
routeList.push({
|
||||||
@ -429,11 +512,11 @@ export default function App()
|
|||||||
|
|
||||||
let profileRoutes = {};
|
let profileRoutes = {};
|
||||||
const gravatarBase = "https://www.gravatar.com/avatar/";
|
const gravatarBase = "https://www.gravatar.com/avatar/";
|
||||||
const hash = Md5.hashStr(user?.email || "user");
|
const hash = Md5.hashStr(loggedInUser?.email || "user");
|
||||||
const profilePicture = `${gravatarBase}${hash}`;
|
const profilePicture = `${gravatarBase}${hash}`;
|
||||||
profileRoutes = {
|
profileRoutes = {
|
||||||
type: "collapse",
|
type: "collapse",
|
||||||
name: user?.name,
|
name: loggedInUser?.name ?? "Anonymous",
|
||||||
key: "username",
|
key: "username",
|
||||||
noCollapse: true,
|
noCollapse: true,
|
||||||
icon: <Avatar src={profilePicture} alt="{user?.name}" />,
|
icon: <Avatar src={profilePicture} alt="{user?.name}" />,
|
||||||
@ -469,7 +552,7 @@ export default function App()
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pathToLabelMap: {[path: string]: string} = {}
|
const pathToLabelMap: {[path: string]: string} = {}
|
||||||
for(let i =0; i<appRoutesList.length; i++)
|
for (let i = 0; i < appRoutesList.length; i++)
|
||||||
{
|
{
|
||||||
const route = appRoutesList[i];
|
const route = appRoutesList[i];
|
||||||
pathToLabelMap[route.route] = route.name;
|
pathToLabelMap[route.route] = route.name;
|
||||||
@ -495,7 +578,10 @@ export default function App()
|
|||||||
{
|
{
|
||||||
if ((e as QException).status === "401")
|
if ((e as QException).status === "401")
|
||||||
{
|
{
|
||||||
|
console.log("Exception is a QException with status = 401. Clearing some of localStorage & cookies");
|
||||||
qController.clearAuthenticationMetaDataLocalStorage();
|
qController.clearAuthenticationMetaDataLocalStorage();
|
||||||
|
localStorage.removeItem("accessToken")
|
||||||
|
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
||||||
|
|
||||||
//////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////
|
||||||
// todo - this is auth0 logout... make more generic //
|
// todo - this is auth0 logout... make more generic //
|
||||||
@ -596,7 +682,7 @@ export default function App()
|
|||||||
}}>
|
}}>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<CommandMenu metaData={metaData}/>
|
<CommandMenu metaData={metaData} />
|
||||||
<Sidenav
|
<Sidenav
|
||||||
color={sidenavColor}
|
color={sidenavColor}
|
||||||
icon={branding.icon}
|
icon={branding.icon}
|
||||||
@ -604,7 +690,6 @@ export default function App()
|
|||||||
appName={branding.appName}
|
appName={branding.appName}
|
||||||
branding={branding}
|
branding={branding}
|
||||||
routes={sideNavRoutes}
|
routes={sideNavRoutes}
|
||||||
pathToLabelMap={pathToLabelMap}
|
|
||||||
onMouseEnter={handleOnMouseEnter}
|
onMouseEnter={handleOnMouseEnter}
|
||||||
onMouseLeave={handleOnMouseLeave}
|
onMouseLeave={handleOnMouseLeave}
|
||||||
/>
|
/>
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
import {useAuth0} from "@auth0/auth0-react";
|
import {useAuth0} from "@auth0/auth0-react";
|
||||||
import React, {useEffect} from "react";
|
import React, {useEffect} from "react";
|
||||||
import {useCookies} from "react-cookie";
|
import {useCookies} from "react-cookie";
|
||||||
import {SESSION_ID_COOKIE_NAME} from "App";
|
import {SESSION_UUID_COOKIE_NAME} from "App";
|
||||||
|
|
||||||
interface Props
|
interface Props
|
||||||
{
|
{
|
||||||
@ -33,13 +33,13 @@ interface Props
|
|||||||
function HandleAuthorizationError({errorMessage}: Props)
|
function HandleAuthorizationError({errorMessage}: Props)
|
||||||
{
|
{
|
||||||
|
|
||||||
const [, , removeCookie] = useCookies([SESSION_ID_COOKIE_NAME]);
|
const [, , removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
|
||||||
const {logout} = useAuth0();
|
const {logout} = useAuth0();
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
logout();
|
logout();
|
||||||
removeCookie(SESSION_ID_COOKIE_NAME, {path: "/"});
|
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||||
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
||||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||||
|
import {QTableVariant} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableVariant";
|
||||||
import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator";
|
import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator";
|
||||||
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
|
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
|
||||||
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
||||||
@ -33,6 +34,7 @@ import DialogContent from "@mui/material/DialogContent";
|
|||||||
import DialogTitle from "@mui/material/DialogTitle";
|
import DialogTitle from "@mui/material/DialogTitle";
|
||||||
import Grid from "@mui/material/Grid";
|
import Grid from "@mui/material/Grid";
|
||||||
import Icon from "@mui/material/Icon";
|
import Icon from "@mui/material/Icon";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import React, {useState} from "react";
|
import React, {useState} from "react";
|
||||||
import {useNavigate} from "react-router-dom";
|
import {useNavigate} from "react-router-dom";
|
||||||
@ -45,8 +47,10 @@ interface Props
|
|||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
metaData: QInstance;
|
metaData: QInstance;
|
||||||
tableMetaData: QTableMetaData;
|
tableMetaData: QTableMetaData;
|
||||||
|
tableVariant?: QTableVariant;
|
||||||
closeHandler: () => void;
|
closeHandler: () => void;
|
||||||
mayClose: boolean;
|
mayClose: boolean;
|
||||||
|
subHeader?: JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
GotoRecordDialog.defaultProps = {
|
GotoRecordDialog.defaultProps = {
|
||||||
@ -155,15 +159,17 @@ function GotoRecordDialog(props: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
setError("");
|
setError("");
|
||||||
const filter = new QQueryFilter([new QFilterCriteria(fieldName, QCriteriaOperator.EQUALS, [values[fieldName]])], null, "AND", null, 10);
|
const filter = new QQueryFilter([new QFilterCriteria(fieldName, QCriteriaOperator.EQUALS, [values[fieldName]])], null, "AND", null, 10);
|
||||||
const queryResult = await qController.query(props.tableMetaData.name, filter)
|
try
|
||||||
if(queryResult.length == 0)
|
{
|
||||||
|
const queryResult = await qController.query(props.tableMetaData.name, filter, null, props.tableVariant)
|
||||||
|
if (queryResult.length == 0)
|
||||||
{
|
{
|
||||||
setError("Record not found.");
|
setError("Record not found.");
|
||||||
setTimeout(() => setError(""), 3000);
|
setTimeout(() => setError(""), 3000);
|
||||||
}
|
}
|
||||||
else if(queryResult.length == 1)
|
else if (queryResult.length == 1)
|
||||||
{
|
{
|
||||||
navigate(`${props.metaData.getTablePathByName(props.tableMetaData.name)}/${queryResult[0].values.get(props.tableMetaData.primaryKeyField)}`);
|
navigate(`${props.metaData.getTablePathByName(props.tableMetaData.name)}/${encodeURIComponent(queryResult[0].values.get(props.tableMetaData.primaryKeyField))}`);
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -172,6 +178,13 @@ function GotoRecordDialog(props: Props): JSX.Element
|
|||||||
setTimeout(() => setError(""), 3000);
|
setTimeout(() => setError(""), 3000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch(e)
|
||||||
|
{
|
||||||
|
// @ts-ignore
|
||||||
|
setError(`Error: ${(e && e.message) ? e.message : e}`);
|
||||||
|
setTimeout(() => setError(""), 6000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if(props.tableMetaData)
|
if(props.tableMetaData)
|
||||||
{
|
{
|
||||||
@ -183,8 +196,19 @@ function GotoRecordDialog(props: Props): JSX.Element
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={props.isOpen} onClose={() => closeRequested} onKeyPress={(e) => keyPressed(e)} fullWidth maxWidth={"sm"}>
|
<Dialog open={props.isOpen} onClose={() => closeRequested} onKeyPress={(e) => keyPressed(e)} fullWidth maxWidth={"sm"}>
|
||||||
<DialogTitle>Go To...</DialogTitle>
|
<DialogTitle sx={{display: "flex"}}>
|
||||||
|
<Box sx={{display: "flex", flexGrow: 1}}>
|
||||||
|
Go To...
|
||||||
|
</Box>
|
||||||
|
<Box sx={{display: "flex"}}>
|
||||||
|
<IconButton onClick={() =>
|
||||||
|
{
|
||||||
|
document.location.href = "/";
|
||||||
|
}}><Icon sx={{align: "right"}} fontSize="small">close</Icon></IconButton>
|
||||||
|
</Box>
|
||||||
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
|
{props.subHeader}
|
||||||
{
|
{
|
||||||
fields.map((field, index) =>
|
fields.map((field, index) =>
|
||||||
(
|
(
|
||||||
@ -237,9 +261,11 @@ interface GotoRecordButtonProps
|
|||||||
{
|
{
|
||||||
metaData: QInstance;
|
metaData: QInstance;
|
||||||
tableMetaData: QTableMetaData;
|
tableMetaData: QTableMetaData;
|
||||||
|
tableVariant?: QTableVariant;
|
||||||
autoOpen?: boolean;
|
autoOpen?: boolean;
|
||||||
buttonVisible?: boolean;
|
buttonVisible?: boolean;
|
||||||
mayClose?: boolean;
|
mayClose?: boolean;
|
||||||
|
subHeader?: JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
GotoRecordButton.defaultProps = {
|
GotoRecordButton.defaultProps = {
|
||||||
@ -268,7 +294,7 @@ export function GotoRecordButton(props: GotoRecordButtonProps): JSX.Element
|
|||||||
{
|
{
|
||||||
props.buttonVisible && hasGotoFieldNames(props.tableMetaData) && <Button onClick={openGoto} >Go To...</Button>
|
props.buttonVisible && hasGotoFieldNames(props.tableMetaData) && <Button onClick={openGoto} >Go To...</Button>
|
||||||
}
|
}
|
||||||
<GotoRecordDialog metaData={props.metaData} tableMetaData={props.tableMetaData} isOpen={gotoIsOpen} closeHandler={closeGoto} mayClose={props.mayClose} />
|
<GotoRecordDialog metaData={props.metaData} tableMetaData={props.tableMetaData} tableVariant={props.tableVariant} isOpen={gotoIsOpen} closeHandler={closeGoto} mayClose={props.mayClose} subHeader={props.subHeader} />
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -228,12 +228,19 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
|
|
||||||
const download = (url: string, fileName: string) =>
|
const download = (url: string, fileName: string) =>
|
||||||
{
|
{
|
||||||
const qController = Client.getInstance();
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// todo - this could be simplified. //
|
||||||
|
// it was originally built like this when we had to submit full access token to backend... //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
let xhr = new XMLHttpRequest();
|
let xhr = new XMLHttpRequest();
|
||||||
xhr.open("POST", url);
|
xhr.open("POST", url);
|
||||||
xhr.responseType = "blob";
|
xhr.responseType = "blob";
|
||||||
let formData = new FormData();
|
let formData = new FormData();
|
||||||
|
|
||||||
|
////////////////////////////////////
|
||||||
|
// todo#authHeader - delete this. //
|
||||||
|
////////////////////////////////////
|
||||||
|
const qController = Client.getInstance();
|
||||||
formData.append("Authorization", qController.getAuthorizationHeaderValue());
|
formData.append("Authorization", qController.getAuthorizationHeaderValue());
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -55,7 +55,7 @@ import {DataGridPro, GridCallbackDetails, GridColDef, GridColumnMenuContainer, G
|
|||||||
import {GridRowModel} from "@mui/x-data-grid/models/gridRows";
|
import {GridRowModel} from "@mui/x-data-grid/models/gridRows";
|
||||||
import FormData from "form-data";
|
import FormData from "form-data";
|
||||||
import React, {forwardRef, useContext, useEffect, useReducer, useRef, useState} from "react";
|
import React, {forwardRef, useContext, useEffect, useReducer, useRef, useState} from "react";
|
||||||
import {useLocation, useNavigate, useSearchParams} from "react-router-dom";
|
import {Navigate, NavigateFunction, useLocation, useNavigate, useSearchParams} from "react-router-dom";
|
||||||
import QContext from "QContext";
|
import QContext from "QContext";
|
||||||
import {QActionsMenuButton, QCancelButton, QCreateNewButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
|
import {QActionsMenuButton, QCancelButton, QCreateNewButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
|
||||||
import MenuButton from "qqq/components/buttons/MenuButton";
|
import MenuButton from "qqq/components/buttons/MenuButton";
|
||||||
@ -539,15 +539,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
<CustomWidthTooltip title={tooltipHTML}>
|
<CustomWidthTooltip title={tooltipHTML}>
|
||||||
<IconButton sx={{p: 0, fontSize: "0.5rem", mb: 1, color: "#9f9f9f", fontVariationSettings: "'wght' 100"}}><Icon fontSize="small">emergency</Icon></IconButton>
|
<IconButton sx={{p: 0, fontSize: "0.5rem", mb: 1, color: "#9f9f9f", fontVariationSettings: "'wght' 100"}}><Icon fontSize="small">emergency</Icon></IconButton>
|
||||||
</CustomWidthTooltip>
|
</CustomWidthTooltip>
|
||||||
{
|
{tableVariant && getTableVariantHeader()}
|
||||||
tableVariant &&
|
|
||||||
<Typography variant="h6" color="text" fontWeight="light">
|
|
||||||
{tableMetaData.variantTableLabel}: {tableVariant.name}
|
|
||||||
<Tooltip title={`Change ${tableMetaData.variantTableLabel}`}>
|
|
||||||
<IconButton onClick={promptForTableVariantSelection} sx={{p: 0, m: 0, ml: .5, mb: .5, color: "#9f9f9f", fontVariationSettings: "'wght' 100"}}><Icon fontSize="small">settings</Icon></IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</Typography>
|
|
||||||
}
|
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -555,19 +547,23 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{label}
|
{label}
|
||||||
{
|
{tableVariant && getTableVariantHeader()}
|
||||||
tableVariant &&
|
|
||||||
<Typography variant="h6" color="text" fontWeight="light">
|
|
||||||
{tableMetaData.variantTableLabel}: {tableVariant.name}
|
|
||||||
<Tooltip title={`Change ${tableMetaData.variantTableLabel}`}>
|
|
||||||
<IconButton onClick={promptForTableVariantSelection} sx={{p: 0, m: 0, ml: .5, mb: .5, color: "#9f9f9f", fontVariationSettings: "'wght' 100"}}><Icon fontSize="small">settings</Icon></IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</Typography>
|
|
||||||
}
|
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getTableVariantHeader = () =>
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<Typography variant="h6" color="text" fontWeight="light">
|
||||||
|
{tableMetaData?.variantTableLabel}: {tableVariant?.name}
|
||||||
|
<Tooltip title={`Change ${tableMetaData?.variantTableLabel}`}>
|
||||||
|
<IconButton onClick={promptForTableVariantSelection} sx={{p: 0, m: 0, ml: .5, mb: .5, color: "#9f9f9f", fontVariationSettings: "'wght' 100"}}><Icon fontSize="small">settings</Icon></IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const updateTable = () =>
|
const updateTable = () =>
|
||||||
{
|
{
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -756,7 +752,42 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
console.log(`Received error for query ${thisQueryId}`);
|
console.log(`Received error for query ${thisQueryId}`);
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// special case for variant errors, if 500 and certain message, just clear out //
|
||||||
|
// local storage of variant and reload the page (rather than black page of death) //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
var errorMessage;
|
var errorMessage;
|
||||||
|
if(tableMetaData?.usesVariants)
|
||||||
|
{
|
||||||
|
if (error.status == "500" && error.message.indexOf("Could not find Backend Variant") != -1)
|
||||||
|
{
|
||||||
|
if (table)
|
||||||
|
{
|
||||||
|
const tableVariantLocalStorageKey = `${TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT}.${table.name}`;
|
||||||
|
localStorage.removeItem(tableVariantLocalStorageKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (error && error.message)
|
||||||
|
{
|
||||||
|
errorMessage = error.message;
|
||||||
|
}
|
||||||
|
else if (error && error.response && error.response.data && error.response.data.error)
|
||||||
|
{
|
||||||
|
errorMessage = error.response.data.error;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
errorMessage = "Unexpected error running query";
|
||||||
|
}
|
||||||
|
|
||||||
|
setAlertContent(errorMessage);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
if (error && error.message)
|
if (error && error.message)
|
||||||
{
|
{
|
||||||
errorMessage = error.message;
|
errorMessage = error.message;
|
||||||
@ -775,7 +806,8 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
setReceivedQueryErrorTimestamp(new Date());
|
setReceivedQueryErrorTimestamp(new Date());
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
});
|
}
|
||||||
|
})
|
||||||
})();
|
})();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1140,6 +1172,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
<body>
|
<body>
|
||||||
Generating file <u>${filename}</u>${totalRecords ? " with " + totalRecords.toLocaleString() + " record" + (totalRecords == 1 ? "" : "s") : ""}...
|
Generating file <u>${filename}</u>${totalRecords ? " with " + totalRecords.toLocaleString() + " record" + (totalRecords == 1 ? "" : "s") : ""}...
|
||||||
<form id="exportForm" method="post" action="${url}" >
|
<form id="exportForm" method="post" action="${url}" >
|
||||||
|
<!-- todo#authHeader - remove this. -->
|
||||||
<input type="hidden" name="Authorization" value="${qController.getAuthorizationHeaderValue()}">
|
<input type="hidden" name="Authorization" value="${qController.getAuthorizationHeaderValue()}">
|
||||||
<input type="hidden" name="fields" value="${visibleFields.join(",")}">
|
<input type="hidden" name="fields" value="${visibleFields.join(",")}">
|
||||||
<input type="hidden" name="filter" id="filter">
|
<input type="hidden" name="filter" id="filter">
|
||||||
@ -1887,10 +1920,27 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
// instead, try to just render a Goto Record button, in auto-open, and may-not-close modes //
|
// instead, try to just render a Goto Record button, in auto-open, and may-not-close modes //
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
if (tableMetaData && !tableMetaData.capabilities.has(Capability.TABLE_QUERY) && tableMetaData.capabilities.has(Capability.TABLE_GET))
|
if (tableMetaData && !tableMetaData.capabilities.has(Capability.TABLE_QUERY) && tableMetaData.capabilities.has(Capability.TABLE_GET))
|
||||||
|
{
|
||||||
|
if(tableMetaData?.usesVariants && (!tableVariant || tableVariantPromptOpen))
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
<GotoRecordButton metaData={metaData} tableMetaData={tableMetaData} autoOpen={true} buttonVisible={false} mayClose={false} />
|
<TableVariantDialog navigate={navigate} table={tableMetaData} isOpen={true} closeHandler={(value: QTableVariant) =>
|
||||||
|
{
|
||||||
|
setTableVariantPromptOpen(false);
|
||||||
|
setTableVariant(value);
|
||||||
|
}} />
|
||||||
|
</BaseLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseLayout>
|
||||||
|
<GotoRecordButton metaData={metaData} tableMetaData={tableMetaData} tableVariant={tableVariant} autoOpen={true} buttonVisible={false} mayClose={false} subHeader={
|
||||||
|
<Box mb={2}>
|
||||||
|
{getTableVariantHeader()}
|
||||||
|
</Box>
|
||||||
|
} />
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -2045,7 +2095,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
|
|
||||||
{
|
{
|
||||||
tableMetaData &&
|
tableMetaData &&
|
||||||
<TableVariantDialog table={tableMetaData} isOpen={tableVariantPromptOpen} closeHandler={(value: QTableVariant) =>
|
<TableVariantDialog navigate={navigate} table={tableMetaData} isOpen={tableVariantPromptOpen} closeHandler={(value: QTableVariant) =>
|
||||||
{
|
{
|
||||||
setTableVariantPromptOpen(false);
|
setTableVariantPromptOpen(false);
|
||||||
setTableVariant(value);
|
setTableVariant(value);
|
||||||
@ -2077,7 +2127,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// mini-component that is the dialog for the user to select a variant on tables with variant backends //
|
// mini-component that is the dialog for the user to select a variant on tables with variant backends //
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
function TableVariantDialog(props: {isOpen: boolean; table: QTableMetaData; closeHandler: (value?: QTableVariant) => void})
|
function TableVariantDialog(props: {navigate: NavigateFunction, isOpen: boolean; table: QTableMetaData; closeHandler: (value?: QTableVariant) => void})
|
||||||
{
|
{
|
||||||
const [value, setValue] = useState(null)
|
const [value, setValue] = useState(null)
|
||||||
const [dropDownOpen, setDropDownOpen] = useState(false)
|
const [dropDownOpen, setDropDownOpen] = useState(false)
|
||||||
@ -2126,7 +2176,17 @@ function TableVariantDialog(props: {isOpen: boolean; table: QTableMetaData; clos
|
|||||||
|
|
||||||
return variants && (
|
return variants && (
|
||||||
<Dialog open={props.isOpen} onKeyPress={(e) => keyPressed(e)}>
|
<Dialog open={props.isOpen} onKeyPress={(e) => keyPressed(e)}>
|
||||||
<DialogTitle>{props.table.variantTableLabel}</DialogTitle>
|
<DialogTitle sx={{display: "flex"}}>
|
||||||
|
<Box sx={{display: "flex", flexGrow: 1}}>
|
||||||
|
{props.table.variantTableLabel}
|
||||||
|
</Box>
|
||||||
|
<Box sx={{display: "flex"}}>
|
||||||
|
<IconButton onClick={() =>
|
||||||
|
{
|
||||||
|
document.location.href = "/";
|
||||||
|
}}><Icon sx={{align: "right"}} fontSize="small">close</Icon></IconButton>
|
||||||
|
</Box>
|
||||||
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText>Select the {props.table.variantTableLabel} to be used on this table:</DialogContentText>
|
<DialogContentText>Select the {props.table.variantTableLabel} to be used on this table:</DialogContentText>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
|
@ -441,6 +441,8 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(record)
|
||||||
|
{
|
||||||
setPageHeader(record.recordLabel);
|
setPageHeader(record.recordLabel);
|
||||||
|
|
||||||
if(!launchingProcess)
|
if(!launchingProcess)
|
||||||
@ -454,6 +456,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
console.error("Error pushing history: " + e);
|
console.error("Error pushing history: " + e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/////////////////////////////////////////////////
|
/////////////////////////////////////////////////
|
||||||
// define the sections, e.g., for the left-bar //
|
// define the sections, e.g., for the left-bar //
|
||||||
|
@ -95,6 +95,11 @@ export default class HtmlUtils
|
|||||||
form.setAttribute("target", "downloadIframe");
|
form.setAttribute("target", "downloadIframe");
|
||||||
iframe.appendChild(form);
|
iframe.appendChild(form);
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// todo#authHeader - remove after comfortable with sessionUUID //
|
||||||
|
// todo - this could be simplified (i think?) //
|
||||||
|
// it was originally built like this when we had to submit full access token to backend... //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
const authorizationInput = document.createElement("input");
|
const authorizationInput = document.createElement("input");
|
||||||
authorizationInput.setAttribute("type", "hidden");
|
authorizationInput.setAttribute("type", "hidden");
|
||||||
authorizationInput.setAttribute("id", "authorizationInput");
|
authorizationInput.setAttribute("id", "authorizationInput");
|
||||||
@ -118,6 +123,11 @@ export default class HtmlUtils
|
|||||||
{
|
{
|
||||||
if(url.startsWith("data:"))
|
if(url.startsWith("data:"))
|
||||||
{
|
{
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// todo#authHeader - remove the Authorization input after comfortable with sessionUUID //
|
||||||
|
// todo - this could be simplified (i think?) //
|
||||||
|
// it was originally built like this when we had to submit full access token to backend... //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
const openInWindow = window.open("", "_blank");
|
const openInWindow = window.open("", "_blank");
|
||||||
openInWindow.document.write(`<html lang="en">
|
openInWindow.document.write(`<html lang="en">
|
||||||
<body style="margin: 0">
|
<body style="margin: 0">
|
||||||
|
@ -49,6 +49,7 @@ module.exports = function (app)
|
|||||||
app.use("/data/*", getRequestHandler());
|
app.use("/data/*", getRequestHandler());
|
||||||
app.use("/widget/*", getRequestHandler());
|
app.use("/widget/*", getRequestHandler());
|
||||||
app.use("/serverInfo", getRequestHandler());
|
app.use("/serverInfo", getRequestHandler());
|
||||||
|
app.use("/manageSession", getRequestHandler());
|
||||||
app.use("/processes", getRequestHandler());
|
app.use("/processes", getRequestHandler());
|
||||||
app.use("/reports", getRequestHandler());
|
app.use("/reports", getRequestHandler());
|
||||||
app.use("/images", getRequestHandler());
|
app.use("/images", getRequestHandler());
|
||||||
|
Reference in New Issue
Block a user