diff --git a/web/packages/teleport/src/Console/Console.tsx b/web/packages/teleport/src/Console/Console.tsx index 36cf572586dbd..e0bc17f879f4d 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,9 @@ 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 +142,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..6d91f451fe0ae --- /dev/null +++ b/web/packages/teleport/src/Console/DocumentDb/ConnectDialog.tsx @@ -0,0 +1,219 @@ +/** + * 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 { Danger } from 'design/Alert'; +import { requiredField } from 'shared/components/Validation/rules'; +import { useAsync } from 'shared/hooks/useAsync'; + +import { useTeleport } from 'teleport'; +import { Database } from 'teleport/services/databases'; +import { DbConnectData } from 'teleport/lib/term/tty'; + +export function ConnectDialog(props: { + clusterId: string; + serviceName: string; + onClose(): void; + onConnect(data: DbConnectData): void; +}) { + // Fetch database information to pre-fill the connection parameters. + const ctx = useTeleport(); + const [attempt, getDatabase] = useAsync( + useCallback(async () => { + const response = await ctx.resourceService.fetchUnifiedResources( + props.clusterId, + { + query: `name == "${props.serviceName}"`, + kinds: ['db'], + sort: { fieldName: 'name', dir: 'ASC' }, + limit: 1, + } + ); + + // 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 response.agents[0]; + }, [props.clusterId, ctx.resourceService, props.serviceName]) + ); + + useEffect(() => { + void getDatabase(); + }, [getDatabase]); + + return ( + + + Connect To Database + + + {attempt.status === 'error' && } + {(attempt.status === '' || attempt.status === 'processing') && ( + + + + )} + {attempt.status === 'success' && ( + + )} + + ); +} + +function ConnectForm(props: { + db: Database; + onConnect(data: DbConnectData): void; + onClose(): void; +}) { + const dbUserOpts = props.db.users + ?.map(user => ({ + value: user, + label: user, + })) + .filter(removeWildcardOption); + const dbNamesOpts = props.db.names + ?.map(name => ({ + value: name, + label: name, + })) + .filter(removeWildcardOption); + const dbRolesOpts = props.db.roles + ?.map(role => ({ + value: role, + label: role, + })) + .filter(removeWildcardOption); + + const [selectedName, setSelectedName] = useState