diff --git a/core/trino-web-ui/src/main/resources/webapp-preview/src/App.tsx b/core/trino-web-ui/src/main/resources/webapp-preview/src/App.tsx
index d4854008f850..2a4be98a3d3d 100644
--- a/core/trino-web-ui/src/main/resources/webapp-preview/src/App.tsx
+++ b/core/trino-web-ui/src/main/resources/webapp-preview/src/App.tsx
@@ -32,6 +32,7 @@ import { routers } from './router.tsx'
import { useConfigStore, Theme as ThemeStore } from './store'
import { darkTheme, lightTheme } from './theme'
import trinoLogo from './assets/trino.svg'
+import { WorkerStatus } from './components/WorkerStatus.tsx'
const App = () => {
const config = useConfigStore()
@@ -74,6 +75,7 @@ const Screen = () => {
{routers.flatMap((router) => {
return []
})}
+ } />
} />
} key={'*'} />
diff --git a/core/trino-web-ui/src/main/resources/webapp-preview/src/api/webapp/api.ts b/core/trino-web-ui/src/main/resources/webapp-preview/src/api/webapp/api.ts
index dda2ad0fbb89..48d20f42c2e7 100644
--- a/core/trino-web-ui/src/main/resources/webapp-preview/src/api/webapp/api.ts
+++ b/core/trino-web-ui/src/main/resources/webapp-preview/src/api/webapp/api.ts
@@ -27,6 +27,65 @@ export interface Stats {
totalCpuTimeSecs: number
}
+export interface Worker {
+ coordinator: boolean
+ nodeId: string
+ nodeIp: string
+ nodeVersion: string
+ state: string
+}
+
+export interface MemoryAllocationItem {
+ tag: string
+ allocation: number
+}
+
+export interface MemoryUsagePool {
+ freeBytes: number
+ maxBytes: number
+ reservedBytes: number
+ reservedRevocableBytes: number
+ queryMemoryAllocations: {
+ [key: string]: MemoryAllocationItem
+ }
+ queryMemoryReservations: {
+ [key: string]: number
+ }
+ queryMemoryRevocableReservations: {
+ [key: string]: number
+ }
+}
+
+export interface WorkerStatusInfo {
+ coordinator: boolean
+ environment: string
+ externalAddress: string
+ heapAvailable: number
+ heapUsed: number
+ internalAddress: string
+ memoryInfo: {
+ availableProcessors: number
+ pool: MemoryUsagePool
+ }
+ nodeId: string
+ nodeVersion: {
+ version: string
+ }
+ nonHeapUsed: number
+ processCpuLoad: number
+ processors: number
+ systemCpuLoad: number
+ uptime: string
+}
+
export async function statsApi(): Promise> {
return await api.get('/ui/api/stats')
}
+
+export async function workerApi(): Promise> {
+ return await api.get('/ui/api/worker')
+}
+
+export async function workerStatusApi(nodeId: string): Promise> {
+ return await api.get(`/ui/api/worker/${nodeId}/status`)
+}
diff --git a/core/trino-web-ui/src/main/resources/webapp-preview/src/components/Dashboard.tsx b/core/trino-web-ui/src/main/resources/webapp-preview/src/components/Dashboard.tsx
index 9e6480137f85..701c287b4274 100644
--- a/core/trino-web-ui/src/main/resources/webapp-preview/src/components/Dashboard.tsx
+++ b/core/trino-web-ui/src/main/resources/webapp-preview/src/components/Dashboard.tsx
@@ -157,7 +157,7 @@ export const Dashboard = () => {
-
+
diff --git a/core/trino-web-ui/src/main/resources/webapp-preview/src/components/MetricCard.tsx b/core/trino-web-ui/src/main/resources/webapp-preview/src/components/MetricCard.tsx
index c0fa342401b5..9f9278038a6a 100644
--- a/core/trino-web-ui/src/main/resources/webapp-preview/src/components/MetricCard.tsx
+++ b/core/trino-web-ui/src/main/resources/webapp-preview/src/components/MetricCard.tsx
@@ -11,28 +11,47 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import { Link as RouterLink } from 'react-router-dom'
import Card from '@mui/material/Card'
import CardContent from '@mui/material/CardContent'
import Typography from '@mui/material/Typography'
import { Grid2 as Grid } from '@mui/material'
import { SparkLineChart } from '@mui/x-charts/SparkLineChart'
+import { styled } from '@mui/material/styles'
interface IMetricCardProps {
title: string
values: number[]
numberFormatter?: (n: number | null) => string
+ link?: string
}
+const StyledLink = styled(RouterLink)(({ theme }) => ({
+ textDecoration: 'none',
+ color: theme.palette.info.main,
+ '&:hover': {
+ textDecoration: 'underline',
+ },
+}))
+
export const MetricCard = (props: IMetricCardProps) => {
- const { title, values, numberFormatter } = props
+ const { title, values, numberFormatter, link } = props
const lastValue = values[values.length - 1]
return (
-
- {title}
-
+ {link ? (
+
+
+ {title}
+
+
+ ) : (
+
+ {title}
+
+ )}
{
+ const { showSnackbar } = useSnackbar()
+ const { nodeId } = useParams()
+ const theme = useTheme()
+ const initialFilledHistory = Array(MAX_HISTORY).fill(0)
+ const [workerStatus, setWorkerStatus] = useState({
+ info: null,
+ processCpuLoad: initialFilledHistory,
+ systemCpuLoad: initialFilledHistory,
+ heapPercentUsed: initialFilledHistory,
+ nonHeapUsed: initialFilledHistory,
+ lastRefresh: null,
+ })
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+
+ useEffect(() => {
+ getWorkerStatus()
+ const intervalId = setInterval(getWorkerStatus, 1000)
+ return () => clearInterval(intervalId)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ useEffect(() => {
+ if (error) {
+ showSnackbar(error, 'error')
+ }
+ }, [error, showSnackbar])
+
+ const getWorkerStatus = () => {
+ setError(null)
+ if (nodeId) {
+ workerStatusApi(nodeId).then((apiResponse: ApiResponse) => {
+ setLoading(false)
+ if (apiResponse.status === 200 && apiResponse.data) {
+ const newWorkerStatusInfo: WorkerStatusInfo = apiResponse.data
+ setWorkerStatus((prevWorkerStatus) => {
+ return {
+ info: newWorkerStatusInfo,
+ processCpuLoad: addToHistory(
+ newWorkerStatusInfo.processCpuLoad * 100.0,
+ prevWorkerStatus.processCpuLoad
+ ),
+ systemCpuLoad: addToHistory(
+ newWorkerStatusInfo.systemCpuLoad * 100.0,
+ prevWorkerStatus.systemCpuLoad
+ ),
+ heapPercentUsed: addToHistory(
+ (newWorkerStatusInfo.heapUsed * 100.0) / newWorkerStatusInfo.heapAvailable,
+ prevWorkerStatus.heapPercentUsed
+ ),
+ nonHeapUsed: addToHistory(newWorkerStatusInfo.nonHeapUsed, prevWorkerStatus.nonHeapUsed),
+
+ lastRefresh: Date.now(),
+ }
+ })
+ } else {
+ setError(`${Texts.Error.Communication} ${apiResponse.status}: ${apiResponse.message}`)
+ }
+ })
+ }
+ }
+
+ const renderPoolChart = (name: string, pool: MemoryUsagePool) => {
+ if (!pool) {
+ return <>>
+ }
+
+ const size = pool.maxBytes
+ const reserved = pool.reservedBytes
+ const revocable = pool.reservedRevocableBytes
+ const available = size - reserved - revocable
+
+ return (
+ <>
+
+
+ {name} Pool
+
+
+ {formatDataSize(size)} total
+
+
+
+
+ formatDataSize(value.value),
+ },
+ ]}
+ height={150}
+ skipAnimation
+ />
+
+
+ >
+ )
+ }
+
+ const renderPoolQueries = (pool: MemoryUsagePool) => {
+ if (!pool) {
+ return <>>
+ }
+
+ const queries: { [key: string]: number[] } = {}
+ const reservations = pool.queryMemoryReservations
+ const revocableReservations = pool.queryMemoryRevocableReservations
+
+ for (let query in reservations) {
+ queries[query] = [reservations[query], 0]
+ }
+
+ for (let query in revocableReservations) {
+ if (queries.hasOwnProperty.call(queries, query)) {
+ queries[query][1] = revocableReservations[query]
+ } else {
+ queries[query] = [0, revocableReservations[query]]
+ }
+ }
+
+ const size = pool.maxBytes
+
+ if (Object.keys(queries).length === 0) {
+ return No queries using pool
+ }
+
+ return (
+
+
+
+
+
+
+ Query Id
+ Reserved %
+ Reserved
+ Revocable
+
+
+
+ {Object.entries(queries)
+ .sort(([, reservationsA], [, reservationsB]) => reservationsB[0] - reservationsA[0])
+ .map(([query, reservations]) => (
+
+ {query}
+
+ {Math.round((reservations[0] * 100.0) / size)}%
+
+ {formatDataSize(reservations[0])}
+ {formatDataSize(reservations[1])}
+
+ ))}
+
+
+
+
+
+ )
+ }
+
+ return (
+ <>
+
+ Worker Status
+
+
+ {loading && }
+ {error && {Texts.Error.NodeInformationNotLoaded}}
+
+ {!loading && !error && workerStatus.info && (
+ <>
+
+ Overview
+
+
+
+
+
+
+
+
+ Node ID
+ {workerStatus.info.nodeId}
+
+
+ Heap Memory
+ {formatDataSize(workerStatus.info.heapAvailable)}
+
+
+ Processors
+ {workerStatus.info.processors}
+
+
+
+
+
+
+
+
+
+
+ Uptime
+ {workerStatus.info.uptime}
+
+
+ External Address
+ {workerStatus.info.externalAddress}
+
+
+ Internal Address
+ {workerStatus.info.internalAddress}
+
+
+
+
+
+
+
+
+ Resource Utilization
+
+
+
+
+
+
+
+
+
+ Process CPU Utilization
+
+
+
+
+
+
+
+ {formatCount(
+ workerStatus.processCpuLoad[workerStatus.processCpuLoad.length - 1]
+ )}
+ %
+
+
+
+
+ System CPU Utilization
+
+
+
+
+
+
+
+ {formatCount(
+ workerStatus.systemCpuLoad[workerStatus.systemCpuLoad.length - 1]
+ )}
+ %
+
+
+
+
+
+
+
+
+
+
+
+
+ Heap Utilization
+
+
+
+
+
+
+
+ {formatCount(
+ (workerStatus.info.heapUsed * 100) / workerStatus.info.heapAvailable
+ )}
+ %
+
+
+
+
+ Non-Heap Memory Used
+
+
+
+
+
+
+ {formatDataSize(workerStatus.info.nonHeapUsed)}
+
+
+
+
+
+
+
+
+ Memory
+
+
+
+
+ {renderPoolChart('Memory Usage', workerStatus.info.memoryInfo.pool)}
+
+ {renderPoolQueries(workerStatus.info.memoryInfo.pool)}
+
+ >
+ )}
+ >
+ )
+}
diff --git a/core/trino-web-ui/src/main/resources/webapp-preview/src/components/Workers.tsx b/core/trino-web-ui/src/main/resources/webapp-preview/src/components/Workers.tsx
deleted file mode 100644
index 2aa1d884e19a..000000000000
--- a/core/trino-web-ui/src/main/resources/webapp-preview/src/components/Workers.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import { Box, Typography } from '@mui/material'
-
-export const Workers = () => {
- return (
- <>
-
- Workers
-
- Placeholder for Workers
- >
- )
-}
diff --git a/core/trino-web-ui/src/main/resources/webapp-preview/src/components/WorkersList.tsx b/core/trino-web-ui/src/main/resources/webapp-preview/src/components/WorkersList.tsx
new file mode 100644
index 000000000000..85d0673c47c0
--- /dev/null
+++ b/core/trino-web-ui/src/main/resources/webapp-preview/src/components/WorkersList.tsx
@@ -0,0 +1,108 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Link as RouterLink } from 'react-router-dom'
+import {
+ Box,
+ Divider,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ Typography,
+} from '@mui/material'
+import { Worker, workerApi } from '../api/webapp/api.ts'
+import { ApiResponse } from '../api/base.ts'
+import { useEffect, useState } from 'react'
+import { Texts } from '../constant.ts'
+import { useSnackbar } from './SnackbarContext.ts'
+import { styled } from '@mui/material/styles'
+
+const StyledLink = styled(RouterLink)(({ theme }) => ({
+ textDecoration: 'none',
+ color: theme.palette.info.main,
+ '&:hover': {
+ textDecoration: 'underline',
+ },
+}))
+
+export const WorkersList = () => {
+ const { showSnackbar } = useSnackbar()
+ const [workersList, setWorkersList] = useState([])
+ const [error, setError] = useState(null)
+
+ useEffect(() => {
+ getWorkersList()
+ const intervalId = setInterval(getWorkersList, 1000)
+ return () => clearInterval(intervalId)
+ }, [])
+
+ useEffect(() => {
+ if (error) {
+ showSnackbar(error, 'error')
+ }
+ }, [error, showSnackbar])
+
+ const getWorkersList = () => {
+ setError(null)
+ workerApi().then((apiResponse: ApiResponse) => {
+ if (apiResponse.status === 200 && apiResponse.data) {
+ setWorkersList(apiResponse.data)
+ } else {
+ setError(`${Texts.Error.Communication} ${apiResponse.status}: ${apiResponse.message}`)
+ }
+ })
+ }
+
+ return (
+ <>
+
+ Workers
+
+
+ Overview
+
+
+
+
+
+
+ Node Id
+ Node IP
+ Node Version
+ Coordinator
+ State
+
+
+
+ {workersList.map((worker: Worker) => (
+
+
+ {worker.nodeId}
+
+
+ {worker.nodeIp}
+
+ {worker.nodeVersion}
+ {String(worker.coordinator)}
+ {worker.state}
+
+ ))}
+
+
+
+ >
+ )
+}
diff --git a/core/trino-web-ui/src/main/resources/webapp-preview/src/constant.ts b/core/trino-web-ui/src/main/resources/webapp-preview/src/constant.ts
index 675eb1d9546e..5858661cc600 100644
--- a/core/trino-web-ui/src/main/resources/webapp-preview/src/constant.ts
+++ b/core/trino-web-ui/src/main/resources/webapp-preview/src/constant.ts
@@ -42,6 +42,7 @@ export const Texts = {
Communication: 'Communication error to Trino.',
Forbidden: 'Forbidden',
Network: 'The network has wandered off, please try again later!',
+ NodeInformationNotLoaded: 'Node information could not be loaded',
},
Menu: {
Header: {
diff --git a/core/trino-web-ui/src/main/resources/webapp-preview/src/router.tsx b/core/trino-web-ui/src/main/resources/webapp-preview/src/router.tsx
index b122c061bf6f..e22ffd21b888 100644
--- a/core/trino-web-ui/src/main/resources/webapp-preview/src/router.tsx
+++ b/core/trino-web-ui/src/main/resources/webapp-preview/src/router.tsx
@@ -19,7 +19,7 @@ import HistoryOutlinedIcon from '@mui/icons-material/HistoryOutlined'
import { RouteProps } from 'react-router-dom'
import { Dashboard } from './components/Dashboard'
import { DemoComponents } from './components/DemoComponents'
-import { Workers } from './components/Workers'
+import { WorkersList } from './components/WorkersList.tsx'
import { QueryHistory } from './components/QueryHistory'
import { Texts } from './constant'
@@ -48,7 +48,7 @@ export const routers: RouterItems = [
icon: ,
routeProps: {
path: '/workers',
- element: ,
+ element: ,
},
},
{