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

Added changes for making tree view persistent, made changes for bugs for loading screen #153

Merged
merged 9 commits into from
Oct 20, 2023
10 changes: 8 additions & 2 deletions common/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ export const PLUGIN_NAME = 'Query Workbench';
export const OPENSEARCH_ACC_DOCUMENTATION_URL = 'https://opensearch.org/docs/latest';
export const ACC_INDEX_TYPE_DOCUMENTATION_URL = 'https://opensearch.org/docs/latest';

export const SKIPPING_INDEX = `skipping_index`;
export const ON_LOAD_QUERY = `SHOW tables LIKE '%';`;
export const SKIPPING_INDEX_NAME = `skipping_index`;
ps48 marked this conversation as resolved.
Show resolved Hide resolved
export const COVERING_INDEX_NAME = `covering_index`;
ps48 marked this conversation as resolved.
Show resolved Hide resolved
export const DATABASE_NAME = `database`;
export const TABLE_NAME = `table`;
export const INDICIES_NAME = `indicies`
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 (
Expand Down Expand Up @@ -82,3 +86,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'
10 changes: 9 additions & 1 deletion common/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,12 @@ export interface CreateAccelerationForm {
formErrors: FormErrorsType;
}

export type AsyncQueryLoadingStatus = "SUCCESS" | "FAILED" | "RUNNING" | "SCHEDULED" | "CANCELED"
export type AsyncQueryLoadingStatus = 'SUCCESS' | 'FAILED' | 'RUNNING' | 'SCHEDULED' | 'CANCELED';
ps48 marked this conversation as resolved.
Show resolved Hide resolved
export type Tree = 'covering_index' | 'skipping_index' | 'table' | 'database' | 'materialized_view';

export interface TreeItem {
name: string;
type: Tree;
isExpanded: boolean;
values?: TreeItem[];
}
252 changes: 141 additions & 111 deletions public/components/SQLPage/TableView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,18 @@ import {
EuiToolTip,
EuiTreeView,
} from '@elastic/eui';
import { Tree, TreeItem } from 'common/types';
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 {
COVERING_INDEX_NAME,
DATABASE_NAME,
FETCH_OPENSEARCH_INDICES_PATH,
LOAD_OPENSEARCH_INDICES_QUERY,
SKIPPING_INDEX_NAME,
TABLE_NAME,
} from '../../../common/constants';
import { AccelerationIndexFlyout } from './acceleration_index_flyout';
import { getJobId, pollQueryStatus } from './utils';

Expand All @@ -28,17 +36,12 @@ interface CustomView {
}

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 [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 [childLoadingStates, setChildLoadingStates] = useState<{ [key: string]: boolean }>({});
const [tableLoadingStates, setTableLoadingStates] = useState<{ [key: string]: boolean }>({});

let indiciesData: string[] = [];
const [treeData, setTreeData] = useState<TreeItem[]>([]);

const resetFlyout = () => {
setIndexFlyout(<></>);
Expand All @@ -62,40 +65,62 @@ export const TableView = ({ http, selectedItems, updateSQLQueries }: CustomView)
);
};

function loadTreeItem(elements: string[], type: Tree): TreeItem[] {
return elements.map((element) => {
let treeItem: TreeItem = {
name: element,
type: type,
isExpanded: true,
};

if (type === DATABASE_NAME || type === TABLE_NAME) {
treeItem.values = [];
treeItem.isExpanded = true;
} else if (type === SKIPPING_INDEX_NAME) {
treeItem.type = SKIPPING_INDEX_NAME;
} else {
treeItem.type = COVERING_INDEX_NAME;
}

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: ON_LOAD_QUERY };
setTableNames([]);
const query = { query: LOAD_OPENSEARCH_INDICES_QUERY };
http
.post(`/api/sql_console/sqlquery`, {
.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) => {
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);
setTableNames(fields);
})
.catch((err) => {
console.error(err);
});
} else {
setIsLoading(true);
setTablenames([]);
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);
data = [].concat(...data);
setTreeData(loadTreeItem(data, DATABASE_NAME));
setIsLoading(false);
});
});
Expand All @@ -107,148 +132,153 @@ export const TableView = ({ http, selectedItems, updateSQLQueries }: CustomView)
getSidebarContent();
}, [selectedItems]);

const handleNodeClick = (nodeLabel: string) => {
setSelectedNode(nodeLabel);
const handleDatabaseClick = (databaseName: string) => {
setSelectedDatabase(databaseName);
setIsLoading(true);
const query = {
lang: 'sql',
query: `SHOW TABLES IN ${selectedItems[0]['label']}.${nodeLabel}`,
query: `SHOW TABLES IN ${selectedItems[0]['label']}.${databaseName}`,
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 values = loadTreeItem(data, TABLE_NAME);
treeData.map((database) => {
if (database.name === databaseName) {
database.values = values;
}
});
sumukhswamy marked this conversation as resolved.
Show resolved Hide resolved
setIsLoading(false);
});
});
};

const callCoverQuery = (nodeLabel1: string) => {
const loadCoveringIndex = (tableName: string) => {
const coverQuery = {
lang: 'sql',
query: `SHOW INDEX ON ${selectedItems[0]['label']}.${selectedNode}.${nodeLabel1}`,
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);
const final = indiciesData.concat(...res);
setIndexedData(final);
setChildLoadingStates((prevState) => ({
...prevState,
[nodeLabel1]: false,
}));
let coverIndexObj = loadTreeItem(res, COVERING_INDEX_NAME);
treeData.map((database) => {
if (database.name === selectedDatabase) {
database.values.map((table) => {
if (table.name === tableName) {
table.values = table.values.concat(...coverIndexObj);
}
});
ps48 marked this conversation as resolved.
Show resolved Hide resolved
}
});
setIsLoading(false);
});
});
};
const handleChildClick = (nodeLabel1: string) => {
setSelectedChildNode(nodeLabel1);

const handleTableClick = (tableName: string) => {
setSelectedTable(tableName);
setIsLoading(true);
const skipQuery = {
lang: 'sql',
query: `DESC SKIPPING INDEX ON ${selectedItems[0]['label']}.${selectedNode}.${nodeLabel1}`,
query: `DESC SKIPPING INDEX ON ${selectedItems[0]['label']}.${selectedDatabase}.${tableName}`,
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);
treeData.map((database) => {
if (database.name === selectedDatabase) {
database.values.map((table) => {
if (table.name === tableName) {
table.values = loadTreeItem([SKIPPING_INDEX_NAME], SKIPPING_INDEX_NAME);
}
});
}
});
}
callCoverQuery(nodeLabel1);
loadCoveringIndex(tableName);
});
});
};

const treeData = tablenames.map((database, index) => ({
label: (
<div>
<EuiToolTip position="right" content={database} delay="long">
<EuiText>{_.truncate(database, { length: 50 })}</EuiText>
const labelCreation = (name: string) => {
ps48 marked this conversation as resolved.
Show resolved Hide resolved
return (
<div key={name}>
ps48 marked this conversation as resolved.
Show resolved Hide resolved
<EuiToolTip position="right" content={name} delay="long">
<EuiText>{_.truncate(name, { length: 50 })}</EuiText>
</EuiToolTip>{' '}
{tableLoadingStates[database] && <EuiLoadingSpinner size="m" />}
</div>
),
);
};

const OpenSearchIndicesTree = tableNames.map((database, index) => ({
label: labelCreation(database),
icon: <EuiIcon type="database" size="m" />,
id: 'element_' + index,
}));

const treeDataDatabases = treeData.map((database, index) => ({
label: labelCreation(database.name),
icon: <EuiIcon type="database" size="m" />,
id: 'element_' + index,
callback: () => {
setChildData([]);
selectedItems[0].label !== 'OpenSearch' && handleNodeClick(database);
if (database.values.length === 0 && selectedItems[0].label !== 'OpenSearch') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we checking for database.values.length === 0 here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the handle call to the function happens if it checks the corresponding list is empty or not wither for tables or indicies

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean, once a value list is loaded for a treeitem it cannot be reloaded? Is that the expected behaviour? You might want to check with the UX. Otherwise only option to reload the tree is to refresh the whole page.

handleDatabaseClick(database.name);
}
},
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,
children: database.values.map((table) => ({
label: labelCreation(table.name),
id: `${database.name}_${table.name}`,
icon: <EuiIcon type="tableDensityCompact" size="s" />,
callback: () => {
if (table.values.length === 0) {
handleTableClick(table.name);
}
},
isSelectable: true,
isExpanded: true,
ps48 marked this conversation as resolved.
Show resolved Hide resolved
children: table.values.map((indexChild) => ({
label: labelCreation(indexChild.name),
id: `${database.name}_${table.name}_${indexChild.name}`,
icon: <EuiIcon type="bolt" size="s" />,
callback: () =>
handleAccelerationIndexClick(
selectedItems[0].label,
database.name,
table.name,
indexChild.name
),
})),
})),
}));

return (
<>
<EuiFlexGroup>
{isLoading ? (
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>Loading databases</EuiFlexItem>
<EuiFlexGroup alignItems="center" gutterSize="s" direction="column">
<EuiFlexItem>
<EuiLoadingSpinner size="m" />
<EuiLoadingSpinner size="l" />
</EuiFlexItem>
<EuiFlexItem grow={false}>Loading databases</EuiFlexItem>
<EuiFlexItem grow={false}>
<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>
</EuiFlexItem>
</EuiFlexGroup>
) : treeData.length > 0 ? (
) : OpenSearchIndicesTree.length > 0 || treeDataDatabases.length > 0 ? (
<EuiFlexItem grow={false}>
<EuiTreeView aria-label="Sample Folder Tree" items={treeData} />
{selectedItems[0].label === 'OpenSearch' ? (
<EuiTreeView aria-label="Sample Folder Tree" items={OpenSearchIndicesTree} />
) : (
<EuiTreeView aria-label="Sample Folder Tree" items={treeDataDatabases} />
)}
</EuiFlexItem>
) : (
<EuiFlexItem grow={false}>
Expand Down
Loading