This commit is contained in:
Flatlogic Bot 2026-05-15 09:08:49 +00:00
parent 2ec27ee5f1
commit eb455b41dd
37 changed files with 698 additions and 253 deletions

View File

@ -90,86 +90,6 @@ await queryInterface.bulkInsert("rolesPermissionsPermissions", [
{ createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('CREATE_USERS') },
{ createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('READ_USERS') },
{ createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('UPDATE_USERS') },
{ createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('DELETE_USERS') },
{ createdAt, updatedAt, roles_permissionsId: getId("SupportLead"), permissionId: getId('READ_USERS') },
{ createdAt, updatedAt, roles_permissionsId: getId("SupportLead"), permissionId: getId('UPDATE_USERS') },
{ createdAt, updatedAt, roles_permissionsId: getId("AgentCurator"), permissionId: getId('READ_USERS') },
{ createdAt, updatedAt, roles_permissionsId: getId("Analyst"), permissionId: getId('READ_USERS') },
{ createdAt, updatedAt, roles_permissionsId: getId("Member"), permissionId: getId('READ_USERS') },
{ createdAt, updatedAt, roles_permissionsId: getId("Member"), permissionId: getId('UPDATE_USERS') },
@ -749,4 +669,3 @@ await queryInterface.bulkInsert("rolesPermissionsPermissions", [
}
};

View File

@ -2,6 +2,13 @@
const ValidationError = require('../services/notifications/errors/validation');
const RolesDBApi = require('../db/api/roles');
const USER_PERMISSIONS = new Set([
'CREATE_USERS',
'READ_USERS',
'UPDATE_USERS',
'DELETE_USERS',
]);
// Cache for the 'Public' role object
let publicRoleCache = null;
@ -47,7 +54,16 @@ function checkPermissions(permission) {
return next(); // User has access to their own resource
}
// 2. Check Custom Permissions (only if the user is authenticated)
// 2. Only administrators can use global users permissions.
if (USER_PERMISSIONS.has(permission)) {
const roleName = currentUser?.app_role?.name;
if (roleName !== 'Administrator') {
return next(new ValidationError('auth.forbidden', `Role '${roleName || 'anonymous'}' denied access to '${permission}'.`));
}
}
// 3. Check Custom Permissions (only if the user is authenticated)
if (currentUser) {
// Ensure custom_permissions is an array before using find
const customPermissions = Array.isArray(currentUser.custom_permissions)
@ -61,7 +77,7 @@ function checkPermissions(permission) {
}
}
// 3. Determine the "effective" role for permission check
// 4. Determine the "effective" role for permission check
let effectiveRole = null;
try {
if (currentUser && currentUser.app_role) {
@ -90,7 +106,7 @@ function checkPermissions(permission) {
return next(new Error("Internal Server Error: Could not determine effective role."));
}
// 4. Check Permissions on the "effective" role
// 5. Check Permissions on the "effective" role
// Assume the effectiveRole object (from app_role or RolesDBApi) has a getPermissions() method
// or a 'permissions' property (if permissions are eagerly loaded).
let rolePermissions = [];
@ -146,4 +162,3 @@ module.exports = {
checkPermissions,
checkCrudPermissions,
};

View File

@ -16,6 +16,7 @@ import {loadColumns} from "./configureAgentsCols";
import _ from 'lodash';
import dataFormatter from '../../helpers/dataFormatter'
import {dataGridStyles} from "../../styles";
import DataGridLoadingOverlay from '../DataGridLoadingOverlay';
@ -45,6 +46,7 @@ const TableSampleAgents = ({ filterItems, setFilterItems, filters, showGrid }) =
const focusRing = useAppSelector((state) => state.style.focusRingColor);
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
const corners = useAppSelector((state) => state.style.corners);
const isDataGridLoading = !currentUser || loading || columns.length === 0;
const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage);
for (let i = 0; i < numPages; i++) {
pagesList.push(i);
@ -211,7 +213,8 @@ const TableSampleAgents = ({ filterItems, setFilterItems, filters, showGrid }) =
<div className='relative overflow-x-auto'>
<DataGrid
autoHeight
rowHeight={64}
rowHeight={56}
columnHeaderHeight={48}
sx={dataGridStyles}
className={'datagrid--table'}
getRowClassName={() => `datagrid--row`}
@ -251,7 +254,10 @@ const TableSampleAgents = ({ filterItems, setFilterItems, filters, showGrid }) =
rowCount={count}
pageSizeOptions={[10]}
paginationMode={'server'}
loading={loading}
loading={isDataGridLoading}
slots={{
loadingOverlay: DataGridLoadingOverlay,
}}
onPaginationModelChange={(params) => {
onPageChange(params.page);
}}

View File

@ -12,6 +12,7 @@ import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter'
import DataGridMultiSelect from "../DataGridMultiSelect";
import ListActionsPopover from '../ListActionsPopover';
import { makeReadableColumns } from '../../helpers/dataGridColumns';
import {hasPermission} from "../../helpers/userPermissions";
@ -39,7 +40,7 @@ export const loadColumns = async (
const hasUpdatePermission = hasPermission(user, 'UPDATE_AGENTS')
return [
const columns = [
{
field: 'name',
@ -204,4 +205,6 @@ export const loadColumns = async (
},
},
];
return makeReadableColumns(columns);
};

View File

@ -1,30 +1,18 @@
import React from 'react'
import { MenuAsideItem } from '../interfaces'
import AsideMenuLayer from './AsideMenuLayer'
import OverlayLayer from './OverlayLayer'
type Props = {
menu: MenuAsideItem[]
isAsideMobileExpanded: boolean
isAsideLgActive: boolean
onAsideLgClose: () => void
}
export default function AsideMenu({
isAsideMobileExpanded = false,
isAsideLgActive = false,
...props
}: Props) {
return (
<>
<AsideMenuLayer
menu={props.menu}
className={`${isAsideMobileExpanded ? 'left-0' : '-left-64 lg:left-0'} ${
!isAsideLgActive ? 'lg:hidden xl:flex' : ''
}`}
onAsideLgCloseClick={props.onAsideLgClose}
/>
{isAsideLgActive && <OverlayLayer zIndex="z-30" onClick={props.onAsideLgClose} />}
</>
<AsideMenuLayer
menu={props.menu}
className="left-0"
/>
)
}

View File

@ -1,6 +1,4 @@
import React from 'react'
import { mdiClose } from '@mdi/js'
import BaseIcon from './BaseIcon'
import AsideMenuList from './AsideMenuList'
import { MenuAsideItem } from '../interfaces'
import { useAppSelector } from '../stores/hooks'
@ -9,49 +7,36 @@ import { useAppSelector } from '../stores/hooks'
type Props = {
menu: MenuAsideItem[]
className?: string
onAsideLgCloseClick: () => void
}
export default function AsideMenuLayer({ menu, className = '', ...props }: Props) {
export default function AsideMenuLayer({ menu, className = '' }: Props) {
const asideScrollbarsStyle = useAppSelector((state) => state.style.asideScrollbarsStyle)
const darkMode = useAppSelector((state) => state.style.darkMode)
const handleAsideLgCloseClick = (e: React.MouseEvent) => {
e.preventDefault()
props.onAsideLgCloseClick()
}
return (
<aside
id='asideMenu'
className={`${className} fixed top-12 z-40 flex h-[calc(100vh-3rem)] w-64 overflow-hidden px-2 pb-2 transition-position`}
className={`${className} fixed inset-y-0 z-40 flex h-screen w-64 overflow-hidden border-r border-slate-200 bg-white transition-position shadow-[0_24px_60px_-42px_rgba(15,23,42,0.28)] lg:shadow-none dark:border-dark-700 dark:bg-dark-900`}
>
<div
className="flex flex-1 flex-col overflow-hidden rounded-[10px] border border-slate-200 bg-white shadow-none dark:border-dark-700 dark:bg-dark-900"
className="flex flex-1 flex-col overflow-hidden bg-white dark:bg-dark-900"
>
<div
className="flex h-11 items-center justify-between border-b border-slate-200 px-4 dark:border-dark-700"
className="flex h-12 items-center justify-between border-b border-slate-200 px-5 dark:border-dark-700"
>
<div className="min-w-0 flex-1">
<span className="block truncate text-[14px] font-semibold tracking-[-0.02em] text-slate-900 dark:text-white">
<span className="block truncate text-[15px] font-semibold tracking-[-0.03em] text-slate-900 dark:text-white">
AI Chat Workspace
</span>
</div>
<button
className="hidden rounded-[8px] border border-slate-200 bg-white p-2 text-slate-500 lg:inline-flex xl:hidden dark:border-dark-700 dark:bg-dark-900 dark:text-slate-300"
onClick={handleAsideLgCloseClick}
type="button"
>
<BaseIcon path={mdiClose} size={16} />
</button>
</div>
<div
className={`flex-1 overflow-y-auto overflow-x-hidden ${
darkMode ? 'aside-scrollbars-[slate]' : asideScrollbarsStyle
}`}
>
<AsideMenuList className="px-2 py-2.5" menu={menu} />
<AsideMenuList className="px-3 py-3" menu={menu} />
</div>
</div>
</aside>

View File

@ -16,6 +16,7 @@ import {loadColumns} from "./configureAttachmentsCols";
import _ from 'lodash';
import dataFormatter from '../../helpers/dataFormatter'
import {dataGridStyles} from "../../styles";
import DataGridLoadingOverlay from '../DataGridLoadingOverlay';
@ -45,6 +46,7 @@ const TableSampleAttachments = ({ filterItems, setFilterItems, filters, showGrid
const focusRing = useAppSelector((state) => state.style.focusRingColor);
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
const corners = useAppSelector((state) => state.style.corners);
const isDataGridLoading = !currentUser || loading || columns.length === 0;
const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage);
for (let i = 0; i < numPages; i++) {
pagesList.push(i);
@ -211,7 +213,8 @@ const TableSampleAttachments = ({ filterItems, setFilterItems, filters, showGrid
<div className='relative overflow-x-auto'>
<DataGrid
autoHeight
rowHeight={64}
rowHeight={56}
columnHeaderHeight={48}
sx={dataGridStyles}
className={'datagrid--table'}
getRowClassName={() => `datagrid--row`}
@ -251,7 +254,10 @@ const TableSampleAttachments = ({ filterItems, setFilterItems, filters, showGrid
rowCount={count}
pageSizeOptions={[10]}
paginationMode={'server'}
loading={loading}
loading={isDataGridLoading}
slots={{
loadingOverlay: DataGridLoadingOverlay,
}}
onPaginationModelChange={(params) => {
onPageChange(params.page);
}}

View File

@ -12,6 +12,7 @@ import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter'
import DataGridMultiSelect from "../DataGridMultiSelect";
import ListActionsPopover from '../ListActionsPopover';
import { makeReadableColumns } from '../../helpers/dataGridColumns';
import {hasPermission} from "../../helpers/userPermissions";
@ -39,7 +40,7 @@ export const loadColumns = async (
const hasUpdatePermission = hasPermission(user, 'UPDATE_ATTACHMENTS')
return [
const columns = [
{
field: 'message',
@ -225,4 +226,6 @@ export const loadColumns = async (
},
},
];
return makeReadableColumns(columns);
};

View File

@ -16,6 +16,7 @@ import {loadColumns} from "./configureConversationsCols";
import _ from 'lodash';
import dataFormatter from '../../helpers/dataFormatter'
import {dataGridStyles} from "../../styles";
import DataGridLoadingOverlay from '../DataGridLoadingOverlay';
@ -45,6 +46,7 @@ const TableSampleConversations = ({ filterItems, setFilterItems, filters, showGr
const focusRing = useAppSelector((state) => state.style.focusRingColor);
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
const corners = useAppSelector((state) => state.style.corners);
const isDataGridLoading = !currentUser || loading || columns.length === 0;
const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage);
for (let i = 0; i < numPages; i++) {
pagesList.push(i);
@ -211,7 +213,8 @@ const TableSampleConversations = ({ filterItems, setFilterItems, filters, showGr
<div className='relative overflow-x-auto'>
<DataGrid
autoHeight
rowHeight={64}
rowHeight={56}
columnHeaderHeight={48}
sx={dataGridStyles}
className={'datagrid--table'}
getRowClassName={() => `datagrid--row`}
@ -251,7 +254,10 @@ const TableSampleConversations = ({ filterItems, setFilterItems, filters, showGr
rowCount={count}
pageSizeOptions={[10]}
paginationMode={'server'}
loading={loading}
loading={isDataGridLoading}
slots={{
loadingOverlay: DataGridLoadingOverlay,
}}
onPaginationModelChange={(params) => {
onPageChange(params.page);
}}

View File

@ -12,6 +12,7 @@ import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter'
import DataGridMultiSelect from "../DataGridMultiSelect";
import ListActionsPopover from '../ListActionsPopover';
import { makeReadableColumns } from '../../helpers/dataGridColumns';
import {hasPermission} from "../../helpers/userPermissions";
@ -39,7 +40,7 @@ export const loadColumns = async (
const hasUpdatePermission = hasPermission(user, 'UPDATE_CONVERSATIONS')
return [
const columns = [
{
field: 'user',
@ -203,4 +204,6 @@ export const loadColumns = async (
},
},
];
return makeReadableColumns(columns);
};

View File

@ -0,0 +1,32 @@
import React from 'react';
const SKELETON_ROWS = 4;
export default function DataGridLoadingOverlay() {
return (
<div className="flex h-full min-h-[240px] items-center justify-center bg-white/80 px-6 py-8">
<div className="w-full max-w-3xl">
<div className="mb-5 flex items-center gap-3">
<div className="h-7 w-7 animate-spin rounded-full border-2 border-slate-200 border-t-slate-700" />
<div>
<p className="text-[13px] font-medium text-slate-700">Loading data</p>
<p className="text-[12px] text-slate-400">Fetching the latest rows for this table.</p>
</div>
</div>
<div className="space-y-3">
{Array.from({ length: SKELETON_ROWS }).map((_, index) => (
<div
className="grid grid-cols-[1fr_0.8fr_0.7fr_0.55fr] gap-3"
key={index}
>
<div className="h-10 rounded-[10px] bg-slate-100" />
<div className="h-10 rounded-[10px] bg-slate-100" />
<div className="h-10 rounded-[10px] bg-slate-100" />
<div className="h-10 rounded-[10px] bg-slate-100" />
</div>
))}
</div>
</div>
</div>
);
}

View File

@ -12,13 +12,25 @@ type Props = {
accept?: string;
color: ColorButtonKey;
isRoundIcon?: boolean;
showFilename?: boolean;
path: string;
schema: object;
field: any,
form: any,
};
const FormImagePicker = ({ label, icon, accept, color, isRoundIcon, path, schema, form, field }: Props) => {
const FormImagePicker = ({
label,
icon,
accept,
color,
isRoundIcon,
showFilename = false,
path,
schema,
form,
field,
}: Props) => {
const [file, setFile] = useState(null);
const [loading, setLoading] = useState(false);
const corners = useAppSelector((state) => state.style.corners);
@ -51,14 +63,14 @@ const FormImagePicker = ({ label, icon, accept, color, isRoundIcon, path, schema
setLoading(false);
};
const showFilename = !isRoundIcon && file;
const shouldShowFilename = Boolean(showFilename && !isRoundIcon && file);
return (
<div className='flex items-stretch justify-start relative'>
<label className='inline-flex'>
<BaseButton
className={`${isRoundIcon ? 'w-12 h-12' : ''} ${
showFilename ? 'rounded-r-none' : ''
shouldShowFilename ? 'rounded-r-none' : ''
}`}
iconSize={isRoundIcon ? 24 : undefined}
label={isRoundIcon ? null : label}
@ -76,7 +88,7 @@ const FormImagePicker = ({ label, icon, accept, color, isRoundIcon, path, schema
disabled={loading}
/>
</label>
{showFilename && !loading && (
{shouldShowFilename && !loading && (
<div className={` ${cornersRight} px-4 py-2 max-w-full flex-grow-0 overflow-x-hidden ${bgColor} dark:bg-slate-800 border-gray-200 dark:border-slate-700 border `}>
<span className='text-ellipsis max-w-full line-clamp-1'>
{file.name}

View File

@ -16,6 +16,7 @@ import {loadColumns} from "./configureMessagesCols";
import _ from 'lodash';
import dataFormatter from '../../helpers/dataFormatter'
import {dataGridStyles} from "../../styles";
import DataGridLoadingOverlay from '../DataGridLoadingOverlay';
@ -45,6 +46,7 @@ const TableSampleMessages = ({ filterItems, setFilterItems, filters, showGrid })
const focusRing = useAppSelector((state) => state.style.focusRingColor);
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
const corners = useAppSelector((state) => state.style.corners);
const isDataGridLoading = !currentUser || loading || columns.length === 0;
const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage);
for (let i = 0; i < numPages; i++) {
pagesList.push(i);
@ -211,7 +213,8 @@ const TableSampleMessages = ({ filterItems, setFilterItems, filters, showGrid })
<div className='relative overflow-x-auto'>
<DataGrid
autoHeight
rowHeight={64}
rowHeight={56}
columnHeaderHeight={48}
sx={dataGridStyles}
className={'datagrid--table'}
getRowClassName={() => `datagrid--row`}
@ -251,7 +254,10 @@ const TableSampleMessages = ({ filterItems, setFilterItems, filters, showGrid })
rowCount={count}
pageSizeOptions={[10]}
paginationMode={'server'}
loading={loading}
loading={isDataGridLoading}
slots={{
loadingOverlay: DataGridLoadingOverlay,
}}
onPaginationModelChange={(params) => {
onPageChange(params.page);
}}

View File

@ -12,6 +12,7 @@ import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter'
import DataGridMultiSelect from "../DataGridMultiSelect";
import ListActionsPopover from '../ListActionsPopover';
import { makeReadableColumns } from '../../helpers/dataGridColumns';
import {hasPermission} from "../../helpers/userPermissions";
@ -39,7 +40,7 @@ export const loadColumns = async (
const hasUpdatePermission = hasPermission(user, 'UPDATE_MESSAGES')
return [
const columns = [
{
field: 'conversation',
@ -266,4 +267,6 @@ export const loadColumns = async (
},
},
];
return makeReadableColumns(columns);
};

View File

@ -1,4 +1,4 @@
import React, { ReactNode, useState, useEffect } from 'react'
import React, { ReactNode, useState, useEffect, CSSProperties } from 'react'
import { mdiClose, mdiDotsVertical } from '@mdi/js'
import { containerMaxW } from '../config'
import BaseIcon from './BaseIcon'
@ -12,9 +12,10 @@ type Props = {
className: string
contentClassName?: string
children: ReactNode
style?: CSSProperties
}
export default function NavBar({ menu, className = '', contentClassName = containerMaxW, children }: Props) {
export default function NavBar({ menu, className = '', contentClassName = containerMaxW, children, style }: Props) {
const [isMenuNavBarActive, setIsMenuNavBarActive] = useState(false)
const [isScrolled, setIsScrolled] = useState(false);
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
@ -37,7 +38,8 @@ export default function NavBar({ menu, className = '', contentClassName = contai
return (
<nav
className={`${className} top-0 inset-x-0 fixed ${bgColor} h-12 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-auto dark:bg-dark-800`}
style={style}
>
<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-12">{children}</div>

View File

@ -16,6 +16,7 @@ import {loadColumns} from "./configurePermissionsCols";
import _ from 'lodash';
import dataFormatter from '../../helpers/dataFormatter'
import {dataGridStyles} from "../../styles";
import DataGridLoadingOverlay from '../DataGridLoadingOverlay';
@ -45,6 +46,7 @@ const TableSamplePermissions = ({ filterItems, setFilterItems, filters, showGrid
const focusRing = useAppSelector((state) => state.style.focusRingColor);
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
const corners = useAppSelector((state) => state.style.corners);
const isDataGridLoading = !currentUser || loading || columns.length === 0;
const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage);
for (let i = 0; i < numPages; i++) {
pagesList.push(i);
@ -211,7 +213,8 @@ const TableSamplePermissions = ({ filterItems, setFilterItems, filters, showGrid
<div className='relative overflow-x-auto'>
<DataGrid
autoHeight
rowHeight={64}
rowHeight={56}
columnHeaderHeight={48}
sx={dataGridStyles}
className={'datagrid--table'}
getRowClassName={() => `datagrid--row`}
@ -251,7 +254,10 @@ const TableSamplePermissions = ({ filterItems, setFilterItems, filters, showGrid
rowCount={count}
pageSizeOptions={[10]}
paginationMode={'server'}
loading={loading}
loading={isDataGridLoading}
slots={{
loadingOverlay: DataGridLoadingOverlay,
}}
onPaginationModelChange={(params) => {
onPageChange(params.page);
}}

View File

@ -12,6 +12,7 @@ import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter'
import DataGridMultiSelect from "../DataGridMultiSelect";
import ListActionsPopover from '../ListActionsPopover';
import { makeReadableColumns } from '../../helpers/dataGridColumns';
import {hasPermission} from "../../helpers/userPermissions";
@ -39,7 +40,7 @@ export const loadColumns = async (
const hasUpdatePermission = hasPermission(user, 'UPDATE_PERMISSIONS')
return [
const columns = [
{
field: 'name',
@ -80,4 +81,6 @@ export const loadColumns = async (
},
},
];
return makeReadableColumns(columns);
};

View File

@ -16,6 +16,7 @@ import {loadColumns} from "./configureRolesCols";
import _ from 'lodash';
import dataFormatter from '../../helpers/dataFormatter'
import {dataGridStyles} from "../../styles";
import DataGridLoadingOverlay from '../DataGridLoadingOverlay';
@ -45,6 +46,7 @@ const TableSampleRoles = ({ filterItems, setFilterItems, filters, showGrid }) =>
const focusRing = useAppSelector((state) => state.style.focusRingColor);
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
const corners = useAppSelector((state) => state.style.corners);
const isDataGridLoading = !currentUser || loading || columns.length === 0;
const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage);
for (let i = 0; i < numPages; i++) {
pagesList.push(i);
@ -211,7 +213,8 @@ const TableSampleRoles = ({ filterItems, setFilterItems, filters, showGrid }) =>
<div id="rolesTable" className='relative overflow-x-auto'>
<DataGrid
autoHeight
rowHeight={64}
rowHeight={56}
columnHeaderHeight={48}
sx={dataGridStyles}
className={'datagrid--table'}
getRowClassName={() => `datagrid--row`}
@ -251,7 +254,10 @@ const TableSampleRoles = ({ filterItems, setFilterItems, filters, showGrid }) =>
rowCount={count}
pageSizeOptions={[10]}
paginationMode={'server'}
loading={loading}
loading={isDataGridLoading}
slots={{
loadingOverlay: DataGridLoadingOverlay,
}}
onPaginationModelChange={(params) => {
onPageChange(params.page);
}}

View File

@ -12,6 +12,7 @@ import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter'
import DataGridMultiSelect from "../DataGridMultiSelect";
import ListActionsPopover from '../ListActionsPopover';
import { makeReadableColumns } from '../../helpers/dataGridColumns';
import {hasPermission} from "../../helpers/userPermissions";
@ -39,7 +40,7 @@ export const loadColumns = async (
const hasUpdatePermission = hasPermission(user, 'UPDATE_ROLES')
return [
const columns = [
{
field: 'name',
@ -100,4 +101,6 @@ export const loadColumns = async (
},
},
];
return makeReadableColumns(columns);
};

View File

@ -16,6 +16,7 @@ import {loadColumns} from "./configureUsage_eventsCols";
import _ from 'lodash';
import dataFormatter from '../../helpers/dataFormatter'
import {dataGridStyles} from "../../styles";
import DataGridLoadingOverlay from '../DataGridLoadingOverlay';
@ -45,6 +46,7 @@ const TableSampleUsage_events = ({ filterItems, setFilterItems, filters, showGri
const focusRing = useAppSelector((state) => state.style.focusRingColor);
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
const corners = useAppSelector((state) => state.style.corners);
const isDataGridLoading = !currentUser || loading || columns.length === 0;
const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage);
for (let i = 0; i < numPages; i++) {
pagesList.push(i);
@ -211,7 +213,8 @@ const TableSampleUsage_events = ({ filterItems, setFilterItems, filters, showGri
<div className='relative overflow-x-auto'>
<DataGrid
autoHeight
rowHeight={64}
rowHeight={56}
columnHeaderHeight={48}
sx={dataGridStyles}
className={'datagrid--table'}
getRowClassName={() => `datagrid--row`}
@ -251,7 +254,10 @@ const TableSampleUsage_events = ({ filterItems, setFilterItems, filters, showGri
rowCount={count}
pageSizeOptions={[10]}
paginationMode={'server'}
loading={loading}
loading={isDataGridLoading}
slots={{
loadingOverlay: DataGridLoadingOverlay,
}}
onPaginationModelChange={(params) => {
onPageChange(params.page);
}}

View File

@ -12,6 +12,7 @@ import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter'
import DataGridMultiSelect from "../DataGridMultiSelect";
import ListActionsPopover from '../ListActionsPopover';
import { makeReadableColumns } from '../../helpers/dataGridColumns';
import {hasPermission} from "../../helpers/userPermissions";
@ -39,7 +40,7 @@ export const loadColumns = async (
const hasUpdatePermission = hasPermission(user, 'UPDATE_USAGE_EVENTS')
return [
const columns = [
{
field: 'user',
@ -295,4 +296,6 @@ export const loadColumns = async (
},
},
];
return makeReadableColumns(columns);
};

View File

@ -16,6 +16,7 @@ import {loadColumns} from "./configureUsersCols";
import _ from 'lodash';
import dataFormatter from '../../helpers/dataFormatter'
import {dataGridStyles} from "../../styles";
import DataGridLoadingOverlay from '../DataGridLoadingOverlay';
@ -45,6 +46,7 @@ const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) =>
const focusRing = useAppSelector((state) => state.style.focusRingColor);
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
const corners = useAppSelector((state) => state.style.corners);
const isDataGridLoading = !currentUser || loading || columns.length === 0;
const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage);
for (let i = 0; i < numPages; i++) {
pagesList.push(i);
@ -211,7 +213,8 @@ const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) =>
<div id="usersTable" className='relative overflow-x-auto'>
<DataGrid
autoHeight
rowHeight={64}
rowHeight={56}
columnHeaderHeight={48}
sx={dataGridStyles}
className={'datagrid--table'}
getRowClassName={() => `datagrid--row`}
@ -251,7 +254,10 @@ const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) =>
rowCount={count}
pageSizeOptions={[10]}
paginationMode={'server'}
loading={loading}
loading={isDataGridLoading}
slots={{
loadingOverlay: DataGridLoadingOverlay,
}}
onPaginationModelChange={(params) => {
onPageChange(params.page);
}}

View File

@ -12,6 +12,7 @@ import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter'
import DataGridMultiSelect from "../DataGridMultiSelect";
import ListActionsPopover from '../ListActionsPopover';
import { makeReadableColumns } from '../../helpers/dataGridColumns';
import {hasPermission} from "../../helpers/userPermissions";
@ -39,7 +40,7 @@ export const loadColumns = async (
const hasUpdatePermission = hasPermission(user, 'UPDATE_USERS')
return [
const columns = [
{
field: 'firstName',
@ -204,4 +205,6 @@ export const loadColumns = async (
},
},
];
return makeReadableColumns(columns);
};

View File

@ -17,7 +17,7 @@ import { useRouter } from 'next/router';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { toast } from 'react-toastify';
import BaseIcon from '../BaseIcon';
import { hasPermission } from '../../helpers/userPermissions';
import { ADMIN_ENTRY_PERMISSIONS, hasPermission } from '../../helpers/userPermissions';
import { useAppSelector } from '../../stores/hooks';
import ChatMarkdown from './ChatMarkdown';
@ -474,7 +474,7 @@ export default function WorkspaceShell() {
const currentUserName = currentUser?.firstName || currentUser?.email || 'You';
const currentUserAvatarUrl = getAvatarUrl(currentUser?.avatar);
const currentUserInitial = getUserInitial(currentUser);
const canAccessAdmin = hasPermission(currentUser, 'READ_USERS');
const canAccessAdmin = hasPermission(currentUser, ADMIN_ENTRY_PERMISSIONS);
const activeConversations = useMemo(
() => conversations.filter((conversation) => conversation.status !== 'archived'),

View File

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

View File

@ -71,36 +71,47 @@
}
.datagrid--table, .MuiDataGrid-root {
@apply rounded border-none !important;
@apply rounded-[10px] border-none bg-white text-slate-700 !important;
}
.datagrid--table .MuiDataGrid-columnHeaders {
@apply border-b border-slate-200 bg-slate-50/70 !important;
}
.datagrid--header {
@apply uppercase !important;
@apply text-[11px] font-semibold tracking-[0.14em] text-slate-400 !important;
}
.datagrid--header,
.datagrid--header .MuiIconButton-root,
.datagrid--cell,
.datagrid--cell .MuiIconButton-root {
@apply dark:text-white;
@apply text-slate-700 dark:text-white;
}
.datagrid--cell .MuiDataGrid-booleanCell {
@apply dark:text-white !important;
@apply text-slate-700 dark:text-white !important;
}
.datagrid--cell .MuiIconButton-root:hover {
@apply dark:text-white dark:bg-dark-700;
@apply bg-slate-100 text-slate-700 dark:text-white dark:bg-dark-700;
}
.datagrid--row {
@apply even:bg-gray-100 dark:even:bg-[#1B1D22] dark:odd:bg-dark-900 !important;
@apply border-b border-slate-100 bg-white dark:odd:bg-dark-900 dark:even:bg-dark-900 !important;
}
.datagrid--row:hover {
@apply bg-slate-50 !important;
}
.datagrid--content {
@apply block min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-[13px] font-normal leading-5 text-slate-700;
}
.datagrid--table .MuiTablePagination-root {
@apply dark:text-white;
@apply text-[13px] text-slate-500 dark:text-white;
}
.datagrid--table .MuiTablePagination-root .MuiButtonBase-root:disabled {
@ -114,4 +125,15 @@
.MuiButton-colorInherit {
@apply text-blue-600 dark:text-dark-700 !important;
}
.datagrid--table .MuiDataGrid-columnSeparator {
display: none !important;
}
.datagrid--table .MuiDataGrid-cell:focus,
.datagrid--table .MuiDataGrid-cell:focus-within,
.datagrid--table .MuiDataGrid-columnHeader:focus,
.datagrid--table .MuiDataGrid-columnHeader:focus-within {
outline: none !important;
}
}

View File

@ -0,0 +1,243 @@
import React from 'react';
import type { GridColDef } from '@mui/x-data-grid';
import { humanize } from './humanize';
const HEADER_OVERRIDES: Record<string, string> = {
app_role: 'Role',
author_user: 'Author',
client_context_json: 'Client context',
completed_at: 'Completed',
content_markdown: 'Markdown',
createdAt: 'Created',
firstName: 'First name',
is_default: 'Default',
is_pinned: 'Pinned',
last_message_at: 'Last message',
lastName: 'Last name',
phoneNumber: 'Phone',
public_url: 'Public URL',
sent_at: 'Sent',
system_prompt: 'System prompt',
updatedAt: 'Updated',
usage_type: 'Usage type',
};
const WIDTH_OVERRIDES: Record<string, number> = {
actions: 64,
agent: 160,
app_role: 140,
author_user: 150,
client_context_json: 220,
content: 240,
content_markdown: 220,
conversation: 190,
createdAt: 150,
description: 220,
email: 220,
event_type: 150,
last_message_at: 170,
message: 220,
model: 150,
name: 160,
permissions: 240,
phoneNumber: 160,
role: 120,
status: 130,
summary: 220,
system_prompt: 240,
title: 200,
updatedAt: 150,
usage_type: 150,
user: 150,
};
const FIXED_WIDTH_FIELDS = new Set([
'actions',
'app_role',
'author_user',
'completed_at',
'createdAt',
'event_type',
'is_default',
'is_pinned',
'last_message_at',
'model',
'phoneNumber',
'public_url',
'role',
'sent_at',
'status',
'updatedAt',
'usage_type',
'user',
]);
function appendClassName(existing: string | undefined, next: string) {
if (!existing) {
return next;
}
if (existing.includes(next)) {
return existing;
}
return `${existing} ${next}`;
}
function getHeaderName(column: GridColDef) {
if (HEADER_OVERRIDES[column.field]) {
return HEADER_OVERRIDES[column.field];
}
if (column.headerName) {
return humanize(column.headerName);
}
return humanize(column.field);
}
function formatDateValue(value: string | Date) {
const parsed = value instanceof Date ? value : new Date(value);
if (Number.isNaN(parsed.getTime())) {
return String(value);
}
return parsed.toLocaleString([], {
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
});
}
function formatCellValue(value: unknown, field: string) {
if (value === null || value === undefined || value === '') {
return '—';
}
if (typeof value === 'boolean') {
if (value) {
return 'Yes';
}
return 'No';
}
if (value instanceof Date) {
return formatDateValue(value);
}
if (typeof value === 'string') {
if (field.endsWith('_at') || field.endsWith('At')) {
return formatDateValue(value);
}
return value;
}
if (Array.isArray(value)) {
const text = value
.map((item) => formatCellValue(item, field))
.filter(Boolean)
.join(', ');
if (!text) {
return '—';
}
return text;
}
if (typeof value === 'object') {
if ('firstName' in value || 'lastName' in value) {
const firstName = 'firstName' in value && value.firstName ? String(value.firstName) : '';
const lastName = 'lastName' in value && value.lastName ? String(value.lastName) : '';
const fullName = `${firstName} ${lastName}`.trim();
if (fullName) {
return fullName;
}
}
if ('label' in value && value.label) {
return String(value.label);
}
if ('name' in value && value.name) {
return String(value.name);
}
if ('title' in value && value.title) {
return String(value.title);
}
if ('content' in value && value.content) {
return String(value.content);
}
if ('email' in value && value.email) {
return String(value.email);
}
const json = JSON.stringify(value);
if (json.length > 120) {
return `${json.slice(0, 117)}...`;
}
return json;
}
return String(value);
}
export function makeReadableColumns(columns: GridColDef[]) {
return columns.map((column) => {
if (column.type === 'actions') {
return {
...column,
field: column.field,
headerName: '',
width: WIDTH_OVERRIDES.actions,
minWidth: WIDTH_OVERRIDES.actions,
flex: 0,
sortable: false,
filterable: false,
disableColumnMenu: true,
headerClassName: appendClassName(column.headerClassName, 'datagrid--header'),
cellClassName: appendClassName(column.cellClassName, 'datagrid--cell'),
};
}
const headerName = getHeaderName(column);
const targetMinWidth = WIDTH_OVERRIDES[column.field] || Math.max(column.minWidth || 140, Math.min(220, headerName.length * 11 + 40));
const flex = typeof column.flex === 'number' ? column.flex : FIXED_WIDTH_FIELDS.has(column.field) ? 0 : 1;
return {
...column,
headerName,
minWidth: targetMinWidth,
flex,
headerClassName: appendClassName(column.headerClassName, 'datagrid--header'),
cellClassName: appendClassName(column.cellClassName, 'datagrid--cell'),
renderCell:
column.renderCell ||
((params) => {
const rowValue = params.row?.[column.field];
const value =
rowValue !== undefined && rowValue !== null
? rowValue
: params.formattedValue ?? params.value;
const text = formatCellValue(value, column.field);
return (
<div className="datagrid--content" title={text}>
{text}
</div>
);
}),
};
});
}

View File

@ -3,9 +3,13 @@ export function humanize(str: string) {
return '';
}
return str.toString()
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
.replace(/^[\s_]+|[\s_]+$/g, '')
.replace(/[_\s]+/g, ' ')
.replace(/\bjson\b/gi, 'JSON')
.replace(/\bid\b/gi, 'ID')
.replace(/^[a-z]/, function (m) {
return m.toUpperCase();
});
}
}

View File

@ -1,18 +1,49 @@
const USER_PERMISSIONS = new Set([
'CREATE_USERS',
'READ_USERS',
'UPDATE_USERS',
'DELETE_USERS',
]);
export const ADMIN_ENTRY_PERMISSIONS = [
'READ_AGENTS',
'READ_CONVERSATIONS',
'READ_MESSAGES',
'READ_USAGE_EVENTS',
];
export function hasPermission(user, permission_name: string | string[]) {
if (!user?.app_role?.name) return false;
if (!permission_name) {
return true;
}
const isAdministrator = user.app_role.name === 'Administrator';
const permissions = new Set<string>([
...(user?.custom_permissions ?? []).map((p) => p.name),
...(user?.app_role_permissions ?? []).map((p) => p.name),
]);
if (typeof permission_name === 'string') {
return permissions.has(permission_name) || user.app_role.name === 'Administrator'
if (USER_PERMISSIONS.has(permission_name) && !isAdministrator) {
return false;
}
} else {
const requestsUsersPermission = permission_name.some((permission) => USER_PERMISSIONS.has(permission));
if (requestsUsersPermission && !isAdministrator) {
return false;
}
}
if (typeof permission_name === 'string') {
return permissions.has(permission_name) || isAdministrator
} else {
if (isAdministrator) {
return true;
}
return permission_name.some((permission) => permissions.has(permission));
}
}

View File

@ -1,9 +1,6 @@
import React, { ReactNode, useEffect, useRef, useState } from 'react'
import jwt from 'jsonwebtoken';
import {
mdiForwardburger,
mdiBackburger,
mdiMenu,
mdiChevronDown,
mdiCogOutline,
mdiLogout,
@ -11,7 +8,6 @@ import {
import menuAside from '../menuAside'
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'
@ -26,7 +22,7 @@ import {hasPermission} from "../helpers/userPermissions";
type Props = {
children: ReactNode
permission?: string
permission?: string | string[]
}
@ -60,33 +56,50 @@ export default function LayoutAuthenticated({
permission
}: Props) {
const asideWidth = 256
const dispatch = useAppDispatch()
const router = useRouter()
const { token, currentUser } = useAppSelector((state) => state.auth)
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
const isWorkspaceRoute = router.pathname.startsWith('/workspace');
let localToken
if (typeof window !== 'undefined') {
// Perform localStorage action
localToken = localStorage.getItem('token')
}
const [localToken, setLocalToken] = useState<string | null>(null)
const effectiveToken = token || localToken || ''
const isAuthBootstrapped = Boolean(token) || localToken !== null
const isTokenValid = (value: string) => {
if (!value) {
return false;
}
const isTokenValid = () => {
const token = localStorage.getItem('token');
if (!token) return;
const date = new Date().getTime() / 1000;
const data = jwt.decode(token);
if (!data) return;
const data = jwt.decode(value);
if (!data) {
return false;
}
return date < data.exp;
};
useEffect(() => {
dispatch(findMe());
if (!isTokenValid()) {
dispatch(logoutUser());
router.push('/login');
if (typeof window === 'undefined') {
return;
}
}, [token, localToken]);
setLocalToken(localStorage.getItem('token') || '')
}, [])
useEffect(() => {
if (!effectiveToken) {
return;
}
if (!isTokenValid(effectiveToken)) {
dispatch(logoutUser());
return;
}
dispatch(findMe());
}, [dispatch, effectiveToken]);
useEffect(() => {
@ -98,8 +111,6 @@ export default function LayoutAuthenticated({
const darkMode = useAppSelector((state) => state.style.darkMode)
const [isAsideMobileExpanded, setIsAsideMobileExpanded] = useState(false)
const [isAsideLgActive, setIsAsideLgActive] = useState(false)
const [isWorkspaceAccountMenuOpen, setIsWorkspaceAccountMenuOpen] = useState(false)
const workspaceAccountMenuButtonRef = useRef(null)
const currentUserAvatarUrl = getAvatarUrl(currentUser?.avatar)
@ -107,8 +118,6 @@ export default function LayoutAuthenticated({
useEffect(() => {
const handleRouteChangeStart = () => {
setIsAsideMobileExpanded(false)
setIsAsideLgActive(false)
setIsWorkspaceAccountMenuOpen(false)
}
@ -121,44 +130,88 @@ export default function LayoutAuthenticated({
}
}, [router.events, dispatch])
const contentStyle = isWorkspaceRoute
? undefined
: { paddingLeft: `${asideWidth}px` }
const navStyle = isWorkspaceRoute
? undefined
: { left: `${asideWidth}px`, width: `calc(100% - ${asideWidth}px)` }
const layoutAsidePadding = isWorkspaceRoute ? '' : 'xl:pl-64'
const layoutOffsetClass = isWorkspaceRoute ? '' : isAsideMobileExpanded ? 'ml-64 lg:ml-0' : ''
if (!isAuthBootstrapped) {
return (
<div className={`${darkMode ? 'dark' : ''}`}>
<div className={`min-h-screen ${bgColor} dark:bg-dark-800 dark:text-slate-100`}>
<div className="flex min-h-screen items-center justify-center px-6 py-10">
<div className="w-full max-w-md rounded-[12px] border border-slate-200 bg-white p-6 shadow-[0_24px_60px_-40px_rgba(15,23,42,0.25)] dark:border-slate-800 dark:bg-dark-900">
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400">
Loading
</p>
<h1 className="mt-3 text-[28px] font-semibold tracking-[-0.03em] text-slate-900 dark:text-slate-50">
Restoring your workspace.
</h1>
<p className="mt-3 text-[14px] leading-6 text-slate-500 dark:text-slate-400">
We are checking your session before opening the app.
</p>
</div>
</div>
</div>
</div>
)
}
if (!effectiveToken) {
return (
<div className={`${darkMode ? 'dark' : ''}`}>
<div className={`min-h-screen ${bgColor} px-6 py-10 dark:bg-dark-800 dark:text-slate-100`}>
<div className="mx-auto flex min-h-[calc(100vh-5rem)] max-w-md items-center justify-center">
<div className="w-full rounded-[12px] border border-slate-200 bg-white p-6 shadow-[0_24px_60px_-40px_rgba(15,23,42,0.25)] dark:border-slate-800 dark:bg-dark-900">
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400">
Session expired
</p>
<h1 className="mt-3 text-[28px] font-semibold tracking-[-0.03em] text-slate-900 dark:text-slate-50">
Sign in again to continue.
</h1>
<p className="mt-3 text-[14px] leading-6 text-slate-500 dark:text-slate-400">
Your session has ended. Open the sign-in screen to continue working in the workspace.
</p>
<div className="mt-6">
<Link
className="inline-flex h-11 items-center rounded-[8px] bg-slate-900 px-4 text-[14px] font-medium text-white dark:bg-slate-100 dark:text-slate-900"
href="/login"
>
Open login
</Link>
</div>
</div>
</div>
</div>
</div>
)
}
return (
<div className={`${darkMode ? 'dark' : ''} overflow-hidden lg:overflow-visible`}>
<div
className={`${layoutAsidePadding} ${layoutOffsetClass} pt-12 min-h-screen w-screen transition-position lg:w-auto ${bgColor} dark:bg-dark-800 dark:text-slate-100`}
className={`pt-12 min-h-screen ${bgColor} dark:bg-dark-800 dark:text-slate-100`}
style={contentStyle}
>
<NavBar
menu={[]}
className={`${layoutAsidePadding} ${layoutOffsetClass}`}
className=""
contentClassName="w-full"
style={navStyle}
>
<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>
</>
{isWorkspaceRoute && (
<Link
className="truncate text-[14px] font-semibold tracking-[-0.02em] text-slate-700 dark:text-slate-100"
href="/workspace"
>
AI Chat Workspace
</Link>
)}
<Link
className="truncate text-[14px] font-semibold tracking-[-0.02em] text-slate-700 dark:text-slate-100"
href="/workspace"
>
AI Chat Workspace
</Link>
</div>
<div className="relative">
<button
@ -221,10 +274,7 @@ export default function LayoutAuthenticated({
</NavBar>
{!isWorkspaceRoute && (
<AsideMenu
isAsideMobileExpanded={isAsideMobileExpanded}
isAsideLgActive={isAsideLgActive}
menu={menuAside}
onAsideLgClose={() => setIsAsideLgActive(false)}
/>
)}
{children}

View File

@ -1,5 +1,6 @@
import * as icon from '@mdi/js';
import { MenuAsideItem } from './interfaces'
import { ADMIN_ENTRY_PERMISSIONS } from './helpers/userPermissions'
const backofficeMenu: MenuAsideItem[] = [
{
@ -83,7 +84,7 @@ const menuAside: MenuAsideItem[] = [
label: 'Admin',
icon: icon.mdiCogOutline,
menu: backofficeMenu,
permissions: 'READ_USERS'
permissions: ADMIN_ENTRY_PERMISSIONS
},
]

View File

@ -7,7 +7,7 @@ import { store } from '../stores/store';
import { Provider } from 'react-redux';
import '../css/main.css';
import 'react-toastify/dist/ReactToastify.css';
import { ToastContainer } from 'react-toastify';
import { ToastContainer, toast } from 'react-toastify';
import axios from 'axios';
import { baseURLApi } from '../config';
import { useRouter } from 'next/router';
@ -18,6 +18,7 @@ import { appWithTranslation } from 'next-i18next';
import '../i18n';
import IntroGuide from '../components/IntroGuide';
import { appSteps, loginSteps, usersSteps, rolesSteps } from '../stores/introSteps';
import { logoutUser } from '../stores/authSlice';
// Initialize axios
axios.defaults.baseURL = process.env.NEXT_PUBLIC_BACK_API
@ -41,24 +42,60 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
const [stepsEnabled, setStepsEnabled] = React.useState(false);
const [stepName, setStepName] = React.useState('');
const [steps, setSteps] = React.useState([]);
const unauthorizedToastLock = React.useRef(false);
axios.interceptors.request.use(
config => {
const token = localStorage.getItem('token');
React.useEffect(() => {
const requestInterceptor = axios.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
} else {
delete config.headers.Authorization;
}
return config;
},
error => {
return Promise.reject(error);
if (token) {
config.headers.Authorization = `Bearer ${token}`;
} else {
delete config.headers.Authorization;
}
return config;
},
(error) => {
return Promise.reject(error);
},
);
const responseInterceptor = axios.interceptors.response.use(
(response) => {
return response;
},
(error) => {
const status = error?.response?.status;
const requestUrl = String(error?.config?.url || '');
const isSigninRequest = requestUrl.includes('auth/signin/local');
if (status === 401 && !isSigninRequest) {
store.dispatch(logoutUser());
if (!unauthorizedToastLock.current) {
unauthorizedToastLock.current = true;
toast.error('Session expired. Please sign in again.', {
position: 'bottom-center',
});
window.setTimeout(() => {
unauthorizedToastLock.current = false;
}, 3000);
}
}
return Promise.reject(error);
},
);
return () => {
axios.interceptors.request.eject(requestInterceptor);
axios.interceptors.response.eject(responseInterceptor);
};
}, []);
// TODO: Remove this code in future releases
React.useEffect(() => {
const allowedOrigin = (() => {

View File

@ -18,7 +18,7 @@ import type { ReactElement } from 'react';
import BaseIcon from '../components/BaseIcon';
import SectionMain from '../components/SectionMain';
import { getPageTitle } from '../config';
import { hasPermission } from '../helpers/userPermissions';
import { ADMIN_ENTRY_PERMISSIONS, hasPermission } from '../helpers/userPermissions';
import LayoutAuthenticated from '../layouts/Authenticated';
import { useAppSelector } from '../stores/hooks';
@ -283,7 +283,7 @@ function Dashboard() {
}
Dashboard.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated permission="READ_USERS">{page}</LayoutAuthenticated>;
return <LayoutAuthenticated permission={ADMIN_ENTRY_PERMISSIONS}>{page}</LayoutAuthenticated>;
};
export default Dashboard;

View File

@ -94,7 +94,7 @@ const ProfilePage = () => {
<title>{getPageTitle('Profile')}</title>
</Head>
<SectionMain>
<div className="mx-auto flex w-full max-w-5xl flex-col gap-5">
<div className="flex w-full flex-col gap-5">
<div className="rounded-[12px] border border-slate-200 bg-white px-6 py-6">
<Link
className="inline-flex items-center gap-2 text-[12px] font-medium text-slate-500"

View File

@ -13,13 +13,16 @@ 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 { ADMIN_ENTRY_PERMISSIONS, hasPermission } from '../helpers/userPermissions';
import { useAppSelector } from '../stores/hooks';
function SettingsPage() {
const { currentUser } = useAppSelector((state) => state.auth);
const canAccessAdmin = hasPermission(currentUser, 'READ_USERS');
const canAccessAdmin = hasPermission(currentUser, ADMIN_ENTRY_PERMISSIONS);
const canAccessApiDocs = hasPermission(currentUser, 'READ_API_DOCS');
const apiDocsHref = process.env.NEXT_PUBLIC_BACK_API
? `${process.env.NEXT_PUBLIC_BACK_API.replace(/\/api$/, '')}/api-docs/`
: '/api-docs/';
return (
<>
@ -27,7 +30,7 @@ function SettingsPage() {
<title>{getPageTitle('Settings')}</title>
</Head>
<SectionMain>
<div className="mx-auto flex w-full max-w-4xl flex-col gap-6">
<div className="flex w-full 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
@ -92,9 +95,10 @@ function SettingsPage() {
)}
{canAccessApiDocs && (
<Link
<a
className="group rounded-[12px] border border-slate-200 bg-white px-5 py-5"
href="/api-docs"
href={apiDocsHref}
rel="noreferrer"
target="_blank"
>
<div className="flex items-start justify-between gap-4">
@ -115,7 +119,7 @@ function SettingsPage() {
<BaseIcon path={mdiChevronRight} size={18} />
</div>
</div>
</Link>
</a>
)}
</div>
</div>

View File

@ -56,28 +56,52 @@ export const white: StyleObject = {
export const dataGridStyles = {
'& .MuiDataGrid-cell': {
paddingX: 3,
paddingX: 1.75,
border: 'none',
alignItems: 'center',
},
'& .MuiDataGrid-columnHeader': {
paddingX: 3,
paddingX: 1.75,
},
'& .MuiDataGrid-columnHeaderCheckbox': {
paddingX: 0,
paddingX: 0.5,
},
'& .MuiDataGrid-columnHeaders': {
paddingY: 4,
borderStartStartRadius: 7,
borderStartEndRadius: 7,
paddingY: 0,
minHeight: '48px !important',
maxHeight: '48px !important',
borderStartStartRadius: 10,
borderStartEndRadius: 10,
},
'& .MuiDataGrid-footerContainer': {
paddingY: 0.5,
borderEndStartRadius: 7,
borderEndEndRadius: 7,
minHeight: '48px',
borderTop: '1px solid rgb(226 232 240)',
borderEndStartRadius: 10,
borderEndEndRadius: 10,
color: 'rgb(100 116 139)',
},
'& .MuiDataGrid-root': {
border: 'none',
},
'& .MuiDataGrid-row': {
minHeight: '56px !important',
maxHeight: '56px !important',
},
'& .MuiDataGrid-cellCheckbox': {
paddingLeft: 1,
paddingRight: 1,
},
'& .MuiDataGrid-columnHeaderTitleContainer': {
paddingLeft: 0,
paddingRight: 0,
},
'& .MuiDataGrid-columnHeaderTitle': {
fontSize: '11px',
fontWeight: 600,
letterSpacing: '0.14em',
textTransform: 'none',
color: 'rgb(148 163 184)',
},
};
export const basic: StyleObject = {

View File

@ -29,6 +29,10 @@ http {
}
location = /api-docs {
return 301 /api-docs/;
}
location /api-docs/ {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
@ -93,4 +97,4 @@ http {
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}
}