diff --git a/core/trino-web-ui/src/main/resources/webapp-preview/src/api/base.ts b/core/trino-web-ui/src/main/resources/webapp-preview/src/api/base.ts index a6929d48632f..0aa8eab5179d 100644 --- a/core/trino-web-ui/src/main/resources/webapp-preview/src/api/base.ts +++ b/core/trino-web-ui/src/main/resources/webapp-preview/src/api/base.ts @@ -30,13 +30,19 @@ export class ClientApi { return queryString } - fetchData = async (url: string, method: 'GET' | 'POST', params?: P): Promise> => { + fetchData = async ( + url: string, + method: 'GET' | 'POST' | 'PUT', + params?: P + ): Promise> => { try { let response: AxiosResponse if (method === 'GET') { response = await this.axiosInstance.get(url) } else if (method === 'POST') { response = await this.axiosInstance.post(url, params) + } else if (method === 'PUT') { + response = await this.axiosInstance.put(url, params) } else { throw new Error(`Unsupported HTTP method: ${method}`) } @@ -64,6 +70,10 @@ export class ClientApi { post = async (url: string, body: Record = {}): Promise> => { return this.fetchData(url, 'POST', body) } + + put = async (url: string, body: string | Record = {}): Promise> => { + return this.fetchData(url, 'PUT', body) + } } export const api = new ClientApi() 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 64e2f8760f0d..782018aacf80 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 @@ -389,3 +389,7 @@ export async function queryApi(): Promise> { export async function queryStatusApi(queryId: string, pruned: boolean = false): Promise> { return await api.get(`/ui/api/query/${queryId}${pruned ? '?pruned=true' : ''}`) } + +export async function killQueryApi(queryId: string): Promise> { + return await api.put(`/ui/api/query/${queryId}/killed`, 'Canceled via web UI') +} diff --git a/core/trino-web-ui/src/main/resources/webapp-preview/src/components/QueryOverview.tsx b/core/trino-web-ui/src/main/resources/webapp-preview/src/components/QueryOverview.tsx index 15fef2ca8384..040010843304 100644 --- a/core/trino-web-ui/src/main/resources/webapp-preview/src/components/QueryOverview.tsx +++ b/core/trino-web-ui/src/main/resources/webapp-preview/src/components/QueryOverview.tsx @@ -17,6 +17,7 @@ import { QueryProgressBar } from './QueryProgressBar' import { Alert, Box, + Button, CircularProgress, Divider, Grid, @@ -29,7 +30,15 @@ import { } from '@mui/material' import { SparkLineChart } from '@mui/x-charts/SparkLineChart' import { Texts } from '../constant.ts' -import { StackInfo, queryStatusApi, QueryStatusInfo, QueryStage, QueryStages, Session } from '../api/webapp/api.ts' +import { + StackInfo, + killQueryApi, + queryStatusApi, + QueryStage, + QueryStages, + QueryStatusInfo, + Session, +} from '../api/webapp/api.ts' import { ApiResponse } from '../api/base.ts' import { addToHistory, @@ -90,6 +99,8 @@ export const QueryOverview = () => { const [queryStatus, setQueryStatus] = useState(initialQueryStatus) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) + const [queryActionError, setQueryActionError] = useState(null) + const [queryActionRunning, setQueryActionRunning] = useState(false) const queryStatusRef = useRef(queryStatus) useEffect(() => { @@ -107,6 +118,7 @@ export const QueryOverview = () => { if (queryId) { queryStatusRef.current = initialQueryStatus + setQueryActionError(null) } runLoop() @@ -458,7 +470,35 @@ export const QueryOverview = () => { ) } + const runQueryAction = (queryAction: (queryId: string) => Promise>, actionName: string) => { + if (!queryId || queryActionRunning || queryStatus.info?.finalQueryInfo) { + return + } + + setQueryActionError(null) + setQueryActionRunning(true) + queryAction(queryId) + .then((apiResponse: ApiResponse) => { + if (apiResponse.status === 403) { + setQueryActionError( + `${Texts.Error.Forbidden}: You do not have permission to ${actionName} this query.` + ) + } else if (apiResponse.status !== 202 && apiResponse.status !== 409) { + setQueryActionError(`${Texts.Error.Communication} ${apiResponse.status}: ${apiResponse.message}`) + } + getQueryStatus() + }) + .finally(() => { + setQueryActionRunning(false) + }) + } + + const handleCancel = () => { + runQueryAction(killQueryApi, 'cancel') + } + const taskRetriesEnabled = queryStatus.info?.retryPolicy == 'TASK' + const queryActionDisabled = queryActionRunning || !!queryStatus.info?.finalQueryInfo return ( <> {loading && } @@ -467,9 +507,27 @@ export const QueryOverview = () => { {!loading && !error && queryStatus.info && ( - - + + + + + + + + {queryActionError && ( + + {queryActionError} + + )}