Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve search response rendering; other minor fixes #523

Merged
merged 10 commits into from
Dec 11, 2024
21 changes: 21 additions & 0 deletions common/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,27 @@ export type SimulateIngestPipelineResponse = {

export type SearchHit = SimulateIngestPipelineDoc;

export type SearchResponse = {
took: number;
timed_out: boolean;
_shards: {
total: number;
successful: number;
skipped: number;
failed: number;
};
hits: {
total: {
value: number;
relation: string;
};
max_score: number;
hits: SearchHit[];
};
aggregations?: {};
ext?: {};
};

export type IndexResponse = {
indexName: string;
indexDetails: IndexConfiguration;
Expand Down
1 change: 1 addition & 0 deletions public/general_components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export { MultiSelectFilter } from './multi_select_filter';
export { ProcessorsTitle } from './processors_title';
export { ExperimentalBadge } from './experimental_badge';
export { QueryParamsList } from './query_params_list';
export * from './results';
export * from './service_card';
6 changes: 6 additions & 0 deletions public/general_components/results/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

export { Results } from './results';
77 changes: 77 additions & 0 deletions public/general_components/results/results.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React, { useState } from 'react';
import {
EuiPanel,
EuiFlexGroup,
EuiFlexItem,
EuiSmallButtonGroup,
} from '@elastic/eui';
import { SearchResponse } from '../../../common';
import { ResultsTable } from './results_table';
import { ResultsJSON } from './results_json';

interface ResultsProps {
response: SearchResponse;
}

enum VIEW {
HITS_TABLE = 'hits_table',
RAW_JSON = 'raw_json',
}

/**
* Basic component to view OpenSearch response results. Can view hits in a tabular format,
* or the raw JSON response.
*/
export function Results(props: ResultsProps) {
// selected view state
const [selectedView, setSelectedView] = useState<VIEW>(VIEW.HITS_TABLE);

return (
<EuiPanel
hasBorder={false}
hasShadow={false}
paddingSize="none"
style={{ height: '10vh', overflowY: 'scroll', overflowX: 'hidden' }}
>
<EuiFlexGroup
direction="column"
gutterSize="xs"
style={{ height: '100%' }}
>
<EuiFlexItem grow={false}>
<EuiSmallButtonGroup
legend="Choose how to view your data"
options={[
{
id: VIEW.HITS_TABLE,
label: 'Hits',
},
{
id: VIEW.RAW_JSON,
label: 'Raw JSON',
},
]}
idSelected={selectedView}
onChange={(id) => setSelectedView(id as VIEW)}
data-testid="resultsToggleButtonGroup"
/>
</EuiFlexItem>
<EuiFlexItem grow={true}>
<>
{selectedView === VIEW.HITS_TABLE && (
<ResultsTable hits={props.response?.hits?.hits || []} />
)}
{selectedView === VIEW.RAW_JSON && (
<ResultsJSON response={props.response} />
)}
</>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
}
34 changes: 34 additions & 0 deletions public/general_components/results/results_json.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import { EuiCodeEditor } from '@elastic/eui';
import { customStringify, SearchResponse } from '../../../common';

interface ResultsJSONProps {
response: SearchResponse;
}

/**
* Small component to render the raw search response.
*/
export function ResultsJSON(props: ResultsJSONProps) {
return (
<EuiCodeEditor
mode="json"
theme="textmate"
width="100%"
height="100%"
value={customStringify(props.response)}
readOnly={true}
setOptions={{
fontSize: '12px',
autoScrollEditorIntoView: true,
wrap: true,
}}
tabSize={2}
/>
);
}
111 changes: 111 additions & 0 deletions public/general_components/results/results_table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React, { useState } from 'react';
import {
EuiText,
EuiButtonIcon,
RIGHT_ALIGNMENT,
EuiInMemoryTable,
EuiCodeEditor,
EuiPanel,
} from '@elastic/eui';
import { customStringify, SearchHit } from '../../../common';

interface ResultsTableProps {
hits: SearchHit[];
}

/**
* Small component to display a list of search results with pagination.
* Can expand each entry to view the full _source response
*/
export function ResultsTable(props: ResultsTableProps) {
const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState<{
[itemId: string]: any;
}>({});

const toggleDetails = (hit: SearchHit) => {
const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap };
if (itemIdToExpandedRowMapValues[hit._id]) {
delete itemIdToExpandedRowMapValues[hit._id];
} else {
itemIdToExpandedRowMapValues[hit._id] = (
<EuiPanel
style={{ height: '20vh' }}
hasShadow={false}
hasBorder={false}
paddingSize="none"
>
<EuiCodeEditor
mode="json"
theme="textmate"
width="100%"
height="100%"
value={customStringify(hit._source)}
readOnly={true}
setOptions={{
fontSize: '12px',
autoScrollEditorIntoView: true,
wrap: true,
}}
tabSize={2}
/>
</EuiPanel>
);
}
setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues);
};

return (
<EuiInMemoryTable
itemId="_id"
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
items={props.hits}
isExpandable={true}
compressed={true}
pagination={true}
tableLayout="auto"
columns={[
{
field: '_id',
name: '',
sortable: false,
render: (_, item: SearchHit) => {
return (
<EuiText
size="s"
color="subdued"
style={{
whiteSpace: 'nowrap',
overflow: 'hidden',
width: '20vw',
}}
>
{customStringify(item._source)}
</EuiText>
);
},
},
{
align: RIGHT_ALIGNMENT,
width: '40px',
isExpander: true,
render: (item: SearchHit) => (
<EuiButtonIcon
onClick={() => toggleDetails(item)}
aria-label={
itemIdToExpandedRowMap[item._id] ? 'Collapse' : 'Expand'
}
iconType={
itemIdToExpandedRowMap[item._id] ? 'arrowUp' : 'arrowDown'
}
/>
),
},
]}
/>
);
}
3 changes: 0 additions & 3 deletions public/pages/workflow_detail/resizable_workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {
// Inspector panel state vars. Actions taken in the form can update the Inspector panel,
// hence we keep top-level vars here to pass to both form and inspector components.
const [ingestResponse, setIngestResponse] = useState<string>('');
const [queryResponse, setQueryResponse] = useState<string>('');
const [selectedInspectorTabId, setSelectedInspectorTabId] = useState<
INSPECTOR_TAB_ID
>(INSPECTOR_TAB_ID.INGEST);
Expand Down Expand Up @@ -207,8 +206,6 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {
<Tools
workflow={props.workflow}
ingestResponse={ingestResponse}
queryResponse={queryResponse}
setQueryResponse={setQueryResponse}
selectedTabId={selectedInspectorTabId}
setSelectedTabId={setSelectedInspectorTabId}
selectedStep={props.selectedStep}
Expand Down
45 changes: 13 additions & 32 deletions public/pages/workflow_detail/tools/query/query.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
customStringify,
FETCH_ALL_QUERY,
QueryParam,
SearchHit,
SearchResponse,
WorkflowFormValues,
} from '../../../../../common';
import { searchIndex, useAppDispatch } from '../../../../store';
Expand All @@ -32,11 +32,9 @@ import {
getPlaceholdersFromQuery,
injectParameters,
} from '../../../../utils';
import { QueryParamsList } from '../../../../general_components';
import { QueryParamsList, Results } from '../../../../general_components';

interface QueryProps {
queryResponse: string;
setQueryResponse: (queryResponse: string) => void;
hasSearchPipeline: boolean;
hasIngestResources: boolean;
selectedStep: CONFIG_STEP;
Expand Down Expand Up @@ -65,6 +63,11 @@ export function Query(props: QueryProps) {
// use custom query state
const [useCustomQuery, setUseCustomQuery] = useState<boolean>(false);

// query response state
const [queryResponse, setQueryResponse] = useState<
SearchResponse | undefined
>(undefined);

// Standalone / sandboxed search request state. Users can test things out
// without updating the base form / persisted value. We default to different values
// based on the context (ingest or search), and update based on changes to the context
Expand Down Expand Up @@ -116,7 +119,7 @@ export function Query(props: QueryProps) {
}))
);
}
props.setQueryResponse('');
setQueryResponse(undefined);
}, [tempRequest]);

// empty states
Expand Down Expand Up @@ -182,17 +185,11 @@ export function Query(props: QueryProps) {
})
)
.unwrap()
.then(async (resp) => {
props.setQueryResponse(
customStringify(
resp?.hits?.hits?.map(
(hit: SearchHit) => hit._source
)
)
);
.then(async (resp: SearchResponse) => {
setQueryResponse(resp);
})
.catch((error: any) => {
props.setQueryResponse('');
setQueryResponse(undefined);
console.error('Error running query: ', error);
});
}}
Expand Down Expand Up @@ -283,7 +280,7 @@ export function Query(props: QueryProps) {
<EuiText size="m">Results</EuiText>
</EuiFlexItem>
<EuiFlexItem>
{isEmpty(props.queryResponse) ? (
{queryResponse === undefined || isEmpty(queryResponse) ? (
<EuiEmptyPrompt
title={<h2>No results</h2>}
titleSize="s"
Expand All @@ -294,23 +291,7 @@ export function Query(props: QueryProps) {
}
/>
) : (
// Known issue with the editor where resizing the resizablecontainer does not
// trigger vertical scroll updates. Updating the window, or reloading the component
// by switching tabs etc. will refresh it correctly
<EuiCodeEditor
mode="json"
theme="textmate"
width="100%"
height="100%"
value={props.queryResponse}
readOnly={true}
setOptions={{
fontSize: '12px',
autoScrollEditorIntoView: true,
wrap: true,
}}
tabSize={2}
/>
<Results response={queryResponse} />
)}
</EuiFlexItem>
</EuiFlexGroup>
Expand Down
Loading
Loading