From f90e7b4b3146421af7e06614f716f203691fdf1e Mon Sep 17 00:00:00 2001 From: Gabriel Corado Date: Mon, 9 Dec 2024 17:42:35 -0300 Subject: [PATCH 1/8] feat(web): add database terminal access --- web/packages/teleport/src/Console/Console.tsx | 5 +- .../src/Console/DocumentDb/ConnectDialog.tsx | 202 ++++++++++++++++++ .../Console/DocumentDb/DocumentDb.story.tsx | 196 +++++++++++++++++ .../Console/DocumentDb/DocumentDb.test.tsx | 154 +++++++++++++ .../src/Console/DocumentDb/DocumentDb.tsx | 75 +++++++ .../teleport/src/Console/DocumentDb/index.js | 20 ++ .../src/Console/DocumentDb/useDbSession.tsx | 148 +++++++++++++ .../Console/DocumentSsh/Terminal/Terminal.tsx | 9 + .../teleport/src/Console/consoleContext.tsx | 33 ++- .../teleport/src/Console/stores/storeDocs.ts | 4 + .../teleport/src/Console/stores/types.ts | 12 +- .../teleport/src/Console/useTabRouting.ts | 5 +- .../ConnectDialog/ConnectDialog.story.tsx | 12 ++ .../Databases/ConnectDialog/ConnectDialog.tsx | 25 ++- .../UnifiedResources/ResourceActionButton.tsx | 3 +- web/packages/teleport/src/config.ts | 17 ++ .../teleport/src/lib/term/protobuf.ts | 6 + web/packages/teleport/src/lib/term/tty.ts | 14 ++ .../src/services/databases/databases.test.ts | 12 ++ .../src/services/databases/makeDatabase.ts | 2 + .../teleport/src/services/databases/types.ts | 2 + 21 files changed, 946 insertions(+), 10 deletions(-) create mode 100644 web/packages/teleport/src/Console/DocumentDb/ConnectDialog.tsx create mode 100644 web/packages/teleport/src/Console/DocumentDb/DocumentDb.story.tsx create mode 100644 web/packages/teleport/src/Console/DocumentDb/DocumentDb.test.tsx create mode 100644 web/packages/teleport/src/Console/DocumentDb/DocumentDb.tsx create mode 100644 web/packages/teleport/src/Console/DocumentDb/index.js create mode 100644 web/packages/teleport/src/Console/DocumentDb/useDbSession.tsx diff --git a/web/packages/teleport/src/Console/Console.tsx b/web/packages/teleport/src/Console/Console.tsx index 36cf572586dbd..f17a59ae259df 100644 --- a/web/packages/teleport/src/Console/Console.tsx +++ b/web/packages/teleport/src/Console/Console.tsx @@ -37,6 +37,7 @@ import usePageTitle from './usePageTitle'; import useTabRouting from './useTabRouting'; import useOnExitConfirmation from './useOnExitConfirmation'; import useKeyboardNav from './useKeyboardNav'; +import DocumentDb from './DocumentDb'; const POLL_INTERVAL = 5000; // every 5 sec @@ -77,7 +78,7 @@ export default function Console() { return consoleCtx.refreshParties(); } - const disableNewTab = storeDocs.getNodeDocuments().length > 0; + const disableNewTab = storeDocs.getNodeDocuments().length > 0 || storeDocs.getDbDocuments().length > 0; const $docs = documents.map(doc => ( )); @@ -139,6 +140,8 @@ function MemoizedDocument(props: { doc: stores.Document; visible: boolean }) { return ; case 'kubeExec': return ; + case 'db': + return ; default: return ; } diff --git a/web/packages/teleport/src/Console/DocumentDb/ConnectDialog.tsx b/web/packages/teleport/src/Console/DocumentDb/ConnectDialog.tsx new file mode 100644 index 0000000000000..8d270e0271196 --- /dev/null +++ b/web/packages/teleport/src/Console/DocumentDb/ConnectDialog.tsx @@ -0,0 +1,202 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import React, { useCallback, useEffect, useState } from 'react'; +import Dialog, { + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from 'design/Dialog'; +import { + Box, + ButtonPrimary, + ButtonSecondary, + Flex, + Indicator, +} from 'design'; + +import Validation from 'shared/components/Validation'; +import { Option } from 'shared/components/Select'; +import { FieldSelect, FieldSelectCreatable } from 'shared/components/FieldSelect'; +import { Database } from 'teleport/services/databases'; +import { useTeleport } from 'teleport'; +import { useUnifiedResourcesFetch } from 'shared/components/UnifiedResources'; +import { Danger } from 'design/Alert'; +import { requiredField } from 'shared/components/Validation/rules'; + +type Props = { + clusterId: string; + serviceName: string; + onClose(): void; + onConnect: onConnectCallback; +}; + +type onConnectCallback = ( + name: string, + protocol: string, + dbName: string, + dbUser: string, + dbRoles: string[], +) => void; + +function DbConnectDialog({ clusterId, serviceName, onClose, onConnect }: Props) { + // Fetch database information to pre-fill the connection parameters. + const ctx = useTeleport(); + const { + fetch: unifiedFetch, + attempt, + resources, + } = useUnifiedResourcesFetch({ + fetchFunc: useCallback( + async (_, signal) => { + const response = await ctx.resourceService.fetchUnifiedResources( + clusterId, + { + query: `name == "${serviceName}"`, + sort: { fieldName: 'name', dir: 'ASC'}, + limit: 1, + }, + signal + ); + + + // TODO(gabrielcorado): Handle scenarios where there is conflict on the name. + if (response.agents.length !== 1 || response.agents[0].kind !== 'db') { + throw new Error('Unable to retrieve database information.'); + } + + return { agents: response.agents }; + }, + [clusterId, serviceName] + ) + }) + useEffect(() => { unifiedFetch({clear: true})}, []) + + + return ( + + + Connect To Database + + + {attempt.status === 'failed' && } + {(attempt.status === '' || attempt.status === 'processing') && ( + + + + )} + {attempt.status === 'success' && } + + ); +} + +type FormProps = { + db: Database; + onConnect: onConnectCallback; + onClose(): void; +}; + +function DbConnectDialogForm({ db, onConnect, onClose }: FormProps) { + const dbUserOpts = db.users?.map(user => ({value: user, label: user})); + const dbNamesOpts = db.names?.map(name => ({value: name, label: name})); + const dbRolesOpts = db.roles?.map(role => ({value: role, label: role})); + + const [selectedName, setSelectedName] = useState