| // Licensed to the Apache Software Foundation (ASF) under one | |
| // or more contributor license agreements. See the NOTICE file | |
| // distributed with this work for additional information | |
| // regarding copyright ownership. The ASF licenses this file | |
| // to you 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 { useQuery } from '@tanstack/react-query'; | |
| import type { DynamicCell, DynamicTableData } from '../components/operations/dynamicTable'; | |
| import { withRuntimeBasePath } from '../runtimeBasePath'; | |
| import { UiApiError } from './client'; | |
| import { getCsrfToken, setCsrfToken } from './csrf'; | |
| export interface LegacyEnvelope<T> { | |
| code?: number; | |
| msg?: string; | |
| data?: T; | |
| } | |
| export interface LegacyTablePayload { | |
| column_names?: unknown; | |
| rows?: unknown; | |
| href_columns?: unknown; | |
| href_column?: unknown; | |
| parent_url?: unknown; | |
| } | |
| export interface SystemResult { | |
| table: DynamicTableData; | |
| parentPath: string | null; | |
| } | |
| function asCell(value: unknown): DynamicCell { | |
| if (value === null || value === undefined || typeof value === 'string' | |
| || typeof value === 'number' || typeof value === 'boolean') return value; | |
| return JSON.stringify(value); | |
| } | |
| function asStringArray(value: unknown): string[] { | |
| return Array.isArray(value) ? value.map((item) => String(item)) : []; | |
| } | |
| export async function legacyGet<T>(path: string): Promise<T> { | |
| return legacyRequest<T>(path, { method: 'GET' }); | |
| } | |
| export async function legacyPostForm<T>(path: string, form: URLSearchParams): Promise<T> { | |
| return legacyRequest<T>(path, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' }, | |
| body: form.toString(), | |
| }); | |
| } | |
| export async function legacyPostFormMutation<T>(path: string, form: URLSearchParams): Promise<T> { | |
| const token = getCsrfToken(); | |
| const headers: Record<string, string> = { | |
| 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', | |
| }; | |
| if (token) headers['X-Doris-CSRF-Token'] = token; | |
| return legacyRequest<T>(path, { | |
| method: 'POST', | |
| headers, | |
| body: form.toString(), | |
| }); | |
| } | |
| export async function legacyPostJson<T>(path: string, body: unknown): Promise<T> { | |
| return legacyRequest<T>(path, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(body), | |
| }); | |
| } | |
| export async function legacyPostJsonMutation<T>(path: string, body: unknown): Promise<T> { | |
| const token = getCsrfToken(); | |
| const headers: Record<string, string> = { 'Content-Type': 'application/json' }; | |
| if (token) headers['X-Doris-CSRF-Token'] = token; | |
| return legacyRequest<T>(path, { | |
| method: 'POST', | |
| headers, | |
| body: JSON.stringify(body), | |
| }); | |
| } | |
| async function legacyRequest<T>(path: string, init: RequestInit): Promise<T> { | |
| const headers = new Headers(init.headers); | |
| headers.set('Accept', 'application/json'); | |
| const response = await fetch(withRuntimeBasePath(path), { | |
| ...init, | |
| headers, | |
| credentials: 'same-origin', | |
| }); | |
| const requestId = response.headers.get('X-Request-Id') ?? 'unknown'; | |
| let payload: LegacyEnvelope<T> | null = null; | |
| try { | |
| payload = await response.json() as LegacyEnvelope<T>; | |
| } catch { | |
| throw new UiApiError(response.status, { | |
| code: 'UI_INVALID_RESPONSE', | |
| message: 'The server returned invalid JSON.', | |
| requestId, | |
| }); | |
| } | |
| const unauthorized = response.status === 401 || payload.code === 401; | |
| if (unauthorized) { | |
| setCsrfToken(null); | |
| window.dispatchEvent(new CustomEvent('doris-ui:unauthorized')); | |
| } | |
| if (!response.ok || payload.code !== 0 || payload.data === undefined) { | |
| throw new UiApiError(unauthorized ? 401 : response.status || 500, { | |
| code: unauthorized ? 'UI_UNAUTHENTICATED' | |
| : response.status === 403 ? 'UI_FORBIDDEN' | |
| : 'UI_OPERATION_FAILED', | |
| message: payload.msg || 'The operational data could not be loaded.', | |
| requestId, | |
| }); | |
| } | |
| return payload.data; | |
| } | |
| export function adaptLegacyTable(payload: LegacyTablePayload): DynamicTableData { | |
| const columnNames = asStringArray(payload.column_names); | |
| const hrefColumns = asStringArray(payload.href_columns ?? payload.href_column); | |
| const rawRows = Array.isArray(payload.rows) ? payload.rows : []; | |
| return { | |
| columnNames, | |
| rows: rawRows.map((rawRow, rowIndex) => { | |
| const record = rawRow && typeof rawRow === 'object' && !Array.isArray(rawRow) | |
| ? rawRow as Record<string, unknown> | |
| : null; | |
| const cells = Array.isArray(rawRow) | |
| ? columnNames.map((_name, columnIndex) => asCell(rawRow[columnIndex])) | |
| : columnNames.map((name) => asCell(record?.[name])); | |
| const hrefPaths = asStringArray(record?.__hrefPaths); | |
| const links = hrefColumns.reduce<Record<number, string>>((result, name, hrefIndex) => { | |
| const columnIndex = columnNames.indexOf(name); | |
| const href = hrefPaths[hrefIndex]; | |
| if (columnIndex >= 0 && href) result[columnIndex] = href; | |
| return result; | |
| }, {}); | |
| return { key: `row-${rowIndex}`, cells, ...(Object.keys(links).length > 0 ? { links } : {}) }; | |
| }), | |
| }; | |
| } | |
| export function normalizeProcPath(path: string): string { | |
| const decoded = path.trim(); | |
| if (!decoded || decoded === '/') return '/'; | |
| return `/${decoded.split('/').filter(Boolean).join('/')}`; | |
| } | |
| export function procPathFromHref(href: string | null | undefined): string | null { | |
| if (!href) return null; | |
| try { | |
| const url = new URL(href, 'http://doris.local'); | |
| if (url.pathname !== '/rest/v1/system') return null; | |
| return normalizeProcPath(url.searchParams.get('path') ?? '/'); | |
| } catch { | |
| return null; | |
| } | |
| } | |
| export async function fetchSystem(path: string): Promise<SystemResult> { | |
| const normalized = normalizeProcPath(path); | |
| const search = new URLSearchParams({ path: normalized }); | |
| const payload = await legacyGet<LegacyTablePayload>(`/rest/v1/system?${search.toString()}`); | |
| return { | |
| table: adaptLegacyTable(payload), | |
| parentPath: procPathFromHref(typeof payload.parent_url === 'string' ? payload.parent_url : null), | |
| }; | |
| } | |
| export async function fetchSessions(): Promise<DynamicTableData> { | |
| return adaptLegacyTable(await legacyGet<LegacyTablePayload>('/rest/v1/session')); | |
| } | |
| export function useSystem(path: string, enabled: boolean) { | |
| return useQuery({ | |
| queryKey: ['operations', 'system', normalizeProcPath(path)], | |
| queryFn: () => fetchSystem(path), | |
| enabled, | |
| refetchInterval: false, | |
| }); | |
| } | |
| export function useSessions(enabled: boolean) { | |
| return useQuery({ | |
| queryKey: ['operations', 'sessions'], | |
| queryFn: fetchSessions, | |
| enabled, | |
| refetchOnMount: 'always', | |
| refetchInterval: false, | |
| }); | |
| } |