diff --git a/backend/Makefile b/backend/Makefile index d262ec4f..47a74826 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -14,7 +14,7 @@ migrate: run_dev_server: @echo "Running server..." - @rye run python main.py runserver + @rye run python main.py runserver --host 0.0.0.0 --port 5001 run_dev_celery_worker: @echo "Running celery..." diff --git a/backend/app/api/admin_routes/knowledge_base/document/models.py b/backend/app/api/admin_routes/knowledge_base/document/models.py index f9d94dc1..f68bb0ab 100644 --- a/backend/app/api/admin_routes/knowledge_base/document/models.py +++ b/backend/app/api/admin_routes/knowledge_base/document/models.py @@ -1,5 +1,6 @@ from datetime import datetime from typing import Optional +from uuid import UUID from pydantic import BaseModel, Field @@ -44,3 +45,10 @@ class DocumentItem(BaseModel): last_modified_at: datetime created_at: datetime updated_at: datetime + + +class RebuildIndexResult(BaseModel): + reindex_document_ids: list[int] = Field(default_factory=list) + ignore_document_ids: list[int] = Field(default_factory=list) + reindex_chunk_ids: list[UUID] = Field(default_factory=list) + ignore_chunk_ids: list[UUID] = Field(default_factory=list) diff --git a/backend/app/api/admin_routes/knowledge_base/document/routes.py b/backend/app/api/admin_routes/knowledge_base/document/routes.py index 78da0b44..61e1bcde 100644 --- a/backend/app/api/admin_routes/knowledge_base/document/routes.py +++ b/backend/app/api/admin_routes/knowledge_base/document/routes.py @@ -3,11 +3,13 @@ from fastapi import APIRouter, Depends, Query, HTTPException from fastapi_pagination import Params, Page +from sqlmodel import Session from app.api.admin_routes.knowledge_base.models import ChunkItem from app.api.deps import SessionDep, CurrentSuperuserDep -from app.models import Document, DocIndexTaskStatus -from app.models.chunk import KgIndexStatus, get_kb_chunk_model +from app.models import Document +from app.models.chunk import Chunk, KgIndexStatus, get_kb_chunk_model +from app.models.document import DocIndexTaskStatus from app.models.entity import get_kb_entity_model from app.models.relationship import get_kb_relationship_model from app.repositories import knowledge_base_repo, document_repo @@ -15,18 +17,13 @@ from app.api.admin_routes.knowledge_base.document.models import ( DocumentFilters, DocumentItem, + RebuildIndexResult, ) -from app.exceptions import ( - InternalServerError, - KBNotFound, - DocumentNotFound, -) +from app.exceptions import InternalServerError from app.repositories.graph import GraphRepo +from app.tasks.build_index import build_index_for_document, build_kg_index_for_chunk from app.tasks.knowledge_base import stats_for_knowledge_base -from app.tasks import ( - build_kg_index_for_chunk, - build_index_for_document, -) + router = APIRouter() logger = logging.getLogger(__name__) @@ -48,8 +45,8 @@ def list_kb_documents( filters=filters, params=params, ) - except KBNotFound as e: - raise e + except HTTPException: + raise except Exception as e: logger.exception(e) raise InternalServerError() @@ -66,8 +63,8 @@ def get_kb_document_by_id( document = document_repo.must_get(session, doc_id) assert document.knowledge_base_id == kb_id return document - except KBNotFound as e: - raise e + except HTTPException: + raise except Exception as e: logger.exception(e) raise InternalServerError() @@ -84,8 +81,8 @@ def list_kb_document_chunks( kb = knowledge_base_repo.must_get(session, kb_id) chunk_repo = ChunkRepo(get_kb_chunk_model(kb)) return chunk_repo.get_document_chunks(session, doc_id) - except KBNotFound as e: - raise e + except HTTPException: + raise except Exception as e: logger.exception(e) raise InternalServerError() @@ -97,7 +94,7 @@ def remove_kb_document( user: CurrentSuperuserDep, kb_id: int, document_id: int, -): +) -> RebuildIndexResult: try: kb = knowledge_base_repo.must_get(session, kb_id) doc = document_repo.must_get(session, document_id) @@ -127,71 +124,99 @@ def remove_kb_document( stats_for_knowledge_base.delay(kb_id) return {"detail": "success"} - except KBNotFound as e: - raise e - except DocumentNotFound as e: - raise e + except HTTPException: + raise except Exception as e: logger.exception(f"Failed to remove document #{document_id}: {e}") raise InternalServerError() -@router.post("/admin/knowledge_bases/{kb_id}/documents/{doc_id}/reindex") -def retry_kb_document_index( +@router.post("/admin/knowledge_bases/{kb_id}/documents/reindex") +def rebuild_kb_documents_index( session: SessionDep, user: CurrentSuperuserDep, kb_id: int, - doc_id: int, - reindex_completed: bool = True, + document_ids: list[int], + reindex_completed_task: bool = False, ): try: - kb = knowledge_base_repo.must_get(session, kb_id) + return rebuild_kb_document_index_by_ids( + session, kb_id, document_ids, reindex_completed_task + ) + except HTTPException: + raise + except Exception as e: + logger.exception(e, exc_info=True) + raise InternalServerError() + + +@router.post("/admin/knowledge_bases/{kb_id}/documents/{doc_id}/reindex") +def rebuild_kb_document_index( + db_session: SessionDep, + user: CurrentSuperuserDep, + kb_id: int, + doc_id: int, + reindex_completed_task: bool = False, +) -> RebuildIndexResult: + try: + document_ids = [doc_id] + return rebuild_kb_document_index_by_ids( + db_session, kb_id, document_ids, reindex_completed_task + ) + except HTTPException: + raise + except Exception as e: + logger.exception(e, exc_info=True) + raise InternalServerError() - # Retry failed vector index tasks. - doc = document_repo.must_get(session, doc_id) - reindex_document_ids = [] - ignore_document_ids = [] - if doc.index_status == DocIndexTaskStatus.COMPLETED and not reindex_completed: +def rebuild_kb_document_index_by_ids( + db_session: Session, + kb_id: int, + document_ids: list[int], + reindex_completed_task: bool = False, +) -> RebuildIndexResult: + kb = knowledge_base_repo.must_get(db_session, kb_id) + kb_chunk_repo = ChunkRepo(get_kb_chunk_model(kb)) + + # Retry failed vector index tasks. + documents = document_repo.fetch_by_ids(db_session, document_ids) + reindex_document_ids = [] + ignore_document_ids = [] + + for doc in documents: + # TODO: check NOT_STARTED, PENDING, RUNNING + if doc.index_status != DocIndexTaskStatus.FAILED and not reindex_completed_task: ignore_document_ids.append(doc.id) else: reindex_document_ids.append(doc.id) - doc.index_status = DocIndexTaskStatus.PENDING - session.add(doc) - session.commit() - build_index_for_document.delay(kb.id, doc.id) - logger.info(f"Triggered document #{len(doc.id)} to rebuilt vector index.") + doc.index_status = DocIndexTaskStatus.PENDING + db_session.add(doc) + db_session.commit() - # Retry failed kg index tasks. - chunk_repo = ChunkRepo(get_kb_chunk_model(kb)) - chunks = chunk_repo.get_document_chunks(session, doc_id) - reindex_chunk_ids = [] - ignore_chunk_ids = [] - for chunk in chunks: - if chunk.index_status == KgIndexStatus.COMPLETED and not reindex_completed: - ignore_chunk_ids.append(chunk.id) - continue - else: - reindex_chunk_ids.append(chunk.id) - - chunk.index_status = KgIndexStatus.PENDING - session.add(chunk) - session.commit() - build_kg_index_for_chunk.delay(kb_id, chunk.id) + build_index_for_document.delay(kb.id, doc.id) - logger.info( - f"Triggered {len(reindex_chunk_ids)} chunks to rebuilt knowledge graph index." - ) + # Retry failed kg index tasks. + chunks: list[Chunk] = kb_chunk_repo.fetch_by_document_ids(db_session, document_ids) + reindex_chunk_ids = [] + ignore_chunk_ids = [] + for chunk in chunks: + if chunk.index_status == KgIndexStatus.COMPLETED and not reindex_completed_task: + ignore_chunk_ids.append(chunk.id) + continue + else: + reindex_chunk_ids.append(chunk.id) - return { - "reindex_document_ids": reindex_document_ids, - "ignore_document_ids": ignore_document_ids, - "reindex_chunk_ids": reindex_chunk_ids, - "ignore_chunk_ids": ignore_chunk_ids, - } - except HTTPException as e: - raise e - except Exception as e: - logger.exception(e, exc_info=True) - raise InternalServerError() + chunk.index_status = KgIndexStatus.PENDING + db_session.add(chunk) + db_session.commit() + + build_kg_index_for_chunk.delay(kb.id, chunk.id) + + return RebuildIndexResult( + reindex_document_ids=reindex_document_ids, + ignore_document_ids=ignore_document_ids, + reindex_chunk_ids=reindex_chunk_ids, + ignore_chunk_ids=ignore_chunk_ids, + ) diff --git a/backend/app/api/admin_routes/knowledge_base/routes.py b/backend/app/api/admin_routes/knowledge_base/routes.py index e2e362b8..89dc473a 100644 --- a/backend/app/api/admin_routes/knowledge_base/routes.py +++ b/backend/app/api/admin_routes/knowledge_base/routes.py @@ -1,15 +1,12 @@ import logging -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, HTTPException from fastapi_pagination import Params, Page - +from app.api.deps import SessionDep, CurrentSuperuserDep from app.rag.knowledge_base.index_store import ( init_kb_tidb_vector_store, init_kb_tidb_graph_store, ) -from app.repositories.embedding_model import embed_model_repo -from app.repositories.llm import llm_repo - from .models import ( KnowledgeBaseDetail, KnowledgeBaseItem, @@ -18,25 +15,24 @@ VectorIndexError, KGIndexError, ) -from app.api.deps import SessionDep, CurrentSuperuserDep from app.exceptions import ( InternalServerError, - KBException, - KBNotFound, - KBNoVectorIndexConfigured, - DefaultLLMNotFound, - DefaultEmbeddingModelNotFound, KBIsUsedByChatEngines, ) from app.models import ( + DataSource, KnowledgeBase, ) -from app.models.data_source import DataSource +from app.repositories import ( + embed_model_repo, + llm_repo, + data_source_repo, + knowledge_base_repo, +) from app.tasks import ( build_kg_index_for_chunk, build_index_for_document, ) -from app.repositories import knowledge_base_repo, data_source_repo from app.tasks.knowledge_base import ( import_documents_for_knowledge_base, stats_for_knowledge_base, @@ -93,12 +89,8 @@ def create_knowledge_base( import_documents_for_knowledge_base.delay(knowledge_base.id) return knowledge_base - except KBNoVectorIndexConfigured as e: - raise e - except DefaultLLMNotFound as e: - raise e - except DefaultEmbeddingModelNotFound as e: - raise e + except HTTPException: + raise except Exception as e: logger.exception(e) raise InternalServerError() @@ -121,8 +113,8 @@ def get_knowledge_base( ) -> KnowledgeBaseDetail: try: return knowledge_base_repo.must_get(session, knowledge_base_id) - except KBNotFound as e: - raise e + except HTTPException: + raise except Exception as e: logger.exception(e) raise InternalServerError() @@ -139,10 +131,8 @@ def update_knowledge_base_setting( knowledge_base = knowledge_base_repo.must_get(session, knowledge_base_id) knowledge_base = knowledge_base_repo.update(session, knowledge_base, update) return knowledge_base - except KBNotFound as e: - raise e - except KBNoVectorIndexConfigured as e: - raise e + except HTTPException: + raise except Exception as e: logger.exception(e) raise InternalServerError() @@ -155,8 +145,8 @@ def list_kb_linked_chat_engines( try: kb = knowledge_base_repo.must_get(session, kb_id) return knowledge_base_repo.list_linked_chat_engines(session, kb.id) - except KBNotFound as e: - raise e + except HTTPException: + raise except Exception as e: logger.exception(e) raise InternalServerError() @@ -181,8 +171,8 @@ def delete_knowledge_base(session: SessionDep, user: CurrentSuperuserDep, kb_id: purge_knowledge_base_related_resources.apply_async(args=[kb_id], countdown=5) return {"detail": f"Knowledge base #{kb_id} is deleted successfully"} - except KBException as e: - raise e + except HTTPException: + raise except Exception as e: logger.exception(e) raise InternalServerError() @@ -200,8 +190,8 @@ def get_knowledge_base_index_overview( stats_for_knowledge_base.delay(knowledge_base.id) return knowledge_base_repo.get_index_overview(session, knowledge_base) - except KBNotFound as e: - raise e + except HTTPException: + raise except Exception as e: logger.exception(e) raise InternalServerError() @@ -217,8 +207,8 @@ def list_kb_vector_index_errors( try: kb = knowledge_base_repo.must_get(session, kb_id) return knowledge_base_repo.list_vector_index_built_errors(session, kb, params) - except KBNotFound as e: - raise e + except HTTPException: + raise except Exception as e: logger.exception(e) raise InternalServerError() @@ -234,8 +224,8 @@ def list_kb_kg_index_errors( try: kb = knowledge_base_repo.must_get(session, kb_id) return knowledge_base_repo.list_kg_index_built_errors(session, kb, params) - except KBNotFound as e: - raise e + except HTTPException: + raise except Exception as e: logger.exception(e) raise InternalServerError() @@ -267,10 +257,12 @@ def retry_failed_tasks( ) return { - "detail": f"Triggered reindex {len(document_ids)} documents and {len(chunk_ids)} chunks of knowledge base #{kb_id}." + "detail": f"Triggered reindex {len(document_ids)} documents and {len(chunk_ids)} chunks of knowledge base #{kb_id}.", + "reindex_document_ids": document_ids, + "reindex_chunk_ids": chunk_ids, } - except KBNotFound as e: - raise e + except HTTPException: + raise except Exception as e: logger.exception(e) raise InternalServerError() diff --git a/backend/app/models/document.py b/backend/app/models/document.py index dd4287be..77b3c84d 100644 --- a/backend/app/models/document.py +++ b/backend/app/models/document.py @@ -43,7 +43,6 @@ class Document(UpdatableBaseModel, table=True): # TODO: rename to vector_index_status, vector_index_result. index_status: DocIndexTaskStatus = DocIndexTaskStatus.NOT_STARTED - vector_index_task_id: str = Field(sa_column=Column(String(128), nullable=False)) index_result: str = Field(sa_column=Column(Text, nullable=True)) # TODO: add kg_index_status, kg_index_result column, unify the index status. diff --git a/backend/app/rag/chat/retrieve/retrieve_flow.py b/backend/app/rag/chat/retrieve/retrieve_flow.py index 1e9b929c..e3ee1906 100644 --- a/backend/app/rag/chat/retrieve/retrieve_flow.py +++ b/backend/app/rag/chat/retrieve/retrieve_flow.py @@ -142,9 +142,7 @@ def search_relevant_chunks(self, user_question: str) -> List[NodeWithScore]: def get_documents_from_nodes(self, nodes: List[NodeWithScore]) -> List[DBDocument]: document_ids = [n.node.metadata["document_id"] for n in nodes] - documents = document_repo.list_full_documents_by_ids( - self.db_session, document_ids - ) + documents = document_repo.fetch_by_ids(self.db_session, document_ids) # Keep the original order of document ids, which is sorted by similarity. return sorted(documents, key=lambda x: document_ids.index(x.id)) diff --git a/backend/app/rag/retrievers/chunk/fusion_retriever.py b/backend/app/rag/retrievers/chunk/fusion_retriever.py index 5a6a65d6..53d96cf6 100644 --- a/backend/app/rag/retrievers/chunk/fusion_retriever.py +++ b/backend/app/rag/retrievers/chunk/fusion_retriever.py @@ -9,6 +9,7 @@ ChunkSimpleRetriever, ) from app.rag.retrievers.chunk.schema import ( + RetrievedChunkDocument, VectorSearchRetrieverConfig, ChunksRetrievalResult, ChunkRetriever, @@ -97,13 +98,16 @@ def retrieve_chunks( chunks = map_nodes_to_chunks(nodes_with_score) document_ids = [c.document_id for c in chunks] + documents = document_repo.fetch_by_ids(self._db_session, document_ids) if full_document: - documents = document_repo.list_full_documents_by_ids( - self._db_session, document_ids - ) + return ChunksRetrievalResult(chunks=chunks, documents=documents) else: - documents = document_repo.list_simple_documents_by_ids( - self._db_session, document_ids + return ChunksRetrievalResult( + chunks=chunks, + documents=[ + RetrievedChunkDocument( + id=d.id, name=d.name, source_uri=d.source_uri + ) + for d in documents + ], ) - - return ChunksRetrievalResult(chunks=chunks, documents=documents) diff --git a/backend/app/rag/retrievers/chunk/simple_retriever.py b/backend/app/rag/retrievers/chunk/simple_retriever.py index e06ae78f..b8d2747e 100644 --- a/backend/app/rag/retrievers/chunk/simple_retriever.py +++ b/backend/app/rag/retrievers/chunk/simple_retriever.py @@ -15,6 +15,7 @@ from app.rag.knowledge_base.config import get_kb_embed_model from app.rag.rerankers.resolver import resolve_reranker_by_id from app.rag.retrievers.chunk.schema import ( + RetrievedChunkDocument, VectorSearchRetrieverConfig, ChunksRetrievalResult, ChunkRetriever, @@ -120,13 +121,17 @@ def retrieve_chunks( nodes_with_score = self.retrieve(query_str) chunks = map_nodes_to_chunks(nodes_with_score) document_ids = [c.document_id for c in chunks] + documents = document_repo.fetch_by_ids(self._db_session, document_ids) + if full_document: - documents = document_repo.list_full_documents_by_ids( - self._db_session, document_ids - ) + return ChunksRetrievalResult(chunks=chunks, documents=documents) else: - documents = document_repo.list_simple_documents_by_ids( - self._db_session, document_ids + return ChunksRetrievalResult( + chunks=chunks, + documents=[ + RetrievedChunkDocument( + id=d.id, name=d.name, source_uri=d.source_uri + ) + for d in documents + ], ) - - return ChunksRetrievalResult(chunks=chunks, documents=documents) diff --git a/backend/app/repositories/__init__.py b/backend/app/repositories/__init__.py index f96a9cac..5a2d5f54 100644 --- a/backend/app/repositories/__init__.py +++ b/backend/app/repositories/__init__.py @@ -6,3 +6,5 @@ from .chunk import chunk_repo from .data_source import data_source_repo from .knowledge_base import knowledge_base_repo +from .llm import llm_repo +from .embedding_model import embed_model_repo diff --git a/backend/app/repositories/chunk.py b/backend/app/repositories/chunk.py index f38cf5b2..0421afe2 100644 --- a/backend/app/repositories/chunk.py +++ b/backend/app/repositories/chunk.py @@ -39,6 +39,11 @@ def get_document_chunks(self, session: Session, document_id: int): select(self.model_cls).where(self.model_cls.document_id == document_id) ).all() + def fetch_by_document_ids(self, session: Session, document_ids: list[int]): + return session.exec( + select(self.model_cls).where(self.model_cls.document_id.in_(document_ids)) + ).all() + def count(self, session: Session): return session.scalar(select(func.count(self.model_cls.id))) diff --git a/backend/app/repositories/document.py b/backend/app/repositories/document.py index ba48f2dc..2ea3e25c 100644 --- a/backend/app/repositories/document.py +++ b/backend/app/repositories/document.py @@ -8,7 +8,6 @@ from app.api.admin_routes.knowledge_base.document.models import DocumentFilters from app.exceptions import DocumentNotFound from app.models import Document -from app.rag.retrievers.chunk.schema import RetrievedChunkDocument from app.repositories.base_repo import BaseRepo @@ -64,23 +63,9 @@ def delete_by_datasource(self, session: Session, datasource_id: int): stmt = delete(Document).where(Document.data_source_id == datasource_id) session.exec(stmt) - def list_full_documents_by_ids( - self, session: Session, document_ids: list[int] - ) -> list[Document]: + def fetch_by_ids(self, session: Session, document_ids: list[int]) -> list[Document]: stmt = select(Document).where(Document.id.in_(document_ids)) return session.exec(stmt).all() - def list_simple_documents_by_ids( - self, session: Session, document_ids: list[int] - ) -> list[RetrievedChunkDocument]: - stmt = select(Document.id, Document.name, Document.source_uri).where( - Document.id.in_(document_ids) - ) - rows = session.exec(stmt).all() - return [ - RetrievedChunkDocument(id=row[0], name=row[1], source_uri=row[2]) - for row in rows - ] - document_repo = DocumentRepo() diff --git a/frontend/app/src/api/datasources.ts b/frontend/app/src/api/datasources.ts index d2ac5750..5beecfcf 100644 --- a/frontend/app/src/api/datasources.ts +++ b/frontend/app/src/api/datasources.ts @@ -65,6 +65,8 @@ export type DatasourceVectorIndexError = { } export type DatasourceKgIndexError = { + document_id: number + document_name: string chunk_id: string source_uri: string error: string | null diff --git a/frontend/app/src/api/knowledge-base.ts b/frontend/app/src/api/knowledge-base.ts index dd426533..06f6870c 100644 --- a/frontend/app/src/api/knowledge-base.ts +++ b/frontend/app/src/api/knowledge-base.ts @@ -143,6 +143,8 @@ const vectorIndexErrorSchema = z.object({ }) satisfies ZodType; const kgIndexErrorSchema = z.object({ + document_id: z.number(), + document_name: z.string(), chunk_id: z.string(), source_uri: z.string(), error: z.string().nullable(), @@ -197,6 +199,14 @@ export async function deleteKnowledgeBaseDocument (id: number, documentId: numbe .then(handleErrors); } +export async function rebuildKBDocumentIndex (kb_id: number, doc_id: number) { + return await fetch(requestUrl(`/api/v1/admin/knowledge_bases/${kb_id}/documents/${doc_id}/reindex`), { + method: 'POST', + headers: await authenticationHeaders(), + }) + .then(handleErrors); +} + export async function createKnowledgeBase (params: CreateKnowledgeBaseParams) { return await fetch(requestUrl('/api/v1/admin/knowledge_bases'), { method: 'POST', diff --git a/frontend/app/src/components/cells/actions.tsx b/frontend/app/src/components/cells/actions.tsx index 43d0125f..679daa60 100644 --- a/frontend/app/src/components/cells/actions.tsx +++ b/frontend/app/src/components/cells/actions.tsx @@ -1,21 +1,22 @@ import { DangerousActionButton, type DangerousActionButtonProps } from '@/components/dangerous-action-button'; import { buttonVariants } from '@/components/ui/button'; -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; import { useDataTable } from '@/components/use-data-table'; import { cn } from '@/lib/utils'; import type { CellContext } from '@tanstack/react-table'; -import { EllipsisVerticalIcon, Loader2Icon } from 'lucide-react'; +import { EllipsisIcon, Loader2Icon } from 'lucide-react'; import type { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime'; import { useRouter } from 'next/navigation'; import { type Dispatch, type ReactNode, type SetStateAction, type TransitionStartFunction, useState, useTransition } from 'react'; export interface CellAction { + type?: 'button' | 'label' | 'separator'; key?: string | number; icon?: ReactNode; title?: ReactNode; disabled?: boolean; dangerous?: Pick; - action: (context: ActionUIContext) => Promise | void; + action?: (context: ActionUIContext) => Promise | void; } export interface ActionUIContext { @@ -35,12 +36,18 @@ export function actions (items: (row: Row) => CellAction[]) { return ( - + - {actionItems.map((item, index) => ( - - ))} + {actionItems.map((item, index) => { + if (item.type === 'label') { + return {item.title}; + } else if (item.type === 'separator') { + return ; + } else { + return ; + } + })} ); @@ -56,7 +63,7 @@ function Action ({ item, open, setOpen }: { item: CellAction, open: boolean, set const onAction = async () => { try { setBusy(true); - await item.action({ startTransition, router, table, dropdownOpen: open, setDropdownOpen: setOpen }); + await item?.action?.({ startTransition, router, table, dropdownOpen: open, setDropdownOpen: setOpen }); } finally { setBusy(false); } diff --git a/frontend/app/src/components/cells/link.tsx b/frontend/app/src/components/cells/link.tsx index 1464e0a7..d87c64aa 100644 --- a/frontend/app/src/components/cells/link.tsx +++ b/frontend/app/src/components/cells/link.tsx @@ -2,17 +2,31 @@ import type { CellContext } from '@tanstack/react-table'; import Link from 'next/link'; export interface LinkCellProps { - url: (row: Row) => string; + icon?: React.ReactNode; + url?: (row: Row) => string; text?: (row: Row) => string; + truncate?: boolean; + truncate_length?: number; } -export function link ({ url, text }: LinkCellProps) { +const format_link = (url: string, maxLength: number = 30): string => { + if (!url || url.length <= maxLength) return url; + const start = url.substring(0, maxLength / 2); + const end = url.substring(url.length - maxLength / 2); + return `${start}...${end}`; +}; + +export function link ({ icon, url, text, truncate, truncate_length }: LinkCellProps) { // eslint-disable-next-line react/display-name - return (context: CellContext) => ( - - {text ? text(context.row.original) : String(context.getValue())} + return (context: CellContext) => { + const href_value = url ? url(context.row.original) : String(context.getValue()); + const text_value = text ? text(context.row.original) : String(context.getValue()); + const display_text = truncate ? format_link(text_value, truncate_length) : text_value; + + return + {icon} {display_text} - ); + }; } diff --git a/frontend/app/src/components/dangerous-action-button.tsx b/frontend/app/src/components/dangerous-action-button.tsx index c5db44b2..d0e9a68f 100644 --- a/frontend/app/src/components/dangerous-action-button.tsx +++ b/frontend/app/src/components/dangerous-action-button.tsx @@ -36,7 +36,7 @@ export const DangerousActionButton = forwardRef : ( -