First pass at permissions; Updated auth0 to work with access token instead of id token

This commit is contained in:
2023-01-11 16:31:39 -06:00
parent eff5a04c0f
commit e096e055a4
16 changed files with 273 additions and 96 deletions

View File

@ -7,7 +7,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.40", "@kingsrook/qqq-frontend-core": "1.0.41",
"@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",

View File

@ -40,6 +40,7 @@ import Sidenav from "qqq/components/horseshoe/sidenav/SideNav";
import theme from "qqq/components/legacy/Theme"; import theme from "qqq/components/legacy/Theme";
import {setMiniSidenav, setOpenConfigurator, 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 ProcessRun from "qqq/pages/processes/ProcessRun"; import ProcessRun from "qqq/pages/processes/ProcessRun";
import ReportRun from "qqq/pages/processes/ReportRun"; import ReportRun from "qqq/pages/processes/ReportRun";
import EntityCreate from "qqq/pages/records/create/RecordCreate"; import EntityCreate from "qqq/pages/records/create/RecordCreate";
@ -53,7 +54,6 @@ import ProcessUtils from "qqq/utils/qqq/ProcessUtils";
const qController = Client.getInstance(); const qController = Client.getInstance();
export const SESSION_ID_COOKIE_NAME = "sessionId"; export const SESSION_ID_COOKIE_NAME = "sessionId";
LicenseInfo.setLicenseKey(process.env.REACT_APP_MATERIAL_UI_LICENSE_KEY);
export default function App() export default function App()
{ {
@ -63,6 +63,9 @@ export default function App()
const [isFullyAuthenticated, setIsFullyAuthenticated] = useState(false); const [isFullyAuthenticated, setIsFullyAuthenticated] = useState(false);
const [profileRoutes, setProfileRoutes] = useState({}); const [profileRoutes, setProfileRoutes] = useState({});
const [branding, setBranding] = useState({} as QBrandingMetaData); const [branding, setBranding] = useState({} as QBrandingMetaData);
const [needLicenseKey, setNeedLicenseKey] = useState(true);
const [defaultRoute, setDefaultRoute] = useState("/no-apps");
useEffect(() => useEffect(() =>
{ {
@ -83,17 +86,21 @@ export default function App()
///////////////////////////////////////// /////////////////////////////////////////
try try
{ {
console.log("Loading token..."); console.log("Loading token from auth0...");
await getAccessTokenSilently(); const accessToken = await getAccessTokenSilently();
const idToken = await getIdTokenClaims(); qController.setAuthorizationHeaderValue("Bearer " + accessToken);
setCookie(SESSION_ID_COOKIE_NAME, idToken.__raw, {path: "/"});
/////////////////////////////////////////////////////////////////////////////////
// we've stopped using session id cook with auth0, so make sure it is not set. //
/////////////////////////////////////////////////////////////////////////////////
removeCookie(SESSION_ID_COOKIE_NAME);
setIsFullyAuthenticated(true); setIsFullyAuthenticated(true);
console.log("Token load complete."); console.log("Token load complete.");
} }
catch (e) catch (e)
{ {
console.log(`Error loading token: ${JSON.stringify(e)}`); console.log(`Error loading token: ${JSON.stringify(e)}`);
removeCookie(SESSION_ID_COOKIE_NAME);
qController.clearAuthenticationMetaDataLocalStorage(); qController.clearAuthenticationMetaDataLocalStorage();
logout(); logout();
return; return;
@ -105,6 +112,7 @@ export default function App()
// use a random token if anonymous or mock // // use a random token if anonymous or mock //
///////////////////////////////////////////// /////////////////////////////////////////////
console.log("Generating random token..."); console.log("Generating random token...");
qController.setAuthorizationHeaderValue(null);
setIsFullyAuthenticated(true); setIsFullyAuthenticated(true);
setCookie(SESSION_ID_COOKIE_NAME, Md5.hashStr(`${new Date()}`), {path: "/"}); setCookie(SESSION_ID_COOKIE_NAME, Md5.hashStr(`${new Date()}`), {path: "/"});
console.log("Token generation complete."); console.log("Token generation complete.");
@ -119,6 +127,16 @@ export default function App()
})(); })();
}, [loadingToken]); }, [loadingToken]);
if(needLicenseKey)
{
(async () =>
{
const metaData: QInstance = await qController.loadMetaData();
LicenseInfo.setLicenseKey(metaData.environmentValues.get("MATERIAL_UI_LICENSE_KEY") || process.env.REACT_APP_MATERIAL_UI_LICENSE_KEY);
setNeedLicenseKey(false);
})();
}
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);
@ -205,6 +223,8 @@ export default function App()
} }
} }
let foundFirstApp = false;
function addAppToAppRoutesList(metaData: QInstance, app: QAppTreeNode, routeList: any[], parentPath: string, depth: number) function addAppToAppRoutesList(metaData: QInstance, app: QAppTreeNode, routeList: any[], parentPath: string, depth: number)
{ {
const path = `${parentPath}/${app.name}`; const path = `${parentPath}/${app.name}`;
@ -224,6 +244,16 @@ export default function App()
route: path, route: path,
component: <AppHome app={metaData.apps.get(app.name)} />, component: <AppHome app={metaData.apps.get(app.name)} />,
}); });
if(!foundFirstApp)
{
/////////////////////////////////////////////////////////////////////////////////////////////////////
// keep track of what the top-most app the user has access to is. set that as their default route //
/////////////////////////////////////////////////////////////////////////////////////////////////////
foundFirstApp = true;
setDefaultRoute(path);
console.log("Set default route to: " + path);
}
} }
else if (app.type === QAppNodeType.TABLE) else if (app.type === QAppNodeType.TABLE)
{ {
@ -363,14 +393,29 @@ export default function App()
const sideNavAppList = [] as any[]; const sideNavAppList = [] as any[];
const appRoutesList = [] as any[]; const appRoutesList = [] as any[];
/////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////
// iterate throught the list to find the 'main dashboard so we can put it first' // // iterate through the list to find the 'main dashboard so we can put it first' //
/////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////
for (let i = 0; i < metaData.appTree.length; i++) if(metaData.appTree && metaData.appTree.length)
{ {
const app = metaData.appTree[i]; for (let i = 0; i < metaData.appTree.length; i++)
addAppToSideNavList(app, sideNavAppList, "", 0); {
addAppToAppRoutesList(metaData, app, appRoutesList, "", 0); const app = metaData.appTree[i];
addAppToSideNavList(app, sideNavAppList, "", 0);
addAppToAppRoutesList(metaData, app, appRoutesList, "", 0);
}
}
else
{
///////////////////////////////////////////////////////////////////
// if the user doesn't have access to any apps, push this route. //
///////////////////////////////////////////////////////////////////
appRoutesList.push({
name: "No Apps",
key: "no-apps",
route: "/no-apps",
component: <NoApps />,
});
} }
const newSideNavRoutes = []; const newSideNavRoutes = [];
@ -390,10 +435,8 @@ export default function App()
console.error(e); console.error(e);
if (e instanceof QException) if (e instanceof QException)
{ {
if ((e as QException).message.indexOf("status code 401") !== -1) if ((e as QException).status === "401")
{ {
removeCookie(SESSION_ID_COOKIE_NAME);
////////////////////////////////////////////////////// //////////////////////////////////////////////////////
// todo - this is auth0 logout... make more generic // // todo - this is auth0 logout... make more generic //
////////////////////////////////////////////////////// //////////////////////////////////////////////////////
@ -486,7 +529,7 @@ export default function App()
onMouseLeave={handleOnMouseLeave} onMouseLeave={handleOnMouseLeave}
/> />
<Routes> <Routes>
<Route path="*" element={<Navigate to="/dashboards/overview" />} /> <Route path="*" element={<Navigate to={defaultRoute} />} />
{appRoutes && getRoutes(appRoutes)} {appRoutes && getRoutes(appRoutes)}
{profileRoutes && getRoutes([profileRoutes])} {profileRoutes && getRoutes([profileRoutes])}
</Routes> </Routes>

View File

@ -81,6 +81,9 @@ authenticationMetaDataPromise.then((authenticationMetaData) =>
// @ts-ignore // @ts-ignore
const clientId = authenticationMetaData.data.clientId; const clientId = authenticationMetaData.data.clientId;
// @ts-ignore
const audience = authenticationMetaData.data.audience;
if(!domain || !clientId) if(!domain || !clientId)
{ {
render( render(
@ -103,7 +106,8 @@ authenticationMetaDataPromise.then((authenticationMetaData) =>
<Auth0ProviderWithRedirectCallback <Auth0ProviderWithRedirectCallback
domain={domain} domain={domain}
clientId={clientId} clientId={clientId}
redirectUri={`${window.location.origin}/dashboards/overview`} audience={audience}
redirectUri={`${window.location.origin}/`}
> >
<MaterialUIControllerProvider> <MaterialUIControllerProvider>
<ProtectedRoute component={App} /> <ProtectedRoute component={App} />

View File

@ -88,7 +88,7 @@ function EntityForm(props: Props): JSX.Element
const [tableSections, setTableSections] = useState(null as QTableSection[]); const [tableSections, setTableSections] = useState(null as QTableSection[]);
const [, forceUpdate] = useReducer((x) => x + 1, 0); const [, forceUpdate] = useReducer((x) => x + 1, 0);
const [noCapabilityError, setNoCapabilityError] = useState(null as string); const [notAllowedError, setNotAllowedError] = useState(null as string);
const {pageHeader, setPageHeader} = useContext(QContext); const {pageHeader, setPageHeader} = useContext(QContext);
@ -189,7 +189,11 @@ function EntityForm(props: Props): JSX.Element
if (!tableMetaData.capabilities.has(Capability.TABLE_UPDATE)) if (!tableMetaData.capabilities.has(Capability.TABLE_UPDATE))
{ {
setNoCapabilityError("You may not edit records in this table"); setNotAllowedError("Records may not be edited in this table");
}
else if (!tableMetaData.editPermission)
{
setNotAllowedError(`You do not have permission to edit ${tableMetaData.label} records`);
} }
} }
else else
@ -206,7 +210,11 @@ function EntityForm(props: Props): JSX.Element
if (!tableMetaData.capabilities.has(Capability.TABLE_INSERT)) if (!tableMetaData.capabilities.has(Capability.TABLE_INSERT))
{ {
setNoCapabilityError("You may not create records in this table"); setNotAllowedError("Records may not be created in this table");
}
else if (!tableMetaData.insertPermission)
{
setNotAllowedError(`You do not have permission to create ${tableMetaData.label} records`);
} }
//////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////
@ -420,14 +428,19 @@ function EntityForm(props: Props): JSX.Element
const formId = props.id != null ? `edit-${tableMetaData?.name}-form` : `create-${tableMetaData?.name}-form`; const formId = props.id != null ? `edit-${tableMetaData?.name}-form` : `create-${tableMetaData?.name}-form`;
let body; let body;
if (noCapabilityError) if (notAllowedError)
{ {
body = ( body = (
<Box mb={3}> <Box mb={3}>
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid item xs={12}> <Grid item xs={12}>
<Box mb={3}> <Box mb={3}>
<Alert severity="error">{noCapabilityError}</Alert> <Alert severity="error">{notAllowedError}</Alert>
{props.isModal &&
<Box mt={5}>
<QCancelButton onClickHandler={props.isModal ? props.closeModalHandler : handleCancelClicked} label="Close" disabled={false} />
</Box>
}
</Box> </Box>
</Grid> </Grid>
</Grid> </Grid>

View File

@ -39,8 +39,8 @@ interface Props
export function GoogleDriveFolderPicker({showDefaultFoldersView, showSharedDrivesView, qInstance}: Props): JSX.Element export function GoogleDriveFolderPicker({showDefaultFoldersView, showSharedDrivesView, qInstance}: Props): JSX.Element
{ {
const clientId = "649816208522-m6oa971vqicrc1hlam7333pt4qck0tm8.apps.googleusercontent.com"; const clientId = qInstance.environmentValues.get("GOOGLE_APP_CLIENT_ID") || process.env.REACT_APP_GOOGLE_APP_CLIENT_ID;
const appApiKey = "AIzaSyBhXK34CF2fUfCgUS1VIHoKZbHxEBuHtDM"; const appApiKey = qInstance.environmentValues.get("GOOGLE_APP_API_KEY") || process.env.REACT_APP_GOOGLE_APP_API_KEY;
if(!clientId) if(!clientId)
{ {
console.error("Missing environmentValue GOOGLE_APP_CLIENT_ID") console.error("Missing environmentValue GOOGLE_APP_CLIENT_ID")

View File

@ -24,6 +24,7 @@ import Card from "@mui/material/Card";
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 {ReactNode} from "react"; import {ReactNode} from "react";
import colors from "qqq/assets/theme/base/colors";
import MDTypography from "qqq/components/legacy/MDTypography"; import MDTypography from "qqq/components/legacy/MDTypography";
interface Props interface Props
@ -37,33 +38,34 @@ interface Props
label: string; label: string;
}; };
icon: ReactNode; icon: ReactNode;
isDisabled?: boolean;
[key: string]: any; [key: string]: any;
} }
function ProcessLinkCard({ function ProcessLinkCard({
color, isReport, title, percentage, icon, color, isReport, title, percentage, icon, isDisabled
}: Props): JSX.Element }: Props): JSX.Element
{ {
return ( return (
<Card> <Card>
<Box display="flex" justifyContent="space-between" pt={3} px={2}> <Box display="flex" justifyContent="space-between" pt={3} px={2} title={isDisabled ? `You do not have permission to access this ${isReport ? "report" : "process"}` : ""}>
<Box <Box
color={color === "light" ? "dark" : "white"} color={color === "light" ? "#000000" : "#FFFFFF"}
borderRadius="xl" borderRadius="xl"
display="flex" display="flex"
justifyContent="center" justifyContent="center"
alignItems="center" alignItems="center"
width="4rem" width="4rem"
height="4rem" height="4rem"
mt={-3} mt={-1.5}
sx={{backgroundColor: color}} sx={{borderRadius: "10px", backgroundColor: isDisabled ? colors.secondary.main : colors.info.main}}
> >
<Icon fontSize="medium" color="inherit"> <Icon fontSize="medium" color="inherit">
{icon} {icon}
</Icon> </Icon>
</Box> </Box>
<Box textAlign="right" lineHeight={1.25}> <Box textAlign="right" lineHeight={1.25} mt={1}>
<MDTypography variant="button" fontWeight="bold" color="text"> <MDTypography variant="button" fontWeight="bold" color="text">
{title} {title}
</MDTypography> </MDTypography>
@ -81,9 +83,15 @@ function ProcessLinkCard({
{percentage.amount} {percentage.amount}
</MDTypography> </MDTypography>
{ {
isReport isDisabled ? (
? `Click here to access the ${title} report.` isReport
: `Click here to run the process called ${title}.` ? `You do not have permission to access the ${title} report.`
: `You do not have permission to run the process called ${title}.`
) : (
isReport
? `Click here to access the ${title} report.`
: `Click here to run the process called ${title}.`
)
} }
{percentage.label} {percentage.label}
</MDTypography> </MDTypography>
@ -100,6 +108,7 @@ ProcessLinkCard.defaultProps = {
text: "", text: "",
label: "", label: "",
}, },
isDisabled: false,
}; };
export default ProcessLinkCard; export default ProcessLinkCard;

View File

@ -61,7 +61,10 @@ function RecordGridWidget({title, data}: Props): JSX.Element
const tableMetaData = new QTableMetaData(data.childTableMetaData); const tableMetaData = new QTableMetaData(data.childTableMetaData);
const {rows, columnsToRender} = DataGridUtils.makeRows(records, tableMetaData); const {rows, columnsToRender} = DataGridUtils.makeRows(records, tableMetaData);
const childTablePath = data.tablePath + (data.tablePath.endsWith("/") ? "" : "/") /////////////////////////////////////////////////////////////////////////////////
// note - tablePath may be null, if the user doesn't have access to the table. //
/////////////////////////////////////////////////////////////////////////////////
const childTablePath = data.tablePath ? data.tablePath + (data.tablePath.endsWith("/") ? "" : "/") : data.tablePath;
const columns = DataGridUtils.setupGridColumns(tableMetaData, columnsToRender, childTablePath); const columns = DataGridUtils.setupGridColumns(tableMetaData, columnsToRender, childTablePath);
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////

View File

@ -46,6 +46,7 @@ interface Props {
component: ReactNode; component: ReactNode;
}; };
direction?: "right" | "left"; direction?: "right" | "left";
isDisabled?: boolean;
[key: string]: any; [key: string]: any;
} }
@ -56,6 +57,7 @@ function MiniStatisticsCard({
percentage, percentage,
icon, icon,
direction, direction,
isDisabled,
}: Props): JSX.Element }: Props): JSX.Element
{ {
const [controller] = useMaterialUIController(); const [controller] = useMaterialUIController();
@ -108,7 +110,7 @@ function MiniStatisticsCard({
justifyContent="center" justifyContent="center"
alignItems="center" alignItems="center"
color="#FFFFFF" color="#FFFFFF"
sx={{borderRadius: "10px", backgroundColor: colors.info.main}} sx={{borderRadius: "10px", backgroundColor: isDisabled ? colors.secondary.main : colors.info.main}}
> >
<Icon fontSize="medium" color="inherit"> <Icon fontSize="medium" color="inherit">
{icon.component} {icon.component}
@ -134,6 +136,7 @@ MiniStatisticsCard.defaultProps = {
text: "", text: "",
}, },
direction: "right", direction: "right",
isDisabled: false,
}; };
export default MiniStatisticsCard; export default MiniStatisticsCard;

View File

@ -125,7 +125,7 @@ function AppHome({app}: Props): JSX.Element
{ {
const tableMetaData = await qController.loadTableMetaData(table.name); const tableMetaData = await qController.loadTableMetaData(table.name);
let countResult = null; let countResult = null;
if(tableMetaData.capabilities.has(Capability.TABLE_COUNT)) if(tableMetaData.capabilities.has(Capability.TABLE_COUNT) && tableMetaData.readPermission)
{ {
countResult = await qController.count(table.name); countResult = await qController.count(table.name);
@ -183,6 +183,20 @@ function AppHome({app}: Props): JSX.Element
}, 1); }, 1);
}; };
const hasTablePermission = (tableName: string) =>
{
return tables.find(t => t.name === tableName && (t.readPermission || t.insertPermission || t.editPermission || t.deletePermission));
};
const hasProcessPermission = (processName: string) =>
{
return processes.find(p => p.name === processName && p.hasPermission);
};
const hasReportPermission = (reportName: string) =>
{
return reports.find(r => r.name === reportName && r.hasPermission);
};
return ( return (
<BaseLayout> <BaseLayout>
@ -210,7 +224,7 @@ function AppHome({app}: Props): JSX.Element
</Box> </Box>
{ {
section.processes ? ( section.processes ? (
<Box p={3} pl={5} pt={0}> <Box p={3} pl={5} pt={0} pb={1}>
<MDTypography variant="h6">Actions</MDTypography> <MDTypography variant="h6">Actions</MDTypography>
</Box> </Box>
) : null ) : null
@ -224,12 +238,19 @@ function AppHome({app}: Props): JSX.Element
let process = app.childMap.get(processName); let process = app.childMap.get(processName);
return ( return (
<Grid key={process.name} item xs={12} md={12} lg={tileSizeLg}> <Grid key={process.name} item xs={12} md={12} lg={tileSizeLg}>
<Link to={process.name}> {hasProcessPermission(processName) ?
<Link to={process.name}>
<ProcessLinkCard
icon={process.iconName || app.iconName}
title={process.label}
/>
</Link> :
<ProcessLinkCard <ProcessLinkCard
icon={process.iconName || app.iconName} icon={process.iconName || app.iconName}
title={process.label} title={process.label}
isDisabled={true}
/> />
</Link> }
</Grid> </Grid>
); );
}) })
@ -244,7 +265,7 @@ function AppHome({app}: Props): JSX.Element
} }
{ {
section.reports ? ( section.reports ? (
<Box p={3} pl={5} pt={0}> <Box p={3} pl={5} pt={0} pb={1}>
<MDTypography variant="h6">Reports</MDTypography> <MDTypography variant="h6">Reports</MDTypography>
</Box> </Box>
) : null ) : null
@ -258,13 +279,21 @@ function AppHome({app}: Props): JSX.Element
let report = app.childMap.get(reportName); let report = app.childMap.get(reportName);
return ( return (
<Grid key={report.name} item xs={12} md={12} lg={tileSizeLg}> <Grid key={report.name} item xs={12} md={12} lg={tileSizeLg}>
<Link to={report.name}> {hasReportPermission(reportName) ?
<Link to={report.name}>
<ProcessLinkCard
icon={report.iconName || app.iconName}
title={report.label}
isReport={true}
/>
</Link> :
<ProcessLinkCard <ProcessLinkCard
icon={report.iconName || app.iconName} icon={report.iconName || app.iconName}
title={report.label} title={report.label}
isReport={true} isReport={true}
isDisabled={true}
/> />
</Link> }
</Grid> </Grid>
); );
}) })
@ -279,7 +308,7 @@ function AppHome({app}: Props): JSX.Element
} }
{ {
section.tables ? ( section.tables ? (
<Box p={3} pl={5} pb={0} pt={0}> <Box p={3} pl={5} pb={1} pt={0}>
<MDTypography variant="h6">Data</MDTypography> <MDTypography variant="h6">Data</MDTypography>
</Box> </Box>
) : null ) : null
@ -293,16 +322,27 @@ function AppHome({app}: Props): JSX.Element
let table = app.childMap.get(tableName); let table = app.childMap.get(tableName);
return ( return (
<Grid key={table.name} item xs={12} md={12} lg={tileSizeLg}> <Grid key={table.name} item xs={12} md={12} lg={tileSizeLg}>
<Link to={table.name}> {hasTablePermission(tableName) ?
<Box mb={3}> <Link to={table.name}>
<Box mb={3}>
<MiniStatisticsCard
title={{fontWeight: "bold", text: table.label}}
count={!tableCounts.has(table.name) || tableCounts.get(table.name).isLoading ? "..." : (tableCountNumbers.get(table.name))}
percentage={{color: "info", text: (!tableCounts.has(table.name) || tableCounts.get(table.name).isLoading ? "" : (tableCountTexts.get(table.name)))}}
icon={{color: "info", component: <Icon>{table.iconName || app.iconName}</Icon>}}
/>
</Box>
</Link> :
<Box mb={3} title="You do not have permission to access this table">
<MiniStatisticsCard <MiniStatisticsCard
title={{fontWeight: "bold", text: table.label}} title={{fontWeight: "bold", text: table.label}}
count={!tableCounts.has(table.name) || tableCounts.get(table.name).isLoading ? "..." : (tableCountNumbers.get(table.name))} count={!tableCounts.has(table.name) || tableCounts.get(table.name).isLoading ? "..." : (tableCountNumbers.get(table.name))}
percentage={{color: "info", text: (!tableCounts.has(table.name) || tableCounts.get(table.name).isLoading ? "" : (tableCountTexts.get(table.name)))}} percentage={{color: "info", text: (!tableCounts.has(table.name) || tableCounts.get(table.name).isLoading ? "" : (tableCountTexts.get(table.name)))}}
icon={{color: "info", component: <Icon>{table.iconName || app.iconName}</Icon>}} icon={{color: "info", component: <Icon>{table.iconName || app.iconName}</Icon>}}
isDisabled={true}
/> />
</Box> </Box>
</Link> }
</Grid> </Grid>
); );
}) })

View File

@ -0,0 +1,23 @@
import {Alert} from "@mui/material";
import BaseLayout from "qqq/layouts/BaseLayout";
interface Props
{
foo: string;
}
NoApps.defaultProps = {
foo: null,
};
function NoApps({foo}: Props): JSX.Element
{
return (
<BaseLayout>
<Alert color="error">You do not have permission to access any apps.</Alert>
</BaseLayout>
);
}
export default NoApps;

View File

@ -71,16 +71,18 @@ interface Props
defaultProcessValues?: any; defaultProcessValues?: any;
isModal?: boolean; isModal?: boolean;
isWidget?: boolean; isWidget?: boolean;
isReport?: boolean;
recordIds?: string | QQueryFilter; recordIds?: string | QQueryFilter;
closeModalHandler?: (event: object, reason: string) => void; closeModalHandler?: (event: object, reason: string) => void;
forceReInit?: number; forceReInit?: number;
overrideLabel?: string
} }
const INITIAL_RETRY_MILLIS = 1_500; const INITIAL_RETRY_MILLIS = 1_500;
const RETRY_MAX_MILLIS = 12_000; const RETRY_MAX_MILLIS = 12_000;
const BACKOFF_AMOUNT = 1.5; const BACKOFF_AMOUNT = 1.5;
function ProcessRun({process, defaultProcessValues, isModal, isWidget, recordIds, closeModalHandler, forceReInit}: Props): JSX.Element function ProcessRun({process, defaultProcessValues, isModal, isWidget, isReport, recordIds, closeModalHandler, forceReInit, overrideLabel}: Props): JSX.Element
{ {
const processNameParam = useParams().processName; const processNameParam = useParams().processName;
const processName = process === null ? processNameParam : process.name; const processName = process === null ? processNameParam : process.name;
@ -236,9 +238,9 @@ function ProcessRun({process, defaultProcessValues, isModal, isWidget, recordIds
Error Error
</MDTypography> </MDTypography>
<MDTypography color="body" variant="button"> <MDTypography color="body" variant="button">
An error occurred while running the process: An error occurred while running the {isReport ? "report" : "process"}:
{" "} {" "}
{process.label} {overrideLabel ?? process.label}
{ {
isUserFacingError ? ( isUserFacingError ? (
<Box mt={1}> <Box mt={1}>
@ -331,7 +333,7 @@ function ProcessRun({process, defaultProcessValues, isModal, isWidget, recordIds
/////////////////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
!isWidget && !isWidget &&
<MDTypography variant={isWidget ? "h6" : "h5"} component="div" fontWeight="bold"> <MDTypography variant={isWidget ? "h6" : "h5"} component="div" fontWeight="bold">
{(isModal) ? `${process.label}: ` : ""} {(isModal) ? `${overrideLabel ?? process.label}: ` : ""}
{step?.label} {step?.label}
</MDTypography> </MDTypography>
} }
@ -616,7 +618,7 @@ function ProcessRun({process, defaultProcessValues, isModal, isWidget, recordIds
if(! isWidget) if(! isWidget)
{ {
setPageHeader(processMetaData.label); setPageHeader(overrideLabel ?? processMetaData.label);
} }
let newIndex = null; let newIndex = null;
@ -935,6 +937,18 @@ function ProcessRun({process, defaultProcessValues, isModal, isWidget, recordIds
} }
}, [needToCheckJobStatus, retryMillis]); }, [needToCheckJobStatus, retryMillis]);
const handlePermissionDenied = (e: any): boolean =>
{
if ((e as QException).status === "403")
{
setProcessError(`You do not have permission to run this ${isReport ? "report" : "process"}.`, true)
return (true);
}
return (false);
}
////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////
// do the initial load of data for the process - that is, meta data, plus the init step // // do the initial load of data for the process - that is, meta data, plus the init step //
// also - allow the component that contains this component to force a re-init, by // // also - allow the component that contains this component to force a re-init, by //
@ -1011,7 +1025,7 @@ function ProcessRun({process, defaultProcessValues, isModal, isWidget, recordIds
} }
catch (e) catch (e)
{ {
setProcessError("Error loading process definition."); handlePermissionDenied(e) || setProcessError("Error loading process definition.");
return; return;
} }
@ -1031,7 +1045,7 @@ function ProcessRun({process, defaultProcessValues, isModal, isWidget, recordIds
} }
catch (e) catch (e)
{ {
setProcessError("Error initializing process."); handlePermissionDenied(e) || setProcessError("Error initializing process.");
} }
})(); })();
} }
@ -1286,9 +1300,11 @@ ProcessRun.defaultProps = {
defaultProcessValues: {}, defaultProcessValues: {},
isModal: false, isModal: false,
isWidget: false, isWidget: false,
isReport: false,
recordIds: null, recordIds: null,
closeModalHandler: null, closeModalHandler: null,
forceReInit: 0 forceReInit: 0,
overrideLabel: null,
}; };
export default ProcessRun; export default ProcessRun;

View File

@ -53,7 +53,7 @@ function ReportRun({report}: Props): JSX.Element
setPageHeader(report.label); setPageHeader(report.label);
const process = metaData.processes.get(report.processName); const process = metaData.processes.get(report.processName);
const defaultProcessValues = {reportName: report.name}; const defaultProcessValues = {reportName: report.name};
return (<ProcessRun process={process} defaultProcessValues={defaultProcessValues} />); return (<ProcessRun process={process} overrideLabel={report.label} isReport={true} defaultProcessValues={defaultProcessValues} />);
} }
else else
{ {

View File

@ -1044,27 +1044,27 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
keepMounted keepMounted
> >
{ {
table.capabilities.has(Capability.TABLE_INSERT) && table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission &&
<MenuItem onClick={bulkLoadClicked}> <MenuItem onClick={bulkLoadClicked}>
<ListItemIcon><Icon>library_add</Icon></ListItemIcon> <ListItemIcon><Icon>library_add</Icon></ListItemIcon>
Bulk Load Bulk Load
</MenuItem> </MenuItem>
} }
{ {
table.capabilities.has(Capability.TABLE_UPDATE) && table.capabilities.has(Capability.TABLE_UPDATE) && table.editPermission &&
<MenuItem onClick={bulkEditClicked}> <MenuItem onClick={bulkEditClicked}>
<ListItemIcon><Icon>edit</Icon></ListItemIcon> <ListItemIcon><Icon>edit</Icon></ListItemIcon>
Bulk Edit Bulk Edit
</MenuItem> </MenuItem>
} }
{ {
table.capabilities.has(Capability.TABLE_DELETE) && table.capabilities.has(Capability.TABLE_DELETE) && table.deletePermission &&
<MenuItem onClick={bulkDeleteClicked}> <MenuItem onClick={bulkDeleteClicked}>
<ListItemIcon><Icon>delete</Icon></ListItemIcon> <ListItemIcon><Icon>delete</Icon></ListItemIcon>
Bulk Delete Bulk Delete
</MenuItem> </MenuItem>
} }
{(table.capabilities.has(Capability.TABLE_INSERT) || table.capabilities.has(Capability.TABLE_UPDATE) || table.capabilities.has(Capability.TABLE_DELETE)) && tableProcesses.length > 0 && <Divider />} {((table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission) || (table.capabilities.has(Capability.TABLE_UPDATE) && table.editPermission) || (table.capabilities.has(Capability.TABLE_DELETE) && table.deletePermission)) && tableProcesses.length > 0 && <Divider />}
{tableProcesses.map((process) => ( {tableProcesses.map((process) => (
<MenuItem key={process.name} onClick={() => processClicked(process)}> <MenuItem key={process.name} onClick={() => processClicked(process)}>
<ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon> <ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon>
@ -1072,7 +1072,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
</MenuItem> </MenuItem>
))} ))}
{ {
tableProcesses.length == 0 && !table.capabilities.has(Capability.TABLE_INSERT) && !table.capabilities.has(Capability.TABLE_UPDATE) && !table.capabilities.has(Capability.TABLE_DELETE) && tableProcesses.length == 0 && !(table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission) && !(table.capabilities.has(Capability.TABLE_UPDATE) && table.editPermission) && !(table.capabilities.has(Capability.TABLE_DELETE) && table.deletePermission) &&
<MenuItem disabled> <MenuItem disabled>
<ListItemIcon><Icon>block</Icon></ListItemIcon> <ListItemIcon><Icon>block</Icon></ListItemIcon>
<i>No actions available</i> <i>No actions available</i>
@ -1112,6 +1112,17 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
document.scrollingElement.scrollTop = 0; document.scrollingElement.scrollTop = 0;
}, [ pageNumber, rowsPerPage ]); }, [ pageNumber, rowsPerPage ]);
if(tableMetaData && !tableMetaData.readPermission)
{
return (
<DashboardLayout>
<NavBar />
<Alert severity="error">
You do not have permission to view {tableMetaData?.label} records
</Alert>
</DashboardLayout>
);
}
return ( return (
<DashboardLayout> <DashboardLayout>
@ -1148,7 +1159,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
</Box> </Box>
{ {
table.capabilities.has(Capability.TABLE_INSERT) && table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission &&
<QCreateNewButton /> <QCreateNewButton />
} }

View File

@ -95,6 +95,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
const [allTableProcesses, setAllTableProcesses] = useState([] as QProcessMetaData[]); const [allTableProcesses, setAllTableProcesses] = useState([] as QProcessMetaData[]);
const [actionsMenu, setActionsMenu] = useState(null); const [actionsMenu, setActionsMenu] = useState(null);
const [notFoundMessage, setNotFoundMessage] = useState(null); const [notFoundMessage, setNotFoundMessage] = useState(null);
const [successMessage, setSuccessMessage] = useState(null as string);
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const {setPageHeader} = useContext(QContext); const {setPageHeader} = useContext(QContext);
const [activeModalProcess, setActiveModalProcess] = useState(null as QProcessMetaData); const [activeModalProcess, setActiveModalProcess] = useState(null as QProcessMetaData);
@ -108,6 +109,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
const reload = () => const reload = () =>
{ {
setSuccessMessage(null);
setNotFoundMessage(null); setNotFoundMessage(null);
setAsyncLoadInited(false); setAsyncLoadInited(false);
setTableMetaData(null); setTableMetaData(null);
@ -267,21 +269,30 @@ function RecordView({table, launchProcess}: Props): JSX.Element
} }
catch (e) catch (e)
{ {
const historyPurge = (path: string) =>
{
try
{
HistoryUtils.ensurePathNotInHistory(location.pathname);
}
catch(e)
{
console.error("Error pushing history: " + e);
}
}
if (e instanceof QException) if (e instanceof QException)
{ {
if ((e as QException).status === "404") if ((e as QException).status === "404")
{ {
setNotFoundMessage(`${tableMetaData.label} ${id} could not be found.`); setNotFoundMessage(`${tableMetaData.label} ${id} could not be found.`);
historyPurge(location.pathname)
try return;
{ }
HistoryUtils.ensurePathNotInHistory(location.pathname); else if ((e as QException).status === "403")
} {
catch(e) setNotFoundMessage(`You do not have permission to view ${tableMetaData.label} records`);
{ historyPurge(location.pathname)
console.error("Error pushing history: " + e);
}
return; return;
} }
} }
@ -298,7 +309,6 @@ function RecordView({table, launchProcess}: Props): JSX.Element
console.error("Error pushing history: " + e); console.error("Error pushing history: " + e);
} }
///////////////////////////////////////////////// /////////////////////////////////////////////////
// define the sections, e.g., for the left-bar // // define the sections, e.g., for the left-bar //
///////////////////////////////////////////////// /////////////////////////////////////////////////
@ -344,10 +354,10 @@ function RecordView({table, launchProcess}: Props): JSX.Element
{ {
section.fieldNames.map((fieldName: string) => ( section.fieldNames.map((fieldName: string) => (
<Box key={fieldName} flexDirection="row" pr={2}> <Box key={fieldName} flexDirection="row" pr={2}>
<Typography variant="button" textTransform="none" fontWeight="bold" pr={1}> <Typography variant="button" textTransform="none" fontWeight="bold" pr={1} color="rgb(52, 71, 103)">
{tableMetaData.fields.get(fieldName).label}: {tableMetaData.fields.get(fieldName).label}:
</Typography> </Typography>
<Typography variant="button" textTransform="none" fontWeight="regular" color="text"> <Typography variant="button" textTransform="none" fontWeight="regular" color="rgb(123, 128, 154)">
{ValueUtils.getDisplayValue(tableMetaData.fields.get(fieldName), record, "view")} {ValueUtils.getDisplayValue(tableMetaData.fields.get(fieldName), record, "view")}
</Typography> </Typography>
</Box> </Box>
@ -396,6 +406,11 @@ function RecordView({table, launchProcess}: Props): JSX.Element
setSectionFieldElements(sectionFieldElements); setSectionFieldElements(sectionFieldElements);
setNonT1TableSections(nonT1TableSections); setNonT1TableSections(nonT1TableSections);
if (searchParams.get("createSuccess") || searchParams.get("updateSuccess"))
{
setSuccessMessage(`${tableMetaData.label} successfully ${searchParams.get("createSuccess") ? "created" : "updated"}`);
}
forceUpdate(); forceUpdate();
})(); })();
} }
@ -445,14 +460,14 @@ function RecordView({table, launchProcess}: Props): JSX.Element
keepMounted keepMounted
> >
{ {
table.capabilities.has(Capability.TABLE_UPDATE) && table.capabilities.has(Capability.TABLE_UPDATE) && table.editPermission &&
<MenuItem onClick={() => navigate("edit")}> <MenuItem onClick={() => navigate("edit")}>
<ListItemIcon><Icon>edit</Icon></ListItemIcon> <ListItemIcon><Icon>edit</Icon></ListItemIcon>
Edit Edit
</MenuItem> </MenuItem>
} }
{ {
table.capabilities.has(Capability.TABLE_DELETE) && table.capabilities.has(Capability.TABLE_DELETE) && table.deletePermission &&
<MenuItem onClick={() => <MenuItem onClick={() =>
{ {
setActionsMenu(null); setActionsMenu(null);
@ -463,7 +478,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
Delete Delete
</MenuItem> </MenuItem>
} }
{tableProcesses.length > 0 && (table.capabilities.has(Capability.TABLE_UPDATE) || table.capabilities.has(Capability.TABLE_DELETE)) && <Divider />} {tableProcesses.length > 0 && ((table.capabilities.has(Capability.TABLE_UPDATE) && table.editPermission) || (table.capabilities.has(Capability.TABLE_DELETE) && table.deletePermission)) && <Divider />}
{tableProcesses.map((process) => ( {tableProcesses.map((process) => (
<MenuItem key={process.name} onClick={() => processClicked(process)}> <MenuItem key={process.name} onClick={() => processClicked(process)}>
<ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon> <ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon>
@ -552,21 +567,18 @@ function RecordView({table, launchProcess}: Props): JSX.Element
{ {
notFoundMessage notFoundMessage
? ?
<Box>{notFoundMessage}</Box> <Alert color="error" sx={{mb: 3}}>{notFoundMessage}</Alert>
: :
<Box pb={3}> <Box pb={3}>
{ {
(searchParams.get("createSuccess") || searchParams.get("updateSuccess")) ? ( successMessage ?
<Alert color="success" onClose={() => <Alert color="success" sx={{mb: 3}} onClose={() =>
{}}> {
{tableMetaData?.label} setSuccessMessage(null)
{" "} }}>
successfully {successMessage}
{" "}
{searchParams.get("createSuccess") ? "created" : "updated"}
</Alert> </Alert>
) : ("") : ("")
} }
<Grid container spacing={3}> <Grid container spacing={3}>
@ -588,7 +600,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
</Box> </Box>
<Box display="flex" justifyContent="space-between" width="100%" alignItems="center"> <Box display="flex" justifyContent="space-between" width="100%" alignItems="center">
<Typography variant="h5"> <Typography variant="h5">
{tableMetaData && record ? `Viewing ${tableMetaData?.label}: ${record?.recordLabel}` : ""} {tableMetaData && record ? `Viewing ${tableMetaData?.label}: ${record?.recordLabel || ""}` : ""}
</Typography> </Typography>
<QActionsMenuButton isOpen={actionsMenu} onClickHandler={openActionsMenu} /> <QActionsMenuButton isOpen={actionsMenu} onClickHandler={openActionsMenu} />
{renderActionsMenu} {renderActionsMenu}
@ -610,10 +622,10 @@ function RecordView({table, launchProcess}: Props): JSX.Element
<Box component="form" p={3}> <Box component="form" p={3}>
<Grid container justifyContent="flex-end" spacing={3}> <Grid container justifyContent="flex-end" spacing={3}>
{ {
table.capabilities.has(Capability.TABLE_DELETE) && <QDeleteButton onClickHandler={handleClickDeleteButton} /> table.capabilities.has(Capability.TABLE_DELETE) && table.deletePermission && <QDeleteButton onClickHandler={handleClickDeleteButton} />
} }
{ {
table.capabilities.has(Capability.TABLE_UPDATE) && <QEditButton /> table.capabilities.has(Capability.TABLE_UPDATE) && table.editPermission && <QEditButton />
} }
</Grid> </Grid>
</Box> </Box>

View File

@ -182,7 +182,7 @@ export default class DataGridUtils
); );
} }
if (key === tableMetaData.primaryKeyField) if (key === tableMetaData.primaryKeyField && linkBase)
{ {
columns.splice(0, 0, column); columns.splice(0, 0, column);
column.renderCell = (cellValues: any) => ( column.renderCell = (cellValues: any) => (

View File

@ -95,8 +95,8 @@ class ValueUtils
let tablePath = ValueUtils.getQInstance().getTablePathByName(toRecordFromTable); let tablePath = ValueUtils.getQInstance().getTablePathByName(toRecordFromTable);
if (!tablePath) if (!tablePath)
{ {
console.log("Couldn't find path for table: " + tablePath); console.log("Couldn't find path for table: " + toRecordFromTable);
return (""); return (displayValue ?? rawValue);
} }
if (!tablePath.endsWith("/")) if (!tablePath.endsWith("/"))