diff --git a/web/src/app/admin/indexing/status/CCPairIndexingStatusTable.tsx b/web/src/app/admin/indexing/status/CCPairIndexingStatusTable.tsx index 9ffa57a81..e57be36a1 100644 --- a/web/src/app/admin/indexing/status/CCPairIndexingStatusTable.tsx +++ b/web/src/app/admin/indexing/status/CCPairIndexingStatusTable.tsx @@ -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({ + 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[]> = {} as Record[]>; @@ -338,12 +359,139 @@ export function CCPairIndexingStatusTable({ }; }); + // Apply filters to create filtered grouped statuses + const filteredGrouped: Record< + ValidSources, + ConnectorIndexingStatus[] + > = {} as Record[]>; + + 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 ( - - - +
+ + - -
- 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} + /> + +
+ 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" + /> - - -
- - {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) => + + +
+ + + {hasActiveFilters && ( +
+ {filterOptions.accessType && + filterOptions.accessType.length > 0 && ( + + Access: {filterOptions.accessType.join(", ")} + + )} + + {filterOptions.lastStatus && + filterOptions.lastStatus.length > 0 && ( + + Status:{" "} + {filterOptions.lastStatus + .map((s) => s.replace(/_/g, " ")) + .join(", ")} + + )} + + {filterOptions.docsCountFilter.operator && + filterOptions.docsCountFilter.value !== null && ( + + Docs {filterOptions.docsCountFilter.operator}{" "} + {filterOptions.docsCountFilter.value} + + )} + + {filterOptions.docsCountFilter.operator && + filterOptions.docsCountFilter.value === null && ( + + Docs {filterOptions.docsCountFilter.operator} any + + )} + + { + if (filterComponentRef.current) { + filterComponentRef.current.resetFilters(); + setFilterOptions({ + accessType: null, + docsCountFilter: { + operator: null, + value: null, + }, + lastStatus: null, + }); + } + }} + > + Clear + +
+ )} +
+
+ + {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 ( - -
- toggleSource(source)} - /> - {connectorsToggled[source] && ( - <> - - Name - Last Indexed - Activity - {isPaidEnterpriseFeaturesEnabled && ( - Permissions - )} - Total Docs - Last Status - - - {(sourceMatches - ? groupedStatuses[source] - : matchingConnectors - ).map((ccPairsIndexingStatus) => ( - - e.cc_pair_id === ccPairsIndexingStatus.cc_pair_id - )} - /> - ))} - - )} -
); - } - return null; - })} -
-
+ + if (sourceMatches || matchingConnectors.length > 0) { + return ( + +
+ toggleSource(source)} + /> + {connectorsToggled[source] && ( + <> + + Name + Last Indexed + Activity + {isPaidEnterpriseFeaturesEnabled && ( + Permissions + )} + Total Docs + Last Status + + + {(sourceMatches ? statuses : matchingConnectors).map( + (ccPairsIndexingStatus) => ( + + e.cc_pair_id === + ccPairsIndexingStatus.cc_pair_id + )} + /> + ) + )} + + )} +
+ ); + } + return null; + })} + + + ); } diff --git a/web/src/app/admin/indexing/status/FilterComponent.tsx b/web/src/app/admin/indexing/status/FilterComponent.tsx new file mode 100644 index 000000000..24a06aee9 --- /dev/null +++ b/web/src/app/admin/indexing/status/FilterComponent.tsx @@ -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({ + 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(""); + const [selectedAccessTypes, setSelectedAccessTypes] = useState( + [] + ); + const [selectedStatuses, setSelectedStatuses] = useState([]); + + // 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 ( +
+ + + + + e.preventDefault()} + > +
+ + Filter Connectors + + {hasActiveFilters && ( + + )} +
+ + + + + Access Type + +
e.stopPropagation()}> + handleAccessTypeChange("public")} + className="flex items-center justify-between" + onSelect={(e) => e.preventDefault()} + > + Public + + handleAccessTypeChange("private")} + className="flex items-center justify-between" + onSelect={(e) => e.preventDefault()} + > + Private + + handleAccessTypeChange("sync")} + className="flex items-center justify-between" + onSelect={(e) => e.preventDefault()} + > + Auto-Sync + +
+
+ + + + + + Last Status + +
e.stopPropagation()}> + handleStatusChange("success")} + className="flex items-center justify-between" + onSelect={(e) => e.preventDefault()} + > + Success + + handleStatusChange("failed")} + className="flex items-center justify-between" + onSelect={(e) => e.preventDefault()} + > + Failed + + handleStatusChange("in_progress")} + className="flex items-center justify-between" + onSelect={(e) => e.preventDefault()} + > + In Progress + + handleStatusChange("not_started")} + className="flex items-center justify-between" + onSelect={(e) => e.preventDefault()} + > + Not Started + + + handleStatusChange("completed_with_errors") + } + className="flex items-center justify-between" + onSelect={(e) => e.preventDefault()} + > + Completed with Errors + +
+
+ + + + + + Document Count + +
e.stopPropagation()} + > +
+ + + +
+ setDocsValue(e.target.value)} + className="h-8 w-full" + onClick={(e) => e.stopPropagation()} + /> +
+
+ +
+
+
+
+ + {hasActiveFilters && ( +
+ +
+ )} +
+ ); +}); + +FilterComponent.displayName = "FilterComponent"; diff --git a/web/src/app/ee/admin/groups/ConnectorEditor.tsx b/web/src/app/ee/admin/groups/ConnectorEditor.tsx index b8185f01a..18459316f 100644 --- a/web/src/app/ee/admin/groups/ConnectorEditor.tsx +++ b/web/src/app/ee/admin/groups/ConnectorEditor.tsx @@ -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 ( -
- {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 ( -
{ - if (isSelected) { - setSetCCPairIds( - selectedCCPairIds.filter( - (ccPairId) => ccPairId !== ccPair.cc_pair_id - ) - ); - } else { - setSetCCPairIds([...selectedCCPairIds, ccPair.cc_pair_id]); - } - }} - > -
- -
-
- ); - })} -
+ ); }; diff --git a/web/src/app/ee/admin/groups/[groupId]/AddConnectorForm.tsx b/web/src/app/ee/admin/groups/[groupId]/AddConnectorForm.tsx index a9ca6da14..01b06020c 100644 --- a/web/src/app/ee/admin/groups/[groupId]/AddConnectorForm.tsx +++ b/web/src/app/ee/admin/groups/[groupId]/AddConnectorForm.tsx @@ -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[]; userGroup: UserGroup; @@ -23,132 +25,64 @@ export const AddConnectorForm: React.FC = ({ }) => { const [selectedCCPairIds, setSelectedCCPairIds] = useState([]); - 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 ( onClose()}>
-
- {selectedCCPairs.length > 0 && - selectedCCPairs.map((ccPair) => ( -
{ - 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`} - > - - -
- ))} -
+ -
- - !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 }) => ( -
-
- } - isLink={false} - showMetadata={false} - /> -
-
- -
-
- )} - /> - -
+ 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 +
); diff --git a/web/src/components/ConnectorMultiSelect.tsx b/web/src/components/ConnectorMultiSelect.tsx index e3cd24178..704f99390 100644 --- a/web/src/components/ConnectorMultiSelect.tsx +++ b/web/src/components/ConnectorMultiSelect.tsx @@ -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 ( -
+
{label && }

@@ -100,7 +138,11 @@ export const ConnectorMultiSelect = ({

{/* Persistent search bar */}
-
+
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} />
{/* Dropdown for unselected connectors */} - {open && ( + {open && !allConnectorsSelected && (
- {/*
- -
*/}
))}
@@ -158,13 +203,8 @@ export const ConnectorMultiSelect = ({ )}
- {/* Selected connectors display */} {selectedConnectors.length > 0 ? ( - //
- {/*
- Selected: -
*/}
{selectedConnectors.map((connector) => (
)} - + {showError && ( + + )}
); };