Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,25 @@
* 2.0.
*/

import type { CriteriaWithPagination, EuiSearchBarOnChangeArgs, UseEuiTheme } from '@elastic/eui';
import type { CriteriaWithPagination } from '@elastic/eui';
import {
type EuiBasicTableColumn,
EuiButtonEmpty,
EuiContextMenuItem,
EuiContextMenuPanel,
EuiFlexGroup,
EuiFlexItem,
EuiHighlight,
EuiInMemoryTable,
type EuiInMemoryTableProps,
EuiPanel,
EuiPopover,
EuiSkeletonLoading,
EuiSkeletonText,
EuiText,
} from '@elastic/eui';
import { css } from '@emotion/react';
import { FormattedMessage } from '@kbn/i18n-react';
import type { Tool as McpTool } from '@kbn/mcp-client';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import useToggle from 'react-use/lib/useToggle';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { labels } from '../../../utils/i18n';
import { truncateAtSentence } from '../../../utils/truncate_at_sentence';
import { McpToolsSelectionTableHeader } from './mcp_tools_selection_table_header';
import type { McpToolField } from './types';
import { useMcpToolsSearch } from './use_mcp_tools_search';

const DEFAULT_PAGE_SIZE = 10;
const PAGE_SIZE_OPTIONS = [10, 25, 50];
Expand All @@ -42,23 +36,6 @@ const tableContainerStyles = (isDisabled = false) => css`
`}
`;

const tableHeaderContainerStyles = ({ euiTheme }: UseEuiTheme) => css`
margin-block-start: -${euiTheme.size.s};
`;

const tableHeaderStyles = css`
min-height: 24px;
`;

const tableHeaderSkeletonStyles = css`
display: inline-block;
width: 200px;
`;

const tableHeaderButtonStyles = ({ euiTheme }: UseEuiTheme) => css`
font-weight: ${euiTheme.font.weight.semiBold};
`;

export interface McpToolsSelectionTableProps {
tools: readonly McpTool[];
selectedTools: McpToolField[];
Expand All @@ -78,16 +55,25 @@ export const McpToolsSelectionTable: React.FC<McpToolsSelectionTableProps> = ({
isDisabled,
disabledMessage,
}) => {
const [searchQuery, setSearchQuery] = useState('');
const [tablePageIndex, setTablePageIndex] = useState(0);
const [tablePageSize, setTablePageSize] = useState(DEFAULT_PAGE_SIZE);
const [isSelectionPopoverOpen, toggleSelectionPopover] = useToggle(false);

// Track when "select all" is active to prevent the table's internal selection
// mechanism from limiting selection to only the visible page items.
// Using a ref to avoid stale closure issues in the selection change callback.
const isSelectAllActiveRef = useRef(false);

const {
searchConfig,
searchQuery,
results: filteredTools,
} = useMcpToolsSearch({ tools, isDisabled });

// Reset page index when filtered results change
useEffect(() => {
setTablePageIndex(0);
}, [filteredTools]);

const selectedMcpTools = useMemo(() => {
const selectedNames = new Set(selectedTools.map((tool) => tool.name));
return tools.filter((tool) => selectedNames.has(tool.name));
Expand Down Expand Up @@ -127,13 +113,6 @@ export const McpToolsSelectionTable: React.FC<McpToolsSelectionTableProps> = ({
[searchQuery]
);

const paginationStart = Math.min(tablePageIndex * tablePageSize + 1, tools.length);
const paginationEnd = Math.min((tablePageIndex + 1) * tablePageSize, tools.length);

const handleSearchChange = useCallback(({ queryText }: EuiSearchBarOnChangeArgs) => {
setSearchQuery(queryText);
}, []);

const handleSelectionChange = useCallback(
(newSelection: McpTool[]) => {
// When "select all" is active, the table fires onSelectionChange twice:
Expand All @@ -149,19 +128,13 @@ export const McpToolsSelectionTable: React.FC<McpToolsSelectionTableProps> = ({
);

const handleClearSelection = useCallback(() => {
isSelectAllActiveRef.current = false;
onChange([]);
}, [onChange]);

const handleSelectAll = useCallback(() => {
isSelectAllActiveRef.current = true;
onChange([...tools]);
toggleSelectionPopover(false);
}, [onChange, tools, toggleSelectionPopover]);

const closeSelectionPopover = useCallback(() => {
toggleSelectionPopover(false);
}, [toggleSelectionPopover]);
}, [onChange, tools]);

const selection: EuiInMemoryTableProps<McpTool>['selection'] = useMemo(
() => ({
Expand All @@ -173,124 +146,42 @@ export const McpToolsSelectionTable: React.FC<McpToolsSelectionTableProps> = ({
[isDisabled, handleSelectionChange, selectedMcpTools]
);

const search: EuiInMemoryTableProps<McpTool>['search'] = useMemo(
() => ({
onChange: handleSearchChange,
box: {
incremental: true,
placeholder: labels.tools.bulkImportMcp.sourceSection.searchPlaceholder,
disabled: isDisabled || tools.length === 0,
'data-test-subj': 'bulkImportMcpToolsSearchInput',
},
}),
[handleSearchChange, isDisabled, tools.length]
);

const emptyMessage = useMemo(() => {
if (isLoading) {
return labels.tools.bulkImportMcp.sourceSection.loadingToolsMessage;
}
if (isDisabled) {
return disabledMessage ?? null;
}
if (searchQuery && tools.length > 0) {
if (searchQuery && tools.length > 0 && filteredTools.length === 0) {
return labels.tools.bulkImportMcp.sourceSection.noMatchingToolsMessage;
}
if (tools.length === 0) {
return labels.tools.bulkImportMcp.sourceSection.noToolsMessage;
}
return undefined;
}, [isDisabled, isLoading, searchQuery, tools.length, disabledMessage]);
}, [isDisabled, isLoading, searchQuery, tools.length, filteredTools.length, disabledMessage]);

const tableHeader = (
<EuiSkeletonLoading
<McpToolsSelectionTableHeader
isLoading={isLoading}
css={tableHeaderContainerStyles}
loadingContent={<EuiSkeletonText css={tableHeaderSkeletonStyles} lines={1} size="xs" />}
loadedContent={
tools.length > 0 ? (
<EuiFlexGroup gutterSize="s" alignItems="center" css={tableHeaderStyles}>
<EuiFlexItem grow={false}>
<EuiText size="xs">
<FormattedMessage
id="xpack.onechat.tools.bulkImportMcp.sourceSection.tableSummary"
defaultMessage="Showing {start}-{end} of {total}"
values={{
start: <strong>{paginationStart}</strong>,
end: <strong>{paginationEnd}</strong>,
total: tools.length,
}}
/>
</EuiText>
</EuiFlexItem>
{selectedMcpTools.length > 0 && (
<EuiFlexGroup gutterSize="none" alignItems="center">
<EuiFlexItem grow={false}>
<EuiPopover
button={
<EuiButtonEmpty
iconType="arrowDown"
iconSide="right"
iconSize="s"
size="xs"
onClick={toggleSelectionPopover}
data-test-subj="bulkImportMcpToolsSelectionPopoverButton"
css={tableHeaderButtonStyles}
>
{labels.tools.bulkImportMcp.sourceSection.selectedCount(
selectedMcpTools.length
)}
</EuiButtonEmpty>
}
isOpen={isSelectionPopoverOpen}
closePopover={closeSelectionPopover}
panelPaddingSize="none"
anchorPosition="downLeft"
>
<EuiContextMenuPanel
size="s"
items={[
<EuiContextMenuItem
key="selectAll"
icon="pagesSelect"
onClick={handleSelectAll}
data-test-subj="bulkImportMcpToolsSelectAllButton"
>
{labels.tools.selectAllToolsButtonLabel}
</EuiContextMenuItem>,
]}
/>
</EuiPopover>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
iconType="cross"
iconSize="s"
size="xs"
color="danger"
onClick={handleClearSelection}
data-test-subj="bulkImportMcpToolsClearSelectionButton"
css={tableHeaderButtonStyles}
>
{labels.tools.bulkImportMcp.sourceSection.clearSelection}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
)}
</EuiFlexGroup>
) : null
}
pageIndex={tablePageIndex}
pageSize={tablePageSize}
totalCount={filteredTools.length}
selectedCount={selectedMcpTools.length}
onSelectAll={handleSelectAll}
onClearSelection={handleClearSelection}
/>
);

return (
<EuiPanel hasBorder paddingSize="m" css={tableContainerStyles(isDisabled)}>
<EuiInMemoryTable
items={tools as McpTool[]}
items={filteredTools}
columns={columns}
itemId="name"
selection={selection}
search={search}
search={searchConfig}
onTableChange={({ page }: CriteriaWithPagination<McpTool>) => {
if (page) {
setTablePageIndex(page.index);
Expand Down
Loading
Loading