Skip to content

Commit

Permalink
OH2-193 | Display dashboard components base on user role/permission (#…
Browse files Browse the repository at this point in the history
…535)

* Align API spec file

* Update TPermission and fix issues related to the update

* Get dashboard config using news APIs. Display widgets based on user permissions

* Ensure new widgets are added to the user dashboard if his permissions change

* Manage empty dashboard customization

* Display empty layout is no stored config found
  • Loading branch information
SilverD3 authored Dec 6, 2023
1 parent 0aa4415 commit b2d86f5
Show file tree
Hide file tree
Showing 15 changed files with 18,639 additions and 9,212 deletions.
26,787 changes: 17,859 additions & 8,928 deletions api/oh.yaml

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion src/components/accessories/dashboard/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { DashboardContent } from "./dashboardContent/DashboardContent";
import "./styles.scss";
import { IStateProps, TProps } from "./types";
import { Chart, registerables } from "chart.js";
import { Permission } from "../../../libraries/permissionUtils/Permission";

Chart.register(...registerables);

Expand All @@ -24,7 +25,9 @@ const Dashboard: FunctionComponent<TProps> = ({ userCredentials }) => {
breadcrumbMap={breadcrumbMap}
/>
<div className="dashboard__background">
<DashboardContent />
<Permission require="dashboard.access">
<DashboardContent />
</Permission>
</div>
<Footer />
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,57 @@
import React, { FunctionComponent } from "react";
import { DashboardFilter } from "./filter/DashboardFilter";
import { GridLayoutToolbox } from "../layouts/toolbox/GridLayoutToolBox";
import GridLayoutToolbox from "../layouts/toolbox/GridLayoutToolBox";
import GridLayoutContainer from "../layouts/container/GridLayoutContainer";
import { setDashboardPeriod } from "../../../../state/dashboard/actions";
import { useDispatch } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import "./styles.scss";
import { IState } from "../../../../types";
import { TAPIResponseStatus } from "../../../../state/types";
import { CircularProgress } from "@material-ui/core";
import { Navigate } from "react-router";
import { PATHS } from "../../../../consts";

export const DashboardContent: FunctionComponent = () => {
const dispatch = useDispatch();
const handlePeriodChange = (value: string[]) => {
dispatch(setDashboardPeriod(value));
};

const authUserStatus = useSelector<IState, TAPIResponseStatus>(
(state) => state.main.authentication.status ?? "IDLE"
);

return (
<div className="dashboard__content">
<div className="dashboard__main">
<div className="dashboard__main-content">
<div className="dashboard__main-header">
<DashboardFilter onPeriodChange={handlePeriodChange} />
</div>
<div className="dashboard__main-body">
<GridLayoutContainer />
<>
{authUserStatus === "SUCCESS" && (
<div className="dashboard__content">
<div className="dashboard__main">
<div className="dashboard__main-content">
<div className="dashboard__main-header">
<DashboardFilter onPeriodChange={handlePeriodChange} />
</div>
<div className="dashboard__main-body">
<GridLayoutContainer />
</div>
</div>
<div className="dashboard__main-side">
<GridLayoutToolbox />
</div>
</div>
</div>
<div className="dashboard__main-side">
<GridLayoutToolbox />
</div>
</div>
</div>
)}

{authUserStatus === "LOADING" && (
<CircularProgress
style={{
marginLeft: "50%",
marginTop: "200px",
position: "relative",
}}
/>
)}

{authUserStatus === "FAIL" && <Navigate to={PATHS.login} />}
</>
);
};
102 changes: 89 additions & 13 deletions src/components/accessories/dashboard/layouts/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ import {
LayoutConfiguration,
TDashboardComponent,
} from "./types";
import { TPermission } from "../../../../types";
import { getAuthenticationFromSession } from "../../../../libraries/authUtils/getAuthenticationFromSession";

/**
* This array contains all dashboard widgets
* If a dashboard widget is added, its name should be added in this array
* and a `switch case` should be added in the switch control of a component
* src\components\accessories\dashboard\layouts\item\GridLayoutItem.tsx
*/
export const DASHBOARDS = [
export const DASHBOARDS: TDashboardComponent[] = [
"opdByAgeType",
"opdBySex",
"admissionBySex",
Expand All @@ -24,6 +26,23 @@ export const DASHBOARDS = [
"dischargeByType",
];

/**
* This array is a map of dashboard widgets and related permissions
*/
export const DASHBOARDS_PERMISSIONS: Record<TDashboardComponent, TPermission> =
{
admissionByAgeType: "admission.read",
admissionBySex: "admission.read",
admissionByType: "admission.read",
admissionByWard: "admission.read",
dischargeByAgeType: "discharges.read",
dischargeBySex: "discharges.read",
dischargeByType: "discharges.read",
dischargeByWard: "discharges.read",
opdByAgeType: "opd.read",
opdBySex: "opd.read",
};

export const defaultLayoutConfig: Layouts = {
lg: generateLayout("lg"),
md: generateLayout("md"),
Expand Down Expand Up @@ -77,6 +96,8 @@ export const getBreakpointFromWidth = (width: number): string => {
* Get dashboard label's translation key
* @param dashboardKey Dashboard key
* @returns Return the translation key
* @todo Create a map like DASHBOARDS_PERMISSIONS use it
* to get dashboards widget labels.
*/
export function getDashboardLabel(dashboardKey: TDashboardComponent): string {
switch (dashboardKey) {
Expand Down Expand Up @@ -144,6 +165,10 @@ export function removeDuplicates(input: Layouts): Layouts {
export function removeDoubles(input1: Layouts, input2: Layouts): Layouts {
let cleanInput: Layouts = {};

if (Object.keys(input2).length === 0) {
return input1;
}

Object.keys(input1).forEach((breakpoint) => {
let breakpointConfig = input1[breakpoint].filter((layout) => {
return !input2[breakpoint].some((layout1) => layout1.i == layout.i);
Expand All @@ -155,13 +180,47 @@ export function removeDoubles(input1: Layouts, input2: Layouts): Layouts {
return cleanInput;
}

/**
* This function uses permissions stored in session storage
* to check if Authenticated user has permission to vie
* @returns Allowed dashboard widgets
*/
export function allowedDashboards(): TDashboardComponent[] {
let allowedDashboards: TDashboardComponent[] = [];

let permissions: TPermission[] = [];
try {
permissions = getAuthenticationFromSession().permissions;
} catch (error) {
//console.log(error);
}

DASHBOARDS.forEach((dash) => {
if (permissions.includes(DASHBOARDS_PERMISSIONS[dash])) {
allowedDashboards.push(dash);
}
});

return allowedDashboards;
}

export function isEmptyLayout(input: Layouts): boolean {
let nbWidgets = 0;

["lg", "md", "sm", "xs", "xxs"].forEach((breakpoint) => {
nbWidgets += input[breakpoint] ? input[breakpoint].length : 0;
});

return nbWidgets == 0;
}

/**
* Generate layout for random Dashboards
* @param nbDashboard Number of dashboard to generate
* @returns Returns the layout config for specified Dashboards number
*/
export function randomLayout(nbDashboard: number): Layouts {
let randomDashboards = randomItems(DASHBOARDS, nbDashboard);
let randomDashboards = randomItems(allowedDashboards(), nbDashboard);

return removeDuplicates({
lg: generateLayout("lg", randomDashboards),
Expand Down Expand Up @@ -196,7 +255,7 @@ export function toolboxDashboards(
});

["lg", "md", "sm", "xs", "xxs"].forEach((breakpoint) => {
unknownDashboard[breakpoint] = DASHBOARDS.filter(
unknownDashboard[breakpoint] = allowedDashboards().filter(
(dashboard) => !knownDashboard[breakpoint].includes(dashboard)
);
});
Expand Down Expand Up @@ -274,7 +333,7 @@ export function generateLayout(
dashboards?: string[]
): Layout[] {
if (!dashboards || dashboards.length == 0) {
dashboards = DASHBOARDS;
dashboards = allowedDashboards();
}

return dashboards.map((dashboardKey, index) => {
Expand Down Expand Up @@ -398,15 +457,32 @@ export const addWidget = (
breakpoint: LayoutBreakpoints
): Layouts => {
let layouts: Layouts = {};
Object.keys(input).forEach((currentBreakpoint) => {
let breakpointConfig = [
...input[currentBreakpoint],
breakpoint === currentBreakpoint
? widget
: generateLayout(currentBreakpoint as LayoutBreakpoints, [widget.i])[0],
];
layouts[currentBreakpoint] = breakpointConfig;
});

if (Object.keys(input).length === 0) {
["lg", "md", "sm", "xs", "xxs"].forEach((currentBreakpoint) => {
let breakpointConfig = [
...(input[currentBreakpoint] ?? []),
breakpoint === currentBreakpoint
? widget
: generateLayout(currentBreakpoint as LayoutBreakpoints, [
widget.i,
])[0],
];
layouts[currentBreakpoint] = breakpointConfig;
});
} else {
Object.keys(input).forEach((currentBreakpoint) => {
let breakpointConfig = [
...input[currentBreakpoint],
breakpoint === currentBreakpoint
? widget
: generateLayout(currentBreakpoint as LayoutBreakpoints, [
widget.i,
])[0],
];
layouts[currentBreakpoint] = breakpointConfig;
});
}

return layouts;
};
Expand Down
Loading

0 comments on commit b2d86f5

Please sign in to comment.