Enhance Knowledge Page with Bulk Document Deletion
- Implemented bulk selection and deletion functionality in the Knowledge page. - Added a confirmation dialog for bulk deletions, providing user feedback on success or failure. - Updated AgGrid configuration to support multiple row selection and checkbox functionality. - Styled checkboxes for better visibility and user experience. - Refactored related components to accommodate new bulk actions.
This commit is contained in:
parent
ef39b75da1
commit
3d74edfe3c
4 changed files with 149 additions and 11 deletions
|
|
@ -7,6 +7,8 @@ import {
|
|||
HardDrive,
|
||||
Loader2,
|
||||
Search,
|
||||
Trash2,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { AgGridReact, CustomCellRendererProps } from "ag-grid-react";
|
||||
import {
|
||||
|
|
@ -29,6 +31,9 @@ import { ColDef, RowClickedEvent } from "ag-grid-community";
|
|||
import "@/components/AgGrid/registerAgGridModules";
|
||||
import "@/components/AgGrid/agGridStyles.css";
|
||||
import { KnowledgeActionsDropdown } from "@/components/knowledge-actions-dropdown";
|
||||
import { DeleteConfirmationDialog } from "../../../components/confirmation-dialog";
|
||||
import { useDeleteDocument } from "../api/mutations/useDeleteDocument";
|
||||
import { toast } from "sonner";
|
||||
|
||||
// Function to get the appropriate icon for a connector type
|
||||
function getSourceIcon(connectorType?: string) {
|
||||
|
|
@ -52,6 +57,10 @@ function SearchPage() {
|
|||
const [query, setQuery] = useState("");
|
||||
const [queryInputText, setQueryInputText] = useState("");
|
||||
const [selectedFile, setSelectedFile] = useState<string | null>(null);
|
||||
const [selectedRows, setSelectedRows] = useState<File[]>([]);
|
||||
const [showBulkDeleteDialog, setShowBulkDeleteDialog] = useState(false);
|
||||
|
||||
const deleteDocumentMutation = useDeleteDocument();
|
||||
|
||||
const {
|
||||
data = [],
|
||||
|
|
@ -86,9 +95,11 @@ function SearchPage() {
|
|||
{
|
||||
field: "filename",
|
||||
headerName: "Source",
|
||||
checkboxSelection: true,
|
||||
headerCheckboxSelection: true,
|
||||
cellRenderer: ({ data, value }: CustomCellRendererProps<File>) => {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 ml-2">
|
||||
{getSourceIcon(data?.connector_type)}
|
||||
<span className="font-medium text-foreground truncate">
|
||||
{value}
|
||||
|
|
@ -96,6 +107,10 @@ function SearchPage() {
|
|||
</div>
|
||||
);
|
||||
},
|
||||
cellStyle: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
},
|
||||
},
|
||||
{
|
||||
field: "size",
|
||||
|
|
@ -163,6 +178,52 @@ function SearchPage() {
|
|||
suppressMovable: true,
|
||||
};
|
||||
|
||||
const onSelectionChanged = useCallback(() => {
|
||||
if (gridRef.current) {
|
||||
const selectedNodes = gridRef.current.api.getSelectedRows();
|
||||
setSelectedRows(selectedNodes);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleBulkDelete = async () => {
|
||||
if (selectedRows.length === 0) return;
|
||||
|
||||
try {
|
||||
// Delete each file individually since the API expects one filename at a time
|
||||
const deletePromises = selectedRows.map(row =>
|
||||
deleteDocumentMutation.mutateAsync({ filename: row.filename })
|
||||
);
|
||||
|
||||
await Promise.all(deletePromises);
|
||||
|
||||
toast.success(
|
||||
`Successfully deleted ${selectedRows.length} document${
|
||||
selectedRows.length > 1 ? "s" : ""
|
||||
}`
|
||||
);
|
||||
setSelectedRows([]);
|
||||
setShowBulkDeleteDialog(false);
|
||||
|
||||
// Clear selection in the grid
|
||||
if (gridRef.current) {
|
||||
gridRef.current.api.deselectAll();
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to delete some documents"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const clearSelection = useCallback(() => {
|
||||
setSelectedRows([]);
|
||||
if (gridRef.current) {
|
||||
gridRef.current.api.deselectAll();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`fixed inset-0 md:left-72 top-[53px] flex flex-col transition-all duration-300 ${
|
||||
|
|
@ -183,6 +244,33 @@ function SearchPage() {
|
|||
<h2 className="text-lg font-semibold">Project Knowledge</h2>
|
||||
<KnowledgeDropdown variant="button" />
|
||||
</div>
|
||||
|
||||
{/* Bulk Actions Bar */}
|
||||
{selectedRows.length > 0 && (
|
||||
<div className="flex items-center justify-between bg-muted/20 rounded-lg border border-border/50 px-4 py-3 mb-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm font-medium">
|
||||
{selectedRows.length} document
|
||||
{selectedRows.length > 1 ? "s" : ""} selected
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowBulkDeleteDialog(true)}
|
||||
className="text-destructive border-destructive hover:bg-destructive hover:text-destructive-foreground"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
Delete Selected
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={clearSelection}>
|
||||
<X className="h-4 w-4 mr-2" />
|
||||
Clear Selection
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Search Input Area */}
|
||||
<div className="flex-shrink-0 mb-6 lg:max-w-[75%] xl:max-w-[50%]">
|
||||
<form onSubmit={handleSearch} className="flex gap-3">
|
||||
|
|
@ -259,8 +347,15 @@ function SearchPage() {
|
|||
loading={isFetching}
|
||||
ref={gridRef}
|
||||
rowData={fileResults}
|
||||
rowSelection="multiple"
|
||||
suppressRowClickSelection={true}
|
||||
getRowId={params => params.data.filename}
|
||||
onSelectionChanged={onSelectionChanged}
|
||||
onRowClicked={(params: RowClickedEvent<File>) => {
|
||||
setSelectedFile(params.data?.filename ?? "");
|
||||
// Only navigate to chunks if no rows are selected and not clicking on checkbox
|
||||
if (selectedRows.length === 0) {
|
||||
setSelectedFile(params.data?.filename ?? "");
|
||||
}
|
||||
}}
|
||||
noRowsOverlayComponent={() => (
|
||||
<div className="text-center">
|
||||
|
|
@ -276,6 +371,24 @@ function SearchPage() {
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bulk Delete Confirmation Dialog */}
|
||||
<DeleteConfirmationDialog
|
||||
open={showBulkDeleteDialog}
|
||||
onOpenChange={setShowBulkDeleteDialog}
|
||||
title="Delete Documents"
|
||||
description={`Are you sure you want to delete ${
|
||||
selectedRows.length
|
||||
} document${
|
||||
selectedRows.length > 1 ? "s" : ""
|
||||
}? This will remove all chunks and data associated with these documents. This action cannot be undone.
|
||||
|
||||
Documents to be deleted:
|
||||
${selectedRows.map(row => `• ${row.filename}`).join("\n")}`}
|
||||
confirmText="Delete All"
|
||||
onConfirm={handleBulkDelete}
|
||||
isLoading={deleteDocumentMutation.isPending}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,12 @@ body {
|
|||
--ag-wrapper-border: none;
|
||||
--ag-font-family: var(--font-sans);
|
||||
|
||||
/* Checkbox styling */
|
||||
--ag-checkbox-background-color: hsl(var(--background));
|
||||
--ag-checkbox-border-color: hsl(var(--border));
|
||||
--ag-checkbox-checked-color: hsl(var(--primary));
|
||||
--ag-checkbox-unchecked-color: transparent;
|
||||
|
||||
.ag-header {
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
margin-bottom: 0.5rem;
|
||||
|
|
@ -18,4 +24,20 @@ body {
|
|||
.ag-row {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Make sure checkboxes are visible */
|
||||
.ag-selection-checkbox,
|
||||
.ag-header-select-all {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.ag-checkbox-input-wrapper {
|
||||
border: 1px solid hsl(var(--border));
|
||||
background-color: hsl(var(--background));
|
||||
}
|
||||
|
||||
.ag-checkbox-input-wrapper.ag-checked {
|
||||
background-color: hsl(var(--primary));
|
||||
border-color: hsl(var(--primary));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
DateFilterModule,
|
||||
EventApiModule,
|
||||
GridStateModule,
|
||||
RowSelectionModule,
|
||||
} from 'ag-grid-community';
|
||||
|
||||
// Importing necessary modules from ag-grid-community
|
||||
|
|
@ -27,6 +28,7 @@ import {
|
|||
DateFilterModule,
|
||||
EventApiModule,
|
||||
GridStateModule,
|
||||
RowSelectionModule,
|
||||
// The ValidationModule adds helpful console warnings/errors that can help identify bad configuration during development.
|
||||
...(process.env.NODE_ENV !== 'production' ? [ValidationModule] : []),
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -89,8 +89,8 @@ INDEX_BODY = {
|
|||
"type": "knn_vector",
|
||||
"dimension": VECTOR_DIM,
|
||||
"method": {
|
||||
"name": "disk_ann",
|
||||
"engine": "jvector",
|
||||
"name": "hnsw",
|
||||
"engine": "lucene",
|
||||
"space_type": "l2",
|
||||
"parameters": {"ef_construction": 100, "m": 16},
|
||||
},
|
||||
|
|
@ -255,8 +255,8 @@ class AppClients:
|
|||
self.opensearch = AsyncOpenSearch(
|
||||
hosts=[{"host": OPENSEARCH_HOST, "port": OPENSEARCH_PORT}],
|
||||
connection_class=AIOHttpConnection,
|
||||
scheme="https",
|
||||
use_ssl=True,
|
||||
scheme="http",
|
||||
use_ssl=False,
|
||||
verify_certs=False,
|
||||
ssl_assert_fingerprint=None,
|
||||
http_auth=(OPENSEARCH_USERNAME, OPENSEARCH_PASSWORD),
|
||||
|
|
@ -381,17 +381,18 @@ class AppClients:
|
|||
)
|
||||
|
||||
def create_user_opensearch_client(self, jwt_token: str):
|
||||
"""Create OpenSearch client with user's JWT token for OIDC auth"""
|
||||
headers = {"Authorization": f"Bearer {jwt_token}"}
|
||||
"""Create OpenSearch client with basic auth (JWT not used in current setup)"""
|
||||
# Note: jwt_token parameter kept for compatibility but not used
|
||||
# Using basic auth instead of JWT Bearer tokens
|
||||
|
||||
return AsyncOpenSearch(
|
||||
hosts=[{"host": OPENSEARCH_HOST, "port": OPENSEARCH_PORT}],
|
||||
connection_class=AIOHttpConnection,
|
||||
scheme="https",
|
||||
use_ssl=True,
|
||||
scheme="http",
|
||||
use_ssl=False,
|
||||
verify_certs=False,
|
||||
ssl_assert_fingerprint=None,
|
||||
headers=headers,
|
||||
http_auth=(OPENSEARCH_USERNAME, OPENSEARCH_PASSWORD), # Use basic auth
|
||||
http_compress=True,
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue