mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-03-26 17:51:54 +01:00
k
This commit is contained in:
parent
4142ae0afd
commit
da17b4aa36
@ -26,6 +26,8 @@ import {
|
||||
FiUnlock,
|
||||
FiRefreshCw,
|
||||
FiPauseCircle,
|
||||
FiFilter,
|
||||
FiX,
|
||||
} from "react-icons/fi";
|
||||
import {
|
||||
Tooltip,
|
||||
@ -41,7 +43,7 @@ import Cookies from "js-cookie";
|
||||
import { TOGGLED_CONNECTORS_COOKIE_NAME } from "@/lib/constants";
|
||||
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
|
||||
import { ConnectorCredentialPairStatus } from "../../connector/[ccPairId]/types";
|
||||
import { SortIcon } from "@/components/icons/icons";
|
||||
import { FilterComponent, FilterOptions } from "./FilterComponent";
|
||||
|
||||
function SummaryRow({
|
||||
source,
|
||||
@ -286,7 +288,26 @@ export function CCPairIndexingStatusTable({
|
||||
return savedState ? JSON.parse(savedState) : {};
|
||||
});
|
||||
|
||||
const { groupedStatuses, sortedSources, groupSummaries } = useMemo(() => {
|
||||
const [filterOptions, setFilterOptions] = useState<FilterOptions>({
|
||||
accessType: null,
|
||||
docsCountFilter: {
|
||||
operator: null,
|
||||
value: null,
|
||||
},
|
||||
lastStatus: null,
|
||||
});
|
||||
|
||||
// Reference to the FilterComponent for resetting its state
|
||||
const filterComponentRef = useRef<{
|
||||
resetFilters: () => void;
|
||||
} | null>(null);
|
||||
|
||||
const {
|
||||
groupedStatuses,
|
||||
sortedSources,
|
||||
groupSummaries,
|
||||
filteredGroupedStatuses,
|
||||
} = useMemo(() => {
|
||||
const grouped: Record<ValidSources, ConnectorIndexingStatus<any, any>[]> =
|
||||
{} as Record<ValidSources, ConnectorIndexingStatus<any, any>[]>;
|
||||
|
||||
@ -338,12 +359,139 @@ export function CCPairIndexingStatusTable({
|
||||
};
|
||||
});
|
||||
|
||||
// Apply filters to create filtered grouped statuses
|
||||
const filteredGrouped: Record<
|
||||
ValidSources,
|
||||
ConnectorIndexingStatus<any, any>[]
|
||||
> = {} as Record<ValidSources, ConnectorIndexingStatus<any, any>[]>;
|
||||
|
||||
sorted.forEach((source) => {
|
||||
const statuses = grouped[source];
|
||||
|
||||
// Apply filters
|
||||
const filteredStatuses = statuses.filter((status) => {
|
||||
// Filter by access type
|
||||
if (filterOptions.accessType && filterOptions.accessType.length > 0) {
|
||||
if (!filterOptions.accessType.includes(status.access_type)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by last status
|
||||
if (filterOptions.lastStatus && filterOptions.lastStatus.length > 0) {
|
||||
if (
|
||||
!filterOptions.lastStatus.includes(
|
||||
status.last_finished_status as any
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by docs count
|
||||
if (filterOptions.docsCountFilter.operator) {
|
||||
const { operator, value } = filterOptions.docsCountFilter;
|
||||
|
||||
// If only operator is selected (no value), show all
|
||||
if (value === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (operator === ">" && !(status.docs_indexed > value)) {
|
||||
return false;
|
||||
} else if (operator === "<" && !(status.docs_indexed < value)) {
|
||||
return false;
|
||||
} else if (operator === "=" && status.docs_indexed !== value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (filteredStatuses.length > 0) {
|
||||
filteredGrouped[source] = filteredStatuses;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
groupedStatuses: grouped,
|
||||
sortedSources: sorted,
|
||||
groupSummaries: summaries,
|
||||
filteredGroupedStatuses: filteredGrouped,
|
||||
};
|
||||
}, [ccPairsIndexingStatuses, editableCcPairsIndexingStatuses]);
|
||||
}, [ccPairsIndexingStatuses, editableCcPairsIndexingStatuses, filterOptions]);
|
||||
|
||||
// Determine which sources to display based on filters and search
|
||||
const displaySources = useMemo(() => {
|
||||
const hasActiveFilters =
|
||||
(filterOptions.accessType && filterOptions.accessType.length > 0) ||
|
||||
(filterOptions.lastStatus && filterOptions.lastStatus.length > 0) ||
|
||||
filterOptions.docsCountFilter.operator !== null;
|
||||
|
||||
if (hasActiveFilters) {
|
||||
return Object.keys(filteredGroupedStatuses) as ValidSources[];
|
||||
}
|
||||
|
||||
return sortedSources;
|
||||
}, [sortedSources, filteredGroupedStatuses, filterOptions]);
|
||||
|
||||
const handleFilterChange = (newFilters: FilterOptions) => {
|
||||
setFilterOptions(newFilters);
|
||||
|
||||
// Auto-expand sources when filters are applied
|
||||
if (
|
||||
(newFilters.accessType && newFilters.accessType.length > 0) ||
|
||||
(newFilters.lastStatus && newFilters.lastStatus.length > 0) ||
|
||||
newFilters.docsCountFilter.operator !== null
|
||||
) {
|
||||
// We need to wait for the filteredGroupedStatuses to be updated
|
||||
// before we can expand the sources
|
||||
setTimeout(() => {
|
||||
const sourcesToExpand = Object.keys(
|
||||
filteredGroupedStatuses
|
||||
) as ValidSources[];
|
||||
const newConnectorsToggled = { ...connectorsToggled };
|
||||
|
||||
sourcesToExpand.forEach((source) => {
|
||||
newConnectorsToggled[source] = true;
|
||||
});
|
||||
|
||||
setConnectorsToggled(newConnectorsToggled);
|
||||
Cookies.set(
|
||||
TOGGLED_CONNECTORS_COOKIE_NAME,
|
||||
JSON.stringify(newConnectorsToggled)
|
||||
);
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const clearAllFilters = () => {
|
||||
const emptyFilters: FilterOptions = {
|
||||
accessType: null,
|
||||
docsCountFilter: {
|
||||
operator: null,
|
||||
value: null,
|
||||
},
|
||||
lastStatus: null,
|
||||
};
|
||||
|
||||
setFilterOptions(emptyFilters);
|
||||
|
||||
// Reset the FilterComponent's internal state
|
||||
if (filterComponentRef.current) {
|
||||
filterComponentRef.current.resetFilters();
|
||||
}
|
||||
};
|
||||
|
||||
// Check if filters are active
|
||||
const hasActiveFilters = useMemo(() => {
|
||||
return (
|
||||
(filterOptions.accessType && filterOptions.accessType.length > 0) ||
|
||||
(filterOptions.lastStatus && filterOptions.lastStatus.length > 0) ||
|
||||
filterOptions.docsCountFilter.operator !== null
|
||||
);
|
||||
}, [filterOptions]);
|
||||
|
||||
const toggleSource = (
|
||||
source: ValidSources,
|
||||
@ -377,130 +525,194 @@ export function CCPairIndexingStatusTable({
|
||||
sortedSources.length;
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<ConnectorRow
|
||||
invisible
|
||||
ccPairsIndexingStatus={{
|
||||
cc_pair_id: 1,
|
||||
name: "Sample File Connector",
|
||||
cc_pair_status: ConnectorCredentialPairStatus.ACTIVE,
|
||||
last_status: "success",
|
||||
connector: {
|
||||
<>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<ConnectorRow
|
||||
invisible
|
||||
ccPairsIndexingStatus={{
|
||||
cc_pair_id: 1,
|
||||
name: "Sample File Connector",
|
||||
source: ValidSources.File,
|
||||
input_type: "poll",
|
||||
connector_specific_config: {
|
||||
file_locations: ["/path/to/sample/file.txt"],
|
||||
cc_pair_status: ConnectorCredentialPairStatus.ACTIVE,
|
||||
last_status: "success",
|
||||
connector: {
|
||||
name: "Sample File Connector",
|
||||
source: ValidSources.File,
|
||||
input_type: "poll",
|
||||
connector_specific_config: {
|
||||
file_locations: ["/path/to/sample/file.txt"],
|
||||
},
|
||||
refresh_freq: 86400,
|
||||
prune_freq: null,
|
||||
indexing_start: new Date("2023-07-01T12:00:00Z"),
|
||||
id: 1,
|
||||
credential_ids: [],
|
||||
access_type: "public",
|
||||
time_created: "2023-07-01T12:00:00Z",
|
||||
time_updated: "2023-07-01T12:00:00Z",
|
||||
},
|
||||
credential: {
|
||||
id: 1,
|
||||
name: "Sample Credential",
|
||||
source: ValidSources.File,
|
||||
user_id: "1",
|
||||
time_created: "2023-07-01T12:00:00Z",
|
||||
time_updated: "2023-07-01T12:00:00Z",
|
||||
credential_json: {},
|
||||
admin_public: false,
|
||||
},
|
||||
refresh_freq: 86400,
|
||||
prune_freq: null,
|
||||
indexing_start: new Date("2023-07-01T12:00:00Z"),
|
||||
id: 1,
|
||||
credential_ids: [],
|
||||
access_type: "public",
|
||||
time_created: "2023-07-01T12:00:00Z",
|
||||
time_updated: "2023-07-01T12:00:00Z",
|
||||
},
|
||||
credential: {
|
||||
id: 1,
|
||||
name: "Sample Credential",
|
||||
source: ValidSources.File,
|
||||
user_id: "1",
|
||||
time_created: "2023-07-01T12:00:00Z",
|
||||
time_updated: "2023-07-01T12:00:00Z",
|
||||
credential_json: {},
|
||||
admin_public: false,
|
||||
},
|
||||
access_type: "public",
|
||||
docs_indexed: 1000,
|
||||
last_success: "2023-07-01T12:00:00Z",
|
||||
last_finished_status: "success",
|
||||
latest_index_attempt: null,
|
||||
groups: [], // Add this line
|
||||
}}
|
||||
isEditable={false}
|
||||
/>
|
||||
</TableHeader>
|
||||
<div className="flex -mt-12 items-center w-0 m4 gap-x-2">
|
||||
<input
|
||||
type="text"
|
||||
ref={searchInputRef}
|
||||
placeholder="Search connectors..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="ml-1 w-96 h-9 border border-border flex-none rounded-md bg-background-50 px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
/>
|
||||
docs_indexed: 1000,
|
||||
last_success: "2023-07-01T12:00:00Z",
|
||||
last_finished_status: "success",
|
||||
latest_index_attempt: null,
|
||||
groups: [], // Add this line
|
||||
}}
|
||||
isEditable={false}
|
||||
/>
|
||||
</TableHeader>
|
||||
<div className="flex -mt-12 items-center w-0 m4 gap-x-2">
|
||||
<input
|
||||
type="text"
|
||||
ref={searchInputRef}
|
||||
placeholder="Search connectors..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="ml-1 w-96 h-9 border border-border flex-none rounded-md bg-background-50 px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
/>
|
||||
|
||||
<Button className="h-9" onClick={() => toggleSources()}>
|
||||
{!shouldExpand ? "Collapse All" : "Expand All"}
|
||||
</Button>
|
||||
<button className="p-1 rounded-md borde border-neutral- ">
|
||||
<SortIcon size={24} className="text-neutral-800" />
|
||||
</button>
|
||||
</div>
|
||||
<TableBody>
|
||||
{sortedSources
|
||||
.filter(
|
||||
(source) => source != "not_applicable" && source != "ingestion_api"
|
||||
)
|
||||
.map((source, ind) => {
|
||||
const sourceMatches = source
|
||||
.toLowerCase()
|
||||
.includes(searchTerm.toLowerCase());
|
||||
const matchingConnectors = groupedStatuses[source].filter(
|
||||
(status) =>
|
||||
<Button className="h-9" onClick={() => toggleSources()}>
|
||||
{!shouldExpand ? "Collapse All" : "Expand All"}
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<FilterComponent
|
||||
onFilterChange={handleFilterChange}
|
||||
ref={filterComponentRef}
|
||||
/>
|
||||
|
||||
{hasActiveFilters && (
|
||||
<div className="flex flex-none items-center gap-1 ml-2 max-w-[500px]">
|
||||
{filterOptions.accessType &&
|
||||
filterOptions.accessType.length > 0 && (
|
||||
<Badge variant="secondary" className="px-2 py-0.5 text-xs">
|
||||
Access: {filterOptions.accessType.join(", ")}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{filterOptions.lastStatus &&
|
||||
filterOptions.lastStatus.length > 0 && (
|
||||
<Badge variant="secondary" className="px-2 py-0.5 text-xs">
|
||||
Status:{" "}
|
||||
{filterOptions.lastStatus
|
||||
.map((s) => s.replace(/_/g, " "))
|
||||
.join(", ")}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{filterOptions.docsCountFilter.operator &&
|
||||
filterOptions.docsCountFilter.value !== null && (
|
||||
<Badge variant="secondary" className="px-2 py-0.5 text-xs">
|
||||
Docs {filterOptions.docsCountFilter.operator}{" "}
|
||||
{filterOptions.docsCountFilter.value}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{filterOptions.docsCountFilter.operator &&
|
||||
filterOptions.docsCountFilter.value === null && (
|
||||
<Badge variant="secondary" className="px-2 py-0.5 text-xs">
|
||||
Docs {filterOptions.docsCountFilter.operator} any
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="px-2 py-0.5 text-xs border-red-400 bg-red-100 hover:border-red-600 cursor-pointer hover:bg-red-100 dark:hover:bg-red-900"
|
||||
onClick={() => {
|
||||
if (filterComponentRef.current) {
|
||||
filterComponentRef.current.resetFilters();
|
||||
setFilterOptions({
|
||||
accessType: null,
|
||||
docsCountFilter: {
|
||||
operator: null,
|
||||
value: null,
|
||||
},
|
||||
lastStatus: null,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="text-red-500 dark:text-red-400">Clear</span>
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<TableBody>
|
||||
{displaySources
|
||||
.filter(
|
||||
(source) =>
|
||||
source != "not_applicable" && source != "ingestion_api"
|
||||
)
|
||||
.map((source, ind) => {
|
||||
const sourceMatches = source
|
||||
.toLowerCase()
|
||||
.includes(searchTerm.toLowerCase());
|
||||
|
||||
const statuses =
|
||||
filteredGroupedStatuses[source] || groupedStatuses[source];
|
||||
|
||||
const matchingConnectors = statuses.filter((status) =>
|
||||
(status.name || "")
|
||||
.toLowerCase()
|
||||
.includes(searchTerm.toLowerCase())
|
||||
);
|
||||
if (sourceMatches || matchingConnectors.length > 0) {
|
||||
return (
|
||||
<React.Fragment key={ind}>
|
||||
<br className="mt-4" />
|
||||
<SummaryRow
|
||||
source={source}
|
||||
summary={groupSummaries[source]}
|
||||
isOpen={connectorsToggled[source] || false}
|
||||
onToggle={() => toggleSource(source)}
|
||||
/>
|
||||
{connectorsToggled[source] && (
|
||||
<>
|
||||
<TableRow
|
||||
noHover
|
||||
className="border ! border-border dark:border-neutral-700"
|
||||
>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Last Indexed</TableHead>
|
||||
<TableHead>Activity</TableHead>
|
||||
{isPaidEnterpriseFeaturesEnabled && (
|
||||
<TableHead>Permissions</TableHead>
|
||||
)}
|
||||
<TableHead>Total Docs</TableHead>
|
||||
<TableHead>Last Status</TableHead>
|
||||
<TableHead></TableHead>
|
||||
</TableRow>
|
||||
{(sourceMatches
|
||||
? groupedStatuses[source]
|
||||
: matchingConnectors
|
||||
).map((ccPairsIndexingStatus) => (
|
||||
<ConnectorRow
|
||||
key={ccPairsIndexingStatus.cc_pair_id}
|
||||
ccPairsIndexingStatus={ccPairsIndexingStatus}
|
||||
isEditable={editableCcPairsIndexingStatuses.some(
|
||||
(e) =>
|
||||
e.cc_pair_id === ccPairsIndexingStatus.cc_pair_id
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
if (sourceMatches || matchingConnectors.length > 0) {
|
||||
return (
|
||||
<React.Fragment key={ind}>
|
||||
<br className="mt-4" />
|
||||
<SummaryRow
|
||||
source={source}
|
||||
summary={groupSummaries[source]}
|
||||
isOpen={connectorsToggled[source] || false}
|
||||
onToggle={() => toggleSource(source)}
|
||||
/>
|
||||
{connectorsToggled[source] && (
|
||||
<>
|
||||
<TableRow className="border border-border dark:border-neutral-700">
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Last Indexed</TableHead>
|
||||
<TableHead>Activity</TableHead>
|
||||
{isPaidEnterpriseFeaturesEnabled && (
|
||||
<TableHead>Permissions</TableHead>
|
||||
)}
|
||||
<TableHead>Total Docs</TableHead>
|
||||
<TableHead>Last Status</TableHead>
|
||||
<TableHead></TableHead>
|
||||
</TableRow>
|
||||
{(sourceMatches ? statuses : matchingConnectors).map(
|
||||
(ccPairsIndexingStatus) => (
|
||||
<ConnectorRow
|
||||
key={ccPairsIndexingStatus.cc_pair_id}
|
||||
ccPairsIndexingStatus={ccPairsIndexingStatus}
|
||||
isEditable={editableCcPairsIndexingStatuses.some(
|
||||
(e) =>
|
||||
e.cc_pair_id ===
|
||||
ccPairsIndexingStatus.cc_pair_id
|
||||
)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
385
web/src/app/admin/indexing/status/FilterComponent.tsx
Normal file
385
web/src/app/admin/indexing/status/FilterComponent.tsx
Normal file
@ -0,0 +1,385 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useImperativeHandle, forwardRef } from "react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { SortIcon } from "@/components/icons/icons";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { AccessType, ValidStatuses } from "@/lib/types";
|
||||
import { FiFilter, FiX, FiCheck } from "react-icons/fi";
|
||||
|
||||
export interface FilterOptions {
|
||||
accessType: AccessType[] | null;
|
||||
docsCountFilter: {
|
||||
operator: ">" | "<" | "=" | null;
|
||||
value: number | null;
|
||||
};
|
||||
lastStatus: ValidStatuses[] | null;
|
||||
}
|
||||
|
||||
interface FilterComponentProps {
|
||||
onFilterChange: (filters: FilterOptions) => void;
|
||||
}
|
||||
|
||||
export const FilterComponent = forwardRef<
|
||||
{ resetFilters: () => void },
|
||||
FilterComponentProps
|
||||
>(({ onFilterChange }, ref) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [filters, setFilters] = useState<FilterOptions>({
|
||||
accessType: null,
|
||||
docsCountFilter: {
|
||||
operator: null,
|
||||
value: null,
|
||||
},
|
||||
lastStatus: null,
|
||||
});
|
||||
|
||||
// Local state for tracking selected filters before applying
|
||||
const [docsOperator, setDocsOperator] = useState<">" | "<" | "=" | null>(
|
||||
null
|
||||
);
|
||||
const [docsValue, setDocsValue] = useState<string>("");
|
||||
const [selectedAccessTypes, setSelectedAccessTypes] = useState<AccessType[]>(
|
||||
[]
|
||||
);
|
||||
const [selectedStatuses, setSelectedStatuses] = useState<ValidStatuses[]>([]);
|
||||
|
||||
// Expose resetFilters method via ref
|
||||
useImperativeHandle(ref, () => ({
|
||||
resetFilters: () => {
|
||||
setDocsOperator(null);
|
||||
setDocsValue("");
|
||||
setSelectedAccessTypes([]);
|
||||
setSelectedStatuses([]);
|
||||
setFilters({
|
||||
accessType: null,
|
||||
docsCountFilter: {
|
||||
operator: null,
|
||||
value: null,
|
||||
},
|
||||
lastStatus: null,
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
const handleAccessTypeChange = (accessType: AccessType) => {
|
||||
const newAccessTypes = selectedAccessTypes.includes(accessType)
|
||||
? selectedAccessTypes.filter((type) => type !== accessType)
|
||||
: [...selectedAccessTypes, accessType];
|
||||
|
||||
setSelectedAccessTypes(newAccessTypes);
|
||||
};
|
||||
|
||||
const handleStatusChange = (status: ValidStatuses) => {
|
||||
const newStatuses = selectedStatuses.includes(status)
|
||||
? selectedStatuses.filter((s) => s !== status)
|
||||
: [...selectedStatuses, status];
|
||||
|
||||
setSelectedStatuses(newStatuses);
|
||||
};
|
||||
|
||||
const handleDocsFilterChange = () => {
|
||||
if (docsOperator && docsValue) {
|
||||
const newFilters = {
|
||||
...filters,
|
||||
accessType: selectedAccessTypes.length > 0 ? selectedAccessTypes : null,
|
||||
lastStatus: selectedStatuses.length > 0 ? selectedStatuses : null,
|
||||
docsCountFilter: {
|
||||
operator: docsOperator,
|
||||
value: parseInt(docsValue),
|
||||
},
|
||||
};
|
||||
|
||||
setFilters(newFilters);
|
||||
onFilterChange(newFilters);
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const applyFilters = () => {
|
||||
const newFilters = {
|
||||
...filters,
|
||||
accessType: selectedAccessTypes.length > 0 ? selectedAccessTypes : null,
|
||||
lastStatus: selectedStatuses.length > 0 ? selectedStatuses : null,
|
||||
docsCountFilter: {
|
||||
operator: docsOperator,
|
||||
value: docsValue ? parseInt(docsValue) : null,
|
||||
},
|
||||
};
|
||||
|
||||
setFilters(newFilters);
|
||||
onFilterChange(newFilters);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const clearFilters = () => {
|
||||
setSelectedAccessTypes([]);
|
||||
setSelectedStatuses([]);
|
||||
setDocsOperator(null);
|
||||
setDocsValue("");
|
||||
|
||||
const newFilters = {
|
||||
accessType: null,
|
||||
docsCountFilter: {
|
||||
operator: null,
|
||||
value: null,
|
||||
},
|
||||
lastStatus: null,
|
||||
};
|
||||
|
||||
setFilters(newFilters);
|
||||
onFilterChange(newFilters);
|
||||
};
|
||||
|
||||
// Sync local state with filters when dropdown opens
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (open) {
|
||||
// When opening, initialize local state from current filters
|
||||
setSelectedAccessTypes(filters.accessType || []);
|
||||
setSelectedStatuses(filters.lastStatus || []);
|
||||
setDocsOperator(filters.docsCountFilter.operator);
|
||||
setDocsValue(
|
||||
filters.docsCountFilter.value !== null
|
||||
? filters.docsCountFilter.value.toString()
|
||||
: ""
|
||||
);
|
||||
}
|
||||
setIsOpen(open);
|
||||
};
|
||||
|
||||
const hasActiveFilters =
|
||||
(filters.accessType && filters.accessType.length > 0) ||
|
||||
(filters.lastStatus && filters.lastStatus.length > 0) ||
|
||||
filters.docsCountFilter.operator !== null;
|
||||
|
||||
// Get active filter count for badge
|
||||
const getActiveFilterCount = () => {
|
||||
let count = 0;
|
||||
if (filters.accessType && filters.accessType.length > 0) count++;
|
||||
if (filters.lastStatus && filters.lastStatus.length > 0) count++;
|
||||
if (filters.docsCountFilter.operator !== null) count++;
|
||||
return count;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<DropdownMenu open={isOpen} onOpenChange={handleOpenChange}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className={`p-2 h-9 ${
|
||||
hasActiveFilters ? "border-primary bg-primary/5" : ""
|
||||
}`}
|
||||
>
|
||||
<SortIcon size={20} className="text-neutral-800" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className="w-72"
|
||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<div className="flex items-center justify-between px-2 py-1.5">
|
||||
<DropdownMenuLabel className="text-base font-medium">
|
||||
Filter Connectors
|
||||
</DropdownMenuLabel>
|
||||
{hasActiveFilters && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 px-2 text-xs"
|
||||
onClick={clearFilters}
|
||||
>
|
||||
Clear All
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel className="px-2 py-1.5 text-xs text-muted-foreground">
|
||||
Access Type
|
||||
</DropdownMenuLabel>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={selectedAccessTypes.includes("public")}
|
||||
onCheckedChange={() => handleAccessTypeChange("public")}
|
||||
className="flex items-center justify-between"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Public
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={selectedAccessTypes.includes("private")}
|
||||
onCheckedChange={() => handleAccessTypeChange("private")}
|
||||
className="flex items-center justify-between"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Private
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={selectedAccessTypes.includes("sync")}
|
||||
onCheckedChange={() => handleAccessTypeChange("sync")}
|
||||
className="flex items-center justify-between"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Auto-Sync
|
||||
</DropdownMenuCheckboxItem>
|
||||
</div>
|
||||
</DropdownMenuGroup>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel className="px-2 py-1.5 text-xs text-muted-foreground">
|
||||
Last Status
|
||||
</DropdownMenuLabel>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={selectedStatuses.includes("success")}
|
||||
onCheckedChange={() => handleStatusChange("success")}
|
||||
className="flex items-center justify-between"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Success
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={selectedStatuses.includes("failed")}
|
||||
onCheckedChange={() => handleStatusChange("failed")}
|
||||
className="flex items-center justify-between"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Failed
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={selectedStatuses.includes("in_progress")}
|
||||
onCheckedChange={() => handleStatusChange("in_progress")}
|
||||
className="flex items-center justify-between"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
In Progress
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={selectedStatuses.includes("not_started")}
|
||||
onCheckedChange={() => handleStatusChange("not_started")}
|
||||
className="flex items-center justify-between"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Not Started
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={selectedStatuses.includes("completed_with_errors")}
|
||||
onCheckedChange={() =>
|
||||
handleStatusChange("completed_with_errors")
|
||||
}
|
||||
className="flex items-center justify-between"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Completed with Errors
|
||||
</DropdownMenuCheckboxItem>
|
||||
</div>
|
||||
</DropdownMenuGroup>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel className="px-2 py-1.5 text-xs text-muted-foreground">
|
||||
Document Count
|
||||
</DropdownMenuLabel>
|
||||
<div
|
||||
className="flex items-center px-2 py-2 gap-2"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant={docsOperator === ">" ? "default" : "outline"}
|
||||
size="sm"
|
||||
className="h-8 px-2"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDocsOperator(docsOperator === ">" ? null : ">");
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
>
|
||||
</Button>
|
||||
<Button
|
||||
variant={docsOperator === "<" ? "default" : "outline"}
|
||||
size="sm"
|
||||
className="h-8 px-2"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDocsOperator(docsOperator === "<" ? null : "<");
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<
|
||||
</Button>
|
||||
<Button
|
||||
variant={docsOperator === "=" ? "default" : "outline"}
|
||||
size="sm"
|
||||
className="h-8 px-2"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDocsOperator(docsOperator === "=" ? null : "=");
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
=
|
||||
</Button>
|
||||
</div>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Count"
|
||||
value={docsValue}
|
||||
onChange={(e) => setDocsValue(e.target.value)}
|
||||
className="h-8 w-full"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</div>
|
||||
<div className="px-2 py-1.5">
|
||||
<Button
|
||||
size="sm"
|
||||
className="w-full h-8"
|
||||
disabled={false}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
applyFilters();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{hasActiveFilters && (
|
||||
<div className="absolute -top-1 -right-1">
|
||||
<Badge className="h-2 bg-red-400 border-red-400 w-2 p-0 border-2 flex items-center justify-center" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
FilterComponent.displayName = "FilterComponent";
|
@ -1,5 +1,5 @@
|
||||
import { ConnectorIndexingStatus, ConnectorStatus } from "@/lib/types";
|
||||
import { ConnectorTitle } from "@/components/admin/connectors/ConnectorTitle";
|
||||
import { ConnectorStatus } from "@/lib/types";
|
||||
import { ConnectorMultiSelect } from "@/components/ConnectorMultiSelect";
|
||||
|
||||
interface ConnectorEditorProps {
|
||||
selectedCCPairIds: number[];
|
||||
@ -12,55 +12,20 @@ export const ConnectorEditor = ({
|
||||
setSetCCPairIds,
|
||||
allCCPairs,
|
||||
}: ConnectorEditorProps) => {
|
||||
// Filter out public docs, since they don't make sense as part of a group
|
||||
const privateCCPairs = allCCPairs.filter(
|
||||
(ccPair) => ccPair.access_type === "private"
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mb-3 flex gap-2 flex-wrap">
|
||||
{allCCPairs
|
||||
// remove public docs, since they don't make sense as part of a group
|
||||
.filter((ccPair) => !(ccPair.access_type === "public"))
|
||||
.map((ccPair) => {
|
||||
const ind = selectedCCPairIds.indexOf(ccPair.cc_pair_id);
|
||||
const isSelected = ind !== -1;
|
||||
return (
|
||||
<div
|
||||
key={`${ccPair.connector.id}-${ccPair.credential.id}`}
|
||||
className={
|
||||
`
|
||||
px-3
|
||||
py-1
|
||||
rounded-lg
|
||||
border
|
||||
border-border
|
||||
w-fit
|
||||
flex
|
||||
cursor-pointer ` +
|
||||
(isSelected
|
||||
? " bg-accent-background-hovered"
|
||||
: " hover:bg-accent-background")
|
||||
}
|
||||
onClick={() => {
|
||||
if (isSelected) {
|
||||
setSetCCPairIds(
|
||||
selectedCCPairIds.filter(
|
||||
(ccPairId) => ccPairId !== ccPair.cc_pair_id
|
||||
)
|
||||
);
|
||||
} else {
|
||||
setSetCCPairIds([...selectedCCPairIds, ccPair.cc_pair_id]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="my-auto">
|
||||
<ConnectorTitle
|
||||
connector={ccPair.connector}
|
||||
ccPairId={ccPair.cc_pair_id}
|
||||
ccPairName={ccPair.name}
|
||||
isLink={false}
|
||||
showMetadata={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<ConnectorMultiSelect
|
||||
name="connectors"
|
||||
label="Connectors"
|
||||
connectors={privateCCPairs}
|
||||
selectedIds={selectedCCPairIds}
|
||||
onChange={setSetCCPairIds}
|
||||
placeholder="Search for connectors..."
|
||||
showError={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,13 +1,15 @@
|
||||
import { Button } from "@/components/Button";
|
||||
import { SearchMultiSelectDropdown } from "@/components/Dropdown";
|
||||
import { Modal } from "@/components/Modal";
|
||||
import { useState } from "react";
|
||||
import { FiPlus, FiX } from "react-icons/fi";
|
||||
import { FiX } from "react-icons/fi";
|
||||
import { updateUserGroup } from "./lib";
|
||||
import { PopupSpec } from "@/components/admin/connectors/Popup";
|
||||
import { ConnectorStatus, UserGroup } from "@/lib/types";
|
||||
import { ConnectorTitle } from "@/components/admin/connectors/ConnectorTitle";
|
||||
import { Connector } from "@/lib/connectors/connectors";
|
||||
import { ConnectorMultiSelect } from "@/components/ConnectorMultiSelect";
|
||||
import { Form } from "formik";
|
||||
|
||||
interface AddConnectorFormProps {
|
||||
ccPairs: ConnectorStatus<any, any>[];
|
||||
userGroup: UserGroup;
|
||||
@ -23,132 +25,64 @@ export const AddConnectorForm: React.FC<AddConnectorFormProps> = ({
|
||||
}) => {
|
||||
const [selectedCCPairIds, setSelectedCCPairIds] = useState<number[]>([]);
|
||||
|
||||
const selectedCCPairs = ccPairs.filter((ccPair) =>
|
||||
selectedCCPairIds.includes(ccPair.cc_pair_id)
|
||||
);
|
||||
// Filter out ccPairs that are already in the user group and are not private
|
||||
const availableCCPairs = ccPairs
|
||||
.filter(
|
||||
(ccPair) =>
|
||||
!userGroup.cc_pairs
|
||||
.map((userGroupCCPair) => userGroupCCPair.id)
|
||||
.includes(ccPair.cc_pair_id)
|
||||
)
|
||||
.filter((ccPair) => ccPair.access_type === "private");
|
||||
|
||||
return (
|
||||
<Modal title="Add New Connector" onOutsideClick={() => onClose()}>
|
||||
<div className="px-6 pt-4 pb-12">
|
||||
<div className="mb-2 flex flex-wrap gap-x-2">
|
||||
{selectedCCPairs.length > 0 &&
|
||||
selectedCCPairs.map((ccPair) => (
|
||||
<div
|
||||
key={ccPair.cc_pair_id}
|
||||
onClick={() => {
|
||||
setSelectedCCPairIds(
|
||||
selectedCCPairIds.filter(
|
||||
(ccPairId) => ccPairId !== ccPair.cc_pair_id
|
||||
)
|
||||
);
|
||||
}}
|
||||
className={`
|
||||
flex
|
||||
rounded-lg
|
||||
px-2
|
||||
py-1
|
||||
my-1
|
||||
border
|
||||
border-border
|
||||
hover:bg-accent-background-hovered
|
||||
cursor-pointer`}
|
||||
>
|
||||
<ConnectorTitle
|
||||
ccPairId={ccPair.cc_pair_id}
|
||||
ccPairName={ccPair.name}
|
||||
connector={ccPair.connector}
|
||||
isLink={false}
|
||||
showMetadata={false}
|
||||
/>
|
||||
<FiX className="ml-1 my-auto" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<ConnectorMultiSelect
|
||||
name="connectors"
|
||||
label="Select Connectors"
|
||||
connectors={availableCCPairs}
|
||||
selectedIds={selectedCCPairIds}
|
||||
onChange={setSelectedCCPairIds}
|
||||
placeholder="Search for connectors to add..."
|
||||
showError={false}
|
||||
/>
|
||||
|
||||
<div className="flex">
|
||||
<SearchMultiSelectDropdown
|
||||
options={ccPairs
|
||||
.filter(
|
||||
(ccPair) =>
|
||||
!selectedCCPairIds.includes(ccPair.cc_pair_id) &&
|
||||
!userGroup.cc_pairs
|
||||
.map((userGroupCCPair) => userGroupCCPair.id)
|
||||
.includes(ccPair.cc_pair_id)
|
||||
)
|
||||
// remove public and synced docs, since they don't make sense as part of a group
|
||||
.filter((ccPair) => ccPair.access_type === "private")
|
||||
.map((ccPair) => {
|
||||
return {
|
||||
name: ccPair.name?.toString() || "",
|
||||
value: ccPair.cc_pair_id?.toString(),
|
||||
metadata: {
|
||||
ccPairId: ccPair.cc_pair_id,
|
||||
connector: ccPair.connector,
|
||||
},
|
||||
};
|
||||
})}
|
||||
onSelect={(option) => {
|
||||
setSelectedCCPairIds([
|
||||
...Array.from(
|
||||
new Set([
|
||||
...selectedCCPairIds,
|
||||
parseInt(option.value as string),
|
||||
])
|
||||
),
|
||||
]);
|
||||
}}
|
||||
itemComponent={({ option }) => (
|
||||
<div className="flex px-4 py-2.5 hover:bg-accent-background-hovered cursor-pointer">
|
||||
<div className="my-auto">
|
||||
<ConnectorTitle
|
||||
ccPairId={option?.metadata?.ccPairId as number}
|
||||
ccPairName={option.name}
|
||||
connector={option?.metadata?.connector as Connector<any>}
|
||||
isLink={false}
|
||||
showMetadata={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-auto my-auto">
|
||||
<FiPlus />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
className="ml-3 flex-nowrap w-48"
|
||||
onClick={async () => {
|
||||
const newCCPairIds = [
|
||||
...Array.from(
|
||||
new Set(
|
||||
userGroup.cc_pairs
|
||||
.map((ccPair) => ccPair.id)
|
||||
.concat(selectedCCPairIds)
|
||||
)
|
||||
),
|
||||
];
|
||||
const response = await updateUserGroup(userGroup.id, {
|
||||
user_ids: userGroup.users.map((user) => user.id),
|
||||
cc_pair_ids: newCCPairIds,
|
||||
<Button
|
||||
className="mt-4 flex-nowrap w-48"
|
||||
onClick={async () => {
|
||||
const newCCPairIds = [
|
||||
...Array.from(
|
||||
new Set(
|
||||
userGroup.cc_pairs
|
||||
.map((ccPair) => ccPair.id)
|
||||
.concat(selectedCCPairIds)
|
||||
)
|
||||
),
|
||||
];
|
||||
const response = await updateUserGroup(userGroup.id, {
|
||||
user_ids: userGroup.users.map((user) => user.id),
|
||||
cc_pair_ids: newCCPairIds,
|
||||
});
|
||||
if (response.ok) {
|
||||
setPopup({
|
||||
message: "Successfully added connectors to group",
|
||||
type: "success",
|
||||
});
|
||||
if (response.ok) {
|
||||
setPopup({
|
||||
message: "Successfully added users to group",
|
||||
type: "success",
|
||||
});
|
||||
onClose();
|
||||
} else {
|
||||
const responseJson = await response.json();
|
||||
const errorMsg = responseJson.detail || responseJson.message;
|
||||
setPopup({
|
||||
message: `Failed to add users to group - ${errorMsg}`,
|
||||
type: "error",
|
||||
});
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Add Connectors
|
||||
</Button>
|
||||
</div>
|
||||
onClose();
|
||||
} else {
|
||||
const responseJson = await response.json();
|
||||
const errorMsg = responseJson.detail || responseJson.message;
|
||||
setPopup({
|
||||
message: `Failed to add connectors to group - ${errorMsg}`,
|
||||
type: "error",
|
||||
});
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Add Connectors
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
@ -15,6 +15,7 @@ interface ConnectorMultiSelectProps {
|
||||
onChange: (selectedIds: number[]) => void;
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
showError?: boolean;
|
||||
}
|
||||
|
||||
export const ConnectorMultiSelect = ({
|
||||
@ -25,6 +26,7 @@ export const ConnectorMultiSelect = ({
|
||||
onChange,
|
||||
disabled = false,
|
||||
placeholder = "Search connectors...",
|
||||
showError = false,
|
||||
}: ConnectorMultiSelectProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
@ -40,6 +42,9 @@ export const ConnectorMultiSelect = ({
|
||||
(connector) => !selectedIds.includes(connector.cc_pair_id)
|
||||
);
|
||||
|
||||
// Check if all connectors are selected
|
||||
const allConnectorsSelected = unselectedConnectors.length === 0;
|
||||
|
||||
// Filter unselected connectors based on search query
|
||||
const filteredUnselectedConnectors = unselectedConnectors.filter(
|
||||
(connector) => {
|
||||
@ -48,15 +53,40 @@ export const ConnectorMultiSelect = ({
|
||||
}
|
||||
);
|
||||
|
||||
// Close dropdown if there are no more connectors to select
|
||||
useEffect(() => {
|
||||
if (allConnectorsSelected && open) {
|
||||
setOpen(false);
|
||||
// Blur the input to remove focus when all connectors are selected
|
||||
inputRef.current?.blur();
|
||||
// Clear search query when all connectors are selected
|
||||
setSearchQuery("");
|
||||
}
|
||||
}, [allConnectorsSelected, open]);
|
||||
|
||||
// Also check when selectedIds changes to handle the case when the last connector is selected
|
||||
useEffect(() => {
|
||||
if (allConnectorsSelected) {
|
||||
inputRef.current?.blur();
|
||||
setSearchQuery("");
|
||||
}
|
||||
}, [allConnectorsSelected, selectedIds]);
|
||||
|
||||
// Handle selection
|
||||
const selectConnector = (connectorId: number) => {
|
||||
onChange([...selectedIds, connectorId]);
|
||||
const newSelectedIds = [...selectedIds, connectorId];
|
||||
onChange(newSelectedIds);
|
||||
setSearchQuery(""); // Clear search after selection
|
||||
|
||||
// Focus back on input after selection
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
}, 0);
|
||||
// Check if this was the last connector to select
|
||||
const willAllBeSelected = connectors.length === newSelectedIds.length;
|
||||
|
||||
// Only focus back on input if there are still connectors to select
|
||||
if (!willAllBeSelected) {
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
// Remove a selected connector
|
||||
@ -90,8 +120,16 @@ export const ConnectorMultiSelect = ({
|
||||
}
|
||||
};
|
||||
|
||||
// Determine the placeholder text based on whether all connectors are selected
|
||||
const effectivePlaceholder = allConnectorsSelected
|
||||
? "All connectors selected"
|
||||
: placeholder;
|
||||
|
||||
// Determine if the input should be disabled
|
||||
const isInputDisabled = disabled || allConnectorsSelected;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col max-w-md space-y-2 mb-4">
|
||||
<div className="flex flex-col w-full space-y-2 mb-4">
|
||||
{label && <Label className="text-base font-medium">{label}</Label>}
|
||||
|
||||
<p className="text-xs text-neutral-500 ">
|
||||
@ -100,7 +138,11 @@ export const ConnectorMultiSelect = ({
|
||||
</p>
|
||||
{/* Persistent search bar */}
|
||||
<div className="relative">
|
||||
<div className="flex items-center border border-input rounded-md border border-neutral-200 focus-within:ring-1 focus-within:ring-ring focus-within:border-neutral-400 transition-colors">
|
||||
<div
|
||||
className={`flex items-center border border-input rounded-md border border-neutral-200 ${
|
||||
allConnectorsSelected ? "bg-neutral-50" : ""
|
||||
} focus-within:ring-1 focus-within:ring-ring focus-within:border-neutral-400 transition-colors`}
|
||||
>
|
||||
<Search className="absolute left-3 h-4 w-4 text-neutral-500" />
|
||||
<input
|
||||
ref={inputRef}
|
||||
@ -110,16 +152,22 @@ export const ConnectorMultiSelect = ({
|
||||
setSearchQuery(e.target.value);
|
||||
setOpen(true);
|
||||
}}
|
||||
onFocus={() => setOpen(true)}
|
||||
onFocus={() => {
|
||||
if (!allConnectorsSelected) {
|
||||
setOpen(true);
|
||||
}
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
className="h-9 w-full pl-9 pr-10 py-2 bg-transparent text-sm outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||
disabled={disabled}
|
||||
placeholder={effectivePlaceholder}
|
||||
className={`h-9 w-full pl-9 pr-10 py-2 bg-transparent text-sm outline-none disabled:cursor-not-allowed disabled:opacity-50 ${
|
||||
allConnectorsSelected ? "text-neutral-500" : ""
|
||||
}`}
|
||||
disabled={isInputDisabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Dropdown for unselected connectors */}
|
||||
{open && (
|
||||
{open && !allConnectorsSelected && (
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
className="absolute z-50 w-full mt-1 rounded-md border border-neutral-200 bg-white shadow-md default-scrollbar max-h-[300px] overflow-auto"
|
||||
@ -147,9 +195,6 @@ export const ConnectorMultiSelect = ({
|
||||
showMetadata={false}
|
||||
/>
|
||||
</div>
|
||||
{/* <div className="flex-shrink-0 text-neutral-400 hover:text-blue-500">
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
</div> */}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -158,13 +203,8 @@ export const ConnectorMultiSelect = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Selected connectors display */}
|
||||
{selectedConnectors.length > 0 ? (
|
||||
// <div className="mt-3 p-3 border border-neutral-200 rounded-md bg-background-50">
|
||||
<div className="mt-3 ">
|
||||
{/* <div className="text-xs font-medium text-neutral-700 mb-2">
|
||||
Selected:
|
||||
</div> */}
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{selectedConnectors.map((connector) => (
|
||||
<div
|
||||
@ -199,11 +239,13 @@ export const ConnectorMultiSelect = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ErrorMessage
|
||||
name={name}
|
||||
component="div"
|
||||
className="text-red-500 text-xs mt-1"
|
||||
/>
|
||||
{showError && (
|
||||
<ErrorMessage
|
||||
name={name}
|
||||
component="div"
|
||||
className="text-red-500 text-xs mt-1"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user