This commit is contained in:
lovebird 2026-04-06 23:28:59 +02:00
parent 24a54fd2e6
commit d80d046f65
9 changed files with 330 additions and 59 deletions

View File

@ -6,6 +6,8 @@ import { QueryClientProvider } from "@tanstack/react-query";
import { queryClient } from "@/lib/queryClient";
import { BrowserRouter, Routes, Route, useLocation } from "react-router-dom";
import { AuthProvider, useAuth } from "@/hooks/useAuth";
import { AuthProvider as OidcProvider } from "react-oidc-context";
import { WebStorageStateStore } from "oidc-client-ts";
import { LogProvider } from "@/contexts/LogContext";
@ -24,6 +26,7 @@ registerAllWidgets();
import Index from "./pages/Index";
import Auth from "./pages/Auth";
import AuthZ from "./pages/AuthZ";
const UpdatePassword = React.lazy(() => import("./pages/UpdatePassword"));
@ -55,6 +58,7 @@ let TypesPlayground: any;
let VariablePlayground: any;
let I18nPlayground: any;
let PlaygroundChat: any;
let PlaygroundVfs: any;
let GridSearch: any;
let LocationDetail: any;
let Tetris: any;
@ -79,6 +83,7 @@ if (enablePlaygrounds) {
VariablePlayground = React.lazy(() => import("./components/variables/VariablesEditor").then(module => ({ default: module.VariablesEditor })));
I18nPlayground = React.lazy(() => import("./components/playground/I18nPlayground"));
PlaygroundChat = React.lazy(() => import("./pages/PlaygroundChat"));
PlaygroundVfs = React.lazy(() => import("./pages/PlaygroundVfs"));
SupportChat = React.lazy(() => import("./pages/SupportChat"));
}
@ -130,6 +135,7 @@ const AppWrapper = () => {
{/* Top-level routes (no organization context) */}
<Route path="/" element={<Index />} />
<Route path="/auth" element={<Auth />} />
<Route path="/authz" element={<AuthZ />} />
<Route path="/auth/update-password" element={<React.Suspense fallback={<div>Loading...</div>}><UpdatePassword /></React.Suspense>} />
<Route path="/profile/*" element={<Profile />} />
<Route path="/post/new" element={<React.Suspense fallback={<div>Loading...</div>}><EditPost /></React.Suspense>} />
@ -192,6 +198,7 @@ const AppWrapper = () => {
<Route path="/variables-editor" element={<React.Suspense fallback={<div>Loading...</div>}><VariablePlayground /></React.Suspense>} />
<Route path="/playground/i18n" element={<React.Suspense fallback={<div>Loading...</div>}><I18nPlayground /></React.Suspense>} />
<Route path="/playground/chat" element={<React.Suspense fallback={<div>Loading...</div>}><PlaygroundChat /></React.Suspense>} />
<Route path="/playground/vfs" element={<React.Suspense fallback={<div>Loading...</div>}><PlaygroundVfs /></React.Suspense>} />
</>
)}
@ -251,10 +258,27 @@ const App = () => {
initFormatDetection();
}, []);
const oidcConfig = {
authority: "https://auth.polymech.info",
client_id: "367440527605432321",
redirect_uri: window.location.origin + "/authz", // Where Zitadel sends the code back to
post_logout_redirect_uri: window.location.origin,
response_type: "code",
scope: "openid profile email",
loadUserInfo: true, // Specifically instruct the client to fetch /userinfo
// Store tokens in localStorage instead of sessionStorage so they survive new tabs
userStore: new WebStorageStateStore({ store: window.localStorage }),
onSigninCallback: () => {
// Clean up the URL after successful login
window.history.replaceState({}, document.title, window.location.pathname);
}
};
return (
<HelmetProvider>
<SWRConfig value={{ provider: () => new Map() }}>
<QueryClientProvider client={queryClient}>
<OidcProvider {...oidcConfig}>
<QueryClientProvider client={queryClient}>
<AuthProvider>
<LogProvider>
<MediaRefreshProvider>
@ -281,9 +305,10 @@ const App = () => {
</MediaRefreshProvider>
</LogProvider>
</AuthProvider>
</QueryClientProvider>
</QueryClientProvider>
</OidcProvider>
</SWRConfig>
</HelmetProvider >
</HelmetProvider>
);
};

View File

@ -153,11 +153,12 @@ const FileBrowser: React.FC<{
mode?: 'simple' | 'advanced',
index?: boolean,
disableRoutingSync?: boolean,
initialMount?: string,
onSelect?: (node: INode | null, mount?: string) => void
}> = ({ allowPanels, mode, index, disableRoutingSync, onSelect }) => {
}> = ({ allowPanels, mode, index, disableRoutingSync, initialMount: propInitialMount, onSelect }) => {
const location = useLocation();
let initialMount: string | undefined;
let initialMount = propInitialMount;
let initialPath: string | undefined;
if (!disableRoutingSync) {
@ -166,10 +167,10 @@ const FileBrowser: React.FC<{
const segments = urlRest.split('/').filter(Boolean);
if (segments.length > 0) {
initialMount = segments[0];
initialMount = initialMount || segments[0];
initialPath = segments.slice(1).join('/');
} else {
initialMount = localStorage.getItem('fb-last-mount') || undefined;
initialMount = initialMount || localStorage.getItem('fb-last-mount') || undefined;
initialPath = localStorage.getItem('fb-last-path') || undefined;
}
}

View File

@ -1,14 +1,16 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import { Loader2 } from 'lucide-react';
import { useAuth } from '@/hooks/useAuth';
import ImageLightbox from '@/components/ImageLightbox';
import LightboxText from '@/modules/storage/views/LightboxText';
import LightboxIframe from '@/modules/storage/views/LightboxIframe';
import { renderFileViewer } from '@/modules/storage/FileViewerRegistry';
import { useDragDrop } from '@/contexts/DragDropContext';
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@/components/ui/resizable';
import type { INode, SortKey } from '@/modules/storage/types';
import { getMimeCategory, vfsUrl, formatSize } from '@/modules/storage/helpers';
import { serverUrl } from '@/lib/db';
import FileBrowserToolbar from '@/modules/storage/FileBrowserToolbar';
import FileListView from '@/modules/storage/FileListView';
import FileGridView from '@/modules/storage/FileGridView';
@ -140,6 +142,37 @@ const FileBrowserPanel: React.FC<FileBrowserPanelProps> = ({
}
}, [onFilterChange]);
// ── Drag & Drop Uploads ───────────────────────────────────────
type UploadingNode = INode & { _uploading: boolean; _progress: number; _error?: string; _file?: File };
const { setLocalZoneActive, resetDragState } = useDragDrop();
const [uploads, setUploads] = useState<UploadingNode[]>([]);
const [isDragOver, setIsDragOver] = useState(false);
const handleDragEnter = (e: React.DragEvent) => {
if (!e.dataTransfer.types.includes('Files')) return;
e.preventDefault();
e.stopPropagation();
setLocalZoneActive(true);
setIsDragOver(true);
};
const handleDragOver = (e: React.DragEvent) => {
if (!e.dataTransfer.types.includes('Files')) return;
e.preventDefault();
e.stopPropagation();
if (!isDragOver) setIsDragOver(true);
};
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault();
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
if (e.clientX <= rect.left || e.clientX >= rect.right || e.clientY <= rect.top || e.clientY >= rect.bottom) {
setIsDragOver(false);
setLocalZoneActive(false);
}
};
// ── Available mounts ─────────────────────────────────────────
const [availableMounts, setAvailableMounts] = useState<string[]>([]);
@ -207,6 +240,90 @@ const FileBrowserPanel: React.FC<FileBrowserPanelProps> = ({
// ── View Mode & Zoom ─────────────────────────────────────────
const displayNodes = useMemo(() => [...sorted, ...(uploads as INode[])], [sorted, uploads]);
const handleDrop = async (e: React.DragEvent) => {
if (!e.dataTransfer.types.includes('Files')) return;
e.preventDefault();
e.stopPropagation();
setIsDragOver(false);
setLocalZoneActive(false);
resetDragState();
const files = Array.from(e.dataTransfer.files);
if (files.length === 0) return;
const newUploads = files.map((f, index) => ({
name: f.name,
path: currentPath ? `${currentPath}/${f.name}` : f.name,
size: f.size,
mtime: Date.now(),
parent: currentPath,
type: 'file',
_uploading: true,
_progress: 0,
_file: f
}));
setUploads(prev => [...prev, ...newUploads]);
const uploadOne = (uploadItem: typeof newUploads[0]) => new Promise<void>((resolve) => {
const xhr = new XMLHttpRequest();
const cleanDir = (currentPath || '').replace(/^\/+|\/+$/g, '');
const filePath = cleanDir ? `${cleanDir}/${uploadItem.name}` : uploadItem.name;
const uploadUrl = `${serverUrl}/api/vfs/upload/${mount}/${filePath}`;
xhr.open('POST', uploadUrl, true);
if (accessToken) {
xhr.setRequestHeader('Authorization', `Bearer ${accessToken}`);
}
xhr.upload.onprogress = (evt) => {
if (evt.lengthComputable) {
const percent = Math.round((evt.loaded / evt.total) * 100);
setUploads(prev => prev.map(u =>
u.name === uploadItem.name ? { ...u, _progress: percent } : u
));
}
};
const markError = (msg: string) => {
setUploads(prev => prev.map(u =>
u.name === uploadItem.name ? { ...u, _uploading: false, _error: msg } : u
));
setTimeout(() => {
setUploads(prev => prev.filter(u => u.name !== uploadItem.name));
}, 4000);
};
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
setUploads(prev => prev.filter(u => u.name !== uploadItem.name));
} else {
let msg = `Error ${xhr.status}`;
try {
const body = JSON.parse(xhr.responseText);
msg = body.message || body.error || msg;
} catch {}
markError(msg);
}
resolve();
};
xhr.onerror = () => {
markError('Network error');
resolve();
};
const formData = new FormData();
if (uploadItem._file) {
formData.append('file', uploadItem._file);
}
xhr.send(formData);
});
await Promise.all(newUploads.map(uploadOne));
fetchDir(currentPath || '/');
};
const [internalViewMode, setInternalViewMode] = useState<'list' | 'thumbs' | 'tree'>(() => {
if (autoSaveId) {
const saved = localStorage.getItem(`${autoSaveId}-viewMode`);
@ -283,7 +400,7 @@ const FileBrowserPanel: React.FC<FileBrowserPanelProps> = ({
handleItemClick,
clearSelection
} = useSelection({
sorted,
sorted: displayNodes,
canGoUp,
onSelect
});
@ -348,7 +465,7 @@ const FileBrowserPanel: React.FC<FileBrowserPanelProps> = ({
const [pendingFileSelect, setPendingFileSelect] = useState<string | null>(null);
const { goUp } = useDefaultSelectionHandler({
sorted,
sorted: displayNodes,
canGoUp,
rawGoUp,
currentPath,
@ -410,7 +527,7 @@ const FileBrowserPanel: React.FC<FileBrowserPanelProps> = ({
searchQuery,
isSearchMode,
onSelect,
sorted,
sorted: displayNodes,
});
// ── Default Actions ──────────────────────────────────────────
@ -436,7 +553,7 @@ const FileBrowserPanel: React.FC<FileBrowserPanelProps> = ({
pathProp,
accessToken,
selected,
sorted,
sorted: displayNodes,
canGoUp,
setFocusIdx,
setSelected,
@ -459,9 +576,14 @@ const FileBrowserPanel: React.FC<FileBrowserPanelProps> = ({
ref={containerRef}
data-testid="file-browser-panel"
tabIndex={viewMode === 'tree' ? undefined : 0}
className="fb-panel-container"
className={`fb-panel-container ${isDragOver ? 'ring-2 ring-primary bg-primary/5' : ''}`}
onKeyDown={handleKeyDown}
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
style={{
position: 'relative',
display: 'flex', flexDirection: 'column', height: '100%', minHeight: 0,
border: '1px solid var(--border, #334155)', borderRadius: 6, overflow: 'hidden',
@ -474,6 +596,27 @@ const FileBrowserPanel: React.FC<FileBrowserPanelProps> = ({
}
`}</style>
{/* ═══ Drop Overlay ═══════════════════════════════ */}
{isDragOver && (
<div style={{
position: 'absolute', inset: 0, zIndex: 50,
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 12,
background: 'rgba(59, 130, 246, 0.08)',
border: '2px dashed var(--primary, #3b82f6)',
borderRadius: 6,
pointerEvents: 'none',
}}>
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="var(--primary, #3b82f6)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="17 8 12 3 7 8" />
<line x1="12" y1="3" x2="12" y2="15" />
</svg>
<span style={{ fontSize: 15, fontWeight: 600, color: 'var(--primary, #3b82f6)', letterSpacing: '0.02em' }}>
Drop files to upload
</span>
</div>
)}
{/* ═══ Toolbar ═══════════════════════════════════ */}
{showToolbar && (
<FileBrowserToolbar
@ -553,7 +696,7 @@ const FileBrowserPanel: React.FC<FileBrowserPanelProps> = ({
{viewMode === 'tree' ? (
<div className="flex-1 min-h-0 overflow-hidden pt-1">
<FileTree
data={sorted}
data={displayNodes}
canGoUp={canGoUp}
onGoUp={goUp}
selectedId={selected.length === 1 ? selected[0].path : undefined}
@ -587,7 +730,7 @@ const FileBrowserPanel: React.FC<FileBrowserPanelProps> = ({
<div className="flex-1 min-h-0 overflow-hidden pt-1 flex flex-col w-full h-full">
<FileListView
listRef={listRef}
sorted={sorted}
sorted={displayNodes}
canGoUp={canGoUp}
goUp={goUp}
focusIdx={focusIdx}
@ -605,7 +748,7 @@ const FileBrowserPanel: React.FC<FileBrowserPanelProps> = ({
<div className="flex-1 flex flex-col min-h-0 overflow-hidden w-full h-full pt-1">
<FileGridView
listRef={listRef}
sorted={sorted}
sorted={displayNodes}
canGoUp={canGoUp}
goUp={goUp}
focusIdx={focusIdx}

View File

@ -237,7 +237,6 @@ export const CreationWizardPopup: React.FC<CreationWizardPopupProps> = ({
for (const img of preloadedImages) {
// Handle External Pages (Links)
if (img.type === 'page-external') {
console.log('Skipping upload for external page:', img.title);
await createPicture({
user_id: user.id,
title: img.title || 'Untitled Link',
@ -257,7 +256,6 @@ export const CreationWizardPopup: React.FC<CreationWizardPopupProps> = ({
if (file.type.startsWith('video/')) continue;
const { publicUrl } = await uploadImage(file, user.id);
console.log('image uploaded, url:', publicUrl);
let dbData = {
user_id: user.id,
@ -514,7 +512,7 @@ export const CreationWizardPopup: React.FC<CreationWizardPopupProps> = ({
) : (
<>
<Upload className="h-4 w-4 mr-2" />
<T>Upload Image</T>
<T>Upload as Picture</T>
</>
)}
</Button>

View File

@ -57,16 +57,19 @@ const FileGridView: React.FC<FileGridViewProps> = ({
const isDir = getMimeCategory(node) === 'dir';
const isFocused = focusIdx === idx;
const isSelected = selected.some(sel => sel.path === node.path);
const { _uploading, _progress, _error } = node as any;
return (
<div key={node.path || node.name} data-fb-idx={idx}
data-testid="file-grid-node"
data-node-id={node.path || node.name}
onClick={(e) => onItemClick(idx, e)}
onDoubleClick={() => onItemDoubleClick(idx)}
onDoubleClick={() => !_uploading && onItemDoubleClick(idx)}
className="fb-thumb" style={{
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
padding: 6, borderRadius: 6, cursor: 'pointer', gap: 6, overflow: 'hidden',
padding: 6, borderRadius: 6, cursor: _uploading ? 'default' : 'pointer', gap: 6, overflow: 'hidden',
background: isSelected ? 'rgba(59, 130, 246, 0.05)' : 'transparent',
opacity: (_uploading || _error) ? 0.7 : 1,
}}>
<div style={{
width: '100%', aspectRatio: '1/1',
@ -77,8 +80,29 @@ const FileGridView: React.FC<FileGridViewProps> = ({
borderStyle: isSelected ? 'solid' : 'solid',
outline: isFocused ? `2px solid ${FOCUS_BORDER}` : 'none',
outlineOffset: '2px',
position: 'relative'
}}>
{isDir ? <NodeIcon node={node} size={Math.max(24, Math.floor(thumbSize * 0.5))} /> : <ThumbPreview node={node} mount={mount} tokenParam={tokenParam} thumbSize={thumbSize} />}
{_uploading && (
<div style={{
position: 'absolute', bottom: 0, left: 0, right: 0, height: 6,
background: 'rgba(0,0,0,0.5)', overflow: 'hidden'
}}>
<div style={{
height: '100%', width: `${_progress || 0}%`,
background: 'var(--primary, #3b82f6)', transition: 'width 0.2s linear'
}} />
</div>
)}
{_error && (
<div style={{
position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center',
background: 'rgba(239, 68, 68, 0.12)', borderRadius: 6,
}}>
<span style={{ fontSize: 11, color: '#ef4444', fontWeight: 600, textAlign: 'center', padding: 4 }}>{_error}</span>
</div>
)}
</div>
<div style={{ display: 'flex', flexDirection: 'column', width: '100%', alignItems: 'center', gap: 2 }}>
<span style={{

View File

@ -53,21 +53,25 @@ const FileListView: React.FC<FileListViewProps> = ({
const isDir = getMimeCategory(node) === 'dir';
const isFocused = focusIdx === idx;
const isSelected = selected.some(sel => sel.path === node.path);
const { _uploading, _progress, _error } = node as any;
return (
<div key={node.path || node.name} data-fb-idx={idx}
data-testid="file-list-node"
data-node-id={node.path || node.name}
onClick={(e) => onItemClick(idx, e)}
onDoubleClick={() => onItemDoubleClick(idx)}
onDoubleClick={() => !_uploading && onItemDoubleClick(idx)}
className="fb-row" style={{
display: 'flex', alignItems: 'center', gap: 8, padding: isSearchMode ? '8px 10px' : '5px 10px',
cursor: 'pointer', fontSize,
cursor: _uploading ? 'default' : 'pointer', fontSize,
borderBottomWidth: 1, borderBottomColor: 'rgba(255,255,255,0.06)', borderBottomStyle: 'solid',
background: isSelected ? SELECTED_BG : isFocused ? FOCUS_BG : 'transparent',
borderLeftWidth: 2, borderLeftColor: isSelected ? SELECTED_BORDER : 'transparent',
borderLeftStyle: isSelected ? 'outset' : 'solid',
outline: isFocused ? `2px solid ${FOCUS_BORDER}` : 'none',
outlineOffset: '-2px',
opacity: _uploading ? 0.7 : 1,
position: 'relative'
}}>
<NodeIcon node={node} />
<div style={{ flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column', gap: isSearchMode ? 2 : 0 }}>
@ -99,13 +103,27 @@ const FileListView: React.FC<FileListViewProps> = ({
{node.parent || '/'}
</span>
)}
{_uploading && (
<div style={{
width: '100%', height: 4, background: 'rgba(0,0,0,0.2)',
borderRadius: 2, overflow: 'hidden', marginTop: 2
}}>
<div style={{
height: '100%', width: `${_progress || 0}%`,
background: 'var(--primary, #3b82f6)', transition: 'width 0.2s linear'
}} />
</div>
)}
{_error && (
<span style={{ fontSize: 10, color: '#ef4444', marginTop: 2 }}>{_error}</span>
)}
</div>
{node.size !== undefined && (
<span style={{ color: 'var(--muted-foreground, #64748b)', fontSize: 10, flexShrink: 0 }}>
{formatSize(node.size)}
<span style={{ color: _error ? '#ef4444' : 'var(--muted-foreground, #64748b)', fontSize: 10, flexShrink: 0 }}>
{_error ? '✕' : _uploading ? `${Math.round(_progress || 0)}%` : formatSize(node.size)}
</span>
)}
{mode === 'advanced' && node.mtime && (
{mode === 'advanced' && node.mtime && !_uploading && (
<span style={{ color: 'var(--muted-foreground, #64748b)', fontSize: 10, flexShrink: 0, width: 120, textAlign: 'right' }}>
{formatDate(node.mtime)}
</span>

View File

@ -52,39 +52,6 @@ const Auth = () => {
const { data } = await signUp(formData.email, formData.password, formData.username, formData.displayName);
// If signing up from an organization context, add user to that organization
if (data?.user && isOrgContext && orgSlug) {
try {
// Get organization ID from slug
const { data: org, error: orgError } = await supabase
.from('organizations')
.select('id')
.eq('slug', orgSlug)
.single();
if (orgError) throw orgError;
if (org) {
// Add user to organization
const { error: memberError } = await supabase
.from('user_organizations')
.insert({
user_id: data.user.id,
organization_id: org.id,
role: 'member'
});
if (memberError) throw memberError;
toast({
title: translate('Welcome!'),
description: translate("You've been added to the organization.")
});
}
} catch (error) {
console.error('Error adding user to organization:', error);
}
}
};
const handleSignIn = async (e: React.FormEvent) => {

View File

@ -0,0 +1,78 @@
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Shield, ArrowRight } from 'lucide-react';
import { T, translate } from '@/i18n';
import { useAuth } from 'react-oidc-context';
import { useNavigate } from 'react-router-dom';
const AuthZ = () => {
const auth = useAuth();
const navigate = useNavigate();
const [isLoading, setIsLoading] = useState(false);
// Monitor auth state changes and log them for debugging
useEffect(() => {
console.log("🛡️ [AuthZ] Auth State Update:", {
isAuthenticated: auth.isAuthenticated,
isLoading: auth.isLoading,
hasError: !!auth.error,
errorMsg: auth.error?.message
});
if (auth.user) {
console.log("🛡️ [AuthZ] Identity Token Profile:", auth.user.profile);
console.log("🛡️ [AuthZ] Access Token:", auth.user.access_token);
}
if (auth.isAuthenticated) {
console.log(`🛡️ [AuthZ] Successfully logged in as: ${auth.user?.profile.email || 'Unknown'}. Redirecting to /...`);
navigate('/');
}
}, [auth.isAuthenticated, auth.isLoading, auth.error, auth.user, navigate]);
const handleZitadelLogin = async () => {
try {
setIsLoading(true);
await auth.signinRedirect();
} catch (e) {
console.error(e);
setIsLoading(false);
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-background via-secondary/20 to-accent/20 flex items-center justify-center p-4">
<Card className="w-full max-w-md glass-morphism border-white/20">
<CardHeader className="text-center">
<div className="mx-auto bg-primary/10 w-12 h-12 rounded-full justify-center items-center flex mb-4">
<Shield className="w-6 h-6 text-primary" />
</div>
<CardTitle className="text-2xl font-bold bg-gradient-primary bg-clip-text text-transparent">
<T>Corporate Login</T>
</CardTitle>
<CardDescription>
<T>Sign in securely via Zitadel Identity</T>
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-center text-muted-foreground">
<T>You will be redirected to the secure portal to complete your authentication via Google or company credentials.</T>
</p>
<Button
className="w-full mt-4"
size="lg"
onClick={handleZitadelLogin}
disabled={isLoading}
>
{isLoading ? translate("Redirecting...") : translate("Continue to Security Portal")}
</Button>
</CardContent>
</Card>
</div>
);
};
export default AuthZ;

View File

@ -0,0 +1,17 @@
import React from 'react';
import FileBrowser from '@/apps/filebrowser/FileBrowser';
const PlaygroundVfs = () => {
return (
<div style={{ height: 'calc(100vh - 56px)', overflow: 'hidden' }}>
<FileBrowser
allowPanels={false}
mode="simple"
disableRoutingSync={true}
initialMount="home"
/>
</div>
);
};
export default PlaygroundVfs;