diff --git a/backend/core/auth_middleware.py b/backend/core/authentication_middleware.py
similarity index 92%
rename from backend/core/auth_middleware.py
rename to backend/core/authentication_middleware.py
index 08aeea19..99c6aca3 100644
--- a/backend/core/auth_middleware.py
+++ b/backend/core/authentication_middleware.py
@@ -9,10 +9,10 @@
from rag_solution.file_management.database import get_db
from rag_solution.services.user_service import UserService
-logging.basicConfig(level=logging.DEBUG)
+logging.basicConfig(level=settings.log_level)
logger = logging.getLogger(__name__)
-class AuthMiddleware(BaseHTTPMiddleware):
+class AuthenticationMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
logger.info(f"AuthMiddleware: Processing request to {request.url.path}")
logger.debug(f"AuthMiddleware: Request headers: {request.headers}")
@@ -29,7 +29,8 @@ async def dispatch(self, request: Request, call_next):
'id': payload.get('sub'),
'email': payload.get('email'),
'name': payload.get('name'),
- 'uuid': payload.get('uuid') # Extract UUID from payload
+ 'uuid': payload.get('uuid'), # Extract UUID from payload
+ 'role': payload.get('role')
}
logger.info(f"AuthMiddleware: JWT token validated successfully. User: {request.state.user}")
except jwt.ExpiredSignatureError:
diff --git a/backend/core/authorization.py b/backend/core/authorization.py
new file mode 100644
index 00000000..fc652c26
--- /dev/null
+++ b/backend/core/authorization.py
@@ -0,0 +1,64 @@
+import functools
+import logging
+import re
+from fastapi import HTTPException, Request
+from fastapi.responses import JSONResponse
+from core.config import settings
+
+logging.basicConfig(level=settings.log_level)
+logger = logging.getLogger(__name__)
+
+open_paths = ['/api/auth/login', '/api/auth/callback', '/api/health', '/api/auth/oidc-config', '/api/auth/token', '/api/auth/userinfo']
+
+async def authorize_dependency(request: Request):
+ """
+ Dependency to check if the user is authorized to access the resource.
+ Uses the RBAC mapping from settings.rbac_mapping to check if the user is authorized to access the resource.
+
+ Args:
+ request (Request): The request object.
+
+ Returns:
+ bool: True if the request is authorized, raises HTTPException otherwise.
+ """
+ logger.info(f"AuthorizationMiddleware: Processing request to {request.url.path} by {request.state.user}")
+ # print(f"AuthorizationMiddleware: Processing {request.method} request to {request.url.path} by {request.state.user}")
+ if request.url.path in open_paths:
+ return True
+
+ rrole = request.state.user.get('role')
+ rpath = request.url.path
+ try:
+ if rrole:
+ for pattern, method in settings.rbac_mapping[rrole].items():
+ if re.match(pattern, rpath) and request.method in method:
+ return True
+ raise HTTPException(status_code=403, detail="Failed to authorize request. {rpath} / {rrole}")
+
+ except (KeyError, ValueError) as exc:
+ logger.warning(f"Failed to authorize request. {rpath} / {rrole}")
+ raise HTTPException(status_code=403, detail="Failed to authorize request. {rpath} / {rrole}") from exc
+
+def authorize_decorator(role: str):
+ """
+ Decorator to check if the user is authorized to access the resource.
+
+ Args:
+ role (str): The role required to access the resource.
+
+ Returns:
+ function: Goes to the original handler (function) if the request is authorized, raises HTTPException otherwise.
+ """
+ def decorator(handler):
+ @functools.wraps(handler)
+ async def wrapper(*args, **kwargs):
+ request = kwargs['request']
+ # print(f"AuthorizationDecorator: Processing {request.method} request to {request.url.path} by {request.state.user}")
+ if request.url.path not in open_paths:
+ if not request.state.user or request.state.user.get('role') != role:
+ logger.warning(f"AuthorizationDecorator: Unauthorized request to {request.url.path}")
+ return JSONResponse(status_code=403, content={"detail": f"User is not authorized to access this resource (requires {role} role)"})
+ return await handler(*args, **kwargs)
+ return wrapper
+ return decorator
+
diff --git a/backend/core/config.py b/backend/core/config.py
index ae537274..9ad2019a 100644
--- a/backend/core/config.py
+++ b/backend/core/config.py
@@ -38,7 +38,7 @@ class Settings(BaseSettings):
react_app_api_url: str
# Logging Level
- log_level: Optional[str] = None
+ log_level: Optional[str] = "INFO"
# File storage path
file_storage_path: str = tempfile.gettempdir()
@@ -106,11 +106,34 @@ class Settings(BaseSettings):
# JWT settings
jwt_secret_key: str = Field(..., env='JWT_SECRET_KEY')
jwt_algorithm: str = "HS256"
-
+ frontend_callback: str = "/callback"
+
+ # Role settings
+ # This is a sample RBAC mapping role / url_patterns / http_methods
+ rbac_mapping: dict = {
+ 'admin': {
+ r'^/api/user-collections/(.+)$': ['GET'],
+ r'^/api/user-collections/(.+)/(.+)$': ['POST', 'DELETE'],
+ },
+ 'user': {
+ r'^/api/user-collections/(.+)/(.+)$': ['POST', 'DELETE'],
+ r'^/api/user-collections/(.+)$': ['GET'],
+ r'^/api/user-collections/collection/(.+)$': ['GET'],
+ r'^/api/user-collections/collection/(.+)/users$': ['DELETE'],
+ r'^/api/collections/(.+)$': ['GET']
+ },
+ 'guest': {
+ r'^/api/user-collections$': ['GET', 'POST', 'DELETE', 'PUT'],
+ r'^/api/collections$': ['GET', 'POST', 'DELETE', 'PUT'],
+ r'^/api/collection/(.+)$': ['GET', 'POST', 'DELETE', 'PUT']
+ }
+ }
+
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
+
settings = Settings(
react_app_api_url="http://localhost:3000",
)
diff --git a/backend/core/loggingcors_middleware.py b/backend/core/loggingcors_middleware.py
new file mode 100644
index 00000000..9e78e270
--- /dev/null
+++ b/backend/core/loggingcors_middleware.py
@@ -0,0 +1,20 @@
+import logging
+from fastapi import Request
+from fastapi.middleware.cors import CORSMiddleware
+from core.config import settings
+
+logging.basicConfig(level=settings.log_level)
+logger = logging.getLogger(__name__)
+
+class LoggingCORSMiddleware(CORSMiddleware):
+ async def __call__(self, scope, receive, send):
+ if scope["type"] == "http":
+ logger.debug(f"CORS Request: method={scope['method']}, path={scope['path']}")
+ logger.debug(f"CORS Request headers: {scope['headers']}")
+
+ response = await super().__call__(scope, receive, send)
+
+ if scope["type"] == "http":
+ logger.debug(f"CORS Response headers: {response.headers}")
+
+ return response
diff --git a/backend/main.py b/backend/main.py
index 99d41dcd..6744016b 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -4,17 +4,22 @@
from fastapi import FastAPI, Depends, HTTPException, Request, Header
from fastapi.openapi.utils import get_openapi
-from fastapi.middleware.cors import CORSMiddleware
+from fastapi.responses import JSONResponse
from starlette.middleware.sessions import SessionMiddleware
from pydantic import BaseModel
from sqlalchemy import inspect, text
+from auth.oidc import verify_jwt_token
import jwt
+# Import core modules
+from core.authentication_middleware import AuthenticationMiddleware
+# from backend.core.authorization_decorator import AuthorizationMiddleware
+from core.loggingcors_middleware import LoggingCORSMiddleware
+from core.authorization import authorize_dependency
from core.config import settings
-from auth.oidc import get_current_user, verify_jwt_token
-from rag_solution.file_management.database import Base, engine, get_db
# Import all models
+from rag_solution.file_management.database import Base, engine, get_db
from rag_solution.models.user import User
from rag_solution.models.collection import Collection
from rag_solution.models.file import File
@@ -22,8 +27,7 @@
from rag_solution.models.user_team import UserTeam
from rag_solution.models.team import Team
-
-from core.auth_middleware import AuthMiddleware
+# Import all routers
from rag_solution.file_management.database import Base, engine
from rag_solution.router.collection_router import router as collection_router
from rag_solution.router.file_router import router as file_router
@@ -33,24 +37,10 @@
from rag_solution.router.user_team_router import router as user_team_router
from rag_solution.router.health_router import router as health_router
from rag_solution.router.auth_router import router as auth_router
-from auth.oidc import get_current_user, oauth
logging.basicConfig(level=settings.log_level)
logger = logging.getLogger(__name__)
-class LoggingCORSMiddleware(CORSMiddleware):
- async def __call__(self, scope, receive, send):
- if scope["type"] == "http":
- logger.debug(f"CORS Request: method={scope['method']}, path={scope['path']}")
- logger.debug(f"CORS Request headers: {scope['headers']}")
-
- response = await super().__call__(scope, receive, send)
-
- if scope["type"] == "http":
- logger.debug(f"CORS Response headers: {response.headers}")
-
- return response
-
@asynccontextmanager
async def lifespan(app: FastAPI):
logger.info("Starting database initialization")
@@ -123,27 +113,39 @@ async def lifespan(app: FastAPI):
)
# Add Auth middleware
-app.add_middleware(AuthMiddleware)
-
-async def auth_dependency(authorization: str = Header(...)):
- try:
- scheme, token = authorization.split()
- if scheme.lower() != 'bearer':
- raise HTTPException(status_code=401, detail="Invalid authentication scheme")
- payload = verify_jwt_token(token)
- return payload
- except (jwt.PyJWTError, ValueError):
- raise HTTPException(status_code=401, detail="Invalid or expired token")
+app.add_middleware(AuthenticationMiddleware)
+
+# Replacing with a decorator on each endpoint to allow fine-grained authorization
+# app.add_middleware(AuthorizationMiddleware)
+
+# Already included in AuthMiddleware
+# async def auth_dependency(authorization: str = Header(...)):
+# try:
+# scheme, token = authorization.split()
+# if scheme.lower() != 'bearer':
+# raise HTTPException(status_code=401, detail="Invalid authentication scheme")
+# payload = verify_jwt_token(token)
+# return payload
+# except (jwt.PyJWTError, ValueError):
+# raise HTTPException(status_code=401, detail="Invalid or expired token")
# Include routers
app.include_router(auth_router)
app.include_router(health_router)
-app.include_router(collection_router, dependencies=[Depends(auth_dependency)])
-app.include_router(file_router, dependencies=[Depends(auth_dependency)])
-app.include_router(team_router, dependencies=[Depends(auth_dependency)])
-app.include_router(user_router, dependencies=[Depends(auth_dependency)])
-app.include_router(user_collection_router, dependencies=[Depends(auth_dependency)])
-app.include_router(user_team_router, dependencies=[Depends(auth_dependency)])
+app.include_router(collection_router)
+app.include_router(file_router)
+app.include_router(team_router)
+app.include_router(user_router)
+app.include_router(user_collection_router, dependencies=[Depends(authorize_dependency)])
+# app.include_router(user_collection_router)
+app.include_router(user_team_router)
+# app.include_router(collection_router, dependencies=[Depends(auth_dependency)])
+# app.include_router(file_router, dependencies=[Depends(auth_dependency)])
+# app.include_router(team_router, dependencies=[Depends(auth_dependency)])
+# app.include_router(user_router, dependencies=[Depends(auth_dependency)])
+# app.include_router(user_collection_router, dependencies=[Depends(auth_dependency)])
+# app.include_router(user_team_router, dependencies=[Depends(auth_dependency)])
+# app.include_router(collection_router, dependencies=[Depends(auth_dependency)])
def custom_openapi():
if app.openapi_schema:
diff --git a/backend/rag_solution/router/auth_router.py b/backend/rag_solution/router/auth_router.py
index 7e4ad314..71047143 100644
--- a/backend/rag_solution/router/auth_router.py
+++ b/backend/rag_solution/router/auth_router.py
@@ -151,12 +151,14 @@ async def auth(request: Request, db: Session = Depends(get_db)):
"sub": user['sub'],
"email": user['email'],
"name": user.get('name', 'Unknown'),
- "uuid": str(db_user.id) # Include the UUID in the JWT payload
+ "uuid": str(db_user.id) , # Include the UUID in the JWT payload
+ "exp": token.get('expires_at'),
+ "role": "admin"
}
custom_jwt = jwt.encode(custom_jwt_payload, settings.ibm_client_secret, algorithm="HS256")
- redirect_url = f"{settings.frontend_url}/?token={custom_jwt}"
+ redirect_url = f"{settings.frontend_url}{settings.frontend_callback}/?token={custom_jwt}"
logger.info(f"Redirecting to frontend: {redirect_url}")
return RedirectResponse(url=redirect_url)
diff --git a/backend/rag_solution/router/collection_router.py b/backend/rag_solution/router/collection_router.py
index 2430ffb3..fc991eaf 100644
--- a/backend/rag_solution/router/collection_router.py
+++ b/backend/rag_solution/router/collection_router.py
@@ -9,8 +9,7 @@
from rag_solution.schemas.user_schema import UserInput
from rag_solution.services.user_service import UserService
from rag_solution.services.collection_service import CollectionService
-from rag_solution.services.file_management_service import \
- FileManagementService
+from rag_solution.services.file_management_service import FileManagementService
import logging
logging.basicConfig(level=logging.INFO)
diff --git a/backend/rag_solution/router/user_collection_router.py b/backend/rag_solution/router/user_collection_router.py
index f42db3cd..290e77ce 100644
--- a/backend/rag_solution/router/user_collection_router.py
+++ b/backend/rag_solution/router/user_collection_router.py
@@ -8,6 +8,8 @@
from rag_solution.services.user_collection_interaction_service import UserCollectionInteractionService
from rag_solution.schemas.user_collection_schema import UserCollectionOutput, UserCollectionsOutput
+from core.authorization import authorize_decorator
+
router = APIRouter(prefix="/api/user-collections", tags=["user-collections"])
@router.post("/{user_id}/{collection_id}",
@@ -48,7 +50,8 @@ def remove_user_from_collection(user_id: UUID, collection_id: UUID, db: Session
500: {"description": "Internal server error"}
}
)
-def get_user_collections(user_id: UUID, request: Request, db: Session = Depends(get_db)):
+@authorize_decorator(role="admin")
+async def get_user_collections(user_id: UUID, request: Request, db: Session = Depends(get_db)):
if not hasattr(request.state, 'user') or request.state.user['uuid'] != str(user_id):
raise HTTPException(status_code=403, detail="Not authorized to access this resource")
diff --git a/webui/jsconfig.json b/webui/jsconfig.json
new file mode 100644
index 00000000..b8fc3230
--- /dev/null
+++ b/webui/jsconfig.json
@@ -0,0 +1,6 @@
+{
+ "compilerOptions": {
+ "baseUrl": "."
+ },
+ "include": ["src"]
+}
\ No newline at end of file
diff --git a/webui/package.json b/webui/package.json
index 8d0918b4..59bac685 100644
--- a/webui/package.json
+++ b/webui/package.json
@@ -23,7 +23,8 @@
"start": "react-scripts start",
"build": "DISABLE_ESLINT_PLUGIN=true react-scripts build",
"test": "react-scripts test",
- "eject": "react-scripts eject"
+ "eject": "react-scripts eject",
+ "dev": "WDS_SOCKET_PORT=3000 CHOKIDAR_USEPOLLING=true WATCHPACK_POLLING=true FAST_REFRESH=false react-scripts start"
},
"proxy": "http://localhost:8000",
"eslintConfig": {
diff --git a/webui/src/App.css b/webui/src/App.css
index 4c67a141..5b4cd17f 100644
--- a/webui/src/App.css
+++ b/webui/src/App.css
@@ -4,18 +4,19 @@
min-height: 100vh;
}
-.bx--content {
- margin-top: 3rem; /* Adjust based on your Header height */
- flex-grow: 1;
- background-color: #f4f4f4;
-}
-
.page-container {
- max-width: 1200px;
- margin: 0 auto;
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
padding: 2rem;
}
+/* .bx--content {
+ margin-top: 3rem; Adjust based on your Header height
+ flex-grow: 1;
+ background-color: #f4f4f4;
+} */
+
@media (max-width: 1056px) {
.page-container {
padding: 1rem;
diff --git a/webui/src/App.js b/webui/src/App.js
index b59d1b2f..856b4b1d 100644
--- a/webui/src/App.js
+++ b/webui/src/App.js
@@ -1,75 +1,42 @@
-import React, { useEffect, useState } from 'react';
-import { BrowserRouter as Router, Route, Routes, Navigate, useNavigate, useLocation } from 'react-router-dom';
-import { Content } from 'carbon-components-react';
-import { AuthProvider, useAuth } from './contexts/AuthContext';
-import { NotificationProvider } from './contexts/NotificationContext';
-import Header from './components/Header';
-import Footer from './components/Footer';
-import Dashboard from './components/Dashboard';
-import SearchInterface from './components/SearchInterface';
-import CollectionBrowser from './components/CollectionBrowser';
-import DocumentViewer from './components/DocumentViewer';
-import LoginPage from './components/LoginPage';
-import { handleAuthCallback } from './services/authService';
-import config from './config/config';
-import './App.css';
-import './styles/global.css';
-
-console.log('App.js loaded', { config });
-
-const ProtectedRoute = ({ children }) => {
- const { user, loading } = useAuth();
- const location = useLocation();
-
- console.log('ProtectedRoute - user:', user, 'loading:', loading, 'location:', location);
-
- if (loading) {
- return
Loading...
;
- }
-
- if (!user) {
- console.log('ProtectedRoute - Redirecting to login');
- return ;
- }
-
- return children;
-};
-
-const AppLayout = ({ children }) => {
- const { user } = useAuth();
-
- console.log('AppLayout - user:', user);
-
- return (
-
- {user &&
}
-
-
- {children}
-
-
- {user &&
}
-
- );
-};
+import React, { useEffect } from "react";
+import {
+ BrowserRouter as Router,
+ Route,
+ Routes,
+ Navigate,
+ useNavigate,
+ useLocation,
+} from "react-router-dom";
+import { Content } from "carbon-components-react";
+import { AuthProvider, useAuth } from "./contexts/AuthContext";
+import { NotificationProvider } from "./contexts/NotificationContext";
+import Header from "./components/layout/Header";
+import Footer from "./components/layout/Footer";
+import Dashboard from "./components/dashboard/Dashboard";
+import SearchInterface from "./components/common/SearchInterface";
+import DocumentViewer from "./components/document/DocumentViewer";
+import LoginPage from "./components/LoginPage";
+import { handleAuthCallback } from "./services/authService";
+
+import Collection from "./components/collection/Collection";
+import CollectionForm from "./components/collection/CollectionForm";
+import CollectionViewer from "./components/collection/CollectionViewer";
+
+import "./App.css";
+import "./styles/global.css";
function AuthCallback() {
const navigate = useNavigate();
const { fetchUser } = useAuth();
- console.log('AuthCallback component rendered');
-
useEffect(() => {
const processCallback = async () => {
- console.log('Processing auth callback');
- const result = handleAuthCallback();
+ const result = await handleAuthCallback();
if (result.success) {
- console.log('Auth callback successful, fetching user');
await fetchUser();
- navigate('/');
+ navigate("/");
} else {
- console.log('Auth callback failed, redirecting to login');
- navigate('/login');
+ navigate("/login");
}
};
@@ -79,23 +46,40 @@ function AuthCallback() {
return Processing authentication...
;
}
-function AppContent() {
- const { user, loading, fetchUser } = useAuth();
- const location = useLocation();
-
- console.log('AppContent - user:', user, 'loading:', loading, 'location:', location);
+const AppLayout = ({ children }) => {
+ const { user, fetchUser } = useAuth();
useEffect(() => {
- if (!user && !loading) {
- console.log('AppContent - Fetching user');
+ if (!user) {
fetchUser();
}
- }, [user, loading, fetchUser]);
+ }, []);
+
+ return (
+
+ {user && }
+ {children}
+ {user && }
+
+ );
+};
+
+const ProtectedRoute = ({ children }) => {
+ const { user, loading } = useAuth();
+ const location = useLocation();
if (loading) {
return Loading...
;
}
+ if (!user) {
+ return ;
+ }
+
+ return children;
+};
+
+function AppContent() {
return (
} />
@@ -120,10 +104,14 @@ function AppContent() {
path="/collections"
element={
-
+
}
- />
+ >
+ }>
+ }>
+ }>
+
}
/>
+
+
+
+ }
+ />
} />
);
}
-function App() {
- console.log('App component rendered', { windowLocation: window.location });
-
+const App = () => {
return (
@@ -151,6 +145,6 @@ function App() {
);
-}
+};
export default App;
diff --git a/webui/src/api/api.js b/webui/src/api/api.js
index ef26551b..6084903b 100644
--- a/webui/src/api/api.js
+++ b/webui/src/api/api.js
@@ -100,6 +100,7 @@ export const getUserCollections = async () => {
const response = await api.get(url);
console.log('User collections fetched successfully', response.data);
return response.data;
+
} catch (error) {
console.error('Error in getUserCollections:', error);
throw handleApiError(error, 'Error fetching user collections');
diff --git a/webui/src/components/Dashboard.js b/webui/src/components/Dashboard.js
deleted file mode 100644
index 030f9e04..00000000
--- a/webui/src/components/Dashboard.js
+++ /dev/null
@@ -1,111 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { Link } from 'react-router-dom';
-import {
- Tile,
- ClickableTile,
- Loading,
- StructuredListWrapper,
- StructuredListHead,
- StructuredListBody,
- StructuredListRow,
- StructuredListCell,
- Button
-} from 'carbon-components-react';
-import { Add, Search, Document } from '@carbon/icons-react';
-import { getUserCollections } from '../api/api';
-import { useNotification } from '../contexts/NotificationContext';
-import './Dashboard.css';
-
-const Dashboard = () => {
- const [collections, setCollections] = useState([]);
- const [loading, setLoading] = useState(true);
- const { addNotification } = useNotification();
-
- useEffect(() => {
- fetchDashboardData();
- }, []);
-
- const fetchDashboardData = async () => {
- setLoading(true);
- try {
- const collectionsData = await getUserCollections(1, 5);
- console.log('Fetched collections data:', collectionsData);
- setCollections(collectionsData.data || []);
- } catch (error) {
- console.error('Error fetching dashboard data:', error);
- addNotification('error', 'Error', 'Failed to load dashboard data. Please try again later.');
- setCollections([]);
- } finally {
- setLoading(false);
- }
- };
-
- if (loading) {
- return ;
- }
-
- if (collections.length === 0) {
- return (
-
-
Welcome to IBM RAG Solution
-
No collections found. Start by creating a new collection.
-
- );
- }
-
- return (
-
-
Welcome to IBM RAG Solution
-
-
- Quick Actions
-
-
-
- Search Documents
- Search across all your collections
-
-
-
- Create Collection
- Add a new document collection
-
-
-
- Upload Document
- Add a new document to a collection
-
-
-
-
- Recent Collections
-
-
-
- Name
- Documents
- Last Updated
-
-
-
- {collections.map((collection) => (
-
-
- {collection.name}
-
- {collection.documentCount}
- {new Date(collection.lastUpdated).toLocaleDateString()}
-
- ))}
-
-
-
-
-
-
-
-
- );
-};
-
-export default Dashboard;
diff --git a/webui/src/components/Header.css b/webui/src/components/Header.css
deleted file mode 100644
index e5f866b8..00000000
--- a/webui/src/components/Header.css
+++ /dev/null
@@ -1,29 +0,0 @@
-.bx--header__nav::before {
- display: none;
-}
-
-.bx--header__menu-item {
- position: relative;
-}
-
-.bx--header__menu-item.active::after {
- content: '';
- position: absolute;
- bottom: 0;
- left: 0;
- width: 100%;
- height: 3px;
- background-color: #0f62fe;
-}
-
-.bx--header__menu-item:hover {
- background-color: #353535;
-}
-
-.bx--header__action:hover {
- background-color: #353535;
-}
-
-.bx--header__name:hover {
- background-color: #353535;
-}
\ No newline at end of file
diff --git a/webui/src/components/Header.js b/webui/src/components/Header.js
deleted file mode 100644
index 930c8007..00000000
--- a/webui/src/components/Header.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import React from 'react';
-import { Link, useLocation } from 'react-router-dom';
-import {
- Header as CarbonHeader,
- HeaderName,
- HeaderNavigation,
- HeaderMenuItem,
- HeaderGlobalBar,
- HeaderGlobalAction,
- SkipToContent,
-} from 'carbon-components-react';
-import { User, Logout } from '@carbon/icons-react';
-import { useAuth } from '../contexts/AuthContext';
-import './Header.css';
-
-const Header = () => {
- const { isAuthenticated, user, logout } = useAuth();
- const location = useLocation();
-
- const isActive = (path) => {
- return location.pathname === path ? 'active' : '';
- };
-
- return (
-
-
-
- RAG Solution
-
-
-
- Dashboard
-
-
- Search
-
-
- Collections
-
-
-
- {isAuthenticated && (
- <>
- {}}>
-
-
-
-
-
- >
- )}
-
-
- );
-};
-
-export default Header;
diff --git a/webui/src/components/LoginPage.js b/webui/src/components/LoginPage.js
index 82a0da78..b01eb945 100644
--- a/webui/src/components/LoginPage.js
+++ b/webui/src/components/LoginPage.js
@@ -1,28 +1,25 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState } from 'react';
import { Button, Loading } from 'carbon-components-react';
-import { useAuth } from '../contexts/AuthContext';
-import config, { getFullApiUrl, API_ROUTES } from '../config/config';
+import { getFullApiUrl, API_ROUTES } from '../config/config';
import './LoginPage.css';
-console.log('LoginPage component loaded with config:', config);
+// console.log('LoginPage component loaded with config:', config);
const LoginPage = () => {
- const { signIn, user } = useAuth();
+ // const { signIn, user } = useAuth();
const [isLoading, setIsLoading] = useState(false);
- useEffect(() => {
- console.log('LoginPage mounted, user:', user);
- console.log('Current API URL:', config.apiUrl);
- console.log('Full login URL:', getFullApiUrl(API_ROUTES.LOGIN));
- }, [user]);
+ // useEffect(() => {
+ // console.log('LoginPage mounted, user:', user);
+ // console.log('Current API URL:', config.apiUrl);
+ // console.log('Full login URL:', getFullApiUrl(API_ROUTES.LOGIN));
+ // }, [user]);
const handleLogin = () => {
- console.log('Login button clicked');
setIsLoading(true);
try {
- console.log('Initiating sign-in process');
const loginUrl = getFullApiUrl(API_ROUTES.LOGIN);
- console.log('Redirecting to:', loginUrl);
+ // console.log('Redirecting to:', loginUrl);
window.location.href = loginUrl;
} catch (error) {
console.error('Login failed:', error);
diff --git a/webui/src/components/Dashboard.css b/webui/src/components/collection/Collection.css
similarity index 94%
rename from webui/src/components/Dashboard.css
rename to webui/src/components/collection/Collection.css
index ada5bb6c..a8b7b8f0 100644
--- a/webui/src/components/Dashboard.css
+++ b/webui/src/components/collection/Collection.css
@@ -1,7 +1,5 @@
.dashboard {
- max-width: 1200px;
- margin: 0 auto;
- padding: 2rem;
+
}
.dashboard-content {
diff --git a/webui/src/components/Collections.js b/webui/src/components/collection/Collection.js
similarity index 96%
rename from webui/src/components/Collections.js
rename to webui/src/components/collection/Collection.js
index a037675a..ee538386 100644
--- a/webui/src/components/Collections.js
+++ b/webui/src/components/collection/Collection.js
@@ -1,6 +1,5 @@
import React, { useEffect, useState } from "react";
-import { useNavigate } from "react-router-dom";
-
+import { Outlet, useNavigate } from "react-router-dom";
import {
Button,
Content,
@@ -24,12 +23,13 @@ import {
FlexGrid,
UnorderedList,
ListItem,
- Pagination
+ Pagination,
} from "@carbon/react";
import { Add } from "@carbon/icons-react";
-import { getUserCollections } from "../api/api";
-import "../styles/view-ui.css";
+import { getUserCollections } from "src/api/api";
+import "./Collection.css";
+import "src/styles/view-ui.css";
const Collections = () => {
let navigate = useNavigate();
@@ -63,6 +63,7 @@ const Collections = () => {
setIsLoadingCollections(true);
try {
const collections = (await getUserCollections())?.collections;
+ console.log(collections);
// setUserCollections(Array.isArray(collections) ? collections : []);
// datatable requires a field called id
@@ -91,7 +92,7 @@ const Collections = () => {
};
const createNewCollection = () => {
- navigate("/create-collection");
+ navigate("/collections/create", { replace: true });
};
const onClickCollectionTable = (collectionId) => {
@@ -108,7 +109,7 @@ const Collections = () => {
};
return (
-
+
<>
Collections
@@ -282,6 +283,7 @@ const Collections = () => {
+
);
};
diff --git a/webui/src/components/collection/CollectionForm.css b/webui/src/components/collection/CollectionForm.css
new file mode 100644
index 00000000..e69de29b
diff --git a/webui/src/components/CollectionForm.js b/webui/src/components/collection/CollectionForm.js
similarity index 98%
rename from webui/src/components/CollectionForm.js
rename to webui/src/components/collection/CollectionForm.js
index 0faaadae..50c5decb 100644
--- a/webui/src/components/CollectionForm.js
+++ b/webui/src/components/collection/CollectionForm.js
@@ -16,8 +16,9 @@ import {
Column
} from '@carbon/react';
import { TrashCan, Document } from '@carbon/icons-react';
-import { createCollectionWithDocuments, getUserCollections } from '../api/api';
-import { useAuth } from '../contexts/AuthContext';
+
+import { createCollectionWithDocuments, getUserCollections } from 'src/api/api';
+import { useAuth } from 'src/contexts/AuthContext';
const CollectionForm = () => {
const { user, loading: authLoading } = useAuth();
@@ -144,7 +145,7 @@ const CollectionForm = () => {
}
return (
-
+
Your Collections
{isLoadingCollections ? (
diff --git a/webui/src/components/CollectionForm.test.js b/webui/src/components/collection/CollectionForm.test.js
similarity index 95%
rename from webui/src/components/CollectionForm.test.js
rename to webui/src/components/collection/CollectionForm.test.js
index 335254b8..d2a65907 100644
--- a/webui/src/components/CollectionForm.test.js
+++ b/webui/src/components/collection/CollectionForm.test.js
@@ -1,9 +1,9 @@
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
-import CollectionForm from './CollectionForm';
-import { AuthProvider } from '../contexts/AuthContext';
-import { createCollectionWithDocuments, getUserCollections } from '../api/api';
+import CollectionForm from './collection/CollectionForm';
+import { AuthProvider } from 'src/contexts/AuthContext';
+import { createCollectionWithDocuments, getUserCollections } from 'src/api/api';
// Mock the API functions
jest.mock('../api/api', () => ({
@@ -93,6 +93,9 @@ describe('CollectionForm', () => {
await waitFor(() => {
expect(createCollectionWithDocuments).toHaveBeenCalled();
+ });
+
+ await waitFor(() => {
expect(screen.getByText('Collection Created')).toBeInTheDocument();
});
});
diff --git a/webui/src/components/CollectionBrowser.css b/webui/src/components/collection/CollectionViewer.css
similarity index 89%
rename from webui/src/components/CollectionBrowser.css
rename to webui/src/components/collection/CollectionViewer.css
index 32b2853e..aeda608a 100644
--- a/webui/src/components/CollectionBrowser.css
+++ b/webui/src/components/collection/CollectionViewer.css
@@ -1,7 +1,5 @@
.collection-browser {
- max-width: 1200px;
- margin: 0 auto;
- padding: 2rem;
+
}
.collection-actions {
diff --git a/webui/src/components/CollectionBrowser.js b/webui/src/components/collection/CollectionViewer.js
similarity index 94%
rename from webui/src/components/CollectionBrowser.js
rename to webui/src/components/collection/CollectionViewer.js
index 7208eb04..9b987ef1 100644
--- a/webui/src/components/CollectionBrowser.js
+++ b/webui/src/components/collection/CollectionViewer.js
@@ -20,11 +20,12 @@ import {
ModalHeader,
ModalBody,
ModalFooter
-} from 'carbon-components-react';
+} from "@carbon/react";
import { Add, TrashCan, Document, Edit} from '@carbon/icons-react';
-import { getUserCollections, createCollectionWithDocuments, updateCollection, deleteCollection, getDocumentsInCollection, deleteDocument, moveDocument } from '../api/api';
-import { useNotification } from '../contexts/NotificationContext';
-import './CollectionBrowser.css';
+import { getUserCollections, createCollectionWithDocuments, updateCollection, deleteCollection, getDocumentsInCollection, deleteDocument, moveDocument } from '../../api/api';
+import { useNotification } from 'src/contexts/NotificationContext';
+
+import './CollectionViewer.css';
const CollectionBrowser = () => {
const [collections, setCollections] = useState([]);
@@ -199,7 +200,7 @@ const CollectionBrowser = () => {
{ key: 'actions', header: 'Actions' },
];
- const collectionRows = collections.map((collection) => ({
+ const collectionRows = collections?.map((collection) => ({
id: collection.id,
name: collection.name,
description: collection.description,
@@ -235,7 +236,7 @@ const CollectionBrowser = () => {
),
}));
- const documentRows = documents.map((document) => ({
+ const documentRows = documents?.map((document) => ({
id: document.id,
name: document.name,
type: document.type,
@@ -262,8 +263,9 @@ const CollectionBrowser = () => {
}));
return (
-
-
{selectedCollection ? `Documents in ${selectedCollection.name}` : 'Document Collections'}
+
+
+
{selectedCollection ? `Documents in ${selectedCollection.name}` : 'Document Collections'}
{
) : (
<>
-
+
{({ rows, headers, getHeaderProps, getTableProps }) => (
@@ -389,8 +391,11 @@ const CollectionBrowser = () => {
Select a collection to move the document to:
c.id !== selectedCollection.id)}
+ items={collections ? collections.filter((c) => c.id !== selectedCollection.id) : []}
itemToString={(item) => (item ? item.name : '')}
onChange={({ selectedItem }) => setTargetCollection(selectedItem)}
/>
diff --git a/webui/src/components/Auth.js b/webui/src/components/common/Auth.js
similarity index 97%
rename from webui/src/components/Auth.js
rename to webui/src/components/common/Auth.js
index d5a8b183..a7aa2b4b 100644
--- a/webui/src/components/Auth.js
+++ b/webui/src/components/common/Auth.js
@@ -1,5 +1,5 @@
import React, { useEffect, useState, useCallback, memo } from 'react';
-import { signIn, signOut, getUserData, loadIBMScripts, handleAuthCallback } from '../services/authService';
+import { signIn, signOut, getUserData, loadIBMScripts, handleAuthCallback } from 'src/services/authService';
const ErrorMessage = memo(({ message }) => Error: {message}
);
const SignInButton = memo(({ onSignIn }) => );
diff --git a/webui/src/components/ErrorBoundary.js b/webui/src/components/common/ErrorBoundary.js
similarity index 100%
rename from webui/src/components/ErrorBoundary.js
rename to webui/src/components/common/ErrorBoundary.js
diff --git a/webui/src/components/QueryInput.js b/webui/src/components/common/QueryInput.js
similarity index 100%
rename from webui/src/components/QueryInput.js
rename to webui/src/components/common/QueryInput.js
diff --git a/webui/src/components/ResultsDisplay.js b/webui/src/components/common/ResultsDisplay.js
similarity index 100%
rename from webui/src/components/ResultsDisplay.js
rename to webui/src/components/common/ResultsDisplay.js
diff --git a/webui/src/components/SearchInterface.css b/webui/src/components/common/SearchInterface.css
similarity index 95%
rename from webui/src/components/SearchInterface.css
rename to webui/src/components/common/SearchInterface.css
index c41ada9f..c2470f5c 100644
--- a/webui/src/components/SearchInterface.css
+++ b/webui/src/components/common/SearchInterface.css
@@ -1,7 +1,4 @@
.search-interface {
- max-width: 1200px;
- margin: 0 auto;
- padding: 2rem;
}
.search-box {
diff --git a/webui/src/components/SearchInterface.js b/webui/src/components/common/SearchInterface.js
similarity index 95%
rename from webui/src/components/SearchInterface.js
rename to webui/src/components/common/SearchInterface.js
index 6a6ba846..566eee6e 100644
--- a/webui/src/components/SearchInterface.js
+++ b/webui/src/components/common/SearchInterface.js
@@ -13,8 +13,8 @@ import {
InlineLoading
} from 'carbon-components-react';
import { Search, Filter } from '@carbon/icons-react';
-import { getUserCollections } from '../api/api'; // Removed queryCollection import
-import { useNotification } from '../contexts/NotificationContext';
+import { getUserCollections } from '../../api/api'; // Removed queryCollection import
+import { useNotification } from '../../contexts/NotificationContext';
import './SearchInterface.css';
const SearchInterface = () => {
@@ -78,8 +78,8 @@ const SearchInterface = () => {
};
return (
-
-
Search Documents
+
+
Search Documents
{
+ const [collections, setCollections] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const { addNotification } = useNotification();
+
+ useEffect(() => {
+ fetchDashboardData();
+ }, []);
+
+
+ const fetchDashboardData = async () => {
+ setLoading(true);
+ try {
+ const collectionsData = await getUserCollections();
+ // console.log("Fetched collections data:", collectionsData.collections);
+ setCollections(collectionsData.collections);
+ } catch (error) {
+ console.error("Error fetching dashboard data:", error);
+ addNotification(
+ "error",
+ "Error",
+ "Failed to load dashboard data. Please try again later."
+ );
+ setCollections([]);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (loading) {
+ return ;
+ }
+
+ if (collections?.length === 0) {
+ return (
+
+
Welcome to IBM RAG Solution
+
No collections found. Start by creating a new collection.
+
+ );
+ }
+
+ return (
+
+
Welcome to IBM RAG Solution
+
+
+ Quick Actions
+
+
+
+ Search Documents
+ Search across all your collections
+
+
+
+ Create Collection
+ Add a new document collection
+
+
+
+ Upload Document
+ Add a new document to a collection
+
+
+
+
+ Recent Collections
+
+
+
+ Name
+ Documents
+ Last Updated
+
+
+
+ {collections?.map((collection) => (
+
+
+
+ {collection.name}
+
+
+
+ {collection.documentCount}
+
+
+ {new Date(collection.lastUpdated).toLocaleDateString()}
+
+
+ ))}
+
+
+
+
+
+
+
+
+ );
+};
+
+
+export default Dashboard;
diff --git a/webui/src/components/DashboardSettings.js b/webui/src/components/dashboard/DashboardSettings.js
similarity index 100%
rename from webui/src/components/DashboardSettings.js
rename to webui/src/components/dashboard/DashboardSettings.js
diff --git a/webui/src/components/DocumentViewer.css b/webui/src/components/document/DocumentViewer.css
similarity index 95%
rename from webui/src/components/DocumentViewer.css
rename to webui/src/components/document/DocumentViewer.css
index f560bb61..1cc15af2 100644
--- a/webui/src/components/DocumentViewer.css
+++ b/webui/src/components/document/DocumentViewer.css
@@ -1,7 +1,5 @@
.document-viewer {
- max-width: 1200px;
- margin: 0 auto;
- padding: 2rem;
+
}
.document-header {
diff --git a/webui/src/components/DocumentViewer.js b/webui/src/components/document/DocumentViewer.js
similarity index 96%
rename from webui/src/components/DocumentViewer.js
rename to webui/src/components/document/DocumentViewer.js
index 8c3b3970..431f34f7 100644
--- a/webui/src/components/DocumentViewer.js
+++ b/webui/src/components/document/DocumentViewer.js
@@ -19,8 +19,8 @@ import {
} from 'carbon-components-react';
import { Document, Page } from 'react-pdf';
import { Download, Edit } from '@carbon/icons-react';
-import { getDocument } from '../api/api'; // Removed updateDocumentMetadata import
-import { useNotification } from '../contexts/NotificationContext';
+import { getDocument } from 'src/api/api'; // Removed updateDocumentMetadata import
+import { useNotification } from 'src/contexts/NotificationContext';
import './DocumentViewer.css';
const DocumentViewer = () => {
@@ -129,7 +129,7 @@ const DocumentViewer = () => {
}
return (
-
+
Home
Collections
@@ -140,7 +140,8 @@ const DocumentViewer = () => {
-
{document.title}
+
{document.title}
+