diff --git a/src/containers/AppWithClusters/ExtendedNode/ExtendedNode.tsx b/src/containers/AppWithClusters/ExtendedNode/ExtendedNode.tsx index d26d492a12..dbe55137b5 100644 --- a/src/containers/AppWithClusters/ExtendedNode/ExtendedNode.tsx +++ b/src/containers/AppWithClusters/ExtendedNode/ExtendedNode.tsx @@ -1,8 +1,10 @@ +import {useClusterBaseInfo} from '../../../store/reducers/cluster/cluster'; import type {Node} from '../../Node/Node'; -import {useClusterData} from '../useClusterData'; +import {useAdditionalNodeProps} from '../useClusterData'; export function ExtendedNode({component: NodeComponent}: {component: typeof Node}) { - const {additionalNodesProps} = useClusterData(); + const {balancer} = useClusterBaseInfo(); + const {additionalNodesProps} = useAdditionalNodeProps({balancer}); return ; } diff --git a/src/containers/AppWithClusters/ExtendedTenant/ExtendedTenant.tsx b/src/containers/AppWithClusters/ExtendedTenant/ExtendedTenant.tsx index 36500b21c2..c85ac8521e 100644 --- a/src/containers/AppWithClusters/ExtendedTenant/ExtendedTenant.tsx +++ b/src/containers/AppWithClusters/ExtendedTenant/ExtendedTenant.tsx @@ -1,8 +1,9 @@ import {MonitoringButton} from '../../../components/MonitoringButton/MonitoringButton'; +import {useClusterBaseInfo} from '../../../store/reducers/cluster/cluster'; import type {ETenantType} from '../../../types/api/tenant'; import type {GetMonitoringLink} from '../../../utils/monitoring'; import type {Tenant} from '../../Tenant/Tenant'; -import {useClusterData} from '../useClusterData'; +import {useAdditionalNodeProps} from '../useClusterData'; export interface ExtendedTenantProps { component: typeof Tenant; @@ -13,7 +14,8 @@ export function ExtendedTenant({ component: TenantComponent, getMonitoringLink, }: ExtendedTenantProps) { - const {additionalNodesProps, cluster, monitoring} = useClusterData(); + const {balancer, monitoring} = useClusterBaseInfo(); + const {additionalNodesProps} = useAdditionalNodeProps({balancer}); const additionalTenantProps = { getMonitoringLink: (dbName?: string, dbType?: ETenantType) => { @@ -22,7 +24,6 @@ export function ExtendedTenant({ monitoring, dbName, dbType, - clusterName: cluster?.Name, }); return href ? : null; } diff --git a/src/containers/AppWithClusters/useClusterData.ts b/src/containers/AppWithClusters/useClusterData.ts index 53fece6f65..2f4434b519 100644 --- a/src/containers/AppWithClusters/useClusterData.ts +++ b/src/containers/AppWithClusters/useClusterData.ts @@ -1,16 +1,14 @@ import React from 'react'; -import {useLocation} from 'react-router-dom'; +import {StringParam, useQueryParam} from 'use-query-params'; -import {parseQuery} from '../../routes'; import {clustersApi} from '../../store/reducers/clusters/clusters'; import {getAdditionalNodesProps} from '../../utils/additionalProps'; import {USE_CLUSTER_BALANCER_AS_BACKEND_KEY} from '../../utils/constants'; import {useSetting} from '../../utils/hooks'; export function useClusterData() { - const location = useLocation(); - const {clusterName} = parseQuery(location); + const [clusterName] = useQueryParam('clusterName', StringParam); const {data} = clustersApi.useGetClustersListQuery(undefined); @@ -21,16 +19,19 @@ export function useClusterData() { const {solomon: monitoring, balancer, versions, cluster} = info || {}; - const [useClusterBalancerAsBackend] = useSetting(USE_CLUSTER_BALANCER_AS_BACKEND_KEY); - - const additionalNodesProps = getAdditionalNodesProps(balancer, useClusterBalancerAsBackend); - return { monitoring, balancer, versions, cluster, - useClusterBalancerAsBackend, - additionalNodesProps, + ...useAdditionalNodeProps({balancer}), }; } + +export function useAdditionalNodeProps({balancer}: {balancer?: string}) { + const [useClusterBalancerAsBackend] = useSetting(USE_CLUSTER_BALANCER_AS_BACKEND_KEY); + + const additionalNodesProps = getAdditionalNodesProps(balancer, useClusterBalancerAsBackend); + + return {additionalNodesProps, useClusterBalancerAsBackend}; +} diff --git a/src/containers/Header/Header.tsx b/src/containers/Header/Header.tsx index 9dfd1612aa..6397be1d04 100644 --- a/src/containers/Header/Header.tsx +++ b/src/containers/Header/Header.tsx @@ -1,13 +1,11 @@ import React from 'react'; import {Breadcrumbs} from '@gravity-ui/uikit'; -import {get} from 'lodash'; -import {StringParam, useQueryParams} from 'use-query-params'; import {InternalLink} from '../../components/InternalLink'; import {LinkWithIcon} from '../../components/LinkWithIcon/LinkWithIcon'; import {backend, customBackend} from '../../store'; -import {clusterApi} from '../../store/reducers/cluster/cluster'; +import {useClusterBaseInfo} from '../../store/reducers/cluster/cluster'; import {cn} from '../../utils/cn'; import {DEVELOPER_UI_TITLE} from '../../utils/constants'; import {useTypedSelector} from '../../utils/hooks'; @@ -32,18 +30,12 @@ interface HeaderProps { } function Header({mainPage}: HeaderProps) { - const [queryParams] = useQueryParams({clusterName: StringParam}); - const singleClusterMode = useTypedSelector((state) => state.singleClusterMode); const {page, pageBreadcrumbsOptions} = useTypedSelector((state) => state.header); - const clusterInfo = clusterApi.useGetClusterInfoQuery(queryParams.clusterName ?? undefined); + const clusterInfo = useClusterBaseInfo(); - const clusterName = get( - clusterInfo, - ['currentData', 'clusterData', 'Name'], - queryParams.clusterName, - ); + const clusterName = clusterInfo.title || clusterInfo.name; const breadcrumbItems = React.useMemo(() => { const rawBreadcrumbs: RawBreadcrumbItem[] = []; diff --git a/src/containers/Tenant/Query/ExecuteResult/ExecuteResult.scss b/src/containers/Tenant/Query/ExecuteResult/ExecuteResult.scss index 408a6be1e4..52ec4bdb1e 100644 --- a/src/containers/Tenant/Query/ExecuteResult/ExecuteResult.scss +++ b/src/containers/Tenant/Query/ExecuteResult/ExecuteResult.scss @@ -84,10 +84,4 @@ @include query-buttons-animations(); } } - - &__trace-link { - &_loading { - color: var(--g-color-text-secondary); - } - } } diff --git a/src/containers/Tenant/Query/ExecuteResult/ExecuteResult.tsx b/src/containers/Tenant/Query/ExecuteResult/ExecuteResult.tsx index 6548883d56..e1f3e76b58 100644 --- a/src/containers/Tenant/Query/ExecuteResult/ExecuteResult.tsx +++ b/src/containers/Tenant/Query/ExecuteResult/ExecuteResult.tsx @@ -23,7 +23,6 @@ import {cn} from '../../../../utils/cn'; import {getStringifiedData} from '../../../../utils/dataFormatters/dataFormatters'; import {useTypedDispatch} from '../../../../utils/hooks'; import {parseQueryError} from '../../../../utils/query'; -import {ClusterModeGuard} from '../../../ClusterModeGuard'; import {PaneVisibilityToggleButtons} from '../../utils/paneVisibilityToggleHelpers'; import {SimplifiedPlan} from '../ExplainResult/components/SimplifiedPlan/SimplifiedPlan'; import {ResultIssues} from '../Issues/Issues'; @@ -275,9 +274,7 @@ export function ExecuteResult({ ) : null} - - - + {data?.traceId ? : null}
{renderClipboardButton()} diff --git a/src/containers/Tenant/Query/ExecuteResult/TraceButton.tsx b/src/containers/Tenant/Query/ExecuteResult/TraceButton.tsx index 0e659c6fcd..e07042390d 100644 --- a/src/containers/Tenant/Query/ExecuteResult/TraceButton.tsx +++ b/src/containers/Tenant/Query/ExecuteResult/TraceButton.tsx @@ -1,49 +1,27 @@ import React from 'react'; +import {ArrowUpRightFromSquare} from '@gravity-ui/icons'; import {Button} from '@gravity-ui/uikit'; -import {StringParam, useQueryParams} from 'use-query-params'; -import {LinkWithIcon} from '../../../../components/LinkWithIcon/LinkWithIcon'; -import {clusterApi} from '../../../../store/reducers/cluster/cluster'; +import {useClusterBaseInfo} from '../../../../store/reducers/cluster/cluster'; import {traceApi} from '../../../../store/reducers/trace'; -import type {TClusterInfo} from '../../../../types/api/cluster'; -import {cn} from '../../../../utils/cn'; import {SECOND_IN_MS} from '../../../../utils/constants'; import {useDelayed} from '../../../../utils/hooks/useDelayed'; import {replaceParams} from '../utils/replaceParams'; import i18n from './i18n'; -const b = cn('ydb-query-execute-result'); - const TIME_BEFORE_CHECK = 15 * SECOND_IN_MS; interface TraceUrlButtonProps { - traceId?: string; -} - -function hasValidTraceCheckUrl(cluster?: TClusterInfo): cluster is {TraceCheck: {url: string}} { - return Boolean(cluster && cluster.TraceCheck && typeof cluster.TraceCheck.url === 'string'); -} - -function hasValidTraceViewUrl(cluster?: TClusterInfo): cluster is {TraceView: {url: string}} { - return Boolean(cluster && cluster.TraceView && typeof cluster.TraceView.url === 'string'); + traceId: string; } export function TraceButton({traceId}: TraceUrlButtonProps) { - const [queryParams] = useQueryParams({clusterName: StringParam}); + const {traceCheck, traceView} = useClusterBaseInfo(); - const {data: {clusterData: cluster} = {}} = clusterApi.useGetClusterInfoQuery( - queryParams.clusterName ?? undefined, - {skip: !traceId}, - ); - - const hasTraceCheck = Boolean(hasValidTraceCheckUrl(cluster) && traceId); - - const checkTraceUrl = - traceId && hasValidTraceCheckUrl(cluster) - ? replaceParams(cluster.TraceCheck.url, {traceId}) - : ''; + const checkTraceUrl = traceCheck?.url ? replaceParams(traceCheck.url, {traceId}) : ''; + const traceUrl = traceView?.url ? replaceParams(traceView.url, {traceId}) : ''; // We won't get any trace data at first 15 seconds for sure const [readyToFetch, resetDelay] = useDelayed(TIME_BEFORE_CHECK); @@ -52,28 +30,27 @@ export function TraceButton({traceId}: TraceUrlButtonProps) { resetDelay(); }, [traceId, resetDelay]); - const {currentData: traceData, error: traceError} = traceApi.useCheckTraceQuery( + const {isFetching} = traceApi.useCheckTraceQuery( {url: checkTraceUrl}, - {skip: !hasTraceCheck || !readyToFetch}, + {skip: !checkTraceUrl || !readyToFetch}, ); - const traceUrl = - hasValidTraceViewUrl(cluster) && traceId - ? replaceParams(cluster.TraceView.url, {traceId}) - : ''; - if (!traceUrl) { return null; } + const loading = !readyToFetch || isFetching; return ( - ); } diff --git a/src/services/api.ts b/src/services/api.ts index ef0e45daed..7483a6b21b 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -14,7 +14,12 @@ import type {TComputeInfo} from '../types/api/compute'; import type {DescribeConsumerResult} from '../types/api/consumer'; import type {HealthCheckAPIResponse} from '../types/api/healthcheck'; import type {JsonHotKeysResponse} from '../types/api/hotkeys'; -import type {MetaCluster, MetaClusters, MetaTenants} from '../types/api/meta'; +import type { + MetaCluster, + MetaClusters, + MetaGeneralClusterInfo, + MetaTenants, +} from '../types/api/meta'; import type {ModifyDiskResponse} from '../types/api/modifyDisk'; import type {TNetInfo} from '../types/api/netInfo'; import type {TNodesInfo} from '../types/api/nodes'; @@ -749,13 +754,6 @@ export class YdbEmbeddedAPI extends AxiosWrapper { ); } - // used if not single cluster mode - getClustersList(_?: never, {signal}: {signal?: AbortSignal} = {}) { - return this.get(`${META_BACKEND || ''}/meta/clusters`, null, { - requestConfig: {signal}, - }); - } - createSchemaDirectory( {database, path}: {database: string; path: string}, {signal}: {signal?: AbortSignal} = {}, @@ -772,9 +770,26 @@ export class YdbEmbeddedAPI extends AxiosWrapper { }, ); } + + getClustersList(_?: never, __: {signal?: AbortSignal} = {}): Promise { + throw new Error('Method is not implemented.'); + } + + getClusterBaseInfo( + _clusterName: string, + _opts: AxiosOptions = {}, + ): Promise { + throw new Error('Method is not implemented.'); + } } export class YdbWebVersionAPI extends YdbEmbeddedAPI { + getClustersList(_?: never, {signal}: {signal?: AbortSignal} = {}) { + return this.get(`${META_BACKEND || ''}/meta/clusters`, null, { + requestConfig: {signal}, + }); + } + getClusterInfo(clusterName: string, {signal}: AxiosOptions = {}) { return this.get( `${META_BACKEND || ''}/meta/cluster`, @@ -785,15 +800,28 @@ export class YdbWebVersionAPI extends YdbEmbeddedAPI { ).then(parseMetaCluster); } - getTenants(clusterName: string, {concurrentId, signal}: AxiosOptions = {}) { + getTenants(clusterName: string, {signal}: AxiosOptions = {}) { return this.get( `${META_BACKEND || ''}/meta/cp_databases`, { cluster_name: clusterName, }, - {concurrentId, requestConfig: {signal}}, + {requestConfig: {signal}}, ).then(parseMetaTenants); } + + getClusterBaseInfo( + clusterName: string, + {concurrentId, signal}: AxiosOptions = {}, + ): Promise { + return this.get( + `${META_BACKEND || ''}/meta/db_clusters`, + { + name: clusterName, + }, + {concurrentId, requestConfig: {signal}}, + ).then((data) => data[0]); + } } export function createApi({webVersion = false, withCredentials = false} = {}) { diff --git a/src/services/parsers/parseMetaCluster.ts b/src/services/parsers/parseMetaCluster.ts index e6a11e92d9..5b4cbd09c8 100644 --- a/src/services/parsers/parseMetaCluster.ts +++ b/src/services/parsers/parseMetaCluster.ts @@ -1,14 +1,6 @@ import type {TClusterInfo} from '../../types/api/cluster'; import type {MetaCluster} from '../../types/api/meta'; -import type {TTraceCheck, TTraceView} from '../../types/api/trace'; - -function isTraceCheck(obj: Partial): obj is TTraceCheck { - return Boolean(obj instanceof Object && obj.url); -} - -function isTraceView(obj: Partial): obj is TTraceView { - return Boolean(obj instanceof Object && obj.url); -} +import {traceCheckSchema, traceViewSchema} from '../../types/api/trace'; export const parseMetaCluster = (data: MetaCluster): TClusterInfo => { const {cluster = {}} = data; @@ -21,30 +13,10 @@ export const parseMetaCluster = (data: MetaCluster): TClusterInfo => { trace_view: traceView, } = cluster; - let TraceCheck: TTraceCheck | undefined; - let TraceView: TTraceView | undefined; - - try { - if (traceCheck) { - const parsedTraceCheck = JSON.parse(traceCheck); - if (isTraceCheck(parsedTraceCheck)) { - TraceCheck = parsedTraceCheck; - } else { - console.error('Parsed traceCheck does not match TTraceCheck structure'); - } - } - - if (traceView) { - const parsedTraceView = JSON.parse(traceView); - if (isTraceView(parsedTraceView)) { - TraceView = parsedTraceView; - } else { - console.error('Parsed traceView does not match TTraceView structure'); - } - } - } catch (e) { - console.error('Error parsing JSON:', e); - } + const {traceCheck: TraceCheck, traceView: TraceView} = parseTraceFields({ + traceCheck, + traceView, + }); return { ...generalClusterInfo, @@ -56,3 +28,22 @@ export const parseMetaCluster = (data: MetaCluster): TClusterInfo => { TraceView, }; }; + +export function parseTraceFields({ + traceCheck, + traceView, +}: { + traceCheck?: string; + traceView?: string; +}) { + try { + return { + traceCheck: traceCheck ? traceCheckSchema.parse(JSON.parse(traceCheck)) : undefined, + traceView: traceView ? traceViewSchema.parse(JSON.parse(traceView)) : undefined, + }; + } catch (e) { + console.error('Error parsing trace fields:', e); + } + + return {}; +} diff --git a/src/store/reducers/cluster/cluster.ts b/src/store/reducers/cluster/cluster.ts index ad98920ce6..b5aa65b1ae 100644 --- a/src/store/reducers/cluster/cluster.ts +++ b/src/store/reducers/cluster/cluster.ts @@ -1,8 +1,11 @@ import {createSlice} from '@reduxjs/toolkit'; import type {Dispatch, PayloadAction} from '@reduxjs/toolkit'; +import {skipToken} from '@reduxjs/toolkit/query'; +import {StringParam, useQueryParam} from 'use-query-params'; import type {ClusterTab} from '../../../containers/Cluster/utils'; import {clusterTabsIds, isClusterTab} from '../../../containers/Cluster/utils'; +import {parseTraceFields} from '../../../services/parsers/parseMetaCluster'; import type {TClusterInfo} from '../../../types/api/cluster'; import {DEFAULT_CLUSTER_TAB_KEY} from '../../../utils/constants'; import {isQueryErrorResponse} from '../../../utils/query'; @@ -96,6 +99,38 @@ export const clusterApi = api.injectEndpoints({ }, providesTags: ['All'], }), + getClusterBaseInfo: builder.query({ + queryFn: async (clusterName: string, {signal}) => { + try { + const data = await window.api.getClusterBaseInfo(clusterName, {signal}); + return {data}; + } catch (error) { + return {error}; + } + }, + providesTags: ['All'], + }), }), overrideExisting: 'throw', }); + +export function useClusterBaseInfo() { + const [clusterName] = useQueryParam('clusterName', StringParam); + + const {currentData} = clusterApi.useGetClusterBaseInfoQuery(clusterName ?? skipToken); + + const { + solomon: monitoring, + name, + trace_check: traceCheck, + trace_view: traceView, + ...data + } = currentData || {}; + + return { + ...data, + ...parseTraceFields({traceCheck, traceView}), + name: name ?? clusterName ?? undefined, + monitoring, + }; +} diff --git a/src/store/reducers/trace.ts b/src/store/reducers/trace.ts index 3bb88c320d..6d1ec2ae1a 100644 --- a/src/store/reducers/trace.ts +++ b/src/store/reducers/trace.ts @@ -1,19 +1,14 @@ -import type {BaseQueryFn, EndpointBuilder} from '@reduxjs/toolkit/query'; - import {api} from './api'; interface CheckTraceParams { - url?: string; + url: string; } -function endpoints(build: EndpointBuilder) { - return { +export const traceApi = api.injectEndpoints({ + endpoints: (build) => ({ checkTrace: build.query({ queryFn: async ({url}: CheckTraceParams, {signal}) => { try { - if (!url) { - throw new Error('no tracecheck url provided'); - } const response = await window.api.checkTrace({url}, {signal}); return {data: response}; } catch (error) { @@ -21,7 +16,6 @@ function endpoints(build: EndpointBuilder) { } }, }), - }; -} - -export const traceApi = api.injectEndpoints({endpoints}); + }), + overrideExisting: 'throw', +}); diff --git a/src/types/api/trace.ts b/src/types/api/trace.ts index ddbce00d50..6136624aaf 100644 --- a/src/types/api/trace.ts +++ b/src/types/api/trace.ts @@ -1,9 +1,13 @@ -import type {AxiosRequestConfig} from 'axios'; +import {z} from 'zod'; -export interface TTraceCheck { - url: AxiosRequestConfig['url']; -} +export const traceCheckSchema = z.object({ + url: z.string().url(), +}); -export interface TTraceView { - url: string; -} +export type TTraceCheck = z.infer; + +export const traceViewSchema = z.object({ + url: z.string().url(), +}); + +export type TTraceView = z.infer;