6
This commit is contained in:
parent
2ec27ee5f1
commit
eb455b41dd
@ -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", [
|
|||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,13 @@
|
|||||||
const ValidationError = require('../services/notifications/errors/validation');
|
const ValidationError = require('../services/notifications/errors/validation');
|
||||||
const RolesDBApi = require('../db/api/roles');
|
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
|
// Cache for the 'Public' role object
|
||||||
let publicRoleCache = null;
|
let publicRoleCache = null;
|
||||||
|
|
||||||
@ -47,7 +54,16 @@ function checkPermissions(permission) {
|
|||||||
return next(); // User has access to their own resource
|
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) {
|
if (currentUser) {
|
||||||
// Ensure custom_permissions is an array before using find
|
// Ensure custom_permissions is an array before using find
|
||||||
const customPermissions = Array.isArray(currentUser.custom_permissions)
|
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;
|
let effectiveRole = null;
|
||||||
try {
|
try {
|
||||||
if (currentUser && currentUser.app_role) {
|
if (currentUser && currentUser.app_role) {
|
||||||
@ -90,7 +106,7 @@ function checkPermissions(permission) {
|
|||||||
return next(new Error("Internal Server Error: Could not determine effective role."));
|
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
|
// Assume the effectiveRole object (from app_role or RolesDBApi) has a getPermissions() method
|
||||||
// or a 'permissions' property (if permissions are eagerly loaded).
|
// or a 'permissions' property (if permissions are eagerly loaded).
|
||||||
let rolePermissions = [];
|
let rolePermissions = [];
|
||||||
@ -146,4 +162,3 @@ module.exports = {
|
|||||||
checkPermissions,
|
checkPermissions,
|
||||||
checkCrudPermissions,
|
checkCrudPermissions,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import {loadColumns} from "./configureAgentsCols";
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import dataFormatter from '../../helpers/dataFormatter'
|
import dataFormatter from '../../helpers/dataFormatter'
|
||||||
import {dataGridStyles} from "../../styles";
|
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 focusRing = useAppSelector((state) => state.style.focusRingColor);
|
||||||
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
|
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
|
||||||
const corners = useAppSelector((state) => state.style.corners);
|
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);
|
const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage);
|
||||||
for (let i = 0; i < numPages; i++) {
|
for (let i = 0; i < numPages; i++) {
|
||||||
pagesList.push(i);
|
pagesList.push(i);
|
||||||
@ -211,7 +213,8 @@ const TableSampleAgents = ({ filterItems, setFilterItems, filters, showGrid }) =
|
|||||||
<div className='relative overflow-x-auto'>
|
<div className='relative overflow-x-auto'>
|
||||||
<DataGrid
|
<DataGrid
|
||||||
autoHeight
|
autoHeight
|
||||||
rowHeight={64}
|
rowHeight={56}
|
||||||
|
columnHeaderHeight={48}
|
||||||
sx={dataGridStyles}
|
sx={dataGridStyles}
|
||||||
className={'datagrid--table'}
|
className={'datagrid--table'}
|
||||||
getRowClassName={() => `datagrid--row`}
|
getRowClassName={() => `datagrid--row`}
|
||||||
@ -251,7 +254,10 @@ const TableSampleAgents = ({ filterItems, setFilterItems, filters, showGrid }) =
|
|||||||
rowCount={count}
|
rowCount={count}
|
||||||
pageSizeOptions={[10]}
|
pageSizeOptions={[10]}
|
||||||
paginationMode={'server'}
|
paginationMode={'server'}
|
||||||
loading={loading}
|
loading={isDataGridLoading}
|
||||||
|
slots={{
|
||||||
|
loadingOverlay: DataGridLoadingOverlay,
|
||||||
|
}}
|
||||||
onPaginationModelChange={(params) => {
|
onPaginationModelChange={(params) => {
|
||||||
onPageChange(params.page);
|
onPageChange(params.page);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {saveFile} from "../../helpers/fileSaver";
|
|||||||
import dataFormatter from '../../helpers/dataFormatter'
|
import dataFormatter from '../../helpers/dataFormatter'
|
||||||
import DataGridMultiSelect from "../DataGridMultiSelect";
|
import DataGridMultiSelect from "../DataGridMultiSelect";
|
||||||
import ListActionsPopover from '../ListActionsPopover';
|
import ListActionsPopover from '../ListActionsPopover';
|
||||||
|
import { makeReadableColumns } from '../../helpers/dataGridColumns';
|
||||||
|
|
||||||
import {hasPermission} from "../../helpers/userPermissions";
|
import {hasPermission} from "../../helpers/userPermissions";
|
||||||
|
|
||||||
@ -39,7 +40,7 @@ export const loadColumns = async (
|
|||||||
|
|
||||||
const hasUpdatePermission = hasPermission(user, 'UPDATE_AGENTS')
|
const hasUpdatePermission = hasPermission(user, 'UPDATE_AGENTS')
|
||||||
|
|
||||||
return [
|
const columns = [
|
||||||
|
|
||||||
{
|
{
|
||||||
field: 'name',
|
field: 'name',
|
||||||
@ -204,4 +205,6 @@ export const loadColumns = async (
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
return makeReadableColumns(columns);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,30 +1,18 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { MenuAsideItem } from '../interfaces'
|
import { MenuAsideItem } from '../interfaces'
|
||||||
import AsideMenuLayer from './AsideMenuLayer'
|
import AsideMenuLayer from './AsideMenuLayer'
|
||||||
import OverlayLayer from './OverlayLayer'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
menu: MenuAsideItem[]
|
menu: MenuAsideItem[]
|
||||||
isAsideMobileExpanded: boolean
|
|
||||||
isAsideLgActive: boolean
|
|
||||||
onAsideLgClose: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AsideMenu({
|
export default function AsideMenu({
|
||||||
isAsideMobileExpanded = false,
|
|
||||||
isAsideLgActive = false,
|
|
||||||
...props
|
...props
|
||||||
}: Props) {
|
}: Props) {
|
||||||
return (
|
return (
|
||||||
<>
|
<AsideMenuLayer
|
||||||
<AsideMenuLayer
|
menu={props.menu}
|
||||||
menu={props.menu}
|
className="left-0"
|
||||||
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} />}
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { mdiClose } from '@mdi/js'
|
|
||||||
import BaseIcon from './BaseIcon'
|
|
||||||
import AsideMenuList from './AsideMenuList'
|
import AsideMenuList from './AsideMenuList'
|
||||||
import { MenuAsideItem } from '../interfaces'
|
import { MenuAsideItem } from '../interfaces'
|
||||||
import { useAppSelector } from '../stores/hooks'
|
import { useAppSelector } from '../stores/hooks'
|
||||||
@ -9,49 +7,36 @@ import { useAppSelector } from '../stores/hooks'
|
|||||||
type Props = {
|
type Props = {
|
||||||
menu: MenuAsideItem[]
|
menu: MenuAsideItem[]
|
||||||
className?: string
|
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 asideScrollbarsStyle = useAppSelector((state) => state.style.asideScrollbarsStyle)
|
||||||
const darkMode = useAppSelector((state) => state.style.darkMode)
|
const darkMode = useAppSelector((state) => state.style.darkMode)
|
||||||
|
|
||||||
const handleAsideLgCloseClick = (e: React.MouseEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
props.onAsideLgCloseClick()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
id='asideMenu'
|
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
|
<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
|
<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">
|
<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
|
AI Chat Workspace
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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>
|
||||||
<div
|
<div
|
||||||
className={`flex-1 overflow-y-auto overflow-x-hidden ${
|
className={`flex-1 overflow-y-auto overflow-x-hidden ${
|
||||||
darkMode ? 'aside-scrollbars-[slate]' : asideScrollbarsStyle
|
darkMode ? 'aside-scrollbars-[slate]' : asideScrollbarsStyle
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<AsideMenuList className="px-2 py-2.5" menu={menu} />
|
<AsideMenuList className="px-3 py-3" menu={menu} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import {loadColumns} from "./configureAttachmentsCols";
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import dataFormatter from '../../helpers/dataFormatter'
|
import dataFormatter from '../../helpers/dataFormatter'
|
||||||
import {dataGridStyles} from "../../styles";
|
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 focusRing = useAppSelector((state) => state.style.focusRingColor);
|
||||||
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
|
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
|
||||||
const corners = useAppSelector((state) => state.style.corners);
|
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);
|
const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage);
|
||||||
for (let i = 0; i < numPages; i++) {
|
for (let i = 0; i < numPages; i++) {
|
||||||
pagesList.push(i);
|
pagesList.push(i);
|
||||||
@ -211,7 +213,8 @@ const TableSampleAttachments = ({ filterItems, setFilterItems, filters, showGrid
|
|||||||
<div className='relative overflow-x-auto'>
|
<div className='relative overflow-x-auto'>
|
||||||
<DataGrid
|
<DataGrid
|
||||||
autoHeight
|
autoHeight
|
||||||
rowHeight={64}
|
rowHeight={56}
|
||||||
|
columnHeaderHeight={48}
|
||||||
sx={dataGridStyles}
|
sx={dataGridStyles}
|
||||||
className={'datagrid--table'}
|
className={'datagrid--table'}
|
||||||
getRowClassName={() => `datagrid--row`}
|
getRowClassName={() => `datagrid--row`}
|
||||||
@ -251,7 +254,10 @@ const TableSampleAttachments = ({ filterItems, setFilterItems, filters, showGrid
|
|||||||
rowCount={count}
|
rowCount={count}
|
||||||
pageSizeOptions={[10]}
|
pageSizeOptions={[10]}
|
||||||
paginationMode={'server'}
|
paginationMode={'server'}
|
||||||
loading={loading}
|
loading={isDataGridLoading}
|
||||||
|
slots={{
|
||||||
|
loadingOverlay: DataGridLoadingOverlay,
|
||||||
|
}}
|
||||||
onPaginationModelChange={(params) => {
|
onPaginationModelChange={(params) => {
|
||||||
onPageChange(params.page);
|
onPageChange(params.page);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {saveFile} from "../../helpers/fileSaver";
|
|||||||
import dataFormatter from '../../helpers/dataFormatter'
|
import dataFormatter from '../../helpers/dataFormatter'
|
||||||
import DataGridMultiSelect from "../DataGridMultiSelect";
|
import DataGridMultiSelect from "../DataGridMultiSelect";
|
||||||
import ListActionsPopover from '../ListActionsPopover';
|
import ListActionsPopover from '../ListActionsPopover';
|
||||||
|
import { makeReadableColumns } from '../../helpers/dataGridColumns';
|
||||||
|
|
||||||
import {hasPermission} from "../../helpers/userPermissions";
|
import {hasPermission} from "../../helpers/userPermissions";
|
||||||
|
|
||||||
@ -39,7 +40,7 @@ export const loadColumns = async (
|
|||||||
|
|
||||||
const hasUpdatePermission = hasPermission(user, 'UPDATE_ATTACHMENTS')
|
const hasUpdatePermission = hasPermission(user, 'UPDATE_ATTACHMENTS')
|
||||||
|
|
||||||
return [
|
const columns = [
|
||||||
|
|
||||||
{
|
{
|
||||||
field: 'message',
|
field: 'message',
|
||||||
@ -225,4 +226,6 @@ export const loadColumns = async (
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
return makeReadableColumns(columns);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import {loadColumns} from "./configureConversationsCols";
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import dataFormatter from '../../helpers/dataFormatter'
|
import dataFormatter from '../../helpers/dataFormatter'
|
||||||
import {dataGridStyles} from "../../styles";
|
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 focusRing = useAppSelector((state) => state.style.focusRingColor);
|
||||||
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
|
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
|
||||||
const corners = useAppSelector((state) => state.style.corners);
|
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);
|
const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage);
|
||||||
for (let i = 0; i < numPages; i++) {
|
for (let i = 0; i < numPages; i++) {
|
||||||
pagesList.push(i);
|
pagesList.push(i);
|
||||||
@ -211,7 +213,8 @@ const TableSampleConversations = ({ filterItems, setFilterItems, filters, showGr
|
|||||||
<div className='relative overflow-x-auto'>
|
<div className='relative overflow-x-auto'>
|
||||||
<DataGrid
|
<DataGrid
|
||||||
autoHeight
|
autoHeight
|
||||||
rowHeight={64}
|
rowHeight={56}
|
||||||
|
columnHeaderHeight={48}
|
||||||
sx={dataGridStyles}
|
sx={dataGridStyles}
|
||||||
className={'datagrid--table'}
|
className={'datagrid--table'}
|
||||||
getRowClassName={() => `datagrid--row`}
|
getRowClassName={() => `datagrid--row`}
|
||||||
@ -251,7 +254,10 @@ const TableSampleConversations = ({ filterItems, setFilterItems, filters, showGr
|
|||||||
rowCount={count}
|
rowCount={count}
|
||||||
pageSizeOptions={[10]}
|
pageSizeOptions={[10]}
|
||||||
paginationMode={'server'}
|
paginationMode={'server'}
|
||||||
loading={loading}
|
loading={isDataGridLoading}
|
||||||
|
slots={{
|
||||||
|
loadingOverlay: DataGridLoadingOverlay,
|
||||||
|
}}
|
||||||
onPaginationModelChange={(params) => {
|
onPaginationModelChange={(params) => {
|
||||||
onPageChange(params.page);
|
onPageChange(params.page);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {saveFile} from "../../helpers/fileSaver";
|
|||||||
import dataFormatter from '../../helpers/dataFormatter'
|
import dataFormatter from '../../helpers/dataFormatter'
|
||||||
import DataGridMultiSelect from "../DataGridMultiSelect";
|
import DataGridMultiSelect from "../DataGridMultiSelect";
|
||||||
import ListActionsPopover from '../ListActionsPopover';
|
import ListActionsPopover from '../ListActionsPopover';
|
||||||
|
import { makeReadableColumns } from '../../helpers/dataGridColumns';
|
||||||
|
|
||||||
import {hasPermission} from "../../helpers/userPermissions";
|
import {hasPermission} from "../../helpers/userPermissions";
|
||||||
|
|
||||||
@ -39,7 +40,7 @@ export const loadColumns = async (
|
|||||||
|
|
||||||
const hasUpdatePermission = hasPermission(user, 'UPDATE_CONVERSATIONS')
|
const hasUpdatePermission = hasPermission(user, 'UPDATE_CONVERSATIONS')
|
||||||
|
|
||||||
return [
|
const columns = [
|
||||||
|
|
||||||
{
|
{
|
||||||
field: 'user',
|
field: 'user',
|
||||||
@ -203,4 +204,6 @@ export const loadColumns = async (
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
return makeReadableColumns(columns);
|
||||||
};
|
};
|
||||||
|
|||||||
32
frontend/src/components/DataGridLoadingOverlay.tsx
Normal file
32
frontend/src/components/DataGridLoadingOverlay.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -12,13 +12,25 @@ type Props = {
|
|||||||
accept?: string;
|
accept?: string;
|
||||||
color: ColorButtonKey;
|
color: ColorButtonKey;
|
||||||
isRoundIcon?: boolean;
|
isRoundIcon?: boolean;
|
||||||
|
showFilename?: boolean;
|
||||||
path: string;
|
path: string;
|
||||||
schema: object;
|
schema: object;
|
||||||
field: any,
|
field: any,
|
||||||
form: 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 [file, setFile] = useState(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const corners = useAppSelector((state) => state.style.corners);
|
const corners = useAppSelector((state) => state.style.corners);
|
||||||
@ -51,14 +63,14 @@ const FormImagePicker = ({ label, icon, accept, color, isRoundIcon, path, schema
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const showFilename = !isRoundIcon && file;
|
const shouldShowFilename = Boolean(showFilename && !isRoundIcon && file);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex items-stretch justify-start relative'>
|
<div className='flex items-stretch justify-start relative'>
|
||||||
<label className='inline-flex'>
|
<label className='inline-flex'>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
className={`${isRoundIcon ? 'w-12 h-12' : ''} ${
|
className={`${isRoundIcon ? 'w-12 h-12' : ''} ${
|
||||||
showFilename ? 'rounded-r-none' : ''
|
shouldShowFilename ? 'rounded-r-none' : ''
|
||||||
}`}
|
}`}
|
||||||
iconSize={isRoundIcon ? 24 : undefined}
|
iconSize={isRoundIcon ? 24 : undefined}
|
||||||
label={isRoundIcon ? null : label}
|
label={isRoundIcon ? null : label}
|
||||||
@ -76,7 +88,7 @@ const FormImagePicker = ({ label, icon, accept, color, isRoundIcon, path, schema
|
|||||||
disabled={loading}
|
disabled={loading}
|
||||||
/>
|
/>
|
||||||
</label>
|
</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 `}>
|
<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'>
|
<span className='text-ellipsis max-w-full line-clamp-1'>
|
||||||
{file.name}
|
{file.name}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import {loadColumns} from "./configureMessagesCols";
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import dataFormatter from '../../helpers/dataFormatter'
|
import dataFormatter from '../../helpers/dataFormatter'
|
||||||
import {dataGridStyles} from "../../styles";
|
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 focusRing = useAppSelector((state) => state.style.focusRingColor);
|
||||||
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
|
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
|
||||||
const corners = useAppSelector((state) => state.style.corners);
|
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);
|
const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage);
|
||||||
for (let i = 0; i < numPages; i++) {
|
for (let i = 0; i < numPages; i++) {
|
||||||
pagesList.push(i);
|
pagesList.push(i);
|
||||||
@ -211,7 +213,8 @@ const TableSampleMessages = ({ filterItems, setFilterItems, filters, showGrid })
|
|||||||
<div className='relative overflow-x-auto'>
|
<div className='relative overflow-x-auto'>
|
||||||
<DataGrid
|
<DataGrid
|
||||||
autoHeight
|
autoHeight
|
||||||
rowHeight={64}
|
rowHeight={56}
|
||||||
|
columnHeaderHeight={48}
|
||||||
sx={dataGridStyles}
|
sx={dataGridStyles}
|
||||||
className={'datagrid--table'}
|
className={'datagrid--table'}
|
||||||
getRowClassName={() => `datagrid--row`}
|
getRowClassName={() => `datagrid--row`}
|
||||||
@ -251,7 +254,10 @@ const TableSampleMessages = ({ filterItems, setFilterItems, filters, showGrid })
|
|||||||
rowCount={count}
|
rowCount={count}
|
||||||
pageSizeOptions={[10]}
|
pageSizeOptions={[10]}
|
||||||
paginationMode={'server'}
|
paginationMode={'server'}
|
||||||
loading={loading}
|
loading={isDataGridLoading}
|
||||||
|
slots={{
|
||||||
|
loadingOverlay: DataGridLoadingOverlay,
|
||||||
|
}}
|
||||||
onPaginationModelChange={(params) => {
|
onPaginationModelChange={(params) => {
|
||||||
onPageChange(params.page);
|
onPageChange(params.page);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {saveFile} from "../../helpers/fileSaver";
|
|||||||
import dataFormatter from '../../helpers/dataFormatter'
|
import dataFormatter from '../../helpers/dataFormatter'
|
||||||
import DataGridMultiSelect from "../DataGridMultiSelect";
|
import DataGridMultiSelect from "../DataGridMultiSelect";
|
||||||
import ListActionsPopover from '../ListActionsPopover';
|
import ListActionsPopover from '../ListActionsPopover';
|
||||||
|
import { makeReadableColumns } from '../../helpers/dataGridColumns';
|
||||||
|
|
||||||
import {hasPermission} from "../../helpers/userPermissions";
|
import {hasPermission} from "../../helpers/userPermissions";
|
||||||
|
|
||||||
@ -39,7 +40,7 @@ export const loadColumns = async (
|
|||||||
|
|
||||||
const hasUpdatePermission = hasPermission(user, 'UPDATE_MESSAGES')
|
const hasUpdatePermission = hasPermission(user, 'UPDATE_MESSAGES')
|
||||||
|
|
||||||
return [
|
const columns = [
|
||||||
|
|
||||||
{
|
{
|
||||||
field: 'conversation',
|
field: 'conversation',
|
||||||
@ -266,4 +267,6 @@ export const loadColumns = async (
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
return makeReadableColumns(columns);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 { mdiClose, mdiDotsVertical } from '@mdi/js'
|
||||||
import { containerMaxW } from '../config'
|
import { containerMaxW } from '../config'
|
||||||
import BaseIcon from './BaseIcon'
|
import BaseIcon from './BaseIcon'
|
||||||
@ -12,9 +12,10 @@ type Props = {
|
|||||||
className: string
|
className: string
|
||||||
contentClassName?: string
|
contentClassName?: string
|
||||||
children: ReactNode
|
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 [isMenuNavBarActive, setIsMenuNavBarActive] = useState(false)
|
||||||
const [isScrolled, setIsScrolled] = useState(false);
|
const [isScrolled, setIsScrolled] = useState(false);
|
||||||
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
|
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
|
||||||
@ -37,7 +38,8 @@ export default function NavBar({ menu, className = '', contentClassName = contai
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<nav
|
<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 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>
|
<div className="flex flex-1 items-stretch h-12">{children}</div>
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import {loadColumns} from "./configurePermissionsCols";
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import dataFormatter from '../../helpers/dataFormatter'
|
import dataFormatter from '../../helpers/dataFormatter'
|
||||||
import {dataGridStyles} from "../../styles";
|
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 focusRing = useAppSelector((state) => state.style.focusRingColor);
|
||||||
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
|
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
|
||||||
const corners = useAppSelector((state) => state.style.corners);
|
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);
|
const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage);
|
||||||
for (let i = 0; i < numPages; i++) {
|
for (let i = 0; i < numPages; i++) {
|
||||||
pagesList.push(i);
|
pagesList.push(i);
|
||||||
@ -211,7 +213,8 @@ const TableSamplePermissions = ({ filterItems, setFilterItems, filters, showGrid
|
|||||||
<div className='relative overflow-x-auto'>
|
<div className='relative overflow-x-auto'>
|
||||||
<DataGrid
|
<DataGrid
|
||||||
autoHeight
|
autoHeight
|
||||||
rowHeight={64}
|
rowHeight={56}
|
||||||
|
columnHeaderHeight={48}
|
||||||
sx={dataGridStyles}
|
sx={dataGridStyles}
|
||||||
className={'datagrid--table'}
|
className={'datagrid--table'}
|
||||||
getRowClassName={() => `datagrid--row`}
|
getRowClassName={() => `datagrid--row`}
|
||||||
@ -251,7 +254,10 @@ const TableSamplePermissions = ({ filterItems, setFilterItems, filters, showGrid
|
|||||||
rowCount={count}
|
rowCount={count}
|
||||||
pageSizeOptions={[10]}
|
pageSizeOptions={[10]}
|
||||||
paginationMode={'server'}
|
paginationMode={'server'}
|
||||||
loading={loading}
|
loading={isDataGridLoading}
|
||||||
|
slots={{
|
||||||
|
loadingOverlay: DataGridLoadingOverlay,
|
||||||
|
}}
|
||||||
onPaginationModelChange={(params) => {
|
onPaginationModelChange={(params) => {
|
||||||
onPageChange(params.page);
|
onPageChange(params.page);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {saveFile} from "../../helpers/fileSaver";
|
|||||||
import dataFormatter from '../../helpers/dataFormatter'
|
import dataFormatter from '../../helpers/dataFormatter'
|
||||||
import DataGridMultiSelect from "../DataGridMultiSelect";
|
import DataGridMultiSelect from "../DataGridMultiSelect";
|
||||||
import ListActionsPopover from '../ListActionsPopover';
|
import ListActionsPopover from '../ListActionsPopover';
|
||||||
|
import { makeReadableColumns } from '../../helpers/dataGridColumns';
|
||||||
|
|
||||||
import {hasPermission} from "../../helpers/userPermissions";
|
import {hasPermission} from "../../helpers/userPermissions";
|
||||||
|
|
||||||
@ -39,7 +40,7 @@ export const loadColumns = async (
|
|||||||
|
|
||||||
const hasUpdatePermission = hasPermission(user, 'UPDATE_PERMISSIONS')
|
const hasUpdatePermission = hasPermission(user, 'UPDATE_PERMISSIONS')
|
||||||
|
|
||||||
return [
|
const columns = [
|
||||||
|
|
||||||
{
|
{
|
||||||
field: 'name',
|
field: 'name',
|
||||||
@ -80,4 +81,6 @@ export const loadColumns = async (
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
return makeReadableColumns(columns);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import {loadColumns} from "./configureRolesCols";
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import dataFormatter from '../../helpers/dataFormatter'
|
import dataFormatter from '../../helpers/dataFormatter'
|
||||||
import {dataGridStyles} from "../../styles";
|
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 focusRing = useAppSelector((state) => state.style.focusRingColor);
|
||||||
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
|
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
|
||||||
const corners = useAppSelector((state) => state.style.corners);
|
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);
|
const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage);
|
||||||
for (let i = 0; i < numPages; i++) {
|
for (let i = 0; i < numPages; i++) {
|
||||||
pagesList.push(i);
|
pagesList.push(i);
|
||||||
@ -211,7 +213,8 @@ const TableSampleRoles = ({ filterItems, setFilterItems, filters, showGrid }) =>
|
|||||||
<div id="rolesTable" className='relative overflow-x-auto'>
|
<div id="rolesTable" className='relative overflow-x-auto'>
|
||||||
<DataGrid
|
<DataGrid
|
||||||
autoHeight
|
autoHeight
|
||||||
rowHeight={64}
|
rowHeight={56}
|
||||||
|
columnHeaderHeight={48}
|
||||||
sx={dataGridStyles}
|
sx={dataGridStyles}
|
||||||
className={'datagrid--table'}
|
className={'datagrid--table'}
|
||||||
getRowClassName={() => `datagrid--row`}
|
getRowClassName={() => `datagrid--row`}
|
||||||
@ -251,7 +254,10 @@ const TableSampleRoles = ({ filterItems, setFilterItems, filters, showGrid }) =>
|
|||||||
rowCount={count}
|
rowCount={count}
|
||||||
pageSizeOptions={[10]}
|
pageSizeOptions={[10]}
|
||||||
paginationMode={'server'}
|
paginationMode={'server'}
|
||||||
loading={loading}
|
loading={isDataGridLoading}
|
||||||
|
slots={{
|
||||||
|
loadingOverlay: DataGridLoadingOverlay,
|
||||||
|
}}
|
||||||
onPaginationModelChange={(params) => {
|
onPaginationModelChange={(params) => {
|
||||||
onPageChange(params.page);
|
onPageChange(params.page);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {saveFile} from "../../helpers/fileSaver";
|
|||||||
import dataFormatter from '../../helpers/dataFormatter'
|
import dataFormatter from '../../helpers/dataFormatter'
|
||||||
import DataGridMultiSelect from "../DataGridMultiSelect";
|
import DataGridMultiSelect from "../DataGridMultiSelect";
|
||||||
import ListActionsPopover from '../ListActionsPopover';
|
import ListActionsPopover from '../ListActionsPopover';
|
||||||
|
import { makeReadableColumns } from '../../helpers/dataGridColumns';
|
||||||
|
|
||||||
import {hasPermission} from "../../helpers/userPermissions";
|
import {hasPermission} from "../../helpers/userPermissions";
|
||||||
|
|
||||||
@ -39,7 +40,7 @@ export const loadColumns = async (
|
|||||||
|
|
||||||
const hasUpdatePermission = hasPermission(user, 'UPDATE_ROLES')
|
const hasUpdatePermission = hasPermission(user, 'UPDATE_ROLES')
|
||||||
|
|
||||||
return [
|
const columns = [
|
||||||
|
|
||||||
{
|
{
|
||||||
field: 'name',
|
field: 'name',
|
||||||
@ -100,4 +101,6 @@ export const loadColumns = async (
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
return makeReadableColumns(columns);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import {loadColumns} from "./configureUsage_eventsCols";
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import dataFormatter from '../../helpers/dataFormatter'
|
import dataFormatter from '../../helpers/dataFormatter'
|
||||||
import {dataGridStyles} from "../../styles";
|
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 focusRing = useAppSelector((state) => state.style.focusRingColor);
|
||||||
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
|
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
|
||||||
const corners = useAppSelector((state) => state.style.corners);
|
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);
|
const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage);
|
||||||
for (let i = 0; i < numPages; i++) {
|
for (let i = 0; i < numPages; i++) {
|
||||||
pagesList.push(i);
|
pagesList.push(i);
|
||||||
@ -211,7 +213,8 @@ const TableSampleUsage_events = ({ filterItems, setFilterItems, filters, showGri
|
|||||||
<div className='relative overflow-x-auto'>
|
<div className='relative overflow-x-auto'>
|
||||||
<DataGrid
|
<DataGrid
|
||||||
autoHeight
|
autoHeight
|
||||||
rowHeight={64}
|
rowHeight={56}
|
||||||
|
columnHeaderHeight={48}
|
||||||
sx={dataGridStyles}
|
sx={dataGridStyles}
|
||||||
className={'datagrid--table'}
|
className={'datagrid--table'}
|
||||||
getRowClassName={() => `datagrid--row`}
|
getRowClassName={() => `datagrid--row`}
|
||||||
@ -251,7 +254,10 @@ const TableSampleUsage_events = ({ filterItems, setFilterItems, filters, showGri
|
|||||||
rowCount={count}
|
rowCount={count}
|
||||||
pageSizeOptions={[10]}
|
pageSizeOptions={[10]}
|
||||||
paginationMode={'server'}
|
paginationMode={'server'}
|
||||||
loading={loading}
|
loading={isDataGridLoading}
|
||||||
|
slots={{
|
||||||
|
loadingOverlay: DataGridLoadingOverlay,
|
||||||
|
}}
|
||||||
onPaginationModelChange={(params) => {
|
onPaginationModelChange={(params) => {
|
||||||
onPageChange(params.page);
|
onPageChange(params.page);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {saveFile} from "../../helpers/fileSaver";
|
|||||||
import dataFormatter from '../../helpers/dataFormatter'
|
import dataFormatter from '../../helpers/dataFormatter'
|
||||||
import DataGridMultiSelect from "../DataGridMultiSelect";
|
import DataGridMultiSelect from "../DataGridMultiSelect";
|
||||||
import ListActionsPopover from '../ListActionsPopover';
|
import ListActionsPopover from '../ListActionsPopover';
|
||||||
|
import { makeReadableColumns } from '../../helpers/dataGridColumns';
|
||||||
|
|
||||||
import {hasPermission} from "../../helpers/userPermissions";
|
import {hasPermission} from "../../helpers/userPermissions";
|
||||||
|
|
||||||
@ -39,7 +40,7 @@ export const loadColumns = async (
|
|||||||
|
|
||||||
const hasUpdatePermission = hasPermission(user, 'UPDATE_USAGE_EVENTS')
|
const hasUpdatePermission = hasPermission(user, 'UPDATE_USAGE_EVENTS')
|
||||||
|
|
||||||
return [
|
const columns = [
|
||||||
|
|
||||||
{
|
{
|
||||||
field: 'user',
|
field: 'user',
|
||||||
@ -295,4 +296,6 @@ export const loadColumns = async (
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
return makeReadableColumns(columns);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import {loadColumns} from "./configureUsersCols";
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import dataFormatter from '../../helpers/dataFormatter'
|
import dataFormatter from '../../helpers/dataFormatter'
|
||||||
import {dataGridStyles} from "../../styles";
|
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 focusRing = useAppSelector((state) => state.style.focusRingColor);
|
||||||
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
|
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
|
||||||
const corners = useAppSelector((state) => state.style.corners);
|
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);
|
const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage);
|
||||||
for (let i = 0; i < numPages; i++) {
|
for (let i = 0; i < numPages; i++) {
|
||||||
pagesList.push(i);
|
pagesList.push(i);
|
||||||
@ -211,7 +213,8 @@ const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) =>
|
|||||||
<div id="usersTable" className='relative overflow-x-auto'>
|
<div id="usersTable" className='relative overflow-x-auto'>
|
||||||
<DataGrid
|
<DataGrid
|
||||||
autoHeight
|
autoHeight
|
||||||
rowHeight={64}
|
rowHeight={56}
|
||||||
|
columnHeaderHeight={48}
|
||||||
sx={dataGridStyles}
|
sx={dataGridStyles}
|
||||||
className={'datagrid--table'}
|
className={'datagrid--table'}
|
||||||
getRowClassName={() => `datagrid--row`}
|
getRowClassName={() => `datagrid--row`}
|
||||||
@ -251,7 +254,10 @@ const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) =>
|
|||||||
rowCount={count}
|
rowCount={count}
|
||||||
pageSizeOptions={[10]}
|
pageSizeOptions={[10]}
|
||||||
paginationMode={'server'}
|
paginationMode={'server'}
|
||||||
loading={loading}
|
loading={isDataGridLoading}
|
||||||
|
slots={{
|
||||||
|
loadingOverlay: DataGridLoadingOverlay,
|
||||||
|
}}
|
||||||
onPaginationModelChange={(params) => {
|
onPaginationModelChange={(params) => {
|
||||||
onPageChange(params.page);
|
onPageChange(params.page);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {saveFile} from "../../helpers/fileSaver";
|
|||||||
import dataFormatter from '../../helpers/dataFormatter'
|
import dataFormatter from '../../helpers/dataFormatter'
|
||||||
import DataGridMultiSelect from "../DataGridMultiSelect";
|
import DataGridMultiSelect from "../DataGridMultiSelect";
|
||||||
import ListActionsPopover from '../ListActionsPopover';
|
import ListActionsPopover from '../ListActionsPopover';
|
||||||
|
import { makeReadableColumns } from '../../helpers/dataGridColumns';
|
||||||
|
|
||||||
import {hasPermission} from "../../helpers/userPermissions";
|
import {hasPermission} from "../../helpers/userPermissions";
|
||||||
|
|
||||||
@ -39,7 +40,7 @@ export const loadColumns = async (
|
|||||||
|
|
||||||
const hasUpdatePermission = hasPermission(user, 'UPDATE_USERS')
|
const hasUpdatePermission = hasPermission(user, 'UPDATE_USERS')
|
||||||
|
|
||||||
return [
|
const columns = [
|
||||||
|
|
||||||
{
|
{
|
||||||
field: 'firstName',
|
field: 'firstName',
|
||||||
@ -204,4 +205,6 @@ export const loadColumns = async (
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
return makeReadableColumns(columns);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import { useRouter } from 'next/router';
|
|||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import BaseIcon from '../BaseIcon';
|
import BaseIcon from '../BaseIcon';
|
||||||
import { hasPermission } from '../../helpers/userPermissions';
|
import { ADMIN_ENTRY_PERMISSIONS, hasPermission } from '../../helpers/userPermissions';
|
||||||
import { useAppSelector } from '../../stores/hooks';
|
import { useAppSelector } from '../../stores/hooks';
|
||||||
import ChatMarkdown from './ChatMarkdown';
|
import ChatMarkdown from './ChatMarkdown';
|
||||||
|
|
||||||
@ -474,7 +474,7 @@ export default function WorkspaceShell() {
|
|||||||
const currentUserName = currentUser?.firstName || currentUser?.email || 'You';
|
const currentUserName = currentUser?.firstName || currentUser?.email || 'You';
|
||||||
const currentUserAvatarUrl = getAvatarUrl(currentUser?.avatar);
|
const currentUserAvatarUrl = getAvatarUrl(currentUser?.avatar);
|
||||||
const currentUserInitial = getUserInitial(currentUser);
|
const currentUserInitial = getUserInitial(currentUser);
|
||||||
const canAccessAdmin = hasPermission(currentUser, 'READ_USERS');
|
const canAccessAdmin = hasPermission(currentUser, ADMIN_ENTRY_PERMISSIONS);
|
||||||
|
|
||||||
const activeConversations = useMemo(
|
const activeConversations = useMemo(
|
||||||
() => conversations.filter((conversation) => conversation.status !== 'archived'),
|
() => conversations.filter((conversation) => conversation.status !== 'archived'),
|
||||||
|
|||||||
@ -3,7 +3,7 @@ html {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply pt-12 xl:pl-64 h-full;
|
@apply h-full;
|
||||||
}
|
}
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
|
|||||||
@ -71,36 +71,47 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.datagrid--table, .MuiDataGrid-root {
|
.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 {
|
.datagrid--header {
|
||||||
@apply uppercase !important;
|
@apply text-[11px] font-semibold tracking-[0.14em] text-slate-400 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.datagrid--header,
|
.datagrid--header,
|
||||||
.datagrid--header .MuiIconButton-root,
|
.datagrid--header .MuiIconButton-root,
|
||||||
.datagrid--cell,
|
.datagrid--cell,
|
||||||
.datagrid--cell .MuiIconButton-root {
|
.datagrid--cell .MuiIconButton-root {
|
||||||
@apply dark:text-white;
|
@apply text-slate-700 dark:text-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.datagrid--cell .MuiDataGrid-booleanCell {
|
.datagrid--cell .MuiDataGrid-booleanCell {
|
||||||
@apply dark:text-white !important;
|
@apply text-slate-700 dark:text-white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.datagrid--cell .MuiIconButton-root:hover {
|
.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 {
|
.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 {
|
.datagrid--table .MuiTablePagination-root {
|
||||||
@apply dark:text-white;
|
@apply text-[13px] text-slate-500 dark:text-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.datagrid--table .MuiTablePagination-root .MuiButtonBase-root:disabled {
|
.datagrid--table .MuiTablePagination-root .MuiButtonBase-root:disabled {
|
||||||
@ -114,4 +125,15 @@
|
|||||||
.MuiButton-colorInherit {
|
.MuiButton-colorInherit {
|
||||||
@apply text-blue-600 dark:text-dark-700 !important;
|
@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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
243
frontend/src/helpers/dataGridColumns.tsx
Normal file
243
frontend/src/helpers/dataGridColumns.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -3,9 +3,13 @@ export function humanize(str: string) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return str.toString()
|
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_]+|[\s_]+$/g, '')
|
||||||
.replace(/[_\s]+/g, ' ')
|
.replace(/[_\s]+/g, ' ')
|
||||||
|
.replace(/\bjson\b/gi, 'JSON')
|
||||||
|
.replace(/\bid\b/gi, 'ID')
|
||||||
.replace(/^[a-z]/, function (m) {
|
.replace(/^[a-z]/, function (m) {
|
||||||
return m.toUpperCase();
|
return m.toUpperCase();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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[]) {
|
export function hasPermission(user, permission_name: string | string[]) {
|
||||||
if (!user?.app_role?.name) return false;
|
if (!user?.app_role?.name) return false;
|
||||||
if (!permission_name) {
|
if (!permission_name) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isAdministrator = user.app_role.name === 'Administrator';
|
||||||
const permissions = new Set<string>([
|
const permissions = new Set<string>([
|
||||||
...(user?.custom_permissions ?? []).map((p) => p.name),
|
...(user?.custom_permissions ?? []).map((p) => p.name),
|
||||||
...(user?.app_role_permissions ?? []).map((p) => p.name),
|
...(user?.app_role_permissions ?? []).map((p) => p.name),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (typeof permission_name === 'string') {
|
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 {
|
} 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));
|
return permission_name.some((permission) => permissions.has(permission));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,6 @@
|
|||||||
import React, { ReactNode, useEffect, useRef, useState } from 'react'
|
import React, { ReactNode, useEffect, useRef, useState } from 'react'
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import {
|
import {
|
||||||
mdiForwardburger,
|
|
||||||
mdiBackburger,
|
|
||||||
mdiMenu,
|
|
||||||
mdiChevronDown,
|
mdiChevronDown,
|
||||||
mdiCogOutline,
|
mdiCogOutline,
|
||||||
mdiLogout,
|
mdiLogout,
|
||||||
@ -11,7 +8,6 @@ import {
|
|||||||
import menuAside from '../menuAside'
|
import menuAside from '../menuAside'
|
||||||
import BaseIcon from '../components/BaseIcon'
|
import BaseIcon from '../components/BaseIcon'
|
||||||
import NavBar from '../components/NavBar'
|
import NavBar from '../components/NavBar'
|
||||||
import NavBarItemPlain from '../components/NavBarItemPlain'
|
|
||||||
import AsideMenu from '../components/AsideMenu'
|
import AsideMenu from '../components/AsideMenu'
|
||||||
import FooterBar from '../components/FooterBar'
|
import FooterBar from '../components/FooterBar'
|
||||||
import { useAppDispatch, useAppSelector } from '../stores/hooks'
|
import { useAppDispatch, useAppSelector } from '../stores/hooks'
|
||||||
@ -26,7 +22,7 @@ import {hasPermission} from "../helpers/userPermissions";
|
|||||||
type Props = {
|
type Props = {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
|
|
||||||
permission?: string
|
permission?: string | string[]
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,33 +56,50 @@ export default function LayoutAuthenticated({
|
|||||||
permission
|
permission
|
||||||
|
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const asideWidth = 256
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { token, currentUser } = useAppSelector((state) => state.auth)
|
const { token, currentUser } = useAppSelector((state) => state.auth)
|
||||||
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
|
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
|
||||||
const isWorkspaceRoute = router.pathname.startsWith('/workspace');
|
const isWorkspaceRoute = router.pathname.startsWith('/workspace');
|
||||||
let localToken
|
const [localToken, setLocalToken] = useState<string | null>(null)
|
||||||
if (typeof window !== 'undefined') {
|
const effectiveToken = token || localToken || ''
|
||||||
// Perform localStorage action
|
const isAuthBootstrapped = Boolean(token) || localToken !== null
|
||||||
localToken = localStorage.getItem('token')
|
|
||||||
}
|
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 date = new Date().getTime() / 1000;
|
||||||
const data = jwt.decode(token);
|
const data = jwt.decode(value);
|
||||||
if (!data) return;
|
if (!data) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return date < data.exp;
|
return date < data.exp;
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(findMe());
|
if (typeof window === 'undefined') {
|
||||||
if (!isTokenValid()) {
|
return;
|
||||||
dispatch(logoutUser());
|
|
||||||
router.push('/login');
|
|
||||||
}
|
}
|
||||||
}, [token, localToken]);
|
|
||||||
|
setLocalToken(localStorage.getItem('token') || '')
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!effectiveToken) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isTokenValid(effectiveToken)) {
|
||||||
|
dispatch(logoutUser());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(findMe());
|
||||||
|
}, [dispatch, effectiveToken]);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -98,8 +111,6 @@ export default function LayoutAuthenticated({
|
|||||||
|
|
||||||
const darkMode = useAppSelector((state) => state.style.darkMode)
|
const darkMode = useAppSelector((state) => state.style.darkMode)
|
||||||
|
|
||||||
const [isAsideMobileExpanded, setIsAsideMobileExpanded] = useState(false)
|
|
||||||
const [isAsideLgActive, setIsAsideLgActive] = useState(false)
|
|
||||||
const [isWorkspaceAccountMenuOpen, setIsWorkspaceAccountMenuOpen] = useState(false)
|
const [isWorkspaceAccountMenuOpen, setIsWorkspaceAccountMenuOpen] = useState(false)
|
||||||
const workspaceAccountMenuButtonRef = useRef(null)
|
const workspaceAccountMenuButtonRef = useRef(null)
|
||||||
const currentUserAvatarUrl = getAvatarUrl(currentUser?.avatar)
|
const currentUserAvatarUrl = getAvatarUrl(currentUser?.avatar)
|
||||||
@ -107,8 +118,6 @@ export default function LayoutAuthenticated({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleRouteChangeStart = () => {
|
const handleRouteChangeStart = () => {
|
||||||
setIsAsideMobileExpanded(false)
|
|
||||||
setIsAsideLgActive(false)
|
|
||||||
setIsWorkspaceAccountMenuOpen(false)
|
setIsWorkspaceAccountMenuOpen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,44 +130,88 @@ export default function LayoutAuthenticated({
|
|||||||
}
|
}
|
||||||
}, [router.events, dispatch])
|
}, [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'
|
if (!isAuthBootstrapped) {
|
||||||
const layoutOffsetClass = isWorkspaceRoute ? '' : isAsideMobileExpanded ? 'ml-64 lg:ml-0' : ''
|
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 (
|
return (
|
||||||
<div className={`${darkMode ? 'dark' : ''} overflow-hidden lg:overflow-visible`}>
|
<div className={`${darkMode ? 'dark' : ''} overflow-hidden lg:overflow-visible`}>
|
||||||
<div
|
<div
|
||||||
className={`${layoutAsidePadding} ${layoutOffsetClass} pt-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
|
<NavBar
|
||||||
menu={[]}
|
menu={[]}
|
||||||
className={`${layoutAsidePadding} ${layoutOffsetClass}`}
|
className=""
|
||||||
contentClassName="w-full"
|
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 flex-1 items-center justify-between px-3 sm:px-5">
|
||||||
<div className="flex min-w-0 items-center">
|
<div className="flex min-w-0 items-center">
|
||||||
{!isWorkspaceRoute && (
|
{isWorkspaceRoute && (
|
||||||
<>
|
<Link
|
||||||
<NavBarItemPlain
|
className="truncate text-[14px] font-semibold tracking-[-0.02em] text-slate-700 dark:text-slate-100"
|
||||||
display="flex lg:hidden"
|
href="/workspace"
|
||||||
onClick={() => setIsAsideMobileExpanded(!isAsideMobileExpanded)}
|
>
|
||||||
>
|
AI Chat Workspace
|
||||||
<BaseIcon path={isAsideMobileExpanded ? mdiBackburger : mdiForwardburger} size="24" />
|
</Link>
|
||||||
</NavBarItemPlain>
|
|
||||||
<NavBarItemPlain
|
|
||||||
display="hidden lg:flex xl:hidden"
|
|
||||||
onClick={() => setIsAsideLgActive(true)}
|
|
||||||
>
|
|
||||||
<BaseIcon path={mdiMenu} size="24" />
|
|
||||||
</NavBarItemPlain>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
<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>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<button
|
||||||
@ -221,10 +274,7 @@ export default function LayoutAuthenticated({
|
|||||||
</NavBar>
|
</NavBar>
|
||||||
{!isWorkspaceRoute && (
|
{!isWorkspaceRoute && (
|
||||||
<AsideMenu
|
<AsideMenu
|
||||||
isAsideMobileExpanded={isAsideMobileExpanded}
|
|
||||||
isAsideLgActive={isAsideLgActive}
|
|
||||||
menu={menuAside}
|
menu={menuAside}
|
||||||
onAsideLgClose={() => setIsAsideLgActive(false)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import * as icon from '@mdi/js';
|
import * as icon from '@mdi/js';
|
||||||
import { MenuAsideItem } from './interfaces'
|
import { MenuAsideItem } from './interfaces'
|
||||||
|
import { ADMIN_ENTRY_PERMISSIONS } from './helpers/userPermissions'
|
||||||
|
|
||||||
const backofficeMenu: MenuAsideItem[] = [
|
const backofficeMenu: MenuAsideItem[] = [
|
||||||
{
|
{
|
||||||
@ -83,7 +84,7 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
label: 'Admin',
|
label: 'Admin',
|
||||||
icon: icon.mdiCogOutline,
|
icon: icon.mdiCogOutline,
|
||||||
menu: backofficeMenu,
|
menu: backofficeMenu,
|
||||||
permissions: 'READ_USERS'
|
permissions: ADMIN_ENTRY_PERMISSIONS
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { store } from '../stores/store';
|
|||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import '../css/main.css';
|
import '../css/main.css';
|
||||||
import 'react-toastify/dist/ReactToastify.css';
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
import { ToastContainer } from 'react-toastify';
|
import { ToastContainer, toast } from 'react-toastify';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { baseURLApi } from '../config';
|
import { baseURLApi } from '../config';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
@ -18,6 +18,7 @@ import { appWithTranslation } from 'next-i18next';
|
|||||||
import '../i18n';
|
import '../i18n';
|
||||||
import IntroGuide from '../components/IntroGuide';
|
import IntroGuide from '../components/IntroGuide';
|
||||||
import { appSteps, loginSteps, usersSteps, rolesSteps } from '../stores/introSteps';
|
import { appSteps, loginSteps, usersSteps, rolesSteps } from '../stores/introSteps';
|
||||||
|
import { logoutUser } from '../stores/authSlice';
|
||||||
|
|
||||||
// Initialize axios
|
// Initialize axios
|
||||||
axios.defaults.baseURL = process.env.NEXT_PUBLIC_BACK_API
|
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 [stepsEnabled, setStepsEnabled] = React.useState(false);
|
||||||
const [stepName, setStepName] = React.useState('');
|
const [stepName, setStepName] = React.useState('');
|
||||||
const [steps, setSteps] = React.useState([]);
|
const [steps, setSteps] = React.useState([]);
|
||||||
|
const unauthorizedToastLock = React.useRef(false);
|
||||||
|
|
||||||
axios.interceptors.request.use(
|
React.useEffect(() => {
|
||||||
config => {
|
const requestInterceptor = axios.interceptors.request.use(
|
||||||
const token = localStorage.getItem('token');
|
(config) => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
} else {
|
} else {
|
||||||
delete config.headers.Authorization;
|
delete config.headers.Authorization;
|
||||||
}
|
|
||||||
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// TODO: Remove this code in future releases
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const allowedOrigin = (() => {
|
const allowedOrigin = (() => {
|
||||||
|
|||||||
@ -18,7 +18,7 @@ import type { ReactElement } from 'react';
|
|||||||
import BaseIcon from '../components/BaseIcon';
|
import BaseIcon from '../components/BaseIcon';
|
||||||
import SectionMain from '../components/SectionMain';
|
import SectionMain from '../components/SectionMain';
|
||||||
import { getPageTitle } from '../config';
|
import { getPageTitle } from '../config';
|
||||||
import { hasPermission } from '../helpers/userPermissions';
|
import { ADMIN_ENTRY_PERMISSIONS, hasPermission } from '../helpers/userPermissions';
|
||||||
import LayoutAuthenticated from '../layouts/Authenticated';
|
import LayoutAuthenticated from '../layouts/Authenticated';
|
||||||
import { useAppSelector } from '../stores/hooks';
|
import { useAppSelector } from '../stores/hooks';
|
||||||
|
|
||||||
@ -283,7 +283,7 @@ function Dashboard() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Dashboard.getLayout = function getLayout(page: ReactElement) {
|
Dashboard.getLayout = function getLayout(page: ReactElement) {
|
||||||
return <LayoutAuthenticated permission="READ_USERS">{page}</LayoutAuthenticated>;
|
return <LayoutAuthenticated permission={ADMIN_ENTRY_PERMISSIONS}>{page}</LayoutAuthenticated>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Dashboard;
|
export default Dashboard;
|
||||||
|
|||||||
@ -94,7 +94,7 @@ const ProfilePage = () => {
|
|||||||
<title>{getPageTitle('Profile')}</title>
|
<title>{getPageTitle('Profile')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<SectionMain>
|
<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">
|
<div className="rounded-[12px] border border-slate-200 bg-white px-6 py-6">
|
||||||
<Link
|
<Link
|
||||||
className="inline-flex items-center gap-2 text-[12px] font-medium text-slate-500"
|
className="inline-flex items-center gap-2 text-[12px] font-medium text-slate-500"
|
||||||
|
|||||||
@ -13,13 +13,16 @@ import BaseIcon from '../components/BaseIcon';
|
|||||||
import LayoutAuthenticated from '../layouts/Authenticated';
|
import LayoutAuthenticated from '../layouts/Authenticated';
|
||||||
import SectionMain from '../components/SectionMain';
|
import SectionMain from '../components/SectionMain';
|
||||||
import { getPageTitle } from '../config';
|
import { getPageTitle } from '../config';
|
||||||
import { hasPermission } from '../helpers/userPermissions';
|
import { ADMIN_ENTRY_PERMISSIONS, hasPermission } from '../helpers/userPermissions';
|
||||||
import { useAppSelector } from '../stores/hooks';
|
import { useAppSelector } from '../stores/hooks';
|
||||||
|
|
||||||
function SettingsPage() {
|
function SettingsPage() {
|
||||||
const { currentUser } = useAppSelector((state) => state.auth);
|
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 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -27,7 +30,7 @@ function SettingsPage() {
|
|||||||
<title>{getPageTitle('Settings')}</title>
|
<title>{getPageTitle('Settings')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<SectionMain>
|
<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">
|
<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">
|
<p className="text-[11px] font-medium uppercase tracking-[0.28em] text-slate-400">
|
||||||
Settings
|
Settings
|
||||||
@ -92,9 +95,10 @@ function SettingsPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{canAccessApiDocs && (
|
{canAccessApiDocs && (
|
||||||
<Link
|
<a
|
||||||
className="group rounded-[12px] border border-slate-200 bg-white px-5 py-5"
|
className="group rounded-[12px] border border-slate-200 bg-white px-5 py-5"
|
||||||
href="/api-docs"
|
href={apiDocsHref}
|
||||||
|
rel="noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex items-start justify-between gap-4">
|
||||||
@ -115,7 +119,7 @@ function SettingsPage() {
|
|||||||
<BaseIcon path={mdiChevronRight} size={18} />
|
<BaseIcon path={mdiChevronRight} size={18} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -56,28 +56,52 @@ export const white: StyleObject = {
|
|||||||
|
|
||||||
export const dataGridStyles = {
|
export const dataGridStyles = {
|
||||||
'& .MuiDataGrid-cell': {
|
'& .MuiDataGrid-cell': {
|
||||||
paddingX: 3,
|
paddingX: 1.75,
|
||||||
border: 'none',
|
border: 'none',
|
||||||
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
'& .MuiDataGrid-columnHeader': {
|
'& .MuiDataGrid-columnHeader': {
|
||||||
paddingX: 3,
|
paddingX: 1.75,
|
||||||
},
|
},
|
||||||
'& .MuiDataGrid-columnHeaderCheckbox': {
|
'& .MuiDataGrid-columnHeaderCheckbox': {
|
||||||
paddingX: 0,
|
paddingX: 0.5,
|
||||||
},
|
},
|
||||||
'& .MuiDataGrid-columnHeaders': {
|
'& .MuiDataGrid-columnHeaders': {
|
||||||
paddingY: 4,
|
paddingY: 0,
|
||||||
borderStartStartRadius: 7,
|
minHeight: '48px !important',
|
||||||
borderStartEndRadius: 7,
|
maxHeight: '48px !important',
|
||||||
|
borderStartStartRadius: 10,
|
||||||
|
borderStartEndRadius: 10,
|
||||||
},
|
},
|
||||||
'& .MuiDataGrid-footerContainer': {
|
'& .MuiDataGrid-footerContainer': {
|
||||||
paddingY: 0.5,
|
minHeight: '48px',
|
||||||
borderEndStartRadius: 7,
|
borderTop: '1px solid rgb(226 232 240)',
|
||||||
borderEndEndRadius: 7,
|
borderEndStartRadius: 10,
|
||||||
|
borderEndEndRadius: 10,
|
||||||
|
color: 'rgb(100 116 139)',
|
||||||
},
|
},
|
||||||
'& .MuiDataGrid-root': {
|
'& .MuiDataGrid-root': {
|
||||||
border: 'none',
|
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 = {
|
export const basic: StyleObject = {
|
||||||
|
|||||||
@ -29,6 +29,10 @@ http {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
location = /api-docs {
|
||||||
|
return 301 /api-docs/;
|
||||||
|
}
|
||||||
|
|
||||||
location /api-docs/ {
|
location /api-docs/ {
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
@ -93,4 +97,4 @@ http {
|
|||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user