update ui layout

This commit is contained in:
rustmailer
2026-05-10 03:19:25 +08:00
parent 213c6452a8
commit 2f8ba5ad40
41 changed files with 337 additions and 628 deletions
-2
View File
@@ -49,7 +49,6 @@
"@tanstack/react-router": "^1.86.1", "@tanstack/react-router": "^1.86.1",
"@tanstack/react-table": "^8.20.5", "@tanstack/react-table": "^8.20.5",
"@tanstack/react-virtual": "^3.11.2", "@tanstack/react-virtual": "^3.11.2",
"ace-builds": "^1.37.1",
"axios": "^1.7.9", "axios": "^1.7.9",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@@ -62,7 +61,6 @@
"lucide-react": "^0.468.0", "lucide-react": "^0.468.0",
"radix-ui": "^1.4.3", "radix-ui": "^1.4.3",
"react": "^18.3.1", "react": "^18.3.1",
"react-ace": "^13.0.0",
"react-day-picker": "9.13.0", "react-day-picker": "9.13.0",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-hook-form": "^7.54.0", "react-hook-form": "^7.54.0",
-79
View File
@@ -1,79 +0,0 @@
//
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
//
// This file is part of the Bichon Email Archiving Project
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from 'react';
import AceEditor from 'react-ace';
import { cn } from '@/lib/utils';
import 'ace-builds/src-noconflict/mode-handlebars';
import 'ace-builds/src-noconflict/mode-json';
import 'ace-builds/src-noconflict/mode-python';
import 'ace-builds/src-noconflict/mode-markdown';
import 'ace-builds/src-noconflict/theme-kuroir';
import 'ace-builds/src-noconflict/theme-monokai';
import 'ace-builds/src-noconflict/ext-language_tools';
interface ReactAceEditorProps {
value?: string;
onChange?: (value: string) => void;
placeholder?: string;
className?: string;
readOnly?: boolean;
theme?: 'kuroir' | 'monokai';
mode?: 'handlebars' | 'json' | 'markdown' | 'python';
}
const ReactAceEditor: React.FC<ReactAceEditorProps> = ({
value,
onChange,
placeholder,
className,
readOnly = false,
theme = 'github',
mode = 'handlebars'
}) => {
return (
<div className={cn('w-full h-[100px]', className)}>
<AceEditor
mode={mode}
theme={theme}
readOnly={readOnly}
value={value || ''}
onChange={onChange}
placeholder={placeholder}
fontSize={14}
showPrintMargin={false}
showGutter={true}
highlightActiveLine={true}
width="100%"
height="100%"
setOptions={{
useWorker: false,
enableBasicAutocompletion: true,
enableMobileMenu: true,
enableLiveAutocompletion: false,
enableSnippets: false,
showLineNumbers: true,
tabSize: 2,
}}
/>
</div>
);
};
export default ReactAceEditor;
-113
View File
@@ -1,113 +0,0 @@
//
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
//
// This file is part of the Bichon Email Archiving Project
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from 'react'
import { useNavigate } from '@tanstack/react-router'
import {
IconArrowRightDashed,
IconMoon,
IconSun,
} from '@tabler/icons-react'
import { useSearch } from '@/context/search-context'
import { useTheme } from '@/context/theme-context'
import {
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from '@/components/ui/command'
import { useSidebarData } from './layout/data/sidebar-data'
import { ScrollArea } from './ui/scroll-area'
import { useTranslation } from 'react-i18next'
export function CommandMenu() {
const navigate = useNavigate()
const { setTheme } = useTheme()
const { open, setOpen } = useSearch()
const sidebarData = useSidebarData()
const { t } = useTranslation()
const runCommand = React.useCallback(
(command: () => unknown) => {
setOpen(false)
command()
},
[setOpen]
)
return (
<CommandDialog modal open={open} onOpenChange={setOpen}>
<CommandInput placeholder={t('command.typeCommandOrSearch')} />
<CommandList>
<ScrollArea type='hover' className='h-72 pr-1'>
<CommandEmpty>{t('command.noResultsFound')}</CommandEmpty>
{sidebarData.navGroups.map((group) => (
<CommandGroup key={group.title} heading={group.title}>
{group.items.map((navItem, i) => {
if (navItem.url)
return (
<CommandItem
key={`${navItem.url}-${i}`}
value={navItem.title}
onSelect={() => {
runCommand(() => navigate({ to: navItem.url }))
}}
>
<div className='mr-2 flex h-4 w-4 items-center justify-center'>
<IconArrowRightDashed className='size-2 text-muted-foreground/80' />
</div>
{navItem.title}
</CommandItem>
)
return navItem.items?.map((subItem, i) => (
<CommandItem
key={`${subItem.url}-${i}`}
value={subItem.title}
onSelect={() => {
runCommand(() => navigate({ to: subItem.url }))
}}
>
<div className='mr-2 flex h-4 w-4 items-center justify-center'>
<IconArrowRightDashed className='size-2 text-muted-foreground/80' />
</div>
{subItem.title}
</CommandItem>
))
})}
</CommandGroup>
))}
<CommandSeparator />
<CommandGroup heading={t('command.theme')}>
<CommandItem onSelect={() => runCommand(() => setTheme('light'))}>
<IconSun /> <span>{t('command.light')}</span>
</CommandItem>
<CommandItem onSelect={() => runCommand(() => setTheme('dark'))}>
<IconMoon className='scale-90' />
<span>{t('command.dark')}</span>
</CommandItem>
</CommandGroup>
</ScrollArea>
</CommandList>
</CommandDialog>
)
}
-110
View File
@@ -1,110 +0,0 @@
//
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
//
// This file is part of the Bichon Email Archiving Project
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useEffect, useState } from 'react';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
import { AlertTriangle } from 'lucide-react';
import AceEditor from './ace-editor';
interface CodeEditorWithDraftProps {
value: string | undefined;
onChange: (value: string) => void;
localStorageKey: string;
mode?: 'handlebars' | 'json' | 'markdown' | 'python';
theme?: 'kuroir' | 'monokai';
placeholder?: string;
className?: string;
}
export function CodeEditorWithDraft({
value,
onChange,
localStorageKey,
mode,
theme,
placeholder,
className,
}: CodeEditorWithDraftProps) {
const [hasDraft, setHasDraft] = useState(false);
useEffect(() => {
const saved = localStorage.getItem(localStorageKey);
if (saved && !value) {
setHasDraft(true);
}
}, []);
useEffect(() => {
const save = () => {
if (value) {
localStorage.setItem(localStorageKey, value);
}
};
const handle = setInterval(save, 2000);
return () => {
clearInterval(handle);
}
}, [value]);
const handleRestore = () => {
const saved = localStorage.getItem(localStorageKey);
if (saved) {
onChange(saved);
setHasDraft(false);
}
};
const handleDiscard = () => {
localStorage.removeItem(localStorageKey);
setHasDraft(false);
};
return (
<div>
{hasDraft && (
<Alert variant="destructive" className="mt-2 flex items-start gap-2">
<AlertTriangle className="w-5 h-5 text-red-600 mt-1" />
<div>
<AlertTitle>Unsaved draft detected</AlertTitle>
<AlertDescription>
A draft was found in your browser. Would you like to restore it?
<div className="mt-2 flex gap-2">
<Button size="sm" onClick={handleRestore}>
Restore
</Button>
<Button size="sm" variant="outline" onClick={handleDiscard}>
Discard
</Button>
</div>
</AlertDescription>
</div>
</Alert>
)}
<AceEditor
placeholder={placeholder}
value={value}
onChange={onChange}
className={className}
mode={mode}
theme={theme}
/>
</div>
);
}
+2 -2
View File
@@ -24,7 +24,7 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu' } from '@/components/ui/dropdown-menu'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Languages } from 'lucide-react' import { Globe } from 'lucide-react'
const LANGUAGES = [ const LANGUAGES = [
{ code: 'ar', label: 'العربية' }, { code: 'ar', label: 'العربية' },
@@ -59,7 +59,7 @@ export function LanguageSwitch() {
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon"> <Button variant="ghost" size="icon">
<Languages className="h-4 w-4" /> <Globe/>
<span className="sr-only">Change language</span> <span className="sr-only">Change language</span>
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
@@ -27,7 +27,6 @@ import { LanguageSwitch } from "../language-switch";
export const FixedHeader = () => { export const FixedHeader = () => {
return ( return (
<Header fixed> <Header fixed>
{/* <Search /> */}
<div className='ml-auto flex items-center space-x-4'> <div className='ml-auto flex items-center space-x-4'>
<NotificationPopover /> <NotificationPopover />
<GithubLinkButton /> <GithubLinkButton />
+22 -7
View File
@@ -17,32 +17,47 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react"; import React, { useEffect, useState } from "react";
import { GitHubLogoIcon } from "@radix-ui/react-icons"; import { GitHubLogoIcon } from "@radix-ui/react-icons";
import { Star } from "lucide-react";
interface GithubLinkButtonProps { interface GithubLinkButtonProps {
/** GitHub repository or profile URL */
href?: string; href?: string;
/** Icon size (default: 20) */ repo?: string;
size?: number; size?: number;
/** Optional tooltip title */
title?: string; title?: string;
} }
export const GithubLinkButton: React.FC<GithubLinkButtonProps> = ({ export const GithubLinkButton: React.FC<GithubLinkButtonProps> = ({
href = "https://github.com/rustmailer/bichon", href = "https://github.com/rustmailer/bichon",
size = 20, repo = "rustmailer/bichon",
size = 18,
title = "View on GitHub", title = "View on GitHub",
}) => { }) => {
const [stars, setStars] = useState<number | null>(null);
useEffect(() => {
fetch(`https://api.github.com/repos/${repo}`)
.then(res => res.json())
.then(data => setStars(data.stargazers_count))
.catch(() => { });
}, [repo]);
return ( return (
<a <a
href={href} href={href}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
title={title} title={title}
className="inline-flex items-center justify-center rounded-full p-2 text-muted-foreground hover:text-foreground hover:bg-muted transition-colors" className="inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-muted-foreground hover:text-foreground hover:bg-muted transition-colors text-xs font-medium"
> >
<GitHubLogoIcon className="w-5 h-5" style={{ width: size, height: size }} /> <GitHubLogoIcon style={{ width: size, height: size }} />
{stars !== null && (
<>
<Star className="h-3 w-3 fill-current" />
<span>{stars >= 1000 ? `${(stars / 1000).toFixed(1)}k` : stars}</span>
</>
)}
</a> </a>
); );
}; };
-2
View File
@@ -19,7 +19,6 @@
import React from 'react' import React from 'react'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { Separator } from '@/components/ui/separator'
import { SidebarTrigger } from '@/components/ui/sidebar' import { SidebarTrigger } from '@/components/ui/sidebar'
interface HeaderProps extends React.HTMLAttributes<HTMLElement> { interface HeaderProps extends React.HTMLAttributes<HTMLElement> {
@@ -58,7 +57,6 @@ export const Header = ({
{...props} {...props}
> >
<SidebarTrigger variant='outline' className='scale-125 sm:scale-100' /> <SidebarTrigger variant='outline' className='scale-125 sm:scale-100' />
<Separator orientation='vertical' className='h-6' />
{children} {children}
</header> </header>
) )
-126
View File
@@ -1,126 +0,0 @@
//
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
//
// This file is part of the Bichon Email Archiving Project
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import {
BadgeCheck,
Bell,
ChevronsUpDown,
CreditCard,
LogOut,
Sparkles,
} from 'lucide-react'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from '@/components/ui/sidebar'
export function NavUser({
user,
}: {
user: {
name: string
email: string
avatar: string
}
}) {
const { isMobile } = useSidebar()
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size='lg'
className='data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground'
>
<Avatar className='h-8 w-8 rounded-lg'>
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className='rounded-lg'>SN</AvatarFallback>
</Avatar>
<div className='grid flex-1 text-left text-sm leading-tight'>
<span className='truncate font-semibold'>{user.name}</span>
<span className='truncate text-xs'>{user.email}</span>
</div>
<ChevronsUpDown className='ml-auto size-4' />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className='w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg'
side={isMobile ? 'bottom' : 'right'}
align='end'
sideOffset={4}
>
<DropdownMenuLabel className='p-0 font-normal'>
<div className='flex items-center gap-2 px-1 py-1.5 text-left text-sm'>
<Avatar className='h-8 w-8 rounded-lg'>
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className='rounded-lg'>SN</AvatarFallback>
</Avatar>
<div className='grid flex-1 text-left text-sm leading-tight'>
<span className='truncate font-semibold'>{user.name}</span>
<span className='truncate text-xs'>{user.email}</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<Sparkles />
Upgrade to Pro
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<BadgeCheck />
Account
</DropdownMenuItem>
<DropdownMenuItem>
<CreditCard />
Billing
</DropdownMenuItem>
<DropdownMenuItem>
<Bell />
Notifications
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
<LogOut />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
}
+9 -5
View File
@@ -31,6 +31,7 @@ import { get_notifications } from "@/api/system/api";
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
import { useMemo } from "react"; import { useMemo } from "react";
import { useTranslation } from "react-i18next";
interface Release { interface Release {
tag_name: string; tag_name: string;
@@ -58,6 +59,9 @@ export function NotificationPopover() {
staleTime: 1000 * 60 * 30, // 30 minutes staleTime: 1000 * 60 * 30, // 30 minutes
}); });
const {t} = useTranslation();
const activeNotifications = useMemo((): ActiveNotification[] => { const activeNotifications = useMemo((): ActiveNotification[] => {
if (!data) return []; if (!data) return [];
@@ -107,8 +111,8 @@ export function NotificationPopover() {
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-[32rem] p-0" align="end"> <PopoverContent className="w-[32rem] p-0" align="end">
<div className="p-4 border-b"> <div className="p-4 border-b">
<h4 className="font-medium"> <h4 className="font-medium text-sm">
System Notifications {t('system.notifications')}
{showNotificationBadge && ` (${activeNotifications.length})`} {showNotificationBadge && ` (${activeNotifications.length})`}
</h4> </h4>
</div> </div>
@@ -144,7 +148,7 @@ function ReleaseNotificationView({ data }: { data: Release }) {
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h3 className="text-lg font-semibold"> <h3 className="text-sm font-semibold">
{data.tag_name} {data.tag_name}
</h3> </h3>
<span className="text-xs bg-green-100 text-green-800 px-2 py-1 rounded-full"> <span className="text-xs bg-green-100 text-green-800 px-2 py-1 rounded-full">
@@ -156,7 +160,7 @@ function ReleaseNotificationView({ data }: { data: Release }) {
</p> </p>
</div> </div>
<div className="prose prose-xs dark:prose-invert max-w-none text-sm"> <div className="prose prose-xs dark:prose-invert max-w-none text-xs">
<ReactMarkdown remarkPlugins={[remarkGfm]}> <ReactMarkdown remarkPlugins={[remarkGfm]}>
{data.body} {data.body}
</ReactMarkdown> </ReactMarkdown>
@@ -168,7 +172,7 @@ function ReleaseNotificationView({ data }: { data: Release }) {
href={data.html_url} href={data.html_url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-sm text-primary hover:underline inline-flex items-center" className="text-xs text-primary hover:underline inline-flex items-center"
> >
View full release notes <ExternalLinkIcon className="ml-1 h-3 w-3" /> View full release notes <ExternalLinkIcon className="ml-1 h-3 w-3" />
</a> </a>
+2 -3
View File
@@ -26,8 +26,7 @@ import { Button } from '@/components/ui/button'
export function ThemeSwitch() { export function ThemeSwitch() {
const { theme, setTheme } = useTheme() const { theme, setTheme } = useTheme()
/* Update theme-color meta tag
* when theme is updated */
useEffect(() => { useEffect(() => {
const themeColor = theme === 'dark' ? '#020817' : '#fff' const themeColor = theme === 'dark' ? '#020817' : '#fff'
const metaThemeColor = document.querySelector("meta[name='theme-color']") const metaThemeColor = document.querySelector("meta[name='theme-color']")
@@ -39,7 +38,7 @@ export function ThemeSwitch() {
}, [theme]) }, [theme])
return ( return (
<Button variant='ghost' size='icon' className='scale-95 rounded-full' onClick={toggleTheme}> <Button variant='ghost' size='icon' className='scale-95' onClick={toggleTheme}>
<IconSun className='size-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0' /> <IconSun className='size-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0' />
<IconMoon className='absolute size-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100' /> <IconMoon className='absolute size-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100' />
<span className='sr-only'>Toggle theme</span> <span className='sr-only'>Toggle theme</span>
-2
View File
@@ -18,7 +18,6 @@
import React from 'react' import React from 'react'
import { CommandMenu } from '@/components/command-menu'
interface SearchContextType { interface SearchContextType {
open: boolean open: boolean
@@ -48,7 +47,6 @@ export function SearchProvider({ children }: Props) {
return ( return (
<SearchContext.Provider value={{ open, setOpen }}> <SearchContext.Provider value={{ open, setOpen }}>
{children} {children}
<CommandMenu />
</SearchContext.Provider> </SearchContext.Provider>
) )
} }
@@ -59,7 +59,11 @@ export function RunningStateCellAction({ row }: Props) {
}) })
} }
}}> }}>
<span className="text-xs text-blue-500 cursor-pointer underline hover:text-blue-700">{t('accounts.viewDetails')}</span> <span
className="text-xs text-primary cursor-pointer underline underline-offset-2 hover:opacity-80 transition-opacity"
>
{t('accounts.viewDetails')}
</span>
</Button> </Button>
) )
} }
+2 -20
View File
@@ -13,7 +13,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import { XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, BarChart, Bar } from 'recharts'; import { XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, BarChart, Bar } from 'recharts';
import { Mail, Users, Inbox, Zap } from 'lucide-react'; import { Mail, Users, Inbox, Zap, Paperclip } from 'lucide-react';
import { formatBytes, formatNumber } from '@/lib/utils'; import { formatBytes, formatNumber } from '@/lib/utils';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { get_dashboard_stats, INITIAL_DASHBOARD_STATS, TimeBucket } from '@/api/system/api'; import { get_dashboard_stats, INITIAL_DASHBOARD_STATS, TimeBucket } from '@/api/system/api';
@@ -30,24 +30,6 @@ interface DailyActivity {
timestamp_ms: number; timestamp_ms: number;
} }
const GithubIcon = ({ className }: { className?: string }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4" />
<path d="M9 18c-4.51 2-5-2-7-2" />
</svg>
);
function convertRecentActivity(timeBuckets: TimeBucket[], locale: string): DailyActivity[] { function convertRecentActivity(timeBuckets: TimeBucket[], locale: string): DailyActivity[] {
const dateFormatter = new Intl.DateTimeFormat(locale, { const dateFormatter = new Intl.DateTimeFormat(locale, {
month: 'short', month: 'short',
@@ -223,7 +205,7 @@ export default function MailArchiveDashboard() {
<Card className="md:col-span-2 lg:col-span-2"> <Card className="md:col-span-2 lg:col-span-2">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-xs font-bold uppercase tracking-wider">{t('dashboard.totalAttachments')}</CardTitle> <CardTitle className="text-xs font-bold uppercase tracking-wider">{t('dashboard.totalAttachments')}</CardTitle>
<Mail className="h-4 w-4 text-muted-foreground" /> <Paperclip className="h-4 w-4 text-muted-foreground" />
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-xl font-bold">{formatNumber(stats1.attachment_count)}</div> <div className="text-xl font-bold">{formatNumber(stats1.attachment_count)}</div>
+120
View File
@@ -0,0 +1,120 @@
//
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
//
// This file is part of the Bichon Email Archiving Project
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { useTranslation } from 'react-i18next'
import useMinimalAccountList from '@/hooks/use-minimal-account-list'
import { useCurrentUser } from '@/hooks/use-current-user'
import { PermissionsDialog } from './permissions-dialog'
import Logo from '@/assets/logo.svg'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
export function AccountAccessList() {
const { t } = useTranslation()
const { data: user } = useCurrentUser()
const { getEmailById } = useMinimalAccountList()
const [permissionsOpen, setPermissionsOpen] = useState(false)
const [permissionsAccountId, setPermissionsAccountId] = useState<number | undefined>(undefined)
if (!user) return null
const accessibleAccountIds = user.account_access_map instanceof Map
? Array.from(user.account_access_map.keys())
: Object.keys(user.account_access_map || {}).map(Number)
const roleSummary = user.account_roles_summary || {}
if (accessibleAccountIds.length === 0) {
return (
<div className="flex h-[450px] items-center justify-center rounded-md border border-dashed mt-4">
<div className="mx-auto flex max-w-[420px] flex-col items-center justify-center text-center px-4">
<img
src={Logo}
className="max-h-[100px] w-auto opacity-20 saturate-0 object-contain"
alt="Bichon Logo"
/>
<h3 className="mt-4 text-lg font-semibold">{t('settings.access.empty.title')}</h3>
<p className="mt-2 text-sm text-muted-foreground">
{t('settings.access.empty.description')}
</p>
</div>
</div>
)
}
return (
<>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
{accessibleAccountIds.map((accountId) => {
const email = getEmailById(accountId)
const roleName = roleSummary[accountId]
if (!email) return null
return (
<Card key={accountId} className="group hover:bg-accent/40 transition-all h-fit">
<CardHeader className="flex flex-row items-center gap-3 space-y-0 pb-3">
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-primary/10 text-primary text-sm font-bold shrink-0">
{email.charAt(0).toUpperCase()}
</div>
<div className="flex flex-col min-w-0">
<CardTitle className="text-sm font-semibold truncate">{email}</CardTitle>
<CardDescription className="text-[10px] font-mono">
{t('settings.profile.account.id', { id: accountId })}
</CardDescription>
</div>
</CardHeader>
<CardContent className="flex items-center justify-between pt-3 border-t">
{roleName ? (
<Badge variant="secondary" className="text-[11px]">
{roleName}
</Badge>
) : (
<span />
)}
<Button
type="button"
variant="outline"
size="sm"
className="text-xs h-7"
onClick={() => {
setPermissionsAccountId(accountId)
setPermissionsOpen(true)
}}
>
{t('settings.profile.button.permissions')}
</Button>
</CardContent>
</Card>
)
})}
</div>
<PermissionsDialog
currentRow={user}
open={permissionsOpen}
onOpenChange={setPermissionsOpen}
mode="account"
accountId={permissionsAccountId}
/>
</>
)
}
@@ -25,11 +25,12 @@ import {
DialogTitle, DialogTitle,
} from '@/components/ui/dialog' } from '@/components/ui/dialog'
import { getPermissions, User } from '@/api/users/api' import { getPermissions, User } from '@/api/users/api'
import { CheckCircle2, XCircle } from 'lucide-react' import { CheckCircle, XCircle } from 'lucide-react'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import useMinimalAccountList from '@/hooks/use-minimal-account-list'
interface Props { interface Props {
currentRow?: User currentRow?: User
@@ -95,6 +96,7 @@ export function PermissionsDialog({
}: Props) { }: Props) {
const { t } = useTranslation() const { t } = useTranslation()
const { getEmailById } = useMinimalAccountList();
const ownedPermissions = React.useMemo<string[]>(() => { const ownedPermissions = React.useMemo<string[]>(() => {
if (!currentRow) return [] if (!currentRow) return []
@@ -140,56 +142,51 @@ export function PermissionsDialog({
<Badge variant="outline" className="text-[10px]"> <Badge variant="outline" className="text-[10px]">
{mode === 'global' {mode === 'global'
? t('permission.scope.global') ? t('permission.scope.global')
: t('permission.scope.account', { id: accountId })} : t('permission.scope.account', { id: getEmailById(accountId!) })}
</Badge> </Badge>
</div> </div>
<DialogDescription>{description}</DialogDescription> <DialogDescription>{description}</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="flex-1 overflow-y-auto py-4"> <div className="flex-1 overflow-y-auto py-4">
<div className="grid gap-6 px-1"> <div className="flex flex-col gap-6 px-1">
{categories.map((cat) => ( {categories.map((cat) => (
<div key={cat.title} className="flex flex-col"> <div key={cat.title} className="flex flex-col">
<h3 className="text-[11px] font-bold text-slate-500 border-l-4 border-blue-500 pl-2 mb-4 uppercase tracking-widest"> <h3 className="text-[11px] font-bold text-muted-foreground border-l-2 border-primary pl-2 mb-3 uppercase tracking-widest">
{cat.title} {cat.title}
</h3> </h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-1.5">
{cat.keys.map((key) => { {cat.keys.map((key) => {
const item = permissions.get(key) const item = permissions.get(key)
if (!item) return null if (!item) return null
const hasPermission = const hasPermission = ownedPermissions.includes(item.value)
ownedPermissions.includes(item.value)
return ( return (
<div <div
key={item.value} key={item.value}
className={cn( className={cn(
'flex items-center gap-2.5 p-2 rounded-md transition-all text-xs border', 'flex items-center gap-2.5 p-2.5 rounded-md border text-xs transition-all',
hasPermission hasPermission
? 'bg-green-50/40 border-green-100 text-green-800 shadow-sm' ? 'bg-primary/5 border-primary/20'
: 'bg-slate-50/30 border-transparent text-slate-400 opacity-60', : 'bg-muted/30 border-border opacity-50',
)} )}
> >
{hasPermission ? ( {hasPermission ? (
<CheckCircle2 className="w-3.5 h-3.5 text-green-600 shrink-0" /> <CheckCircle className="w-4 h-4 text-primary shrink-0" />
) : ( ) : (
<XCircle className="w-3.5 h-3.5 text-slate-300 shrink-0" /> <XCircle className="w-4 h-4 text-muted-foreground/40 shrink-0" />
)} )}
<div className="flex flex-col min-w-0 flex-1"> <div className="flex flex-col min-w-0 flex-1">
<span <span className={cn(
className={cn( 'font-medium text-xs leading-none truncate',
'font-semibold text-sm leading-none truncate', hasPermission ? 'text-foreground' : 'text-muted-foreground',
hasPermission )}>
? 'text-slate-900'
: 'text-slate-500',
)}
>
{item.label} {item.label}
</span> </span>
<span className="text-xs opacity-70 font-mono mt-1 truncate"> <span className="text-[10px] text-muted-foreground font-mono mt-1 truncate">
{item.value} {item.value}
</span> </span>
</div> </div>
+6 -1
View File
@@ -20,7 +20,7 @@
import { Outlet } from '@tanstack/react-router' import { Outlet } from '@tanstack/react-router'
import { Main } from '@/components/layout/main' import { Main } from '@/components/layout/main'
import SidebarNav from './components/sidebar-nav' import SidebarNav from './components/sidebar-nav'
import { KeyRound, Palette, SettingsIcon, UserCog, Waypoints } from 'lucide-react' import { KeyRound, Palette, SettingsIcon, ShieldCheck, UserCog, Waypoints } from 'lucide-react'
import { FixedHeader } from '@/components/layout/fixed-header' import { FixedHeader } from '@/components/layout/fixed-header'
import { useCurrentUser } from '@/hooks/use-current-user' import { useCurrentUser } from '@/hooks/use-current-user'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@@ -36,6 +36,11 @@ export default function Settings() {
href: '/settings/profile', href: '/settings/profile',
icon: <UserCog size={18} />, icon: <UserCog size={18} />,
}, },
{
title: t('settings.sidebar.access'),
href: '/settings/access',
icon: <ShieldCheck size={18} />
},
{ {
title: t('settings.appearance.title'), title: t('settings.appearance.title'),
href: '/settings/appearance', href: '/settings/appearance',
@@ -35,17 +35,13 @@ import {
} from '@/components/ui/form' } from '@/components/ui/form'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { ScrollArea } from '@/components/ui/scroll-area'
import { toast } from '@/hooks/use-toast' import { toast } from '@/hooks/use-toast'
import { PasswordInput } from '@/components/password-input' import { PasswordInput } from '@/components/password-input'
import { update_user, User } from '@/api/users/api' import { update_user, User } from '@/api/users/api'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Separator } from '@/components/ui/separator'
import { FileWithPreview } from '@/hooks/use-file-upload' import { FileWithPreview } from '@/hooks/use-file-upload'
import AvatarUpload from './avatar-upload' import AvatarUpload from './avatar-upload'
import useMinimalAccountList from '@/hooks/use-minimal-account-list' import { PermissionsDialog } from '../access/permissions-dialog'
import { PermissionsDialog } from './permissions-dialog'
const profileSchema = (t: (key: string) => string) => z.object({ const profileSchema = (t: (key: string) => string) => z.object({
username: z username: z
@@ -104,10 +100,7 @@ export function UserProfileForm({ user }: UserProfileFormProps) {
const [avatarFile, setAvatarFile] = useState<FileWithPreview | null>(null) const [avatarFile, setAvatarFile] = useState<FileWithPreview | null>(null)
const [permissionsOpen, setPermissionsOpen] = useState(false) const [permissionsOpen, setPermissionsOpen] = useState(false)
const [permissionsMode, setPermissionsMode] = const [permissionsAccountId, setPermissionsAccountId] = useState<number | undefined>(undefined)
useState<'global' | 'account'>('global')
const [permissionsAccountId, setPermissionsAccountId] =
useState<number | undefined>(undefined)
const form = useForm<ProfileFormValues>({ const form = useForm<ProfileFormValues>({
resolver: zodResolver(profileSchema(t)), resolver: zodResolver(profileSchema(t)),
@@ -149,15 +142,7 @@ export function UserProfileForm({ user }: UserProfileFormProps) {
}, },
}) })
const { getEmailById } = useMinimalAccountList()
const accessibleAccountIds = user.account_access_map instanceof Map
? Array.from(user.account_access_map.keys())
: Object.keys(user.account_access_map || {}).map(Number)
const hasAccess = accessibleAccountIds.length > 0
const roleNames = user.global_roles_names const roleNames = user.global_roles_names
const roleSummary = user.account_roles_summary || {}
return ( return (
<> <>
@@ -188,7 +173,6 @@ export function UserProfileForm({ user }: UserProfileFormProps) {
size="sm" size="sm"
className="text-xs" className="text-xs"
onClick={() => { onClick={() => {
setPermissionsMode('global')
setPermissionsAccountId(undefined) setPermissionsAccountId(undefined)
setPermissionsOpen(true) setPermissionsOpen(true)
}} }}
@@ -260,77 +244,6 @@ export function UserProfileForm({ user }: UserProfileFormProps) {
)} )}
/> />
</div> </div>
{hasAccess && (
<Separator orientation="vertical" className="hidden lg:block" />
)}
{hasAccess && (
<div className="space-y-4">
<h2 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
{t('settings.profile.section.accounts', {
count: accessibleAccountIds.length,
})}
</h2>
<ScrollArea className="h-[calc(100vh-16rem)] pr-4">
<div className="grid grid-cols-1 gap-3">
{accessibleAccountIds.map((accountId) => {
const email = getEmailById(accountId)
const roleName = roleSummary[accountId]
if (!email) return null
return (
<div
key={accountId}
className="group flex items-center justify-between p-3 rounded-xl border bg-card hover:bg-accent/40 transition-all shadow-sm"
>
<div className="flex items-center min-w-0">
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-primary/10 text-primary text-sm font-bold mr-4 shrink-0">
{email.charAt(0).toUpperCase()}
</div>
<div className="flex flex-col min-w-0">
<span className="text-xs font-semibold truncate">
{email}
</span>
<span className="text-[10px] text-muted-foreground font-mono">
{t('settings.profile.account.id', {
id: accountId,
})}
</span>
</div>
</div>
<div className="flex items-center gap-2">
{roleName && (
<Badge
variant="outline"
className="text-[11px]"
>
{roleName}
</Badge>
)}
<Button
type="button"
variant="ghost"
className="text-[10px]"
onClick={() => {
setPermissionsMode('account')
setPermissionsAccountId(accountId)
setPermissionsOpen(true)
}}
>
{t('settings.profile.button.permissions')}
</Button>
</div>
</div>
)
})}
</div>
</ScrollArea>
</div>
)}
</div> </div>
<div className="flex justify-start pt-4"> <div className="flex justify-start pt-4">
@@ -346,12 +259,11 @@ export function UserProfileForm({ user }: UserProfileFormProps) {
</div> </div>
</form> </form>
</Form> </Form>
<PermissionsDialog <PermissionsDialog
currentRow={user} currentRow={user}
open={permissionsOpen} open={permissionsOpen}
onOpenChange={setPermissionsOpen} onOpenChange={setPermissionsOpen}
mode={permissionsMode} mode="global"
accountId={permissionsAccountId} accountId={permissionsAccountId}
/> />
</> </>
@@ -41,7 +41,11 @@ export function PermissionsCellAction({ row }: Props) {
setCurrentRow(row.original) setCurrentRow(row.original)
setOpen('permissions') setOpen('permissions')
}}> }}>
<span className="text-xs text-blue-500 cursor-pointer underline hover:text-blue-700">{t('roles.details.view_permissions')}</span> <span
className="text-xs text-primary cursor-pointer underline underline-offset-2 hover:opacity-80 transition-opacity"
>
{t('roles.details.view_permissions')}
</span>
</Button> </Button>
) )
} }
@@ -24,7 +24,7 @@ import {
DialogTitle, DialogTitle,
} from '@/components/ui/dialog' } from '@/components/ui/dialog'
import { getPermissions, UserRole } from '@/api/users/api' import { getPermissions, UserRole } from '@/api/users/api'
import { CheckCircle2, XCircle } from 'lucide-react' import { CheckCircle, XCircle } from 'lucide-react'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
@@ -71,7 +71,7 @@ export function PermissionsDialog({ currentRow, open, onOpenChange }: Props) {
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-7xl w-[90vw] overflow-hidden flex flex-col max-h-[90vh]"> <DialogContent className="max-w-7xl w-[40vw] overflow-hidden flex flex-col max-h-[90vh]">
<DialogHeader className="pb-4 border-b"> <DialogHeader className="pb-4 border-b">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<DialogTitle> <DialogTitle>
@@ -87,19 +87,15 @@ export function PermissionsDialog({ currentRow, open, onOpenChange }: Props) {
: t('roles.details.desc_account')} : t('roles.details.desc_account')}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="flex-1 overflow-y-auto py-4"> <div className="flex-1 overflow-y-auto py-4">
<div className={cn( <div className="flex flex-col gap-6 px-1">
"grid gap-6 px-1",
roleType === 'Global' ? "grid-cols-1 md:grid-cols-2" : "grid-cols-1"
)}>
{categories.map((cat) => ( {categories.map((cat) => (
<div key={cat.titleKey} className="flex flex-col"> <div key={cat.titleKey} className="flex flex-col">
<h3 className="text-[11px] font-bold text-slate-500 border-l-4 border-blue-500 pl-2 mb-4 uppercase tracking-widest"> <h3 className="text-[11px] font-bold text-muted-foreground border-l-2 border-primary pl-2 mb-3 uppercase tracking-widest">
{t(cat.titleKey)} {t(cat.titleKey)}
</h3> </h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-1.5">
{cat.keys.map((key) => { {cat.keys.map((key) => {
const item = getPermissions(t).find(p => p.value === key); const item = getPermissions(t).find(p => p.value === key);
if (!item) return null; if (!item) return null;
@@ -110,26 +106,23 @@ export function PermissionsDialog({ currentRow, open, onOpenChange }: Props) {
<div <div
key={item.value} key={item.value}
className={cn( className={cn(
"flex items-center gap-2.5 p-2 rounded-md transition-all text-xs border", "flex items-center gap-2.5 p-2.5 rounded-md border text-xs transition-all",
hasPermission hasPermission
? "bg-green-50/40 border-green-100 text-green-800 shadow-sm" ? "bg-primary/5 border-primary/20 text-foreground"
: "bg-slate-50/30 border-transparent text-slate-400 opacity-60" : "bg-muted/30 border-border text-muted-foreground opacity-50"
)} )}
> >
{hasPermission ? ( {hasPermission ? (
<CheckCircle2 className="w-3.5 h-3.5 text-green-600 shrink-0" /> <CheckCircle className="w-4 h-4 text-primary shrink-0" />
) : ( ) : (
<XCircle className="w-3.5 h-3.5 text-slate-300 shrink-0" /> <XCircle className="w-4 h-4 text-muted-foreground/30 shrink-0" />
)} )}
<div className="flex flex-col min-w-0 flex-1"> <div className="flex flex-col min-w-0 flex-1">
<span className={cn( <span className="font-medium text-xs leading-none truncate">
"font-semibold text-sm leading-none truncate",
hasPermission ? "text-slate-900" : "text-slate-500"
)}>
{item.label} {item.label}
</span> </span>
<span className="text-xs opacity-70 font-mono mt-1 truncate"> <span className="text-[10px] text-muted-foreground font-mono mt-1 truncate">
{item.value} {item.value}
</span> </span>
</div> </div>
@@ -141,7 +134,6 @@ export function PermissionsDialog({ currentRow, open, onOpenChange }: Props) {
))} ))}
</div> </div>
</div> </div>
<div className="flex justify-end pt-4 border-t mt-auto"> <div className="flex justify-end pt-4 border-t mt-auto">
<Button <Button
variant="ghost" variant="ghost"
@@ -223,7 +223,7 @@ export function UserActionDialog({ currentRow, open, onOpenChange }: Props) {
<DialogHeader className="p-6 pb-0 shrink-0"> <DialogHeader className="p-6 pb-0 shrink-0">
<div className="flex items-center gap-4 mb-4"> <div className="flex items-center gap-4 mb-4">
{isEdit && currentRow?.avatar ? ( {isEdit && currentRow?.avatar ? (
<img src={`data:image/png;base64,${currentRow.avatar}`} className="h-12 w-12 rounded-full border shadow-sm" alt="" /> <img src={`data:image/png;base64,${currentRow.avatar}`} className="h-12 w-12 rounded-full border shadow-sm object-cover" alt="" />
) : ( ) : (
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-primary/10 text-primary"> <div className="flex h-12 w-12 items-center justify-center rounded-full bg-primary/10 text-primary">
<UserIcon className="h-6 w-6" /> <UserIcon className="h-6 w-6" />
+5 -1
View File
@@ -915,7 +915,7 @@
"role_badge": "دور {{type}}", "role_badge": "دور {{type}}",
"title": "الصلاحيات: {{name}}", "title": "الصلاحيات: {{name}}",
"unknown": "دور غير معروف", "unknown": "دور غير معروف",
"view_permissions": "عرض التفاصيل" "view_permissions": "التفاصيل"
}, },
"empty": { "empty": {
"desc": "لم تقم بإنشاء أي أدوار مخصصة بعد.", "desc": "لم تقم بإنشاء أي أدوار مخصصة بعد.",
@@ -1272,6 +1272,7 @@
"rootTitle": "المستخدم الجذر", "rootTitle": "المستخدم الجذر",
"selectAll": "تحديد الكل", "selectAll": "تحديد الكل",
"sidebar": { "sidebar": {
"access": "صلاحيات الوصول",
"apiTokens": "رموز API", "apiTokens": "رموز API",
"configurations": "تكوينات النظام", "configurations": "تكوينات النظام",
"profile": "الملف الشخصي", "profile": "الملف الشخصي",
@@ -1300,6 +1301,9 @@
"desc": "هل أنت متأكد أنك تريد تسجيل الخروج؟ ستحتاج إلى تسجيل الدخول مرة أخرى للوصول إلى حسابك.", "desc": "هل أنت متأكد أنك تريد تسجيل الخروج؟ ستحتاج إلى تسجيل الدخول مرة أخرى للوصول إلى حسابك.",
"title": "تسجيل الخروج" "title": "تسجيل الخروج"
}, },
"system": {
"notifications": "إشعارات النظام"
},
"systemConfig": { "systemConfig": {
"pageDescription": "يتم تهيئة هذه المعلمات عبر علامات CLI أو متغيرات البيئة. وهي حاليًا للقراءة فقط لضمان اتساق البيئة.", "pageDescription": "يتم تهيئة هذه المعلمات عبر علامات CLI أو متغيرات البيئة. وهي حاليًا للقراءة فقط لضمان اتساق البيئة.",
"pageTitle": "تكوينات مستوى النظام", "pageTitle": "تكوينات مستوى النظام",
+5 -1
View File
@@ -915,7 +915,7 @@
"role_badge": "{{type}} rolle", "role_badge": "{{type}} rolle",
"title": "Rettigheder: {{name}}", "title": "Rettigheder: {{name}}",
"unknown": "Ukendt rolle", "unknown": "Ukendt rolle",
"view_permissions": "Se detaljer" "view_permissions": "Detaljer"
}, },
"empty": { "empty": {
"desc": "Du har ikke oprettet nogen brugerdefinerede roller endnu.", "desc": "Du har ikke oprettet nogen brugerdefinerede roller endnu.",
@@ -1272,6 +1272,7 @@
"rootTitle": "Root", "rootTitle": "Root",
"selectAll": "Vælg alle", "selectAll": "Vælg alle",
"sidebar": { "sidebar": {
"access": "Adgang",
"apiTokens": "API-tokens", "apiTokens": "API-tokens",
"configurations": "Systemkonfigurationer", "configurations": "Systemkonfigurationer",
"profile": "Profil", "profile": "Profil",
@@ -1300,6 +1301,9 @@
"desc": "Er du sikker på, at du vil logge ud? Du skal logge ind igen for at få adgang til din konto.", "desc": "Er du sikker på, at du vil logge ud? Du skal logge ind igen for at få adgang til din konto.",
"title": "Log ud" "title": "Log ud"
}, },
"system": {
"notifications": "Systemmeddelelser"
},
"systemConfig": { "systemConfig": {
"pageDescription": "Disse parametre initialiseres via CLI-flag eller miljøvariabler. De er skrivebeskyttede.", "pageDescription": "Disse parametre initialiseres via CLI-flag eller miljøvariabler. De er skrivebeskyttede.",
"pageTitle": "Systemkonfigurationer", "pageTitle": "Systemkonfigurationer",
+5 -1
View File
@@ -915,7 +915,7 @@
"role_badge": "{{type}}-Rolle", "role_badge": "{{type}}-Rolle",
"title": "Berechtigungen: {{name}}", "title": "Berechtigungen: {{name}}",
"unknown": "Unbekannte Rolle", "unknown": "Unbekannte Rolle",
"view_permissions": "Details anzeigen" "view_permissions": "Details"
}, },
"empty": { "empty": {
"desc": "Sie haben noch keine benutzerdefinierten Rollen erstellt.", "desc": "Sie haben noch keine benutzerdefinierten Rollen erstellt.",
@@ -1272,6 +1272,7 @@
"rootTitle": "Root-Benutzer", "rootTitle": "Root-Benutzer",
"selectAll": "Alle auswählen", "selectAll": "Alle auswählen",
"sidebar": { "sidebar": {
"access": "Zugriffsrechte",
"apiTokens": "API-Token", "apiTokens": "API-Token",
"configurations": "Systemkonfigurationen", "configurations": "Systemkonfigurationen",
"profile": "Profil", "profile": "Profil",
@@ -1300,6 +1301,9 @@
"desc": "Möchten Sie sich wirklich abmelden? Sie müssen sich erneut anmelden, um auf Ihr Konto zuzugreifen.", "desc": "Möchten Sie sich wirklich abmelden? Sie müssen sich erneut anmelden, um auf Ihr Konto zuzugreifen.",
"title": "Abmelden" "title": "Abmelden"
}, },
"system": {
"notifications": "Systembenachrichtigungen"
},
"systemConfig": { "systemConfig": {
"pageDescription": "Diese Parameter werden über CLI-Flags oder Umgebungsvariablen initialisiert. Sie sind schreibgeschützt, um die Umgebungskonsistenz zu gewährleisten.", "pageDescription": "Diese Parameter werden über CLI-Flags oder Umgebungsvariablen initialisiert. Sie sind schreibgeschützt, um die Umgebungskonsistenz zu gewährleisten.",
"pageTitle": "Systemkonfigurationen", "pageTitle": "Systemkonfigurationen",
+5 -1
View File
@@ -915,7 +915,7 @@
"role_badge": "{{type}} Role", "role_badge": "{{type}} Role",
"title": "Role Permissions: {{name}}", "title": "Role Permissions: {{name}}",
"unknown": "Unknown Role", "unknown": "Unknown Role",
"view_permissions": "Permission Details" "view_permissions": "Details"
}, },
"empty": { "empty": {
"desc": "You haven't created any custom roles yet.", "desc": "You haven't created any custom roles yet.",
@@ -1272,6 +1272,7 @@
"rootTitle": "Root", "rootTitle": "Root",
"selectAll": "Select all", "selectAll": "Select all",
"sidebar": { "sidebar": {
"access": "Access Permissions",
"apiTokens": "API Tokens", "apiTokens": "API Tokens",
"configurations": "System Configurations", "configurations": "System Configurations",
"profile": "Profile", "profile": "Profile",
@@ -1300,6 +1301,9 @@
"desc": "Are you sure you want to sign out? You will need to sign in again to access your account.", "desc": "Are you sure you want to sign out? You will need to sign in again to access your account.",
"title": "Sign out" "title": "Sign out"
}, },
"system": {
"notifications": "System Notifications"
},
"systemConfig": { "systemConfig": {
"pageDescription": "These parameters are initialized via CLI flags or Environment Variables. They are currently read-only to ensure environment consistency.", "pageDescription": "These parameters are initialized via CLI flags or Environment Variables. They are currently read-only to ensure environment consistency.",
"pageTitle": "System-level Configurations", "pageTitle": "System-level Configurations",
+5 -1
View File
@@ -915,7 +915,7 @@
"role_badge": "Rol {{type}}", "role_badge": "Rol {{type}}",
"title": "Permisos: {{name}}", "title": "Permisos: {{name}}",
"unknown": "Rol desconocido", "unknown": "Rol desconocido",
"view_permissions": "Ver detalles" "view_permissions": "Detalles"
}, },
"empty": { "empty": {
"desc": "Aún no ha creado ningún rol personalizado.", "desc": "Aún no ha creado ningún rol personalizado.",
@@ -1272,6 +1272,7 @@
"rootTitle": "Usuario raíz", "rootTitle": "Usuario raíz",
"selectAll": "Seleccionar todo", "selectAll": "Seleccionar todo",
"sidebar": { "sidebar": {
"access": "Permisos de acceso",
"apiTokens": "Tokens API", "apiTokens": "Tokens API",
"configurations": "Configuraciones del sistema", "configurations": "Configuraciones del sistema",
"profile": "Perfil", "profile": "Perfil",
@@ -1300,6 +1301,9 @@
"desc": "¿Está seguro de que desea cerrar sesión? Necesitará iniciar sesión nuevamente para acceder a su cuenta.", "desc": "¿Está seguro de que desea cerrar sesión? Necesitará iniciar sesión nuevamente para acceder a su cuenta.",
"title": "Cerrar sesión" "title": "Cerrar sesión"
}, },
"system": {
"notifications": "Notificaciones del sistema"
},
"systemConfig": { "systemConfig": {
"pageDescription": "Estos parámetros se inicializan mediante indicadores CLI o variables de entorno. Actualmente son de solo lectura.", "pageDescription": "Estos parámetros se inicializan mediante indicadores CLI o variables de entorno. Actualmente son de solo lectura.",
"pageTitle": "Configuraciones a nivel de sistema", "pageTitle": "Configuraciones a nivel de sistema",
+5 -1
View File
@@ -915,7 +915,7 @@
"role_badge": "{{type}}-rooli", "role_badge": "{{type}}-rooli",
"title": "Oikeudet: {{name}}", "title": "Oikeudet: {{name}}",
"unknown": "Tuntematon rooli", "unknown": "Tuntematon rooli",
"view_permissions": "Katso yksityiskohdat" "view_permissions": "Tiedot"
}, },
"empty": { "empty": {
"desc": "Et ole vielä luonut mukautettuja rooleja.", "desc": "Et ole vielä luonut mukautettuja rooleja.",
@@ -1272,6 +1272,7 @@
"rootTitle": "Pääkäyttäjä", "rootTitle": "Pääkäyttäjä",
"selectAll": "Valitse kaikki", "selectAll": "Valitse kaikki",
"sidebar": { "sidebar": {
"access": "Käyttöoikeudet",
"apiTokens": "API-tunnukset", "apiTokens": "API-tunnukset",
"configurations": "Järjestelmäasetukset", "configurations": "Järjestelmäasetukset",
"profile": "Profiili", "profile": "Profiili",
@@ -1300,6 +1301,9 @@
"desc": "Haluatko varmasti kirjautua ulos? Sinun täytyy kirjautua uudelleen päästäksesi tilillesi.", "desc": "Haluatko varmasti kirjautua ulos? Sinun täytyy kirjautua uudelleen päästäksesi tilillesi.",
"title": "Kirjaudu ulos" "title": "Kirjaudu ulos"
}, },
"system": {
"notifications": "Järjestelmäilmoitukset"
},
"systemConfig": { "systemConfig": {
"pageDescription": "Nämä parametrit alustetaan CLI-lippujen tai ympäristömuuttujien kautta. Ne ovat vain luku -tilassa.", "pageDescription": "Nämä parametrit alustetaan CLI-lippujen tai ympäristömuuttujien kautta. Ne ovat vain luku -tilassa.",
"pageTitle": "Järjestelmätason konfiguraatiot", "pageTitle": "Järjestelmätason konfiguraatiot",
+5 -1
View File
@@ -915,7 +915,7 @@
"role_badge": "Rôle {{type}}", "role_badge": "Rôle {{type}}",
"title": "Permissions : {{name}}", "title": "Permissions : {{name}}",
"unknown": "Rôle inconnu", "unknown": "Rôle inconnu",
"view_permissions": "Détails des permissions" "view_permissions": "Détails"
}, },
"empty": { "empty": {
"desc": "Vous n'avez pas encore créé de rôles personnalisés.", "desc": "Vous n'avez pas encore créé de rôles personnalisés.",
@@ -1272,6 +1272,7 @@
"rootTitle": "Root", "rootTitle": "Root",
"selectAll": "Tout sélectionner", "selectAll": "Tout sélectionner",
"sidebar": { "sidebar": {
"access": "Droits d'accès",
"apiTokens": "Jetons API", "apiTokens": "Jetons API",
"configurations": "Configurations système", "configurations": "Configurations système",
"profile": "Profil", "profile": "Profil",
@@ -1300,6 +1301,9 @@
"desc": "Êtes-vous sûr de vouloir vous déconnecter ? Vous devrez vous reconnecter pour accéder à votre compte.", "desc": "Êtes-vous sûr de vouloir vous déconnecter ? Vous devrez vous reconnecter pour accéder à votre compte.",
"title": "Déconnexion" "title": "Déconnexion"
}, },
"system": {
"notifications": "Notifications système"
},
"systemConfig": { "systemConfig": {
"pageDescription": "Ces paramètres sont initialisés via des drapeaux CLI ou des variables d'environnement. Ils sont en lecture seule pour garantir la cohérence de l'environnement.", "pageDescription": "Ces paramètres sont initialisés via des drapeaux CLI ou des variables d'environnement. Ils sont en lecture seule pour garantir la cohérence de l'environnement.",
"pageTitle": "Configurations au niveau système", "pageTitle": "Configurations au niveau système",
+5 -1
View File
@@ -915,7 +915,7 @@
"role_badge": "Ruolo {{type}}", "role_badge": "Ruolo {{type}}",
"title": "Permessi: {{name}}", "title": "Permessi: {{name}}",
"unknown": "Ruolo sconosciuto", "unknown": "Ruolo sconosciuto",
"view_permissions": "Dettagli permessi" "view_permissions": "Dettagli"
}, },
"empty": { "empty": {
"desc": "Non hai ancora creato alcun ruolo personalizzato.", "desc": "Non hai ancora creato alcun ruolo personalizzato.",
@@ -1272,6 +1272,7 @@
"rootTitle": "Root", "rootTitle": "Root",
"selectAll": "Seleziona Tutto", "selectAll": "Seleziona Tutto",
"sidebar": { "sidebar": {
"access": "Permessi di accesso",
"apiTokens": "Token API", "apiTokens": "Token API",
"configurations": "Configurazioni di sistema", "configurations": "Configurazioni di sistema",
"profile": "Profilo", "profile": "Profilo",
@@ -1300,6 +1301,9 @@
"desc": "Sei sicuro di voler disconnetterti? Dovrai accedere di nuovo per usare il tuo account.", "desc": "Sei sicuro di voler disconnetterti? Dovrai accedere di nuovo per usare il tuo account.",
"title": "Disconnetti" "title": "Disconnetti"
}, },
"system": {
"notifications": "Notifiche di sistema"
},
"systemConfig": { "systemConfig": {
"pageDescription": "Questi parametri sono inizializzati tramite flag CLI o variabili d'ambiente. Sono di sola lettura per garantire la coerenza.", "pageDescription": "Questi parametri sono inizializzati tramite flag CLI o variabili d'ambiente. Sono di sola lettura per garantire la coerenza.",
"pageTitle": "Configurazioni a livello di sistema", "pageTitle": "Configurazioni a livello di sistema",
+5 -1
View File
@@ -915,7 +915,7 @@
"role_badge": "{{type}} ロール", "role_badge": "{{type}} ロール",
"title": "ロール権限: {{name}}", "title": "ロール権限: {{name}}",
"unknown": "不明なロール", "unknown": "不明なロール",
"view_permissions": "権限の詳細" "view_permissions": "詳細"
}, },
"empty": { "empty": {
"desc": "カスタムロールがまだ作成されていません。", "desc": "カスタムロールがまだ作成されていません。",
@@ -1272,6 +1272,7 @@
"rootTitle": "ルート", "rootTitle": "ルート",
"selectAll": "すべて選択", "selectAll": "すべて選択",
"sidebar": { "sidebar": {
"access": "アクセス権限",
"apiTokens": "APIトークン", "apiTokens": "APIトークン",
"configurations": "システム設定", "configurations": "システム設定",
"profile": "プロフィール", "profile": "プロフィール",
@@ -1300,6 +1301,9 @@
"desc": "本当にサインアウトしますか? アカウントにアクセスするには再度サインインが必要です。", "desc": "本当にサインアウトしますか? アカウントにアクセスするには再度サインインが必要です。",
"title": "サインアウト" "title": "サインアウト"
}, },
"system": {
"notifications": "システム通知"
},
"systemConfig": { "systemConfig": {
"pageDescription": "これらのパラメータは、CLIフラグまたは環境変数を介して初期化されます。環境の一貫性を確保するため、現在は読み取り専用です。", "pageDescription": "これらのパラメータは、CLIフラグまたは環境変数を介して初期化されます。環境の一貫性を確保するため、現在は読み取り専用です。",
"pageTitle": "システムレベルの設定", "pageTitle": "システムレベルの設定",
+5 -1
View File
@@ -915,7 +915,7 @@
"role_badge": "{{type}} 역할", "role_badge": "{{type}} 역할",
"title": "역할 권한: {{name}}", "title": "역할 권한: {{name}}",
"unknown": "알 수 없는 역할", "unknown": "알 수 없는 역할",
"view_permissions": "권한 상세 정보" "view_permissions": "상세 정보"
}, },
"empty": { "empty": {
"desc": "사용자 정의 역할이 아직 생성되지 않았습니다.", "desc": "사용자 정의 역할이 아직 생성되지 않았습니다.",
@@ -1272,6 +1272,7 @@
"rootTitle": "루트", "rootTitle": "루트",
"selectAll": "모두 선택", "selectAll": "모두 선택",
"sidebar": { "sidebar": {
"access": "액세스 권한",
"apiTokens": "API 토큰", "apiTokens": "API 토큰",
"configurations": "시스템 설정", "configurations": "시스템 설정",
"profile": "프로필", "profile": "프로필",
@@ -1300,6 +1301,9 @@
"desc": "로그아웃하시겠습니까? 계정에 접근하려면 다시 로그인해야 합니다.", "desc": "로그아웃하시겠습니까? 계정에 접근하려면 다시 로그인해야 합니다.",
"title": "로그아웃" "title": "로그아웃"
}, },
"system": {
"notifications": "시스템 알림"
},
"systemConfig": { "systemConfig": {
"pageDescription": "이 매개변수는 CLI 플래그 또는 환경 변수를 통해 초기화됩니다. 환경의 일관성을 위해 현재 읽기 전용입니다.", "pageDescription": "이 매개변수는 CLI 플래그 또는 환경 변수를 통해 초기화됩니다. 환경의 일관성을 위해 현재 읽기 전용입니다.",
"pageTitle": "시스템 레벨 구성", "pageTitle": "시스템 레벨 구성",
+5 -1
View File
@@ -915,7 +915,7 @@
"role_badge": "{{type}} rol", "role_badge": "{{type}} rol",
"title": "Permissies: {{name}}", "title": "Permissies: {{name}}",
"unknown": "Onbekende rol", "unknown": "Onbekende rol",
"view_permissions": "Details bekijken" "view_permissions": "Details"
}, },
"empty": { "empty": {
"desc": "U heeft nog geen aangepaste rollen gemaakt.", "desc": "U heeft nog geen aangepaste rollen gemaakt.",
@@ -1272,6 +1272,7 @@
"rootTitle": "Root", "rootTitle": "Root",
"selectAll": "Alles selecteren", "selectAll": "Alles selecteren",
"sidebar": { "sidebar": {
"access": "Toegangsrechten",
"apiTokens": "API-tokens", "apiTokens": "API-tokens",
"configurations": "Systeemconfiguraties", "configurations": "Systeemconfiguraties",
"profile": "Profiel", "profile": "Profiel",
@@ -1300,6 +1301,9 @@
"desc": "Weet je zeker dat je wilt uitloggen? Je moet opnieuw inloggen om toegang te krijgen tot je account.", "desc": "Weet je zeker dat je wilt uitloggen? Je moet opnieuw inloggen om toegang te krijgen tot je account.",
"title": "Uitloggen" "title": "Uitloggen"
}, },
"system": {
"notifications": "Systeemmeldingen"
},
"systemConfig": { "systemConfig": {
"pageDescription": "Deze parameters worden geïnitialiseerd via CLI-flags of omgevingsvariabelen. Ze zijn alleen-lezen.", "pageDescription": "Deze parameters worden geïnitialiseerd via CLI-flags of omgevingsvariabelen. Ze zijn alleen-lezen.",
"pageTitle": "Systeemconfiguraties", "pageTitle": "Systeemconfiguraties",
+5 -1
View File
@@ -915,7 +915,7 @@
"role_badge": "{{type}}-rolle", "role_badge": "{{type}}-rolle",
"title": "Tillatelser: {{name}}", "title": "Tillatelser: {{name}}",
"unknown": "Ukjent rolle", "unknown": "Ukjent rolle",
"view_permissions": "Se detaljer" "view_permissions": "Detaljer"
}, },
"empty": { "empty": {
"desc": "Du har ikke opprettet noen egendefinerte roller ennå.", "desc": "Du har ikke opprettet noen egendefinerte roller ennå.",
@@ -1272,6 +1272,7 @@
"rootTitle": "Root", "rootTitle": "Root",
"selectAll": "Velg alle", "selectAll": "Velg alle",
"sidebar": { "sidebar": {
"access": "Tilgang",
"apiTokens": "API-tokener", "apiTokens": "API-tokener",
"configurations": "Systemkonfigurasjoner", "configurations": "Systemkonfigurasjoner",
"profile": "Profil", "profile": "Profil",
@@ -1300,6 +1301,9 @@
"desc": "Er du sikker på at du vil logge ut? Du må logge inn igjen for å få tilgang til kontoen din.", "desc": "Er du sikker på at du vil logge ut? Du må logge inn igjen for å få tilgang til kontoen din.",
"title": "Logg ut" "title": "Logg ut"
}, },
"system": {
"notifications": "Systemvarsler"
},
"systemConfig": { "systemConfig": {
"pageDescription": "Disse parametrene initialiseres via CLI-flagg eller miljøvariabler. De er skrivebeskyttede.", "pageDescription": "Disse parametrene initialiseres via CLI-flagg eller miljøvariabler. De er skrivebeskyttede.",
"pageTitle": "Systemkonfigurasjoner", "pageTitle": "Systemkonfigurasjoner",
+5 -1
View File
@@ -915,7 +915,7 @@
"role_badge": "Rola {{type}}", "role_badge": "Rola {{type}}",
"title": "Uprawnienia: {{name}}", "title": "Uprawnienia: {{name}}",
"unknown": "Nieznana rola", "unknown": "Nieznana rola",
"view_permissions": "Zobacz szczegóły" "view_permissions": "Szczegóły"
}, },
"empty": { "empty": {
"desc": "Nie utworzyłeś jeszcze żadnych własnych ról.", "desc": "Nie utworzyłeś jeszcze żadnych własnych ról.",
@@ -1272,6 +1272,7 @@
"rootTitle": "Root", "rootTitle": "Root",
"selectAll": "Zaznacz wszystko", "selectAll": "Zaznacz wszystko",
"sidebar": { "sidebar": {
"access": "Uprawnienia dostępu",
"apiTokens": "Tokeny API", "apiTokens": "Tokeny API",
"configurations": "Konfiguracja systemu", "configurations": "Konfiguracja systemu",
"profile": "Profil", "profile": "Profil",
@@ -1300,6 +1301,9 @@
"desc": "Czy na pewno chcesz się wylogować? Aby uzyskać dostęp do konta, będziesz musiał zalogować się ponownie.", "desc": "Czy na pewno chcesz się wylogować? Aby uzyskać dostęp do konta, będziesz musiał zalogować się ponownie.",
"title": "Wyloguj się" "title": "Wyloguj się"
}, },
"system": {
"notifications": "Powiadomienia systemowe"
},
"systemConfig": { "systemConfig": {
"pageDescription": "Parametry te są inicjowane za pomocą flag CLI lub zmiennych środowiskowych. Są one obecnie tylko do odczytu.", "pageDescription": "Parametry te są inicjowane za pomocą flag CLI lub zmiennych środowiskowych. Są one obecnie tylko do odczytu.",
"pageTitle": "Konfiguracje na poziomie systemu", "pageTitle": "Konfiguracje na poziomie systemu",
+5 -1
View File
@@ -915,7 +915,7 @@
"role_badge": "Função {{type}}", "role_badge": "Função {{type}}",
"title": "Permissões: {{name}}", "title": "Permissões: {{name}}",
"unknown": "Função desconhecida", "unknown": "Função desconhecida",
"view_permissions": "Ver detalhes" "view_permissions": "Detalhes"
}, },
"empty": { "empty": {
"desc": "Você ainda não criou funções personalizadas.", "desc": "Você ainda não criou funções personalizadas.",
@@ -1272,6 +1272,7 @@
"rootTitle": "Root", "rootTitle": "Root",
"selectAll": "Selecionar Todos", "selectAll": "Selecionar Todos",
"sidebar": { "sidebar": {
"access": "Permissões de acesso",
"apiTokens": "Tokens API", "apiTokens": "Tokens API",
"configurations": "Configurações do sistema", "configurations": "Configurações do sistema",
"profile": "Perfil", "profile": "Perfil",
@@ -1300,6 +1301,9 @@
"desc": "Tem certeza de que deseja sair? Você precisará entrar novamente para acessar sua conta.", "desc": "Tem certeza de que deseja sair? Você precisará entrar novamente para acessar sua conta.",
"title": "Sair" "title": "Sair"
}, },
"system": {
"notifications": "Notificações do sistema"
},
"systemConfig": { "systemConfig": {
"pageDescription": "Estes parâmetros são inicializados via flags CLI ou variáveis de ambiente. Estão em modo somente leitura.", "pageDescription": "Estes parâmetros são inicializados via flags CLI ou variáveis de ambiente. Estão em modo somente leitura.",
"pageTitle": "Configurações a nível de sistema", "pageTitle": "Configurações a nível de sistema",
+5 -1
View File
@@ -915,7 +915,7 @@
"role_badge": "Роль: {{type}}", "role_badge": "Роль: {{type}}",
"title": "Права роли: {{name}}", "title": "Права роли: {{name}}",
"unknown": "Неизвестная роль", "unknown": "Неизвестная роль",
"view_permissions": "Детали прав" "view_permissions": "Подробнее"
}, },
"empty": { "empty": {
"desc": "Вы еще не создали ни одной кастомной роли.", "desc": "Вы еще не создали ни одной кастомной роли.",
@@ -1272,6 +1272,7 @@
"rootTitle": "Root", "rootTitle": "Root",
"selectAll": "Выбрать все", "selectAll": "Выбрать все",
"sidebar": { "sidebar": {
"access": "Права доступа",
"apiTokens": "API-токены", "apiTokens": "API-токены",
"configurations": "Системные конфигурации", "configurations": "Системные конфигурации",
"profile": "Профиль", "profile": "Профиль",
@@ -1300,6 +1301,9 @@
"desc": "Вы уверены, что хотите выйти? Вам нужно будет снова войти, чтобы получить доступ к аккаунту.", "desc": "Вы уверены, что хотите выйти? Вам нужно будет снова войти, чтобы получить доступ к аккаунту.",
"title": "Выйти" "title": "Выйти"
}, },
"system": {
"notifications": "Системные уведомления"
},
"systemConfig": { "systemConfig": {
"pageDescription": "Эти параметры инициализируются через флаги CLI или переменные окружения. Они доступны только для чтения.", "pageDescription": "Эти параметры инициализируются через флаги CLI или переменные окружения. Они доступны только для чтения.",
"pageTitle": "Конфигурации системного уровня", "pageTitle": "Конфигурации системного уровня",
+5 -1
View File
@@ -915,7 +915,7 @@
"role_badge": "{{type}} Roll", "role_badge": "{{type}} Roll",
"title": "Rollbehörigheter: {{name}}", "title": "Rollbehörigheter: {{name}}",
"unknown": "Okänd roll", "unknown": "Okänd roll",
"view_permissions": "Visa behörigheter" "view_permissions": "Detaljer"
}, },
"empty": { "empty": {
"desc": "Du har inte skapat några anpassade roller än.", "desc": "Du har inte skapat några anpassade roller än.",
@@ -1272,6 +1272,7 @@
"rootTitle": "Root", "rootTitle": "Root",
"selectAll": "Välj alla", "selectAll": "Välj alla",
"sidebar": { "sidebar": {
"access": "Behörigheter",
"apiTokens": "API-tokens", "apiTokens": "API-tokens",
"configurations": "Systemkonfigurationer", "configurations": "Systemkonfigurationer",
"profile": "Profil", "profile": "Profil",
@@ -1300,6 +1301,9 @@
"desc": "Är du säker på att du vill logga ut? Du måste logga in igen för att få åtkomst till ditt konto.", "desc": "Är du säker på att du vill logga ut? Du måste logga in igen för att få åtkomst till ditt konto.",
"title": "Logga ut" "title": "Logga ut"
}, },
"system": {
"notifications": "Systemaviseringar"
},
"systemConfig": { "systemConfig": {
"pageDescription": "Dessa parametrar initieras via CLI-flaggor eller miljövariabler. De är skrivskyddade.", "pageDescription": "Dessa parametrar initieras via CLI-flaggor eller miljövariabler. De är skrivskyddade.",
"pageTitle": "Systemkonfigurationer", "pageTitle": "Systemkonfigurationer",
+5 -1
View File
@@ -915,7 +915,7 @@
"role_badge": "{{type}} 角色", "role_badge": "{{type}} 角色",
"title": "角色權限: {{name}}", "title": "角色權限: {{name}}",
"unknown": "未知角色", "unknown": "未知角色",
"view_permissions": "權限詳情" "view_permissions": "詳情"
}, },
"empty": { "empty": {
"desc": "您尚未建立任何自定義角色。點擊下方按鈕開始新增新角色。", "desc": "您尚未建立任何自定義角色。點擊下方按鈕開始新增新角色。",
@@ -1272,6 +1272,7 @@
"rootTitle": "根目錄", "rootTitle": "根目錄",
"selectAll": "全選", "selectAll": "全選",
"sidebar": { "sidebar": {
"access": "訪問權限",
"apiTokens": "API 權杖", "apiTokens": "API 權杖",
"configurations": "系統配置", "configurations": "系統配置",
"profile": "個人資料", "profile": "個人資料",
@@ -1300,6 +1301,9 @@
"desc": "您確定要登出嗎?您需要重新登入才能存取帳戶。", "desc": "您確定要登出嗎?您需要重新登入才能存取帳戶。",
"title": "登出" "title": "登出"
}, },
"system": {
"notifications": "系統通知"
},
"systemConfig": { "systemConfig": {
"pageDescription": "這些參數透過 CLI 標誌或環境變數初始化。它們目前為唯讀,以確保環境一致性。", "pageDescription": "這些參數透過 CLI 標誌或環境變數初始化。它們目前為唯讀,以確保環境一致性。",
"pageTitle": "系統級配置", "pageTitle": "系統級配置",
+5 -1
View File
@@ -915,7 +915,7 @@
"role_badge": "{{type}} 角色", "role_badge": "{{type}} 角色",
"title": "角色权限: {{name}}", "title": "角色权限: {{name}}",
"unknown": "未知角色", "unknown": "未知角色",
"view_permissions": "权限详情" "view_permissions": "详情"
}, },
"empty": { "empty": {
"desc": "您尚未创建任何自定义角色。点击下方按钮开始添加新角色。", "desc": "您尚未创建任何自定义角色。点击下方按钮开始添加新角色。",
@@ -1272,6 +1272,7 @@
"rootTitle": "管理员账户", "rootTitle": "管理员账户",
"selectAll": "全选", "selectAll": "全选",
"sidebar": { "sidebar": {
"access": "访问权限",
"apiTokens": "API 令牌", "apiTokens": "API 令牌",
"configurations": "系统配置", "configurations": "系统配置",
"profile": "个人资料", "profile": "个人资料",
@@ -1300,6 +1301,9 @@
"desc": "您确定要退出登录吗?您需要重新登录才能访问账户。", "desc": "您确定要退出登录吗?您需要重新登录才能访问账户。",
"title": "退出登录" "title": "退出登录"
}, },
"system": {
"notifications": "系统通知"
},
"systemConfig": { "systemConfig": {
"pageDescription": "这些参数通过 CLI 标志或环境变量初始化。它们当前为只读,以确保环境一致性。", "pageDescription": "这些参数通过 CLI 标志或环境变量初始化。它们当前为只读,以确保环境一致性。",
"pageTitle": "系统级配置", "pageTitle": "系统级配置",
+32
View File
@@ -72,6 +72,9 @@ const AuthenticatedSettingsAppearanceLazyImport = createFileRoute(
const AuthenticatedSettingsApiTokensLazyImport = createFileRoute( const AuthenticatedSettingsApiTokensLazyImport = createFileRoute(
'/_authenticated/settings/api-tokens', '/_authenticated/settings/api-tokens',
)() )()
const AuthenticatedSettingsAccessLazyImport = createFileRoute(
'/_authenticated/settings/access',
)()
// Create/Update Routes // Create/Update Routes
@@ -298,6 +301,15 @@ const AuthenticatedSettingsApiTokensLazyRoute =
), ),
) )
const AuthenticatedSettingsAccessLazyRoute =
AuthenticatedSettingsAccessLazyImport.update({
id: '/access',
path: '/access',
getParentRoute: () => AuthenticatedSettingsRouteLazyRoute,
} as any).lazy(() =>
import('./routes/_authenticated/settings/access.lazy').then((d) => d.Route),
)
// Populate the FileRoutesByPath interface // Populate the FileRoutesByPath interface
declare module '@tanstack/react-router' { declare module '@tanstack/react-router' {
@@ -379,6 +391,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthenticatedIndexImport preLoaderRoute: typeof AuthenticatedIndexImport
parentRoute: typeof AuthenticatedRouteImport parentRoute: typeof AuthenticatedRouteImport
} }
'/_authenticated/settings/access': {
id: '/_authenticated/settings/access'
path: '/access'
fullPath: '/settings/access'
preLoaderRoute: typeof AuthenticatedSettingsAccessLazyImport
parentRoute: typeof AuthenticatedSettingsRouteLazyImport
}
'/_authenticated/settings/api-tokens': { '/_authenticated/settings/api-tokens': {
id: '/_authenticated/settings/api-tokens' id: '/_authenticated/settings/api-tokens'
path: '/api-tokens' path: '/api-tokens'
@@ -490,6 +509,7 @@ declare module '@tanstack/react-router' {
// Create and export the route tree // Create and export the route tree
interface AuthenticatedSettingsRouteLazyRouteChildren { interface AuthenticatedSettingsRouteLazyRouteChildren {
AuthenticatedSettingsAccessLazyRoute: typeof AuthenticatedSettingsAccessLazyRoute
AuthenticatedSettingsApiTokensLazyRoute: typeof AuthenticatedSettingsApiTokensLazyRoute AuthenticatedSettingsApiTokensLazyRoute: typeof AuthenticatedSettingsApiTokensLazyRoute
AuthenticatedSettingsAppearanceLazyRoute: typeof AuthenticatedSettingsAppearanceLazyRoute AuthenticatedSettingsAppearanceLazyRoute: typeof AuthenticatedSettingsAppearanceLazyRoute
AuthenticatedSettingsConfigurationsLazyRoute: typeof AuthenticatedSettingsConfigurationsLazyRoute AuthenticatedSettingsConfigurationsLazyRoute: typeof AuthenticatedSettingsConfigurationsLazyRoute
@@ -500,6 +520,7 @@ interface AuthenticatedSettingsRouteLazyRouteChildren {
const AuthenticatedSettingsRouteLazyRouteChildren: AuthenticatedSettingsRouteLazyRouteChildren = const AuthenticatedSettingsRouteLazyRouteChildren: AuthenticatedSettingsRouteLazyRouteChildren =
{ {
AuthenticatedSettingsAccessLazyRoute: AuthenticatedSettingsAccessLazyRoute,
AuthenticatedSettingsApiTokensLazyRoute: AuthenticatedSettingsApiTokensLazyRoute:
AuthenticatedSettingsApiTokensLazyRoute, AuthenticatedSettingsApiTokensLazyRoute,
AuthenticatedSettingsAppearanceLazyRoute: AuthenticatedSettingsAppearanceLazyRoute:
@@ -576,6 +597,7 @@ export interface FileRoutesByFullPath {
'/404': typeof errors404LazyRoute '/404': typeof errors404LazyRoute
'/503': typeof errors503LazyRoute '/503': typeof errors503LazyRoute
'/': typeof AuthenticatedIndexRoute '/': typeof AuthenticatedIndexRoute
'/settings/access': typeof AuthenticatedSettingsAccessLazyRoute
'/settings/api-tokens': typeof AuthenticatedSettingsApiTokensLazyRoute '/settings/api-tokens': typeof AuthenticatedSettingsApiTokensLazyRoute
'/settings/appearance': typeof AuthenticatedSettingsAppearanceLazyRoute '/settings/appearance': typeof AuthenticatedSettingsAppearanceLazyRoute
'/settings/configurations': typeof AuthenticatedSettingsConfigurationsLazyRoute '/settings/configurations': typeof AuthenticatedSettingsConfigurationsLazyRoute
@@ -601,6 +623,7 @@ export interface FileRoutesByTo {
'/404': typeof errors404LazyRoute '/404': typeof errors404LazyRoute
'/503': typeof errors503LazyRoute '/503': typeof errors503LazyRoute
'/': typeof AuthenticatedIndexRoute '/': typeof AuthenticatedIndexRoute
'/settings/access': typeof AuthenticatedSettingsAccessLazyRoute
'/settings/api-tokens': typeof AuthenticatedSettingsApiTokensLazyRoute '/settings/api-tokens': typeof AuthenticatedSettingsApiTokensLazyRoute
'/settings/appearance': typeof AuthenticatedSettingsAppearanceLazyRoute '/settings/appearance': typeof AuthenticatedSettingsAppearanceLazyRoute
'/settings/configurations': typeof AuthenticatedSettingsConfigurationsLazyRoute '/settings/configurations': typeof AuthenticatedSettingsConfigurationsLazyRoute
@@ -631,6 +654,7 @@ export interface FileRoutesById {
'/(errors)/500': typeof errors500LazyRoute '/(errors)/500': typeof errors500LazyRoute
'/(errors)/503': typeof errors503LazyRoute '/(errors)/503': typeof errors503LazyRoute
'/_authenticated/': typeof AuthenticatedIndexRoute '/_authenticated/': typeof AuthenticatedIndexRoute
'/_authenticated/settings/access': typeof AuthenticatedSettingsAccessLazyRoute
'/_authenticated/settings/api-tokens': typeof AuthenticatedSettingsApiTokensLazyRoute '/_authenticated/settings/api-tokens': typeof AuthenticatedSettingsApiTokensLazyRoute
'/_authenticated/settings/appearance': typeof AuthenticatedSettingsAppearanceLazyRoute '/_authenticated/settings/appearance': typeof AuthenticatedSettingsAppearanceLazyRoute
'/_authenticated/settings/configurations': typeof AuthenticatedSettingsConfigurationsLazyRoute '/_authenticated/settings/configurations': typeof AuthenticatedSettingsConfigurationsLazyRoute
@@ -661,6 +685,7 @@ export interface FileRouteTypes {
| '/404' | '/404'
| '/503' | '/503'
| '/' | '/'
| '/settings/access'
| '/settings/api-tokens' | '/settings/api-tokens'
| '/settings/appearance' | '/settings/appearance'
| '/settings/configurations' | '/settings/configurations'
@@ -685,6 +710,7 @@ export interface FileRouteTypes {
| '/404' | '/404'
| '/503' | '/503'
| '/' | '/'
| '/settings/access'
| '/settings/api-tokens' | '/settings/api-tokens'
| '/settings/appearance' | '/settings/appearance'
| '/settings/configurations' | '/settings/configurations'
@@ -713,6 +739,7 @@ export interface FileRouteTypes {
| '/(errors)/500' | '/(errors)/500'
| '/(errors)/503' | '/(errors)/503'
| '/_authenticated/' | '/_authenticated/'
| '/_authenticated/settings/access'
| '/_authenticated/settings/api-tokens' | '/_authenticated/settings/api-tokens'
| '/_authenticated/settings/appearance' | '/_authenticated/settings/appearance'
| '/_authenticated/settings/configurations' | '/_authenticated/settings/configurations'
@@ -797,6 +824,7 @@ export const routeTree = rootRoute
"filePath": "_authenticated/settings/route.lazy.tsx", "filePath": "_authenticated/settings/route.lazy.tsx",
"parent": "/_authenticated", "parent": "/_authenticated",
"children": [ "children": [
"/_authenticated/settings/access",
"/_authenticated/settings/api-tokens", "/_authenticated/settings/api-tokens",
"/_authenticated/settings/appearance", "/_authenticated/settings/appearance",
"/_authenticated/settings/configurations", "/_authenticated/settings/configurations",
@@ -833,6 +861,10 @@ export const routeTree = rootRoute
"filePath": "_authenticated/index.tsx", "filePath": "_authenticated/index.tsx",
"parent": "/_authenticated" "parent": "/_authenticated"
}, },
"/_authenticated/settings/access": {
"filePath": "_authenticated/settings/access.lazy.tsx",
"parent": "/_authenticated/settings"
},
"/_authenticated/settings/api-tokens": { "/_authenticated/settings/api-tokens": {
"filePath": "_authenticated/settings/api-tokens.lazy.tsx", "filePath": "_authenticated/settings/api-tokens.lazy.tsx",
"parent": "/_authenticated/settings" "parent": "/_authenticated/settings"
@@ -0,0 +1,6 @@
import { AccountAccessList } from '@/features/settings/access'
import { createLazyFileRoute } from '@tanstack/react-router'
export const Route = createLazyFileRoute('/_authenticated/settings/access')({
component: AccountAccessList,
})