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 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,
|
||||
};
|
||||
|
||||
|
||||
@ -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);
|
||||
}}
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
}}
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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);
|
||||
}}
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
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;
|
||||
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}
|
||||
|
||||
@ -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);
|
||||
}}
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
}}
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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);
|
||||
}}
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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);
|
||||
}}
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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);
|
||||
}}
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -3,7 +3,7 @@ html {
|
||||
}
|
||||
|
||||
body {
|
||||
@apply pt-12 xl:pl-64 h-full;
|
||||
@apply h-full;
|
||||
}
|
||||
|
||||
#app {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
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 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@ -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 = (() => {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user