diff --git a/presto-ui/src/components/QueryDetail.jsx b/presto-ui/src/components/QueryDetail.jsx index 36687ba82286b..229e285d362db 100644 --- a/presto-ui/src/components/QueryDetail.jsx +++ b/presto-ui/src/components/QueryDetail.jsx @@ -12,7 +12,7 @@ * limitations under the License. */ -import React from "react"; +import React, { useState, useEffect, useRef } from "react"; import DataTable, {createTheme} from 'react-data-table-component'; import { @@ -349,29 +349,22 @@ const HISTOGRAM_PROPERTIES = { disableHiddenCheck: true, }; -class RuntimeStatsList extends React.Component { - constructor(props) { - super(props); - this.state = { - expanded: false - }; - } +const RuntimeStatsList = ({ stats }) => { + const [expanded, setExpanded] = React.useState(false); - getExpandedIcon() { - return this.state.expanded ? "bi bi-chevron-up" : "bi bi-chevron-down"; - } + const getExpandedIcon = () => { + return expanded ? "bi bi-chevron-up" : "bi bi-chevron-down"; + }; - getExpandedStyle() { - return this.state.expanded ? {} : {display: "none"}; - } + const getExpandedStyle = () => { + return expanded ? {} : {display: "none"}; + }; - toggleExpanded() { - this.setState({ - expanded: !this.state.expanded, - }) - } + const toggleExpanded = () => { + setExpanded(!expanded); + }; - renderMetricValue(unit, value) { + const renderMetricValue = (unit, value) => { if (unit === "NANO") { return formatDuration(parseDuration(value + "ns")); } @@ -379,69 +372,52 @@ class RuntimeStatsList extends React.Component { return formatDataSize(value); } return formatCount(value); // NONE - } - - render() { - return ( - - - - - - - - - - - { - Object - .values(this.props.stats) - .sort((m1, m2) => (m1.name.localeCompare(m2.name))) - .map((metric) => - - - - - - - - ) - } - -
Metric NameSumCountMinMax - - - -
{metric.name}{this.renderMetricValue(metric.unit, metric.sum)}{formatCount(metric.count)}{this.renderMetricValue(metric.unit, metric.min)}{this.renderMetricValue(metric.unit, metric.max)}
- ); - } -} - -class StageSummary extends React.Component { - constructor(props) { - super(props); - this.state = { - expanded: false, - lastRender: null, - taskFilter: TASK_FILTER.ALL - }; - } + }; - getExpandedIcon() { - return this.state.expanded ? "bi bi-chevron-up" : "bi bi-chevron-down"; - } + return ( + + + + + + + + + + + { + Object + .values(stats) + .sort((m1, m2) => (m1.name.localeCompare(m2.name))) + .map((metric) => + + + + + + + + ) + } + +
Metric NameSumCountMinMax + + + +
{metric.name}{renderMetricValue(metric.unit, metric.sum)}{formatCount(metric.count)}{renderMetricValue(metric.unit, metric.min)}{renderMetricValue(metric.unit, metric.max)}
+ ); +}; - getExpandedStyle() { - return this.state.expanded ? {} : {display: "none"}; - } +const StageSummary = ({ stage }) => { + const [expanded, setExpanded] = useState(false); + const lastRenderRef = useRef(null); + const [taskFilter, setTaskFilter] = useState(TASK_FILTER.ALL); - toggleExpanded() { - this.setState({ - expanded: !this.state.expanded, - }) - } + const getExpandedIcon = () => (expanded ? "bi bi-chevron-up" : "bi bi-chevron-down"); + const getExpandedStyle = () => (expanded ? {} : { display: "none" }); + const toggleExpanded = () => setExpanded(!expanded); - static renderHistogram(histogramId, inputData, numberFormatter) { + const renderHistogram = (histogramId, inputData, numberFormatter) => { const numBuckets = Math.min(HISTOGRAM_WIDTH, Math.sqrt(inputData.length)); const dataMin = Math.min.apply(null, inputData); const dataMax = Math.max.apply(null, inputData); @@ -450,12 +426,10 @@ class StageSummary extends React.Component { let histogramData = []; if (bucketSize === 0) { histogramData = [inputData.length]; - } - else { + } else { for (let i = 0; i < numBuckets + 1; i++) { histogramData.push(0); } - for (let i in inputData) { const dataPoint = inputData[i]; const bucket = Math.floor((dataPoint - dataMin) / bucketSize); @@ -463,405 +437,380 @@ class StageSummary extends React.Component { } } - const tooltipValueLookups = {'offset': {}}; + const tooltipValueLookups = { offset: {} }; for (let i = 0; i < histogramData.length; i++) { - tooltipValueLookups['offset'][i] = numberFormatter(dataMin + (i * bucketSize)) + "-" + numberFormatter(dataMin + ((i + 1) * bucketSize)); + tooltipValueLookups["offset"][i] = + numberFormatter(dataMin + i * bucketSize) + + "-" + + numberFormatter(dataMin + (i + 1) * bucketSize); } - const stageHistogramProperties = $.extend({}, HISTOGRAM_PROPERTIES, {barWidth: (HISTOGRAM_WIDTH / histogramData.length), tooltipValueLookups: tooltipValueLookups}); + const stageHistogramProperties = $.extend({}, HISTOGRAM_PROPERTIES, { + barWidth: HISTOGRAM_WIDTH / histogramData.length, + tooltipValueLookups: tooltipValueLookups, + }); $(histogramId).sparkline(histogramData, stageHistogramProperties); - } + }; - componentDidUpdate() { - const stage = this.props.stage; - const numTasks = stage.latestAttemptExecutionInfo.tasks.length; + const renderBarChart = (barChartId, inputData, numTasks) => { + // this needs to be a string otherwise it will also be passed to numberFormatter + const tooltipValueLookups = { offset: {} }; + const stageId = getStageNumber(stage.stageId); + for (let i = 0; i < numTasks; i++) { + tooltipValueLookups["offset"][i] = stageId + "." + i; + } - // sort the x-axis - stage.latestAttemptExecutionInfo.tasks.sort((taskA, taskB) => getTaskNumber(taskA.taskId) - getTaskNumber(taskB.taskId)); + const stageBarChartProperties = $.extend({}, BAR_CHART_PROPERTIES, { + barWidth: BAR_CHART_WIDTH / numTasks, + tooltipValueLookups: tooltipValueLookups, + }); - const scheduledTimes = stage.latestAttemptExecutionInfo.tasks.map(task => parseDuration(task.stats.totalScheduledTimeInNanos + "ns")); - const cpuTimes = stage.latestAttemptExecutionInfo.tasks.map(task => parseDuration(task.stats.totalCpuTimeInNanos + "ns")); + $(barChartId).sparkline( + inputData, + $.extend({}, stageBarChartProperties, { numberFormatter: formatDuration }) + ); + }; - // prevent multiple calls to componentDidUpdate (resulting from calls to setState or otherwise) within the refresh interval from re-rendering sparklines/charts - if (this.state.lastRender === null || (Date.now() - this.state.lastRender) >= 1000) { - const renderTimestamp = Date.now(); - const stageId = getStageNumber(stage.stageId); + useEffect(() => { + if (!stage || !stage.latestAttemptExecutionInfo) { + return; + } - StageSummary.renderHistogram('#scheduled-time-histogram-' + stageId, scheduledTimes, formatDuration); - StageSummary.renderHistogram('#cpu-time-histogram-' + stageId, cpuTimes, formatDuration); + const numTasks = stage.latestAttemptExecutionInfo.tasks.length; + // sort the x-axis without mutating props + const sortedTasks = [...stage.latestAttemptExecutionInfo.tasks].sort( + (taskA, taskB) => getTaskNumber(taskA.taskId) - getTaskNumber(taskB.taskId) + ); - if (this.state.expanded) { - // this needs to be a string otherwise it will also be passed to numberFormatter - const tooltipValueLookups = {'offset': {}}; - for (let i = 0; i < numTasks; i++) { - tooltipValueLookups['offset'][i] = getStageNumber(stage.stageId) + "." + i; - } + const scheduledTimes = sortedTasks.map((task) => + parseDuration(task.stats.totalScheduledTimeInNanos + "ns") + ); + const cpuTimes = sortedTasks.map((task) => + parseDuration(task.stats.totalCpuTimeInNanos + "ns") + ); - const stageBarChartProperties = $.extend({}, BAR_CHART_PROPERTIES, {barWidth: BAR_CHART_WIDTH / numTasks, tooltipValueLookups: tooltipValueLookups}); + // prevent multiple re-renders within the refresh interval from re-rendering sparklines/charts + if (lastRenderRef.current === null || Date.now() - lastRenderRef.current >= 1000) { + const renderTimestamp = Date.now(); + const stageId = getStageNumber(stage.stageId); + + renderHistogram("#scheduled-time-histogram-" + stageId, scheduledTimes, formatDuration); + renderHistogram("#cpu-time-histogram-" + stageId, cpuTimes, formatDuration); - $('#scheduled-time-bar-chart-' + stageId).sparkline(scheduledTimes, $.extend({}, stageBarChartProperties, {numberFormatter: formatDuration})); - $('#cpu-time-bar-chart-' + stageId).sparkline(cpuTimes, $.extend({}, stageBarChartProperties, {numberFormatter: formatDuration})); + if (expanded) { + renderBarChart("#scheduled-time-bar-chart-" + stageId, scheduledTimes, numTasks); + renderBarChart("#cpu-time-bar-chart-" + stageId, cpuTimes, numTasks); } - this.setState({ - lastRender: renderTimestamp - }); + lastRenderRef.current = renderTimestamp; } - } - - renderStageExecutionAttemptsTasks(attempts) { - return attempts.map(attempt => { - return this.renderTaskList(attempt.tasks) - }); - } + }, [stage, expanded]); - renderTaskList(tasks) { - tasks = this.state.expanded ? tasks : []; - tasks = tasks.filter(task => this.state.taskFilter.predicate(task.taskStatus.state), this); + const renderTaskList = (tasks) => { + tasks = expanded ? tasks : []; + tasks = tasks.filter((task) => taskFilter.predicate(task.taskStatus.state)); return ( - + - + ); - } - - renderTaskFilterListItem(taskFilter) { - return ( -
  • {taskFilter.text}
  • - ); - } + }; - handleTaskFilterClick(filter, event) { - this.setState({ - taskFilter: filter + const renderStageExecutionAttemptsTasks = (attempts) => { + return attempts.map((attempt) => { + return renderTaskList(attempt.tasks); }); + }; + + const handleTaskFilterClick = (filter, event) => { + setTaskFilter(filter); event.preventDefault(); - } + }; + + const renderTaskFilterListItem = (candidateFilter) => ( +
  • + handleTaskFilterClick(candidateFilter, e)} + > + {candidateFilter.text} + +
  • + ); - renderTaskFilter() { - return (
    + const renderTaskFilter = () => ( +

    Tasks

    - - - + + +
    -
    - -
      - {this.renderTaskFilterListItem(TASK_FILTER.ALL)} - {this.renderTaskFilterListItem(TASK_FILTER.PLANNED)} - {this.renderTaskFilterListItem(TASK_FILTER.RUNNING)} - {this.renderTaskFilterListItem(TASK_FILTER.FINISHED)} - {this.renderTaskFilterListItem(TASK_FILTER.FAILED)} -
    -
    -
    +
    + +
      + {renderTaskFilterListItem(TASK_FILTER.ALL)} + {renderTaskFilterListItem(TASK_FILTER.PLANNED)} + {renderTaskFilterListItem(TASK_FILTER.RUNNING)} + {renderTaskFilterListItem(TASK_FILTER.FINISHED)} + {renderTaskFilterListItem(TASK_FILTER.FAILED)} +
    +
    +
    -
    ); +
    + ); + if (stage === undefined || !stage.hasOwnProperty("plan")) { + return ( + + Information about this stage is unavailable. + + ); } - render() { - const stage = this.props.stage; - if (stage === undefined || !stage.hasOwnProperty('plan')) { - return ( - - Information about this stage is unavailable. - ); - } - - const totalBufferedBytes = stage.latestAttemptExecutionInfo.tasks - .map(task => task.outputBuffers.totalBufferedBytes) - .reduce((a, b) => a + b, 0); + const totalBufferedBytes = stage.latestAttemptExecutionInfo.tasks + .map((task) => task.outputBuffers.totalBufferedBytes) + .reduce((a, b) => a + b, 0); - const stageId = getStageNumber(stage.stageId); + const stageId = getStageNumber(stage.stageId); - return ( - - -
    {stageId}
    - - - - + return ( + + + - ); - } -} + {renderStageExecutionAttemptsTasks([stage.latestAttemptExecutionInfo])} + {renderStageExecutionAttemptsTasks(stage.previousAttemptsExecutionInfos)} + +
    +
    {stageId}
    +
    + + - + - + - - + + - {this.renderStageExecutionAttemptsTasks([stage.latestAttemptExecutionInfo])} - - {this.renderStageExecutionAttemptsTasks(stage.previousAttemptsExecutionInfos)} - -
    - - - + + + - - - - - - - - - - - - + + + + + + + + + + + +
    - Time - -
    Time +
    - Scheduled - - {stage.latestAttemptExecutionInfo.stats.totalScheduledTime} -
    - Blocked - - {stage.latestAttemptExecutionInfo.stats.totalBlockedTime} -
    - CPU - - {stage.latestAttemptExecutionInfo.stats.totalCpuTime} -
    Scheduled{stage.latestAttemptExecutionInfo.stats.totalScheduledTime}
    Blocked{stage.latestAttemptExecutionInfo.stats.totalBlockedTime}
    CPU{stage.latestAttemptExecutionInfo.stats.totalCpuTime}
    - - - + + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + +
    - Memory - -
    Memory +
    - Cumulative - - {formatDataSize(stage.latestAttemptExecutionInfo.stats.cumulativeUserMemory / 1000)} -
    - Cumulative Total - - {formatDataSize(stage.latestAttemptExecutionInfo.stats.cumulativeTotalMemory / 1000)} -
    - Current - - {stage.latestAttemptExecutionInfo.stats.userMemoryReservation} -
    - Buffers - - {formatDataSize(totalBufferedBytes)} -
    - Peak - - {stage.latestAttemptExecutionInfo.stats.peakUserMemoryReservation} -
    Cumulative{formatDataSize(stage.latestAttemptExecutionInfo.stats.cumulativeUserMemory / 1000)}
    Cumulative Total{formatDataSize(stage.latestAttemptExecutionInfo.stats.cumulativeTotalMemory / 1000)}
    Current{stage.latestAttemptExecutionInfo.stats.userMemoryReservation}
    Buffers{formatDataSize(totalBufferedBytes)}
    Peak{stage.latestAttemptExecutionInfo.stats.peakUserMemoryReservation}
    - - - + + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + +
    - Tasks - -
    Tasks +
    - Pending - - {stage.latestAttemptExecutionInfo.tasks.filter(task => task.taskStatus.state === "PLANNED").length} -
    - Running - - {stage.latestAttemptExecutionInfo.tasks.filter(task => task.taskStatus.state === "RUNNING").length} -
    - Blocked - - {stage.latestAttemptExecutionInfo.tasks.filter(task => task.stats.fullyBlocked).length} -
    - Total - - {stage.latestAttemptExecutionInfo.tasks.length} -
    Pending{stage.latestAttemptExecutionInfo.tasks.filter((task) => task.taskStatus.state === "PLANNED").length}
    Running{stage.latestAttemptExecutionInfo.tasks.filter((task) => task.taskStatus.state === "RUNNING").length}
    Blocked{stage.latestAttemptExecutionInfo.tasks.filter((task) => task.stats.fullyBlocked).length}
    Total{stage.latestAttemptExecutionInfo.tasks.length}
    - - - + + + - - - + + +
    - Scheduled Time Skew -
    Scheduled Time Skew
    -
    -
    + +
    + +
    - - - + + + - - - + + +
    - CPU Time Skew -
    CPU Time Skew
    -
    -
    + +
    + +
    - - + +
    - - - - + + + +
    - Task Scheduled Time - -
    -
    Task Scheduled Time + +
    + +
    - - - - + + + +
    - Task CPU Time - -
    -
    Task CPU Time + +
    + +
    - {this.renderTaskFilter()} -
    {renderTaskFilter()}
    -
    + + + ); +}; -class StageList extends React.Component { - getStages(stage) { +const StageList = ({ outputStage }) => { + const getStages = (stage) => { if (stage === undefined || !stage.hasOwnProperty('subStages')) { return [] } - return [].concat.apply(stage, stage.subStages.map(this.getStages, this)); - } - - render() { - const stages = this.getStages(this.props.outputStage); - - if (stages === undefined || stages.length === 0) { - return ( -
    -
    - No stage information available. -
    -
    - ); - } + return [].concat.apply(stage, stage.subStages.map(getStages)); + }; - const renderedStages = stages.map(stage => ); + const stages = getStages(outputStage); + if (stages === undefined || stages.length === 0) { return (
    - - - {renderedStages} - -
    + No stage information available.
    ); } -} + + const renderedStages = stages.map(stage => ); + + return ( +
    +
    + + + {renderedStages} + +
    +
    +
    + ); +}; const SMALL_SPARKLINE_PROPERTIES = { width: '100%', diff --git a/presto-ui/src/components/QueryHeader.jsx b/presto-ui/src/components/QueryHeader.jsx index 9a374016a05a4..34a494e991909 100644 --- a/presto-ui/src/components/QueryHeader.jsx +++ b/presto-ui/src/components/QueryHeader.jsx @@ -17,13 +17,8 @@ import { clsx } from 'clsx'; import {getHumanReadableState, getProgressBarPercentage, getProgressBarTitle, getQueryStateColor, isQueryEnded} from "../utils"; -export class QueryHeader extends React.Component { - constructor(props) { - super(props); - } - - renderProgressBar() { - const query = this.props.query; +export const QueryHeader = ({ query }) => { + const renderProgressBar = () => { const queryStateColor = getQueryStateColor( query.state, query.queryStats && query.queryStats.fullyBlocked, @@ -82,55 +77,49 @@ export class QueryHeader extends React.Component { ); - } - - isActive(path) { - if (window.location.pathname.includes(path)) { - return true; - } + }; - return false; - } + const isActive = (path) => { + return window.location.pathname.includes(path); + }; - render() { - const query = this.props.query; - const queryId = this.props.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} - - -

    -
    -
    - -
    + 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 ( -
    -
    -
    -
    -
    - {query.queryId} - {this.renderWarning()} -
    -
    - {formatShortTime(new Date(Date.parse(query.createTime)))} -
    + return ( +
    +
    +
    +
    +
    + {query.queryId} + {renderWarning()}
    -
    -
    - -    - {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} +
    +
    +
    +
    +
    +
    +
    + {progressBarTitle}
    -
    -
    -
    {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 ( - ); - } + }; - handleStateFilterClick(filter) { - const newFilters = this.state.stateFilters.slice(); - if (this.state.stateFilters.indexOf(filter) > -1) { - newFilters.splice(newFilters.indexOf(filter), 1); + const handleErrorTypeFilterClick = (errorType) => { + const newFilters = state.errorTypeFilters.slice(); + if (state.errorTypeFilters.indexOf(errorType) > -1) { + newFilters.splice(newFilters.indexOf(errorType), 1); } else { - newFilters.push(filter); + newFilters.push(errorType); } - const filteredQueries = this.filterQueries(this.state.allQueries, newFilters, this.state.errorTypeFilters, this.state.searchString); - this.sortAndLimitQueries(filteredQueries, this.state.currentSortType, this.state.currentSortOrder); + const filteredQueries = filterQueries(dataSet.current.allQueries, dataSet.current.stateFilters, newFilters, dataSet.current.searchString); + sortAndLimitQueries(filteredQueries, dataSet.current.currentSortType, dataSet.current.currentSortOrder, dataSet.current.maxQueries); - this.setState({ - stateFilters: newFilters, - displayedQueries: filteredQueries - }); - } + dataSet.current.errorTypeFilters = newFilters; + dataSet.current.displayedQueries = filteredQueries; + setState(prev => ({ ...prev, errorTypeFilters: newFilters, displayedQueries: filteredQueries })); + }; - renderErrorTypeListItem(errorType, errorTypeText) { + const renderErrorTypeListItem = (errorType, errorTypeText) => { let checkmarkStyle = {color: '#ffffff'}; - if (this.state.errorTypeFilters.indexOf(errorType) > -1) { + if (state.errorTypeFilters.indexOf(errorType) > -1) { checkmarkStyle = {color: 'black'}; } return (
  • - + handleErrorTypeFilterClick(errorType)}>  {errorTypeText}
  • ); - } - - handleErrorTypeFilterClick(errorType) { - const newFilters = this.state.errorTypeFilters.slice(); - if (this.state.errorTypeFilters.indexOf(errorType) > -1) { - newFilters.splice(newFilters.indexOf(errorType), 1); - } - else { - newFilters.push(errorType); - } - - const filteredQueries = this.filterQueries(this.state.allQueries, this.state.stateFilters, newFilters, this.state.searchString); - this.sortAndLimitQueries(filteredQueries, this.state.currentSortType, this.state.currentSortOrder); - - this.setState({ - errorTypeFilters: newFilters, - displayedQueries: filteredQueries - }); - } - - render() { - let queryList = ; - if (this.state.displayedQueries === null || this.state.displayedQueries.length === 0) { - let label = (
    Loading...
    ); - if (this.state.initialized) { - if (this.state.allQueries === null || this.state.allQueries.length === 0) { - label = "No queries"; - } - else { - label = "No queries matched filters"; - } + }; + + let queryList = ; + if (state.displayedQueries === null || state.displayedQueries.length === 0) { + let label = (
    Loading...
    ); + if (state.initialized) { + if (state.allQueries === null || state.allQueries.length === 0) { + label = "No queries"; + } + else { + label = "No queries matched filters"; } - queryList = ( -
    -

    {label}

    -
    - ); } + queryList = ( +
    +

    {label}

    +
    + ); + } - return ( -
    -
    -
    -
    - -
    + return ( +
    +
    +
    +
    + +
    -
    - State: - {this.renderFilterButton(FILTER_TYPE.RUNNING, "Running")} - {this.renderFilterButton(FILTER_TYPE.QUEUED, "Queued")} - {this.renderFilterButton(FILTER_TYPE.FINISHED, "Finished")} - -
      - {this.renderErrorTypeListItem(ERROR_TYPE.INTERNAL_ERROR, "Internal Error")} - {this.renderErrorTypeListItem(ERROR_TYPE.EXTERNAL, "External Error")} - {this.renderErrorTypeListItem(ERROR_TYPE.INSUFFICIENT_RESOURCES, "Resources Error")} - {this.renderErrorTypeListItem(ERROR_TYPE.USER_ERROR, "User Error")} -
    +
    + State: + {renderFilterButton(FILTER_TYPE.RUNNING, "Running")} + {renderFilterButton(FILTER_TYPE.QUEUED, "Queued")} + {renderFilterButton(FILTER_TYPE.FINISHED, "Finished")} + +
      + {renderErrorTypeListItem(ERROR_TYPE.INTERNAL_ERROR, "Internal Error")} + {renderErrorTypeListItem(ERROR_TYPE.EXTERNAL, "External Error")} + {renderErrorTypeListItem(ERROR_TYPE.INSUFFICIENT_RESOURCES, "Resources Error")} + {renderErrorTypeListItem(ERROR_TYPE.USER_ERROR, "User Error")} +
    -
    -   -
    - -
      - {this.renderSortListItem(SORT_TYPE.CREATED, "Creation Time")} - {this.renderSortListItem(SORT_TYPE.ELAPSED, "Elapsed Time")} - {this.renderSortListItem(SORT_TYPE.CPU, "CPU Time")} - {this.renderSortListItem(SORT_TYPE.EXECUTION, "Execution Time")} - {this.renderSortListItem(SORT_TYPE.CURRENT_MEMORY, "Current Memory")} - {this.renderSortListItem(SORT_TYPE.CUMULATIVE_MEMORY, "Cumulative User Memory")} -
    -
    -   -
    - -
      - {this.renderReorderListItem(1000, "1s")} - {this.renderReorderListItem(5000, "5s")} - {this.renderReorderListItem(10000, "10s")} - {this.renderReorderListItem(30000, "30s")} -
      - {this.renderReorderListItem(0, "Off")} -
    -
    -   -
    - -
      - {this.renderMaxQueriesListItem(20, "20 queries")} - {this.renderMaxQueriesListItem(50, "50 queries")} - {this.renderMaxQueriesListItem(100, "100 queries")} -
      - {this.renderMaxQueriesListItem(0, "All queries")} -
    -
    +
    +   +
    + +
      + {renderSortListItem(SORT_TYPE.CREATED, "Creation Time")} + {renderSortListItem(SORT_TYPE.ELAPSED, "Elapsed Time")} + {renderSortListItem(SORT_TYPE.CPU, "CPU Time")} + {renderSortListItem(SORT_TYPE.EXECUTION, "Execution Time")} + {renderSortListItem(SORT_TYPE.CURRENT_MEMORY, "Current Memory")} + {renderSortListItem(SORT_TYPE.CUMULATIVE_MEMORY, "Cumulative User Memory")} +
    +
    +   +
    + +
      + {renderReorderListItem(1000, "1s")} + {renderReorderListItem(5000, "5s")} + {renderReorderListItem(10000, "10s")} + {renderReorderListItem(30000, "30s")} +
      + {renderReorderListItem(0, "Off")} +
    +
    +   +
    + +
      + {renderMaxQueriesListItem(20, "20 queries")} + {renderMaxQueriesListItem(50, "50 queries")} + {renderMaxQueriesListItem(100, "100 queries")} +
      + {renderMaxQueriesListItem(0, "All queries")} +
    +
    - {queryList} -
    - ); - } -} + {queryList} +
    + ); +}; -export default QueryList; \ No newline at end of file +export default QueryList;