/*
* 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 .
*/
import {useAuth0} from "@auth0/auth0-react";
import {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException";
import {QAppNodeType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAppNodeType";
import {QAppTreeNode} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAppTreeNode";
import {QBrandingMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QBrandingMetaData";
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
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 {useCookies} from "react-cookie";
import {Navigate, Route, Routes, useLocation,} from "react-router-dom";
import {Md5} from "ts-md5/dist/md5";
import {setMiniSidenav, setOpenConfigurator, useMaterialUIController} from "context";
import Settings from "layouts/pages/account/settings";
import ProfileOverview from "layouts/pages/profile/profile-overview";
import Sidenav from "qqq/components/Sidenav";
import Configurator from "qqq/components/Temporary/Configurator";
import MDAvatar from "qqq/components/Temporary/MDAvatar";
import MDBox from "qqq/components/Temporary/MDBox";
import theme from "qqq/components/Temporary/Theme";
import AppHome from "qqq/pages/app-home";
import CarrierPerformance from "qqq/pages/dashboards/CarrierPerformance";
import Overview from "qqq/pages/dashboards/Overview";
import EntityCreate from "qqq/pages/entity-create";
import EntityEdit from "qqq/pages/entity-edit";
import EntityList from "qqq/pages/entity-list";
import EntityView from "qqq/pages/entity-view";
import ProcessRun from "qqq/pages/process-run";
import QClient from "qqq/utils/QClient";
import QProcessUtils from "qqq/utils/QProcessUtils";
///////////////////////////////////////////////////////////////////////////////////////////////
// define the parts of the nav that are static - before the qqq tables etc get dynamic added //
///////////////////////////////////////////////////////////////////////////////////////////////
function getStaticRoutes()
{
return [
{type: "divider", key: "divider-0"},
{
type: "collapse",
name: "Dashboards",
key: "dashboards",
icon: dashboard,
collapse: [
{
name: "Overview",
key: "overview",
route: "/dashboards/overview",
component: ,
},
{
name: "Carrier Performance",
key: "carrierPerformance",
route: "/dashboards/carrierPerformance",
component: ,
},
],
},
{type: "divider", key: "divider-1"},
];
}
export const SESSION_ID_COOKIE_NAME = "sessionId";
LicenseInfo.setLicenseKey(process.env.REACT_APP_MATERIAL_UI_LICENSE_KEY);
export default function App()
{
const [, setCookie, removeCookie] = useCookies([SESSION_ID_COOKIE_NAME]);
const {user, getAccessTokenSilently, getIdTokenClaims, logout} = useAuth0();
const [loadingToken, setLoadingToken] = useState(false);
const [isFullyAuthenticated, setIsFullyAuthenticated] = useState(false);
const [profileRoutes, setProfileRoutes] = useState({});
const [branding, setBranding] = useState({} as QBrandingMetaData);
useEffect(() =>
{
if (loadingToken)
{
return;
}
setLoadingToken(true);
(async () =>
{
try
{
console.log("Loading token...");
await getAccessTokenSilently();
const idToken = await getIdTokenClaims();
setCookie(SESSION_ID_COOKIE_NAME, idToken.__raw, {path: "/"});
setIsFullyAuthenticated(true);
console.log("Token load complete.");
}
catch (e)
{
console.log(`Error loading token: ${JSON.stringify(e)}`);
removeCookie(SESSION_ID_COOKIE_NAME);
logout();
return;
}
})();
}, [loadingToken]);
const [controller, dispatch] = useMaterialUIController();
const {miniSidenav, direction, layout, openConfigurator, sidenavColor} = controller;
const [onMouseEnter, setOnMouseEnter] = useState(false);
const {pathname} = useLocation();
const [needToLoadRoutes, setNeedToLoadRoutes] = useState(true);
const [sideNavRoutes, setSideNavRoutes] = useState(getStaticRoutes());
const [appRoutes, setAppRoutes] = useState(null as any);
////////////////////////////////////////////
// load qqq meta data to make more routes //
////////////////////////////////////////////
useEffect(() =>
{
if (!needToLoadRoutes || !isFullyAuthenticated)
{
return;
}
setNeedToLoadRoutes(false);
(async () =>
{
function addAppToSideNavList(app: QAppTreeNode, appList: any[], parentPath: string, depth: number)
{
const path = `${parentPath}/${app.name}`;
if (app.type !== QAppNodeType.APP)
{
return;
}
if (depth > 2)
{
console.warn("App depth is greater than 2 - not including app in side nav...");
return;
}
const childList: any[] = [];
if (app.children)
{
app.children.forEach((child: QAppTreeNode) =>
{
addAppToSideNavList(child, childList, path, depth + 1);
});
}
if (childList.length === 0)
{
if (depth === 0)
{
/////////////////////////////////////////////////////
// at level 0, the entry must always be a collapse //
/////////////////////////////////////////////////////
appList.push({
type: "collapse",
name: app.label,
key: app.name,
route: path,
icon: {app.iconName},
noCollapse: true,
component: ,
});
}
else
{
appList.push({
name: app.label,
key: app.name,
route: path,
icon: {app.iconName},
component: ,
});
}
}
else
{
appList.push({
type: "collapse",
name: app.label,
key: app.name,
dropdown: true,
icon: {app.iconName},
collapse: childList,
});
}
}
function addAppToAppRoutesList(metaData: QInstance, app: QAppTreeNode, routeList: any[], parentPath: string, depth: number)
{
const path = `${parentPath}/${app.name}`;
if (app.type === QAppNodeType.APP)
{
if (app.children)
{
app.children.forEach((child: QAppTreeNode) =>
{
addAppToAppRoutesList(metaData, child, routeList, path, depth + 1);
});
}
routeList.push({
name: `${app.label}`,
key: app.name,
route: path,
component: ,
});
}
else if (app.type === QAppNodeType.TABLE)
{
const table = metaData.tables.get(app.name);
routeList.push({
name: `${app.label}`,
key: app.name,
route: path,
component: ,
});
routeList.push({
name: `${app.label} Create`,
key: `${app.name}.create`,
route: `${path}/create`,
component: ,
});
routeList.push({
name: `${app.label} View`,
key: `${app.name}.view`,
route: `${path}/:id`,
component: ,
});
routeList.push({
name: `${app.label}`,
key: `${app.name}.edit`,
route: `${path}/:id/edit`,
component: ,
});
const processesForTable = QProcessUtils.getProcessesForTable(metaData, table.name, true);
processesForTable.forEach((process) =>
{
routeList.push({
name: process.label,
key: process.name,
route: `${path}/${process.name}`,
component: ,
});
});
}
else if (app.type === QAppNodeType.PROCESS)
{
const process = metaData.processes.get(app.name);
routeList.push({
name: `${app.label}`,
key: app.name,
route: path,
component: ,
});
}
}
try
{
const metaData = await QClient.getInstance().loadMetaData();
if (metaData.branding)
{
setBranding(metaData.branding);
const favicon = document.querySelector("link[rel~='icon']") as HTMLLinkElement;
const appleIcon = document.querySelector("link[rel~='apple-touch-icon']") as HTMLLinkElement;
if (favicon)
{
favicon.href = metaData.branding.icon;
}
if (appleIcon)
{
appleIcon.href = metaData.branding.icon;
}
}
let profileRoutes = {};
const gravatarBase = "https://www.gravatar.com/avatar/";
const hash = Md5.hashStr(user.email);
const profilePicture = `${gravatarBase}${hash}`;
profileRoutes = {
type: "collapse",
name: user.name,
key: user.name,
icon: ,
collapse: [
{
name: "My Profile",
key: "my-profile",
route: "/pages/profile/profile-overview",
component: ,
},
{
name: "Settings",
key: "profile-settings",
route: "/pages/account/settings",
component: ,
},
],
};
setProfileRoutes(profileRoutes);
const sideNavAppList = [] as any[];
const appRoutesList = [] as any[];
for (let i = 0; i < metaData.appTree.length; i++)
{
const app = metaData.appTree[i];
addAppToSideNavList(app, sideNavAppList, "", 0);
addAppToAppRoutesList(metaData, app, appRoutesList, "", 0);
}
const newSideNavRoutes = getStaticRoutes();
// @ts-ignore
newSideNavRoutes.unshift(profileRoutes);
for (let i = 0; i < sideNavAppList.length; i++)
{
newSideNavRoutes.push(sideNavAppList[i]);
}
setSideNavRoutes(newSideNavRoutes);
setAppRoutes(appRoutesList);
}
catch (e)
{
if (e instanceof QException)
{
if ((e as QException).message.indexOf("status code 401") !== -1)
{
removeCookie(SESSION_ID_COOKIE_NAME);
logout();
return;
}
}
}
})();
}, [needToLoadRoutes, isFullyAuthenticated]);
// Open sidenav when mouse enter on mini sidenav
const handleOnMouseEnter = () =>
{
if (miniSidenav && !onMouseEnter)
{
setMiniSidenav(dispatch, false);
setOnMouseEnter(true);
}
};
// Close sidenav when mouse leave mini sidenav
const handleOnMouseLeave = () =>
{
if (onMouseEnter)
{
setMiniSidenav(dispatch, true);
setOnMouseEnter(false);
}
};
// Change the openConfigurator state
const handleConfiguratorOpen = () => setOpenConfigurator(dispatch, !openConfigurator);
// Setting the dir attribute for the body element
useEffect(() =>
{
document.body.setAttribute("dir", direction);
}, [direction]);
// Setting page scroll to 0 when changing the route
useEffect(() =>
{
document.documentElement.scrollTop = 0;
document.scrollingElement.scrollTop = 0;
}, [pathname]);
///////////////////////////////////////////////////////////////////////////////////////////
// convert an object that works for the Sidenav into one that works for the react-router //
///////////////////////////////////////////////////////////////////////////////////////////
const getRoutes = (allRoutes: any[]): any => allRoutes.map(
(route: {
collapse: any;
route: string;
component: ReactElement>;
key: Key;
}) =>
{
if (route.collapse)
{
return getRoutes(route.collapse);
}
if (route.route)
{
return ;
}
return null;
},
);
const configsButton = (
settings
);
return (
appRoutes && (
{layout === "dashboard" && (
<>
>
)}
} />
{appRoutes && getRoutes(appRoutes)}
{getRoutes(getStaticRoutes())}
{profileRoutes && getRoutes([profileRoutes])}
)
);
}