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
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' : ''
}`}
onAsideLgCloseClick={props.onAsideLgClose}

View File

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

View File

@ -29,13 +29,13 @@ export default function AsideMenuLayer({ menu, className = '', ...props }: Props
return (
<aside
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
className={`flex-1 flex flex-col overflow-hidden dark:bg-dark-900 ${asideStyle} ${corners}`}
>
<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">
@ -44,7 +44,7 @@ export default function AsideMenuLayer({ menu, className = '', ...props }: Props
</div>
<button
className="hidden lg:inline-block xl:hidden p-3"
className="hidden lg:inline-block xl:hidden p-2.5"
onClick={handleAsideLgCloseClick}
>
<BaseIcon path={mdiClose} />

View File

@ -37,13 +37,13 @@ export default function NavBar({ menu, className = '', contentClassName = contai
return (
<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 flex-1 items-stretch h-14">{children}</div>
<div className="flex flex-1 items-stretch h-12">{children}</div>
{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}>
<BaseIcon path={isMenuNavBarActive ? mdiClose : mdiDotsVertical} size="24" />
</NavBarItemPlain>
@ -51,7 +51,7 @@ export default function NavBar({ menu, className = '', contentClassName = contai
<div
className={`${
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} />
</div>

View File

@ -901,11 +901,11 @@ export default function WorkspaceShell() {
return (
<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
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
@ -1115,7 +1115,7 @@ export default function WorkspaceShell() {
</button>
<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"
href="/profile"
href="/settings"
>
<BaseIcon path={mdiCogOutline} size={16} />
Settings

View File

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

View File

@ -10,14 +10,12 @@ import {
mdiLogout,
} from '@mdi/js'
import menuAside from '../menuAside'
import menuNavBar from '../menuNavBar'
import BaseIcon from '../components/BaseIcon'
import NavBar from '../components/NavBar'
import NavBarItemPlain from '../components/NavBarItemPlain'
import AsideMenu from '../components/AsideMenu'
import FooterBar from '../components/FooterBar'
import { useAppDispatch, useAppSelector } from '../stores/hooks'
import Search from '../components/Search';
import { useRouter } from 'next/router'
import {findMe, logoutUser} from "../stores/authSlice";
import Link from 'next/link';
@ -44,7 +42,6 @@ export default function LayoutAuthenticated({
const { token, currentUser } = useAppSelector((state) => state.auth)
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
const isWorkspaceRoute = router.pathname.startsWith('/workspace');
const workspaceNavMenu = isWorkspaceRoute ? [] : menuNavBar;
let localToken
if (typeof window !== 'undefined') {
// Perform localStorage action
@ -100,92 +97,90 @@ export default function LayoutAuthenticated({
}, [router.events, dispatch])
const layoutAsidePadding = isWorkspaceRoute ? '' : 'xl:pl-60'
const layoutOffsetClass = isWorkspaceRoute ? '' : isAsideMobileExpanded ? 'ml-60 lg:ml-0' : ''
const layoutAsidePadding = isWorkspaceRoute ? '' : 'xl:pl-72'
const layoutOffsetClass = isWorkspaceRoute ? '' : isAsideMobileExpanded ? 'ml-72 lg:ml-0' : ''
return (
<div className={`${darkMode ? 'dark' : ''} overflow-hidden lg:overflow-visible`}>
<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
menu={workspaceNavMenu}
menu={[]}
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-4 sm:px-6">
<div className="flex min-w-0 flex-1 items-center justify-between px-3 sm:px-5">
<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
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"
>
AI Chat Workspace
</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>
) : (
<>
<NavBarItemPlain
display="flex lg:hidden"
onClick={() => setIsAsideMobileExpanded(!isAsideMobileExpanded)}
<div className="relative">
<button
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"
onClick={() => setIsWorkspaceAccountMenuOpen((previous) => !previous)}
ref={workspaceAccountMenuButtonRef}
type="button"
>
<BaseIcon path={isAsideMobileExpanded ? mdiBackburger : mdiForwardburger} size="24" />
</NavBarItemPlain>
<NavBarItemPlain
display="hidden lg:flex xl:hidden"
onClick={() => setIsAsideLgActive(true)}
>
<BaseIcon path={mdiMenu} size="24" />
</NavBarItemPlain>
<NavBarItemPlain useMargin>
<Search />
</NavBarItemPlain>
</>
)}
<BaseIcon path={mdiAccountCircleOutline} size="18" />
<span className="hidden max-w-[220px] truncate md:inline">
{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="/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>
{!isWorkspaceRoute && (
<AsideMenu

View File

@ -5,7 +5,7 @@ const backofficeMenu: MenuAsideItem[] = [
{
href: '/dashboard',
icon: icon.mdiViewDashboardOutline,
label: 'Dashboard',
label: 'Overview',
},
{
href: '/users/users-list',
@ -15,22 +15,6 @@ const backofficeMenu: MenuAsideItem[] = [
icon: icon.mdiAccountGroup ?? icon.mdiTable,
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',
label: 'Agents',
@ -55,14 +39,6 @@ const backofficeMenu: MenuAsideItem[] = [
icon: 'mdiMessageTextOutline' in icon ? icon['mdiMessageTextOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
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',
label: 'Usage events',
@ -72,11 +48,21 @@ const backofficeMenu: MenuAsideItem[] = [
permissions: 'READ_USAGE_EVENTS'
},
{
href: '/api-docs',
target: '_blank',
label: 'Swagger API',
icon: icon.mdiFileCode,
permissions: 'READ_API_DOCS'
href: '/roles/roles-list',
label: 'Roles',
withDevider: true,
// 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'
},
]
@ -89,7 +75,7 @@ const menuAside: MenuAsideItem[] = [
label: 'Workspace',
},
{
href: '/profile',
href: '/settings',
label: 'Settings',
icon: icon.mdiAccountCircle,
},

View File

@ -1,381 +1,289 @@
import * as icon from '@mdi/js';
import Head from 'next/head'
import React from 'react'
import {
mdiAccountGroup,
mdiArrowTopRight,
mdiChartTimelineVariant,
mdiChatOutline,
mdiCogOutline,
mdiMessageTextOutline,
mdiRobot,
mdiShieldAccountOutline,
mdiShieldAccountVariantOutline,
} from '@mdi/js';
import axios from 'axios';
import type { ReactElement } from 'react'
import LayoutAuthenticated from '../layouts/Authenticated'
import SectionMain from '../components/SectionMain'
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'
import BaseIcon from "../components/BaseIcon";
import { getPageTitle } from '../config'
import Link from "next/link";
import Head from 'next/head';
import Link from 'next/link';
import React from 'react';
import type { ReactElement } from 'react';
import { hasPermission } from "../helpers/userPermissions";
import { fetchWidgets } from '../stores/roles/rolesSlice';
import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator';
import { SmartWidget } from '../components/SmartWidget/SmartWidget';
import BaseIcon from '../components/BaseIcon';
import SectionMain from '../components/SectionMain';
import { getPageTitle } from '../config';
import { hasPermission } from '../helpers/userPermissions';
import LayoutAuthenticated from '../layouts/Authenticated';
import { useAppSelector } from '../stores/hooks';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
const Dashboard = () => {
const dispatch = useAppDispatch();
const iconsColor = useAppSelector((state) => state.style.iconsColor);
const corners = useAppSelector((state) => state.style.corners);
const cardsStyle = useAppSelector((state) => state.style.cardsStyle);
type DashboardCard = {
key: string;
label: string;
description: string;
href: string;
countPath: string;
permission: string;
iconPath: string;
};
const loadingMessage = 'Loading...';
const loadingMessage = 'Loading...';
const [users, setUsers] = React.useState(loadingMessage);
const [roles, setRoles] = React.useState(loadingMessage);
const [permissions, setPermissions] = React.useState(loadingMessage);
const [agents, setAgents] = React.useState(loadingMessage);
const [conversations, setConversations] = React.useState(loadingMessage);
const [messages, setMessages] = React.useState(loadingMessage);
const [attachments, setAttachments] = React.useState(loadingMessage);
const [usage_events, setUsage_events] = React.useState(loadingMessage);
const workspaceCards: DashboardCard[] = [
{
key: 'users',
label: 'Users',
description: 'Workspace members and account access.',
href: '/users/users-list',
countPath: '/users/count',
permission: 'READ_USERS',
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 [widgetsRole, setWidgetsRole] = React.useState({
role: { value: '', label: '' },
const accessCards: DashboardCard[] = [
{
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);
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,];
setCounts(nextCounts);
const requests = entities.map((entity, index) => {
if(hasPermission(currentUser, `READ_${entity.toUpperCase()}`)) {
return axios.get(`/${entity.toLowerCase()}/count`);
} else {
fns[index](null);
return Promise.resolve({data: {count: null}});
}
});
const results = await Promise.allSettled(
visibleCards.map((card) => {
return axios.get(card.countPath);
}),
);
Promise.allSettled(requests).then((results) => {
results.forEach((result, i) => {
if (result.status === 'fulfilled') {
fns[i](result.value.data.count);
} else {
fns[i](result.reason.message);
}
});
});
const resolvedCounts: Record<string, number | string | null> = {};
results.forEach((result, index) => {
const card = visibleCards[index];
if (result.status === 'fulfilled') {
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(() => {
if (!currentUser || !widgetsRole?.role?.value) return;
getWidgets(widgetsRole?.role?.value || '').then();
}, [widgetsRole?.role?.value]);
loadData().catch((error) => {
throw error;
});
}, [currentUser]);
return (
<>
<Head>
<title>
{getPageTitle('Admin')}
</title>
<title>{getPageTitle('Admin')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton
icon={icon.mdiChartTimelineVariant}
title='Admin'
main>
{''}
</SectionTitleLineWithButton>
{hasPermission(currentUser, 'CREATE_ROLES') && <WidgetCreator
currentUser={currentUser}
isFetchingQuery={isFetchingQuery}
setWidgetsRole={setWidgetsRole}
widgetsRole={widgetsRole}
/>}
{!!rolesWidgets.length &&
hasPermission(currentUser, 'CREATE_ROLES') && (
<p className=' text-gray-500 dark:text-gray-400 mb-4'>
{`${widgetsRole?.role?.label || 'Users'}'s widgets`}
</p>
)}
<div className='grid grid-cols-1 gap-6 lg:grid-cols-4 mb-6 grid-flow-dense'>
{(isFetchingQuery || loading) && (
<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
className={`${iconsColor} animate-spin mr-5`}
w='w-16'
h='h-16'
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 className="mb-5 rounded-[10px] border border-slate-200 bg-white px-5 py-5">
<p className="text-[11px] font-medium uppercase tracking-[0.28em] text-slate-400">
Admin
</p>
<h1 className="mt-2 text-[1.65rem] font-semibold tracking-[-0.04em] text-slate-900">
Keep an eye on the workspace and the people using it.
</h1>
<p className="mt-2 max-w-3xl text-sm leading-6 text-slate-500">
This area is for review, support, and control. Product work stays in the workspace.
Admin stays focused on data, access, and assistant configuration.
</p>
<div className="mt-4 flex flex-wrap gap-3">
<Link
className="inline-flex items-center gap-2 rounded-[10px] border border-slate-200 px-4 py-2 text-sm font-medium text-slate-700"
href="/workspace"
>
Open workspace
<BaseIcon path={mdiArrowTopRight} size={16} />
</Link>
<Link
className="inline-flex items-center gap-2 rounded-[10px] border border-slate-200 px-4 py-2 text-sm font-medium text-slate-700"
href="/settings"
>
Settings
<BaseIcon path={mdiCogOutline} size={16} />
</Link>
</div>
</div>
{!!rolesWidgets.length && <hr className='my-6 ' />}
<div id="dashboard" className='grid grid-cols-1 gap-6 lg:grid-cols-3 mb-6'>
{hasPermission(currentUser, 'READ_USERS') && <Link href={'/users/users-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
<div className="mb-7">
<div className="mb-4">
<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.
</p>
</div>
<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>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Users
</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 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>
</Link>}
{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 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>
</Link>}
{hasPermission(currentUser, 'READ_PERMISSIONS') && <Link href={'/permissions/permissions-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">
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 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>
</div>
</Link>}
{hasPermission(currentUser, 'READ_AGENTS') && <Link href={'/agents/agents-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">
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>}
<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>
{!!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>
</>
)
);
}
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';
import Head from 'next/head';
import React, { ReactElement, useEffect, useState } from 'react';
import { ToastContainer, toast } from 'react-toastify';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
import { toast } from 'react-toastify';
import CardBox from '../components/CardBox';
import LayoutAuthenticated from '../layouts/Authenticated';
@ -19,19 +17,15 @@ import FormField from '../components/FormField';
import BaseDivider from '../components/BaseDivider';
import BaseButtons from '../components/BaseButtons';
import BaseButton from '../components/BaseButton';
import FormCheckRadio from '../components/FormCheckRadio';
import FormCheckRadioGroup from '../components/FormCheckRadioGroup';
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 { useRouter } from 'next/router';
import {findMe} from "../stores/authSlice";
const EditUsers = () => {
const { currentUser, isFetching, token } = useAppSelector(
const ProfilePage = () => {
const { currentUser } = useAppSelector(
(state) => state.auth,
);
const router = useRouter();
@ -42,8 +36,6 @@ const EditUsers = () => {
lastName: '',
phoneNumber: '',
email: '',
app_role: '',
disabled: false,
avatar: [],
password: ''
};
@ -64,19 +56,19 @@ const EditUsers = () => {
const handleSubmit = async (data) => {
await dispatch(update({ id: currentUser.id, data }));
await dispatch(findMe());
await router.push('/users/users-list');
await router.push('/settings');
notify('success', 'Profile was updated!');
};
return (
<>
<Head>
<title>{getPageTitle('Edit profile')}</title>
<title>{getPageTitle('Profile')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton
icon={mdiChartTimelineVariant}
title='Edit profile'
title='Profile'
main
>
{''}
@ -124,25 +116,6 @@ const EditUsers = () => {
<Field name='email' placeholder='E-Mail' disabled />
</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
label="Password"
>
@ -162,7 +135,7 @@ const EditUsers = () => {
color='danger'
outline
label='Cancel'
onClick={() => router.push('/users/users-list')}
onClick={() => router.push('/settings')}
/>
</BaseButtons>
</Form>
@ -173,8 +146,8 @@ const EditUsers = () => {
);
};
EditUsers.getLayout = function getLayout(page: ReactElement) {
ProfilePage.getLayout = function getLayout(page: ReactElement) {
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;