diff --git a/.changeset/honest-numbers-wear.md b/.changeset/honest-numbers-wear.md new file mode 100644 index 000000000..4c8dd37bd --- /dev/null +++ b/.changeset/honest-numbers-wear.md @@ -0,0 +1,5 @@ +--- +'@orchestrator-ui/orchestrator-ui-components': minor +--- + +Added tool calling chain component to agent and made it possible to export to CSV diff --git a/packages/orchestrator-ui-components/src/components/WfoAgent/ExportButton/ExportButton.tsx b/packages/orchestrator-ui-components/src/components/WfoAgent/ExportButton/ExportButton.tsx new file mode 100644 index 000000000..d0dade2f0 --- /dev/null +++ b/packages/orchestrator-ui-components/src/components/WfoAgent/ExportButton/ExportButton.tsx @@ -0,0 +1,96 @@ +import React from 'react'; + +import { useTranslations } from 'next-intl'; + +import { EuiIcon, EuiLoadingSpinner } from '@elastic/eui'; + +import { useShowToastMessage, useWithOrchestratorTheme } from '@/hooks'; +import { useLazyGetAgentExportQuery } from '@/rtk/endpoints/agentExport'; +import { GraphQLPageInfo } from '@/types'; +import { getCsvFileNameWithDate } from '@/utils'; +import { csvDownloadHandler } from '@/utils/csvDownload'; + +import { getExportButtonStyles } from './styles'; + +export type ExportData = { + action: string; + download_url: string; + message: string; +}; + +export type ExportButtonProps = { + exportData: ExportData; +}; + +type ExportApiResponse = { + page: object[]; + pageInfo?: GraphQLPageInfo; +}; + +export function ExportButton({ exportData }: ExportButtonProps) { + const { showToastMessage } = useShowToastMessage(); + const tError = useTranslations('errors'); + const [triggerExport, { isFetching }] = useLazyGetAgentExportQuery(); + + const { + containerStyle, + buttonWrapperStyle, + titleStyle, + fileRowStyle, + fileInfoStyle, + filenameStyle, + downloadButtonStyle, + } = useWithOrchestratorTheme(getExportButtonStyles); + + const filename = getCsvFileNameWithDate('export'); + + const onDownloadClick = async () => { + const data = await triggerExport(exportData.download_url).unwrap(); + + const keyOrder = data.page.length > 0 ? Object.keys(data.page[0]) : []; + + const handleExport = csvDownloadHandler( + async () => data, + (data: ExportApiResponse) => data.page, + (data: ExportApiResponse) => + data.pageInfo ?? { + totalItems: data.page.length, + startCursor: 0, + endCursor: data.page.length - 1, + hasNextPage: false, + hasPreviousPage: false, + sortFields: [], + filterFields: [], + }, + keyOrder, + filename, + showToastMessage, + tError, + ); + + await handleExport(); + }; + + return ( +
+
+ {exportData.message && ( +
{exportData.message}
+ )} +
+
+ + {filename} +
+
+ {isFetching ? ( + + ) : ( + + )} +
+
+
+
+ ); +} diff --git a/packages/orchestrator-ui-components/src/components/WfoAgent/ExportButton/index.ts b/packages/orchestrator-ui-components/src/components/WfoAgent/ExportButton/index.ts new file mode 100644 index 000000000..6b75481ab --- /dev/null +++ b/packages/orchestrator-ui-components/src/components/WfoAgent/ExportButton/index.ts @@ -0,0 +1 @@ +export * from './ExportButton'; diff --git a/packages/orchestrator-ui-components/src/components/WfoAgent/ExportButton/styles.ts b/packages/orchestrator-ui-components/src/components/WfoAgent/ExportButton/styles.ts new file mode 100644 index 000000000..30ddd0598 --- /dev/null +++ b/packages/orchestrator-ui-components/src/components/WfoAgent/ExportButton/styles.ts @@ -0,0 +1,69 @@ +import { css } from '@emotion/react'; + +import { WfoTheme } from '@/hooks'; + +export const getExportButtonStyles = ({ theme }: WfoTheme) => { + const containerStyle = css({ + marginTop: theme.size.xl, + marginBottom: theme.size.xl, + width: '100%', + }); + + const buttonWrapperStyle = css({ + backgroundColor: theme.colors.lightestShade, + padding: `${theme.size.xl} ${theme.size.xl}`, + border: `${theme.border.width.thin} solid transparent`, + display: 'flex', + flexDirection: 'column', + gap: theme.size.l, + }); + + const titleStyle = css({ + fontSize: theme.size.m, + fontWeight: theme.font.weight.semiBold, + color: theme.colors.darkestShade, + }); + + const fileRowStyle = css({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + gap: theme.size.m, + border: `${theme.border.width.thin} solid ${theme.colors.lightShade}`, + borderRadius: theme.border.radius.medium, + padding: `${theme.size.m} ${theme.size.l}`, + backgroundColor: theme.colors.emptyShade, + cursor: 'pointer', + }); + + const fileInfoStyle = css({ + display: 'flex', + alignItems: 'center', + gap: theme.size.m, + flex: 1, + color: theme.colors.darkestShade, + }); + + const filenameStyle = css({ + fontSize: theme.size.m, + fontWeight: theme.font.weight.medium, + color: theme.colors.darkestShade, + }); + + const downloadButtonStyle = css({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + color: theme.colors.darkestShade, + }); + + return { + containerStyle, + buttonWrapperStyle, + titleStyle, + fileRowStyle, + fileInfoStyle, + filenameStyle, + downloadButtonStyle, + }; +}; diff --git a/packages/orchestrator-ui-components/src/components/WfoAgent/FilterDisplay/index.ts b/packages/orchestrator-ui-components/src/components/WfoAgent/FilterDisplay/index.ts deleted file mode 100644 index bacbb5ef8..000000000 --- a/packages/orchestrator-ui-components/src/components/WfoAgent/FilterDisplay/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './FilterDisplay'; diff --git a/packages/orchestrator-ui-components/src/components/WfoAgent/ToolProgress/DiscoverFilterPathsDisplay.tsx b/packages/orchestrator-ui-components/src/components/WfoAgent/ToolProgress/DiscoverFilterPathsDisplay.tsx new file mode 100644 index 000000000..c950a3d03 --- /dev/null +++ b/packages/orchestrator-ui-components/src/components/WfoAgent/ToolProgress/DiscoverFilterPathsDisplay.tsx @@ -0,0 +1,114 @@ +import React from 'react'; + +import { EuiSpacer, EuiText } from '@elastic/eui'; + +import { WfoBadge } from '@/components/WfoBadges'; +import { WfoPathBreadcrumb } from '@/components/WfoSearchPage/WfoSearchResults/WfoPathBreadcrumb'; + +interface DiscoverFilterPathsResult { + status?: string; + leaves?: Array<{ + paths?: string[]; + name?: string; + }>; +} + +type DiscoverFilterPathsDisplayProps = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + result?: any; + parameters: { + field_names?: string[]; + entity_type?: string; + }; +}; + +export const DiscoverFilterPathsDisplay = ({ + parameters, + result, +}: DiscoverFilterPathsDisplayProps) => { + const { field_names = [] } = parameters; + + const foundFields: [string, DiscoverFilterPathsResult][] = result + ? Object.entries( + result as Record, + ).filter(([, fieldResult]) => fieldResult.status !== 'NOT_FOUND') + : []; + + // Count total paths across all found fields + const totalPaths = foundFields.reduce((count, [, fieldResult]) => { + const pathCount = + fieldResult.leaves?.reduce((leafCount: number, leaf) => { + return leafCount + (leaf.paths?.length || 1); + }, 0) || 0; + return count + pathCount; + }, 0); + + return ( +
+ {field_names.length > 0 && ( + <> + + Looking for{' '} + {field_names.map((name, idx) => ( + + {idx > 0 && ', '} + + {name} + + + ))} + + + + )} + + {result && totalPaths > 0 && ( +
+ + + Found {totalPaths} path + {totalPaths > 1 ? 's' : ''}: + + + + {foundFields.map(([fieldName, fieldResult]) => ( +
+ {fieldResult.leaves && + fieldResult.leaves.length > 0 && + fieldResult.leaves.map( + (leaf, leafIdx: number) => { + const paths = + leaf.paths || + (leaf.name ? [leaf.name] : []); + return ( + + {paths.map( + ( + path: string, + pathIdx: number, + ) => ( +
+ +
+ ), + )} +
+ ); + }, + )} +
+ ))} +
+ )} +
+ ); +}; diff --git a/packages/orchestrator-ui-components/src/components/WfoAgent/ToolProgress/RunSearchDisplay.tsx b/packages/orchestrator-ui-components/src/components/WfoAgent/ToolProgress/RunSearchDisplay.tsx new file mode 100644 index 000000000..864ab43b3 --- /dev/null +++ b/packages/orchestrator-ui-components/src/components/WfoAgent/ToolProgress/RunSearchDisplay.tsx @@ -0,0 +1,34 @@ +import React from 'react'; + +import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; + +import { WfoBadge } from '@/components/WfoBadges'; + +type RunSearchDisplayProps = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + result?: any; + parameters: { + limit?: number; + }; +}; + +export const RunSearchDisplay = ({ parameters }: RunSearchDisplayProps) => { + const { limit = 10 } = parameters; + + return ( +
+ + + + Results Limit + + + + + {limit} + + + +
+ ); +}; diff --git a/packages/orchestrator-ui-components/src/components/WfoAgent/FilterDisplay/styles.ts b/packages/orchestrator-ui-components/src/components/WfoAgent/ToolProgress/SetFilterTreeDisplay.styles.ts similarity index 100% rename from packages/orchestrator-ui-components/src/components/WfoAgent/FilterDisplay/styles.ts rename to packages/orchestrator-ui-components/src/components/WfoAgent/ToolProgress/SetFilterTreeDisplay.styles.ts diff --git a/packages/orchestrator-ui-components/src/components/WfoAgent/FilterDisplay/FilterDisplay.tsx b/packages/orchestrator-ui-components/src/components/WfoAgent/ToolProgress/SetFilterTreeDisplay.tsx similarity index 62% rename from packages/orchestrator-ui-components/src/components/WfoAgent/FilterDisplay/FilterDisplay.tsx rename to packages/orchestrator-ui-components/src/components/WfoAgent/ToolProgress/SetFilterTreeDisplay.tsx index aa1c84e6e..5dce48370 100644 --- a/packages/orchestrator-ui-components/src/components/WfoAgent/FilterDisplay/FilterDisplay.tsx +++ b/packages/orchestrator-ui-components/src/components/WfoAgent/ToolProgress/SetFilterTreeDisplay.tsx @@ -2,13 +2,7 @@ import React from 'react'; import { useTranslations } from 'next-intl'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiPanel, - EuiSpacer, - EuiText, -} from '@elastic/eui'; +import { EuiText } from '@elastic/eui'; import { WfoBadge } from '@/components/WfoBadges'; import { WfoPathBreadcrumb } from '@/components/WfoSearchPage/WfoSearchResults/WfoPathBreadcrumb'; @@ -16,22 +10,13 @@ import { getOperatorDisplay, isCondition, } from '@/components/WfoSearchPage/utils'; -import { useWithOrchestratorTheme } from '@/index'; -import { AnySearchParameters, Condition, Group, PathDataType } from '@/types'; +import { useWithOrchestratorTheme } from '@/hooks'; +import { Condition, Group, PathDataType } from '@/types'; -import { getFilterDisplayStyles } from './styles'; +import { getFilterDisplayStyles } from './SetFilterTreeDisplay.styles'; const DEPTH_INDENT = 16; -type FilterDisplayProps = { - parameters: { - action?: AnySearchParameters['action'] | string; - entity_type?: AnySearchParameters['entity_type'] | string; - filters?: Group | null; - query?: string | null; - }; -}; - interface BetweenValue { start?: string | number; end?: string | number; @@ -39,9 +24,17 @@ interface BetweenValue { to?: string | number; } -export function FilterDisplay({ parameters }: FilterDisplayProps) { - const t = useTranslations('agent.page'); +type SetFilterTreeDisplayProps = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + result?: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + parameters: any; +}; +export const SetFilterTreeDisplay = ({ + parameters, +}: SetFilterTreeDisplayProps) => { + const t = useTranslations('agent.page'); const { wrapStyle, columnGroupWrapStyle, @@ -50,15 +43,9 @@ export function FilterDisplay({ parameters }: FilterDisplayProps) { operatorStyle, valueStyle, } = useWithOrchestratorTheme(getFilterDisplayStyles); - const { action, entity_type, filters, query } = parameters ?? {}; - if (!parameters || Object.keys(parameters).length === 0) return null; - - const sectionTitle = (text: string) => ( - - {text} - - ); + // Parameters might be the Group directly, or wrapped in a filters property + const filters = (parameters?.filters || parameters) as Group; const formatFilterValue = (condition: Condition['condition']): string => { if ('value' in condition && condition.value !== undefined) { @@ -136,47 +123,13 @@ export function FilterDisplay({ parameters }: FilterDisplayProps) { ); }; - return ( - - - - {sectionTitle(t('action'))} - - - {action || 'N/A'} - - - - - {sectionTitle(t('entityType'))} - - - {entity_type || 'N/A'} - - - - {query ? ( - - {sectionTitle(t('searchQuery'))} - - - "{query}" - - - ) : null} - - - - {sectionTitle(t('activeFilters'))} - + if (!filters || !filters.children || filters.children.length === 0) { + return ( + + {t('noFiltersApplied')} + + ); + } - {filters && filters.children && filters.children.length > 0 ? ( - renderFilterGroup(filters) - ) : ( - - {t('noFiltersApplied')} - - )} - - ); -} + return
{renderFilterGroup(filters)}
; +}; diff --git a/packages/orchestrator-ui-components/src/components/WfoAgent/ToolProgress/StartNewSearchDisplay.tsx b/packages/orchestrator-ui-components/src/components/WfoAgent/ToolProgress/StartNewSearchDisplay.tsx new file mode 100644 index 000000000..4de06be90 --- /dev/null +++ b/packages/orchestrator-ui-components/src/components/WfoAgent/ToolProgress/StartNewSearchDisplay.tsx @@ -0,0 +1,62 @@ +import React from 'react'; + +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui'; + +import { WfoBadge } from '@/components/WfoBadges'; + +type StartNewSearchDisplayProps = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + result?: any; + parameters: { + entity_type?: string; + action?: string; + query?: string; + }; +}; + +export const StartNewSearchDisplay = ({ + parameters, +}: StartNewSearchDisplayProps) => { + const { entity_type, action, query } = parameters; + + return ( +
+ + {entity_type && ( + + + Entity Type + + + + {entity_type} + + + )} + {action && ( + + + Action + + + + {action} + + + )} + + {query && ( + <> + + + Query + + + + "{query}" + + + )} +
+ ); +}; diff --git a/packages/orchestrator-ui-components/src/components/WfoAgent/ToolProgress/ToolProgress.tsx b/packages/orchestrator-ui-components/src/components/WfoAgent/ToolProgress/ToolProgress.tsx new file mode 100644 index 000000000..79dbf6467 --- /dev/null +++ b/packages/orchestrator-ui-components/src/components/WfoAgent/ToolProgress/ToolProgress.tsx @@ -0,0 +1,138 @@ +import React, { useState } from 'react'; + +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; + +import { useWithOrchestratorTheme } from '@/hooks'; +import { useOrchestratorTheme } from '@/hooks/useOrchestratorTheme'; +import { + WfoCheckmarkCircleFill, + WfoChevronDown, + WfoChevronUp, + WfoXCircleFill, +} from '@/icons'; +import { WfoWrench } from '@/icons/heroicons'; + +import { DiscoverFilterPathsDisplay } from './DiscoverFilterPathsDisplay'; +import { RunSearchDisplay } from './RunSearchDisplay'; +import { SetFilterTreeDisplay } from './SetFilterTreeDisplay'; +import { StartNewSearchDisplay } from './StartNewSearchDisplay'; +import { getToolProgressStyles } from './styles'; + +type ToolProgressProps = { + name: string; + status: 'executing' | 'inProgress' | 'complete' | 'failed'; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + args?: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + result?: any; +}; + +// Mapping of tool names to their display components +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const TOOL_DISPLAY_COMPONENTS: Record> = { + set_filter_tree: SetFilterTreeDisplay, + start_new_search: StartNewSearchDisplay, + run_search: RunSearchDisplay, + discover_filter_paths: DiscoverFilterPathsDisplay, +}; + +export const ToolProgress = ({ + name, + status, + args, + result, +}: ToolProgressProps) => { + const [isExpanded, setIsExpanded] = useState(false); + + const { + containerStyle, + containerClickableStyle, + headerStyle, + nameStyle, + expandedContentStyle, + iconSize, + iconStyle, + } = useWithOrchestratorTheme(getToolProgressStyles); + + const { theme } = useOrchestratorTheme(); + + const renderStatus = () => { + if (status === 'complete') { + return ( + + ); + } + if (status === 'inProgress' || status === 'executing') { + return ; + } + if (status === 'failed') { + return ( + + ); + } + return null; + }; + + const DisplayComponent = TOOL_DISPLAY_COMPONENTS[name]; + const hasArgs = DisplayComponent && args; + + return ( +
+
hasArgs && setIsExpanded(!isExpanded)} + > + + +
+ +
+
+ + {name} + + + {renderStatus()} + + + {hasArgs && + (isExpanded ? ( + + ) : ( + + ))} + +
+
+ {hasArgs && isExpanded && ( +
+ +
+ )} +
+ ); +}; diff --git a/packages/orchestrator-ui-components/src/components/WfoAgent/ToolProgress/index.ts b/packages/orchestrator-ui-components/src/components/WfoAgent/ToolProgress/index.ts new file mode 100644 index 000000000..71c6523ab --- /dev/null +++ b/packages/orchestrator-ui-components/src/components/WfoAgent/ToolProgress/index.ts @@ -0,0 +1 @@ +export { ToolProgress } from './ToolProgress'; diff --git a/packages/orchestrator-ui-components/src/components/WfoAgent/ToolProgress/styles.ts b/packages/orchestrator-ui-components/src/components/WfoAgent/ToolProgress/styles.ts new file mode 100644 index 000000000..d4d06deff --- /dev/null +++ b/packages/orchestrator-ui-components/src/components/WfoAgent/ToolProgress/styles.ts @@ -0,0 +1,50 @@ +import { css } from '@emotion/react'; + +import { WfoTheme } from '@/hooks'; + +export const getToolProgressStyles = ({ theme }: WfoTheme) => { + const containerStyle = css({ + border: `${theme.border.width.thin} solid ${theme.colors.lightShade}`, + borderRadius: theme.border.radius.medium, + backgroundColor: theme.colors.emptyShade, + transition: `all ${theme.animation.normal} ease`, + }); + + const containerClickableStyle = css({ + cursor: 'pointer', + '&:hover': { + borderColor: theme.colors.primary, + backgroundColor: theme.colors.lightestShade, + }, + }); + + const headerStyle = css({ + padding: `${theme.size.base} ${theme.size.l}`, + }); + + const nameStyle = css({ + fontSize: theme.size.m, + fontWeight: theme.font.weight.medium, + }); + + const expandedContentStyle = css({ + borderTop: `${theme.border.width.thin} solid ${theme.colors.lightShade}`, + padding: `${theme.size.base} ${theme.size.l}`, + }); + + const iconSize = 18; + + const iconStyle = css({ + color: theme.colors.subduedText, + }); + + return { + containerStyle, + containerClickableStyle, + headerStyle, + nameStyle, + expandedContentStyle, + iconSize, + iconStyle, + }; +}; diff --git a/packages/orchestrator-ui-components/src/components/WfoAgent/WfoAgent/WfoAgent.tsx b/packages/orchestrator-ui-components/src/components/WfoAgent/WfoAgent/WfoAgent.tsx index 831a8e5db..b7966f618 100644 --- a/packages/orchestrator-ui-components/src/components/WfoAgent/WfoAgent/WfoAgent.tsx +++ b/packages/orchestrator-ui-components/src/components/WfoAgent/WfoAgent/WfoAgent.tsx @@ -2,28 +2,44 @@ import React from 'react'; import { useTranslations } from 'next-intl'; -import { useCoAgent } from '@copilotkit/react-core'; +import { + CatchAllActionRenderProps, + useCoAgent, + useCoAgentStateRender, + useCopilotAction, +} from '@copilotkit/react-core'; import { CopilotSidebar } from '@copilotkit/react-ui'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui'; import { WfoSearchResults } from '@/components/WfoSearchPage/WfoSearchResults'; -import { AnySearchParameters, AnySearchResult, PathFilter } from '@/types'; - -import { FilterDisplay } from '../FilterDisplay'; +import { AnySearchParameters, SearchResult } from '@/types'; + +import { ExportButton, ExportData } from '../ExportButton'; +import { ToolProgress } from '../ToolProgress'; + +type SearchResultsData = { + action: string; + query_id: string; + results_url: string; + total_count: number; + message: string; + results: SearchResult[]; +}; type SearchState = { - parameters: AnySearchParameters; - results: AnySearchResult[]; + run_id: string | null; + query_id: string | null; + parameters: AnySearchParameters | null; + results_data: SearchResultsData | null; + export_data: ExportData | null; }; const initialState: SearchState = { - parameters: { - action: 'select', - entity_type: 'SUBSCRIPTION', - filters: [] as PathFilter[], - query: null, - }, - results: [], + run_id: null, + query_id: null, + parameters: null, + results_data: null, + export_data: null, }; export function WfoAgent() { @@ -34,23 +50,38 @@ export function WfoAgent() { name: 'query_agent', initialState, }); - const { parameters, results } = state; - - const hasStarted = !!( - state.parameters && - Array.isArray(state.parameters.filters) && - state.parameters.filters.length > 0 - ); - - const isLoadingResults = - hasStarted && (!state.results || state.results.length === 0); + const { results_data } = state; + + // Automatically render all tool calls + useCopilotAction({ + name: '*', + render: ({ + name, + status, + args, + result, + }: CatchAllActionRenderProps<[]>) => { + return ( + + ); + }, + }); - const displayParameters = parameters && { - ...parameters, - filters: Array.isArray(parameters.filters) - ? { op: 'AND' as const, children: parameters.filters } - : parameters.filters, - }; + // Render export button from state + useCoAgentStateRender({ + name: 'query_agent', + render: ({ state }) => { + if (!state?.export_data || state.export_data.action !== 'export') { + return null; + } + return ; + }, + }); return ( @@ -60,29 +91,25 @@ export function WfoAgent() { - -

{tPage('filledParameters')}

-
- - {displayParameters && ( - - )} - - - -

- {tPage('results')}{' '} - {results ? `(${results.length})` : ''} -

-
- - {}} - /> + {results_data && results_data.action === 'view_results' && ( + <> + {results_data.message && ( + <> + +

{results_data.message}

+
+ + + )} + {}} + /> + + )} diff --git a/packages/orchestrator-ui-components/src/components/WfoAgent/index.ts b/packages/orchestrator-ui-components/src/components/WfoAgent/index.ts index 1fc506036..bcd383082 100644 --- a/packages/orchestrator-ui-components/src/components/WfoAgent/index.ts +++ b/packages/orchestrator-ui-components/src/components/WfoAgent/index.ts @@ -1,2 +1 @@ export * from './WfoAgent'; -export * from './FilterDisplay'; diff --git a/packages/orchestrator-ui-components/src/components/WfoSearchPage/WfoSearch/WfoSearch.tsx b/packages/orchestrator-ui-components/src/components/WfoSearchPage/WfoSearch/WfoSearch.tsx index 6b3485c5f..1267ccd37 100644 --- a/packages/orchestrator-ui-components/src/components/WfoSearchPage/WfoSearch/WfoSearch.tsx +++ b/packages/orchestrator-ui-components/src/components/WfoSearchPage/WfoSearch/WfoSearch.tsx @@ -19,8 +19,6 @@ import { WfoSubscription } from '@/components'; import { WfoBadge } from '@/components/WfoBadges'; import { ENTITY_TABS, - findResultIndexById, - getRecordId, isSubscriptionSearchResult, } from '@/components/WfoSearchPage/utils'; import { TreeProvider } from '@/contexts'; @@ -172,9 +170,8 @@ export const WfoSearch = () => { useEffect(() => { if (results.data.length > 0) { if (selectedRecordId) { - const foundIndex = findResultIndexById( - results.data, - selectedRecordId, + const foundIndex = results.data.findIndex( + (result) => result.entity_id === selectedRecordId, ); if (foundIndex !== -1) { @@ -368,9 +365,9 @@ export const WfoSearch = () => { setSelectedRecordIndex(index); const record = results.data[index]; if (record) { - const recordId = - getRecordId(record); - setSelectedRecordId(recordId); + setSelectedRecordId( + record.entity_id, + ); } }} /> @@ -395,8 +392,7 @@ export const WfoSearch = () => { subscriptionId={ results.data[ selectedRecordIndex - ].subscription - .subscription_id + ].entity_id } /> diff --git a/packages/orchestrator-ui-components/src/components/WfoSearchPage/WfoSearchResults/WfoSearchResultItem.tsx b/packages/orchestrator-ui-components/src/components/WfoSearchPage/WfoSearchResults/WfoSearchResultItem.tsx index 59548ae05..10595da2b 100644 --- a/packages/orchestrator-ui-components/src/components/WfoSearchPage/WfoSearchResults/WfoSearchResultItem.tsx +++ b/packages/orchestrator-ui-components/src/components/WfoSearchPage/WfoSearchResults/WfoSearchResultItem.tsx @@ -13,14 +13,14 @@ import { import { WfoBadge } from '@/components/WfoBadges'; import { useOrchestratorTheme } from '@/hooks'; -import { AnySearchResult } from '@/types'; +import { SearchResult } from '@/types'; -import { getDescription, getDetailUrl } from '../utils'; +import { getDetailUrl } from '../utils'; import { WfoHighlightedText } from './WfoHighlightedText'; import { WfoPathBreadcrumb } from './WfoPathBreadcrumb'; interface WfoSearchResultItemProps { - result: AnySearchResult; + result: SearchResult; index: number; isSelected?: boolean; onSelect?: () => void; @@ -79,7 +79,7 @@ export const WfoSearchResultItem: FC = ({ fontWeight: theme.font.weight.semiBold, }} > - {getDescription(result)} + {result.entity_title} {matchingField && ( diff --git a/packages/orchestrator-ui-components/src/components/WfoSearchPage/WfoSearchResults/WfoSearchResults.tsx b/packages/orchestrator-ui-components/src/components/WfoSearchPage/WfoSearchResults/WfoSearchResults.tsx index eea49ed37..a456a63c5 100644 --- a/packages/orchestrator-ui-components/src/components/WfoSearchPage/WfoSearchResults/WfoSearchResults.tsx +++ b/packages/orchestrator-ui-components/src/components/WfoSearchPage/WfoSearchResults/WfoSearchResults.tsx @@ -1,16 +1,15 @@ -import React, { useState } from 'react'; +import React from 'react'; import { EuiFlexGroup, EuiPanel } from '@elastic/eui'; -import { AnySearchResult } from '@/types'; +import { SearchResult } from '@/types'; import { WfoSearchEmptyState } from './WfoSearchEmptyState'; import { WfoSearchLoadingState } from './WfoSearchLoadingState'; import { WfoSearchResultItem } from './WfoSearchResultItem'; -import { WfoSubscriptionDetailModal } from './WfoSubscriptionDetailModal'; interface WfoSearchResultsProps { - results: AnySearchResult[]; + results: SearchResult[]; loading: boolean; selectedRecordIndex?: number; onRecordSelect?: (index: number) => void; @@ -22,15 +21,6 @@ export const WfoSearchResults = ({ selectedRecordIndex = 0, onRecordSelect, }: WfoSearchResultsProps) => { - const [modalData, setModalData] = useState<{ - subscription: unknown; - matchingField?: unknown; - } | null>(null); - - const handleCloseModal = () => { - setModalData(null); - }; - if (loading) { return ; } @@ -40,26 +30,20 @@ export const WfoSearchResults = ({ } return ( - <> - - - {results.map((result, idx) => ( - onRecordSelect?.(idx)} - /> - ))} - - - - + + + {results.map((result, idx) => ( + { + onRecordSelect?.(idx); + }} + /> + ))} + + ); }; diff --git a/packages/orchestrator-ui-components/src/components/WfoSearchPage/WfoSearchResults/WfoSubscriptionDetailModal.tsx b/packages/orchestrator-ui-components/src/components/WfoSearchPage/WfoSearchResults/WfoSubscriptionDetailModal.tsx deleted file mode 100644 index dc1185869..000000000 --- a/packages/orchestrator-ui-components/src/components/WfoSearchPage/WfoSearchResults/WfoSubscriptionDetailModal.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React, { FC } from 'react'; - -import { useTranslations } from 'next-intl'; - -import { - EuiButton, - EuiModal, - EuiModalBody, - EuiModalFooter, - EuiModalHeader, - EuiModalHeaderTitle, -} from '@elastic/eui'; - -import { WfoSubscription } from '@/components'; -import { TreeProvider } from '@/contexts'; - -interface WfoSubscriptionDetailModalProps { - isVisible: boolean; - onClose: () => void; - subscriptionData: unknown | null; - matchingField?: unknown; -} - -export const WfoSubscriptionDetailModal: FC< - WfoSubscriptionDetailModalProps -> = ({ isVisible, onClose, subscriptionData }) => { - const t = useTranslations('search.page'); - if (!isVisible || !subscriptionData) return null; - - const subscriptionId = - subscriptionData && - (subscriptionData as { subscription_id: string }).subscription_id; - - return ( - - - - {t('subscriptionDetails')} - - - - - - - - - - - - {t('closeButton')} - - - - ); -}; diff --git a/packages/orchestrator-ui-components/src/components/WfoSearchPage/WfoSearchResults/index.ts b/packages/orchestrator-ui-components/src/components/WfoSearchPage/WfoSearchResults/index.ts index c0977a583..5405fff3a 100644 --- a/packages/orchestrator-ui-components/src/components/WfoSearchPage/WfoSearchResults/index.ts +++ b/packages/orchestrator-ui-components/src/components/WfoSearchPage/WfoSearchResults/index.ts @@ -7,4 +7,3 @@ export * from './WfoSearchMetadataHeader'; export * from './WfoSearchPaginationInfo'; export * from './WfoHighlightedText'; export * from './WfoPathBreadcrumb'; -export * from './WfoSubscriptionDetailModal'; diff --git a/packages/orchestrator-ui-components/src/components/WfoSearchPage/utils.ts b/packages/orchestrator-ui-components/src/components/WfoSearchPage/utils.ts index 3509d3754..616333b39 100644 --- a/packages/orchestrator-ui-components/src/components/WfoSearchPage/utils.ts +++ b/packages/orchestrator-ui-components/src/components/WfoSearchPage/utils.ts @@ -1,36 +1,19 @@ -import { - AnySearchResult, - Condition, - EntityKind, - Group, - ProcessSearchResult, - ProductSearchResult, - SubscriptionSearchResult, - WorkflowSearchResult, -} from '@/types'; +import { Condition, EntityKind, Group, SearchResult } from '@/types'; -export function isSubscriptionSearchResult( - item: AnySearchResult, -): item is SubscriptionSearchResult { - return 'subscription' in item && typeof item.subscription === 'object'; +export function isSubscriptionSearchResult(item: SearchResult): boolean { + return item.entity_type === 'SUBSCRIPTION'; } -export function isProcessSearchResult( - item: AnySearchResult, -): item is ProcessSearchResult { - return 'process' in item && typeof item.process === 'object'; +export function isProcessSearchResult(item: SearchResult): boolean { + return item.entity_type === 'PROCESS'; } -export function isProductSearchResult( - item: AnySearchResult, -): item is ProductSearchResult { - return 'product' in item && typeof item.product === 'object'; +export function isProductSearchResult(item: SearchResult): boolean { + return item.entity_type === 'PRODUCT'; } -export function isWorkflowSearchResult( - item: AnySearchResult, -): item is WorkflowSearchResult { - return 'workflow' in item && typeof item.workflow === 'object'; +export function isWorkflowSearchResult(item: SearchResult): boolean { + return item.entity_type === 'WORKFLOW'; } export const isCondition = (item: Group | Condition): item is Condition => { @@ -48,92 +31,9 @@ export const getEndpointPath = (entityType: EntityKind): string => { return ENDPOINT_PATHS[entityType] || ENDPOINT_PATHS.SUBSCRIPTION; }; -export const getDisplayText = (item: AnySearchResult): string => { - if (isSubscriptionSearchResult(item)) { - return item.subscription.description || 'Subscription'; - } - if (isProcessSearchResult(item)) { - return item.process.workflowName; - } - if (isProductSearchResult(item)) { - return item.product.name; - } - if (isWorkflowSearchResult(item)) { - return item.workflow.name; - } - return 'Unknown result type'; -}; - -export const getRecordId = (result: AnySearchResult): string => { - if (isSubscriptionSearchResult(result)) { - return result.subscription.subscription_id; - } - if (isProductSearchResult(result)) { - return result.product.product_id; - } - if (isProcessSearchResult(result)) { - return result.process.processId; - } - if (isWorkflowSearchResult(result)) { - return result.workflow.name; - } - return ''; -}; - -export const findResultIndexById = ( - results: AnySearchResult[], - recordId: string, -): number => { - return results.findIndex((result) => { - if (isSubscriptionSearchResult(result)) { - return result.subscription.subscription_id === recordId; - } - if (isProductSearchResult(result)) { - return result.product.product_id === recordId; - } - if (isProcessSearchResult(result)) { - return result.process.processId === recordId; - } - if (isWorkflowSearchResult(result)) { - return result.workflow.name === recordId; - } - return false; - }); -}; - -export const getDetailUrl = ( - result: AnySearchResult, - baseUrl: string, -): string => { - if (isSubscriptionSearchResult(result)) { - return `${baseUrl}/subscriptions/${result.subscription.subscription_id}`; - } - if (isProductSearchResult(result)) { - return `${baseUrl}/products/${result.product.product_id}`; - } - if (isProcessSearchResult(result)) { - return `${baseUrl}/processes/${result.process.processId}`; - } - if (isWorkflowSearchResult(result)) { - return `${baseUrl}/workflows/${result.workflow.name}`; - } - return '#'; -}; - -export const getDescription = (result: AnySearchResult): string => { - if (isSubscriptionSearchResult(result)) { - return result.subscription.description; - } - if (isProductSearchResult(result)) { - return result.product.description || result.product.name; - } - if (isWorkflowSearchResult(result)) { - return result.workflow.description || result.workflow.name; - } - if (isProcessSearchResult(result)) { - return result.process.workflowName; - } - return 'Unknown'; +export const getDetailUrl = (result: SearchResult, baseUrl: string): string => { + const endpointPath = getEndpointPath(result.entity_type); + return `${baseUrl}/${endpointPath}/${result.entity_id}`; }; export const ENTITY_TABS = [ diff --git a/packages/orchestrator-ui-components/src/hooks/useSearchPagination.ts b/packages/orchestrator-ui-components/src/hooks/useSearchPagination.ts index f609900f8..63596b062 100644 --- a/packages/orchestrator-ui-components/src/hooks/useSearchPagination.ts +++ b/packages/orchestrator-ui-components/src/hooks/useSearchPagination.ts @@ -5,15 +5,15 @@ import { Query } from '@elastic/eui'; import { buildSearchParams } from '@/components/WfoSearchPage/utils'; import { useSearchWithPaginationMutation } from '@/rtk/endpoints'; import { - AnySearchResult, EntityKind, Group, PaginatedSearchResults, + SearchResult, } from '@/types'; interface PageHistoryItem { page: number; - results: AnySearchResult[]; + results: SearchResult[]; cursor: number | null; } diff --git a/packages/orchestrator-ui-components/src/messages/en-GB.json b/packages/orchestrator-ui-components/src/messages/en-GB.json index 697b39895..659cc67fd 100644 --- a/packages/orchestrator-ui-components/src/messages/en-GB.json +++ b/packages/orchestrator-ui-components/src/messages/en-GB.json @@ -475,7 +475,6 @@ "title": "Search results", "page": { "filledParameters": "Filled parameters", - "results": "Results", "emptyGroup": "Empty group", "searchQuery": "Search query", "activeFilters": "Active filters", @@ -484,7 +483,7 @@ "action": "Action", "copilot": { "title": "Database assistant", - "initial": "Ask me things such as:\n• *Find active subscriptions for Surf*\n• *Show terminated workflows”*\n\nThe filled template and results will appear on the left." + "initial": "Ask me things such as:\n• *Find active subscriptions*\n• *Show terminated workflows”*\n\nThe filled template and results will appear on the left." } } }, @@ -521,7 +520,6 @@ "resultsOnPage": "{resultCount} result(s) on this page", "searchResultsPagination": "Search results pagination", "viewDetails": "View details", - "closeButton": "Close", "selectOrEnterValue": "Select or type value", "enterValue": "Enter value", "fromNumber": "From", diff --git a/packages/orchestrator-ui-components/src/rtk/endpoints/agentExport.ts b/packages/orchestrator-ui-components/src/rtk/endpoints/agentExport.ts new file mode 100644 index 000000000..c6326b19b --- /dev/null +++ b/packages/orchestrator-ui-components/src/rtk/endpoints/agentExport.ts @@ -0,0 +1,23 @@ +import { BaseQueryTypes, orchestratorApi } from '@/rtk'; +import { GraphQLPageInfo } from '@/types'; + +export type AgentExportResponse = { + page: object[]; + pageInfo?: GraphQLPageInfo; +}; + +const agentExportApi = orchestratorApi.injectEndpoints({ + endpoints: (builder) => ({ + getAgentExport: builder.query({ + query: (downloadUrl) => ({ + url: downloadUrl, + method: 'GET', + }), + extraOptions: { + baseQueryType: BaseQueryTypes.fetch, + }, + }), + }), +}); + +export const { useLazyGetAgentExportQuery } = agentExportApi; diff --git a/packages/orchestrator-ui-components/src/types/search.ts b/packages/orchestrator-ui-components/src/types/search.ts index 01ab9d06c..04b3c395b 100644 --- a/packages/orchestrator-ui-components/src/types/search.ts +++ b/packages/orchestrator-ui-components/src/types/search.ts @@ -1,85 +1,23 @@ export type EntityKind = 'SUBSCRIPTION' | 'PRODUCT' | 'WORKFLOW' | 'PROCESS'; -export interface SubscriptionMatchingField { +export interface MatchingField { text: string; path: string; highlight_indices: [number, number][]; } -export interface SubscriptionSearchResult { +export interface SearchResult { + entity_id: string; + entity_type: EntityKind; + entity_title: string; score: number; perfect_match: number; - matching_field?: SubscriptionMatchingField | null; - subscription: { - subscription_id: string; - description: string; - product: { - name: string; - description: string; - }; - }; + matching_field?: MatchingField | null; } -export interface ProcessSearchResult { - score: number; - perfect_match: number; - matching_field?: SubscriptionMatchingField | null; - process: { - processId: string; - workflowName: string; - workflowId: string; - status: string; - isTask: boolean; - createdBy?: string | null; - startedAt: string; - lastModifiedAt: string; - lastStep?: string | null; - failedReason?: string | null; - subscriptionIds?: string[] | null; - }; -} - -export interface ProductSearchResult { - score: number; - perfect_match: number; - matching_field?: SubscriptionMatchingField | null; - product: { - product_id: string; - name: string; - product_type: string; - tag?: string | null; - description?: string | null; - status?: string | null; - created_at?: string | null; - }; -} - -export interface WorkflowSearchResult { - score: number; - perfect_match: number; - matching_field?: SubscriptionMatchingField | null; - workflow: { - name: string; - products: { - product_type: string; - product_id: string; - name: string; - }[]; - description?: string | null; - created_at?: string | null; - }; -} - -/** Union of all search results */ -export type AnySearchResult = - | SubscriptionSearchResult - | ProcessSearchResult - | ProductSearchResult - | WorkflowSearchResult; - /** Paginated search results */ export type PaginatedSearchResults = { - data: AnySearchResult[]; + data: SearchResult[]; page_info: { has_next_page: boolean; next_page_cursor: number | null; @@ -138,7 +76,7 @@ type ActionType = 'select'; type BaseSearchParameters = { query?: string | null; - filters?: PathFilter[] | null; + filters?: PathFilter[] | Group | null; action: ActionType; }; diff --git a/version-compatibility.json b/version-compatibility.json index 32a061769..94766f651 100644 --- a/version-compatibility.json +++ b/version-compatibility.json @@ -1,4 +1,9 @@ [ + { + "orchestratorUiVersion": "6.5.0", + "minimumOrchestratorCoreVersion": "4.6.0", + "changes": "To support new functionality in the FE for the Agent and LLM, the BE endpoints have changed." + }, { "orchestratorUiVersion": "5.3.5", "minimumOrchestratorCoreVersion": "4.2.0",