Skip to content

Commit 6e45802

Browse files
feat: add endpoint to connect to db code snippets (#2198)
1 parent 5c22404 commit 6e45802

File tree

12 files changed

+137
-36
lines changed

12 files changed

+137
-36
lines changed

src/components/ConnectToDB/ConnectToDBDialog.tsx

+31-7
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@ import React from 'react';
22

33
import NiceModal from '@ebay/nice-modal-react';
44
import {Dialog, Tabs} from '@gravity-ui/uikit';
5+
import {skipToken} from '@reduxjs/toolkit/query';
56

7+
import {tenantApi} from '../../store/reducers/tenant/tenant';
68
import {cn} from '../../utils/cn';
9+
import {useTypedSelector} from '../../utils/hooks';
10+
import {useClusterNameFromQuery} from '../../utils/hooks/useDatabaseFromQuery';
711
import {LinkWithIcon} from '../LinkWithIcon/LinkWithIcon';
12+
import {LoaderWrapper} from '../LoaderWrapper/LoaderWrapper';
813
import {YDBSyntaxHighlighterLazy} from '../SyntaxHighlighter/lazy';
914

1015
import {getDocsLink} from './getDocsLink';
@@ -32,9 +37,26 @@ interface ConnectToDBDialogProps extends SnippetParams {
3237
onClose: VoidFunction;
3338
}
3439

35-
function ConnectToDBDialog({open, onClose, database, endpoint}: ConnectToDBDialogProps) {
40+
function ConnectToDBDialog({
41+
open,
42+
onClose,
43+
database,
44+
endpoint: endpointFromProps,
45+
}: ConnectToDBDialogProps) {
3646
const [activeTab, setActiveTab] = React.useState<SnippetLanguage>('bash');
3747

48+
const clusterName = useClusterNameFromQuery();
49+
const singleClusterMode = useTypedSelector((state) => state.singleClusterMode);
50+
51+
// If there is endpoint from props, we don't need to request tenant data
52+
// Also we should not request tenant data if we are in single cluster mode
53+
// Since there is no ControlPlane data in this case
54+
const shouldRequestTenantData = database && !endpointFromProps && !singleClusterMode;
55+
const params = shouldRequestTenantData ? {path: database, clusterName} : skipToken;
56+
const {currentData: tenantData, isLoading: isTenantDataLoading} =
57+
tenantApi.useGetTenantInfoQuery(params);
58+
const endpoint = endpointFromProps ?? tenantData?.ControlPlane?.endpoint;
59+
3860
const snippet = getSnippetCode(activeTab, {database, endpoint});
3961
const docsLink = getDocsLink(activeTab);
4062

@@ -52,12 +74,14 @@ function ConnectToDBDialog({open, onClose, database, endpoint}: ConnectToDBDialo
5274
className={b('dialog-tabs')}
5375
/>
5476
<div className={b('snippet-container')}>
55-
<YDBSyntaxHighlighterLazy
56-
language={activeTab}
57-
text={snippet}
58-
transparentBackground={false}
59-
withClipboardButton={{alwaysVisible: true}}
60-
/>
77+
<LoaderWrapper loading={isTenantDataLoading}>
78+
<YDBSyntaxHighlighterLazy
79+
language={activeTab}
80+
text={snippet}
81+
transparentBackground={false}
82+
withClipboardButton={{alwaysVisible: true}}
83+
/>
84+
</LoaderWrapper>
6185
</div>
6286
{docsLink ? (
6387
<LinkWithIcon
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import {prepareEndpoint} from '../utils';
2+
3+
describe('prepareEndpoint', () => {
4+
test('should remove all search params', () => {
5+
const input = 'grpc://example.com:2139/?database=/root/test&param=value';
6+
const expected = 'grpc://example.com:2139';
7+
expect(prepareEndpoint(input)).toBe(expected);
8+
});
9+
test('should handle URL without path or params', () => {
10+
const input = 'grpc://example.com:2139';
11+
const expected = 'grpc://example.com:2139';
12+
expect(prepareEndpoint(input)).toBe(expected);
13+
});
14+
test('should remove trailing slash from path', () => {
15+
const input = 'grpc://example.com:2139/';
16+
const expected = 'grpc://example.com:2139';
17+
expect(prepareEndpoint(input)).toBe(expected);
18+
});
19+
test('should handle complex paths', () => {
20+
const input = 'grpc://example.com:2139/multi/level/path/?database=/root/test';
21+
const expected = 'grpc://example.com:2139/multi/level/path';
22+
expect(prepareEndpoint(input)).toBe(expected);
23+
});
24+
test('should handle empty string', () => {
25+
expect(prepareEndpoint('')).toBeUndefined();
26+
});
27+
test('should handle undefined input', () => {
28+
expect(prepareEndpoint()).toBeUndefined();
29+
});
30+
test('should return undefined for invalid URL', () => {
31+
const input = 'invalid-url';
32+
expect(prepareEndpoint(input)).toBeUndefined();
33+
});
34+
});

src/components/ConnectToDB/snippets.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type {SnippetLanguage, SnippetParams} from './types';
2+
import {prepareEndpoint} from './utils';
23

34
export function getBashSnippetCode({database, endpoint}: SnippetParams) {
45
return `ydb -e ${endpoint || '<endpoint>'} --token-file ~/my_token
@@ -198,7 +199,12 @@ with ydb.Driver(driver_config) as driver:
198199
print(driver.discovery_debug_details())`;
199200
}
200201

201-
export function getSnippetCode(lang: SnippetLanguage, params: SnippetParams) {
202+
export function getSnippetCode(lang: SnippetLanguage, rawParams: SnippetParams) {
203+
const params = {
204+
...rawParams,
205+
endpoint: prepareEndpoint(rawParams.endpoint),
206+
};
207+
202208
switch (lang) {
203209
case 'cpp': {
204210
return getCPPSnippetCode(params);

src/components/ConnectToDB/utils.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// We have endpoint in format grpc://example.com:2139/?database=/root/test
2+
// We need it to be like grpc://example.com:2139 to make code in snippets work
3+
// We pass database to snippets as a separate param
4+
export function prepareEndpoint(connectionString = '') {
5+
try {
6+
const urlObj = new URL(connectionString);
7+
urlObj.search = '';
8+
9+
let endpoint = urlObj.toString();
10+
11+
// Remove trailing slash if present
12+
if (endpoint.endsWith('/')) {
13+
endpoint = endpoint.slice(0, -1);
14+
}
15+
16+
return endpoint;
17+
} catch {
18+
return undefined;
19+
}
20+
}

src/containers/Header/Header.tsx

+4-6
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,12 @@ function Header({mainPage}: HeaderProps) {
3030
const {page, pageBreadcrumbsOptions} = useTypedSelector((state) => state.header);
3131
const isUserAllowedToMakeChanges = useIsUserAllowedToMakeChanges();
3232

33-
const clusterInfo = useClusterBaseInfo();
33+
const {title: clusterTitle} = useClusterBaseInfo();
3434

3535
const database = useDatabaseFromQuery();
3636
const location = useLocation();
3737
const isDatabasePage = location.pathname === '/tenant';
3838

39-
const clusterName = clusterInfo.title || clusterInfo.name;
40-
4139
const breadcrumbItems = React.useMemo(() => {
4240
const rawBreadcrumbs: RawBreadcrumbItem[] = [];
4341
let options = pageBreadcrumbsOptions;
@@ -46,10 +44,10 @@ function Header({mainPage}: HeaderProps) {
4644
rawBreadcrumbs.push(mainPage);
4745
}
4846

49-
if (clusterName) {
47+
if (clusterTitle) {
5048
options = {
5149
...options,
52-
clusterName,
50+
clusterName: clusterTitle,
5351
};
5452
}
5553

@@ -58,7 +56,7 @@ function Header({mainPage}: HeaderProps) {
5856
return breadcrumbs.map((item) => {
5957
return {...item, action: () => {}};
6058
});
61-
}, [clusterName, mainPage, page, pageBreadcrumbsOptions]);
59+
}, [clusterTitle, mainPage, page, pageBreadcrumbsOptions]);
6260

6361
const renderRightControls = () => {
6462
const elements: React.ReactNode[] = [];

src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {calculateTenantMetrics} from '../../../../store/reducers/tenants/utils';
1010
import type {AdditionalNodesProps, AdditionalTenantsProps} from '../../../../types/additionalProps';
1111
import {TENANT_DEFAULT_TITLE} from '../../../../utils/constants';
1212
import {useAutoRefreshInterval, useTypedSelector} from '../../../../utils/hooks';
13+
import {useClusterNameFromQuery} from '../../../../utils/hooks/useDatabaseFromQuery';
1314
import {mapDatabaseTypeToDBName} from '../../utils/schema';
1415

1516
import {DefaultOverviewContent} from './DefaultOverviewContent/DefaultOverviewContent';
@@ -35,12 +36,11 @@ export function TenantOverview({
3536
}: TenantOverviewProps) {
3637
const {metricsTab} = useTypedSelector((state) => state.tenant);
3738
const [autoRefreshInterval] = useAutoRefreshInterval();
39+
const clusterName = useClusterNameFromQuery();
3840

3941
const {currentData: tenant, isFetching} = tenantApi.useGetTenantInfoQuery(
40-
{path: tenantName},
41-
{
42-
pollingInterval: autoRefreshInterval,
43-
},
42+
{path: tenantName, clusterName},
43+
{pollingInterval: autoRefreshInterval},
4444
);
4545
const tenantLoading = isFetching && tenant === undefined;
4646
const {Name, Type, Overall} = tenant || {};

src/containers/Tenant/ObjectSummary/SchemaTree/SchemaTree.tsx

+2-7
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,7 @@ import {schemaApi} from '../../../../store/reducers/schema/schema';
1212
import {tableSchemaDataApi} from '../../../../store/reducers/tableSchemaData';
1313
import type {EPathType, TEvDescribeSchemeResult} from '../../../../types/api/schema';
1414
import {valueIsDefined} from '../../../../utils';
15-
import {
16-
useQueryExecutionSettings,
17-
useTypedDispatch,
18-
useTypedSelector,
19-
} from '../../../../utils/hooks';
15+
import {useTypedDispatch, useTypedSelector} from '../../../../utils/hooks';
2016
import {getConfirmation} from '../../../../utils/hooks/withConfirmation/useChangeInputWithConfirmation';
2117
import {getSchemaControls} from '../../utils/controls';
2218
import {
@@ -48,7 +44,6 @@ export function SchemaTree(props: SchemaTreeProps) {
4844
{currentData: actionsSchemaData, isFetching: isActionsDataFetching},
4945
] = tableSchemaDataApi.useLazyGetTableSchemaDataQuery();
5046

51-
const [querySettings] = useQueryExecutionSettings();
5247
const [createDirectoryOpen, setCreateDirectoryOpen] = React.useState(false);
5348
const [parentPath, setParentPath] = React.useState('');
5449
const setSchemaTreeKey = useDispatchTreeKey();
@@ -144,8 +139,8 @@ export function SchemaTree(props: SchemaTreeProps) {
144139
dispatch,
145140
input,
146141
isActionsDataFetching,
142+
isDirty,
147143
onActivePathUpdate,
148-
querySettings,
149144
rootPath,
150145
]);
151146

src/services/api/meta.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,15 @@ export class MetaAPI extends BaseYdbAPI {
3030
});
3131
}
3232

33-
getTenants(clusterName?: string, {signal}: AxiosOptions = {}) {
33+
getTenants(
34+
{clusterName, databaseName}: {clusterName?: string; databaseName?: string},
35+
{signal}: AxiosOptions = {},
36+
) {
3437
return this.get<MetaTenants>(
3538
this.getPath('/meta/cp_databases'),
3639
{
3740
cluster_name: clusterName,
41+
database_name: databaseName,
3842
},
3943
{requestConfig: {signal}},
4044
).then(parseMetaTenants);

src/services/api/viewer.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export class ViewerAPI extends BaseYdbAPI {
6565
);
6666
}
6767

68-
getTenants(clusterName?: string, {concurrentId, signal}: AxiosOptions = {}) {
68+
getTenants({clusterName}: {clusterName?: string}, {concurrentId, signal}: AxiosOptions = {}) {
6969
return this.get<TTenantInfo>(
7070
this.getPath('/viewer/json/tenantinfo'),
7171
{

src/store/reducers/cluster/cluster.ts

+13-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import {createSelector, createSlice} from '@reduxjs/toolkit';
22
import type {Dispatch, PayloadAction} from '@reduxjs/toolkit';
33
import {skipToken} from '@reduxjs/toolkit/query';
4-
import {StringParam, useQueryParam} from 'use-query-params';
54

65
import type {ClusterTab} from '../../../containers/Cluster/utils';
76
import {clusterTabsIds, isClusterTab} from '../../../containers/Cluster/utils';
@@ -10,6 +9,7 @@ import {isClusterInfoV2} from '../../../types/api/cluster';
109
import type {TClusterInfo} from '../../../types/api/cluster';
1110
import type {TTabletStateInfo} from '../../../types/api/tablet';
1211
import {CLUSTER_DEFAULT_TITLE, DEFAULT_CLUSTER_TAB_KEY} from '../../../utils/constants';
12+
import {useClusterNameFromQuery} from '../../../utils/hooks/useDatabaseFromQuery';
1313
import {isQueryErrorResponse} from '../../../utils/query';
1414
import type {RootState} from '../../defaultStore';
1515
import {api} from '../api';
@@ -136,16 +136,24 @@ export const clusterApi = api.injectEndpoints({
136136
});
137137

138138
export function useClusterBaseInfo() {
139-
const [clusterName] = useQueryParam('clusterName', StringParam);
139+
const clusterNameFromQuery = useClusterNameFromQuery();
140140

141-
const {currentData} = clusterApi.useGetClusterBaseInfoQuery(clusterName ?? skipToken);
141+
const {currentData} = clusterApi.useGetClusterBaseInfoQuery(clusterNameFromQuery ?? skipToken);
142142

143-
const {solomon: monitoring, name, trace_view: traceView, ...data} = currentData || {};
143+
const {solomon: monitoring, name, title, trace_view: traceView, ...data} = currentData || {};
144+
145+
// name is used for requests, title is used for display
146+
// Example:
147+
// Name: ydb_vla_dev02
148+
// Title: YDB DEV VLA02
149+
const clusterName = name ?? clusterNameFromQuery ?? undefined;
150+
const clusterTitle = title ?? clusterName;
144151

145152
return {
146153
...data,
147154
...parseTraceFields({traceView}),
148-
name: name ?? clusterName ?? undefined,
155+
name: clusterName,
156+
title: clusterTitle,
149157
monitoring,
150158
};
151159
}

src/store/reducers/tenant/tenant.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {createSlice} from '@reduxjs/toolkit';
22
import type {PayloadAction} from '@reduxjs/toolkit';
33

44
import {DEFAULT_USER_SETTINGS, settingsManager} from '../../../services/settings';
5+
import type {TTenantInfo} from '../../../types/api/tenant';
56
import {TENANT_INITIAL_PAGE_KEY} from '../../../utils/constants';
67
import {api} from '../api';
78

@@ -52,9 +53,20 @@ export const {setTenantPage, setQueryTab, setDiagnosticsTab, setSummaryTab, setM
5253
export const tenantApi = api.injectEndpoints({
5354
endpoints: (builder) => ({
5455
getTenantInfo: builder.query({
55-
queryFn: async ({path}: {path: string}, {signal}) => {
56+
queryFn: async (
57+
{path, clusterName}: {path: string; clusterName?: string},
58+
{signal},
59+
) => {
5660
try {
57-
const tenantData = await window.api.viewer.getTenantInfo({path}, {signal});
61+
let tenantData: TTenantInfo;
62+
if (window.api.meta && clusterName) {
63+
tenantData = await window.api.meta.getTenants(
64+
{databaseName: path, clusterName},
65+
{signal},
66+
);
67+
} else {
68+
tenantData = await window.api.viewer.getTenantInfo({path}, {signal});
69+
}
5870
return {data: tenantData.TenantInfo?.[0] ?? null};
5971
} catch (error) {
6072
return {error};

src/store/reducers/tenants/tenants.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ export const tenantsApi = api.injectEndpoints({
2727
queryFn: async ({clusterName}: {clusterName?: string}, {signal}) => {
2828
try {
2929
const response = window.api.meta
30-
? await window.api.meta.getTenants(clusterName, {signal})
31-
: await window.api.viewer.getTenants(clusterName, {signal});
30+
? await window.api.meta.getTenants({clusterName}, {signal})
31+
: await window.api.viewer.getTenants({clusterName}, {signal});
3232
let data: PreparedTenant[];
3333
if (Array.isArray(response.TenantInfo)) {
3434
data = prepareTenants(response.TenantInfo);

0 commit comments

Comments
 (0)