3
This commit is contained in:
parent
ccb52987a0
commit
05382ed1be
@ -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}
|
||||
|
||||
@ -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}`,
|
||||
|
||||
@ -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} />
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -3,7 +3,7 @@ html {
|
||||
}
|
||||
|
||||
body {
|
||||
@apply pt-14 xl:pl-60 h-full;
|
||||
@apply pt-12 xl:pl-72 h-full;
|
||||
}
|
||||
|
||||
#app {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
131
frontend/src/pages/settings.tsx
Normal file
131
frontend/src/pages/settings.tsx
Normal 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;
|
||||
Loading…
x
Reference in New Issue
Block a user