Skip to content

Commit

Permalink
✨ feat(menu): use full menu
Browse files Browse the repository at this point in the history
  • Loading branch information
thrownullexception committed Sep 21, 2023
1 parent 8d56ea5 commit 4be0398
Show file tree
Hide file tree
Showing 35 changed files with 971 additions and 632 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import handler from "pages/api/entities/user-menu";
import handler from "pages/api/menu";
import {
setupAllTestData,
createAuthenticatedMocks,
setupAppConfigTestData,
} from "__tests__/api/_test-utils";

describe("/api/entities/menu", () => {
describe.skip("/api/menu", () => {
beforeAll(async () => {
await setupAllTestData(["schema", "app-config"]);
});
Expand Down
4 changes: 0 additions & 4 deletions src/backend/entities/entities.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@ export class EntitiesApiController {
return await this._entitiesApiService.getActiveEntities();
}

async getUserMenuEntities(userRole: string): Promise<ILabelValue[]> {
return await this._entitiesApiService.getUserMenuEntities(userRole);
}

async listAllEntities(): Promise<ILabelValue[]> {
return await this._entitiesApiService.getAllEntities();
}
Expand Down
25 changes: 0 additions & 25 deletions src/backend/entities/entities.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,31 +41,6 @@ export class EntitiesApiService implements IApplicationService {
return entities.filter(({ value }) => !hiddenEntities.includes(value));
}

async getUserMenuEntities(userRole: string): Promise<ILabelValue[]> {
const [hiddenEntities, hiddenMenuEntities, entitiesOrder, entities] =
await Promise.all([
this._configurationApiService.show<string[]>("disabled_entities"),
this._configurationApiService.show<string[]>("disabled_menu_entities"),
this._configurationApiService.show<string[]>("menu_entities_order"),
this.getAllEntities(),
]);
const activeEntities = entities
.filter(({ value }) => !hiddenMenuEntities.includes(value))
.filter(({ value }) => !hiddenEntities.includes(value));

sortByList(
activeEntities.sort((a, b) => a.value.localeCompare(b.value)),
entitiesOrder,
"value"
);

return await this._rolesApiService.filterPermittedEntities(
userRole,
activeEntities,
"value"
);
}

async getEntityFields(entity: string): Promise<IEntityField[]> {
return (await this.getEntityFromSchema(entity)).fields;
}
Expand Down
16 changes: 16 additions & 0 deletions src/backend/menu/menu.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {
NavigationMenuApiService,
navigationMenuApiService,
} from "./menu.service";

export class MenuApiController {
constructor(private _navigationMenuApiService: NavigationMenuApiService) {}

async getMenuItems(userRole: string) {
return await this._navigationMenuApiService.getMenuItems(userRole);
}
}

export const menuApiController = new MenuApiController(
navigationMenuApiService
);
240 changes: 240 additions & 0 deletions src/backend/menu/menu.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
import { IApplicationService } from "backend/types";
import { nanoid } from "nanoid";
import { canRoleDoThisSync } from "shared/logic/permissions";
import {
INavigationMenuItem,
NavigationMenuItemType,
SystemLinks,
} from "shared/types/menu";
import { userFriendlyCase } from "shared/lib/strings/friendly-case";
import { META_USER_PERMISSIONS, USER_PERMISSIONS } from "shared/constants/user";
import { GranularEntityPermissions } from "shared/types/user";
import {
EntitiesApiService,
entitiesApiService,
} from "backend/entities/entities.service";
import { noop } from "shared/lib/noop";
import {
ConfigurationApiService,
configurationApiService,
} from "backend/configuration/configuration.service";
import { sortByList } from "shared/logic/entities/sort.utils";
import { RolesApiService, rolesApiService } from "backend/roles/roles.service";
import { ILabelValue } from "shared/types/options";
import { portalCheckIfIsMenuAllowed } from "./portal";

const SYSTEM_LINKS_PERMISSION_MAP: Record<SystemLinks, string> = {
[SystemLinks.Settings]: USER_PERMISSIONS.CAN_CONFIGURE_APP,
[SystemLinks.Home]: META_USER_PERMISSIONS.NO_PERMISSION_REQUIRED,
[SystemLinks.Roles]: USER_PERMISSIONS.CAN_MANAGE_PERMISSIONS,
[SystemLinks.Users]: USER_PERMISSIONS.CAN_MANAGE_USERS,
[SystemLinks.Actions]: USER_PERMISSIONS.CAN_MANAGE_INTEGRATIONS,
[SystemLinks.AllDashboards]: META_USER_PERMISSIONS.NO_PERMISSION_REQUIRED,
};

export class NavigationMenuApiService implements IApplicationService {
constructor(
private readonly _entitiesApiService: EntitiesApiService,
private readonly _configurationApiService: ConfigurationApiService,
private readonly _rolesApiService: RolesApiService
) {}

async bootstrap() {
noop();
}

async getMenuItems(userRole: string) {
const navItems = await this.generateMenuItems();

return this.filterOutUserMenuItems(userRole, navItems);
}

async generateMenuItems(): Promise<INavigationMenuItem[]> {
let navItems: INavigationMenuItem[] = [];

navItems = navItems.concat([
{
id: nanoid(),
title: "Home",
icon: "Home",
type: NavigationMenuItemType.System,
link: SystemLinks.Home,
},
// {
// id: nanoid(),
// title: "Dashboards",
// icon: "PieChart",
// type: NavigationMenuItemType.System,
// link: SystemLinks.AllDashboards,
// children: [],
// },
]);

const entitiesToShow = await this.getUserMenuEntities();

navItems = navItems.concat([
{
id: nanoid(),
title: "Entities",
type: NavigationMenuItemType.Header,
children: [],
},
]);

entitiesToShow.forEach((entity) => {
navItems.push({
id: nanoid(),
title: userFriendlyCase(entity.label), // get the current label
icon: "File",
type: NavigationMenuItemType.Entities,
link: entity.value,
});
});

navItems = navItems.concat([
{
id: nanoid(),
title: "Application Menu",
type: NavigationMenuItemType.Header,
},
{
id: nanoid(),
title: "Actions",
icon: "Zap",
type: NavigationMenuItemType.System,
link: SystemLinks.Actions,
},
{
id: nanoid(),
title: "Settings",
icon: "Settings",
type: NavigationMenuItemType.System,
link: SystemLinks.Settings,
children: [],
},
{
id: nanoid(),
title: "Accounts",
icon: "Users",
type: NavigationMenuItemType.System,
link: SystemLinks.Users,
children: [
{
id: nanoid(),
title: "Users",
icon: "Users",
type: NavigationMenuItemType.System,
link: SystemLinks.Users,
children: [],
},
{
id: nanoid(),
title: "Roles",
icon: "Shield",
type: NavigationMenuItemType.System,
link: SystemLinks.Roles,
children: [],
},
],
},
]);

return navItems;
}

private async getUserMenuEntities(): Promise<ILabelValue[]> {
const [hiddenMenuEntities, entitiesOrder, activeEntities] =
await Promise.all([
this._configurationApiService.show<string[]>("disabled_menu_entities"),
this._configurationApiService.show<string[]>("menu_entities_order"),
this._entitiesApiService.getActiveEntities(),
]);

const menuEntities: { label: string; value: string }[] = activeEntities
.filter(({ value }) => !hiddenMenuEntities.includes(value))
.sort((a, b) => a.value.localeCompare(b.value));

sortByList(menuEntities, entitiesOrder, "value");

return menuEntities;
}

async filterOutUserMenuItems(
userRole: string,
navItems: INavigationMenuItem[]
) {
return this.filterMenuItemsBasedOnPermissions(
userRole,
navItems,
await this._rolesApiService.getRolePermissions(userRole)
);
}

private filterMenuItemsBasedOnPermissions(
userRole: string,
menuItems: INavigationMenuItem[],
userPermissions: string[]
): INavigationMenuItem[] {
return menuItems.reduce((allowedMenuItems, menuItem) => {
if (menuItem.children) {
// eslint-disable-next-line no-param-reassign
menuItem.children = this.filterMenuItemsBasedOnPermissions(
userRole,
menuItem.children,
userPermissions
);
}
if (this.isMenuItemAllowed(menuItem, userRole, userPermissions)) {
return [...allowedMenuItems, menuItem];
}
return allowedMenuItems;
}, []);
}

private async isMenuItemAllowed(
menuItem: INavigationMenuItem,
userRole: string,
userPermissions: string[]
): Promise<boolean> {
const isMenuAllowed = await portalCheckIfIsMenuAllowed(
menuItem,
userRole,
userPermissions
);

if (isMenuAllowed) {
return true;
}

switch (menuItem.type) {
case NavigationMenuItemType.Header:
return true;
case NavigationMenuItemType.System:
return canRoleDoThisSync(
userRole,
SYSTEM_LINKS_PERMISSION_MAP[menuItem.link],
false,
userPermissions
);

case NavigationMenuItemType.Entities:
return canRoleDoThisSync(
userRole,
META_USER_PERMISSIONS.APPLIED_CAN_ACCESS_ENTITY(
menuItem.link,
GranularEntityPermissions.Show
),
false,
userPermissions
);
default:
return false;
}
}
}

export const navigationMenuApiService = new NavigationMenuApiService(
entitiesApiService,
configurationApiService,
rolesApiService
);
1 change: 1 addition & 0 deletions src/backend/menu/portal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { portalCheckIfIsMenuAllowed } from "./main";
11 changes: 11 additions & 0 deletions src/backend/menu/portal/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { noop } from "shared/lib/noop";
import { INavigationMenuItem } from "shared/types/menu";

export const portalCheckIfIsMenuAllowed = async (
menuItem: INavigationMenuItem,
userRole: string,
userPermissions: string[]
) => {
noop(menuItem, userRole, userPermissions);
return false;
};
60 changes: 60 additions & 0 deletions src/frontend/_layouts/app/LayoutImpl/NavigationSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/* eslint-disable react/no-array-index-key */
import { BaseSkeleton } from "frontend/design-system/components/Skeleton/Base";
import { Spacer } from "frontend/design-system/primitives/Spacer";
import { Stack } from "frontend/design-system/primitives/Stack";
import { useThemeColorShade } from "frontend/design-system/theme/useTheme";
import React from "react";

export function NavigationSkeleton() {
const getThemeColorShade = useThemeColorShade();

const SCHEMA = [
"header",
"item",
"item",
"header",
"item",
"item",
"item",
"item",
"item",
"header",
"item",
"item",
"item",
];

return (
<Stack
direction="column"
spacing={16}
style={{ padding: 24, marginTop: 48 }}
>
{SCHEMA.map((type, index) => {
if (type === "header") {
return (
<React.Fragment key={index}>
<Spacer size="sm" />
<BaseSkeleton
height="25px"
style={{
background: getThemeColorShade("primary-color", 35),
maxWidth: "120px",
}}
/>
</React.Fragment>
);
}
return (
<BaseSkeleton
key={index}
height="25px"
style={{
background: getThemeColorShade("primary-color", 35),
}}
/>
);
})}
</Stack>
);
}
Loading

0 comments on commit 4be0398

Please sign in to comment.