Compare commits

..

2 Commits

Author SHA1 Message Date
f654208769 Update kingsrook/qqq-frontend-core to 1.0.118 (add more params to manageSession call) 2025-03-07 20:33:09 -06:00
3dacab8d60 Add support for oauth2 authentication module.
In so doing, extract auth0- and anonymous- -authenticationModule implementations from index.tsx and App.tsx, moving each to it own useXyz hook.
2025-03-07 20:10:06 -06:00
20 changed files with 703 additions and 544 deletions

1
.gitignore vendored
View File

@ -5,7 +5,6 @@
.yalc* .yalc*
yalc.lock yalc.lock
.env .env
/certs
# dependencies # dependencies
/node_modules /node_modules

View File

@ -6,7 +6,7 @@
"@auth0/auth0-react": "1.10.2", "@auth0/auth0-react": "1.10.2",
"@emotion/react": "11.7.1", "@emotion/react": "11.7.1",
"@emotion/styled": "11.6.0", "@emotion/styled": "11.6.0",
"@kingsrook/qqq-frontend-core": "1.0.119", "@kingsrook/qqq-frontend-core": "1.0.118",
"@mui/icons-material": "5.4.1", "@mui/icons-material": "5.4.1",
"@mui/material": "5.11.1", "@mui/material": "5.11.1",
"@mui/styles": "5.11.1", "@mui/styles": "5.11.1",
@ -36,6 +36,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", "jwt-decode": "3.1.2",
"oidc-client-ts": "2.4.1",
"react-oidc-context": "2.3.1",
"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",

View File

@ -66,7 +66,7 @@
<dependency> <dependency>
<groupId>com.kingsrook.qqq</groupId> <groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-backend-core</artifactId> <artifactId>qqq-backend-core</artifactId>
<version>0.25.0-integration-sprint-62-20250307-205536</version> <version>0.21.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.slf4j</groupId> <groupId>org.slf4j</groupId>

View File

@ -19,7 +19,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {useAuth0} from "@auth0/auth0-react";
import {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException"; import {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException";
import {QAppNodeType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAppNodeType"; import {QAppNodeType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAppNodeType";
import {QAppTreeNode} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAppTreeNode"; import {QAppTreeNode} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAppTreeNode";
@ -29,18 +28,18 @@ import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstan
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData"; import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import Avatar from "@mui/material/Avatar"; import Avatar from "@mui/material/Avatar";
import Box from "@mui/material/Box";
import CssBaseline from "@mui/material/CssBaseline"; 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 CommandMenu from "CommandMenu"; import CommandMenu from "CommandMenu";
import jwt_decode from "jwt-decode";
import QContext from "QContext"; import QContext from "QContext";
import useAnonymousAuthenticationModule from "qqq/authorization/anonymous/useAnonymousAuthenticationModule";
import useAuth0AuthenticationModule from "qqq/authorization/auth0/useAuth0AuthenticationModule";
import useOAuth2AuthenticationModule from "qqq/authorization/oauth2/useOAuth2AuthenticationModule";
import Sidenav from "qqq/components/horseshoe/sidenav/SideNav"; import Sidenav from "qqq/components/horseshoe/sidenav/SideNav";
import theme from "qqq/components/legacy/Theme"; import theme from "qqq/components/legacy/Theme";
import {getBannerClassName, getBannerStyles, getBanner, makeBannerContent} from "qqq/components/misc/Banners"; import {setMiniSidenav, useMaterialUIController} from "qqq/context";
import {setMiniSidenav, setOpenConfigurator, useMaterialUIController} from "qqq/context";
import AppHome from "qqq/pages/apps/Home"; import AppHome from "qqq/pages/apps/Home";
import NoApps from "qqq/pages/apps/NoApps"; import NoApps from "qqq/pages/apps/NoApps";
import ProcessRun from "qqq/pages/processes/ProcessRun"; import ProcessRun from "qqq/pages/processes/ProcessRun";
@ -67,7 +66,6 @@ export const SESSION_UUID_COOKIE_NAME = "sessionUUID";
export default function App() export default function App()
{ {
const [cookies, setCookie, removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]); const [cookies, setCookie, removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
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);
const [profileRoutes, setProfileRoutes] = useState({}); const [profileRoutes, setProfileRoutes] = useState({});
@ -76,68 +74,21 @@ export default function App()
const [needLicenseKey, setNeedLicenseKey] = useState(true); const [needLicenseKey, setNeedLicenseKey] = useState(true);
const [loggedInUser, setLoggedInUser] = useState({} as { name?: string, email?: string }); const [loggedInUser, setLoggedInUser] = useState({} as { name?: string, email?: string });
const [defaultRoute, setDefaultRoute] = useState("/no-apps"); const [defaultRoute, setDefaultRoute] = useState("/no-apps");
const [authenticationMetaData, setAuthenticationMetaData] = useState(null as QAuthenticationMetaData | null);
const [earlyReturnForAuth, setEarlyReturnForAuth] = useState(null as JSX.Element);
const {setupSession: auth0SetupSession, logout: auth0Logout} = useAuth0AuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth});
const {setupSession: oauth2SetupSession, logout: oauth2Logout} = useOAuth2AuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth});
const {setupSession: anonymousSetupSession, logout: anonymousLogout} = useAnonymousAuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth});
///////////////////////////////////////////////////////// /////////////////////////////////////////////////////////
// tell the client how to do a logout if it sees a 401 // // tell the client how to do a logout if it sees a 401 //
///////////////////////////////////////////////////////// /////////////////////////////////////////////////////////
Client.setUnauthorizedCallback(() => Client.setUnauthorizedCallback(() => doLogout());
{
logout();
});
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);
};
/////////////////////////////////////////////////
// deal with making sure user is authenticated //
/////////////////////////////////////////////////
useEffect(() => useEffect(() =>
{ {
if (loadingToken) if (loadingToken)
@ -149,64 +100,19 @@ export default function App()
(async () => (async () =>
{ {
const authenticationMetaData: QAuthenticationMetaData = await qController.getAuthenticationMetaData(); const authenticationMetaData: QAuthenticationMetaData = await qController.getAuthenticationMetaData();
setAuthenticationMetaData(authenticationMetaData);
if (authenticationMetaData.type === "AUTH_0") if (authenticationMetaData.type === "AUTH_0")
{ {
///////////////////////////////////////// await auth0SetupSession();
// use auth0 if auth type is ... auth0 //
/////////////////////////////////////////
try
{
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 {uuid: newSessionUuid, values} = 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);
localStorage.setItem("sessionValues", JSON.stringify(values));
console.log("Got new sessionUUID from backend, and stored new accessToken");
} }
else else if (authenticationMetaData.type === "OAUTH2")
{ {
console.log("Using existing sessionUUID cookie"); await oauth2SetupSession();
}
setIsFullyAuthenticated(true);
qController.setGotAuthentication();
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;
}
} }
else if (authenticationMetaData.type === "FULLY_ANONYMOUS" || authenticationMetaData.type === "MOCK") else if (authenticationMetaData.type === "FULLY_ANONYMOUS" || authenticationMetaData.type === "MOCK")
{ {
///////////////////////////////////////////// await anonymousSetupSession();
// use a random token if anonymous or mock //
/////////////////////////////////////////////
console.log("Generating random token...");
setIsFullyAuthenticated(true);
qController.setGotAuthentication();
setCookie(SESSION_UUID_COOKIE_NAME, Md5.hashStr(`${new Date()}`), {path: "/"});
console.log("Token generation complete.");
return;
} }
else else
{ {
@ -222,11 +128,34 @@ export default function App()
(async () => (async () =>
{ {
const metaData: QInstance = await qController.loadMetaData(); const metaData: QInstance = await qController.loadMetaData();
LicenseInfo.setLicenseKey(metaData.environmentValues.get("MATERIAL_UI_LICENSE_KEY") || process.env.REACT_APP_MATERIAL_UI_LICENSE_KEY); LicenseInfo.setLicenseKey(metaData.environmentValues?.get("MATERIAL_UI_LICENSE_KEY") || process.env.REACT_APP_MATERIAL_UI_LICENSE_KEY);
setNeedLicenseKey(false); setNeedLicenseKey(false);
})(); })();
} }
/***************************************************************************
** call approprite logout function based on authentication meta data type
***************************************************************************/
function doLogout()
{
if (authenticationMetaData?.type === "AUTH_0")
{
auth0Logout();
}
else if (authenticationMetaData?.type === "OAUTH2")
{
oauth2Logout();
}
else if (authenticationMetaData?.type === "FULLY_ANONYMOUS" || authenticationMetaData?.type === "MOCK")
{
anonymousLogout();
}
else
{
console.log(`No logout callback for authentication type [${authenticationMetaData?.type}].`);
}
}
const [controller, dispatch] = useMaterialUIController(); const [controller, dispatch] = useMaterialUIController();
const {miniSidenav, direction, layout, openConfigurator, sidenavColor} = controller; const {miniSidenav, direction, layout, openConfigurator, sidenavColor} = controller;
const [onMouseEnter, setOnMouseEnter] = useState(false); const [onMouseEnter, setOnMouseEnter] = useState(false);
@ -594,10 +523,7 @@ export default function App()
localStorage.removeItem("accessToken"); localStorage.removeItem("accessToken");
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"}); removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
////////////////////////////////////////////////////// doLogout();
// todo - this is auth0 logout... make more generic //
//////////////////////////////////////////////////////
logout();
return; return;
} }
} }
@ -605,7 +531,9 @@ export default function App()
})(); })();
}, [needToLoadRoutes, isFullyAuthenticated]); }, [needToLoadRoutes, isFullyAuthenticated]);
// Open sidenav when mouse enter on mini sidenav ///////////////////////////////////////////////////
// Open sidenav when mouse enter on mini sidenav //
///////////////////////////////////////////////////
const handleOnMouseEnter = () => const handleOnMouseEnter = () =>
{ {
if (miniSidenav && !onMouseEnter) if (miniSidenav && !onMouseEnter)
@ -615,7 +543,9 @@ export default function App()
} }
}; };
// Close sidenav when mouse leave mini sidenav /////////////////////////////////////////////////
// Close sidenav when mouse leave mini sidenav //
/////////////////////////////////////////////////
const handleOnMouseLeave = () => const handleOnMouseLeave = () =>
{ {
if (onMouseEnter) if (onMouseEnter)
@ -625,16 +555,14 @@ export default function App()
} }
}; };
// Change the openConfigurator state
const handleConfiguratorOpen = () => setOpenConfigurator(dispatch, !openConfigurator);
// Setting the dir attribute for the body element
useEffect(() => useEffect(() =>
{ {
document.body.setAttribute("dir", direction); document.body.setAttribute("dir", direction);
}, [direction]); }, [direction]);
// Setting page scroll to 0 when changing the route //////////////////////////////////////////////////////
// Setting page scroll to 0 when changing the route //
//////////////////////////////////////////////////////
useEffect(() => useEffect(() =>
{ {
document.documentElement.scrollTop = 0; document.documentElement.scrollTop = 0;
@ -674,12 +602,12 @@ export default function App()
const [dotMenuOpen, setDotMenuOpen] = useState(false); const [dotMenuOpen, setDotMenuOpen] = useState(false);
const [keyboardHelpOpen, setKeyboardHelpOpen] = useState(false); const [keyboardHelpOpen, setKeyboardHelpOpen] = useState(false);
const [helpHelpActive] = useState(queryParams.has("helpHelp")); const [helpHelpActive] = useState(queryParams.has("helpHelp"));
const [userId, setUserId] = useState(user?.email); const [userId, setUserId] = useState(loggedInUser?.email);
useEffect(() => useEffect(() =>
{ {
setUserId(user?.email) setUserId(loggedInUser?.email);
}, [user]); }, [loggedInUser]);
const [googleAnalyticsUtils] = useState(new GoogleAnalyticsUtils()); const [googleAnalyticsUtils] = useState(new GoogleAnalyticsUtils());
@ -689,26 +617,18 @@ export default function App()
*******************************************************************************/ *******************************************************************************/
function recordAnalytics(model: AnalyticsModel) function recordAnalytics(model: AnalyticsModel)
{ {
googleAnalyticsUtils.recordAnalytics(model) googleAnalyticsUtils.recordAnalytics(model);
} }
///////////////////////////////////////////////////////////////////
/*************************************************************************** // if any of the auth/session setup code determined that we need //
** // to render something and return early - then do so here. //
***************************************************************************/ ///////////////////////////////////////////////////////////////////
function banner(): JSX.Element | null if (earlyReturnForAuth)
{ {
const banner = getBanner(metaData?.branding, "QFMD_TOP_OF_SITE"); return (earlyReturnForAuth);
if (!banner)
{
return (null);
} }
return (<Box className={getBannerClassName(banner)} sx={{display: "flex", justifyContent: "center", padding: "0.5rem", position: "sticky", top: "0", zIndex: 1, ...getBannerStyles(banner)}}>
{makeBannerContent(banner)}
</Box>);
}
return ( return (
@ -737,7 +657,6 @@ export default function App()
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<CssBaseline /> <CssBaseline />
<CommandMenu metaData={metaData} /> <CommandMenu metaData={metaData} />
{banner()}
<Sidenav <Sidenav
color={sidenavColor} color={sidenavColor}
icon={branding.icon} icon={branding.icon}
@ -747,6 +666,7 @@ export default function App()
routes={sideNavRoutes} routes={sideNavRoutes}
onMouseEnter={handleOnMouseEnter} onMouseEnter={handleOnMouseEnter}
onMouseLeave={handleOnMouseLeave} onMouseLeave={handleOnMouseLeave}
logout={doLogout}
/> />
<Routes> <Routes>
<Route path="*" element={<Navigate to={defaultRoute} />} /> <Route path="*" element={<Navigate to={defaultRoute} />} />

View File

@ -19,116 +19,97 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {Auth0Provider} from "@auth0/auth0-react";
import {QAuthenticationMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAuthenticationMetaData"; import {QAuthenticationMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAuthenticationMetaData";
import React from "react";
import {createRoot} from "react-dom/client";
import {BrowserRouter, useNavigate, useSearchParams} from "react-router-dom";
import App from "App"; import App from "App";
import "qqq/styles/qqq-override-styles.css"; import "qqq/styles/qqq-override-styles.css";
import "qqq/styles/globals.scss"; import "qqq/styles/globals.scss";
import "qqq/styles/raycast.scss"; import "qqq/styles/raycast.scss";
import HandleAuthorizationError from "HandleAuthorizationError"; import useAnonymousAuthenticationModule from "qqq/authorization/anonymous/useAnonymousAuthenticationModule";
import ProtectedRoute from "qqq/authorization/auth0/ProtectedRoute"; import useAuth0AuthenticationModule from "qqq/authorization/auth0/useAuth0AuthenticationModule";
import useOAuth2AuthenticationModule from "qqq/authorization/oauth2/useOAuth2AuthenticationModule";
import {MaterialUIControllerProvider} from "qqq/context"; import {MaterialUIControllerProvider} from "qqq/context";
import Client from "qqq/utils/qqq/Client"; import Client from "qqq/utils/qqq/Client";
import React from "react";
import {createRoot} from "react-dom/client";
import {BrowserRouter} from "react-router-dom";
const qController = Client.getInstance(); const qController = Client.getInstance();
if(document.location.search && document.location.search.indexOf("clearAuthenticationMetaDataLocalStorage") > -1) if (document.location.search && document.location.search.indexOf("clearAuthenticationMetaDataLocalStorage") > -1)
{ {
qController.clearAuthenticationMetaDataLocalStorage() qController.clearAuthenticationMetaDataLocalStorage();
} }
const authenticationMetaDataPromise: Promise<QAuthenticationMetaData> = qController.getAuthenticationMetaData() const authenticationMetaDataPromise: Promise<QAuthenticationMetaData> = qController.getAuthenticationMetaData();
authenticationMetaDataPromise.then((authenticationMetaData) => authenticationMetaDataPromise.then((authenticationMetaData) =>
{ {
// @ts-ignore
function Auth0ProviderWithRedirectCallback({children, ...props})
{
const navigate = useNavigate();
const [searchParams] = useSearchParams();
// @ts-ignore /***************************************************************************
const onRedirectCallback = (appState) => **
***************************************************************************/
function Auth0RouterBody()
{ {
navigate((appState && appState.returnTo) || window.location.pathname); const {renderAppWrapper} = useAuth0AuthenticationModule({});
}; return (renderAppWrapper(authenticationMetaData, null));
if (searchParams.get("error")) }
/***************************************************************************
**
***************************************************************************/
function OAuth2RouterBody()
{ {
return ( const {renderAppWrapper} = useOAuth2AuthenticationModule({});
// @ts-ignore return (renderAppWrapper(authenticationMetaData, (
<Auth0Provider {...props}> <MaterialUIControllerProvider>
<HandleAuthorizationError errorMessage={searchParams.get("error_description")} /> <App />
</Auth0Provider> </MaterialUIControllerProvider>
); )));
} }
else
/***************************************************************************
**
***************************************************************************/
function AnonymousRouterBody()
{ {
return ( const {renderAppWrapper} = useAnonymousAuthenticationModule({});
// @ts-ignore return (renderAppWrapper(authenticationMetaData, (
<Auth0Provider onRedirectCallback={onRedirectCallback} {...props}> <MaterialUIControllerProvider>
{children} <App />
</Auth0Provider> </MaterialUIControllerProvider>
); )));
}
} }
const container = document.getElementById("root"); const container = document.getElementById("root");
const root = createRoot(container); const root = createRoot(container);
if (authenticationMetaData.type === "AUTH_0") if (authenticationMetaData.type === "AUTH_0")
{ {
// @ts-ignore root.render(<BrowserRouter>
let domain: string = authenticationMetaData.data.baseUrl; <Auth0RouterBody />
</BrowserRouter>);
// @ts-ignore
const clientId = authenticationMetaData.data.clientId;
// @ts-ignore
const audience = authenticationMetaData.data.audience;
if(!domain || !clientId)
{
root.render(
<div>Error: AUTH0 authenticationMetaData is missing domain [{domain}] and/or clientId [{clientId}].</div>
);
return;
} }
else if (authenticationMetaData.type === "OAUTH2")
if(domain.endsWith("/"))
{ {
///////////////////////////////////////////////////////////////////////////////////// root.render(<BrowserRouter>
// auth0 lib fails if we have a trailing slash. be a bit more graceful than that. // <OAuth2RouterBody />
///////////////////////////////////////////////////////////////////////////////////// </BrowserRouter>);
domain = domain.replace(/\/$/, "");
} }
else if (authenticationMetaData.type === "FULLY_ANONYMOUS" || authenticationMetaData.type === "MOCK")
root.render( {
<BrowserRouter> root.render(<BrowserRouter>
<Auth0ProviderWithRedirectCallback <AnonymousRouterBody />
domain={domain} </BrowserRouter>);
clientId={clientId}
audience={audience}
redirectUri={`${window.location.origin}/`}
>
<MaterialUIControllerProvider>
<ProtectedRoute component={App} />
</MaterialUIControllerProvider>
</Auth0ProviderWithRedirectCallback>
</BrowserRouter>
);
} }
else else
{ {
root.render( root.render(<div>
<BrowserRouter> Error: Unknown authenticationMetaData type: [{authenticationMetaData.type}].
<MaterialUIControllerProvider> </div>);
<App />
</MaterialUIControllerProvider>
</BrowserRouter>
);
} }
}) });

View File

@ -1,36 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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/>.
*/
package com.kingsrook.qqq.frontend.materialdashboard.model.metadata;
import com.kingsrook.qqq.backend.core.model.metadata.branding.BannerSlot;
/*******************************************************************************
**
*******************************************************************************/
public enum MaterialDashboardBannerSlots implements BannerSlot
{
QFMD_TOP_OF_SITE,
QFMD_TOP_OF_BODY,
QFMD_SIDE_NAV_UNDER_LOGO
}

View File

@ -0,0 +1,82 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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/>.
*/
import {QAuthenticationMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAuthenticationMetaData";
import {SESSION_UUID_COOKIE_NAME} from "App";
import Client from "qqq/utils/qqq/Client";
import {useCookies} from "react-cookie";
import {Md5} from "ts-md5/dist/md5";
const qController = Client.getInstance();
interface Props
{
setIsFullyAuthenticated?: (is: boolean) => void;
setLoggedInUser?: (user: any) => void;
setEarlyReturnForAuth?: (element: JSX.Element | null) => void;
}
/***************************************************************************
** hook for working with the anonymous authentication module
***************************************************************************/
export default function useAnonymousAuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth}: Props)
{
const [cookies, setCookie, removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
/***************************************************************************
**
***************************************************************************/
const setupSession = async () =>
{
console.log("Generating random token...");
setIsFullyAuthenticated(true);
qController.setGotAuthentication();
setCookie(SESSION_UUID_COOKIE_NAME, Md5.hashStr(`${new Date()}`), {path: "/"});
console.log("Token generation complete.");
};
/***************************************************************************
**
***************************************************************************/
const logout = () =>
{
qController.clearAuthenticationMetaDataLocalStorage();
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
};
/***************************************************************************
**
***************************************************************************/
const renderAppWrapper = (authenticationMetaData: QAuthenticationMetaData, children: JSX.Element): JSX.Element =>
{
return children;
};
return {
setupSession,
logout,
renderAppWrapper
};
}

View File

@ -0,0 +1,248 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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/>.
*/
import {Auth0Provider, useAuth0} from "@auth0/auth0-react";
import {QAuthenticationMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAuthenticationMetaData";
import App, {SESSION_UUID_COOKIE_NAME} from "App";
import HandleAuthorizationError from "HandleAuthorizationError";
import jwt_decode from "jwt-decode";
import ProtectedRoute from "qqq/authorization/auth0/ProtectedRoute";
import {MaterialUIControllerProvider} from "qqq/context";
import Client from "qqq/utils/qqq/Client";
import {useCookies} from "react-cookie";
import {useNavigate, useSearchParams} from "react-router-dom";
const qController = Client.getInstance();
interface Props
{
setIsFullyAuthenticated?: (is: boolean) => void;
setLoggedInUser?: (user: any) => void;
setEarlyReturnForAuth?: (element: JSX.Element | null) => void;
}
/***************************************************************************
** hook for working with the Auth0 authentication module
***************************************************************************/
export default function useAuth0AuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth}: Props)
{
const {user: auth0User, getAccessTokenSilently: auth0GetAccessTokenSilently, logout: useAuth0Logout} = useAuth0();
const [cookies, setCookie, removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
/***************************************************************************
**
***************************************************************************/
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);
};
/***************************************************************************
**
***************************************************************************/
const setupSession = async () =>
{
try
{
console.log("Loading token from auth0...");
const accessToken = await auth0GetAccessTokenSilently();
const lsAccessToken = localStorage.getItem("accessToken");
if (shouldStoreNewToken(accessToken, lsAccessToken))
{
console.log("Sending accessToken to backend, requesting a sessionUUID...");
const {uuid: newSessionUuid, values} = 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);
localStorage.setItem("sessionValues", JSON.stringify(values));
console.log("Got new sessionUUID from backend, and stored new accessToken");
}
else
{
console.log("Using existing sessionUUID cookie");
}
setIsFullyAuthenticated(true);
qController.setGotAuthentication();
setLoggedInUser(auth0User);
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: "/"});
useAuth0Logout();
return;
}
};
/***************************************************************************
**
***************************************************************************/
const logout = () =>
{
useAuth0Logout({returnTo: window.location.origin});
};
/***************************************************************************
**
***************************************************************************/
// @ts-ignore
function Auth0ProviderWithRedirectCallback({children, ...props})
{
const navigate = useNavigate();
const [searchParams] = useSearchParams();
// @ts-ignore
const onRedirectCallback = (appState) =>
{
navigate((appState && appState.returnTo) || window.location.pathname);
};
if (searchParams.get("error"))
{
return (
// @ts-ignore
<Auth0Provider {...props}>
<HandleAuthorizationError errorMessage={searchParams.get("error_description")} />
</Auth0Provider>
);
}
else
{
return (
// @ts-ignore
<Auth0Provider onRedirectCallback={onRedirectCallback} {...props}>
{children}
</Auth0Provider>
);
}
}
/***************************************************************************
**
***************************************************************************/
const renderAppWrapper = (authenticationMetaData: QAuthenticationMetaData, children: JSX.Element): JSX.Element =>
{
// @ts-ignore
let domain: string = authenticationMetaData.data.baseUrl;
// @ts-ignore
const clientId = authenticationMetaData.data.clientId;
// @ts-ignore
const audience = authenticationMetaData.data.audience;
if (!domain || !clientId)
{
return (
<div>Error: AUTH0 authenticationMetaData is missing baseUrl [{domain}] and/or clientId [{clientId}].</div>
);
}
if (domain.endsWith("/"))
{
/////////////////////////////////////////////////////////////////////////////////////
// auth0 lib fails if we have a trailing slash. be a bit more graceful than that. //
/////////////////////////////////////////////////////////////////////////////////////
domain = domain.replace(/\/$/, "");
}
return (
<Auth0ProviderWithRedirectCallback
domain={domain}
clientId={clientId}
audience={audience}
redirectUri={`${window.location.origin}/`}>
<MaterialUIControllerProvider>
<ProtectedRoute component={App} />
</MaterialUIControllerProvider>
</Auth0ProviderWithRedirectCallback>
);
};
return {
setupSession,
logout,
renderAppWrapper
};
}

View File

@ -0,0 +1,195 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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/>.
*/
import {QAuthenticationMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAuthenticationMetaData";
import {SESSION_UUID_COOKIE_NAME} from "App";
import Client from "qqq/utils/qqq/Client";
import {useCookies} from "react-cookie";
import {AuthProvider, useAuth} from "react-oidc-context";
import {useNavigate, useSearchParams} from "react-router-dom";
const qController = Client.getInstance();
interface Props
{
setIsFullyAuthenticated?: (is: boolean) => void;
setLoggedInUser?: (user: any) => void;
setEarlyReturnForAuth?: (element: JSX.Element | null) => void;
}
/***************************************************************************
** hook for working with the OAuth2 authentication module
***************************************************************************/
export default function useOAuth2AuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth}: Props)
{
const authOidc = useAuth();
const [cookies, setCookie, removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
const [searchParams] = useSearchParams();
const navigate = useNavigate();
/***************************************************************************
**
***************************************************************************/
const setupSession = async () =>
{
try
{
const preSigninRedirectPathnameKey = "oauth2.preSigninRedirect.pathname";
if (window.location.pathname == "/token")
{
const code = searchParams.get("code");
const state = searchParams.get("state");
const oidcString = localStorage.getItem(`oidc.${state}`);
if (oidcString)
{
const oidcObject = JSON.parse(oidcString) as { [name: string]: any };
console.log(oidcObject);
const manageSessionRequestBody = {code: code, codeVerifier: oidcObject.code_verifier, redirectUri: oidcObject.redirect_uri};
const {uuid: newSessionUuid, values} = await qController.manageSession(null, null, manageSessionRequestBody);
console.log(`we have new session UUID: ${newSessionUuid}`);
setIsFullyAuthenticated(true);
qController.setGotAuthentication();
setLoggedInUser(values?.user);
console.log("Token load complete.");
const preSigninRedirectPathname = localStorage.getItem(preSigninRedirectPathnameKey);
localStorage.removeItem(preSigninRedirectPathname);
navigate(preSigninRedirectPathname ?? "/", {replace: true});
}
}
else
{
const sessionUuid = cookies[SESSION_UUID_COOKIE_NAME];
if (sessionUuid)
{
console.log(`we have session UUID: ${sessionUuid} - validating it...`);
const {uuid: newSessionUuid, values} = await qController.manageSession(null, sessionUuid, null);
setIsFullyAuthenticated(true);
qController.setGotAuthentication();
setLoggedInUser(values?.user);
console.log("Token load complete.");
}
else
{
console.log("Loading token from OAuth2 provider...");
console.log(authOidc);
localStorage.setItem(preSigninRedirectPathnameKey, window.location.pathname);
setEarlyReturnForAuth(<div>Signing in...</div>);
authOidc.signinRedirect();
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// this is what's in the docs, but, it sure doesn't seem to ever hit any case other than the signinRedirect block //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/*
if (authOidc.isLoading)
{
setLoadingToken(false); //? so we can come back in? but i'm missing something here.
setEarlyReturnForAuth(<div>
<div>Loading...</div>
<button onClick={() => incrementCheckLoadingCounter()}>check again?</button>
</div>);
}
else if (authOidc.error)
{
setEarlyReturnForAuth(<div>Error: {authOidc.error.message}</div>);
}
else if (authOidc.isAuthenticated)
{
setEarlyReturnForAuth(
<div>
Welcome, {authOidc.user?.profile.name}!
<button onClick={() => authOidc.signoutRedirect()}>Log out</button>
</div>
);
}
else
{
localStorage.setItem(preSigninRedirectPathnameKey, window.location.pathname);
setEarlyReturnForAuth(<div>Signing in...</div>);
authOidc.signinRedirect();
}
*/
}
}
catch (e)
{
console.log(`Error loading token: ${JSON.stringify(e)}`);
logout();
return;
}
};
/***************************************************************************
**
***************************************************************************/
const logout = () =>
{
qController.clearAuthenticationMetaDataLocalStorage();
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
authOidc.signoutRedirect();
};
/***************************************************************************
**
***************************************************************************/
const renderAppWrapper = (authenticationMetaData: QAuthenticationMetaData, children: JSX.Element): JSX.Element =>
{
const authority: string = authenticationMetaData.data.baseUrl;
const clientId = authenticationMetaData.data.clientId;
if (!authority || !clientId)
{
return (
<div>Error: OAuth2 authenticationMetaData is missing baseUrl [{authority}] and/or clientId [{clientId}].</div>
);
}
const oidcConfig =
{
authority: authority,
client_id: clientId,
redirect_uri: `${window.location.origin}/token`,
response_type: "code",
scope: "openid profile email offline_access",
};
return (<AuthProvider {...oidcConfig}>
{children}
</AuthProvider>
);
};
return {
setupSession,
logout,
renderAppWrapper
};
}

View File

@ -1,38 +0,0 @@
/*
* 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/>.
*/
import {useAuth0} from "@auth0/auth0-react";
import {Button} from "@mui/material";
import React from "react";
function AuthenticationButton()
{
const {loginWithRedirect, logout, isAuthenticated} = useAuth0();
if (isAuthenticated)
{
return <Button onClick={() => logout({returnTo: window.location.origin})}>Log Out</Button>;
}
return <Button onClick={() => loginWithRedirect()}>Log In</Button>;
}
export default AuthenticationButton;

View File

@ -96,7 +96,6 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
if (width == "full") if (width == "full")
{ {
itemSM = 12; itemSM = 12;
itemLG = 12;
} }
return ( return (

View File

@ -20,22 +20,21 @@
*/ */
import {QBrandingMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QBrandingMetaData"; import {QBrandingMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QBrandingMetaData";
import {Button} from "@mui/material";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Divider from "@mui/material/Divider"; import Divider from "@mui/material/Divider";
import Icon from "@mui/material/Icon"; import Icon from "@mui/material/Icon";
import Link from "@mui/material/Link"; import Link from "@mui/material/Link";
import List from "@mui/material/List"; import List from "@mui/material/List";
import {ReactNode, useEffect, useReducer, useState} from "react";
import {NavLink, useLocation} from "react-router-dom";
import AuthenticationButton from "qqq/components/buttons/AuthenticationButton";
import SideNavCollapse from "qqq/components/horseshoe/sidenav/SideNavCollapse"; import SideNavCollapse from "qqq/components/horseshoe/sidenav/SideNavCollapse";
import SideNavItem from "qqq/components/horseshoe/sidenav/SideNavItem"; import SideNavItem from "qqq/components/horseshoe/sidenav/SideNavItem";
import SideNavList from "qqq/components/horseshoe/sidenav/SideNavList"; import SideNavList from "qqq/components/horseshoe/sidenav/SideNavList";
import SidenavRoot from "qqq/components/horseshoe/sidenav/SideNavRoot"; import SidenavRoot from "qqq/components/horseshoe/sidenav/SideNavRoot";
import sidenavLogoLabel from "qqq/components/horseshoe/sidenav/styles/SideNav"; import sidenavLogoLabel from "qqq/components/horseshoe/sidenav/styles/SideNav";
import MDTypography from "qqq/components/legacy/MDTypography"; import MDTypography from "qqq/components/legacy/MDTypography";
import {getBannerClassName, getBannerStyles, getBanner, makeBannerContent} from "qqq/components/misc/Banners";
import {setMiniSidenav, setTransparentSidenav, setWhiteSidenav, useMaterialUIController,} from "qqq/context"; import {setMiniSidenav, setTransparentSidenav, setWhiteSidenav, useMaterialUIController,} from "qqq/context";
import {ReactNode, useEffect, useReducer, useState} from "react";
import {NavLink, useLocation} from "react-router-dom";
interface Props interface Props
@ -45,6 +44,7 @@ interface Props
logo?: string; logo?: string;
appName?: string; appName?: string;
branding?: QBrandingMetaData; branding?: QBrandingMetaData;
logout: () => void;
routes: { routes: {
[key: string]: [key: string]:
| ReactNode | ReactNode
@ -67,7 +67,7 @@ interface Props
[key: string]: any; [key: string]: any;
} }
function Sidenav({color, icon, logo, appName, branding, routes, ...rest}: Props): JSX.Element function Sidenav({color, icon, logo, appName, branding, routes, logout, ...rest}: Props): JSX.Element
{ {
const [openCollapse, setOpenCollapse] = useState<boolean | string>(false); const [openCollapse, setOpenCollapse] = useState<boolean | string>(false);
const [openNestedCollapse, setOpenNestedCollapse] = useState<boolean | string>(false); const [openNestedCollapse, setOpenNestedCollapse] = useState<boolean | string>(false);
@ -258,7 +258,7 @@ function Sidenav({color, icon, logo, appName, branding, routes, ...rest}: Props)
active={key === collapseName} active={key === collapseName}
open={openCollapse === key} open={openCollapse === key}
noCollapse={noCollapse} noCollapse={noCollapse}
onClick={() => (! noCollapse ? (openCollapse === key ? setOpenCollapse(false) : setOpenCollapse(key)) : null) } onClick={() => (!noCollapse ? (openCollapse === key ? setOpenCollapse(false) : setOpenCollapse(key)) : null)}
> >
{collapse ? renderCollapse(collapse) : null} {collapse ? renderCollapse(collapse) : null}
</SideNavCollapse> </SideNavCollapse>
@ -301,30 +301,6 @@ function Sidenav({color, icon, logo, appName, branding, routes, ...rest}: Props)
} }
); );
/***************************************************************************
**
***************************************************************************/
function EnvironmentBanner({branding}: { branding: QBrandingMetaData }): JSX.Element | null
{
// deprecated!
if (branding && branding.environmentBannerText)
{
return <Box mt={2} bgcolor={branding.environmentBannerColor} borderRadius={2}>
{branding.environmentBannerText}
</Box>;
}
const banner = getBanner(branding, "QFMD_SIDE_NAV_UNDER_LOGO");
if (banner)
{
return <Box className={getBannerClassName(banner)} mt={2} borderRadius={2} sx={getBannerStyles(banner)}>
{makeBannerContent(banner)}
</Box>;
}
return (null);
}
return ( return (
<SidenavRoot <SidenavRoot
{...rest} {...rest}
@ -355,7 +331,12 @@ function Sidenav({color, icon, logo, appName, branding, routes, ...rest}: Props)
</Box> </Box>
} }
</Box> </Box>
<EnvironmentBanner branding={branding} /> {
branding && branding.environmentBannerText &&
<Box mt={2} bgcolor={branding.environmentBannerColor} borderRadius={2}>
{branding.environmentBannerText}
</Box>
}
</Box> </Box>
<Divider <Divider
light={ light={
@ -370,7 +351,7 @@ function Sidenav({color, icon, logo, appName, branding, routes, ...rest}: Props)
(darkMode && !transparentSidenav && whiteSidenav) (darkMode && !transparentSidenav && whiteSidenav)
} }
/> />
<AuthenticationButton /> <Button onClick={logout}>Log Out</Button>
</SidenavRoot> </SidenavRoot>
); );
} }

View File

@ -97,7 +97,6 @@ export default styled(Drawer)(({theme, ownerState}: { theme?: Theme | any; owner
margin: "0", margin: "0",
borderRadius: "0", borderRadius: "0",
height: "100%", height: "100%",
top: "unset",
...(miniSidenav ? drawerCloseStyles() : drawerOpenStyles()), ...(miniSidenav ? drawerCloseStyles() : drawerOpenStyles()),
}, },

View File

@ -1,97 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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/>.
*/
import {Banner} from "@kingsrook/qqq-frontend-core/lib/model/metaData/Banner";
import {QBrandingMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QBrandingMetaData";
import parse from "html-react-parser";
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// One may render a banner using the functions in this file as: //
// //
// const banner = getBanner(branding, "QFMD_SIDE_NAV_UNDER_LOGO"); //
// return (<Box className={getBannerClassName(banner)} sx={{padding: "1rem", ...getBannerStyles(banner)}}> //
// {makeBannerContent(banner)} //
// </Box>); //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
/***************************************************************************
**
***************************************************************************/
export function getBanner(branding: QBrandingMetaData, slot: string): Banner | null
{
if (branding?.banners?.has(slot))
{
return (branding.banners.get(slot));
}
return (null);
}
/***************************************************************************
**
***************************************************************************/
export function getBannerStyles(banner: Banner)
{
let bgColor = "";
let color = "";
if (banner)
{
if (banner.backgroundColor)
{
bgColor = banner.backgroundColor;
}
if (banner.textColor)
{
bgColor = banner.textColor;
}
}
const rest = banner?.additionalStyles ?? {};
return ({
backgroundColor: bgColor,
color: color,
...rest
});
}
/***************************************************************************
**
***************************************************************************/
export function getBannerClassName(banner: Banner)
{
return `banner ${banner?.severity?.toLowerCase()}`;
}
/***************************************************************************
**
***************************************************************************/
export function makeBannerContent(banner: Banner): JSX.Element
{
return <>{banner?.messageHTML ? parse(banner?.messageHTML) : banner?.messageText}</>;
}

View File

@ -20,7 +20,6 @@
*/ */
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData"; import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
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 {QTableVariant} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableVariant";
@ -36,11 +35,12 @@ 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 TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import {any} from "prop-types";
import React, {useState} from "react";
import {useNavigate} from "react-router-dom";
import {QCancelButton} from "qqq/components/buttons/DefaultButtons"; import {QCancelButton} from "qqq/components/buttons/DefaultButtons";
import MDButton from "qqq/components/legacy/MDButton"; import MDButton from "qqq/components/legacy/MDButton";
import Client from "qqq/utils/qqq/Client"; import Client from "qqq/utils/qqq/Client";
import React, {useState} from "react";
import {useNavigate} from "react-router-dom";
interface Props interface Props
{ {
@ -182,18 +182,18 @@ function GotoRecordDialog(props: Props): JSX.Element
options[optionIndex].forEach((field) => options[optionIndex].forEach((field) =>
{ {
if (values[field.name]) if(values[field.name])
{ {
anyFieldsInThisOptionHaveAValue = true; anyFieldsInThisOptionHaveAValue = true;
} }
}); })
if (!anyFieldsInThisOptionHaveAValue) if(!anyFieldsInThisOptionHaveAValue)
{ {
return (true); return (true);
} }
return (false); return (false);
}; }
/*************************************************************************** /***************************************************************************
@ -207,13 +207,9 @@ function GotoRecordDialog(props: Props): JSX.Element
const queryStringParts: string[] = []; const queryStringParts: string[] = [];
options[optionIndex].forEach((field) => options[optionIndex].forEach((field) =>
{ {
if (field.type == QFieldType.STRING && !values[field.name][0]) criteria.push(new QFilterCriteria(field.name, QCriteriaOperator.EQUALS, [values[field.name]]))
{ queryStringParts.push(`${field.name}=${encodeURIComponent(values[field.name])}`)
return; })
}
criteria.push(new QFilterCriteria(field.name, QCriteriaOperator.EQUALS, [values[field.name]]));
queryStringParts.push(`${field.name}=${encodeURIComponent(values[field.name])}`);
});
const filter = new QQueryFilter(criteria, null, null, "AND", null, 10); const filter = new QQueryFilter(criteria, null, null, "AND", null, 10);
@ -227,7 +223,7 @@ function GotoRecordDialog(props: Props): JSX.Element
} }
else if (queryResult.length == 1) else if (queryResult.length == 1)
{ {
if (options[optionIndex].length == 1 && options[optionIndex][0].name == pkey?.name) if(options[optionIndex].length == 1 && options[optionIndex][0].name == pkey?.name)
{ {
///////////////////////////////////////////////// /////////////////////////////////////////////////
// navigate by pkey, if that's how we searched // // navigate by pkey, if that's how we searched //

View File

@ -21,12 +21,11 @@
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance"; import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import {ReactNode, useEffect, useState} from "react";
import Footer from "qqq/components/horseshoe/Footer"; import Footer from "qqq/components/horseshoe/Footer";
import NavBar from "qqq/components/horseshoe/NavBar"; import NavBar from "qqq/components/horseshoe/NavBar";
import {getBannerClassName, getBannerStyles, getBanner, makeBannerContent} from "qqq/components/misc/Banners";
import DashboardLayout from "qqq/layouts/DashboardLayout"; import DashboardLayout from "qqq/layouts/DashboardLayout";
import Client from "qqq/utils/qqq/Client"; import Client from "qqq/utils/qqq/Client";
import {ReactNode, useEffect, useState} from "react";
interface Props interface Props
{ {
@ -81,34 +80,12 @@ function BaseLayout({stickyNavbar, children}: Props): JSX.Element
return () => window.removeEventListener("resize", handleTabsOrientation); return () => window.removeEventListener("resize", handleTabsOrientation);
}, [tabsOrientation]); }, [tabsOrientation]);
/***************************************************************************
**
***************************************************************************/
function banner(): JSX.Element | null
{
const banner = getBanner(metaData?.branding, "QFMD_TOP_OF_BODY");
if (!banner)
{
return (null);
}
return (<Box className={getBannerClassName(banner)} sx={{display: "flex", justifyContent: "center", padding: "0.5rem", margin: "-20px", marginBottom: "20px", ...getBannerStyles(banner)}}>
{makeBannerContent(banner)}
</Box>);
}
return ( return (
<>
<DashboardLayout> <DashboardLayout>
{banner()}
<NavBar /> <NavBar />
<Box>{children}</Box> <Box>{children}</Box>
<Footer company={{href: metaData?.branding?.companyUrl, name: metaData?.branding?.companyName}} /> <Footer company={{href: metaData?.branding?.companyUrl, name: metaData?.branding?.companyName}} />
</DashboardLayout> </DashboardLayout>
</>
); );
} }

View File

@ -1838,20 +1838,6 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
return; return;
} }
if(urlSearchParams.get("defaultProcessValues"))
{
if(!defaultProcessValues)
{
defaultProcessValues = {}
}
const values = JSON.parse(urlSearchParams.get("defaultProcessValues"));
for (let key in values)
{
defaultProcessValues[key] = values[key]
}
}
if (defaultProcessValues) if (defaultProcessValues)
{ {
for (let key in defaultProcessValues) for (let key in defaultProcessValues)

View File

@ -1612,22 +1612,6 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
*******************************************************************************/ *******************************************************************************/
const processClicked = (process: QProcessMetaData) => const processClicked = (process: QProcessMetaData) =>
{ {
if (process.minInputRecords != null && process.minInputRecords > 0 && getNoOfSelectedRecords() === 0)
{
setAlertContent(`No records were selected for the process: ${process.label}`);
return;
}
else if (process.minInputRecords != null && getNoOfSelectedRecords() < process.minInputRecords)
{
setAlertContent(`Too few records were selected for the process: ${process.label}. A minimum of ${process.minInputRecords} is required.`);
return;
}
else if (process.maxInputRecords != null && getNoOfSelectedRecords() > process.maxInputRecords)
{
setAlertContent(`Too many records were selected for the process: ${process.label}. A maximum of ${process.maxInputRecords} is allowed.`);
return;
}
// todo - let the process specify that it needs initial rows - err if none selected. // todo - let the process specify that it needs initial rows - err if none selected.
// alternatively, let a process itself have an initial screen to select rows... // alternatively, let a process itself have an initial screen to select rows...
openModalProcess(process); openModalProcess(process);

View File

@ -748,54 +748,35 @@ input[type="search"]::-webkit-search-results-decoration
padding: 8px 0; padding: 8px 0;
} }
.helpContentAlert.info, .helpContentAlert.success
.banner.info
{
background-color: rgb(234, 242, 255);
color: rgb(20, 51, 102);
}
.helpContentAlert.info .MuiAlert-icon .material-icons-round,
.banner.info .MuiAlert-icon .material-icons-round
{
color: #0062FF;
}
.helpContentAlert.success,
.banner.success
{ {
background-color: rgb(240, 248, 241); background-color: rgb(240, 248, 241);
color: rgb(44, 76, 46); color: rgb(44, 76, 46);
} }
.helpContentAlert.success .MuiAlert-icon .material-icons-round, .helpContentAlert.success .MuiAlert-icon .material-icons-round
.banner.success .MuiAlert-icon .material-icons-round
{ {
color: #4CAF50; color: #4CAF50;
} }
.helpContentAlert.warning, .helpContentAlert.warning
.banner.warning
{ {
background-color: rgb(254, 245, 234); background-color: rgb(254, 245, 234);
color: rgb(100, 65, 20); color: rgb(100, 65, 20);
} }
.helpContentAlert.warning .MuiAlert-icon .material-icons-round, .helpContentAlert.warning .MuiAlert-icon .material-icons-round
.banner.warning .MuiAlert-icon .material-icons-round
{ {
color: #fb8c00; color: #fb8c00;
} }
.helpContentAlert.error, .helpContentAlert.error
.banner.error
{ {
background-color: rgb(254, 239, 238); background-color: rgb(254, 239, 238);
color: rgb(98, 41, 37); color: rgb(98, 41, 37);
} }
.helpContentAlert.error .MuiAlert-icon .material-icons-round, .helpContentAlert.error .MuiAlert-icon .material-icons-round
.banner.error .MuiAlert-icon .material-icons-round
{ {
color: #F44335; color: #F44335;
} }

View File

@ -102,7 +102,7 @@ export default class GoogleAnalyticsUtils
console.log("Error reading session values from localStorage: " + e); console.log("Error reading session values from localStorage: " + e);
} }
if (this.metaData.environmentValues.get("GOOGLE_ANALYTICS_ENABLED") == "true" && this.metaData.environmentValues.get("GOOGLE_ANALYTICS_TRACKING_ID")) if (this.metaData.environmentValues?.get("GOOGLE_ANALYTICS_ENABLED") == "true" && this.metaData.environmentValues?.get("GOOGLE_ANALYTICS_TRACKING_ID"))
{ {
this.active = true; this.active = true;