+ const queryId = query.queryId;
+ const tabs = [
+ {path: 'query.html', label: 'Overview'},
+ {path: 'plan.html', label: 'Live Plan'},
+ {path: 'stage.html', label: 'Stage Performance'},
+ {path: 'timeline.html', label: 'Splits'},
+ ];
+
+ return (
+
+
+
+
+ {query.queryId}
+
+
+
+
-
-
-
- {this.renderProgressBar()}
-
+
- );
- }
-}
+
+
+
+ {renderProgressBar()}
+
+
+
+ );
+};
diff --git a/presto-ui/src/components/QueryList.jsx b/presto-ui/src/components/QueryList.jsx
index 52ae8e2b9d8d0..e7b2bfa380b5b 100644
--- a/presto-ui/src/components/QueryList.jsx
+++ b/presto-ui/src/components/QueryList.jsx
@@ -12,7 +12,7 @@
* limitations under the License.
*/
-import React from "react";
+import React, { useState, useEffect, useRef, useCallback } from "react";
import {
formatCount,
@@ -29,7 +29,7 @@ import {
truncateString
} from "../utils";
-function getHumanReadableStateFromInfo(query) {
+const getHumanReadableStateFromInfo = (query) => {
const progress = query.progress;
return getHumanReadableState(
query.queryState,
@@ -40,10 +40,10 @@ function getHumanReadableStateFromInfo(query) {
query.errorCode ? query.errorCode.type : null,
query.errorCode ? query.errorCode.name : null
);
-}
+};
-function ResourceGroupLinks({groupId, length=35}) {
+const ResourceGroupLinks = ({groupId, length=35}) => {
if (!groupId?.length) return ('n/a');
let previousLen = 0;
@@ -68,16 +68,15 @@ function ResourceGroupLinks({groupId, length=35}) {
return (
<>{links}>
);
-}
+};
-export class QueryListItem extends React.Component {
- static stripQueryTextWhitespace(queryText, isTruncated) {
- const lines = queryText.split("\n");
- let minLeadingWhitespace = -1;
- for (let i = 0; i < lines.length; i++) {
- if (minLeadingWhitespace === 0) {
- break;
- }
+const stripQueryTextWhitespace = (queryText, isTruncated) => {
+ const lines = queryText.split("\n");
+ let minLeadingWhitespace = -1;
+ for (let i = 0; i < lines.length; i++) {
+ if (minLeadingWhitespace === 0) {
+ break;
+ }
if (lines[i].trim().length === 0) {
continue;
@@ -117,30 +116,28 @@ export class QueryListItem extends React.Component {
}
}
- return isTruncated ? formattedQueryText + "..." : truncateString(formattedQueryText, maxQueryLength);
- }
+ return isTruncated ? formattedQueryText + "..." : truncateString(formattedQueryText, maxQueryLength);
+};
- renderWarning() {
- const query = this.props.query;
+export const QueryListItem = ({ query }) => {
+ const renderWarning = () => {
if (query.warningCodes && query.warningCodes.length) {
return (
);
}
- }
+ };
- render() {
- const query = this.props.query;
- const queryStateColor = getQueryStateColor(
- query.queryState,
- query.progress && query.progress.blocked,
- query.errorCode ? query.errorCode.type : null,
- query.errorCode ? query.errorCode.name : null
- );
- const progressPercentage = getProgressBarPercentage(query.progress.progressPercentage, query.queryState);
- const progressBarStyle = {width: progressPercentage + "%", backgroundColor: queryStateColor};
- const humanReadableState = getHumanReadableStateFromInfo(query);
- const progressBarTitle = getProgressBarTitle(query.progress.progressPercentage, query.queryState, humanReadableState);
+ const queryStateColor = getQueryStateColor(
+ query.queryState,
+ query.progress && query.progress.blocked,
+ query.errorCode ? query.errorCode.type : null,
+ query.errorCode ? query.errorCode.name : null
+ );
+ const progressPercentage = getProgressBarPercentage(query.progress.progressPercentage, query.queryState);
+ const progressBarStyle = {width: progressPercentage + "%", backgroundColor: queryStateColor};
+ const humanReadableState = getHumanReadableStateFromInfo(query);
+ const progressBarTitle = getProgressBarTitle(query.progress.progressPercentage, query.queryState, humanReadableState);
const driverDetails = (
@@ -229,103 +226,99 @@ export class QueryListItem extends React.Component {
);
}
- return (
-
-
-
-
-
-
- {formatShortTime(new Date(Date.parse(query.createTime)))}
-
+ return (
+
+
+
+
+
-
-
-
-
- {user}
-
-
+
+ {formatShortTime(new Date(Date.parse(query.createTime)))}
-
-
-
-
- {truncateString(query.source, 35)}
-
-
+
+
+
+
+
+ {user}
+
-
-
-
-
-
-
-
+
+
+
+
+
+ {truncateString(query.source, 35)}
+
+
+
+
+
- { query.progress.completedSplits ?
- <>
-
- {newDriverDetails}
-
-
- {splitDetails}
-
- > :
+ { query.progress.completedSplits ?
+ <>
- {driverDetails}
+ {newDriverDetails}
- }
-
- {timingDetails}
-
+
+ {splitDetails}
+
+ > :
- {memoryDetails}
+ {driverDetails}
+ }
+
+ {timingDetails}
-
-
-
-
-
- {progressBarTitle}
-
+
+ {memoryDetails}
+
+
+
+
+
-
-
-
{QueryListItem.stripQueryTextWhitespace(query.query, query.queryTruncated)}
-
+
+
+
+
{stripQueryTextWhitespace(query.query, query.queryTruncated)}
- );
- }
-}
+
+ );
+};
+
+const DisplayedQueriesList = ({ queries }) => {
+ const queryNodes = queries.map((query) => (
+
+ ));
+ return (
+
+ {queryNodes}
+
+ );
+};
-class DisplayedQueriesList extends React.Component {
- render() {
- const queryNodes = this.props.queries.map(function (query) {
- return (
-
- );
- }.bind(this));
- return (
-
- {queryNodes}
-
- );
- }
-}
const FILTER_TYPE = {
RUNNING: function (query) {
@@ -356,61 +349,68 @@ const SORT_ORDER = {
DESCENDING: function (value) {return -value}
};
-export class QueryList extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- allQueries: [],
- displayedQueries: [],
- reorderInterval: 5000,
- currentSortType: SORT_TYPE.CREATED,
- currentSortOrder: SORT_ORDER.DESCENDING,
- stateFilters: [FILTER_TYPE.RUNNING, FILTER_TYPE.QUEUED],
- errorTypeFilters: [ERROR_TYPE.INTERNAL_ERROR, ERROR_TYPE.INSUFFICIENT_RESOURCES, ERROR_TYPE.EXTERNAL],
- searchString: '',
- maxQueries: 100,
- lastRefresh: Date.now(),
- lastReorder: Date.now(),
- initialized: false
- };
+export const QueryList = () => {
+ const [state, setState] = useState({
+ allQueries: [],
+ displayedQueries: [],
+ reorderInterval: 5000,
+ currentSortType: SORT_TYPE.CREATED,
+ currentSortOrder: SORT_ORDER.DESCENDING,
+ stateFilters: [FILTER_TYPE.RUNNING, FILTER_TYPE.QUEUED],
+ errorTypeFilters: [ERROR_TYPE.INTERNAL_ERROR, ERROR_TYPE.INSUFFICIENT_RESOURCES, ERROR_TYPE.EXTERNAL],
+ searchString: '',
+ maxQueries: 100,
+ lastRefresh: Date.now(),
+ lastReorder: Date.now(),
+ initialized: false,
+ });
- this.refreshLoop = this.refreshLoop.bind(this);
- this.handleSearchStringChange = this.handleSearchStringChange.bind(this);
- this.executeSearch = this.executeSearch.bind(this);
- this.handleSortClick = this.handleSortClick.bind(this);
- }
+ const timeoutId = useRef(null);
+ const searchTimeoutId = useRef(null);
+ const dataSet = useRef({
+ allQueries: [],
+ displayedQueries: [],
+ reorderInterval: 5000,
+ currentSortType: SORT_TYPE.CREATED,
+ currentSortOrder: SORT_ORDER.DESCENDING,
+ stateFilters: [FILTER_TYPE.RUNNING, FILTER_TYPE.QUEUED],
+ errorTypeFilters: [ERROR_TYPE.INTERNAL_ERROR, ERROR_TYPE.INSUFFICIENT_RESOURCES, ERROR_TYPE.EXTERNAL],
+ searchString: '',
+ maxQueries: 100,
+ lastReorder: Date.now(),
+ });
- sortAndLimitQueries(queries, sortType, sortOrder, maxQueries) {
- queries.sort(function (queryA, queryB) {
+ const sortAndLimitQueries = (queries, sortType, sortOrder, maxQueriesValue) => {
+ queries.sort((queryA, queryB) => {
return sortOrder(sortType(queryA) - sortType(queryB));
- }, this);
+ });
- if (maxQueries !== 0 && queries.length > maxQueries) {
- queries.splice(maxQueries, (queries.length - maxQueries));
+ if (maxQueriesValue !== 0 && queries.length > maxQueriesValue) {
+ queries.splice(maxQueriesValue, (queries.length - maxQueriesValue));
}
- }
+ };
- filterQueries(queries, stateFilters, errorTypeFilters, searchString) {
- const stateFilteredQueries = queries.filter(function (query) {
- for (let i = 0; i < stateFilters.length; i++) {
- if (stateFilters[i](query)) {
+ const filterQueries = (queries, stateFiltersValue, errorTypeFiltersValue, searchStringValue) => {
+ const stateFilteredQueries = queries.filter((query) => {
+ for (let i = 0; i < stateFiltersValue.length; i++) {
+ if (stateFiltersValue[i](query)) {
return true;
}
}
- for (let i = 0; i < errorTypeFilters.length; i++) {
- if (errorTypeFilters[i](query)) {
+ for (let i = 0; i < errorTypeFiltersValue.length; i++) {
+ if (errorTypeFiltersValue[i](query)) {
return true;
}
}
return false;
});
- if (searchString === '') {
+ if (searchStringValue === '') {
return stateFilteredQueries;
}
else {
- return stateFilteredQueries.filter(function (query) {
- const term = searchString.toLowerCase();
+ return stateFilteredQueries.filter((query) => {
+ const term = searchStringValue.toLowerCase();
const humanReadableState = getHumanReadableStateFromInfo(query);
if (query.queryId.toLowerCase().indexOf(term) !== -1 ||
humanReadableState.toLowerCase().indexOf(term) !== -1 ||
@@ -430,27 +430,17 @@ export class QueryList extends React.Component {
return true;
}
- return query.warningCodes.some(function (warning) {
+ return query.warningCodes.some((warning) => {
if ("warning".indexOf(term) !== -1 || warning.indexOf(term) !== -1) {
return true;
}
});
-
- }, this);
- }
- }
-
- resetTimer() {
- clearTimeout(this.timeoutId);
- // stop refreshing when query finishes or fails
- if (this.state.query === null || !this.state.ended) {
- this.timeoutId = setTimeout(this.refreshLoop, 1000);
+ });
}
- }
+ };
- refreshLoop() {
- clearTimeout(this.timeoutId); // to stop multiple series of refreshLoop from going on simultaneously
- clearTimeout(this.searchTimeoutId);
+ const refreshLoop = useCallback(() => {
+ clearTimeout(timeoutId.current);
$.get('/v1/queryState?includeAllQueries=true&includeAllQueryProgressStats=true&excludeResourceGroupPathInfo=true', function (queryList) {
const queryMap = queryList.reduce(function (map, query) {
@@ -459,7 +449,7 @@ export class QueryList extends React.Component {
}, {});
let updatedQueries = [];
- this.state.displayedQueries.forEach(function (oldQuery) {
+ (dataSet.current.displayedQueries || []).forEach(function (oldQuery) {
if (oldQuery.queryId in queryMap) {
updatedQueries.push(queryMap[oldQuery.queryId]);
queryMap[oldQuery.queryId] = false;
@@ -472,108 +462,132 @@ export class QueryList extends React.Component {
newQueries.push(queryMap[queryId]);
}
}
- newQueries = this.filterQueries(newQueries, this.state.stateFilters, this.state.errorTypeFilters, this.state.searchString);
+ newQueries = filterQueries(newQueries, dataSet.current.stateFilters, dataSet.current.errorTypeFilters, dataSet.current.searchString);
- const lastRefresh = Date.now();
- let lastReorder = this.state.lastReorder;
+ const newLastRefresh = Date.now();
+ let newLastReorder = dataSet.current.lastReorder;
- if (this.state.reorderInterval !== 0 && ((lastRefresh - lastReorder) >= this.state.reorderInterval)) {
- updatedQueries = this.filterQueries(updatedQueries, this.state.stateFilters, this.state.errorTypeFilters, this.state.searchString);
+ if (dataSet.current.reorderInterval !== 0 && ((newLastRefresh - newLastReorder) >= dataSet.current.reorderInterval)) {
+ updatedQueries = filterQueries(updatedQueries, dataSet.current.stateFilters, dataSet.current.errorTypeFilters, dataSet.current.searchString);
updatedQueries = updatedQueries.concat(newQueries);
- this.sortAndLimitQueries(updatedQueries, this.state.currentSortType, this.state.currentSortOrder, 0);
- lastReorder = Date.now();
+ sortAndLimitQueries(updatedQueries, dataSet.current.currentSortType, dataSet.current.currentSortOrder, 0);
+ newLastReorder = Date.now();
}
else {
- this.sortAndLimitQueries(newQueries, this.state.currentSortType, this.state.currentSortOrder, 0);
+ sortAndLimitQueries(newQueries, dataSet.current.currentSortType, dataSet.current.currentSortOrder, 0);
updatedQueries = updatedQueries.concat(newQueries);
}
- if (this.state.maxQueries !== 0 && (updatedQueries.length > this.state.maxQueries)) {
- updatedQueries.splice(this.state.maxQueries, (updatedQueries.length - this.state.maxQueries));
+ if (dataSet.current.maxQueries !== 0 && (updatedQueries.length > dataSet.current.maxQueries)) {
+ updatedQueries.splice(dataSet.current.maxQueries, (updatedQueries.length - dataSet.current.maxQueries));
}
- this.setState({
+ dataSet.current.allQueries = queryList;
+ dataSet.current.displayedQueries = updatedQueries;
+ dataSet.current.lastReorder = newLastReorder;
+
+ setState(prev => ({
+ ...prev,
allQueries: queryList,
displayedQueries: updatedQueries,
- lastRefresh: lastRefresh,
- lastReorder: lastReorder,
- initialized: true
- });
- this.resetTimer();
- }.bind(this))
+ lastRefresh: newLastRefresh,
+ lastReorder: newLastReorder,
+ initialized: true,
+ }));
+
+ timeoutId.current = setTimeout(refreshLoop, 1000);
+ })
.fail(function () {
- this.setState({
- initialized: true,
- });
- this.resetTimer();
- }.bind(this));
- }
+ setState(prev => ({ ...prev, initialized: true }));
+ timeoutId.current = setTimeout(refreshLoop, 1000);
+ });
+ }, []);
- componentDidMount() {
- this.refreshLoop();
+ useEffect(() => {
+ refreshLoop();
$('[data-bs-toggle="tooltip"]')?.tooltip?.();
- }
- handleSearchStringChange(event) {
- const newSearchString = event.target.value;
- clearTimeout(this.searchTimeoutId);
+ return () => {
+ clearTimeout(timeoutId.current);
+ clearTimeout(searchTimeoutId.current);
+ };
+ }, [refreshLoop]);
- this.setState({
- searchString: newSearchString
- });
+ const executeSearch = useCallback(() => {
+ clearTimeout(searchTimeoutId.current);
- this.searchTimeoutId = setTimeout(this.executeSearch, 200);
- }
+ // Use latest values from dataSet to avoid stale closures during debounce
+ const newDisplayedQueries = filterQueries(dataSet.current.allQueries, dataSet.current.stateFilters, dataSet.current.errorTypeFilters, dataSet.current.searchString);
+ sortAndLimitQueries(newDisplayedQueries, dataSet.current.currentSortType, dataSet.current.currentSortOrder, dataSet.current.maxQueries);
- executeSearch() {
- clearTimeout(this.searchTimeoutId);
+ dataSet.current.displayedQueries = newDisplayedQueries;
+ setState(prev => ({ ...prev, displayedQueries: newDisplayedQueries }));
+ }, []);
- const newDisplayedQueries = this.filterQueries(this.state.allQueries, this.state.stateFilters, this.state.errorTypeFilters, this.state.searchString);
- this.sortAndLimitQueries(newDisplayedQueries, this.state.currentSortType, this.state.currentSortOrder, this.state.maxQueries);
+ const handleSearchStringChange = (event) => {
+ const newSearchString = event.target.value;
+ clearTimeout(searchTimeoutId.current);
- this.setState({
- displayedQueries: newDisplayedQueries
- });
- }
+ // Update state and ref immediately for debounce/readers
+ dataSet.current.searchString = newSearchString;
+ setState(prev => ({ ...prev, searchString: newSearchString }));
+
+ searchTimeoutId.current = setTimeout(executeSearch, 200);
+ };
- renderMaxQueriesListItem(maxQueries, maxQueriesText) {
+ const handleMaxQueriesClick = (newMaxQueries) => {
+ const filteredQueries = filterQueries(dataSet.current.allQueries, dataSet.current.stateFilters, dataSet.current.errorTypeFilters, dataSet.current.searchString);
+ sortAndLimitQueries(filteredQueries, dataSet.current.currentSortType, dataSet.current.currentSortOrder, newMaxQueries);
+
+ dataSet.current.maxQueries = newMaxQueries;
+ dataSet.current.displayedQueries = filteredQueries;
+ setState(prev => ({ ...prev, maxQueries: newMaxQueries, displayedQueries: filteredQueries }));
+ };
+
+ const renderMaxQueriesListItem = (maxQueriesValue, maxQueriesText) => {
return (
- {maxQueriesText}
+ handleMaxQueriesClick(maxQueriesValue)}>{maxQueriesText}
);
- }
+ };
- handleMaxQueriesClick(newMaxQueries) {
- const filteredQueries = this.filterQueries(this.state.allQueries, this.state.stateFilters, this.state.errorTypeFilters, this.state.searchString);
- this.sortAndLimitQueries(filteredQueries, this.state.currentSortType, this.state.currentSortOrder, newMaxQueries);
-
- this.setState({
- maxQueries: newMaxQueries,
- displayedQueries: filteredQueries
- });
- }
+ const handleReorderClick = (interval) => {
+ if (dataSet.current.reorderInterval !== interval) {
+ dataSet.current.reorderInterval = interval;
+ setState(prev => ({ ...prev, reorderInterval: interval }));
+ }
+ };
- renderReorderListItem(interval, intervalText) {
+ const renderReorderListItem = (interval, intervalText) => {
return (
- {intervalText}
+ handleReorderClick(interval)}>{intervalText}
);
- }
+ };
- handleReorderClick(interval) {
- if (this.state.reorderInterval !== interval) {
- this.setState({
- reorderInterval: interval,
- });
+ const handleSortClick = (sortType) => {
+ const newSortType = sortType;
+ let newSortOrder = SORT_ORDER.DESCENDING;
+
+ if (state.currentSortType === sortType && state.currentSortOrder === SORT_ORDER.DESCENDING) {
+ newSortOrder = SORT_ORDER.ASCENDING;
}
- }
- renderSortListItem(sortType, sortText) {
- if (this.state.currentSortType === sortType) {
- const directionArrow = this.state.currentSortOrder === SORT_ORDER.ASCENDING ? :
+ const newDisplayedQueries = filterQueries(dataSet.current.allQueries, dataSet.current.stateFilters, dataSet.current.errorTypeFilters, dataSet.current.searchString);
+ sortAndLimitQueries(newDisplayedQueries, newSortType, newSortOrder, dataSet.current.maxQueries);
+
+ dataSet.current.displayedQueries = newDisplayedQueries;
+ dataSet.current.currentSortType = newSortType;
+ dataSet.current.currentSortOrder = newSortOrder;
+ setState(prev => ({ ...prev, displayedQueries: newDisplayedQueries, currentSortType: newSortType, currentSortOrder: newSortOrder }));
+ };
+
+ const renderSortListItem = (sortType, sortText) => {
+ if (state.currentSortType === sortType) {
+ const directionArrow = state.currentSortOrder === SORT_ORDER.ASCENDING ? :
;
return (
-
+ handleSortClick(sortType)}>
{sortText} {directionArrow}
);
@@ -581,188 +595,166 @@ export class QueryList extends React.Component {
else {
return (
-
+ handleSortClick(sortType)}>
{sortText}
);
}
- }
-
- handleSortClick(sortType) {
- const newSortType = sortType;
- let newSortOrder = SORT_ORDER.DESCENDING;
+ };
- if (this.state.currentSortType === sortType && this.state.currentSortOrder === SORT_ORDER.DESCENDING) {
- newSortOrder = SORT_ORDER.ASCENDING;
+ const handleStateFilterClick = (filter) => {
+ const newFilters = state.stateFilters.slice();
+ if (state.stateFilters.indexOf(filter) > -1) {
+ newFilters.splice(newFilters.indexOf(filter), 1);
+ }
+ else {
+ newFilters.push(filter);
}
- const newDisplayedQueries = this.filterQueries(this.state.allQueries, this.state.stateFilters, this.state.errorTypeFilters, this.state.searchString);
- this.sortAndLimitQueries(newDisplayedQueries, newSortType, newSortOrder, this.state.maxQueries);
+ const filteredQueries = filterQueries(dataSet.current.allQueries, newFilters, dataSet.current.errorTypeFilters, dataSet.current.searchString);
+ sortAndLimitQueries(filteredQueries, dataSet.current.currentSortType, dataSet.current.currentSortOrder, dataSet.current.maxQueries);
- this.setState({
- displayedQueries: newDisplayedQueries,
- currentSortType: newSortType,
- currentSortOrder: newSortOrder
- });
- }
+ dataSet.current.stateFilters = newFilters;
+ dataSet.current.displayedQueries = filteredQueries;
+ setState(prev => ({ ...prev, stateFilters: newFilters, displayedQueries: filteredQueries }));
+ };
- renderFilterButton(filterType, filterText) {
+ const renderFilterButton = (filterType, filterText) => {
let checkmarkStyle = {color: '#57aac7'};
let classNames = "btn btn-sm btn-info style-check rounded-0";
- if (this.state.stateFilters.indexOf(filterType) > -1) {
+ if (state.stateFilters.indexOf(filterType) > -1) {
classNames += " active";
checkmarkStyle = {color: '#ffffff'};
}
return (
-