This commit is contained in:
Flatlogic Bot 2026-05-14 16:56:21 +00:00
parent ccb52987a0
commit 05382ed1be
11 changed files with 505 additions and 512 deletions

View File

@ -19,7 +19,7 @@ export default function AsideMenu({
<> <>
<AsideMenuLayer <AsideMenuLayer
menu={props.menu} menu={props.menu}
className={`${isAsideMobileExpanded ? 'left-0' : '-left-60 lg:left-0'} ${ className={`${isAsideMobileExpanded ? 'left-0' : '-left-72 lg:left-0'} ${
!isAsideLgActive ? 'lg:hidden xl:flex' : '' !isAsideLgActive ? 'lg:hidden xl:flex' : ''
}`} }`}
onAsideLgCloseClick={props.onAsideLgClose} onAsideLgCloseClick={props.onAsideLgClose}

View File

@ -43,11 +43,11 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
const asideMenuItemInnerContents = ( const asideMenuItemInnerContents = (
<> <>
{item.icon && ( {item.icon && (
<BaseIcon path={item.icon} className={`flex-none mx-3 ${activeClassAddon}`} size="18" /> <BaseIcon path={item.icon} className={`mt-0.5 flex-none ${activeClassAddon}`} size="18" />
)} )}
<span <span
className={`grow text-ellipsis line-clamp-1 ${ className={`min-w-0 grow break-words leading-6 ${
item.menu ? '' : 'pr-12' item.menu ? '' : 'pr-2'
} ${activeClassAddon}`} } ${activeClassAddon}`}
> >
{item.label} {item.label}
@ -56,15 +56,15 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
<BaseIcon <BaseIcon
path={isDropdownActive ? mdiMinus : mdiPlus} path={isDropdownActive ? mdiMinus : mdiPlus}
className={`flex-none ${activeClassAddon}`} className={`flex-none ${activeClassAddon}`}
w="w-12" w="w-8"
/> />
)} )}
</> </>
) )
const componentClass = [ const componentClass = [
'flex cursor-pointer py-1.5 ', 'flex items-start gap-3 cursor-pointer py-1.5',
isDropdownList ? 'px-6 text-sm' : '', isDropdownList ? 'px-4 text-sm' : '',
item.color item.color
? getButtonColor(item.color, false, true) ? getButtonColor(item.color, false, true)
: `${asideMenuItemStyle}`, : `${asideMenuItemStyle}`,

View File

@ -29,13 +29,13 @@ export default function AsideMenuLayer({ menu, className = '', ...props }: Props
return ( return (
<aside <aside
id='asideMenu' id='asideMenu'
className={`${className} zzz lg:py-2 lg:pl-2 w-60 fixed flex z-40 top-0 h-screen transition-position overflow-hidden`} className={`${className} zzz lg:py-2 lg:pl-2 w-72 fixed flex z-40 top-0 h-screen transition-position overflow-hidden`}
> >
<div <div
className={`flex-1 flex flex-col overflow-hidden dark:bg-dark-900 ${asideStyle} ${corners}`} className={`flex-1 flex flex-col overflow-hidden dark:bg-dark-900 ${asideStyle} ${corners}`}
> >
<div <div
className={`flex flex-row h-14 items-center justify-between ${asideBrandStyle}`} className={`flex flex-row h-12 items-center justify-between ${asideBrandStyle}`}
> >
<div className="text-center flex-1 lg:text-left lg:pl-6 xl:text-center xl:pl-0"> <div className="text-center flex-1 lg:text-left lg:pl-6 xl:text-center xl:pl-0">
@ -44,7 +44,7 @@ export default function AsideMenuLayer({ menu, className = '', ...props }: Props
</div> </div>
<button <button
className="hidden lg:inline-block xl:hidden p-3" className="hidden lg:inline-block xl:hidden p-2.5"
onClick={handleAsideLgCloseClick} onClick={handleAsideLgCloseClick}
> >
<BaseIcon path={mdiClose} /> <BaseIcon path={mdiClose} />

View File

@ -37,13 +37,13 @@ export default function NavBar({ menu, className = '', contentClassName = contai
return ( return (
<nav <nav
className={`${className} top-0 inset-x-0 fixed ${bgColor} h-14 z-30 transition-position w-screen lg:w-auto dark:bg-dark-800`} className={`${className} top-0 inset-x-0 fixed ${bgColor} h-12 z-30 transition-position w-screen lg:w-auto dark:bg-dark-800`}
> >
<div className={`flex lg:items-stretch ${contentClassName} ${isScrolled && `border-b border-pavitra-400 dark:border-dark-700`}`}> <div className={`flex lg:items-stretch ${contentClassName} ${isScrolled && `border-b border-pavitra-400 dark:border-dark-700`}`}>
<div className="flex flex-1 items-stretch h-14">{children}</div> <div className="flex flex-1 items-stretch h-12">{children}</div>
{hasMenu && ( {hasMenu && (
<> <>
<div className="flex-none items-stretch flex h-14 lg:hidden"> <div className="flex-none items-stretch flex h-12 lg:hidden">
<NavBarItemPlain onClick={handleMenuNavBarToggleClick}> <NavBarItemPlain onClick={handleMenuNavBarToggleClick}>
<BaseIcon path={isMenuNavBarActive ? mdiClose : mdiDotsVertical} size="24" /> <BaseIcon path={isMenuNavBarActive ? mdiClose : mdiDotsVertical} size="24" />
</NavBarItemPlain> </NavBarItemPlain>
@ -51,7 +51,7 @@ export default function NavBar({ menu, className = '', contentClassName = contai
<div <div
className={`${ className={`${
isMenuNavBarActive ? 'block' : 'hidden' isMenuNavBarActive ? 'block' : 'hidden'
} flex items-center max-h-screen-menu overflow-y-auto lg:overflow-visible absolute w-screen top-14 left-0 ${bgColor} shadow-lg lg:w-auto lg:flex lg:static lg:shadow-none dark:bg-dark-800`} } flex items-center max-h-screen-menu overflow-y-auto lg:overflow-visible absolute w-screen top-12 left-0 ${bgColor} shadow-lg lg:w-auto lg:flex lg:static lg:shadow-none dark:bg-dark-800`}
> >
<NavBarMenuList menu={menu} /> <NavBarMenuList menu={menu} />
</div> </div>

View File

@ -901,11 +901,11 @@ export default function WorkspaceShell() {
return ( return (
<section <section
className={`${activeConversation ? 'h-[calc(100vh-3.5rem)]' : 'min-h-[calc(100vh-3.5rem)]'} bg-[#f5f5f7] px-0 sm:px-1.5 sm:pb-1.5 sm:pt-1.5`} className={`${activeConversation ? 'h-[calc(100vh-3rem)]' : 'min-h-[calc(100vh-3rem)]'} bg-[#f5f5f7] px-0 sm:px-1.5 sm:pb-1.5 sm:pt-1.5`}
> >
<div <div
className={`grid border border-slate-200 bg-white shadow-[0_16px_48px_-40px_rgba(15,23,42,0.18)] sm:rounded-[10px] lg:grid-cols-[220px_minmax(0,1fr)] ${ className={`grid border border-slate-200 bg-white shadow-[0_16px_48px_-40px_rgba(15,23,42,0.18)] sm:rounded-[10px] lg:grid-cols-[220px_minmax(0,1fr)] ${
activeConversation ? 'h-full min-h-0 overflow-hidden' : 'min-h-[calc(100vh-3.5rem)]' activeConversation ? 'h-full min-h-0 overflow-hidden' : 'min-h-[calc(100vh-3rem)]'
}`} }`}
> >
<aside <aside
@ -1115,7 +1115,7 @@ export default function WorkspaceShell() {
</button> </button>
<Link <Link
className="inline-flex items-center gap-1.5 rounded-[8px] border border-slate-200 bg-white px-2.5 py-1.5 text-[12px] font-medium text-slate-700" className="inline-flex items-center gap-1.5 rounded-[8px] border border-slate-200 bg-white px-2.5 py-1.5 text-[12px] font-medium text-slate-700"
href="/profile" href="/settings"
> >
<BaseIcon path={mdiCogOutline} size={16} /> <BaseIcon path={mdiCogOutline} size={16} />
Settings Settings

View File

@ -3,7 +3,7 @@ html {
} }
body { body {
@apply pt-14 xl:pl-60 h-full; @apply pt-12 xl:pl-72 h-full;
} }
#app { #app {

View File

@ -10,14 +10,12 @@ import {
mdiLogout, mdiLogout,
} from '@mdi/js' } from '@mdi/js'
import menuAside from '../menuAside' import menuAside from '../menuAside'
import menuNavBar from '../menuNavBar'
import BaseIcon from '../components/BaseIcon' import BaseIcon from '../components/BaseIcon'
import NavBar from '../components/NavBar' import NavBar from '../components/NavBar'
import NavBarItemPlain from '../components/NavBarItemPlain' import NavBarItemPlain from '../components/NavBarItemPlain'
import AsideMenu from '../components/AsideMenu' import AsideMenu from '../components/AsideMenu'
import FooterBar from '../components/FooterBar' import FooterBar from '../components/FooterBar'
import { useAppDispatch, useAppSelector } from '../stores/hooks' import { useAppDispatch, useAppSelector } from '../stores/hooks'
import Search from '../components/Search';
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import {findMe, logoutUser} from "../stores/authSlice"; import {findMe, logoutUser} from "../stores/authSlice";
import Link from 'next/link'; import Link from 'next/link';
@ -44,7 +42,6 @@ export default function LayoutAuthenticated({
const { token, currentUser } = useAppSelector((state) => state.auth) const { token, currentUser } = useAppSelector((state) => state.auth)
const bgColor = useAppSelector((state) => state.style.bgLayoutColor); const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
const isWorkspaceRoute = router.pathname.startsWith('/workspace'); const isWorkspaceRoute = router.pathname.startsWith('/workspace');
const workspaceNavMenu = isWorkspaceRoute ? [] : menuNavBar;
let localToken let localToken
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
// Perform localStorage action // Perform localStorage action
@ -100,92 +97,90 @@ export default function LayoutAuthenticated({
}, [router.events, dispatch]) }, [router.events, dispatch])
const layoutAsidePadding = isWorkspaceRoute ? '' : 'xl:pl-60' const layoutAsidePadding = isWorkspaceRoute ? '' : 'xl:pl-72'
const layoutOffsetClass = isWorkspaceRoute ? '' : isAsideMobileExpanded ? 'ml-60 lg:ml-0' : '' const layoutOffsetClass = isWorkspaceRoute ? '' : isAsideMobileExpanded ? 'ml-72 lg:ml-0' : ''
return ( return (
<div className={`${darkMode ? 'dark' : ''} overflow-hidden lg:overflow-visible`}> <div className={`${darkMode ? 'dark' : ''} overflow-hidden lg:overflow-visible`}>
<div <div
className={`${layoutAsidePadding} ${layoutOffsetClass} pt-14 min-h-screen w-screen transition-position lg:w-auto ${bgColor} dark:bg-dark-800 dark:text-slate-100`} className={`${layoutAsidePadding} ${layoutOffsetClass} pt-12 min-h-screen w-screen transition-position lg:w-auto ${bgColor} dark:bg-dark-800 dark:text-slate-100`}
> >
<NavBar <NavBar
menu={workspaceNavMenu} menu={[]}
className={`${layoutAsidePadding} ${layoutOffsetClass}`} className={`${layoutAsidePadding} ${layoutOffsetClass}`}
contentClassName={isWorkspaceRoute ? 'w-full' : undefined} contentClassName="w-full"
> >
{isWorkspaceRoute ? ( <div className="flex min-w-0 flex-1 items-center justify-between px-3 sm:px-5">
<div className="flex min-w-0 flex-1 items-center justify-between px-4 sm:px-6"> <div className="flex min-w-0 items-center">
{!isWorkspaceRoute && (
<>
<NavBarItemPlain
display="flex lg:hidden"
onClick={() => setIsAsideMobileExpanded(!isAsideMobileExpanded)}
>
<BaseIcon path={isAsideMobileExpanded ? mdiBackburger : mdiForwardburger} size="24" />
</NavBarItemPlain>
<NavBarItemPlain
display="hidden lg:flex xl:hidden"
onClick={() => setIsAsideLgActive(true)}
>
<BaseIcon path={mdiMenu} size="24" />
</NavBarItemPlain>
</>
)}
<Link <Link
className="truncate text-xs font-semibold uppercase tracking-[0.3em] text-slate-500 dark:text-slate-300" className="truncate text-[11px] font-semibold uppercase tracking-[0.28em] text-slate-500 dark:text-slate-300"
href="/workspace" href="/workspace"
> >
AI Chat Workspace AI Chat Workspace
</Link> </Link>
<div className="relative hidden md:block">
<button
className="inline-flex items-center gap-2 rounded-[8px] border border-slate-200 bg-white px-3 py-1.5 text-[12px] text-slate-500"
onClick={() => setIsWorkspaceAccountMenuOpen((previous) => !previous)}
ref={workspaceAccountMenuButtonRef}
type="button"
>
<BaseIcon path={mdiAccountCircleOutline} size="18" />
<span className="max-w-[220px] truncate">
{currentUser?.email || 'Workspace user'}
</span>
<BaseIcon path={mdiChevronDown} size="16" />
</button>
{isWorkspaceAccountMenuOpen && (
<div className="absolute right-0 top-[calc(100%+0.5rem)] z-40 min-w-[220px]">
<ClickOutside
excludedElements={[workspaceAccountMenuButtonRef]}
onClickOutside={() => setIsWorkspaceAccountMenuOpen(false)}
>
<div className="overflow-hidden rounded-[10px] border border-slate-200 bg-white p-1 shadow-[0_16px_48px_-32px_rgba(15,23,42,0.25)]">
<Link
className="flex items-center gap-2 rounded-[8px] px-3 py-2 text-[13px] text-slate-700"
href="/profile"
onClick={() => setIsWorkspaceAccountMenuOpen(false)}
>
<BaseIcon path={mdiCogOutline} size="16" />
Settings
</Link>
<button
className="flex w-full items-center gap-2 rounded-[8px] px-3 py-2 text-left text-[13px] text-slate-700"
onClick={() => {
setIsWorkspaceAccountMenuOpen(false)
dispatch(logoutUser())
router.push('/login')
}}
type="button"
>
<BaseIcon path={mdiLogout} size="16" />
Log out
</button>
</div>
</ClickOutside>
</div>
)}
</div>
</div> </div>
) : ( <div className="relative">
<> <button
<NavBarItemPlain className="inline-flex items-center gap-1.5 rounded-[8px] border border-slate-200 bg-white px-2.5 py-1 text-[11px] text-slate-500"
display="flex lg:hidden" onClick={() => setIsWorkspaceAccountMenuOpen((previous) => !previous)}
onClick={() => setIsAsideMobileExpanded(!isAsideMobileExpanded)} ref={workspaceAccountMenuButtonRef}
type="button"
> >
<BaseIcon path={isAsideMobileExpanded ? mdiBackburger : mdiForwardburger} size="24" /> <BaseIcon path={mdiAccountCircleOutline} size="18" />
</NavBarItemPlain> <span className="hidden max-w-[220px] truncate md:inline">
<NavBarItemPlain {currentUser?.email || 'Workspace user'}
display="hidden lg:flex xl:hidden" </span>
onClick={() => setIsAsideLgActive(true)} <BaseIcon path={mdiChevronDown} size="16" />
> </button>
<BaseIcon path={mdiMenu} size="24" /> {isWorkspaceAccountMenuOpen && (
</NavBarItemPlain> <div className="absolute right-0 top-[calc(100%+0.5rem)] z-40 min-w-[220px]">
<NavBarItemPlain useMargin> <ClickOutside
<Search /> excludedElements={[workspaceAccountMenuButtonRef]}
</NavBarItemPlain> onClickOutside={() => setIsWorkspaceAccountMenuOpen(false)}
</> >
)} <div className="overflow-hidden rounded-[10px] border border-slate-200 bg-white p-1 shadow-[0_16px_48px_-32px_rgba(15,23,42,0.25)]">
<Link
className="flex items-center gap-2 rounded-[8px] px-3 py-2 text-[13px] text-slate-700"
href="/settings"
onClick={() => setIsWorkspaceAccountMenuOpen(false)}
>
<BaseIcon path={mdiCogOutline} size="16" />
Settings
</Link>
<button
className="flex w-full items-center gap-2 rounded-[8px] px-3 py-2 text-left text-[13px] text-slate-700"
onClick={() => {
setIsWorkspaceAccountMenuOpen(false)
dispatch(logoutUser())
router.push('/login')
}}
type="button"
>
<BaseIcon path={mdiLogout} size="16" />
Log out
</button>
</div>
</ClickOutside>
</div>
)}
</div>
</div>
</NavBar> </NavBar>
{!isWorkspaceRoute && ( {!isWorkspaceRoute && (
<AsideMenu <AsideMenu

View File

@ -5,7 +5,7 @@ const backofficeMenu: MenuAsideItem[] = [
{ {
href: '/dashboard', href: '/dashboard',
icon: icon.mdiViewDashboardOutline, icon: icon.mdiViewDashboardOutline,
label: 'Dashboard', label: 'Overview',
}, },
{ {
href: '/users/users-list', href: '/users/users-list',
@ -15,22 +15,6 @@ const backofficeMenu: MenuAsideItem[] = [
icon: icon.mdiAccountGroup ?? icon.mdiTable, icon: icon.mdiAccountGroup ?? icon.mdiTable,
permissions: 'READ_USERS' permissions: 'READ_USERS'
}, },
{
href: '/roles/roles-list',
label: 'Roles',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon.mdiShieldAccountVariantOutline ?? icon.mdiTable,
permissions: 'READ_ROLES'
},
{
href: '/permissions/permissions-list',
label: 'Permissions',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon.mdiShieldAccountOutline ?? icon.mdiTable,
permissions: 'READ_PERMISSIONS'
},
{ {
href: '/agents/agents-list', href: '/agents/agents-list',
label: 'Agents', label: 'Agents',
@ -55,14 +39,6 @@ const backofficeMenu: MenuAsideItem[] = [
icon: 'mdiMessageTextOutline' in icon ? icon['mdiMessageTextOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, icon: 'mdiMessageTextOutline' in icon ? icon['mdiMessageTextOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_MESSAGES' permissions: 'READ_MESSAGES'
}, },
{
href: '/attachments/attachments-list',
label: 'Attachments',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiPaperclip' in icon ? icon['mdiPaperclip' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_ATTACHMENTS'
},
{ {
href: '/usage_events/usage_events-list', href: '/usage_events/usage_events-list',
label: 'Usage events', label: 'Usage events',
@ -72,11 +48,21 @@ const backofficeMenu: MenuAsideItem[] = [
permissions: 'READ_USAGE_EVENTS' permissions: 'READ_USAGE_EVENTS'
}, },
{ {
href: '/api-docs', href: '/roles/roles-list',
target: '_blank', label: 'Roles',
label: 'Swagger API', withDevider: true,
icon: icon.mdiFileCode, // eslint-disable-next-line @typescript-eslint/ban-ts-comment
permissions: 'READ_API_DOCS' // @ts-ignore
icon: icon.mdiShieldAccountVariantOutline ?? icon.mdiTable,
permissions: 'READ_ROLES'
},
{
href: '/permissions/permissions-list',
label: 'Permissions',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon.mdiShieldAccountOutline ?? icon.mdiTable,
permissions: 'READ_PERMISSIONS'
}, },
] ]
@ -89,7 +75,7 @@ const menuAside: MenuAsideItem[] = [
label: 'Workspace', label: 'Workspace',
}, },
{ {
href: '/profile', href: '/settings',
label: 'Settings', label: 'Settings',
icon: icon.mdiAccountCircle, icon: icon.mdiAccountCircle,
}, },

View File

@ -1,381 +1,289 @@
import * as icon from '@mdi/js'; import {
import Head from 'next/head' mdiAccountGroup,
import React from 'react' mdiArrowTopRight,
mdiChartTimelineVariant,
mdiChatOutline,
mdiCogOutline,
mdiMessageTextOutline,
mdiRobot,
mdiShieldAccountOutline,
mdiShieldAccountVariantOutline,
} from '@mdi/js';
import axios from 'axios'; import axios from 'axios';
import type { ReactElement } from 'react' import Head from 'next/head';
import LayoutAuthenticated from '../layouts/Authenticated' import Link from 'next/link';
import SectionMain from '../components/SectionMain' import React from 'react';
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton' import type { ReactElement } from 'react';
import BaseIcon from "../components/BaseIcon";
import { getPageTitle } from '../config'
import Link from "next/link";
import { hasPermission } from "../helpers/userPermissions"; import BaseIcon from '../components/BaseIcon';
import { fetchWidgets } from '../stores/roles/rolesSlice'; import SectionMain from '../components/SectionMain';
import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator'; import { getPageTitle } from '../config';
import { SmartWidget } from '../components/SmartWidget/SmartWidget'; import { hasPermission } from '../helpers/userPermissions';
import LayoutAuthenticated from '../layouts/Authenticated';
import { useAppSelector } from '../stores/hooks';
import { useAppDispatch, useAppSelector } from '../stores/hooks'; type DashboardCard = {
const Dashboard = () => { key: string;
const dispatch = useAppDispatch(); label: string;
const iconsColor = useAppSelector((state) => state.style.iconsColor); description: string;
const corners = useAppSelector((state) => state.style.corners); href: string;
const cardsStyle = useAppSelector((state) => state.style.cardsStyle); countPath: string;
permission: string;
iconPath: string;
};
const loadingMessage = 'Loading...'; const loadingMessage = 'Loading...';
const workspaceCards: DashboardCard[] = [
const [users, setUsers] = React.useState(loadingMessage); {
const [roles, setRoles] = React.useState(loadingMessage); key: 'users',
const [permissions, setPermissions] = React.useState(loadingMessage); label: 'Users',
const [agents, setAgents] = React.useState(loadingMessage); description: 'Workspace members and account access.',
const [conversations, setConversations] = React.useState(loadingMessage); href: '/users/users-list',
const [messages, setMessages] = React.useState(loadingMessage); countPath: '/users/count',
const [attachments, setAttachments] = React.useState(loadingMessage); permission: 'READ_USERS',
const [usage_events, setUsage_events] = React.useState(loadingMessage); iconPath: mdiAccountGroup,
},
{
key: 'agents',
label: 'Agents',
description: 'Prompt presets and assistant configurations.',
href: '/agents/agents-list',
countPath: '/agents/count',
permission: 'READ_AGENTS',
iconPath: mdiRobot,
},
{
key: 'conversations',
label: 'Conversations',
description: 'Threads created inside the workspace.',
href: '/conversations/conversations-list',
countPath: '/conversations/count',
permission: 'READ_CONVERSATIONS',
iconPath: mdiChatOutline,
},
{
key: 'messages',
label: 'Messages',
description: 'User prompts and assistant responses.',
href: '/messages/messages-list',
countPath: '/messages/count',
permission: 'READ_MESSAGES',
iconPath: mdiMessageTextOutline,
},
{
key: 'usage_events',
label: 'Usage events',
description: 'Tracked model activity and generation usage.',
href: '/usage_events/usage_events-list',
countPath: '/usage_events/count',
permission: 'READ_USAGE_EVENTS',
iconPath: mdiChartTimelineVariant,
},
];
const accessCards: DashboardCard[] = [
const [widgetsRole, setWidgetsRole] = React.useState({ {
role: { value: '', label: '' }, key: 'roles',
label: 'Roles',
description: 'High-level access groups for the app.',
href: '/roles/roles-list',
countPath: '/roles/count',
permission: 'READ_ROLES',
iconPath: mdiShieldAccountVariantOutline,
},
{
key: 'permissions',
label: 'Permissions',
description: 'Low-level capabilities used by the backoffice.',
href: '/permissions/permissions-list',
countPath: '/permissions/count',
permission: 'READ_PERMISSIONS',
iconPath: mdiShieldAccountOutline,
},
];
function Dashboard() {
const { currentUser } = useAppSelector((state) => state.auth);
const [counts, setCounts] = React.useState<Record<string, number | string | null>>({});
const visibleWorkspaceCards = workspaceCards.filter((card) => {
return hasPermission(currentUser, card.permission);
});
const visibleAccessCards = accessCards.filter((card) => {
return hasPermission(currentUser, card.permission);
});
async function loadData() {
const visibleCards = [...visibleWorkspaceCards, ...visibleAccessCards];
const nextCounts: Record<string, number | string | null> = {};
visibleCards.forEach((card) => {
nextCounts[card.key] = loadingMessage;
}); });
const { currentUser } = useAppSelector((state) => state.auth); setCounts(nextCounts);
const { isFetchingQuery } = useAppSelector((state) => state.openAi);
const { rolesWidgets, loading } = useAppSelector((state) => state.roles);
async function loadData() {
const entities = ['users','roles','permissions','agents','conversations','messages','attachments','usage_events',];
const fns = [setUsers,setRoles,setPermissions,setAgents,setConversations,setMessages,setAttachments,setUsage_events,];
const requests = entities.map((entity, index) => { const results = await Promise.allSettled(
visibleCards.map((card) => {
if(hasPermission(currentUser, `READ_${entity.toUpperCase()}`)) { return axios.get(card.countPath);
return axios.get(`/${entity.toLowerCase()}/count`); }),
} else { );
fns[index](null);
return Promise.resolve({data: {count: null}});
}
});
Promise.allSettled(requests).then((results) => { const resolvedCounts: Record<string, number | string | null> = {};
results.forEach((result, i) => { results.forEach((result, index) => {
if (result.status === 'fulfilled') { const card = visibleCards[index];
fns[i](result.value.data.count);
} else { if (result.status === 'fulfilled') {
fns[i](result.reason.message); resolvedCounts[card.key] = result.value.data.count;
} } else {
}); resolvedCounts[card.key] = result.reason.message;
}); }
});
setCounts(resolvedCounts);
}
React.useEffect(() => {
if (!currentUser) {
return;
} }
async function getWidgets(roleId) {
await dispatch(fetchWidgets(roleId));
}
React.useEffect(() => {
if (!currentUser) return;
loadData().then();
setWidgetsRole({ role: { value: currentUser?.app_role?.id, label: currentUser?.app_role?.name } });
}, [currentUser]);
React.useEffect(() => { loadData().catch((error) => {
if (!currentUser || !widgetsRole?.role?.value) return; throw error;
getWidgets(widgetsRole?.role?.value || '').then(); });
}, [widgetsRole?.role?.value]); }, [currentUser]);
return ( return (
<> <>
<Head> <Head>
<title> <title>{getPageTitle('Admin')}</title>
{getPageTitle('Admin')}
</title>
</Head> </Head>
<SectionMain> <SectionMain>
<SectionTitleLineWithButton <div className="mb-5 rounded-[10px] border border-slate-200 bg-white px-5 py-5">
icon={icon.mdiChartTimelineVariant} <p className="text-[11px] font-medium uppercase tracking-[0.28em] text-slate-400">
title='Admin' Admin
main> </p>
{''} <h1 className="mt-2 text-[1.65rem] font-semibold tracking-[-0.04em] text-slate-900">
</SectionTitleLineWithButton> Keep an eye on the workspace and the people using it.
</h1>
{hasPermission(currentUser, 'CREATE_ROLES') && <WidgetCreator <p className="mt-2 max-w-3xl text-sm leading-6 text-slate-500">
currentUser={currentUser} This area is for review, support, and control. Product work stays in the workspace.
isFetchingQuery={isFetchingQuery} Admin stays focused on data, access, and assistant configuration.
setWidgetsRole={setWidgetsRole} </p>
widgetsRole={widgetsRole} <div className="mt-4 flex flex-wrap gap-3">
/>} <Link
{!!rolesWidgets.length && className="inline-flex items-center gap-2 rounded-[10px] border border-slate-200 px-4 py-2 text-sm font-medium text-slate-700"
hasPermission(currentUser, 'CREATE_ROLES') && ( href="/workspace"
<p className=' text-gray-500 dark:text-gray-400 mb-4'> >
{`${widgetsRole?.role?.label || 'Users'}'s widgets`} Open workspace
</p> <BaseIcon path={mdiArrowTopRight} size={16} />
)} </Link>
<Link
<div className='grid grid-cols-1 gap-6 lg:grid-cols-4 mb-6 grid-flow-dense'> className="inline-flex items-center gap-2 rounded-[10px] border border-slate-200 px-4 py-2 text-sm font-medium text-slate-700"
{(isFetchingQuery || loading) && ( href="/settings"
<div className={` ${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 text-lg leading-tight text-gray-500 flex items-center ${cardsStyle} dark:border-dark-700 p-6`}> >
<BaseIcon Settings
className={`${iconsColor} animate-spin mr-5`} <BaseIcon path={mdiCogOutline} size={16} />
w='w-16' </Link>
h='h-16' </div>
size={48}
path={icon.mdiLoading}
/>{' '}
Loading widgets...
</div>
)}
{ rolesWidgets &&
rolesWidgets.map((widget) => (
<SmartWidget
key={widget.id}
userId={currentUser?.id}
widget={widget}
roleId={widgetsRole?.role?.value || ''}
admin={hasPermission(currentUser, 'CREATE_ROLES')}
/>
))}
</div> </div>
{!!rolesWidgets.length && <hr className='my-6 ' />} <div className="mb-7">
<div className="mb-4">
<div id="dashboard" className='grid grid-cols-1 gap-6 lg:grid-cols-3 mb-6'> <h3 className="text-lg font-semibold text-slate-900">Workspace data</h3>
<p className="mt-1 text-sm text-slate-500">
Core entities that power the AI workspace itself.
{hasPermission(currentUser, 'READ_USERS') && <Link href={'/users/users-list'}> </p>
<div </div>
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`} <div className="grid grid-cols-1 gap-4 xl:grid-cols-2 2xl:grid-cols-3">
{visibleWorkspaceCards.map((card) => {
return (
<Link
key={card.key}
className="rounded-[12px] border border-slate-200 bg-white px-5 py-5"
href={card.href}
> >
<div className="flex justify-between align-center"> <div className="flex items-start justify-between gap-4">
<div> <div className="min-w-0">
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400"> <p className="text-sm font-medium text-slate-900">{card.label}</p>
Users <p className="mt-2 text-sm leading-6 text-slate-500">{card.description}</p>
</div>
<div className="text-3xl leading-tight font-semibold">
{users}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={icon.mdiAccountGroup || icon.mdiTable}
/>
</div>
</div> </div>
</div> <div className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-[10px] border border-slate-200 bg-slate-50 text-slate-600">
</Link>} <BaseIcon path={card.iconPath} size={20} />
{hasPermission(currentUser, 'READ_ROLES') && <Link href={'/roles/roles-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Roles
</div>
<div className="text-3xl leading-tight font-semibold">
{roles}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={icon.mdiShieldAccountVariantOutline || icon.mdiTable}
/>
</div>
</div> </div>
</div> </div>
</Link>} <div className="mt-5 flex items-end justify-between gap-3">
<div>
{hasPermission(currentUser, 'READ_PERMISSIONS') && <Link href={'/permissions/permissions-list'}> <p className="text-[11px] font-medium uppercase tracking-[0.24em] text-slate-400">
<div Count
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`} </p>
> <p className="mt-2 text-[2rem] font-semibold tracking-[-0.04em] text-slate-900">
<div className="flex justify-between align-center"> {counts[card.key] ?? loadingMessage}
<div> </p>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Permissions
</div>
<div className="text-3xl leading-tight font-semibold">
{permissions}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={icon.mdiShieldAccountOutline || icon.mdiTable}
/>
</div>
</div> </div>
</div> <span className="inline-flex items-center gap-1 text-sm font-medium text-slate-500">
</Link>} Open
<BaseIcon path={mdiArrowTopRight} size={15} />
{hasPermission(currentUser, 'READ_AGENTS') && <Link href={'/agents/agents-list'}> </span>
<div </div>
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`} </Link>
> );
<div className="flex justify-between align-center"> })}
<div> </div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Agents
</div>
<div className="text-3xl leading-tight font-semibold">
{agents}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiRobot' in icon ? icon['mdiRobot' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_CONVERSATIONS') && <Link href={'/conversations/conversations-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Conversations
</div>
<div className="text-3xl leading-tight font-semibold">
{conversations}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiChatOutline' in icon ? icon['mdiChatOutline' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_MESSAGES') && <Link href={'/messages/messages-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Messages
</div>
<div className="text-3xl leading-tight font-semibold">
{messages}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiMessageTextOutline' in icon ? icon['mdiMessageTextOutline' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_ATTACHMENTS') && <Link href={'/attachments/attachments-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Attachments
</div>
<div className="text-3xl leading-tight font-semibold">
{attachments}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiPaperclip' in icon ? icon['mdiPaperclip' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_USAGE_EVENTS') && <Link href={'/usage_events/usage_events-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Usage events
</div>
<div className="text-3xl leading-tight font-semibold">
{usage_events}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiChartTimelineVariant' in icon ? icon['mdiChartTimelineVariant' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
</div> </div>
{!!visibleAccessCards.length && (
<div>
<div className="mb-4">
<h3 className="text-lg font-semibold text-slate-900">Access control</h3>
<p className="mt-1 text-sm text-slate-500">
Keep roles and permission mapping visible, but secondary to the product itself.
</p>
</div>
<div className="grid grid-cols-1 gap-4 xl:grid-cols-2">
{visibleAccessCards.map((card) => {
return (
<Link
key={card.key}
className="rounded-[12px] border border-slate-200 bg-white px-5 py-5"
href={card.href}
>
<div className="flex items-start justify-between gap-4">
<div className="min-w-0">
<p className="text-sm font-medium text-slate-900">{card.label}</p>
<p className="mt-2 text-sm leading-6 text-slate-500">{card.description}</p>
</div>
<div className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-[10px] border border-slate-200 bg-slate-50 text-slate-600">
<BaseIcon path={card.iconPath} size={20} />
</div>
</div>
<div className="mt-5 flex items-end justify-between gap-3">
<div>
<p className="text-[11px] font-medium uppercase tracking-[0.24em] text-slate-400">
Count
</p>
<p className="mt-2 text-[2rem] font-semibold tracking-[-0.04em] text-slate-900">
{counts[card.key] ?? loadingMessage}
</p>
</div>
<span className="inline-flex items-center gap-1 text-sm font-medium text-slate-500">
Open
<BaseIcon path={mdiArrowTopRight} size={15} />
</span>
</div>
</Link>
);
})}
</div>
</div>
)}
</SectionMain> </SectionMain>
</> </>
) );
} }
Dashboard.getLayout = function getLayout(page: ReactElement) { Dashboard.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated permission={'READ_USERS'}>{page}</LayoutAuthenticated> return <LayoutAuthenticated permission="READ_USERS">{page}</LayoutAuthenticated>;
} };
export default Dashboard export default Dashboard;

View File

@ -4,9 +4,7 @@ import {
} from '@mdi/js'; } from '@mdi/js';
import Head from 'next/head'; import Head from 'next/head';
import React, { ReactElement, useEffect, useState } from 'react'; import React, { ReactElement, useEffect, useState } from 'react';
import { ToastContainer, toast } from 'react-toastify'; import { toast } from 'react-toastify';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
import CardBox from '../components/CardBox'; import CardBox from '../components/CardBox';
import LayoutAuthenticated from '../layouts/Authenticated'; import LayoutAuthenticated from '../layouts/Authenticated';
@ -19,19 +17,15 @@ import FormField from '../components/FormField';
import BaseDivider from '../components/BaseDivider'; import BaseDivider from '../components/BaseDivider';
import BaseButtons from '../components/BaseButtons'; import BaseButtons from '../components/BaseButtons';
import BaseButton from '../components/BaseButton'; import BaseButton from '../components/BaseButton';
import FormCheckRadio from '../components/FormCheckRadio';
import FormCheckRadioGroup from '../components/FormCheckRadioGroup';
import FormImagePicker from '../components/FormImagePicker'; import FormImagePicker from '../components/FormImagePicker';
import { SwitchField } from '../components/SwitchField';
import { SelectField } from '../components/SelectField';
import { update, fetch } from '../stores/users/usersSlice'; import { update } from '../stores/users/usersSlice';
import { useAppDispatch, useAppSelector } from '../stores/hooks'; import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import {findMe} from "../stores/authSlice"; import {findMe} from "../stores/authSlice";
const EditUsers = () => { const ProfilePage = () => {
const { currentUser, isFetching, token } = useAppSelector( const { currentUser } = useAppSelector(
(state) => state.auth, (state) => state.auth,
); );
const router = useRouter(); const router = useRouter();
@ -42,8 +36,6 @@ const EditUsers = () => {
lastName: '', lastName: '',
phoneNumber: '', phoneNumber: '',
email: '', email: '',
app_role: '',
disabled: false,
avatar: [], avatar: [],
password: '' password: ''
}; };
@ -64,19 +56,19 @@ const EditUsers = () => {
const handleSubmit = async (data) => { const handleSubmit = async (data) => {
await dispatch(update({ id: currentUser.id, data })); await dispatch(update({ id: currentUser.id, data }));
await dispatch(findMe()); await dispatch(findMe());
await router.push('/users/users-list'); await router.push('/settings');
notify('success', 'Profile was updated!'); notify('success', 'Profile was updated!');
}; };
return ( return (
<> <>
<Head> <Head>
<title>{getPageTitle('Edit profile')}</title> <title>{getPageTitle('Profile')}</title>
</Head> </Head>
<SectionMain> <SectionMain>
<SectionTitleLineWithButton <SectionTitleLineWithButton
icon={mdiChartTimelineVariant} icon={mdiChartTimelineVariant}
title='Edit profile' title='Profile'
main main
> >
{''} {''}
@ -124,25 +116,6 @@ const EditUsers = () => {
<Field name='email' placeholder='E-Mail' disabled /> <Field name='email' placeholder='E-Mail' disabled />
</FormField> </FormField>
<FormField label='App Role' labelFor='app_role'>
<Field
name='app_role'
id='app_role'
component={SelectField}
options={initialValues.app_role}
itemRef={'roles'}
showField={'name'}
></Field>
</FormField>
<FormField label='Disabled' labelFor='disabled'>
<Field
name='disabled'
id='disabled'
component={SwitchField}
></Field>
</FormField>
<FormField <FormField
label="Password" label="Password"
> >
@ -162,7 +135,7 @@ const EditUsers = () => {
color='danger' color='danger'
outline outline
label='Cancel' label='Cancel'
onClick={() => router.push('/users/users-list')} onClick={() => router.push('/settings')}
/> />
</BaseButtons> </BaseButtons>
</Form> </Form>
@ -173,8 +146,8 @@ const EditUsers = () => {
); );
}; };
EditUsers.getLayout = function getLayout(page: ReactElement) { ProfilePage.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated>{page}</LayoutAuthenticated>; return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
}; };
export default EditUsers; export default ProfilePage;

View File

@ -0,0 +1,131 @@
import {
mdiAccountCircleOutline,
mdiCogOutline,
mdiChevronRight,
mdiFileCodeOutline,
mdiOpenInNew,
} from '@mdi/js';
import Head from 'next/head';
import Link from 'next/link';
import React, { ReactElement } from 'react';
import BaseIcon from '../components/BaseIcon';
import LayoutAuthenticated from '../layouts/Authenticated';
import SectionMain from '../components/SectionMain';
import { getPageTitle } from '../config';
import { hasPermission } from '../helpers/userPermissions';
import { useAppSelector } from '../stores/hooks';
function SettingsPage() {
const { currentUser } = useAppSelector((state) => state.auth);
const canAccessAdmin = hasPermission(currentUser, 'READ_USERS');
const canAccessApiDocs = hasPermission(currentUser, 'READ_API_DOCS');
return (
<>
<Head>
<title>{getPageTitle('Settings')}</title>
</Head>
<SectionMain>
<div className="mx-auto flex w-full max-w-4xl flex-col gap-6">
<div className="rounded-[12px] border border-slate-200 bg-white px-6 py-6">
<p className="text-[11px] font-medium uppercase tracking-[0.28em] text-slate-400">
Settings
</p>
<h1 className="mt-3 text-[2rem] font-semibold tracking-[-0.04em] text-slate-900">
Manage your account and workspace access.
</h1>
<p className="mt-3 max-w-2xl text-sm leading-6 text-slate-500">
Update your personal details, change your password, and move to admin tools when you
need deeper control over the workspace.
</p>
</div>
<div className="grid gap-4 md:grid-cols-2">
<Link
className="group rounded-[12px] border border-slate-200 bg-white px-5 py-5"
href="/profile"
>
<div className="flex items-start justify-between gap-4">
<div>
<div className="mb-4 inline-flex h-10 w-10 items-center justify-center rounded-[10px] border border-slate-200 bg-slate-50 text-slate-600">
<BaseIcon path={mdiAccountCircleOutline} size={20} />
</div>
<h2 className="text-lg font-medium text-slate-900">Profile</h2>
<p className="mt-2 text-sm leading-6 text-slate-500">
Edit your name, avatar, phone number, and password.
</p>
<p className="mt-4 text-[12px] text-slate-400">
{currentUser?.email || 'Workspace user'}
</p>
</div>
<div className="mt-0.5 text-slate-400">
<BaseIcon path={mdiChevronRight} size={18} />
</div>
</div>
</Link>
{canAccessAdmin && (
<Link
className="group rounded-[12px] border border-slate-200 bg-white px-5 py-5"
href="/dashboard"
>
<div className="flex items-start justify-between gap-4">
<div>
<div className="mb-4 inline-flex h-10 w-10 items-center justify-center rounded-[10px] border border-slate-200 bg-slate-50 text-slate-600">
<BaseIcon path={mdiCogOutline} size={20} />
</div>
<h2 className="text-lg font-medium text-slate-900">Admin</h2>
<p className="mt-2 text-sm leading-6 text-slate-500">
Open the workspace backoffice for users, agents, conversations, usage, and access control.
</p>
<p className="mt-4 inline-flex items-center gap-1 text-[12px] text-slate-400">
Workspace backoffice
<BaseIcon path={mdiOpenInNew} size={14} />
</p>
</div>
<div className="mt-0.5 text-slate-400">
<BaseIcon path={mdiChevronRight} size={18} />
</div>
</div>
</Link>
)}
{canAccessApiDocs && (
<Link
className="group rounded-[12px] border border-slate-200 bg-white px-5 py-5"
href="/api-docs"
target="_blank"
>
<div className="flex items-start justify-between gap-4">
<div>
<div className="mb-4 inline-flex h-10 w-10 items-center justify-center rounded-[10px] border border-slate-200 bg-slate-50 text-slate-600">
<BaseIcon path={mdiFileCodeOutline} size={20} />
</div>
<h2 className="text-lg font-medium text-slate-900">API docs</h2>
<p className="mt-2 text-sm leading-6 text-slate-500">
Open Swagger when you need routes, request formats, and backend reference details.
</p>
<p className="mt-4 inline-flex items-center gap-1 text-[12px] text-slate-400">
Developer reference
<BaseIcon path={mdiOpenInNew} size={14} />
</p>
</div>
<div className="mt-0.5 text-slate-400">
<BaseIcon path={mdiChevronRight} size={18} />
</div>
</div>
</Link>
)}
</div>
</div>
</SectionMain>
</>
);
}
SettingsPage.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
};
export default SettingsPage;