Skip to content

feat: add endpoint to connect to db code snippets #2198

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 31 additions & 7 deletions src/components/ConnectToDB/ConnectToDBDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ import React from 'react';

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

import {tenantApi} from '../../store/reducers/tenant/tenant';
import {cn} from '../../utils/cn';
import {useTypedSelector} from '../../utils/hooks';
import {useClusterNameFromQuery} from '../../utils/hooks/useDatabaseFromQuery';
import {LinkWithIcon} from '../LinkWithIcon/LinkWithIcon';
import {LoaderWrapper} from '../LoaderWrapper/LoaderWrapper';
import {YDBSyntaxHighlighterLazy} from '../SyntaxHighlighter/lazy';

import {getDocsLink} from './getDocsLink';
Expand Down Expand Up @@ -32,9 +37,26 @@ interface ConnectToDBDialogProps extends SnippetParams {
onClose: VoidFunction;
}

function ConnectToDBDialog({open, onClose, database, endpoint}: ConnectToDBDialogProps) {
function ConnectToDBDialog({
open,
onClose,
database,
endpoint: endpointFromProps,
}: ConnectToDBDialogProps) {
const [activeTab, setActiveTab] = React.useState<SnippetLanguage>('bash');

const clusterName = useClusterNameFromQuery();
const singleClusterMode = useTypedSelector((state) => state.singleClusterMode);

// If there is endpoint from props, we don't need to request tenant data
// Also we should not request tenant data if we are in single cluster mode
// Since there is no ControlPlane data in this case
const shouldRequestTenantData = database && !endpointFromProps && !singleClusterMode;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left endpoint in props to make possible to use dialog in Databases table, where we know all the params from table data and do not need to request them additionally

const params = shouldRequestTenantData ? {path: database, clusterName} : skipToken;
const {currentData: tenantData, isLoading: isTenantDataLoading} =
tenantApi.useGetTenantInfoQuery(params);
const endpoint = endpointFromProps ?? tenantData?.ControlPlane?.endpoint;

const snippet = getSnippetCode(activeTab, {database, endpoint});
const docsLink = getDocsLink(activeTab);

Expand All @@ -52,12 +74,14 @@ function ConnectToDBDialog({open, onClose, database, endpoint}: ConnectToDBDialo
className={b('dialog-tabs')}
/>
<div className={b('snippet-container')}>
<YDBSyntaxHighlighterLazy
language={activeTab}
text={snippet}
transparentBackground={false}
withClipboardButton={{alwaysVisible: true}}
/>
<LoaderWrapper loading={isTenantDataLoading}>
<YDBSyntaxHighlighterLazy
language={activeTab}
text={snippet}
transparentBackground={false}
withClipboardButton={{alwaysVisible: true}}
/>
</LoaderWrapper>
</div>
{docsLink ? (
<LinkWithIcon
Expand Down
34 changes: 34 additions & 0 deletions src/components/ConnectToDB/__test__/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {prepareEndpoint} from '../utils';

describe('prepareEndpoint', () => {
test('should remove all search params', () => {
const input = 'grpc://example.com:2139/?database=/root/test&param=value';
const expected = 'grpc://example.com:2139';
expect(prepareEndpoint(input)).toBe(expected);
});
test('should handle URL without path or params', () => {
const input = 'grpc://example.com:2139';
const expected = 'grpc://example.com:2139';
expect(prepareEndpoint(input)).toBe(expected);
});
test('should remove trailing slash from path', () => {
const input = 'grpc://example.com:2139/';
const expected = 'grpc://example.com:2139';
expect(prepareEndpoint(input)).toBe(expected);
});
test('should handle complex paths', () => {
const input = 'grpc://example.com:2139/multi/level/path/?database=/root/test';
const expected = 'grpc://example.com:2139/multi/level/path';
expect(prepareEndpoint(input)).toBe(expected);
});
test('should handle empty string', () => {
expect(prepareEndpoint('')).toBeUndefined();
});
test('should handle undefined input', () => {
expect(prepareEndpoint()).toBeUndefined();
});
test('should return undefined for invalid URL', () => {
const input = 'invalid-url';
expect(prepareEndpoint(input)).toBeUndefined();
});
});
8 changes: 7 additions & 1 deletion src/components/ConnectToDB/snippets.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {SnippetLanguage, SnippetParams} from './types';
import {prepareEndpoint} from './utils';

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

export function getSnippetCode(lang: SnippetLanguage, params: SnippetParams) {
export function getSnippetCode(lang: SnippetLanguage, rawParams: SnippetParams) {
const params = {
...rawParams,
endpoint: prepareEndpoint(rawParams.endpoint),
};

switch (lang) {
case 'cpp': {
return getCPPSnippetCode(params);
Expand Down
20 changes: 20 additions & 0 deletions src/components/ConnectToDB/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// We have endpoint in format grpc://example.com:2139/?database=/root/test
// We need it to be like grpc://example.com:2139 to make code in snippets work
// We pass database to snippets as a separate param
export function prepareEndpoint(connectionString = '') {
try {
const urlObj = new URL(connectionString);
urlObj.search = '';

let endpoint = urlObj.toString();

// Remove trailing slash if present
if (endpoint.endsWith('/')) {
endpoint = endpoint.slice(0, -1);
}

return endpoint;
} catch {
return undefined;
}
}
10 changes: 4 additions & 6 deletions src/containers/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,12 @@ function Header({mainPage}: HeaderProps) {
const {page, pageBreadcrumbsOptions} = useTypedSelector((state) => state.header);
const isUserAllowedToMakeChanges = useIsUserAllowedToMakeChanges();

const clusterInfo = useClusterBaseInfo();
const {title: clusterTitle} = useClusterBaseInfo();

const database = useDatabaseFromQuery();
const location = useLocation();
const isDatabasePage = location.pathname === '/tenant';

const clusterName = clusterInfo.title || clusterInfo.name;

const breadcrumbItems = React.useMemo(() => {
const rawBreadcrumbs: RawBreadcrumbItem[] = [];
let options = pageBreadcrumbsOptions;
Expand All @@ -46,10 +44,10 @@ function Header({mainPage}: HeaderProps) {
rawBreadcrumbs.push(mainPage);
}

if (clusterName) {
if (clusterTitle) {
options = {
...options,
clusterName,
clusterName: clusterTitle,
};
}

Expand All @@ -58,7 +56,7 @@ function Header({mainPage}: HeaderProps) {
return breadcrumbs.map((item) => {
return {...item, action: () => {}};
});
}, [clusterName, mainPage, page, pageBreadcrumbsOptions]);
}, [clusterTitle, mainPage, page, pageBreadcrumbsOptions]);

const renderRightControls = () => {
const elements: React.ReactNode[] = [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {calculateTenantMetrics} from '../../../../store/reducers/tenants/utils';
import type {AdditionalNodesProps, AdditionalTenantsProps} from '../../../../types/additionalProps';
import {TENANT_DEFAULT_TITLE} from '../../../../utils/constants';
import {useAutoRefreshInterval, useTypedSelector} from '../../../../utils/hooks';
import {useClusterNameFromQuery} from '../../../../utils/hooks/useDatabaseFromQuery';
import {mapDatabaseTypeToDBName} from '../../utils/schema';

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

const {currentData: tenant, isFetching} = tenantApi.useGetTenantInfoQuery(
{path: tenantName},
{
pollingInterval: autoRefreshInterval,
},
{path: tenantName, clusterName},
{pollingInterval: autoRefreshInterval},
);
const tenantLoading = isFetching && tenant === undefined;
const {Name, Type, Overall} = tenant || {};
Expand Down
9 changes: 2 additions & 7 deletions src/containers/Tenant/ObjectSummary/SchemaTree/SchemaTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,7 @@ import {schemaApi} from '../../../../store/reducers/schema/schema';
import {tableSchemaDataApi} from '../../../../store/reducers/tableSchemaData';
import type {EPathType, TEvDescribeSchemeResult} from '../../../../types/api/schema';
import {valueIsDefined} from '../../../../utils';
import {
useQueryExecutionSettings,
useTypedDispatch,
useTypedSelector,
} from '../../../../utils/hooks';
import {useTypedDispatch, useTypedSelector} from '../../../../utils/hooks';
import {getConfirmation} from '../../../../utils/hooks/withConfirmation/useChangeInputWithConfirmation';
import {getSchemaControls} from '../../utils/controls';
import {
Expand Down Expand Up @@ -48,7 +44,6 @@ export function SchemaTree(props: SchemaTreeProps) {
{currentData: actionsSchemaData, isFetching: isActionsDataFetching},
] = tableSchemaDataApi.useLazyGetTableSchemaDataQuery();

const [querySettings] = useQueryExecutionSettings();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was used only in hook dependencies

const [createDirectoryOpen, setCreateDirectoryOpen] = React.useState(false);
const [parentPath, setParentPath] = React.useState('');
const setSchemaTreeKey = useDispatchTreeKey();
Expand Down Expand Up @@ -144,8 +139,8 @@ export function SchemaTree(props: SchemaTreeProps) {
dispatch,
input,
isActionsDataFetching,
isDirty,
onActivePathUpdate,
querySettings,
rootPath,
]);

Expand Down
6 changes: 5 additions & 1 deletion src/services/api/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,15 @@ export class MetaAPI extends BaseYdbAPI {
});
}

getTenants(clusterName?: string, {signal}: AxiosOptions = {}) {
getTenants(
{clusterName, databaseName}: {clusterName?: string; databaseName?: string},
{signal}: AxiosOptions = {},
) {
return this.get<MetaTenants>(
this.getPath('/meta/cp_databases'),
{
cluster_name: clusterName,
database_name: databaseName,
},
{requestConfig: {signal}},
).then(parseMetaTenants);
Expand Down
2 changes: 1 addition & 1 deletion src/services/api/viewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export class ViewerAPI extends BaseYdbAPI {
);
}

getTenants(clusterName?: string, {concurrentId, signal}: AxiosOptions = {}) {
getTenants({clusterName}: {clusterName?: string}, {concurrentId, signal}: AxiosOptions = {}) {
return this.get<TTenantInfo>(
this.getPath('/viewer/json/tenantinfo'),
{
Expand Down
18 changes: 13 additions & 5 deletions src/store/reducers/cluster/cluster.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {createSelector, 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';
Expand All @@ -10,6 +9,7 @@ import {isClusterInfoV2} from '../../../types/api/cluster';
import type {TClusterInfo} from '../../../types/api/cluster';
import type {TTabletStateInfo} from '../../../types/api/tablet';
import {CLUSTER_DEFAULT_TITLE, DEFAULT_CLUSTER_TAB_KEY} from '../../../utils/constants';
import {useClusterNameFromQuery} from '../../../utils/hooks/useDatabaseFromQuery';
import {isQueryErrorResponse} from '../../../utils/query';
import type {RootState} from '../../defaultStore';
import {api} from '../api';
Expand Down Expand Up @@ -136,16 +136,24 @@ export const clusterApi = api.injectEndpoints({
});

export function useClusterBaseInfo() {
const [clusterName] = useQueryParam('clusterName', StringParam);
const clusterNameFromQuery = useClusterNameFromQuery();

const {currentData} = clusterApi.useGetClusterBaseInfoQuery(clusterName ?? skipToken);
const {currentData} = clusterApi.useGetClusterBaseInfoQuery(clusterNameFromQuery ?? skipToken);

const {solomon: monitoring, name, trace_view: traceView, ...data} = currentData || {};
const {solomon: monitoring, name, title, trace_view: traceView, ...data} = currentData || {};

// name is used for requests, title is used for display
// Example:
// Name: ydb_vla_dev02
// Title: YDB DEV VLA02
const clusterName = name ?? clusterNameFromQuery ?? undefined;
const clusterTitle = title ?? clusterName;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a little refactoring to make code more clear


return {
...data,
...parseTraceFields({traceView}),
name: name ?? clusterName ?? undefined,
name: clusterName,
title: clusterTitle,
monitoring,
};
}
Expand Down
16 changes: 14 additions & 2 deletions src/store/reducers/tenant/tenant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {createSlice} from '@reduxjs/toolkit';
import type {PayloadAction} from '@reduxjs/toolkit';

import {DEFAULT_USER_SETTINGS, settingsManager} from '../../../services/settings';
import type {TTenantInfo} from '../../../types/api/tenant';
import {TENANT_INITIAL_PAGE_KEY} from '../../../utils/constants';
import {api} from '../api';

Expand Down Expand Up @@ -52,9 +53,20 @@ export const {setTenantPage, setQueryTab, setDiagnosticsTab, setSummaryTab, setM
export const tenantApi = api.injectEndpoints({
endpoints: (builder) => ({
getTenantInfo: builder.query({
queryFn: async ({path}: {path: string}, {signal}) => {
queryFn: async (
{path, clusterName}: {path: string; clusterName?: string},
{signal},
) => {
try {
const tenantData = await window.api.viewer.getTenantInfo({path}, {signal});
let tenantData: TTenantInfo;
if (window.api.meta && clusterName) {
tenantData = await window.api.meta.getTenants(
{databaseName: path, clusterName},
{signal},
);
} else {
tenantData = await window.api.viewer.getTenantInfo({path}, {signal});
}
return {data: tenantData.TenantInfo?.[0] ?? null};
} catch (error) {
return {error};
Expand Down
4 changes: 2 additions & 2 deletions src/store/reducers/tenants/tenants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ export const tenantsApi = api.injectEndpoints({
queryFn: async ({clusterName}: {clusterName?: string}, {signal}) => {
try {
const response = window.api.meta
? await window.api.meta.getTenants(clusterName, {signal})
: await window.api.viewer.getTenants(clusterName, {signal});
? await window.api.meta.getTenants({clusterName}, {signal})
: await window.api.viewer.getTenants({clusterName}, {signal});
let data: PreparedTenant[];
if (Array.isArray(response.TenantInfo)) {
data = prepareTenants(response.TenantInfo);
Expand Down
Loading