mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-21 22:58:43 +00:00
Compare commits
1 Commits
snapshot-f
...
snapshot-f
Author | SHA1 | Date | |
---|---|---|---|
01d18902d7 |
@ -6,7 +6,7 @@
|
||||
"@auth0/auth0-react": "1.10.2",
|
||||
"@emotion/react": "11.7.1",
|
||||
"@emotion/styled": "11.6.0",
|
||||
"@kingsrook/qqq-frontend-core": "1.0.81",
|
||||
"@kingsrook/qqq-frontend-core": "1.0.80",
|
||||
"@mui/icons-material": "5.4.1",
|
||||
"@mui/material": "5.11.1",
|
||||
"@mui/styles": "5.11.1",
|
||||
|
116
src/App.tsx
116
src/App.tsx
@ -33,7 +33,7 @@ import CssBaseline from "@mui/material/CssBaseline";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import {ThemeProvider} from "@mui/material/styles";
|
||||
import {LicenseInfo} from "@mui/x-license-pro";
|
||||
import React, {JSXElementConstructor, Key, ReactElement, useEffect, useState,} from "react";
|
||||
import React, {JSXElementConstructor, Key, ReactElement, useContext, useEffect, useState,} from "react";
|
||||
import {useCookies} from "react-cookie";
|
||||
import {Navigate, Route, Routes, useLocation,} from "react-router-dom";
|
||||
import {Md5} from "ts-md5/dist/md5";
|
||||
@ -57,11 +57,11 @@ import ProcessUtils from "qqq/utils/qqq/ProcessUtils";
|
||||
|
||||
|
||||
const qController = Client.getInstance();
|
||||
export const SESSION_UUID_COOKIE_NAME = "sessionUUID";
|
||||
export const SESSION_ID_COOKIE_NAME = "sessionId";
|
||||
|
||||
export default function App()
|
||||
{
|
||||
const [, setCookie, removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
|
||||
const [, setCookie, removeCookie] = useCookies([SESSION_ID_COOKIE_NAME]);
|
||||
const {user, getAccessTokenSilently, logout} = useAuth0();
|
||||
const [loadingToken, setLoadingToken] = useState(false);
|
||||
const [isFullyAuthenticated, setIsFullyAuthenticated] = useState(false);
|
||||
@ -69,67 +69,8 @@ export default function App()
|
||||
const [branding, setBranding] = useState({} as QBrandingMetaData);
|
||||
const [metaData, setMetaData] = useState({} as QInstance);
|
||||
const [needLicenseKey, setNeedLicenseKey] = useState(true);
|
||||
const [loggedInUser, setLoggedInUser] = useState({} as { name?: string, email?: string });
|
||||
const [defaultRoute, setDefaultRoute] = useState("/no-apps");
|
||||
|
||||
const decodeJWT = (jwt: string): any =>
|
||||
{
|
||||
const base64Url = jwt.split(".")[1];
|
||||
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
||||
const jsonPayload = decodeURIComponent(window.atob(base64).split("").map(function (c)
|
||||
{
|
||||
return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
|
||||
}).join(""));
|
||||
|
||||
return JSON.parse(jsonPayload);
|
||||
};
|
||||
|
||||
const shouldStoreNewToken = (newToken: string, oldToken: string): boolean =>
|
||||
{
|
||||
if (!oldToken)
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
const oldJSON = decodeJWT(oldToken);
|
||||
const newJSON = decodeJWT(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.");
|
||||
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.");
|
||||
}
|
||||
return (different);
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
console.log("Caught in shouldStoreNewToken: " + e)
|
||||
}
|
||||
|
||||
return (true);
|
||||
};
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if (loadingToken)
|
||||
@ -151,38 +92,20 @@ export default function App()
|
||||
{
|
||||
console.log("Loading token from auth0...");
|
||||
const accessToken = await getAccessTokenSilently();
|
||||
|
||||
const lsAccessToken = localStorage.getItem("accessToken");
|
||||
if (shouldStoreNewToken(accessToken, lsAccessToken))
|
||||
{
|
||||
console.log("Sending accessToken to backend, requesting a sessionUUID...");
|
||||
const newSessionUuid = await qController.manageSession(accessToken, null);
|
||||
setCookie(SESSION_UUID_COOKIE_NAME, newSessionUuid, {path: "/"});
|
||||
localStorage.setItem("accessToken", accessToken);
|
||||
}
|
||||
|
||||
/*
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// 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);
|
||||
qController.setGotAuthentication();
|
||||
qController.setAuthorizationHeaderValue("Bearer " + accessToken);
|
||||
|
||||
setLoggedInUser(user);
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
// we've stopped using session id cook with auth0, so make sure it is not set. //
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
removeCookie(SESSION_ID_COOKIE_NAME);
|
||||
|
||||
setIsFullyAuthenticated(true);
|
||||
console.log("Token load complete.");
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
console.log(`Error loading token: ${JSON.stringify(e)}`);
|
||||
qController.clearAuthenticationMetaDataLocalStorage();
|
||||
localStorage.removeItem("accessToken")
|
||||
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
||||
logout();
|
||||
return;
|
||||
}
|
||||
@ -193,9 +116,9 @@ export default function App()
|
||||
// use a random token if anonymous or mock //
|
||||
/////////////////////////////////////////////
|
||||
console.log("Generating random token...");
|
||||
qController.setAuthorizationHeaderValue(Md5.hashStr(`${new Date()}`));
|
||||
qController.setAuthorizationHeaderValue(null);
|
||||
setIsFullyAuthenticated(true);
|
||||
setCookie(SESSION_UUID_COOKIE_NAME, Md5.hashStr(`${new Date()}`), {path: "/"});
|
||||
setCookie(SESSION_ID_COOKIE_NAME, Md5.hashStr(`${new Date()}`), {path: "/"});
|
||||
console.log("Token generation complete.");
|
||||
return;
|
||||
}
|
||||
@ -226,7 +149,7 @@ export default function App()
|
||||
const [needToLoadRoutes, setNeedToLoadRoutes] = useState(true);
|
||||
const [sideNavRoutes, setSideNavRoutes] = useState([]);
|
||||
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 //
|
||||
@ -344,14 +267,14 @@ export default function App()
|
||||
name: `${app.label}`,
|
||||
key: app.name,
|
||||
route: path,
|
||||
component: <RecordQuery table={table} key={table.name} />,
|
||||
component: <RecordQuery table={table} key={table.name}/>,
|
||||
});
|
||||
|
||||
routeList.push({
|
||||
name: `${app.label}`,
|
||||
key: app.name,
|
||||
route: `${path}/savedFilter/:id`,
|
||||
component: <RecordQuery table={table} key={table.name} />,
|
||||
component: <RecordQuery table={table} key={table.name}/>,
|
||||
});
|
||||
|
||||
routeList.push({
|
||||
@ -506,11 +429,11 @@ export default function App()
|
||||
|
||||
let profileRoutes = {};
|
||||
const gravatarBase = "https://www.gravatar.com/avatar/";
|
||||
const hash = Md5.hashStr(loggedInUser?.email || "user");
|
||||
const hash = Md5.hashStr(user?.email || "user");
|
||||
const profilePicture = `${gravatarBase}${hash}`;
|
||||
profileRoutes = {
|
||||
type: "collapse",
|
||||
name: loggedInUser?.name ?? "Anonymous",
|
||||
name: user?.name,
|
||||
key: "username",
|
||||
noCollapse: true,
|
||||
icon: <Avatar src={profilePicture} alt="{user?.name}" />,
|
||||
@ -546,7 +469,7 @@ export default function App()
|
||||
}
|
||||
|
||||
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];
|
||||
pathToLabelMap[route.route] = route.name;
|
||||
@ -572,10 +495,7 @@ export default function App()
|
||||
{
|
||||
if ((e as QException).status === "401")
|
||||
{
|
||||
console.log("Exception is a QException with status = 401. Clearing some of localStorage & cookies");
|
||||
qController.clearAuthenticationMetaDataLocalStorage();
|
||||
localStorage.removeItem("accessToken")
|
||||
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
||||
|
||||
//////////////////////////////////////////////////////
|
||||
// todo - this is auth0 logout... make more generic //
|
||||
@ -676,7 +596,7 @@ export default function App()
|
||||
}}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<CommandMenu metaData={metaData} />
|
||||
<CommandMenu metaData={metaData}/>
|
||||
<Sidenav
|
||||
color={sidenavColor}
|
||||
icon={branding.icon}
|
||||
|
@ -22,7 +22,7 @@
|
||||
import {useAuth0} from "@auth0/auth0-react";
|
||||
import React, {useEffect} from "react";
|
||||
import {useCookies} from "react-cookie";
|
||||
import {SESSION_UUID_COOKIE_NAME} from "App";
|
||||
import {SESSION_ID_COOKIE_NAME} from "App";
|
||||
|
||||
interface Props
|
||||
{
|
||||
@ -33,13 +33,13 @@ interface Props
|
||||
function HandleAuthorizationError({errorMessage}: Props)
|
||||
{
|
||||
|
||||
const [, , removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
|
||||
const [, , removeCookie] = useCookies([SESSION_ID_COOKIE_NAME]);
|
||||
const {logout} = useAuth0();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
logout();
|
||||
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
||||
removeCookie(SESSION_ID_COOKIE_NAME, {path: "/"});
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -62,10 +62,12 @@ import {GoogleDriveFolderPickerWrapper} from "qqq/components/processes/GoogleDri
|
||||
import ProcessSummaryResults from "qqq/components/processes/ProcessSummaryResults";
|
||||
import ValidationReview from "qqq/components/processes/ValidationReview";
|
||||
import BaseLayout from "qqq/layouts/BaseLayout";
|
||||
import {TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT} from "qqq/pages/records/query/RecordQuery";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
import TableUtils from "qqq/utils/qqq/TableUtils";
|
||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||
|
||||
|
||||
interface Props
|
||||
{
|
||||
process?: QProcessMetaData;
|
||||
@ -88,6 +90,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
{
|
||||
const processNameParam = useParams().processName;
|
||||
const processName = process === null ? processNameParam : process.name;
|
||||
const tableVariantLocalStorageKey = `${TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT}.${table.name}`;
|
||||
|
||||
///////////////////
|
||||
// process state //
|
||||
@ -221,19 +224,12 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
|
||||
const download = (url: string, fileName: string) =>
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// todo - this could be simplified. //
|
||||
// it was originally built like this when we had to submit full access token to backend... //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const qController = Client.getInstance();
|
||||
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", url);
|
||||
xhr.responseType = "blob";
|
||||
let formData = new FormData();
|
||||
|
||||
////////////////////////////////////
|
||||
// todo#authHeader - delete this. //
|
||||
////////////////////////////////////
|
||||
const qController = Client.getInstance();
|
||||
formData.append("Authorization", qController.getAuthorizationHeaderValue());
|
||||
|
||||
// @ts-ignore
|
||||
@ -375,15 +371,15 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
// but our first use case, they're the same, so... this needs fixed. //
|
||||
// they also need to know the 'otherValues' in this process - e.g., for filtering //
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
if(formFields && processValues)
|
||||
if (formFields && processValues)
|
||||
{
|
||||
Object.keys(formFields).forEach((key) =>
|
||||
{
|
||||
if(formFields[key].possibleValueProps)
|
||||
if (formFields[key].possibleValueProps)
|
||||
{
|
||||
if(processValues[key])
|
||||
if (processValues[key])
|
||||
{
|
||||
formFields[key].possibleValueProps.initialDisplayValue = processValues[key]
|
||||
formFields[key].possibleValueProps.initialDisplayValue = processValues[key];
|
||||
}
|
||||
|
||||
formFields[key].possibleValueProps.otherValues = formFields[key].possibleValueProps.otherValues ?? new Map<string, any>();
|
||||
@ -392,7 +388,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
formFields[key].possibleValueProps.otherValues.set(otherKey, processValues[otherKey]);
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
@ -748,7 +744,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
formValidations[fieldName] = validation;
|
||||
};
|
||||
|
||||
if(tableMetaData)
|
||||
if (tableMetaData)
|
||||
{
|
||||
console.log("Adding table name field... ?", tableMetaData.name);
|
||||
addField("tableName", {type: "hidden", omitFromQDynamicForm: true}, tableMetaData.name, null);
|
||||
@ -801,15 +797,15 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
Object.keys(dynamicFormFields).forEach((key: any) =>
|
||||
{
|
||||
if(dynamicFormFields[key].possibleValueProps)
|
||||
if (dynamicFormFields[key].possibleValueProps)
|
||||
{
|
||||
dynamicFormFields[key].possibleValueProps.otherValues = dynamicFormFields[key].possibleValueProps.otherValues ?? new Map<string, any>();
|
||||
Object.keys(initialValues).forEach((ivKey: any) =>
|
||||
{
|
||||
dynamicFormFields[key].possibleValueProps.otherValues.set(ivKey, initialValues[ivKey]);
|
||||
})
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
// disable all fields if this is a bulk edit form //
|
||||
@ -1109,6 +1105,12 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
}
|
||||
}
|
||||
|
||||
if (localStorage.getItem(tableVariantLocalStorageKey))
|
||||
{
|
||||
let tableVariant = JSON.parse(localStorage.getItem(tableVariantLocalStorageKey));
|
||||
queryStringPairsForInit.push(`tableVariant=${JSON.stringify(tableVariant)}`);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
const qInstance = await Client.getInstance().loadMetaData();
|
||||
@ -1154,9 +1156,9 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
}
|
||||
}
|
||||
|
||||
if(tableMetaData)
|
||||
if (tableMetaData)
|
||||
{
|
||||
queryStringPairsForInit.push(`tableName=${tableMetaData.name}`)
|
||||
queryStringPairsForInit.push(`tableName=${tableMetaData.name}`);
|
||||
}
|
||||
|
||||
try
|
||||
@ -1194,6 +1196,12 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
formData.append(key, values[key]);
|
||||
});
|
||||
|
||||
if (localStorage.getItem(tableVariantLocalStorageKey))
|
||||
{
|
||||
let tableVariant = JSON.parse(localStorage.getItem(tableVariantLocalStorageKey));
|
||||
formData.append("tableVariant", JSON.stringify(tableVariant));
|
||||
}
|
||||
|
||||
if (doesStepHaveComponent(activeStep, QComponentType.BULK_EDIT_FORM))
|
||||
{
|
||||
const bulkEditEnabledFields: string[] = [];
|
||||
|
@ -82,7 +82,8 @@ const ROWS_PER_PAGE_LOCAL_STORAGE_KEY_ROOT = "qqq.rowsPerPage";
|
||||
const PINNED_COLUMNS_LOCAL_STORAGE_KEY_ROOT = "qqq.pinnedColumns";
|
||||
const SEEN_JOIN_TABLES_LOCAL_STORAGE_KEY_ROOT = "qqq.seenJoinTables";
|
||||
const DENSITY_LOCAL_STORAGE_KEY_ROOT = "qqq.density";
|
||||
const TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT = "qqq.tableVariant";
|
||||
|
||||
export const TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT = "qqq.tableVariant";
|
||||
|
||||
interface Props
|
||||
{
|
||||
@ -1139,7 +1140,6 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
<body>
|
||||
Generating file <u>${filename}</u>${totalRecords ? " with " + totalRecords.toLocaleString() + " record" + (totalRecords == 1 ? "" : "s") : ""}...
|
||||
<form id="exportForm" method="post" action="${url}" >
|
||||
<!-- todo#authHeader - remove this. -->
|
||||
<input type="hidden" name="Authorization" value="${qController.getAuthorizationHeaderValue()}">
|
||||
<input type="hidden" name="fields" value="${visibleFields.join(",")}">
|
||||
<input type="hidden" name="filter" id="filter">
|
||||
|
@ -95,11 +95,6 @@ export default class HtmlUtils
|
||||
form.setAttribute("target", "downloadIframe");
|
||||
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");
|
||||
authorizationInput.setAttribute("type", "hidden");
|
||||
authorizationInput.setAttribute("id", "authorizationInput");
|
||||
@ -123,11 +118,6 @@ export default class HtmlUtils
|
||||
{
|
||||
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");
|
||||
openInWindow.document.write(`<html lang="en">
|
||||
<body style="margin: 0">
|
||||
|
@ -49,7 +49,6 @@ module.exports = function (app)
|
||||
app.use("/data/*", getRequestHandler());
|
||||
app.use("/widget/*", getRequestHandler());
|
||||
app.use("/serverInfo", getRequestHandler());
|
||||
app.use("/manageSession", getRequestHandler());
|
||||
app.use("/processes", getRequestHandler());
|
||||
app.use("/reports", getRequestHandler());
|
||||
app.use("/images", getRequestHandler());
|
||||
|
Reference in New Issue
Block a user