Compare commits

...

11 Commits

Author SHA1 Message Date
575ffe761f CE-704: updates to better handling selecting variants on 'integrations', or if not valid, etc. 2023-10-19 17:00:22 -05:00
0949ee9f78 session cookie fix - to say we need it if it isn't set.
also, just let backend request set it (it already was sending header).  also, log more.  also, remove unused attribute `pathToLabelMap` from `SideNav` (was issuing warnings)
2023-10-13 08:37:19 -05:00
f41b71d3c7 Support combination of non-query table (so goto only) plus variants. 2023-09-29 17:10:06 -05:00
57fefe9671 Merge pull request #33 from Kingsrook/feature/fix-urlencoding-primary-keys
Feature/fix urlencoding primary keys (and some filters)
2023-09-26 10:16:44 -05:00
8a018c34f6 Merge branch 'main' into feature/fix-urlencoding-primary-keys 2023-09-26 08:32:17 -05:00
1b4f70a547 Merge pull request #32 from Kingsrook/feature/CE-609-infrastructure-remove-permissions-from-header
Feature/ce 609 infrastructure remove permissions from header
2023-09-25 16:28:33 -05:00
1f343abbb5 Switch to use jwt_decode library (from auth0) rather than S/O decodeJWT function 2023-09-25 16:27:46 -05:00
7ea50dd7bb Merge branch 'main' into feature/CE-609-infrastructure-remove-permissions-from-header
# Conflicts:
#	package.json
2023-08-17 11:42:51 -05:00
b6b7d8d8b3 CE-609 - Removed DNDTest WIP module 2023-08-15 09:25:45 -05:00
7bf515554d CE-609 - staged-rollout-ready - keeping the auth header, but also setting sessionUUID cookie; placeholder for quick-rollback; added todo#authHeader comments to mark where follow-up needs to happen after happy with new code 2023-08-15 09:08:44 -05:00
7fa42a6eb5 Initial WIP Checkpoint of auth0 userSessions 2023-08-09 09:48:22 -05:00
9 changed files with 276 additions and 83 deletions

View File

@ -33,6 +33,7 @@
"html-react-parser": "1.4.8",
"html-to-text": "^9.0.5",
"http-proxy-middleware": "2.0.6",
"jwt-decode": "3.1.2",
"rapidoc": "9.3.4",
"react": "18.0.0",
"react-ace": "10.1.0",

View File

@ -33,7 +33,8 @@ 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, 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 {Navigate, Route, Routes, useLocation,} from "react-router-dom";
import {Md5} from "ts-md5/dist/md5";
@ -57,11 +58,11 @@ import ProcessUtils from "qqq/utils/qqq/ProcessUtils";
const qController = Client.getInstance();
export const SESSION_ID_COOKIE_NAME = "sessionId";
export const SESSION_UUID_COOKIE_NAME = "sessionUUID";
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 [loadingToken, setLoadingToken] = useState(false);
const [isFullyAuthenticated, setIsFullyAuthenticated] = useState(false);
@ -69,8 +70,62 @@ 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 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(() =>
{
if (loadingToken)
@ -92,20 +147,48 @@ export default function App()
{
console.log("Loading token from auth0...");
const accessToken = await getAccessTokenSilently();
qController.setAuthorizationHeaderValue("Bearer " + accessToken);
/////////////////////////////////////////////////////////////////////////////////
// we've stopped using session id cook with auth0, so make sure it is not set. //
/////////////////////////////////////////////////////////////////////////////////
removeCookie(SESSION_ID_COOKIE_NAME);
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);
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 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);
qController.setGotAuthentication();
qController.setAuthorizationHeaderValue("Bearer " + accessToken);
setLoggedInUser(user);
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;
}
@ -116,9 +199,9 @@ export default function App()
// use a random token if anonymous or mock //
/////////////////////////////////////////////
console.log("Generating random token...");
qController.setAuthorizationHeaderValue(null);
qController.setAuthorizationHeaderValue(Md5.hashStr(`${new Date()}`));
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.");
return;
}
@ -149,7 +232,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 //
@ -267,14 +350,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({
@ -429,11 +512,11 @@ export default function App()
let profileRoutes = {};
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}`;
profileRoutes = {
type: "collapse",
name: user?.name,
name: loggedInUser?.name ?? "Anonymous",
key: "username",
noCollapse: true,
icon: <Avatar src={profilePicture} alt="{user?.name}" />,
@ -469,7 +552,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;
@ -495,7 +578,10 @@ 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 //
@ -596,7 +682,7 @@ export default function App()
}}>
<ThemeProvider theme={theme}>
<CssBaseline />
<CommandMenu metaData={metaData}/>
<CommandMenu metaData={metaData} />
<Sidenav
color={sidenavColor}
icon={branding.icon}
@ -604,7 +690,6 @@ export default function App()
appName={branding.appName}
branding={branding}
routes={sideNavRoutes}
pathToLabelMap={pathToLabelMap}
onMouseEnter={handleOnMouseEnter}
onMouseLeave={handleOnMouseLeave}
/>

View File

@ -22,7 +22,7 @@
import {useAuth0} from "@auth0/auth0-react";
import React, {useEffect} from "react";
import {useCookies} from "react-cookie";
import {SESSION_ID_COOKIE_NAME} from "App";
import {SESSION_UUID_COOKIE_NAME} from "App";
interface Props
{
@ -33,13 +33,13 @@ interface Props
function HandleAuthorizationError({errorMessage}: Props)
{
const [, , removeCookie] = useCookies([SESSION_ID_COOKIE_NAME]);
const [, , removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
const {logout} = useAuth0();
useEffect(() =>
{
logout();
removeCookie(SESSION_ID_COOKIE_NAME, {path: "/"});
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
});
return (

View File

@ -22,6 +22,7 @@
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
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 {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
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 Grid from "@mui/material/Grid";
import Icon from "@mui/material/Icon";
import IconButton from "@mui/material/IconButton";
import TextField from "@mui/material/TextField";
import React, {useState} from "react";
import {useNavigate} from "react-router-dom";
@ -45,8 +47,10 @@ interface Props
isOpen: boolean;
metaData: QInstance;
tableMetaData: QTableMetaData;
tableVariant?: QTableVariant;
closeHandler: () => void;
mayClose: boolean;
subHeader?: JSX.Element;
}
GotoRecordDialog.defaultProps = {
@ -155,21 +159,30 @@ function GotoRecordDialog(props: Props): JSX.Element
{
setError("");
const filter = new QQueryFilter([new QFilterCriteria(fieldName, QCriteriaOperator.EQUALS, [values[fieldName]])], null, "AND", null, 10);
const queryResult = await qController.query(props.tableMetaData.name, filter)
if(queryResult.length == 0)
try
{
setError("Record not found.");
setTimeout(() => setError(""), 3000);
const queryResult = await qController.query(props.tableMetaData.name, filter, null, props.tableVariant)
if (queryResult.length == 0)
{
setError("Record not found.");
setTimeout(() => setError(""), 3000);
}
else if (queryResult.length == 1)
{
navigate(`${props.metaData.getTablePathByName(props.tableMetaData.name)}/${encodeURIComponent(queryResult[0].values.get(props.tableMetaData.primaryKeyField))}`);
close();
}
else
{
setError("More than 1 record found...");
setTimeout(() => setError(""), 3000);
}
}
else if(queryResult.length == 1)
catch(e)
{
navigate(`${props.metaData.getTablePathByName(props.tableMetaData.name)}/${queryResult[0].values.get(props.tableMetaData.primaryKeyField)}`);
close();
}
else
{
setError("More than 1 record found...");
setTimeout(() => setError(""), 3000);
// @ts-ignore
setError(`Error: ${(e && e.message) ? e.message : e}`);
setTimeout(() => setError(""), 6000);
}
}
@ -183,8 +196,19 @@ function GotoRecordDialog(props: Props): JSX.Element
return (
<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>
{props.subHeader}
{
fields.map((field, index) =>
(
@ -237,9 +261,11 @@ interface GotoRecordButtonProps
{
metaData: QInstance;
tableMetaData: QTableMetaData;
tableVariant?: QTableVariant;
autoOpen?: boolean;
buttonVisible?: boolean;
mayClose?: boolean;
subHeader?: JSX.Element;
}
GotoRecordButton.defaultProps = {
@ -268,7 +294,7 @@ export function GotoRecordButton(props: GotoRecordButtonProps): JSX.Element
{
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>
);
}

View File

@ -228,12 +228,19 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
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();
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

View File

@ -55,7 +55,7 @@ import {DataGridPro, GridCallbackDetails, GridColDef, GridColumnMenuContainer, G
import {GridRowModel} from "@mui/x-data-grid/models/gridRows";
import FormData from "form-data";
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 {QActionsMenuButton, QCancelButton, QCreateNewButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
import MenuButton from "qqq/components/buttons/MenuButton";
@ -539,15 +539,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
<CustomWidthTooltip title={tooltipHTML}>
<IconButton sx={{p: 0, fontSize: "0.5rem", mb: 1, color: "#9f9f9f", fontVariationSettings: "'wght' 100"}}><Icon fontSize="small">emergency</Icon></IconButton>
</CustomWidthTooltip>
{
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>
}
{tableVariant && getTableVariantHeader()}
</div>);
}
else
@ -555,19 +547,23 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
return (
<div>
{label}
{
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>
}
{tableVariant && getTableVariantHeader()}
</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 = () =>
{
setLoading(true);
@ -756,26 +752,62 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
console.log(`Received error for query ${thisQueryId}`);
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;
if (error && error.message)
if(tableMetaData?.usesVariants)
{
errorMessage = error.message;
}
else if (error && error.response && error.response.data && error.response.data.error)
{
errorMessage = error.response.data.error;
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
{
errorMessage = "Unexpected error running query";
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";
}
queryErrors[thisQueryId] = errorMessage;
setQueryErrors(queryErrors);
setReceivedQueryErrorTimestamp(new Date());
throw error;
}
queryErrors[thisQueryId] = errorMessage;
setQueryErrors(queryErrors);
setReceivedQueryErrorTimestamp(new Date());
throw error;
});
})
})();
};
@ -1140,6 +1172,7 @@ 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">
@ -1888,9 +1921,26 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
/////////////////////////////////////////////////////////////////////////////////////////////
if (tableMetaData && !tableMetaData.capabilities.has(Capability.TABLE_QUERY) && tableMetaData.capabilities.has(Capability.TABLE_GET))
{
if(tableMetaData?.usesVariants && (!tableVariant || tableVariantPromptOpen))
{
return (
<BaseLayout>
<TableVariantDialog navigate={navigate} table={tableMetaData} isOpen={true} closeHandler={(value: QTableVariant) =>
{
setTableVariantPromptOpen(false);
setTableVariant(value);
}} />
</BaseLayout>
);
}
return (
<BaseLayout>
<GotoRecordButton metaData={metaData} tableMetaData={tableMetaData} autoOpen={true} buttonVisible={false} mayClose={false} />
<GotoRecordButton metaData={metaData} tableMetaData={tableMetaData} tableVariant={tableVariant} autoOpen={true} buttonVisible={false} mayClose={false} subHeader={
<Box mb={2}>
{getTableVariantHeader()}
</Box>
} />
</BaseLayout>
);
}
@ -2045,7 +2095,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
{
tableMetaData &&
<TableVariantDialog table={tableMetaData} isOpen={tableVariantPromptOpen} closeHandler={(value: QTableVariant) =>
<TableVariantDialog navigate={navigate} table={tableMetaData} isOpen={tableVariantPromptOpen} closeHandler={(value: QTableVariant) =>
{
setTableVariantPromptOpen(false);
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 //
////////////////////////////////////////////////////////////////////////////////////////////////////////
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 [dropDownOpen, setDropDownOpen] = useState(false)
@ -2126,7 +2176,17 @@ function TableVariantDialog(props: {isOpen: boolean; table: QTableMetaData; clos
return variants && (
<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>
<DialogContentText>Select the {props.table.variantTableLabel} to be used on this table:</DialogContentText>
<Autocomplete

View File

@ -441,17 +441,20 @@ function RecordView({table, launchProcess}: Props): JSX.Element
}
}
setPageHeader(record.recordLabel);
if(!launchingProcess)
if(record)
{
try
setPageHeader(record.recordLabel);
if(!launchingProcess)
{
HistoryUtils.push({label: `${tableMetaData?.label}: ${record.recordLabel}`, path: location.pathname, iconName: table.iconName});
}
catch(e)
{
console.error("Error pushing history: " + e);
try
{
HistoryUtils.push({label: `${tableMetaData?.label}: ${record.recordLabel}`, path: location.pathname, iconName: table.iconName});
}
catch(e)
{
console.error("Error pushing history: " + e);
}
}
}

View File

@ -95,6 +95,11 @@ 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");
@ -118,6 +123,11 @@ 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">

View File

@ -49,6 +49,7 @@ 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());