diff --git a/common/constants/index.ts b/common/constants/index.ts index ea25495a..db990084 100644 --- a/common/constants/index.ts +++ b/common/constants/index.ts @@ -10,8 +10,14 @@ export const OPENSEARCH_ACC_DOCUMENTATION_URL = export const ACC_INDEX_TYPE_DOCUMENTATION_URL = 'https://github.com/opensearch-project/opensearch-spark/blob/main/docs/index.md'; -export const SKIPPING_INDEX = `skipping_index`; -export const ON_LOAD_QUERY = `SHOW tables LIKE '%';`; +export const TREE_ITEM_SKIPPING_INDEX_DEFAULT_NAME = `skipping_index`; +export const TREE_ITEM_COVERING_INDEX_DEFAULT_NAME = `covering_index`; +export const TREE_ITEM_MATERIALIZED_VIEW_DEFAULT_NAME = `materialized_view`; +export const TREE_ITEM_DATABASE_NAME_DEFAULT_NAME = `database`; +export const TREE_ITEM_TABLE_NAME_DEFAULT_NAME = `table`; +export const TREE_ITEM_LOAD_MATERIALIZED_BADGE_NAME = `Load Materialized View`; +export const TREE_ITEM_BADGE_NAME =`badge` +export const LOAD_OPENSEARCH_INDICES_QUERY = `SHOW tables LIKE '%';`; export const SKIPPING_INDEX_QUERY = `CREATE SKIPPING INDEX ON myS3.logs_db.http_logs (status VALUE_SET) WITH ( @@ -84,3 +90,5 @@ export const ACCELERATION_INDEX_NAME_INFO = `All OpenSearch acceleration indices `; export const SIDEBAR_POLL_INTERVAL_MS = 5000; + +export const FETCH_OPENSEARCH_INDICES_PATH = '/api/sql_console/sqlquery' \ No newline at end of file diff --git a/common/types/index.ts b/common/types/index.ts index a941b29e..5ed212c8 100644 --- a/common/types/index.ts +++ b/common/types/index.ts @@ -79,4 +79,12 @@ export interface CreateAccelerationForm { formErrors: FormErrorsType; } -export type AsyncQueryLoadingStatus = 'SUCCESS' | 'FAILED' | 'RUNNING' | 'SCHEDULED' | 'CANCELLED'; \ No newline at end of file +export type AsyncQueryLoadingStatus = 'SUCCESS' | 'FAILED' | 'RUNNING' | 'SCHEDULED' | 'CANCELLED'; +export type TreeItemType = 'covering_index' | 'skipping_index' | 'table' | 'database' | 'materialized_view' | 'Load Materialized View' | 'badge' + +export interface TreeItem { + name: string; + type: TreeItemType; + isExpanded: boolean; + values?: TreeItem[]; +} diff --git a/public/components/Main/__snapshots__/main.test.tsx.snap b/public/components/Main/__snapshots__/main.test.tsx.snap index baa291c8..6d65fd66 100644 --- a/public/components/Main/__snapshots__/main.test.tsx.snap +++ b/public/components/Main/__snapshots__/main.test.tsx.snap @@ -193,7 +193,7 @@ exports[`<Main /> spec click clear button 1`] = ` > <div class="euiPageSideBar" - style="max-width: 400px; width: 400px;" + style="max-width: 400px; width: 400px; overflow: auto;" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionColumn euiFlexGroup--responsive" @@ -203,7 +203,39 @@ exports[`<Main /> spec click clear button 1`] = ` > <div class="euiFlexItem euiFlexItem--flexGrowZero" - /> + > + <div + class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + > + <button + aria-label="refresh" + class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--medium" + type="button" + > + <svg + aria-hidden="true" + class="euiIcon euiIcon--medium euiIcon--inherit euiButtonIcon__icon" + focusable="false" + height="16" + role="img" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M11.228 2.942a.5.5 0 1 1-.538.842A5 5 0 1 0 13 8a.5.5 0 1 1 1 0 6 6 0 1 1-2.772-5.058ZM14 1.5v3A1.5 1.5 0 0 1 12.5 6h-3a.5.5 0 0 1 0-1h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 1 1 1 0Z" + /> + </svg> + </button> + </div> + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + /> + </div> + </div> <div class="euiSpacer euiSpacer--l" /> @@ -234,11 +266,11 @@ exports[`<Main /> spec click clear button 1`] = ` <div class="euiSpacer euiSpacer--m" /> - <h2 + <h3 class="euiTitle euiTitle--medium" > - Error loading Datasources - </h2> + Error loading data + </h3> </div> </div> </div> @@ -715,7 +747,7 @@ exports[`<Main /> spec click run button, and response causes an error 1`] = ` > <div class="euiPageSideBar" - style="max-width: 400px; width: 400px;" + style="max-width: 400px; width: 400px; overflow: auto;" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionColumn euiFlexGroup--responsive" @@ -725,7 +757,39 @@ exports[`<Main /> spec click run button, and response causes an error 1`] = ` > <div class="euiFlexItem euiFlexItem--flexGrowZero" - /> + > + <div + class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + > + <button + aria-label="refresh" + class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--medium" + type="button" + > + <svg + aria-hidden="true" + class="euiIcon euiIcon--medium euiIcon--inherit euiButtonIcon__icon" + focusable="false" + height="16" + role="img" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M11.228 2.942a.5.5 0 1 1-.538.842A5 5 0 1 0 13 8a.5.5 0 1 1 1 0 6 6 0 1 1-2.772-5.058ZM14 1.5v3A1.5 1.5 0 0 1 12.5 6h-3a.5.5 0 0 1 0-1h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 1 1 1 0Z" + /> + </svg> + </button> + </div> + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + /> + </div> + </div> <div class="euiSpacer euiSpacer--l" /> @@ -756,11 +820,11 @@ exports[`<Main /> spec click run button, and response causes an error 1`] = ` <div class="euiSpacer euiSpacer--m" /> - <h2 + <h3 class="euiTitle euiTitle--medium" > - Error loading Datasources - </h2> + Error loading data + </h3> </div> </div> </div> @@ -1237,7 +1301,7 @@ exports[`<Main /> spec click run button, and response is not ok 1`] = ` > <div class="euiPageSideBar" - style="max-width: 400px; width: 400px;" + style="max-width: 400px; width: 400px; overflow: auto;" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionColumn euiFlexGroup--responsive" @@ -1247,7 +1311,39 @@ exports[`<Main /> spec click run button, and response is not ok 1`] = ` > <div class="euiFlexItem euiFlexItem--flexGrowZero" - /> + > + <div + class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + > + <button + aria-label="refresh" + class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--medium" + type="button" + > + <svg + aria-hidden="true" + class="euiIcon euiIcon--medium euiIcon--inherit euiButtonIcon__icon" + focusable="false" + height="16" + role="img" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M11.228 2.942a.5.5 0 1 1-.538.842A5 5 0 1 0 13 8a.5.5 0 1 1 1 0 6 6 0 1 1-2.772-5.058ZM14 1.5v3A1.5 1.5 0 0 1 12.5 6h-3a.5.5 0 0 1 0-1h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 1 1 1 0Z" + /> + </svg> + </button> + </div> + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + /> + </div> + </div> <div class="euiSpacer euiSpacer--l" /> @@ -1278,11 +1374,11 @@ exports[`<Main /> spec click run button, and response is not ok 1`] = ` <div class="euiSpacer euiSpacer--m" /> - <h2 + <h3 class="euiTitle euiTitle--medium" > - Error loading Datasources - </h2> + Error loading data + </h3> </div> </div> </div> @@ -1759,7 +1855,7 @@ exports[`<Main /> spec click run button, and response is ok 1`] = ` > <div class="euiPageSideBar" - style="max-width: 400px; width: 400px;" + style="max-width: 400px; width: 400px; overflow: auto;" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionColumn euiFlexGroup--responsive" @@ -1769,7 +1865,39 @@ exports[`<Main /> spec click run button, and response is ok 1`] = ` > <div class="euiFlexItem euiFlexItem--flexGrowZero" - /> + > + <div + class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + > + <button + aria-label="refresh" + class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--medium" + type="button" + > + <svg + aria-hidden="true" + class="euiIcon euiIcon--medium euiIcon--inherit euiButtonIcon__icon" + focusable="false" + height="16" + role="img" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M11.228 2.942a.5.5 0 1 1-.538.842A5 5 0 1 0 13 8a.5.5 0 1 1 1 0 6 6 0 1 1-2.772-5.058ZM14 1.5v3A1.5 1.5 0 0 1 12.5 6h-3a.5.5 0 0 1 0-1h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 1 1 1 0Z" + /> + </svg> + </button> + </div> + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + /> + </div> + </div> <div class="euiSpacer euiSpacer--l" /> @@ -2369,7 +2497,7 @@ exports[`<Main /> spec click run button, response fills null and missing values > <div class="euiPageSideBar" - style="max-width: 400px; width: 400px;" + style="max-width: 400px; width: 400px; overflow: auto;" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionColumn euiFlexGroup--responsive" @@ -2379,7 +2507,39 @@ exports[`<Main /> spec click run button, response fills null and missing values > <div class="euiFlexItem euiFlexItem--flexGrowZero" - /> + > + <div + class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + > + <button + aria-label="refresh" + class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--medium" + type="button" + > + <svg + aria-hidden="true" + class="euiIcon euiIcon--medium euiIcon--inherit euiButtonIcon__icon" + focusable="false" + height="16" + role="img" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M11.228 2.942a.5.5 0 1 1-.538.842A5 5 0 1 0 13 8a.5.5 0 1 1 1 0 6 6 0 1 1-2.772-5.058ZM14 1.5v3A1.5 1.5 0 0 1 12.5 6h-3a.5.5 0 0 1 0-1h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 1 1 1 0Z" + /> + </svg> + </button> + </div> + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + /> + </div> + </div> <div class="euiSpacer euiSpacer--l" /> @@ -2978,7 +3138,7 @@ exports[`<Main /> spec click translation button, and response is ok 1`] = ` > <div class="euiPageSideBar" - style="max-width: 400px; width: 400px;" + style="max-width: 400px; width: 400px; overflow: auto;" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionColumn euiFlexGroup--responsive" @@ -2988,7 +3148,39 @@ exports[`<Main /> spec click translation button, and response is ok 1`] = ` > <div class="euiFlexItem euiFlexItem--flexGrowZero" - /> + > + <div + class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + > + <button + aria-label="refresh" + class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--medium" + type="button" + > + <svg + aria-hidden="true" + class="euiIcon euiIcon--medium euiIcon--inherit euiButtonIcon__icon" + focusable="false" + height="16" + role="img" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M11.228 2.942a.5.5 0 1 1-.538.842A5 5 0 1 0 13 8a.5.5 0 1 1 1 0 6 6 0 1 1-2.772-5.058ZM14 1.5v3A1.5 1.5 0 0 1 12.5 6h-3a.5.5 0 0 1 0-1h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 1 1 1 0Z" + /> + </svg> + </button> + </div> + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + /> + </div> + </div> <div class="euiSpacer euiSpacer--l" /> @@ -3019,11 +3211,11 @@ exports[`<Main /> spec click translation button, and response is ok 1`] = ` <div class="euiSpacer euiSpacer--m" /> - <h2 + <h3 class="euiTitle euiTitle--medium" > - Error loading Datasources - </h2> + Error loading data + </h3> </div> </div> </div> @@ -3491,7 +3683,7 @@ exports[`<Main /> spec renders the component 1`] = ` > <div class="euiPageSideBar" - style="max-width: 400px; width: 400px;" + style="max-width: 400px; width: 400px; overflow: auto;" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionColumn euiFlexGroup--responsive" @@ -3501,7 +3693,35 @@ exports[`<Main /> spec renders the component 1`] = ` > <div class="euiFlexItem euiFlexItem--flexGrowZero" - /> + > + <div + class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + > + <button + aria-label="refresh" + class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--medium" + type="button" + > + <svg + aria-hidden="true" + class="euiIcon euiIcon--medium euiIcon--inherit euiIcon-isLoading euiButtonIcon__icon" + focusable="false" + height="16" + role="img" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + /> + </button> + </div> + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + /> + </div> + </div> <div class="euiSpacer euiSpacer--l" /> diff --git a/public/components/Main/main.tsx b/public/components/Main/main.tsx index 7e8382d8..9cf36c94 100644 --- a/public/components/Main/main.tsx +++ b/public/components/Main/main.tsx @@ -5,6 +5,7 @@ import { EuiButton, + EuiButtonIcon, EuiComboBoxOptionOption, EuiFlexGroup, EuiFlexItem, @@ -36,7 +37,7 @@ import QueryResults from '../QueryResults/QueryResults'; import { CreateButton } from '../SQLPage/CreateButton'; import { DataSelect } from '../SQLPage/DataSelect'; import { SQLPage } from '../SQLPage/SQLPage'; -import { TableView } from '../SQLPage/TableView'; +import { TableView } from '../SQLPage/table_view'; interface ResponseData { ok: boolean; @@ -111,6 +112,7 @@ interface MainState { asyncLoadingStatus: AsyncQueryLoadingStatus; asyncQueryError: string; asyncJobId: string; + refreshTree: boolean; isAccelerationFlyoutOpened: boolean; isCallOutVisible: boolean; } @@ -251,6 +253,7 @@ export class Main extends React.Component<MainProps, MainState> { asyncLoadingStatus: 'SUCCESS', asyncQueryError: '', asyncJobId: '', + refreshTree: false, isAccelerationFlyoutOpened: false, isCallOutVisible: false, }; @@ -814,6 +817,12 @@ export class Main extends React.Component<MainProps, MainState> { }); }; + handleReloadTree = () => { + this.setState({ + refreshTree: !this.state.refreshTree, + }); + }; + setIsAccelerationFlyoutOpened = (value: boolean) => { this.setState({ isAccelerationFlyoutOpened: value, @@ -942,20 +951,34 @@ export class Main extends React.Component<MainProps, MainState> { <EuiPage paddingSize="none"> {this.state.language === 'SQL' && ( <EuiPanel> - <EuiPageSideBar style={{ maxWidth: '400px', width: '400px' }}> + <EuiPageSideBar style={{ maxWidth: '400px', width: '400px' , overflow: 'auto' }}> <EuiFlexGroup direction="column"> <EuiFlexItem> <EuiFlexItem grow={false}> - <CreateButton - updateSQLQueries={this.updateSQLQueries} - selectedDatasource={this.state.selectedDatasource} - /> + <EuiFlexGroup direction="row" gutterSize="s"> + <EuiFlexItem grow={false}> + <EuiButtonIcon + display="base" + iconType="refresh" + size='m' + aria-label='refresh' + onClick={this.handleReloadTree} + /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <CreateButton + updateSQLQueries={this.updateSQLQueries} + selectedDatasource={this.state.selectedDatasource} + /> + </EuiFlexItem> + </EuiFlexGroup> </EuiFlexItem> <EuiSpacer /> <TableView http={this.httpClient} selectedItems={this.state.selectedDatasource} updateSQLQueries={this.updateSQLQueries} + refreshTree={this.state.refreshTree} /> <EuiSpacer /> </EuiFlexItem> diff --git a/public/components/SQLPage/TableView.tsx b/public/components/SQLPage/TableView.tsx deleted file mode 100644 index 017fbf19..00000000 --- a/public/components/SQLPage/TableView.tsx +++ /dev/null @@ -1,266 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { - EuiComboBoxOptionOption, - EuiEmptyPrompt, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiLoadingSpinner, - EuiText, - EuiToolTip, - EuiTreeView, -} from '@elastic/eui'; -import _ from 'lodash'; -import React, { useEffect, useState } from 'react'; -import { CoreStart } from '../../../../../src/core/public'; -import { ON_LOAD_QUERY, SKIPPING_INDEX } from '../../../common/constants'; -import { AccelerationIndexFlyout } from './acceleration_index_flyout'; -import { getJobId, pollQueryStatus } from './utils'; - -interface CustomView { - http: CoreStart['http']; - selectedItems: EuiComboBoxOptionOption[]; - updateSQLQueries: (query: string) => void; -} - -export const TableView = ({ http, selectedItems, updateSQLQueries }: CustomView) => { - const [tablenames, setTablenames] = useState<string[]>([]); - const [selectedNode, setSelectedNode] = useState<string | null>(null); - const [childData, setChildData] = useState<string[]>([]); - const [selectedChildNode, setSelectedChildNode] = useState<string | null>(null); - const [indexData, setIndexedData] = useState<string[]>([]); - const [isLoading, setIsLoading] = useState(false); - const [indexFlyout, setIndexFlyout] = useState(<></>); - const [childLoadingStates, setChildLoadingStates] = useState<{ [key: string]: boolean }>({}); - const [tableLoadingStates, setTableLoadingStates] = useState<{ [key: string]: boolean }>({}); - - let indiciesData: string[] = []; - - const resetFlyout = () => { - setIndexFlyout(<></>); - }; - - const handleAccelerationIndexClick = ( - dataSource: string, - database: string, - dataTable: string, - indexName: string - ) => { - setIndexFlyout( - <AccelerationIndexFlyout - dataSource={dataSource} - database={database} - dataTable={dataTable} - indexName={indexName} - resetFlyout={resetFlyout} - updateSQLQueries={updateSQLQueries} - /> - ); - }; - - const get_async_query_results = (id, http, callback) => { - pollQueryStatus(id, http, callback); - }; - - const getSidebarContent = () => { - if (selectedItems[0].label === 'OpenSearch') { - setTablenames([]); - const query = { query: ON_LOAD_QUERY }; - http - .post(`/api/sql_console/sqlquery`, { - body: JSON.stringify(query), - }) - .then((res) => { - const responseObj = res.data.resp ? JSON.parse(res.data.resp) : ''; - const datarows: any[][] = _.get(responseObj, 'datarows'); - const fields = datarows.map((data) => { - return data[2]; - }); - setTablenames(fields); - }) - .catch((err) => { - console.error(err); - }); - } else { - setIsLoading(true); - setTablenames([]); - const query = { - lang: 'sql', - query: `SHOW SCHEMAS IN ${selectedItems[0]['label']}`, - datasource: selectedItems[0]['label'], - }; - getJobId(query, http, (id) => { - get_async_query_results(id, http, (data) => { - setTablenames(data); - setIsLoading(false); - }); - }); - } - }; - - useEffect(() => { - setIsLoading(false); - getSidebarContent(); - }, [selectedItems]); - - const handleNodeClick = (nodeLabel: string) => { - setSelectedNode(nodeLabel); - const query = { - lang: 'sql', - query: `SHOW TABLES IN ${selectedItems[0]['label']}.${nodeLabel}`, - datasource: selectedItems[0]['label'], - }; - setTableLoadingStates((prevState) => ({ - ...prevState, - [nodeLabel]: true, - })); - getJobId(query, http, (id) => { - get_async_query_results(id, http, (data) => { - data = data.map((subArray) => subArray[1]); - setChildData(data); - - setTableLoadingStates((prevState) => ({ - ...prevState, - [nodeLabel]: false, - })); - }); - }); - }; - - const callCoverQuery = (nodeLabel1: string) => { - const coverQuery = { - lang: 'sql', - query: `SHOW INDEX ON ${selectedItems[0]['label']}.${selectedNode}.${nodeLabel1}`, - datasource: selectedItems[0]['label'], - }; - getJobId(coverQuery, http, (id) => { - get_async_query_results(id, http, (data) => { - const res = [].concat(data); - const final = indiciesData.concat(...res); - setIndexedData(final); - setChildLoadingStates((prevState) => ({ - ...prevState, - [nodeLabel1]: false, - })); - }); - }); - }; - const handleChildClick = (nodeLabel1: string) => { - setSelectedChildNode(nodeLabel1); - const skipQuery = { - lang: 'sql', - query: `DESC SKIPPING INDEX ON ${selectedItems[0]['label']}.${selectedNode}.${nodeLabel1}`, - datasource: selectedItems[0]['label'], - }; - setChildLoadingStates((prevState) => ({ - ...prevState, - [nodeLabel1]: true, - })); - - getJobId(skipQuery, http, (id) => { - get_async_query_results(id, http, (data) => { - if (data.length > 0) { - indiciesData.push(SKIPPING_INDEX); - } - callCoverQuery(nodeLabel1); - }); - }); - }; - - const treeData = tablenames.map((database, index) => ({ - label: ( - <div> - <EuiToolTip position="right" content={database} delay="long"> - <EuiText>{_.truncate(database, { length: 50 })}</EuiText> - </EuiToolTip>{' '} - {tableLoadingStates[database] && <EuiLoadingSpinner size="m" />} - </div> - ), - icon: <EuiIcon type="database" size="m" />, - id: 'element_' + index, - callback: () => { - setChildData([]); - selectedItems[0].label !== 'OpenSearch' && handleNodeClick(database); - }, - isSelectable: true, - isExpanded: true, - children: - selectedNode === database - ? childData.map((table) => ({ - label: ( - <div> - <EuiToolTip position="right" content={table} delay="long"> - <EuiText>{_.truncate(table, { length: 50 })}</EuiText> - </EuiToolTip>{' '} - {childLoadingStates[table] && <EuiLoadingSpinner size="m" />} - </div> - ), - id: `${database}_${table}`, - icon: <EuiIcon type="tableDensityCompact" size="s" />, - callback: () => { - setIndexedData([]); - handleChildClick(table); - setChildLoadingStates((prevState) => ({ - ...prevState, - [selectedChildNode]: false, - })); - }, - sSelectable: true, - isExpanded: true, - children: - selectedChildNode === table - ? indexData.map((indexChild) => ({ - label: ( - <div> - <EuiToolTip position="right" content={indexChild} delay="long"> - <EuiText>{_.truncate(indexChild, { length: 50 })}</EuiText> - </EuiToolTip> - </div> - ), - id: `${table}_${indexChild}`, - icon: <EuiIcon type="bolt" size="s" />, - callback: () => - handleAccelerationIndexClick( - selectedItems[0].label, - database, - table, - indexChild - ), - })) - : undefined, - })) - : undefined, - })); - - return ( - <> - <EuiFlexGroup> - {isLoading ? ( - <EuiFlexGroup alignItems="center" gutterSize="s"> - <EuiFlexItem grow={false}>Loading databases</EuiFlexItem> - <EuiFlexItem> - <EuiLoadingSpinner size="m" /> - </EuiFlexItem> - </EuiFlexGroup> - ) : treeData.length > 0 ? ( - <EuiFlexItem grow={false}> - <EuiTreeView aria-label="Sample Folder Tree" items={treeData} /> - </EuiFlexItem> - ) : ( - <EuiFlexItem grow={false}> - <EuiEmptyPrompt - iconType="alert" - iconColor="danger" - title={<h2>Error loading Datasources</h2>} - /> - </EuiFlexItem> - )} - {indexFlyout} - </EuiFlexGroup> - </> - ); -}; diff --git a/public/components/SQLPage/__snapshots__/table_view.test.tsx.snap b/public/components/SQLPage/__snapshots__/table_view.test.tsx.snap new file mode 100644 index 00000000..6b8be8c2 --- /dev/null +++ b/public/components/SQLPage/__snapshots__/table_view.test.tsx.snap @@ -0,0 +1,1016 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render databases in tree fetches and displays database nodes when datasource is s3 1`] = ` +<div> + <div + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + > + <div + class="euiEmptyPrompt" + > + <svg + aria-hidden="true" + class="euiIcon euiIcon--xxLarge euiIcon--danger" + focusable="false" + height="16" + role="img" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M7.59 10.059 7.35 5.18h1.3L8.4 10.06h-.81Zm.394 1.901a.61.61 0 0 1-.448-.186.606.606 0 0 1-.186-.444c0-.174.062-.323.186-.446a.614.614 0 0 1 .448-.184c.169 0 .315.06.44.182.124.122.186.27.186.448a.6.6 0 0 1-.189.446.607.607 0 0 1-.437.184ZM2 14a1 1 0 0 1-.878-1.479l6-11a1 1 0 0 1 1.756 0l6 11A1 1 0 0 1 14 14H2Zm0-1h12L8 2 2 13Z" + fill-rule="evenodd" + /> + </svg> + <div + class="euiSpacer euiSpacer--m" + /> + <h3 + class="euiTitle euiTitle--medium" + > + Error loading data + </h3> + </div> + </div> + </div> +</div> +`; + +exports[`Render databases in tree fetches and displays indicies when datasource is OpenSearch 1`] = ` +<div> + <div + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + > + <div + class="euiText euiText--medium euiTreeView__wrapper" + > + <p + class="euiScreenReaderOnly" + id="some_html_id--instruction" + > + You can quickly navigate this list using arrow keys. + </p> + <ul + aria-describedby="some_html_id--instruction" + aria-label="Sample Folder Tree" + class="euiTreeView" + id="some_html_id" + > + <li + class="euiTreeView__node" + > + <button + aria-controls="some_html_id" + aria-expanded="false" + class="euiTreeView__nodeInner" + data-test-subj="euiTreeViewButton-some_html_id" + id="element_0" + > + <span + class="euiTreeView__iconWrapper" + > + <svg + aria-hidden="true" + class="euiIcon euiIcon--medium euiIcon-isLoading" + focusable="false" + height="16" + role="img" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + /> + </span> + <span + class="euiTreeView__nodeLabel" + > + <div> + <span + class="euiToolTipAnchor" + > + <div + class="euiText euiText--medium" + > + .kibana_1 + </div> + </span> + + </div> + </span> + </button> + <div + id="some_html_id" + /> + </li> + <li + class="euiTreeView__node" + > + <button + aria-controls="some_html_id" + aria-expanded="false" + class="euiTreeView__nodeInner" + data-test-subj="euiTreeViewButton-some_html_id" + id="element_1" + > + <span + class="euiTreeView__iconWrapper" + > + <svg + aria-hidden="true" + class="euiIcon euiIcon--medium euiIcon-isLoading" + focusable="false" + height="16" + role="img" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + /> + </span> + <span + class="euiTreeView__nodeLabel" + > + <div> + <span + class="euiToolTipAnchor" + > + <div + class="euiText euiText--medium" + > + .kibana_2 + </div> + </span> + + </div> + </span> + </button> + <div + id="some_html_id" + /> + </li> + <li + class="euiTreeView__node" + > + <button + aria-controls="some_html_id" + aria-expanded="false" + class="euiTreeView__nodeInner" + data-test-subj="euiTreeViewButton-some_html_id" + id="element_2" + > + <span + class="euiTreeView__iconWrapper" + > + <svg + aria-hidden="true" + class="euiIcon euiIcon--medium euiIcon-isLoading" + focusable="false" + height="16" + role="img" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + /> + </span> + <span + class="euiTreeView__nodeLabel" + > + <div> + <span + class="euiToolTipAnchor" + > + <div + class="euiText euiText--medium" + > + .kibana_3 + </div> + </span> + + </div> + </span> + </button> + <div + id="some_html_id" + /> + </li> + <li + class="euiTreeView__node" + > + <button + aria-controls="some_html_id" + aria-expanded="false" + class="euiTreeView__nodeInner" + data-test-subj="euiTreeViewButton-some_html_id" + id="element_3" + > + <span + class="euiTreeView__iconWrapper" + > + <svg + aria-hidden="true" + class="euiIcon euiIcon--medium euiIcon-isLoading" + focusable="false" + height="16" + role="img" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + /> + </span> + <span + class="euiTreeView__nodeLabel" + > + <div> + <span + class="euiToolTipAnchor" + > + <div + class="euiText euiText--medium" + > + .opendistro-reports-definitions + </div> + </span> + + </div> + </span> + </button> + <div + id="some_html_id" + /> + </li> + <li + class="euiTreeView__node" + > + <button + aria-controls="some_html_id" + aria-expanded="false" + class="euiTreeView__nodeInner" + data-test-subj="euiTreeViewButton-some_html_id" + id="element_4" + > + <span + class="euiTreeView__iconWrapper" + > + <svg + aria-hidden="true" + class="euiIcon euiIcon--medium euiIcon-isLoading" + focusable="false" + height="16" + role="img" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + /> + </span> + <span + class="euiTreeView__nodeLabel" + > + <div> + <span + class="euiToolTipAnchor" + > + <div + class="euiText euiText--medium" + > + .opendistro-reports-instances + </div> + </span> + + </div> + </span> + </button> + <div + id="some_html_id" + /> + </li> + <li + class="euiTreeView__node" + > + <button + aria-controls="some_html_id" + aria-expanded="false" + class="euiTreeView__nodeInner" + data-test-subj="euiTreeViewButton-some_html_id" + id="element_5" + > + <span + class="euiTreeView__iconWrapper" + > + <svg + aria-hidden="true" + class="euiIcon euiIcon--medium euiIcon-isLoading" + focusable="false" + height="16" + role="img" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + /> + </span> + <span + class="euiTreeView__nodeLabel" + > + <div> + <span + class="euiToolTipAnchor" + > + <div + class="euiText euiText--medium" + > + .opensearch-observability + </div> + </span> + + </div> + </span> + </button> + <div + id="some_html_id" + /> + </li> + <li + class="euiTreeView__node" + > + <button + aria-controls="some_html_id" + aria-expanded="false" + class="euiTreeView__nodeInner" + data-test-subj="euiTreeViewButton-some_html_id" + id="element_6" + > + <span + class="euiTreeView__iconWrapper" + > + <svg + aria-hidden="true" + class="euiIcon euiIcon--medium euiIcon-isLoading" + focusable="false" + height="16" + role="img" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + /> + </span> + <span + class="euiTreeView__nodeLabel" + > + <div> + <span + class="euiToolTipAnchor" + > + <div + class="euiText euiText--medium" + > + .plugins-ml-config + </div> + </span> + + </div> + </span> + </button> + <div + id="some_html_id" + /> + </li> + <li + class="euiTreeView__node" + > + <button + aria-controls="some_html_id" + aria-expanded="false" + class="euiTreeView__nodeInner" + data-test-subj="euiTreeViewButton-some_html_id" + id="element_7" + > + <span + class="euiTreeView__iconWrapper" + > + <svg + aria-hidden="true" + class="euiIcon euiIcon--medium euiIcon-isLoading" + focusable="false" + height="16" + role="img" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + /> + </span> + <span + class="euiTreeView__nodeLabel" + > + <div> + <span + class="euiToolTipAnchor" + > + <div + class="euiText euiText--medium" + > + .ql-datasources + </div> + </span> + + </div> + </span> + </button> + <div + id="some_html_id" + /> + </li> + <li + class="euiTreeView__node" + > + <button + aria-controls="some_html_id" + aria-expanded="false" + class="euiTreeView__nodeInner" + data-test-subj="euiTreeViewButton-some_html_id" + id="element_8" + > + <span + class="euiTreeView__iconWrapper" + > + <svg + aria-hidden="true" + class="euiIcon euiIcon--medium euiIcon-isLoading" + focusable="false" + height="16" + role="img" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + /> + </span> + <span + class="euiTreeView__nodeLabel" + > + <div> + <span + class="euiToolTipAnchor" + > + <div + class="euiText euiText--medium" + > + .ql-job-metadata + </div> + </span> + + </div> + </span> + </button> + <div + id="some_html_id" + /> + </li> + <li + class="euiTreeView__node" + > + <button + aria-controls="some_html_id" + aria-expanded="false" + class="euiTreeView__nodeInner" + data-test-subj="euiTreeViewButton-some_html_id" + id="element_9" + > + <span + class="euiTreeView__iconWrapper" + > + <svg + aria-hidden="true" + class="euiIcon euiIcon--medium euiIcon-isLoading" + focusable="false" + height="16" + role="img" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + /> + </span> + <span + class="euiTreeView__nodeLabel" + > + <div> + <span + class="euiToolTipAnchor" + > + <div + class="euiText euiText--medium" + > + .query_execution_result + </div> + </span> + + </div> + </span> + </button> + <div + id="some_html_id" + /> + </li> + <li + class="euiTreeView__node" + > + <button + aria-controls="some_html_id" + aria-expanded="false" + class="euiTreeView__nodeInner" + data-test-subj="euiTreeViewButton-some_html_id" + id="element_10" + > + <span + class="euiTreeView__iconWrapper" + > + <svg + aria-hidden="true" + class="euiIcon euiIcon--medium euiIcon-isLoading" + focusable="false" + height="16" + role="img" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + /> + </span> + <span + class="euiTreeView__nodeLabel" + > + <div> + <span + class="euiToolTipAnchor" + > + <div + class="euiText euiText--medium" + > + flint_mys3_default_alb_logs_temp_10_test_index + </div> + </span> + + </div> + </span> + </button> + <div + id="some_html_id" + /> + </li> + <li + class="euiTreeView__node" + > + <button + aria-controls="some_html_id" + aria-expanded="false" + class="euiTreeView__nodeInner" + data-test-subj="euiTreeViewButton-some_html_id" + id="element_11" + > + <span + class="euiTreeView__iconWrapper" + > + <svg + aria-hidden="true" + class="euiIcon euiIcon--medium euiIcon-isLoading" + focusable="false" + height="16" + role="img" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + /> + </span> + <span + class="euiTreeView__nodeLabel" + > + <div> + <span + class="euiToolTipAnchor" + > + <div + class="euiText euiText--medium" + > + flint_mys3_default_alb_logs_temp_5_mys3_default... + </div> + </span> + + </div> + </span> + </button> + <div + id="some_html_id" + /> + </li> + <li + class="euiTreeView__node" + > + <button + aria-controls="some_html_id" + aria-expanded="false" + class="euiTreeView__nodeInner" + data-test-subj="euiTreeViewButton-some_html_id" + id="element_12" + > + <span + class="euiTreeView__iconWrapper" + > + <svg + aria-hidden="true" + class="euiIcon euiIcon--medium euiIcon-isLoading" + focusable="false" + height="16" + role="img" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + /> + </span> + <span + class="euiTreeView__nodeLabel" + > + <div> + <span + class="euiToolTipAnchor" + > + <div + class="euiText euiText--medium" + > + flint_mys3_default_elb_logs_regex_elb_logs_rege... + </div> + </span> + + </div> + </span> + </button> + <div + id="some_html_id" + /> + </li> + <li + class="euiTreeView__node" + > + <button + aria-controls="some_html_id" + aria-expanded="false" + class="euiTreeView__nodeInner" + data-test-subj="euiTreeViewButton-some_html_id" + id="element_13" + > + <span + class="euiTreeView__iconWrapper" + > + <svg + aria-hidden="true" + class="euiIcon euiIcon--medium euiIcon-isLoading" + focusable="false" + height="16" + role="img" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + /> + </span> + <span + class="euiTreeView__nodeLabel" + > + <div> + <span + class="euiToolTipAnchor" + > + <div + class="euiText euiText--medium" + > + flint_mys3_default_empty_table_empty_table_inde... + </div> + </span> + + </div> + </span> + </button> + <div + id="some_html_id" + /> + </li> + <li + class="euiTreeView__node" + > + <button + aria-controls="some_html_id" + aria-expanded="false" + class="euiTreeView__nodeInner" + data-test-subj="euiTreeViewButton-some_html_id" + id="element_14" + > + <span + class="euiTreeView__iconWrapper" + > + <svg + aria-hidden="true" + class="euiIcon euiIcon--medium euiIcon-isLoading" + focusable="false" + height="16" + role="img" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + /> + </span> + <span + class="euiTreeView__nodeLabel" + > + <div> + <span + class="euiToolTipAnchor" + > + <div + class="euiText euiText--medium" + > + flint_mys3_default_http_logs_skipping_index + </div> + </span> + + </div> + </span> + </button> + <div + id="some_html_id" + /> + </li> + <li + class="euiTreeView__node" + > + <button + aria-controls="some_html_id" + aria-expanded="false" + class="euiTreeView__nodeInner" + data-test-subj="euiTreeViewButton-some_html_id" + id="element_15" + > + <span + class="euiTreeView__iconWrapper" + > + <svg + aria-hidden="true" + class="euiIcon euiIcon--medium euiIcon-isLoading" + focusable="false" + height="16" + role="img" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + /> + </span> + <span + class="euiTreeView__nodeLabel" + > + <div> + <span + class="euiToolTipAnchor" + > + <div + class="euiText euiText--medium" + > + flint_mys3_default_parquet_elb_logs_simple_parq... + </div> + </span> + + </div> + </span> + </button> + <div + id="some_html_id" + /> + </li> + <li + class="euiTreeView__node" + > + <button + aria-controls="some_html_id" + aria-expanded="false" + class="euiTreeView__nodeInner" + data-test-subj="euiTreeViewButton-some_html_id" + id="element_16" + > + <span + class="euiTreeView__iconWrapper" + > + <svg + aria-hidden="true" + class="euiIcon euiIcon--medium euiIcon-isLoading" + focusable="false" + height="16" + role="img" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + /> + </span> + <span + class="euiTreeView__nodeLabel" + > + <div> + <span + class="euiToolTipAnchor" + > + <div + class="euiText euiText--medium" + > + opensearch_dashboards_sample_data_flights + </div> + </span> + + </div> + </span> + </button> + <div + id="some_html_id" + /> + </li> + <li + class="euiTreeView__node" + > + <button + aria-controls="some_html_id" + aria-expanded="false" + class="euiTreeView__nodeInner" + data-test-subj="euiTreeViewButton-some_html_id" + id="element_17" + > + <span + class="euiTreeView__iconWrapper" + > + <svg + aria-hidden="true" + class="euiIcon euiIcon--medium euiIcon-isLoading" + focusable="false" + height="16" + role="img" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + /> + </span> + <span + class="euiTreeView__nodeLabel" + > + <div> + <span + class="euiToolTipAnchor" + > + <div + class="euiText euiText--medium" + > + opensearch_dashboards_sample_data_logs + </div> + </span> + + </div> + </span> + </button> + <div + id="some_html_id" + /> + </li> + <li + class="euiTreeView__node" + > + <button + aria-controls="some_html_id" + aria-expanded="false" + class="euiTreeView__nodeInner" + data-test-subj="euiTreeViewButton-some_html_id" + id="element_18" + > + <span + class="euiTreeView__iconWrapper" + > + <svg + aria-hidden="true" + class="euiIcon euiIcon--medium euiIcon-isLoading" + focusable="false" + height="16" + role="img" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + /> + </span> + <span + class="euiTreeView__nodeLabel" + > + <div> + <span + class="euiToolTipAnchor" + > + <div + class="euiText euiText--medium" + > + ss4o_traces-elb-test + </div> + </span> + + </div> + </span> + </button> + <div + id="some_html_id" + /> + </li> + <li + class="euiTreeView__node" + > + <button + aria-controls="some_html_id" + aria-expanded="false" + class="euiTreeView__nodeInner" + data-test-subj="euiTreeViewButton-some_html_id" + id="element_19" + > + <span + class="euiTreeView__iconWrapper" + > + <svg + aria-hidden="true" + class="euiIcon euiIcon--medium euiIcon-isLoading" + focusable="false" + height="16" + role="img" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + /> + </span> + <span + class="euiTreeView__nodeLabel" + > + <div> + <span + class="euiToolTipAnchor" + > + <div + class="euiText euiText--medium" + > + .flint-elv-mv + </div> + </span> + + </div> + </span> + </button> + <div + id="some_html_id" + /> + </li> + <li + class="euiTreeView__node" + > + <button + aria-controls="some_html_id" + aria-expanded="false" + class="euiTreeView__nodeInner" + data-test-subj="euiTreeViewButton-some_html_id" + id="element_20" + > + <span + class="euiTreeView__iconWrapper" + > + <svg + aria-hidden="true" + class="euiIcon euiIcon--medium euiIcon-isLoading" + focusable="false" + height="16" + role="img" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + /> + </span> + <span + class="euiTreeView__nodeLabel" + > + <div> + <span + class="euiToolTipAnchor" + > + <div + class="euiText euiText--medium" + > + .kibana + </div> + </span> + + </div> + </span> + </button> + <div + id="some_html_id" + /> + </li> + </ul> + </div> + </div> + </div> +</div> +`; diff --git a/public/components/SQLPage/table_view.test.tsx b/public/components/SQLPage/table_view.test.tsx new file mode 100644 index 00000000..3f602918 --- /dev/null +++ b/public/components/SQLPage/table_view.test.tsx @@ -0,0 +1,57 @@ +import '@testing-library/jest-dom'; +import React from 'react'; +import { httpClientMock } from '../../../test/mocks'; + +import '@testing-library/jest-dom/extend-expect'; +import { render, waitFor } from '@testing-library/react'; +import { HttpResponse } from '../../../../../src/core/public'; +import { mockDatabaseQuery, mockJobId, mockOpenSearchIndicies } from '../../../test/mocks/mockData'; +import { TableView } from './table_view'; + +describe('Render databases in tree', () => { + it('fetches and displays indicies when datasource is OpenSearch', async () => { + const client = httpClientMock; + client.post = jest.fn(() => { + return (Promise.resolve(mockOpenSearchIndicies) as unknown) as HttpResponse; + }); + + const asyncTest = () => { + render( + <TableView + http={client} + selectedItems={[{ label: 'OpenSearch' }]} + updateSQLQueries={() => {}} + refreshTree={false} + /> + ); + }; + await asyncTest(); + expect(document.body.children[0]).toMatchSnapshot(); + }); + it('fetches and displays database nodes when datasource is s3', async () => { + const client = httpClientMock; + client.post = jest.fn(() => { + return (Promise.resolve(mockJobId) as unknown) as HttpResponse; + }); + client.get = jest.fn(() => { + return (Promise.resolve(mockDatabaseQuery) as unknown) as HttpResponse; + }); + + const { getByText } = render( + <TableView + http={client} + selectedItems={[{ label: 'my_glue' }]} + updateSQLQueries={() => {}} + refreshTree={false} + /> + ); + await waitFor(() => { + expect( + getByText( + 'Loading can take more than 30s. Queries can be made after the data has loaded. Any queries run before the data is loaded will be queued' + ) + ).toBeInTheDocument(); + }); + expect(document.body.children[0]).toMatchSnapshot(); + }); +}); diff --git a/public/components/SQLPage/table_view.tsx b/public/components/SQLPage/table_view.tsx new file mode 100644 index 00000000..eb2a44bf --- /dev/null +++ b/public/components/SQLPage/table_view.tsx @@ -0,0 +1,405 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiBadge, + EuiComboBoxOptionOption, + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiLoadingSpinner, + EuiText, + EuiToolTip, + EuiTreeView, +} from '@elastic/eui'; +import { TreeItem, TreeItemType } from 'common/types'; +import _ from 'lodash'; +import React, { useEffect, useState } from 'react'; +import { CoreStart } from '../../../../../src/core/public'; +import { + FETCH_OPENSEARCH_INDICES_PATH, + LOAD_OPENSEARCH_INDICES_QUERY, + TREE_ITEM_BADGE_NAME, + TREE_ITEM_COVERING_INDEX_DEFAULT_NAME, + TREE_ITEM_DATABASE_NAME_DEFAULT_NAME, + TREE_ITEM_LOAD_MATERIALIZED_BADGE_NAME, + TREE_ITEM_MATERIALIZED_VIEW_DEFAULT_NAME, + TREE_ITEM_SKIPPING_INDEX_DEFAULT_NAME, + TREE_ITEM_TABLE_NAME_DEFAULT_NAME, +} from '../../../common/constants'; +import { AccelerationIndexFlyout } from './acceleration_index_flyout'; +import { getJobId, pollQueryStatus } from './utils'; + +interface CustomView { + http: CoreStart['http']; + selectedItems: EuiComboBoxOptionOption[]; + updateSQLQueries: (query: string) => void; + refreshTree: boolean; +} + +export const TableView = ({ http, selectedItems, updateSQLQueries, refreshTree }: CustomView) => { + const [tableNames, setTableNames] = useState<string[]>([]); + const [selectedDatabase, setSelectedDatabase] = useState<string>(''); + const [selectedTable, setSelectedTable] = useState<string | null>(null); + const [isLoading, setIsLoading] = useState(false); + const [indexFlyout, setIndexFlyout] = useState(<></>); + const [treeData, setTreeData] = useState<TreeItem[]>([]); + + const resetFlyout = () => { + setIndexFlyout(<></>); + }; + + const handleAccelerationIndexClick = ( + dataSource: string, + database: string, + dataTable: string, + indexName: string + ) => { + setIndexFlyout( + <AccelerationIndexFlyout + dataSource={dataSource} + database={database} + dataTable={dataTable} + indexName={indexName} + resetFlyout={resetFlyout} + updateSQLQueries={updateSQLQueries} + /> + ); + }; + + function loadTreeItem(elements: string[], type: TreeItemType): TreeItem[] { + return elements.map((element) => { + let treeItem: TreeItem = { + name: element, + type: type, + isExpanded: true, + }; + + if ( + type != TREE_ITEM_COVERING_INDEX_DEFAULT_NAME && + type != TREE_ITEM_SKIPPING_INDEX_DEFAULT_NAME + ) { + treeItem.values = []; + } + return treeItem; + }); + } + + const get_async_query_results = (id, http, callback) => { + pollQueryStatus(id, http, callback); + }; + + const getSidebarContent = () => { + if (selectedItems[0].label === 'OpenSearch') { + setTableNames([]); + const query = { query: LOAD_OPENSEARCH_INDICES_QUERY }; + http + .post(FETCH_OPENSEARCH_INDICES_PATH, { + body: JSON.stringify(query), + }) + .then((res) => { + const responseObj = res.data.resp ? JSON.parse(res.data.resp) : {}; + const dataRows: any[][] = _.get(responseObj, 'datarows'); + const fields = dataRows.map((data) => { + return data[2]; + }); + setTableNames(fields); + }) + .catch((err) => { + console.error(err); + }); + } else { + setIsLoading(true); + setTableNames([]); + const query = { + lang: 'sql', + query: `SHOW SCHEMAS IN ${selectedItems[0]['label']}`, + datasource: selectedItems[0]['label'], + }; + getJobId(query, http, (id) => { + get_async_query_results(id, http, (data) => { + data = [].concat(...data); + setTreeData(loadTreeItem(data, TREE_ITEM_DATABASE_NAME_DEFAULT_NAME)); + setIsLoading(false); + }); + }); + } + }; + + useEffect(() => { + setIsLoading(false); + getSidebarContent(); + }, [selectedItems, refreshTree]); + + const handleDatabaseClick = (databaseName: string) => { + setSelectedDatabase(databaseName); + setIsLoading(true); + const query = { + lang: 'sql', + query: `SHOW TABLES IN ${selectedItems[0]['label']}.${databaseName}`, + datasource: selectedItems[0]['label'], + }; + getJobId(query, http, (id) => { + get_async_query_results(id, http, (data) => { + data = data.map((subArray) => subArray[1]); + let values = loadTreeItem(data, TREE_ITEM_TABLE_NAME_DEFAULT_NAME); + let mvObj = loadTreeItem( + [TREE_ITEM_LOAD_MATERIALIZED_BADGE_NAME], + TREE_ITEM_LOAD_MATERIALIZED_BADGE_NAME + ); + values = [...values, ...mvObj]; + setTreeData((prevTreeData) => { + return prevTreeData.map((database) => { + if (database.name === databaseName) { + return { ...database, values: values }; + } + return database; + }); + }); + setIsLoading(false); + }); + }); + }; + + const loadCoveringIndex = (tableName: string) => { + const coverQuery = { + lang: 'sql', + query: `SHOW INDEX ON ${selectedItems[0]['label']}.${selectedDatabase}.${tableName}`, + datasource: selectedItems[0]['label'], + }; + getJobId(coverQuery, http, (id) => { + get_async_query_results(id, http, (data) => { + const res = [].concat(data); + let coverIndexObj = loadTreeItem(res, TREE_ITEM_COVERING_INDEX_DEFAULT_NAME); + setTreeData((prevTreeData) => { + return prevTreeData.map((database) => { + if (database.name === selectedDatabase) { + return { + ...database, + values: database.values?.map((table) => { + if (table.name === tableName) { + return { + ...table, + values: table.values?.concat(...coverIndexObj), + }; + } + return table; + }), + }; + } + return database; + }); + }); + setIsLoading(false); + }); + }); + }; + + const handleButtonClick = (e: MouseEvent, tableName: string) => { + e.stopPropagation(); + setSelectedTable(tableName); + setIsLoading(true); + const materializedViewQuery = { + lang: 'sql', + query: `SHOW MATERIALIZED VIEW IN ${selectedItems[0]['label']}.${selectedDatabase}`, + datasource: selectedItems[0]['label'], + }; + getJobId(materializedViewQuery, http, (id) => { + get_async_query_results(id, http, (data) => { + data = data.map((subArray) => subArray[1]); + let values = loadTreeItem(data, TREE_ITEM_MATERIALIZED_VIEW_DEFAULT_NAME); + if (values.length === 0) { + values = [ + { name: 'No Materialized View', type: TREE_ITEM_BADGE_NAME, isExpanded: false }, + ]; + } + setTreeData((prevTreeData) => { + return prevTreeData.map((database) => { + if (database.name === selectedDatabase) { + const updatedValues = database.values?.filter( + (item) => item.type !== TREE_ITEM_LOAD_MATERIALIZED_BADGE_NAME + ); + return { ...database, values: updatedValues?.concat(...values) }; + } + return database; + }); + }); + setIsLoading(false); + }); + }); + }; + + const handleTableClick = (tableName: string) => { + setSelectedTable(tableName); + setIsLoading(true); + const skipQuery = { + lang: 'sql', + query: `DESC SKIPPING INDEX ON ${selectedItems[0]['label']}.${selectedDatabase}.${tableName}`, + datasource: selectedItems[0]['label'], + }; + getJobId(skipQuery, http, (id) => { + get_async_query_results(id, http, (data) => { + if (data.length > 0) { + setTreeData((prevTreeData) => { + return prevTreeData.map((database) => { + if (database.name === selectedDatabase) { + return { + ...database, + values: database.values?.map((table) => { + if (table.name === tableName) { + return { + ...table, + values: loadTreeItem( + [TREE_ITEM_SKIPPING_INDEX_DEFAULT_NAME], + TREE_ITEM_SKIPPING_INDEX_DEFAULT_NAME + ), + }; + } + return table; + }), + }; + } + return database; + }); + }); + } + loadCoveringIndex(tableName); + }); + }); + }; + + const createLabel = (node: TreeItem, parentName: string, index: number) => { + switch (node.type) { + case TREE_ITEM_BADGE_NAME: + return ( + <div key={`${parentName}.${node.name}.${index}`}> + <EuiToolTip position="right" content={node.name} delay="long"> + <EuiBadge>{_.truncate(node.name, { length: 50 })}</EuiBadge> + </EuiToolTip>{' '} + </div> + ); + + case TREE_ITEM_LOAD_MATERIALIZED_BADGE_NAME: + return ( + <div key={node.name}> + <EuiBadge color="hollow" onClick={handleButtonClick}> + Load Materialized View + </EuiBadge> + </div> + ); + + default: + return ( + <div key={node.name}> + <EuiToolTip position="right" content={node.name} delay="long"> + <EuiText>{_.truncate(node.name, { length: 50 })}</EuiText> + </EuiToolTip>{' '} + </div> + ); + } + }; + + const OpenSearchIndicesTree = tableNames.map((database, index) => ({ + label: ( + <div key={database}> + <EuiToolTip position="right" content={database} delay="long"> + <EuiText>{_.truncate(database, { length: 50 })}</EuiText> + </EuiToolTip>{' '} + </div> + ), + icon: <EuiIcon type="database" size="m" />, + id: 'element_' + index, + isSelectable: false, + })); + + const treeDataDatabases = treeData.map((database, index) => ({ + label: createLabel(database, selectedItems[0].label, index), + icon: <EuiIcon type="database" size="m" />, + id: 'element_' + index, + callback: () => { + if (database.values?.length === 0 && selectedItems[0].label !== 'OpenSearch') { + handleDatabaseClick(database.name); + } + }, + isSelectable: true, + isExpanded: database.isExpanded, + children: database.values?.map((table, index) => ({ + label: createLabel(table, database.name, index), + id: `${database.name}_${table.name}`, + icon: + table.type === TREE_ITEM_LOAD_MATERIALIZED_BADGE_NAME ? ( + <EuiBadge color="hollow">MV</EuiBadge> + ) : table.type === TREE_ITEM_BADGE_NAME ? null : ( + <EuiIcon type="tableDensityCompact" size="s" /> + ), + callback: () => { + if (table.type !== TREE_ITEM_LOAD_MATERIALIZED_BADGE_NAME && table.values?.length === 0) { + handleTableClick(table.name); + } + if (table.values?.length === 0) { + table.values = [{ name: 'No Indicies', type: TREE_ITEM_BADGE_NAME, isExpanded: false }]; + } + }, + isSelectable: true, + isExpanded: table.isExpanded, + children: table.values?.map((indexChild, index) => ({ + label: createLabel(indexChild, table.name, index), + id: `${database.name}_${table.name}_${indexChild.name}`, + icon: indexChild.type === TREE_ITEM_BADGE_NAME ? null : <EuiIcon type="bolt" size="s" />, + callback: () => { + if (indexChild.type !== TREE_ITEM_BADGE_NAME) { + handleAccelerationIndexClick( + selectedItems[0].label, + database.name, + table.name, + indexChild.name + ); + } + }, + })), + })), + })); + + return ( + <> + <EuiFlexGroup> + {isLoading ? ( + <EuiFlexGroup alignItems="center" gutterSize="s" direction="column"> + <EuiFlexItem> + <EuiLoadingSpinner size="l" /> + </EuiFlexItem> + <EuiFlexItem grow={false}>Loading data</EuiFlexItem> + <EuiFlexItem grow={false}> + <div style ={{ padding: '10px' }}> + <EuiText textAlign="center" color="subdued"> + Loading can take more than 30s. Queries can be made after the data has loaded. Any + queries run before the data is loaded will be queued + </EuiText> + </div> + </EuiFlexItem> + </EuiFlexGroup> + ) : OpenSearchIndicesTree.length > 0 || treeDataDatabases.length > 0 ? ( + <EuiFlexItem grow={false}> + {selectedItems[0].label === 'OpenSearch' ? ( + <EuiTreeView aria-label="Sample Folder Tree" items={OpenSearchIndicesTree} /> + ) : ( + <EuiTreeView aria-label="Sample Folder Tree" items={treeDataDatabases} /> + )} + </EuiFlexItem> + ) : ( + <EuiFlexItem grow={false}> + <EuiEmptyPrompt + iconType="alert" + iconColor="danger" + title={<h3>Error loading data</h3>} + /> + </EuiFlexItem> + )} + {indexFlyout} + </EuiFlexGroup> + </> + ); +}; diff --git a/test/mocks/mockData.ts b/test/mocks/mockData.ts index 65904eec..fee838f0 100644 --- a/test/mocks/mockData.ts +++ b/test/mocks/mockData.ts @@ -2349,4 +2349,20 @@ export const mockDatasourcesQuery = } } +export const mockJobId = { + data: { + ok: true, + resp: '{ "queryId": "00fe3fjpnfnn400q" }', + }, +}; + +export const mockOpenSearchIndicies = { + data: { + ok: true, + resp: + '{"schema":[{"name":"TABLE_CAT","type":"keyword"},{"name":"TABLE_SCHEM","type":"keyword"},{"name":"TABLE_NAME","type":"keyword"},{"name":"TABLE_TYPE","type":"keyword"},{"name":"REMARKS","type":"keyword"},{"name":"TYPE_CAT","type":"keyword"},{"name":"TYPE_SCHEM","type":"keyword"},{"name":"TYPE_NAME","type":"keyword"},{"name":"SELF_REFERENCING_COL_NAME","type":"keyword"},{"name":"REF_GENERATION","type":"keyword"}],"datarows":[["opensearch",null,".kibana_1","BASE TABLE",null,null,null,null,null,null],["opensearch",null,".kibana_2","BASE TABLE",null,null,null,null,null,null],["opensearch",null,".kibana_3","BASE TABLE",null,null,null,null,null,null],["opensearch",null,".opendistro-reports-definitions","BASE TABLE",null,null,null,null,null,null],["opensearch",null,".opendistro-reports-instances","BASE TABLE",null,null,null,null,null,null],["opensearch",null,".opensearch-observability","BASE TABLE",null,null,null,null,null,null],["opensearch",null,".plugins-ml-config","BASE TABLE",null,null,null,null,null,null],["opensearch",null,".ql-datasources","BASE TABLE",null,null,null,null,null,null],["opensearch",null,".ql-job-metadata","BASE TABLE",null,null,null,null,null,null],["opensearch",null,".query_execution_result","BASE TABLE",null,null,null,null,null,null],["opensearch",null,"flint_mys3_default_alb_logs_temp_10_test_index","BASE TABLE",null,null,null,null,null,null],["opensearch",null,"flint_mys3_default_alb_logs_temp_5_mys3_default_alb_logs_temp_5_index","BASE TABLE",null,null,null,null,null,null],["opensearch",null,"flint_mys3_default_elb_logs_regex_elb_logs_regex_index_index","BASE TABLE",null,null,null,null,null,null],["opensearch",null,"flint_mys3_default_empty_table_empty_table_index_index","BASE TABLE",null,null,null,null,null,null],["opensearch",null,"flint_mys3_default_http_logs_skipping_index","BASE TABLE",null,null,null,null,null,null],["opensearch",null,"flint_mys3_default_parquet_elb_logs_simple_parquet_index_simple_index","BASE TABLE",null,null,null,null,null,null],["opensearch",null,"opensearch_dashboards_sample_data_flights","BASE TABLE",null,null,null,null,null,null],["opensearch",null,"opensearch_dashboards_sample_data_logs","BASE TABLE",null,null,null,null,null,null],["opensearch",null,"ss4o_traces-elb-test","BASE TABLE",null,null,null,null,null,null],["opensearch",null,".flint-elv-mv","BASE TABLE",null,null,null,null,null,null],["opensearch",null,".kibana","BASE TABLE",null,null,null,null,null,null]],"total":21,"size":21,"status":200}', + }, +}; + +