From ba22a39e20f6417a9f24a2c0f5084cb63c4cf759 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=91=D0=BC=20=D0=9C=D1=83=D1=84=D0=B0?= =?UTF-8?q?=D0=B7=D0=B0=D0=BB=D0=BE=D0=B2?= <67755036+artemmufazalov@users.noreply.github.com> Date: Mon, 27 Jan 2025 10:38:22 +0300 Subject: [PATCH] feat: add connect to DB dialog (#1838) --- src/components/ConnectToDB/ConnectToDB.scss | 13 + .../ConnectToDB/ConnectToDBDialog.tsx | 99 ++++++++ .../ConnectToDBSyntaxHighlighter.scss | 22 ++ .../ConnectToDBSyntaxHighlighter.tsx | 82 +++++++ .../ConnectToDBSyntaxHighlighter/lazy.ts | 6 + src/components/ConnectToDB/getDocsLink.ts | 34 +++ src/components/ConnectToDB/i18n/en.json | 18 ++ src/components/ConnectToDB/i18n/index.ts | 7 + src/components/ConnectToDB/snippets.ts | 229 ++++++++++++++++++ src/components/ConnectToDB/types.ts | 14 ++ src/containers/App/Providers.tsx | 8 +- src/containers/Header/Header.tsx | 60 ++++- src/containers/Header/i18n/en.json | 4 +- .../ObjectSummary/SchemaTree/SchemaTree.tsx | 10 +- src/containers/Tenant/i18n/en.json | 1 + src/containers/Tenant/utils/schemaActions.tsx | 64 +++-- .../internalViewer/internalViewer.test.ts | 2 +- 17 files changed, 637 insertions(+), 36 deletions(-) create mode 100644 src/components/ConnectToDB/ConnectToDB.scss create mode 100644 src/components/ConnectToDB/ConnectToDBDialog.tsx create mode 100644 src/components/ConnectToDB/ConnectToDBSyntaxHighlighter/ConnectToDBSyntaxHighlighter.scss create mode 100644 src/components/ConnectToDB/ConnectToDBSyntaxHighlighter/ConnectToDBSyntaxHighlighter.tsx create mode 100644 src/components/ConnectToDB/ConnectToDBSyntaxHighlighter/lazy.ts create mode 100644 src/components/ConnectToDB/getDocsLink.ts create mode 100644 src/components/ConnectToDB/i18n/en.json create mode 100644 src/components/ConnectToDB/i18n/index.ts create mode 100644 src/components/ConnectToDB/snippets.ts create mode 100644 src/components/ConnectToDB/types.ts diff --git a/src/components/ConnectToDB/ConnectToDB.scss b/src/components/ConnectToDB/ConnectToDB.scss new file mode 100644 index 000000000..2029ec812 --- /dev/null +++ b/src/components/ConnectToDB/ConnectToDB.scss @@ -0,0 +1,13 @@ +.ydb-connect-to-db { + &__dialog-tabs { + margin-top: var(--g-spacing-4); + } + + &__docs { + margin-top: var(--g-spacing-4); + } + + &__snippet-container { + height: 270px; + } +} diff --git a/src/components/ConnectToDB/ConnectToDBDialog.tsx b/src/components/ConnectToDB/ConnectToDBDialog.tsx new file mode 100644 index 000000000..b2c0fc783 --- /dev/null +++ b/src/components/ConnectToDB/ConnectToDBDialog.tsx @@ -0,0 +1,99 @@ +import React from 'react'; + +import NiceModal from '@ebay/nice-modal-react'; +import {Dialog, Tabs} from '@gravity-ui/uikit'; + +import {cn} from '../../utils/cn'; +import {LinkWithIcon} from '../LinkWithIcon/LinkWithIcon'; + +import {ConnectToDBSyntaxHighlighterLazy} from './ConnectToDBSyntaxHighlighter/lazy'; +import {getDocsLink} from './getDocsLink'; +import i18n from './i18n'; +import {getSnippetCode} from './snippets'; +import type {SnippetLanguage, SnippetParams} from './types'; + +import './ConnectToDB.scss'; + +const b = cn('ydb-connect-to-db'); + +const connectionTabs: {id: SnippetLanguage; title: string}[] = [ + {id: 'bash', title: 'Bash'}, + {id: 'cpp', title: 'C++'}, + {id: 'csharp', title: 'C# (.NET)'}, + {id: 'go', title: 'Go'}, + {id: 'java', title: 'Java'}, + {id: 'javascript', title: 'Node JS'}, + {id: 'php', title: 'PHP'}, + {id: 'python', title: 'Python'}, +]; + +interface ConnectToDBDialogProps extends SnippetParams { + open: boolean; + onClose: VoidFunction; +} + +function ConnectToDBDialog({open, onClose, database, endpoint}: ConnectToDBDialogProps) { + const [activeTab, setActiveTab] = React.useState('bash'); + + const snippet = getSnippetCode(activeTab, {database, endpoint}); + const docsLink = getDocsLink(activeTab); + + return ( + + + +
{i18n('connection-info-message')}
+ setActiveTab(tab as SnippetLanguage)} + className={b('dialog-tabs')} + /> +
+ +
+ {docsLink ? ( + + ) : null} +
+ +
+ ); +} + +const ConnectToDBDialogNiceModal = NiceModal.create((props: SnippetParams) => { + const modal = NiceModal.useModal(); + + const handleClose = () => { + modal.hide(); + modal.remove(); + }; + + return ( + { + modal.resolve(false); + handleClose(); + }} + open={modal.visible} + /> + ); +}); + +const CONNECT_TO_DB_DIALOG = 'connect-to-db-dialog'; + +NiceModal.register(CONNECT_TO_DB_DIALOG, ConnectToDBDialogNiceModal); + +export async function getConnectToDBDialog(params: SnippetParams): Promise { + return await NiceModal.show(CONNECT_TO_DB_DIALOG, { + id: CONNECT_TO_DB_DIALOG, + ...params, + }); +} diff --git a/src/components/ConnectToDB/ConnectToDBSyntaxHighlighter/ConnectToDBSyntaxHighlighter.scss b/src/components/ConnectToDB/ConnectToDBSyntaxHighlighter/ConnectToDBSyntaxHighlighter.scss new file mode 100644 index 000000000..1520d7541 --- /dev/null +++ b/src/components/ConnectToDB/ConnectToDBSyntaxHighlighter/ConnectToDBSyntaxHighlighter.scss @@ -0,0 +1,22 @@ +@use '../../../styles/mixins.scss'; + +.ydb-connect-to-db-syntax-highlighter { + &__wrapper { + position: relative; + z-index: 0; + + height: 100%; + } + + &__sticky-container { + z-index: 1; + top: 52px; + @include mixins.sticky-top(); + } + + &__copy { + position: absolute; + top: 13px; + right: 14px; + } +} diff --git a/src/components/ConnectToDB/ConnectToDBSyntaxHighlighter/ConnectToDBSyntaxHighlighter.tsx b/src/components/ConnectToDB/ConnectToDBSyntaxHighlighter/ConnectToDBSyntaxHighlighter.tsx new file mode 100644 index 000000000..ebc4db80d --- /dev/null +++ b/src/components/ConnectToDB/ConnectToDBSyntaxHighlighter/ConnectToDBSyntaxHighlighter.tsx @@ -0,0 +1,82 @@ +import {ClipboardButton, useThemeValue} from '@gravity-ui/uikit'; +import {PrismLight as SyntaxHighlighter} from 'react-syntax-highlighter'; +import bash from 'react-syntax-highlighter/dist/esm/languages/prism/bash'; +import cpp from 'react-syntax-highlighter/dist/esm/languages/prism/cpp'; +import csharp from 'react-syntax-highlighter/dist/esm/languages/prism/csharp'; +import go from 'react-syntax-highlighter/dist/esm/languages/prism/go'; +import java from 'react-syntax-highlighter/dist/esm/languages/prism/java'; +import javascript from 'react-syntax-highlighter/dist/esm/languages/prism/javascript'; +import php from 'react-syntax-highlighter/dist/esm/languages/prism/php'; +import python from 'react-syntax-highlighter/dist/esm/languages/prism/python'; +import {vscDarkPlus} from 'react-syntax-highlighter/dist/esm/styles/prism'; + +import {cn} from '../../../utils/cn'; +import {dark, light} from '../../YqlHighlighter/yql'; +import i18n from '../i18n'; +import type {SnippetLanguage} from '../types'; + +import './ConnectToDBSyntaxHighlighter.scss'; + +SyntaxHighlighter.registerLanguage('bash', bash); +SyntaxHighlighter.registerLanguage('cpp', cpp); +SyntaxHighlighter.registerLanguage('csharp', csharp); +SyntaxHighlighter.registerLanguage('go', go); +SyntaxHighlighter.registerLanguage('java', java); +SyntaxHighlighter.registerLanguage('javascript', javascript); +SyntaxHighlighter.registerLanguage('php', php); +SyntaxHighlighter.registerLanguage('python', python); + +type ConnectToDBSyntaxHighlighterProps = { + text: string; + language: SnippetLanguage; + className?: string; +}; +const darkTheme: Record = { + ...dark, + 'pre[class*="language-"]': { + ...dark['pre[class*="language-"]'], + background: vscDarkPlus['pre[class*="language-"]'].background, + scrollbarColor: `var(--g-color-scroll-handle) transparent`, + }, + 'code[class*="language-"]': { + ...dark['code[class*="language-"]'], + whiteSpace: 'pre', + }, +}; + +const lightTheme: Record = { + ...light, + 'pre[class*="language-"]': { + ...light['pre[class*="language-"]'], + background: 'var(--g-color-base-misc-light)', + scrollbarColor: `var(--g-color-scroll-handle) transparent`, + }, + 'code[class*="language-"]': { + ...light['code[class*="language-"]'], + whiteSpace: 'pre', + }, +}; + +const b = cn('ydb-connect-to-db-syntax-highlighter'); + +export function ConnectToDBSyntaxHighlighter({text, language}: ConnectToDBSyntaxHighlighterProps) { + const themeValue = useThemeValue(); + const isDark = themeValue === 'dark' || themeValue === 'dark-hc'; + + return ( +
+
+ + {i18n('copy')} + +
+ + {text} + +
+ ); +} diff --git a/src/components/ConnectToDB/ConnectToDBSyntaxHighlighter/lazy.ts b/src/components/ConnectToDB/ConnectToDBSyntaxHighlighter/lazy.ts new file mode 100644 index 000000000..b908858c4 --- /dev/null +++ b/src/components/ConnectToDB/ConnectToDBSyntaxHighlighter/lazy.ts @@ -0,0 +1,6 @@ +import {lazyComponent} from '../../../utils/lazyComponent'; + +export const ConnectToDBSyntaxHighlighterLazy = lazyComponent( + () => import('./ConnectToDBSyntaxHighlighter'), + 'ConnectToDBSyntaxHighlighter', +); diff --git a/src/components/ConnectToDB/getDocsLink.ts b/src/components/ConnectToDB/getDocsLink.ts new file mode 100644 index 000000000..62baaab4d --- /dev/null +++ b/src/components/ConnectToDB/getDocsLink.ts @@ -0,0 +1,34 @@ +import i18n from './i18n'; +import type {SnippetLanguage} from './types'; + +export function getDocsLink(snippetLang: SnippetLanguage) { + switch (snippetLang) { + case 'bash': { + return i18n('docs_bash'); + } + case 'cpp': { + return i18n('docs_cpp'); + } + case 'csharp': { + return i18n('docs_dotnet'); + } + case 'go': { + return i18n('docs_go'); + } + case 'java': { + return i18n('docs_java'); + } + case 'javascript': { + return i18n('docs_nodejs'); + } + case 'php': { + return i18n('docs_php'); + } + case 'python': { + return i18n('docs_python'); + } + default: { + return undefined; + } + } +} diff --git a/src/components/ConnectToDB/i18n/en.json b/src/components/ConnectToDB/i18n/en.json new file mode 100644 index 000000000..055261a99 --- /dev/null +++ b/src/components/ConnectToDB/i18n/en.json @@ -0,0 +1,18 @@ +{ + "header": "Connect to the database", + "connection-info-message": "Use the following code to connect to the database", + + "documentation": "Documentation", + + "close": "Close", + "copy": "Copy", + + "docs_bash": "https://ydb.tech/docs/en/concepts/connect", + "docs_cpp": "https://ydb.tech/docs/en/dev/example-app/example-cpp", + "docs_dotnet": "https://ydb.tech/docs/en/dev/example-app/example-dotnet", + "docs_go": "https://ydb.tech/docs/en/dev/example-app/go", + "docs_java": "https://ydb.tech/docs/en/dev/example-app/java", + "docs_nodejs": "https://ydb.tech/docs/en/dev/example-app/example-nodejs", + "docs_php": "https://ydb.tech/docs/en/dev/example-app/example-php", + "docs_python": "https://ydb.tech/docs/en/dev/example-app/python" +} diff --git a/src/components/ConnectToDB/i18n/index.ts b/src/components/ConnectToDB/i18n/index.ts new file mode 100644 index 000000000..1718a7dc4 --- /dev/null +++ b/src/components/ConnectToDB/i18n/index.ts @@ -0,0 +1,7 @@ +import {registerKeysets} from '../../../utils/i18n'; + +import en from './en.json'; + +const COMPONENT = 'ydb-connect-to-db'; + +export default registerKeysets(COMPONENT, {en}); diff --git a/src/components/ConnectToDB/snippets.ts b/src/components/ConnectToDB/snippets.ts new file mode 100644 index 000000000..4ef14ace8 --- /dev/null +++ b/src/components/ConnectToDB/snippets.ts @@ -0,0 +1,229 @@ +import type {SnippetLanguage, SnippetParams} from './types'; + +export function getBashSnippetCode({database, endpoint}: SnippetParams) { + return `ydb -e ${endpoint || ''} --token-file ~/my_token + -d ${database ?? '/'} table query execute -q 'SELECT "Hello, world!"'`; +} + +export function getCPPSnippetCode({database, endpoint}: SnippetParams) { + return `auto connectionParams = TConnectionsParams() + .SetEndpoint("${endpoint ?? ''}") + .SetDatabase("${database ?? '/'}") + .SetAuthToken(GetEnv("YDB_TOKEN")); + +TDriver driver(connectionParams);`; +} + +export function getCSharpSnippetCode({database, endpoint}: SnippetParams) { + return `var config = new DriverConfig( + endpoint: "${endpoint ?? ''}", + database: "${database ?? '/'}", + credentials: credentialsProvider +); + +using var driver = new Driver( + config: config +); + +await driver.Initialize();`; +} + +export function getGoSnippetCode({database, endpoint}: SnippetParams) { + return `package main + +import ( + "context" + "os" + + "github.com/ydb-platform/ydb-go-sdk/v3" + "github.com/ydb-platform/ydb-go-sdk/v3/table" +) + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + db, err := ydb.Open(ctx, + "${endpoint ?? ''}${database ?? '/'}", + ydb.WithAccessTokenCredentials(os.Getenv("YDB_ACCESS_TOKEN_CREDENTIALS")), + ) + if err != nil { + panic(err) + } + + defer db.Close(ctx) + + err = db.Table().Do(ctx, + func(ctx context.Context, s table.Session) error { + _, res, err := s.Execute( + ctx, + table.TxControl(table.BeginTx(table.WithOnlineReadOnly()), table.CommitTx()), + "SELECT 'Hello, world!'", + nil, + ) + if err != nil { + return err + } + defer res.Close() + var val string + + for res.NextResultSet(ctx) { + for res.NextRow() { + err = res.Scan(&val) + if err != nil { + return err + } + println(val) + } + } + return res.Err() + }) + if err != nil { + panic(err) + } +}`; +} + +export function getJavaSnippetCode({database, endpoint}: SnippetParams) { + return `package com.example; + +import java.io.IOException; +import java.nio.charset.Charset; + +import tech.ydb.core.grpc.GrpcTransport; +import tech.ydb.table.SessionRetryContext; +import tech.ydb.table.TableClient; +import tech.ydb.table.query.DataQueryResult; +import tech.ydb.table.result.ResultSetReader; +import tech.ydb.table.transaction.TxControl; +import tech.ydb.auth.TokenAuthProvider; + +public class YDBConnect { + public static void main(String[] args) throws IOException { + try (GrpcTransport transport = GrpcTransport.forEndpoint( + "${endpoint ?? ''}", + "${database ?? '/'}") + .withAuthProvider(new TokenAuthProvider(System.getenv("YDB_ACCESS_TOKEN_CREDENTIALS"))) + .build()) { + try (TableClient tableClient = TableClient.newClient(transport) + .build()) { + SessionRetryContext retryCtx = SessionRetryContext.create(tableClient).build(); + DataQueryResult queryResult = retryCtx.supplyResult( + session -> session.executeDataQuery("SELECT 'Hello, world!'", TxControl.serializableRw()) + ).join().getValue(); + + ResultSetReader rsReader = queryResult.getResultSet(0); + while (rsReader.next()) { + System.out.println(rsReader.getColumn(0).getBytesAsString(Charset.forName("utf8"))); + } + } + } + } +}`; +} + +export function getNodeJSSnippetCode({database, endpoint}: SnippetParams) { + return `const {Driver, getCredentialsFromEnv, getLogger} = require('ydb-sdk'); + +const logger = getLogger({level: 'debug'}); +const endpoint = '${endpoint ?? ''}'; +const database = '${database ?? '/'}'; +const authService = getCredentialsFromEnv(); +const driver = new Driver({endpoint, database, authService}); + +async function run() { + if (!await driver.ready(100)) { + logger.fatal('Driver has not become ready in 10 seconds!'); + process.exit(1); + } + + await driver.tableClient.withSession(async (session) => { + res = await session.executeQuery("SELECT 'Hello, world!'") + console.log(res.resultSets[0].rows[0].items[0].bytesValue.toString()) + return + }); + + process.exit(0) +} + +run();`; +} + +export function getPHPSnippetCode({database, endpoint}: SnippetParams) { + return ` '${database ?? '/'}', + + // Database endpoint + 'endpoint' => '${endpoint ?? ''}', + + // Auto discovery (dedicated server only) + 'discovery' => false, + + // IAM config + 'iam_config' => [ + // 'root_cert_file' => './CA.pem', Root CA file (uncomment for dedicated server only) + ], + + 'credentials' => new AccessTokenAuthentication('') // use from reference/ydb-sdk/auth +]; + +$ydb = new Ydb($config);`; +} + +export function getPythonSnippetCode({database, endpoint}: SnippetParams) { + return `#!/usr/bin/python3 +import ydb + +driver_config = ydb.DriverConfig( + '${endpoint || ''}', '${database ?? '/'}', + credentials=ydb.credentials_from_env_variables(), +) +print(driver_config) +with ydb.Driver(driver_config) as driver: + try: + driver.wait(10) + session = driver.table_client.session().create() + with session.transaction() as tx: + query = "SELECT 'Hello, world!'" + result_set = tx.execute(query)[0] + for row in result_set.rows: + print(row) + except TimeoutError: + print("Connect failed to YDB") + print("Last reported errors by discovery:") + print(driver.discovery_debug_details())`; +} + +export function getSnippetCode(lang: SnippetLanguage, params: SnippetParams) { + switch (lang) { + case 'cpp': { + return getCPPSnippetCode(params); + } + case 'csharp': { + return getCSharpSnippetCode(params); + } + case 'go': { + return getGoSnippetCode(params); + } + case 'java': { + return getJavaSnippetCode(params); + } + case 'javascript': { + return getNodeJSSnippetCode(params); + } + case 'php': { + return getPHPSnippetCode(params); + } + case 'python': { + return getPythonSnippetCode(params); + } + case 'bash': + default: { + return getBashSnippetCode(params); + } + } +} diff --git a/src/components/ConnectToDB/types.ts b/src/components/ConnectToDB/types.ts new file mode 100644 index 000000000..6cdd67dc7 --- /dev/null +++ b/src/components/ConnectToDB/types.ts @@ -0,0 +1,14 @@ +export type SnippetLanguage = + | 'bash' + | 'cpp' + | 'csharp' + | 'go' + | 'java' + | 'javascript' + | 'php' + | 'python'; + +export interface SnippetParams { + database?: string; + endpoint?: string; +} diff --git a/src/containers/App/Providers.tsx b/src/containers/App/Providers.tsx index 400d1fd18..b906e88fa 100644 --- a/src/containers/App/Providers.tsx +++ b/src/containers/App/Providers.tsx @@ -35,11 +35,9 @@ export function Providers({ - - - {children} - - + + {children} + diff --git a/src/containers/Header/Header.tsx b/src/containers/Header/Header.tsx index 6dcf63c7a..aa9e7bdb8 100644 --- a/src/containers/Header/Header.tsx +++ b/src/containers/Header/Header.tsx @@ -1,18 +1,22 @@ import React from 'react'; -import {Breadcrumbs} from '@gravity-ui/uikit'; +import {ArrowUpRightFromSquare, PlugConnection} from '@gravity-ui/icons'; +import {Breadcrumbs, Button, Divider, Flex, Icon} from '@gravity-ui/uikit'; +import {useLocation} from 'react-router-dom'; +import {getConnectToDBDialog} from '../../components/ConnectToDB/ConnectToDBDialog'; import {InternalLink} from '../../components/InternalLink'; -import {LinkWithIcon} from '../../components/LinkWithIcon/LinkWithIcon'; import {selectIsUserAllowedToMakeChanges} from '../../store/reducers/authentication/authentication'; import {useClusterBaseInfo} from '../../store/reducers/cluster/cluster'; import {cn} from '../../utils/cn'; import {DEVELOPER_UI_TITLE} from '../../utils/constants'; import {createDeveloperUIInternalPageHref} from '../../utils/developerUI/developerUI'; import {useTypedSelector} from '../../utils/hooks'; +import {useDatabaseFromQuery} from '../../utils/hooks/useDatabaseFromQuery'; import type {RawBreadcrumbItem} from './breadcrumbs'; import {getBreadcrumbs} from './breadcrumbs'; +import {headerKeyset} from './i18n'; import './Header.scss'; @@ -28,6 +32,10 @@ function Header({mainPage}: HeaderProps) { const clusterInfo = useClusterBaseInfo(); + const database = useDatabaseFromQuery(); + const location = useLocation(); + const isDatabasePage = location.pathname === '/tenant'; + const clusterName = clusterInfo.title || clusterInfo.name; const breadcrumbItems = React.useMemo(() => { @@ -52,6 +60,47 @@ function Header({mainPage}: HeaderProps) { }); }, [clusterName, mainPage, page, pageBreadcrumbsOptions]); + const renderRightControls = () => { + const elements: React.ReactNode[] = []; + + if (isDatabasePage && database) { + elements.push( + , + ); + } + + if (isUserAllowedToMakeChanges) { + elements.push( + , + ); + } + + if (elements.length) { + return ( + + {elements.map((el, index) => { + return ( + + {el} + {index === elements.length - 1 ? null : ( + + )} + + ); + })} + + ); + } + + return null; + }; + const renderHeader = () => { return (
@@ -80,12 +129,7 @@ function Header({mainPage}: HeaderProps) { }} /> - {isUserAllowedToMakeChanges ? ( - - ) : null} + {renderRightControls()}
); }; diff --git a/src/containers/Header/i18n/en.json b/src/containers/Header/i18n/en.json index c457ae341..2ffec2e21 100644 --- a/src/containers/Header/i18n/en.json +++ b/src/containers/Header/i18n/en.json @@ -5,5 +5,7 @@ "breadcrumbs.vDisk": "VDisk", "breadcrumbs.tablet": "Tablet", "breadcrumbs.tablets": "Tablets", - "breadcrumbs.storageGroup": "Storage Group" + "breadcrumbs.storageGroup": "Storage Group", + + "connect": "Connect" } diff --git a/src/containers/Tenant/ObjectSummary/SchemaTree/SchemaTree.tsx b/src/containers/Tenant/ObjectSummary/SchemaTree/SchemaTree.tsx index fdc4286da..e2e003447 100644 --- a/src/containers/Tenant/ObjectSummary/SchemaTree/SchemaTree.tsx +++ b/src/containers/Tenant/ObjectSummary/SchemaTree/SchemaTree.tsx @@ -5,6 +5,7 @@ import React from 'react'; import {NavigationTree} from 'ydb-ui-components'; +import {getConnectToDBDialog} from '../../../../components/ConnectToDB/ConnectToDBDialog'; import {useCreateDirectoryFeatureAvailable} from '../../../../store/reducers/capabilities/hooks'; import {selectUserInput} from '../../../../store/reducers/query/query'; import {schemaApi} from '../../../../store/reducers/schema/schema'; @@ -26,6 +27,7 @@ import { import {getActions} from '../../utils/schemaActions'; import {CreateDirectoryDialog} from '../CreateDirectoryDialog/CreateDirectoryDialog'; import {useDispatchTreeKey, useTreeKey} from '../UpdateTreeContext'; +import {isDomain} from '../transformPath'; interface SchemaTreeProps { rootPath: string; @@ -51,6 +53,10 @@ export function SchemaTree(props: SchemaTreeProps) { const setSchemaTreeKey = useDispatchTreeKey(); const schemaTreeKey = useTreeKey(); + const rootNodeType = isDomain(rootPath, rootType) + ? 'database' + : mapPathTypeToNavigationTreeType(rootType); + const fetchPath = async (path: string) => { let schemaData: TEvDescribeSchemeResult | undefined; do { @@ -127,7 +133,7 @@ export function SchemaTree(props: SchemaTreeProps) { ? handleOpenCreateDirectoryDialog : undefined, getConfirmation: input ? getConfirmation : undefined, - + getConnectToDBDialog, schemaData: actionsSchemaData, isSchemaDataLoading: isActionsDataFetching, }, @@ -159,7 +165,7 @@ export function SchemaTree(props: SchemaTreeProps) { rootState={{ path: rootPath, name: rootName, - type: mapPathTypeToNavigationTreeType(rootType), + type: rootNodeType, collapsed: false, }} fetchPath={fetchPath} diff --git a/src/containers/Tenant/i18n/en.json b/src/containers/Tenant/i18n/en.json index 0cba4a7a9..b36c217d8 100644 --- a/src/containers/Tenant/i18n/en.json +++ b/src/containers/Tenant/i18n/en.json @@ -25,6 +25,7 @@ "actions.copied": "The path is copied to the clipboard", "actions.notCopied": "Couldn’t copy the path", "actions.copyPath": "Copy path", + "actions.connectToDB": "Connect to DB", "actions.dropIndex": "Drop index", "actions.openPreview": "Open preview", "actions.createTable": "Create table...", diff --git a/src/containers/Tenant/utils/schemaActions.tsx b/src/containers/Tenant/utils/schemaActions.tsx index 4d97c673a..f7e0adab2 100644 --- a/src/containers/Tenant/utils/schemaActions.tsx +++ b/src/containers/Tenant/utils/schemaActions.tsx @@ -1,7 +1,9 @@ +import {Copy, PlugConnection} from '@gravity-ui/icons'; import {Flex, Spin} from '@gravity-ui/uikit'; import copy from 'copy-to-clipboard'; import type {NavigationTreeNodeType, NavigationTreeProps} from 'ydb-ui-components'; +import type {SnippetParams} from '../../../components/ConnectToDB/types'; import type {AppDispatch} from '../../../store'; import {TENANT_PAGES_IDS, TENANT_QUERY_TABS_ID} from '../../../store/reducers/tenant/constants'; import {setQueryTab, setTenantPage} from '../../../store/reducers/tenant/tenant'; @@ -40,6 +42,7 @@ interface ActionsAdditionalParams { setActivePath: (path: string) => void; showCreateDirectoryDialog?: (path: string) => void; getConfirmation?: () => Promise; + getConnectToDBDialog?: (params: SnippetParams) => Promise; schemaData?: SchemaData[]; isSchemaDataLoading?: boolean; } @@ -56,8 +59,13 @@ const bindActions = ( dispatch: AppDispatch, additionalEffects: ActionsAdditionalParams, ) => { - const {setActivePath, showCreateDirectoryDialog, getConfirmation, schemaData} = - additionalEffects; + const { + setActivePath, + showCreateDirectoryDialog, + getConfirmation, + getConnectToDBDialog, + schemaData, + } = additionalEffects; const inputQuery = (tmpl: TemplateFn) => () => { const applyInsert = () => { @@ -85,6 +93,7 @@ const bindActions = ( showCreateDirectoryDialog(params.path); } : undefined, + getConnectToDBDialog: () => getConnectToDBDialog?.({database: params.path}), createTable: inputQuery(createTableTemplate), createColumnTable: inputQuery(createColumnTableTemplate), createAsyncReplication: inputQuery(createAsyncReplicationTemplate), @@ -152,26 +161,42 @@ export const getActions = dispatch, additionalEffects, ); - const copyItem = {text: i18n('actions.copyPath'), action: actions.copyPath}; + const copyItem: ActionsSet[0] = { + text: i18n('actions.copyPath'), + action: actions.copyPath, + iconEnd: , + }; + const connectToDBItem = { + text: i18n('actions.connectToDB'), + action: actions.getConnectToDBDialog, + iconEnd: , + }; - const DIR_SET: ActionsSet = [ - [copyItem], - [ - {text: i18n('actions.createTable'), action: actions.createTable}, - {text: i18n('actions.createColumnTable'), action: actions.createColumnTable}, - { - text: i18n('actions.createAsyncReplication'), - action: actions.createAsyncReplication, - }, - {text: i18n('actions.createTopic'), action: actions.createTopic}, - {text: i18n('actions.createView'), action: actions.createView}, - ], + const createEntitiesSet = [ + {text: i18n('actions.createTable'), action: actions.createTable}, + {text: i18n('actions.createColumnTable'), action: actions.createColumnTable}, + { + text: i18n('actions.createAsyncReplication'), + action: actions.createAsyncReplication, + }, + {text: i18n('actions.createTopic'), action: actions.createTopic}, + {text: i18n('actions.createView'), action: actions.createView}, ]; + + const DB_SET: ActionsSet = [[copyItem, connectToDBItem], createEntitiesSet]; + + const DIR_SET: ActionsSet = [[copyItem], createEntitiesSet]; + if (actions.createDirectory) { - DIR_SET.splice(1, 0, [ - {text: i18n('actions.createDirectory'), action: actions.createDirectory}, - ]); + const createDirectoryItem = { + text: i18n('actions.createDirectory'), + action: actions.createDirectory, + }; + + DB_SET.splice(1, 0, [createDirectoryItem]); + DIR_SET.splice(1, 0, [createDirectoryItem]); } + const ROW_TABLE_SET: ActionsSet = [ [copyItem], [ @@ -250,7 +275,8 @@ export const getActions = const nodeTypeToActions: Record = { async_replication: ASYNC_REPLICATION_SET, - database: DIR_SET, + database: DB_SET, + directory: DIR_SET, table: ROW_TABLE_SET, diff --git a/tests/suites/internalViewer/internalViewer.test.ts b/tests/suites/internalViewer/internalViewer.test.ts index cd44ac1c5..cd4125c0a 100644 --- a/tests/suites/internalViewer/internalViewer.test.ts +++ b/tests/suites/internalViewer/internalViewer.test.ts @@ -3,7 +3,7 @@ import {expect, test} from '@playwright/test'; test.describe('Test InternalViewer', async () => { test('Test internalViewer header link', async ({page}) => { page.goto(''); - const link = page.locator('header').locator('a').getByText('Developer UI'); + const link = page.locator('header').locator('a').filter({hasText: 'Developer UI'}); const href = await link.getAttribute('href'); expect(href).not.toBeNull();