diff --git a/web/packages/teleterm/src/services/config/appConfigSchema.ts b/web/packages/teleterm/src/services/config/appConfigSchema.ts
index 9ca8e52c4f650..62d17451d2d38 100644
--- a/web/packages/teleterm/src/services/config/appConfigSchema.ts
+++ b/web/packages/teleterm/src/services/config/appConfigSchema.ts
@@ -35,10 +35,6 @@ export const createAppConfigSchema = (platform: Platform) => {
.boolean()
.default(false)
.describe('Enables collecting of anonymous usage data.'),
- 'feature.searchBar': z
- .boolean()
- .default(true)
- .describe('Replaces the command bar with the new search bar'),
'keymap.tab1': shortcutSchema
.default(defaultKeymap['tab1'])
.describe(getShortcutDesc('open tab 1')),
diff --git a/web/packages/teleterm/src/ui/DocumentTerminal/useDocumentTerminal.ts b/web/packages/teleterm/src/ui/DocumentTerminal/useDocumentTerminal.ts
index 471df9495ddeb..774c67aee21b9 100644
--- a/web/packages/teleterm/src/ui/DocumentTerminal/useDocumentTerminal.ts
+++ b/web/packages/teleterm/src/ui/DocumentTerminal/useDocumentTerminal.ts
@@ -85,6 +85,12 @@ async function startTerminalSession(
documentsService: DocumentsService,
doc: types.DocumentTerminal
) {
+ // DELETE IN 14.0.0
+ //
+ // Logging in to an arbitrary host was removed in 13.0 together with the command bar.
+ // However, there's a slight chance that some users upgrading from 12.x to 13.0 still have
+ // documents with loginHost in the app state (e.g. if the doc failed to connect to the server).
+ // Let's just remove this in 14.0.0 instead to make sure those users can safely upgrade the app.
if (isDocumentTshNodeWithLoginHost(doc)) {
doc = await resolveLoginHost(ctx, logger, documentsService, doc);
}
diff --git a/web/packages/teleterm/src/ui/QuickInput/QuickInput.story.tsx b/web/packages/teleterm/src/ui/QuickInput/QuickInput.story.tsx
deleted file mode 100644
index c2f5fa9c82f92..0000000000000
--- a/web/packages/teleterm/src/ui/QuickInput/QuickInput.story.tsx
+++ /dev/null
@@ -1,299 +0,0 @@
-/**
- * Copyright 2020 Gravitational, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import React from 'react';
-
-import Flex from 'design/Flex';
-
-import AppContextProvider from 'teleterm/ui/appContextProvider';
-import { MockAppContext } from 'teleterm/ui/fixtures/mocks';
-import { getEmptyPendingAccessRequest } from 'teleterm/ui/services/workspacesService/accessRequestsService';
-import * as types from 'teleterm/services/tshd/types';
-import {
- SuggestionCmd,
- SuggestionDatabase,
- SuggestionServer,
- SuggestionSshLogin,
-} from 'teleterm/ui/services/quickInput';
-
-import QuickInput from './QuickInput';
-import QuickInputList from './QuickInputList';
-
-export default {
- title: 'Teleterm/QuickInput',
-};
-
-export const Story = () => {
- const appContext = new MockAppContext();
-
- appContext.workspacesService.state = {
- workspaces: {
- '/clusters/localhost': {
- documents: [],
- location: undefined,
- localClusterUri: '/clusters/localhost',
- accessRequests: {
- pending: getEmptyPendingAccessRequest(),
- isBarCollapsed: true,
- },
- },
- },
- rootClusterUri: '/clusters/localhost',
- };
-
- appContext.clustersService.getClusters = () => {
- return [cluster];
- };
-
- appContext.clustersService.setState(draftState => {
- draftState.clusters = new Map([[cluster.uri, cluster]]);
- });
-
- appContext.resourcesService.fetchServers = async () => ({
- agentsList: servers,
- totalCount: 3,
- startKey: '',
- });
-
- appContext.resourcesService.fetchDatabases = async () => ({
- agentsList: databases,
- totalCount: 3,
- startKey: '',
- });
-
- return (
-
-
-
-
-
- );
-};
-
-export const Suggestions = () => {
- const commandSuggestions: SuggestionCmd[] = [
- {
- kind: 'suggestion.cmd',
- token: '',
- data: {
- displayName: 'tsh foo',
- description: 'Nulla convallis lorem ut ipsum maximus venenatis.',
- },
- },
- {
- kind: 'suggestion.cmd',
- token: '',
- data: {
- displayName: 'tsh bar',
- description: 'Vivamus id nulla sed neque efficitur ornare nec in diam.',
- },
- },
- {
- kind: 'suggestion.cmd',
- token: '',
- data: {
- displayName: 'tsh quux foo',
- description:
- 'Sed porta nibh eget lacus suscipit vehicula. Curabitur eget sapien in lacus blandit pretium.',
- },
- },
- {
- kind: 'suggestion.cmd',
- token: '',
- data: {
- displayName: 'tsh baz quux',
- description: 'Etiam cursus magna at feugiat ornare.',
- },
- },
- ];
-
- const loginSuggestions: SuggestionSshLogin[] =
- cluster.loggedInUser.sshLoginsList.map(login => ({
- kind: 'suggestion.ssh-login',
- token: '',
- appendToToken: '',
- data: login,
- }));
-
- const serverSuggestions: SuggestionServer[] = servers.map(server => ({
- kind: 'suggestion.server',
- token: '',
- data: server,
- }));
-
- const dbSuggestions: SuggestionDatabase[] = databases.map(db => ({
- kind: 'suggestion.database',
- token: '',
- data: db,
- }));
-
- return (
-
-
-
-
-
-
- );
-};
-
-const defaultWidth = 200;
-const defaultHeight = 200;
-
-const QuickInputListWrapper = ({
- items,
- width = defaultWidth,
- height = defaultHeight,
-}) => {
- return (
-
- {}}
- />
-
- );
-};
-
-const longIdentifier =
- 'lorem-ipsum-dolor-sit-amet-consectetur-adipiscing-elit-quisque-elementum-nulla';
-
-const servers: types.Server[] = [
- {
- uri: '/clusters/localhost/servers/foo' as const,
- tunnel: false,
- name: '2018454d-ef3b-4b15-84f7-61ca213d37e3',
- hostname: 'foo',
- addr: 'foo.localhost',
- labelsList: [
- { name: 'env', value: 'prod' },
- { name: 'kernel', value: '5.15.0-1023-aws' },
- ],
- },
- {
- uri: '/clusters/localhost/servers/bar' as const,
- tunnel: false,
- name: '24c7aebe-4741-4464-ab69-f076fe467ebd',
- hostname: 'bar',
- addr: 'bar.localhost',
- labelsList: [
- { name: 'env', value: 'staging' },
- { name: 'kernel', value: '5.14.1-1058-aws' },
- ],
- },
- {
- uri: '/clusters/localhost/servers/lorem' as const,
- tunnel: false,
- name: '24c7aebe-4741-4464-ab69-f076fe467ebd',
- hostname: longIdentifier,
- addr: 'lorem.localhost',
- labelsList: [
- { name: 'env', value: 'staging' },
- { name: 'kernel', value: '5.14.1-1058-aws' },
- { name: 'lorem', value: longIdentifier },
- { name: 'kernel2', value: '5.14.1-1058-aws' },
- { name: 'env2', value: 'staging' },
- { name: 'kernel3', value: '5.14.1-1058-aws' },
- ],
- },
-];
-
-const databases: types.Database[] = [
- {
- uri: '/clusters/localhost/dbs/postgres' as const,
- name: 'postgres',
- desc: 'A PostgreSQL database',
- protocol: 'postgres',
- type: 'self-hosted',
- hostname: 'postgres.localhost',
- addr: 'postgres.localhost',
- labelsList: [
- { name: 'env', value: 'prod' },
- { name: 'kernel', value: '5.15.0-1023-aws' },
- ],
- },
- {
- uri: '/clusters/localhost/dbs/mysql' as const,
- name: 'mysql',
- desc: 'A MySQL database',
- protocol: 'mysql',
- type: 'self-hosted',
- hostname: 'mysql.localhost',
- addr: 'mysql.localhost',
- labelsList: [
- { name: 'env', value: 'staging' },
- { name: 'kernel', value: '5.14.1-1058-aws' },
- ],
- },
- {
- uri: '/clusters/localhost/dbs/lorem' as const,
- name: longIdentifier,
- desc: 'Vestibulum ut blandit est, sed dapibus sem. Pellentesque egestas mi eu scelerisque ultricies.',
- protocol: 'mysql',
- type: 'self-hosted',
- hostname: 'lorem.localhost',
- addr: 'lorem.localhost',
- labelsList: [
- { name: 'env', value: 'staging' },
- { name: 'kernel', value: '5.14.1-1058-aws' },
- { name: 'lorem', value: longIdentifier },
- { name: 'kernel2', value: '5.14.1-1058-aws' },
- { name: 'env2', value: 'staging' },
- { name: 'kernel3', value: '5.14.1-1058-aws' },
- ],
- },
-];
-
-const cluster = {
- uri: '/clusters/localhost' as const,
- name: 'Test',
- leaf: false,
- connected: true,
- proxyHost: 'localhost:3080',
- authClusterId: '73c4746b-d956-4f16-9848-4e3469f70762',
- loggedInUser: {
- activeRequestsList: [],
- name: 'admin',
- acl: {},
- sshLoginsList: ['root', 'ubuntu', 'ansible', longIdentifier],
- rolesList: [],
- requestableRolesList: [],
- suggestedReviewersList: [],
- },
-};
diff --git a/web/packages/teleterm/src/ui/QuickInput/QuickInput.tsx b/web/packages/teleterm/src/ui/QuickInput/QuickInput.tsx
deleted file mode 100644
index 29ce6c9b37618..0000000000000
--- a/web/packages/teleterm/src/ui/QuickInput/QuickInput.tsx
+++ /dev/null
@@ -1,299 +0,0 @@
-/**
- * Copyright 2021 Gravitational, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import React, { useEffect, useMemo, useRef, useState } from 'react';
-import styled from 'styled-components';
-import { debounce } from 'shared/utils/highbar';
-import { Box, Flex } from 'design';
-import { color, height, space, width } from 'styled-system';
-import { Spinner } from 'design/Icon';
-
-import { useAppContext } from 'teleterm/ui/appContextProvider';
-
-import useQuickInput from './useQuickInput';
-import QuickInputList from './QuickInputList';
-
-export default function Container() {
- const { workspacesService } = useAppContext();
-
- workspacesService.useState();
-
- if (!workspacesService.getRootClusterUri()) {
- return null;
- }
- return ;
-}
-
-function QuickInput() {
- const props = useQuickInput();
- const { visible, activeSuggestion, suggestionsAttempt, inputValue } = props;
- const hasSuggestions =
- suggestionsAttempt.data?.length > 0 &&
- suggestionsAttempt.status === 'success';
- const refInput = useRef();
- const measuringInputRef = useRef();
- const refList = useRef();
- const refContainer = useRef();
- const [measuredInputTextWidth, setMeasuredInputTextWidth] =
- useState();
-
- const handleInputChange = useMemo(() => {
- return debounce(() => {
- props.onInputChange(refInput.current.value);
- measureInputTextWidth();
- }, 100);
- }, []);
-
- // Update input value if it changed outside of this component. This happens when the user pick an
- // autocomplete suggestion.
- useEffect(() => {
- if (refInput.current.value !== inputValue) {
- refInput.current.value = inputValue;
- measureInputTextWidth();
- }
- }, [inputValue]);
-
- function handleOnFocus(e: React.SyntheticEvent) {
- // trigger a callback when focus is coming from external element
- if (!refContainer.current.contains(e['relatedTarget'])) {
- props.onFocus(e);
- }
-
- // ensure that
- if (!visible) {
- props.onShow();
- }
- }
-
- function handleOnBlur(e: any) {
- const inside =
- e?.relatedTarget?.contains(refInput.current) ||
- e?.relatedTarget?.contains(refList.current);
-
- if (inside) {
- refInput.current.focus();
- return;
- }
-
- props.onHide();
- }
-
- const handleArrowKey = (e: React.KeyboardEvent, nudge = 0) => {
- e.stopPropagation();
- if (!hasSuggestions) {
- return;
- }
- const next = getNext(
- activeSuggestion + nudge,
- suggestionsAttempt.data?.length
- );
- props.onActiveSuggestion(next);
- };
-
- const measureInputTextWidth = () => {
- const width = measuringInputRef.current?.getBoundingClientRect().width || 0;
- setMeasuredInputTextWidth(width);
- };
-
- const handleKeyDown = (e: React.KeyboardEvent) => {
- const keyCode = e.which;
- switch (keyCode) {
- case KeyEnum.RETURN:
- e.stopPropagation();
- e.preventDefault();
-
- props.onEnter(activeSuggestion);
- return;
- case KeyEnum.ESC:
- props.onEscape();
- return;
- case KeyEnum.TAB:
- return;
- case KeyEnum.UP:
- e.stopPropagation();
- e.preventDefault();
- handleArrowKey(e, -1);
- return;
- case KeyEnum.DOWN:
- e.stopPropagation();
- e.preventDefault();
- handleArrowKey(e, 1);
- return;
- }
- };
-
- useEffect(() => {
- if (visible) {
- refInput.current.focus();
- }
-
- return () => handleInputChange.cancel();
- }, [visible]);
-
- return (
- props.theme.space[7]}px * 2);
- height: 100%;
- `}
- flex={1}
- ref={refContainer}
- onFocus={handleOnFocus}
- onBlur={handleOnBlur}
- >
- {inputValue}
-
- {suggestionsAttempt.status === 'processing' && (
-
-
-
- )}
- {!visible && {props.keyboardShortcut} }
- {visible && hasSuggestions && (
-
- )}
-
- );
-}
-
-const MeasuringInput = styled.span`
- z-index: -1;
- font-size: 14px;
- padding-left: 8px;
- position: absolute;
- visibility: hidden;
-`;
-
-const Input = styled.input(props => {
- const { theme } = props;
- return {
- height: '100%',
- background: 'inherit',
- display: 'flex',
- flex: '1',
- zIndex: '0',
- boxSizing: 'border-box',
- color: theme.colors.text.main,
- width: '100%',
- fontSize: '14px',
- border: `0.5px ${theme.colors.action.disabledBackground} solid`,
- borderRadius: '4px',
- outline: 'none',
- padding: props.isOpened ? '2px 8px' : '2px 46px 2px 8px', // wider right margin makes place for a shortcut
- '::placeholder': {
- color: theme.colors.text.slightlyMuted,
- },
- '&:hover, &:focus': {
- color: theme.colors.text.main,
- borderColor: theme.colors.light,
- },
- '&:focus': {
- borderColor: theme.colors.brand,
- backgroundColor: theme.colors.levels.sunken,
- '::placeholder': {
- color: theme.colors.text.muted,
- },
- },
-
- ...space(props),
- ...width(props),
- ...height(props),
- ...color(props),
- };
-});
-
-const Shortcut = styled(Box)`
- position: absolute;
- right: 12px;
- top: 12px;
- padding: 2px 3px;
- color: ${({ theme }) => theme.colors.text.slightlyMuted};
- background-color: ${({ theme }) => theme.colors.levels.surface};
- line-height: 12px;
- font-size: 12px;
- border-radius: 2px;
-`;
-
-const Animate = styled(Box)`
- position: absolute;
- right: 12px;
- top: 12px;
- padding: 2px 2px;
- line-height: 12px;
- font-size: 12px;
- animation: spin 1s linear infinite;
- @keyframes spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
- }
-`;
-
-const KeyEnum = {
- BACKSPACE: 8,
- TAB: 9,
- RETURN: 13,
- ALT: 18,
- ESC: 27,
- SPACE: 32,
- PAGE_UP: 33,
- PAGE_DOWN: 34,
- END: 35,
- HOME: 36,
- LEFT: 37,
- UP: 38,
- RIGHT: 39,
- DOWN: 40,
- DELETE: 46,
- COMMA: 188,
- PERIOD: 190,
- A: 65,
- Z: 90,
- ZERO: 48,
- NUMPAD_0: 96,
- NUMPAD_9: 105,
-};
-
-function getNext(selectedIndex = 0, max = 0) {
- let index = selectedIndex % max;
- if (index < 0) {
- index += max;
- }
- return index;
-}
diff --git a/web/packages/teleterm/src/ui/QuickInput/QuickInputList/QuickInputList.tsx b/web/packages/teleterm/src/ui/QuickInput/QuickInputList/QuickInputList.tsx
deleted file mode 100644
index ea0f1e2f1742f..0000000000000
--- a/web/packages/teleterm/src/ui/QuickInput/QuickInputList/QuickInputList.tsx
+++ /dev/null
@@ -1,223 +0,0 @@
-/*
-Copyright 2018 Gravitational, Inc.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import React, { useEffect, useRef } from 'react';
-import styled from 'styled-components';
-import { Box, Flex, Label, Text } from 'design';
-import { makeLabelTag } from 'teleport/components/formatters';
-
-import { Cli, Server, Person, Database } from 'design/Icon';
-
-import * as types from 'teleterm/ui/services/quickInput/types';
-
-const QuickInputList = React.forwardRef((props, ref) => {
- // Ideally, this property would be described by the suggestion object itself rather than depending
- // on `kind`. But for now we need it just for a single suggestion kind anyway.
- const shouldSuggestionsStayInPlace =
- props.items[0]?.kind === 'suggestion.cmd';
- const activeItemRef = useRef();
- const { items, activeItem } = props;
- if (items.length === 0) {
- return null;
- }
-
- useEffect(() => {
- // `false` - bottom of the element will be aligned to the bottom of the visible area of the scrollable ancestor
- activeItemRef.current?.scrollIntoView(false);
- }, [activeItem]);
-
- const $items = items.map((r, index) => {
- const Cmpt = ComponentMap[r.kind] || UnknownItem;
- const isActive = index === activeItem;
- return (
-
-
-
- );
- });
-
- function handleClick(e: React.SyntheticEvent) {
- const el = e.target;
- if (el instanceof Element) {
- const itemEl = el.closest('[data-attr]');
- props.onPick(parseInt(itemEl.getAttribute('data-attr')));
- }
- }
-
- return (
-
- {$items}
-
- );
-});
-
-export default QuickInputList;
-
-function CmdItem(props: { item: types.SuggestionCmd }) {
- return (
-
-
-
-
- {/* Equivalent of flex-shrink: 0, but styled-system doesn't support flex-shrink. */}
-
- {props.item.data.displayName}
-
- {props.item.data.description}
-
- );
-}
-
-function SshLoginItem(props: { item: types.SuggestionSshLogin }) {
- return (
-
-
-
-
- {props.item.data}
-
- );
-}
-
-function ServerItem(props: { item: types.SuggestionServer }) {
- const { hostname, labelsList } = props.item.data;
- const $labels = labelsList.map((label, index) => (
-
- {makeLabelTag(label)}
-
- ));
-
- return (
-
-
-
-
-
- {hostname}
- {$labels}
-
-
- );
-}
-
-function DatabaseItem(props: { item: types.SuggestionDatabase }) {
- const db = props.item.data;
- const $labels = db.labelsList.map((label, index) => (
-
- {makeLabelTag(label)}
-
- ));
-
- return (
-
-
-
-
-
-
- {db.name}
-
-
- {db.type}/{db.protocol}
-
-
-
- {$labels}
-
-
- );
-}
-
-function UnknownItem(props: { item: types.Suggestion }) {
- const { kind } = props.item;
- return unknown kind: {kind}
;
-}
-
-const StyledItem = styled.div(({ theme, $active }) => {
- return {
- '&:hover, &:focus': {
- cursor: 'pointer',
- background: theme.colors.levels.elevated,
- },
-
- padding: '2px 8px',
- color: theme.colors.text.main,
- background: $active
- ? theme.colors.levels.surface
- : theme.colors.levels.sunken,
- };
-});
-
-const StyledGlobalSearchResults = styled.div(({ theme, position }) => {
- return {
- boxShadow: '8px 8px 18px rgb(0 0 0)',
- color: theme.colors.text.main,
- background: theme.colors.levels.surface,
- boxSizing: 'border-box',
- marginTop: '42px',
- left: position ? position + 'px' : 0,
- display: 'block',
- transition: '0.12s',
- position: 'absolute',
- borderRadius: '4px',
- fontSize: '12px',
- listStyle: 'none outside none',
- textShadow: 'none',
- zIndex: '1000',
- maxHeight: '350px',
- overflow: 'auto',
- };
-});
-
-const ComponentMap: Record<
- types.Suggestion['kind'],
- React.FC<{ item: types.Suggestion }>
-> = {
- ['suggestion.cmd']: CmdItem,
- ['suggestion.ssh-login']: SshLoginItem,
- ['suggestion.server']: ServerItem,
- ['suggestion.database']: DatabaseItem,
-};
-
-type Props = {
- items: types.Suggestion[];
- activeItem: number;
- position: number;
- onPick(index: number): void;
-};
-
-const SquareIconBackground = styled(Box)`
- background: ${props => props.color};
- display: flex;
- align-items: center;
- justify-content: center;
- height: 14px;
- width: 14px;
- margin-right: 8px;
- border-radius: 2px;
- padding: 4px;
-`;
diff --git a/web/packages/teleterm/src/ui/QuickInput/QuickInputList/index.ts b/web/packages/teleterm/src/ui/QuickInput/QuickInputList/index.ts
deleted file mode 100644
index 1c8ca8cde7b34..0000000000000
--- a/web/packages/teleterm/src/ui/QuickInput/QuickInputList/index.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
-Copyright 2019 Gravitational, Inc.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import QuickInputList from './QuickInputList';
-
-export default QuickInputList;
diff --git a/web/packages/teleterm/src/ui/QuickInput/index.ts b/web/packages/teleterm/src/ui/QuickInput/index.ts
deleted file mode 100644
index 672db18a6f0a7..0000000000000
--- a/web/packages/teleterm/src/ui/QuickInput/index.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
-Copyright 2021 Gravitational, Inc.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import QuickInput from './QuickInput';
-export default QuickInput;
diff --git a/web/packages/teleterm/src/ui/QuickInput/useQuickInput.ts b/web/packages/teleterm/src/ui/QuickInput/useQuickInput.ts
deleted file mode 100644
index 1e49de33d2419..0000000000000
--- a/web/packages/teleterm/src/ui/QuickInput/useQuickInput.ts
+++ /dev/null
@@ -1,215 +0,0 @@
-/*
-Copyright 2019 Gravitational, Inc.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import React, { useEffect } from 'react';
-
-import { CanceledError, useAsync } from 'shared/hooks/useAsync';
-
-import { useAppContext } from 'teleterm/ui/appContextProvider';
-import {
- useKeyboardShortcuts,
- useKeyboardShortcutFormatters,
-} from 'teleterm/ui/services/keyboardShortcuts';
-import {
- AutocompleteCommand,
- AutocompleteToken,
- Suggestion,
-} from 'teleterm/ui/services/quickInput/types';
-import { routing } from 'teleterm/ui/uri';
-import { KeyboardShortcutAction } from 'teleterm/services/config';
-
-import { assertUnreachable, retryWithRelogin } from '../utils';
-
-export default function useQuickInput() {
- const appContext = useAppContext();
- const {
- quickInputService,
- workspacesService,
- commandLauncher,
- usageService,
- } = appContext;
- workspacesService.useState();
- const documentsService =
- workspacesService.getActiveWorkspaceDocumentService();
- const { visible, inputValue } = quickInputService.useState();
- const [activeSuggestion, setActiveSuggestion] = React.useState(0);
-
- const parseResult = React.useMemo(
- () => quickInputService.parse(inputValue),
- // `localClusterUri` has been added to refresh suggestions from
- // `QuickSshLoginPicker` and `QuickServerPicker` when it changes
- [inputValue, workspacesService.getActiveWorkspace()?.localClusterUri]
- );
-
- const [suggestionsAttempt, getSuggestions] = useAsync(() =>
- retryWithRelogin(
- appContext,
- workspacesService.getActiveWorkspace()?.localClusterUri,
- () => parseResult.getSuggestions()
- )
- );
-
- useEffect(() => {
- async function get() {
- const [, err] = await getSuggestions();
- if (err && !(err instanceof CanceledError)) {
- appContext.notificationsService.notifyError({
- title: 'Could not fetch command bar suggestions',
- description: err.message,
- });
- }
- }
-
- get();
- }, [parseResult]);
-
- const hasSuggestions =
- suggestionsAttempt.status === 'success' &&
- suggestionsAttempt.data.length > 0;
- const openSearchBarShortcutAction: KeyboardShortcutAction = 'openSearchBar';
- const { getAccelerator } = useKeyboardShortcutFormatters();
-
- const onFocus = (e: any) => {
- if (e.relatedTarget) {
- quickInputService.lastFocused = new WeakRef(e.relatedTarget);
- }
- };
-
- const onActiveSuggestion = (index: number) => {
- if (!hasSuggestions) {
- return;
- }
- setActiveSuggestion(index);
- };
-
- const onEnter = (index?: number) => {
- if (!hasSuggestions || !visible) {
- executeCommand(parseResult.command);
- return;
- }
-
- pickSuggestion(parseResult.targetToken, suggestionsAttempt.data, index);
- };
-
- const executeCommand = (command: AutocompleteCommand) => {
- switch (command.kind) {
- case 'command.unknown': {
- const params = routing.parseClusterUri(
- workspacesService.getActiveWorkspace()?.localClusterUri
- ).params;
- // ugly hack but QuickInput will be removed in v13
- if (inputValue.startsWith('tsh proxy db')) {
- usageService.captureProtocolUse(
- workspacesService.getRootClusterUri(),
- 'db',
- 'search_bar'
- );
- }
- documentsService.openNewTerminal({
- initCommand: inputValue,
- rootClusterId: routing.parseClusterUri(
- workspacesService.getRootClusterUri()
- ).params.rootClusterId,
- leafClusterId: params.leafClusterId,
- });
- break;
- }
- case 'command.tsh-ssh': {
- const { localClusterUri } = workspacesService.getActiveWorkspace();
-
- commandLauncher.executeCommand('tsh-ssh', {
- loginHost: command.loginHost,
- localClusterUri,
- origin: 'search_bar',
- });
- break;
- }
- case 'command.tsh-install': {
- commandLauncher.executeCommand('tsh-install', undefined);
- break;
- }
- case 'command.tsh-uninstall': {
- commandLauncher.executeCommand('tsh-uninstall', undefined);
- break;
- }
- default: {
- assertUnreachable(command);
- }
- }
-
- quickInputService.clearInputValueAndHide();
- };
-
- const pickSuggestion = (
- targetToken: AutocompleteToken,
- suggestions: Suggestion[],
- index?: number
- ) => {
- const suggestion = suggestions[index];
-
- setActiveSuggestion(index);
- quickInputService.pickSuggestion(targetToken, suggestion);
- };
-
- const onEscape = () => {
- setActiveSuggestion(0);
-
- // If there are suggestions to show, the first onBack call should always just close the
- // suggestions and the second call should actually go back.
- if (visible && hasSuggestions) {
- quickInputService.hide();
- } else {
- quickInputService.goBack();
- }
- };
-
- useKeyboardShortcuts({
- [openSearchBarShortcutAction]: () => {
- quickInputService.show();
- },
- });
-
- // Reset active suggestion when the suggestion list changes.
- // We extract just the tokens and stringify the list to avoid stringifying big objects.
- // See https://github.com/facebook/react/issues/14476#issuecomment-471199055
- // TODO(ravicious): Remove the unnecessary effect.
- // https://beta.reactjs.org/learn/you-might-not-need-an-effect#chains-of-computations
- // https://beta.reactjs.org/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes
- useEffect(() => {
- setActiveSuggestion(0);
- }, [
- // We want to reset the active suggestion only if the
- // suggestions have changed.
- suggestionsAttempt.data?.map(suggestion => suggestion.token).join(','),
- ]);
-
- return {
- visible,
- suggestionsAttempt,
- activeSuggestion,
- inputValue,
- onFocus,
- onEscape,
- onEnter,
- onActiveSuggestion,
- onInputChange: quickInputService.setInputValue,
- onHide: quickInputService.hide,
- onShow: quickInputService.show,
- keyboardShortcut: getAccelerator(openSearchBarShortcutAction),
- };
-}
-
-export type State = ReturnType;
diff --git a/web/packages/teleterm/src/ui/TopBar/AdditionalActions.tsx b/web/packages/teleterm/src/ui/TopBar/AdditionalActions.tsx
index 72f6f2e46fd1d..793d716d9f078 100644
--- a/web/packages/teleterm/src/ui/TopBar/AdditionalActions.tsx
+++ b/web/packages/teleterm/src/ui/TopBar/AdditionalActions.tsx
@@ -40,12 +40,7 @@ type MenuItem = {
function useMenuItems(): MenuItem[] {
const ctx = useAppContext();
- const {
- workspacesService,
- mainProcessClient,
- configService,
- notificationsService,
- } = ctx;
+ const { workspacesService, mainProcessClient, notificationsService } = ctx;
workspacesService.useState();
ctx.clustersService.useState();
const documentsService =
@@ -61,12 +56,11 @@ function useMenuItems(): MenuItem[] {
const { platform } = mainProcessClient.getRuntimeSettings();
const isDarwin = platform === 'darwin';
- const isSearchBarEnabled = configService.get('feature.searchBar').value;
const menuItems: MenuItem[] = [
{
title: 'Open new terminal',
- isVisible: isSearchBarEnabled,
+ isVisible: true,
Icon: icons.Terminal,
keyboardShortcutAction: 'newTerminalTab',
onNavigate: openTerminalTab,
@@ -82,7 +76,7 @@ function useMenuItems(): MenuItem[] {
},
{
title: 'Install tsh in PATH',
- isVisible: isSearchBarEnabled && isDarwin,
+ isVisible: isDarwin,
Icon: icons.Link,
onNavigate: () => {
ctx.commandLauncher.executeCommand('tsh-install', undefined);
@@ -90,7 +84,7 @@ function useMenuItems(): MenuItem[] {
},
{
title: 'Remove tsh from PATH',
- isVisible: isSearchBarEnabled && isDarwin,
+ isVisible: isDarwin,
Icon: icons.Unlink,
onNavigate: () => {
ctx.commandLauncher.executeCommand('tsh-uninstall', undefined);
diff --git a/web/packages/teleterm/src/ui/TopBar/TopBar.tsx b/web/packages/teleterm/src/ui/TopBar/TopBar.tsx
index e61ec37bb7609..ab435bce7e278 100644
--- a/web/packages/teleterm/src/ui/TopBar/TopBar.tsx
+++ b/web/packages/teleterm/src/ui/TopBar/TopBar.tsx
@@ -18,9 +18,7 @@ import React from 'react';
import styled from 'styled-components';
import { Flex } from 'design';
-import QuickInput from '../QuickInput';
import { SearchBar } from '../Search';
-import { useAppContext } from '../appContextProvider';
import { Connections } from './Connections';
import { Clusters } from './Clusters';
@@ -28,9 +26,6 @@ import { Identity } from './Identity';
import { AdditionalActions } from './AdditionalActions';
export function TopBar() {
- const { configService } = useAppContext();
- const isSearchBarEnabled = configService.get('feature.searchBar').value;
-
return (
@@ -38,7 +33,7 @@ export function TopBar() {
- {isSearchBarEnabled ? : }
+
diff --git a/web/packages/teleterm/src/ui/appContext.ts b/web/packages/teleterm/src/ui/appContext.ts
index ea1412bd06147..ae7548f93c978 100644
--- a/web/packages/teleterm/src/ui/appContext.ts
+++ b/web/packages/teleterm/src/ui/appContext.ts
@@ -27,7 +27,6 @@ import { ClustersService } from 'teleterm/ui/services/clusters';
import { ModalsService } from 'teleterm/ui/services/modals';
import { TerminalsService } from 'teleterm/ui/services/terminals';
import { ConnectionTrackerService } from 'teleterm/ui/services/connectionTracker';
-import { QuickInputService } from 'teleterm/ui/services/quickInput';
import { StatePersistenceService } from 'teleterm/ui/services/statePersistence';
import { KeyboardShortcutsService } from 'teleterm/ui/services/keyboardShortcuts';
import { WorkspacesService } from 'teleterm/ui/services/workspacesService/workspacesService';
@@ -48,7 +47,6 @@ export default class AppContext implements IAppContext {
notificationsService: NotificationsService;
terminalsService: TerminalsService;
keyboardShortcutsService: KeyboardShortcutsService;
- quickInputService: QuickInputService;
statePersistenceService: StatePersistenceService;
workspacesService: WorkspacesService;
mainProcessClient: MainProcessClient;
@@ -118,13 +116,6 @@ export default class AppContext implements IAppContext {
this.commandLauncher = new CommandLauncher(this);
- this.quickInputService = new QuickInputService(
- this.commandLauncher,
- this.clustersService,
- this.resourcesService,
- this.workspacesService
- );
-
this.connectionTracker = new ConnectionTrackerService(
this.statePersistenceService,
this.workspacesService,
diff --git a/web/packages/teleterm/src/ui/commandLauncher.test.ts b/web/packages/teleterm/src/ui/commandLauncher.test.ts
deleted file mode 100644
index 99ba9cf9c7f17..0000000000000
--- a/web/packages/teleterm/src/ui/commandLauncher.test.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-/**
- * Copyright 2023 Gravitational, Inc
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { MockAppContext } from 'teleterm/ui/fixtures/mocks';
-
-import { CommandLauncher } from './commandLauncher';
-
-it('returns tsh install & uninstall autocomplete command on macOS', () => {
- const appContext = new MockAppContext({ platform: 'darwin' });
- const commandLauncher = new CommandLauncher(appContext);
- const autocompleteCommandNames = commandLauncher
- .getAutocompleteCommands()
- .map(c => c.displayName);
-
- expect(autocompleteCommandNames).toContain('tsh install');
- expect(autocompleteCommandNames).toContain('tsh uninstall');
-});
-
-it('does not return tsh install & uninstall autocomplete command on Linux', () => {
- const appContext = new MockAppContext({ platform: 'linux' });
- const commandLauncher = new CommandLauncher(appContext);
- const autocompleteCommandNames = commandLauncher
- .getAutocompleteCommands()
- .map(c => c.displayName);
-
- expect(autocompleteCommandNames).not.toContain('tsh install');
- expect(autocompleteCommandNames).not.toContain('tsh uninstall');
-});
-
-it('does not return tsh install & uninstall autocomplete command on Windows', () => {
- const appContext = new MockAppContext({ platform: 'win32' });
- const commandLauncher = new CommandLauncher(appContext);
- const autocompleteCommandNames = commandLauncher
- .getAutocompleteCommands()
- .map(c => c.displayName);
-
- expect(autocompleteCommandNames).not.toContain('tsh install');
- expect(autocompleteCommandNames).not.toContain('tsh uninstall');
-});
diff --git a/web/packages/teleterm/src/ui/commandLauncher.ts b/web/packages/teleterm/src/ui/commandLauncher.ts
index be5780d5c8625..3e3294fbce2c3 100644
--- a/web/packages/teleterm/src/ui/commandLauncher.ts
+++ b/web/packages/teleterm/src/ui/commandLauncher.ts
@@ -15,41 +15,9 @@ limitations under the License.
*/
import { IAppContext } from 'teleterm/ui/types';
-import { ClusterUri, RootClusterUri, routing } from 'teleterm/ui/uri';
-import { Platform } from 'teleterm/mainProcess/types';
-import { DocumentOrigin } from 'teleterm/ui/services/workspacesService';
+import { ClusterUri, RootClusterUri } from 'teleterm/ui/uri';
const commands = {
- // For handling "tsh ssh" executed from the command bar.
- 'tsh-ssh': {
- displayName: '',
- description: '',
- async run(
- ctx: IAppContext,
- args: {
- loginHost: string;
- localClusterUri: ClusterUri;
- origin: DocumentOrigin;
- }
- ) {
- const { loginHost, localClusterUri, origin } = args;
- const rootClusterUri = routing.ensureRootClusterUri(localClusterUri);
- const documentsService =
- ctx.workspacesService.getWorkspaceDocumentService(rootClusterUri);
-
- const doc = documentsService.createTshNodeDocumentFromLoginHost(
- localClusterUri,
- loginHost,
- { origin }
- );
-
- await ctx.workspacesService.setActiveWorkspace(rootClusterUri);
-
- documentsService.add(doc);
- documentsService.setLocation(doc.uri);
- },
- },
-
'tsh-install': {
displayName: '',
description: '',
@@ -147,31 +115,6 @@ const commands = {
},
};
-const autocompleteCommands: {
- displayName: string;
- description: string;
- platforms?: Array;
-}[] = [
- {
- displayName: 'tsh ssh',
- description: 'Run shell or execute a command on a remote SSH node',
- },
- {
- displayName: 'tsh proxy db',
- description: 'Start a local proxy for a database connection',
- },
- {
- displayName: 'tsh install',
- description: 'Install tsh in PATH',
- platforms: ['darwin'],
- },
- {
- displayName: 'tsh uninstall',
- description: 'Uninstall tsh from PATH',
- platforms: ['darwin'],
- },
-];
-
export class CommandLauncher {
appContext: IAppContext;
@@ -183,15 +126,6 @@ export class CommandLauncher {
commands[name].run(this.appContext, args as any);
return undefined;
}
-
- getAutocompleteCommands() {
- const { platform } = this.appContext.mainProcessClient.getRuntimeSettings();
-
- return autocompleteCommands.filter(command => {
- const platforms = command.platforms;
- return !command.platforms || platforms.includes(platform);
- });
- }
}
type CommandName = keyof typeof commands;
diff --git a/web/packages/teleterm/src/ui/services/quickInput/index.ts b/web/packages/teleterm/src/ui/services/quickInput/index.ts
deleted file mode 100644
index 3b2ba6ee632b1..0000000000000
--- a/web/packages/teleterm/src/ui/services/quickInput/index.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
-Copyright 2019 Gravitational, Inc.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-export * from './quickInputService';
-export * from './types';
diff --git a/web/packages/teleterm/src/ui/services/quickInput/parsers.test.ts b/web/packages/teleterm/src/ui/services/quickInput/parsers.test.ts
deleted file mode 100644
index 0c513f080c12f..0000000000000
--- a/web/packages/teleterm/src/ui/services/quickInput/parsers.test.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-/**
- * Copyright 2023 Gravitational, Inc
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { QuickSshLoginSuggester, QuickServerSuggester } from './suggesters';
-
-// Jest doesn't let us selectively automock classes. See https://github.com/facebook/jest/issues/11995
-//
-// So instead for now we just mock all classes in the module and then do `jest.requireActual` when
-// we need to have the actual class when writing tests for it.
-jest.mock('./parsers');
-
-afterEach(() => {
- jest.restoreAllMocks();
-});
-
-test("tsh ssh picker returns unknown command if it's missing the first positional arg", async () => {
- const QuickSshLoginSuggesterMock = QuickSshLoginSuggester as jest.MockedClass<
- typeof QuickSshLoginSuggester
- >;
- const QuickServerSuggesterMock = QuickServerSuggester as jest.MockedClass<
- typeof QuickServerSuggester
- >;
- const ActualQuickTshSshParser =
- jest.requireActual('./parsers').QuickTshSshParser;
-
- const parser = new ActualQuickTshSshParser(
- new QuickSshLoginSuggesterMock(undefined, undefined),
- new QuickServerSuggesterMock(undefined, undefined)
- );
-
- const emptyInput = await parser.parse('', 0);
- expect(emptyInput.command).toEqual({ kind: 'command.unknown' });
-
- const whitespace = await parser.parse(' ', 0);
- expect(whitespace.command).toEqual({ kind: 'command.unknown' });
-});
-
-test('tsh ssh picker returns unknown command if the input includes any additional flags', async () => {
- const QuickSshLoginSuggesterMock = QuickSshLoginSuggester as jest.MockedClass<
- typeof QuickSshLoginSuggester
- >;
- const QuickServerSuggesterMock = QuickServerSuggester as jest.MockedClass<
- typeof QuickServerSuggester
- >;
- const ActualQuickTshSshParser =
- jest.requireActual('./parsers').QuickTshSshParser;
-
- const parser = new ActualQuickTshSshParser(
- new QuickSshLoginSuggesterMock(undefined, undefined),
- new QuickServerSuggesterMock(undefined, undefined)
- );
-
- const fullFlagBefore = await parser.parse('--foo user@node', 0);
- expect(fullFlagBefore.command).toEqual({ kind: 'command.unknown' });
-
- const shortFlagBefore = await parser.parse('-p 22 user@node', 0);
- expect(shortFlagBefore.command).toEqual({ kind: 'command.unknown' });
-
- const commandAfter = await parser.parse('user@node ls', 0);
- expect(commandAfter.command).toEqual({ kind: 'command.unknown' });
-});
diff --git a/web/packages/teleterm/src/ui/services/quickInput/parsers.ts b/web/packages/teleterm/src/ui/services/quickInput/parsers.ts
deleted file mode 100644
index f12b0bce0aef0..0000000000000
--- a/web/packages/teleterm/src/ui/services/quickInput/parsers.ts
+++ /dev/null
@@ -1,340 +0,0 @@
-/*
-Copyright 2019 Gravitational, Inc.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { CommandLauncher } from 'teleterm/ui/commandLauncher';
-
-import {
- AutocompleteCommand,
- AutocompleteToken,
- SuggestionCmd,
- QuickInputParser,
- ParseResult,
-} from './types';
-import * as suggesters from './suggesters';
-
-// Pair of helper values to return when a given parser arrives at a situation where it knows there
-// will be no suggestions to return (and hence also no useful target token to return).
-const emptyTargetToken: AutocompleteToken = { value: '', startIndex: 0 };
-const noSuggestions = () => Promise.resolve([]);
-
-export class QuickCommandParser implements QuickInputParser {
- private parserRegistry: Map;
-
- constructor(private launcher: CommandLauncher) {
- this.parserRegistry = new Map();
- }
-
- registerParserForCommand(command: string, parser: QuickInputParser) {
- this.parserRegistry.set(command, parser);
- }
-
- // TODO(ravicious): Handle env vars.
- parse(rawInput: string): ParseResult {
- // We can safely ignore any whitespace at the start. However, `startIndex` needs to account for
- // any removed whitespace.
- const input = rawInput.trimStart();
- const targetToken = {
- value: input,
- startIndex: rawInput.indexOf(input),
- };
-
- // Return all commands if there's no input.
- if (input === '') {
- const autocompleteCommands = this.launcher.getAutocompleteCommands();
-
- return {
- targetToken,
- command: { kind: 'command.unknown' },
- getSuggestions: () =>
- Promise.resolve(
- this.mapAutocompleteCommandsToSuggestions(autocompleteCommands)
- ),
- };
- }
-
- const matchingAutocompleteCommands = this.launcher
- .getAutocompleteCommands()
- .filter(cmd => {
- const completeMatchRegex = new RegExp(`^${cmd.displayName}\\b`, 'i');
- return (
- cmd.displayName.startsWith(input.toLowerCase()) ||
- // `completeMatchRegex` handles situations where the `input` akin to "tsh ssh foo".
- // In that case, "tsh ssh" is the matching command, even though
- // `cmd.displayName` ("tsh ssh") doesn't start with `input` ("tsh ssh foo").
- completeMatchRegex.test(input)
- );
- });
-
- if (matchingAutocompleteCommands.length === 0) {
- return {
- targetToken: emptyTargetToken,
- command: { kind: 'command.unknown' },
- getSuggestions: noSuggestions,
- };
- }
-
- if (matchingAutocompleteCommands.length > 1) {
- return {
- targetToken,
- command: { kind: 'command.unknown' },
- getSuggestions: () =>
- Promise.resolve(
- this.mapAutocompleteCommandsToSuggestions(
- matchingAutocompleteCommands
- )
- ),
- };
- }
-
- // The rest of the function body handles situation in which there's only one matching
- // autocomplete command.
-
- // Handles a complete match, for example the input is `tsh ssh`.
- const soleMatch = matchingAutocompleteCommands[0];
- const commandToken = soleMatch.displayName;
- const completeMatchRegex = new RegExp(`^${commandToken}\\b`, 'i');
- const isCompleteMatch = completeMatchRegex.test(input);
-
- if (isCompleteMatch) {
- const inputWithoutCommandPrefix = input.replace(completeMatchRegex, '');
- // Add length of the command we just replaced with an empty string to startIndex,
- // so that the next parser has the correct index for the target token.
- const commandStartIndex = targetToken.startIndex + commandToken.length;
- const nextQuickInputParser = this.parserRegistry.get(commandToken);
-
- return nextQuickInputParser.parse(
- inputWithoutCommandPrefix,
- commandStartIndex
- );
- }
-
- // Handles a non-complete match with only a single matching command, for example if the input is
- // `tsh ss` (`tsh ssh` should be the only matching command then).
- return {
- targetToken,
- command: { kind: 'command.unknown' },
- getSuggestions: () =>
- Promise.resolve(
- this.mapAutocompleteCommandsToSuggestions(
- matchingAutocompleteCommands
- )
- ),
- };
- }
-
- private mapAutocompleteCommandsToSuggestions(
- commands: { displayName: string; description: string }[]
- ): SuggestionCmd[] {
- return commands.map(cmd => {
- const acceptsNoArguments =
- this.parserRegistry.get(cmd.displayName) instanceof
- QuickNoArgumentsParser;
- // If a command accepts arguments, let's append a space when that suggestion is picked.
- // This allows us to immediately show autocomplete for the first argument of the command.
- const appendToToken = acceptsNoArguments ? '' : ' ';
-
- return {
- kind: 'suggestion.cmd' as const,
- token: cmd.displayName,
- appendToToken: appendToToken,
- data: cmd,
- };
- });
- }
-}
-
-export class QuickTshSshParser implements QuickInputParser {
- // An SSH login doesn't start with `-`, hence the special group for the first character.
- private sshLoginRegex = /[a-z0-9_][a-z0-9_-]*/i;
- private totalSshLoginRegex = new RegExp(
- `^${this.sshLoginRegex.source}$`,
- 'i'
- );
- // For now we assume there's nothing else after user@host, so if we see any space after `@`, we
- // don't show any matches.
- // To support that properly, we'd need to add account for the cursor index.
- private totalSshLoginAndHostRegex = new RegExp(
- `^(?${this.sshLoginRegex.source}@)(?\\S*)$`,
- 'i'
- );
-
- constructor(
- private sshLoginSuggester: suggesters.QuickSshLoginSuggester,
- private serverSuggester: suggesters.QuickServerSuggester
- ) {}
-
- // TODO: Support cluster arg.
- parse(rawInput: string, startIndex: number): ParseResult {
- // We can safely ignore any whitespace at the start. However, `startIndex` needs to account for
- // any removed whitespace.
- const input = rawInput.trimStart();
- if (input === '') {
- // input is empty, so rawInput must include only whitespace.
- // Add length of the whitespace to startIndex.
- startIndex += rawInput.length;
- } else {
- startIndex += rawInput.indexOf(input);
- }
-
- // Show autocomplete only after at least one space after `tsh ssh`.
- if (rawInput !== '' && input === '') {
- // Returning unknown command for the same reasons as outlined at the end of this function.
- const command = {
- kind: 'command.unknown' as const,
- };
- const targetToken = { value: '', startIndex };
- return {
- targetToken,
- command,
- getSuggestions: () =>
- this.sshLoginSuggester.getSuggestions(targetToken.value),
- };
- }
-
- const hostMatch = input.match(this.totalSshLoginAndHostRegex);
-
- if (hostMatch) {
- const command = {
- kind: 'command.tsh-ssh' as const,
- loginHost: hostMatch[0],
- };
- const hostStartIndex = startIndex + hostMatch.groups.loginPart.length;
- const targetToken = {
- value: hostMatch.groups.hostPart,
- startIndex: hostStartIndex,
- };
-
- return {
- targetToken,
- command,
- getSuggestions: () =>
- this.serverSuggester.getSuggestions(targetToken.value),
- };
- }
-
- const loginMatch = input.match(this.totalSshLoginRegex);
-
- if (loginMatch) {
- const command = {
- kind: 'command.tsh-ssh' as const,
- loginHost: loginMatch[0],
- };
- const targetToken = {
- value: loginMatch[0],
- startIndex,
- };
- return {
- targetToken,
- command,
- getSuggestions: () =>
- this.sshLoginSuggester.getSuggestions(targetToken.value),
- };
- }
-
- // The code gets to this point if either `input` is empty or it has additional arguments besides
- // the first positional argument.
- //
- // In case of the input being empty, we know that at this point we don't have enough arguments
- // to successfully launch tsh ssh. But we also don't have code to handle this error case. So
- // instead we return an unknown command, so that if the user presses enter at this point, we'll
- // launch `tsh ssh` in a local shell and it'll show the appropriate error.
- //
- // In case of additional arguments, the command bar doesn't know how to handle them. This would
- // require adding a real parser which we're going to do soon. In the meantime, we just run the
- // command in a local shell to a similar effect (though the host to which someone tries to
- // connect to won't show up in the connection tracker and so on).
- return {
- command: { kind: 'command.unknown' },
- targetToken: emptyTargetToken,
- getSuggestions: noSuggestions,
- };
- }
-}
-
-export class QuickTshProxyDbParser implements QuickInputParser {
- private totalDbNameRegex = /^\S+$/i;
-
- constructor(private databaseSuggester: suggesters.QuickDatabaseSuggester) {}
-
- parse(rawInput: string, startIndex: number): ParseResult {
- // We can safely ignore any whitespace at the start. However, `startIndex` needs to account for
- // any removed whitespace.
- const input = rawInput.trimStart();
- if (input === '') {
- // input is empty, so rawInput must include only whitespace.
- // Add length of the whitespace to startIndex.
- startIndex += rawInput.length;
- } else {
- startIndex += rawInput.indexOf(input);
- }
-
- // Show autocomplete only after at least one space after `tsh proxy db`.
- if (rawInput !== '' && input === '') {
- const targetToken = {
- value: '',
- startIndex,
- };
- return {
- targetToken,
- command: { kind: 'command.unknown' },
- getSuggestions: () =>
- this.databaseSuggester.getSuggestions(targetToken.value),
- };
- }
-
- const dbNameMatch = input.match(this.totalDbNameRegex);
-
- if (dbNameMatch) {
- const targetToken = {
- value: dbNameMatch[0],
- startIndex,
- };
- return {
- targetToken,
- command: { kind: 'command.unknown' },
- getSuggestions: () =>
- this.databaseSuggester.getSuggestions(targetToken.value),
- };
- }
-
- return {
- targetToken: emptyTargetToken,
- command: { kind: 'command.unknown' },
- getSuggestions: noSuggestions,
- };
- }
-}
-
-// QuickNoArgumentsParser is useful in situations where a command does not accept any arguments.
-// If QuickNoArgumentsParser is registered as the parser for that command, selecting that command
-// from suggestions will simply close autocomplete. Pressing Enter again will execute the command
-// passed to the constructor of QuickNoArgumentsParser.
-export class QuickNoArgumentsParser implements QuickInputParser {
- constructor(private command: AutocompleteCommand) {}
-
- parse(rawInput: string, startIndex: number): ParseResult {
- const targetToken = {
- value: '',
- startIndex,
- };
-
- return {
- targetToken,
- command: this.command,
- getSuggestions: noSuggestions,
- };
- }
-}
diff --git a/web/packages/teleterm/src/ui/services/quickInput/quickInputService.test.ts b/web/packages/teleterm/src/ui/services/quickInput/quickInputService.test.ts
deleted file mode 100644
index 58e0b73879509..0000000000000
--- a/web/packages/teleterm/src/ui/services/quickInput/quickInputService.test.ts
+++ /dev/null
@@ -1,566 +0,0 @@
-/**
- * Copyright 2023 Gravitational, Inc
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { CommandLauncher } from 'teleterm/ui/commandLauncher';
-import { ClustersService } from 'teleterm/ui/services/clusters';
-import { WorkspacesService } from 'teleterm/ui/services/workspacesService';
-import { ResourcesService } from 'teleterm/ui/services/resources';
-
-import { getEmptyPendingAccessRequest } from '../workspacesService/accessRequestsService';
-
-import { QuickInputService } from './quickInputService';
-import * as parsers from './parsers';
-import * as suggestors from './suggesters';
-import { SuggestionCmd, SuggestionSshLogin } from './types';
-
-afterEach(() => {
- jest.restoreAllMocks();
-});
-
-jest.mock('teleterm/ui/commandLauncher');
-jest.mock('teleterm/ui/services/clusters');
-jest.mock('teleterm/ui/services/workspacesService');
-
-const CommandLauncherMock = CommandLauncher as jest.MockedClass<
- typeof CommandLauncher
->;
-const ClustersServiceMock = ClustersService as jest.MockedClass<
- typeof ClustersService
->;
-const ResourcesServiceMock = ResourcesService as jest.MockedClass<
- typeof ResourcesService
->;
-const WorkspacesServiceMock = WorkspacesService as jest.MockedClass<
- typeof WorkspacesService
->;
-
-const onlyTshSshCommand = [
- {
- name: 'autocomplete.tsh-ssh',
- displayName: 'tsh ssh',
- description: '',
- run: () => {},
- },
-];
-
-function createQuickInputService() {
- return new QuickInputService(
- new CommandLauncherMock(undefined),
- new ClustersServiceMock(undefined, undefined, undefined, undefined),
- new ResourcesServiceMock(undefined),
- new WorkspacesServiceMock(undefined, undefined, undefined, undefined)
- );
-}
-
-function mockCommandLauncherAutocompleteCommands(
- commandLauncherMock: jest.MockedClass,
- commands: {
- name: string;
- displayName: string;
- description: string;
- run: () => void;
- }[]
-) {
- jest
- .spyOn(commandLauncherMock.prototype, 'getAutocompleteCommands')
- .mockImplementation(() => {
- return commands;
- });
-}
-
-test('parse returns correct result for a command suggestion with empty input', async () => {
- mockCommandLauncherAutocompleteCommands(
- CommandLauncherMock,
- onlyTshSshCommand
- );
- const quickInputService = createQuickInputService();
-
- const { getSuggestions, targetToken, command } = quickInputService.parse('');
- const suggestions = await getSuggestions();
-
- expect(suggestions).toHaveLength(1);
- expect(targetToken).toEqual({
- value: '',
- startIndex: 0,
- });
- expect(command).toEqual({ kind: 'command.unknown' });
-});
-
-test('parse returns correct result for a command suggestion', async () => {
- mockCommandLauncherAutocompleteCommands(
- CommandLauncherMock,
- onlyTshSshCommand
- );
- const quickInputService = createQuickInputService();
-
- const { getSuggestions, targetToken, command } =
- quickInputService.parse('ts');
- const suggestions = await getSuggestions();
-
- expect(suggestions).toHaveLength(1);
- expect(targetToken).toEqual({
- value: 'ts',
- startIndex: 0,
- });
- expect(command).toEqual({ kind: 'command.unknown' });
-});
-
-test('parse returns correct result for an SSH login suggestion', async () => {
- mockCommandLauncherAutocompleteCommands(
- CommandLauncherMock,
- onlyTshSshCommand
- );
- jest
- .spyOn(suggestors.QuickSshLoginSuggester.prototype, 'getSuggestions')
- .mockImplementation(async () => {
- return [
- {
- kind: 'suggestion.ssh-login',
- token: 'root',
- appendToToken: '@',
- data: 'root',
- },
- ];
- });
- const quickInputService = createQuickInputService();
-
- const { getSuggestions, targetToken, command } =
- quickInputService.parse('tsh ssh roo');
- const suggestions = await getSuggestions();
-
- expect(suggestions).toHaveLength(1);
- expect(targetToken).toEqual({
- value: 'roo',
- startIndex: 8,
- });
- expect(command).toEqual({
- kind: 'command.tsh-ssh',
- loginHost: 'roo',
- });
-});
-
-test('parse returns correct result for an SSH login suggestion with spaces between arguments', async () => {
- mockCommandLauncherAutocompleteCommands(
- CommandLauncherMock,
- onlyTshSshCommand
- );
- jest
- .spyOn(suggestors.QuickSshLoginSuggester.prototype, 'getSuggestions')
- .mockImplementation(async () => {
- return [
- {
- kind: 'suggestion.ssh-login',
- token: 'barfoo',
- appendToToken: '@',
- data: 'barfoo',
- },
- ];
- });
- const quickInputService = createQuickInputService();
-
- const { getSuggestions, targetToken, command } =
- quickInputService.parse(' tsh ssh bar');
- const suggestions = await getSuggestions();
-
- expect(suggestions).toHaveLength(1);
- expect(targetToken).toEqual({
- value: 'bar',
- startIndex: 14,
- });
- expect(command).toEqual({
- kind: 'command.tsh-ssh',
- loginHost: 'bar',
- });
-});
-
-test('parse returns correct result for a database name suggestion', async () => {
- mockCommandLauncherAutocompleteCommands(CommandLauncherMock, [
- {
- name: 'autocomplete.tsh-proxy-db',
- displayName: 'tsh proxy db',
- description: '',
- run: () => {},
- },
- ]);
- jest
- .spyOn(WorkspacesServiceMock.prototype, 'getActiveWorkspace')
- .mockImplementation(() => ({
- accessRequests: {
- assumed: {},
- isBarCollapsed: false,
- pending: getEmptyPendingAccessRequest(),
- },
- localClusterUri: '/clusters/test_uri',
- documents: [],
- location: '/docs/1',
- }));
- jest
- .spyOn(ResourcesServiceMock.prototype, 'fetchDatabases')
- .mockImplementation(() => {
- return Promise.resolve({
- agentsList: [
- {
- hostname: 'foobar',
- uri: '/clusters/test_uri/dbs/foobar',
- name: '',
- desc: '',
- protocol: '',
- type: '',
- addr: '',
- labelsList: null,
- },
- ],
- startKey: '',
- totalCount: 1,
- });
- });
- const quickInputService = createQuickInputService();
-
- const { getSuggestions, targetToken, command } =
- quickInputService.parse('tsh proxy db foo');
- const suggestions = await getSuggestions();
-
- expect(suggestions).toHaveLength(1);
- expect(targetToken).toEqual({
- value: 'foo',
- startIndex: 13,
- });
- expect(command).toEqual({ kind: 'command.unknown' });
-});
-
-test("parse doesn't return any suggestions if the only suggestion completely matches the target token", async () => {
- jest.mock('./parsers');
- const QuickCommandParserMock = parsers.QuickCommandParser as jest.MockedClass<
- typeof parsers.QuickCommandParser
- >;
- jest
- .spyOn(QuickCommandParserMock.prototype, 'parse')
- .mockImplementation(() => {
- return {
- getSuggestions: () =>
- Promise.resolve([
- {
- kind: 'suggestion.ssh-login',
- token: 'foobar',
- appendToToken: '@',
- data: 'foobar',
- },
- ]),
- targetToken: {
- startIndex: 0,
- value: 'foobar',
- },
- command: { kind: 'command.unknown' },
- };
- });
- const quickInputService = createQuickInputService();
-
- const { getSuggestions, command } = quickInputService.parse('foobar');
- const suggestions = await getSuggestions();
-
- expect(suggestions).toHaveLength(0);
- expect(command).toEqual({ kind: 'command.unknown' });
-});
-
-test('parse returns no suggestions if any of the parsers returns empty suggestions array', async () => {
- jest.mock('./parsers');
- const QuickCommandParserMock = parsers.QuickCommandParser as jest.MockedClass<
- typeof parsers.QuickCommandParser
- >;
- jest
- .spyOn(QuickCommandParserMock.prototype, 'parse')
- .mockImplementation(() => {
- return {
- getSuggestions: () => Promise.resolve([]),
- targetToken: {
- startIndex: 0,
- value: '',
- },
- command: { kind: 'command.unknown' },
- };
- });
- const quickInputService = createQuickInputService();
-
- const { command, getSuggestions } = quickInputService.parse('');
- const suggestions = await getSuggestions();
-
- expect(suggestions).toHaveLength(0);
- expect(command).toEqual({ kind: 'command.unknown' });
-});
-
-test("the SSH login autocomplete isn't shown if there's no space after `tsh ssh`", async () => {
- mockCommandLauncherAutocompleteCommands(
- CommandLauncherMock,
- onlyTshSshCommand
- );
- const quickInputService = createQuickInputService();
-
- const { command, getSuggestions } = quickInputService.parse('tsh ssh');
- const suggestions = await getSuggestions();
-
- expect(suggestions).toHaveLength(0);
- expect(command).toEqual({ kind: 'command.unknown' });
-});
-
-test("the SSH login autocomplete is shown only if there's at least one space after `tsh ssh`", async () => {
- mockCommandLauncherAutocompleteCommands(
- CommandLauncherMock,
- onlyTshSshCommand
- );
- jest
- .spyOn(suggestors.QuickSshLoginSuggester.prototype, 'getSuggestions')
- .mockImplementation(async () => {
- return [
- {
- kind: 'suggestion.ssh-login',
- token: 'barfoo',
- appendToToken: '@',
- data: 'barfoo',
- },
- ];
- });
- const quickInputService = createQuickInputService();
-
- const { command, targetToken, getSuggestions } =
- quickInputService.parse('tsh ssh ');
- const suggestions = await getSuggestions();
-
- expect(suggestions).toHaveLength(1);
- expect(targetToken).toEqual({
- value: '',
- startIndex: 8,
- });
- expect(command).toEqual({ kind: 'command.unknown' });
-});
-
-test('parse returns correct result for an SSH host suggestion right after user@', async () => {
- mockCommandLauncherAutocompleteCommands(
- CommandLauncherMock,
- onlyTshSshCommand
- );
- jest
- .spyOn(ResourcesServiceMock.prototype, 'fetchServers')
- .mockImplementation(() => {
- return Promise.resolve({
- agentsList: [
- {
- hostname: 'bazbar',
- name: '',
- addr: '',
- uri: '/clusters/foo/servers/bazbar',
- tunnel: false,
- labelsList: null,
- },
- ],
- startKey: '',
- totalCount: 1,
- });
- });
- jest
- .spyOn(WorkspacesServiceMock.prototype, 'getActiveWorkspace')
- .mockImplementation(() => ({
- accessRequests: {
- assumed: {},
- isBarCollapsed: false,
- pending: getEmptyPendingAccessRequest(),
- },
- localClusterUri: '/clusters/test_uri',
- documents: [],
- location: '/docs/1',
- }));
- const quickInputService = createQuickInputService();
-
- const { getSuggestions, targetToken, command } =
- quickInputService.parse('tsh ssh user@');
- const suggestions = await getSuggestions();
-
- expect(suggestions).toHaveLength(1);
- expect(targetToken).toEqual({
- value: '',
- startIndex: 13,
- });
- expect(command).toEqual({
- kind: 'command.tsh-ssh',
- loginHost: 'user@',
- });
-});
-
-test('parse returns correct result for a partial match on an SSH host suggestion', async () => {
- mockCommandLauncherAutocompleteCommands(
- CommandLauncherMock,
- onlyTshSshCommand
- );
- jest
- .spyOn(ResourcesServiceMock.prototype, 'fetchServers')
- .mockImplementation(() => {
- return Promise.resolve({
- agentsList: [
- {
- hostname: 'bazbar',
- name: '',
- addr: '',
- uri: '/clusters/foo/servers/bazbar',
- tunnel: false,
- labelsList: null,
- },
- ],
- startKey: '',
- totalCount: 1,
- });
- });
- jest
- .spyOn(WorkspacesServiceMock.prototype, 'getActiveWorkspace')
- .mockImplementation(() => ({
- accessRequests: {
- assumed: {},
- isBarCollapsed: false,
- pending: getEmptyPendingAccessRequest(),
- },
- localClusterUri: '/clusters/test_uri',
- documents: [],
- location: '/docs/1',
- }));
- const quickInputService = createQuickInputService();
-
- const { getSuggestions, targetToken, command } = quickInputService.parse(
- ' tsh ssh foo@baz'
- );
- const suggestions = await getSuggestions();
-
- expect(suggestions).toHaveLength(1);
- expect(targetToken).toEqual({
- value: 'baz',
- startIndex: 18,
- });
- expect(command).toEqual({
- kind: 'command.tsh-ssh',
- loginHost: 'foo@baz',
- });
-});
-
-test("parse returns the first argument as loginHost when there's no @ sign", async () => {
- mockCommandLauncherAutocompleteCommands(
- CommandLauncherMock,
- onlyTshSshCommand
- );
- jest
- .spyOn(suggestors.QuickSshLoginSuggester.prototype, 'getSuggestions')
- .mockImplementation(async () => {
- return [
- {
- kind: 'suggestion.ssh-login',
- token: 'barfoo',
- appendToToken: '@',
- data: 'barfoo',
- },
- ];
- });
- const quickInputService = createQuickInputService();
-
- const { getSuggestions, targetToken, command } =
- quickInputService.parse('tsh ssh bar');
- const suggestions = await getSuggestions();
-
- expect(suggestions).toHaveLength(1);
- expect(targetToken).toEqual({
- value: 'bar',
- startIndex: 8,
- });
- expect(command).toEqual({
- kind: 'command.tsh-ssh',
- loginHost: 'bar',
- });
-});
-
-test('picking a command suggestion in an empty input autocompletes the command', () => {
- const quickInputService = createQuickInputService();
- quickInputService.setState({ inputValue: '' });
-
- const targetToken = {
- startIndex: 0,
- value: '',
- };
- const cmd: SuggestionCmd = {
- kind: 'suggestion.cmd',
- token: 'tsh ssh',
- data: {
- displayName: 'tsh ssh',
- description: '',
- },
- };
- quickInputService.pickSuggestion(targetToken, cmd);
-
- expect(quickInputService.getInputValue()).toBe('tsh ssh');
-});
-
-test('picking a command suggestion in an input with a single space preserves the space', () => {
- const quickInputService = createQuickInputService();
- quickInputService.setState({ inputValue: ' ' });
-
- const targetToken = {
- startIndex: 1,
- value: '',
- };
- const cmd: SuggestionCmd = {
- kind: 'suggestion.cmd',
- token: 'tsh ssh',
- data: {
- displayName: 'tsh ssh',
- description: '',
- },
- };
- quickInputService.pickSuggestion(targetToken, cmd);
-
- expect(quickInputService.getInputValue()).toBe(' tsh ssh');
-});
-
-test('picking an SSH login suggestion replaces target token in input value', () => {
- const quickInputService = createQuickInputService();
- quickInputService.setState({ inputValue: 'tsh ssh roo --foo' });
-
- const targetToken = {
- value: 'roo',
- startIndex: 8,
- };
- const sshLogin: SuggestionSshLogin = {
- kind: 'suggestion.ssh-login',
- token: 'root',
- appendToToken: '@',
- data: 'root',
- };
- quickInputService.pickSuggestion(targetToken, sshLogin);
-
- expect(quickInputService.getInputValue()).toBe('tsh ssh root@ --foo');
-});
-
-test('pickSuggestion appends the appendToToken field to the token', () => {
- const quickInputService = createQuickInputService();
- quickInputService.setState({ inputValue: 'tsh ssh foo' });
-
- const targetToken = {
- value: 'foo',
- startIndex: 8,
- };
- const sshLogin: SuggestionSshLogin = {
- kind: 'suggestion.ssh-login',
- token: 'foobar',
- appendToToken: '@barbaz',
- data: 'foobar',
- };
- quickInputService.pickSuggestion(targetToken, sshLogin);
-
- expect(quickInputService.getInputValue()).toBe('tsh ssh foobar@barbaz');
-});
diff --git a/web/packages/teleterm/src/ui/services/quickInput/quickInputService.ts b/web/packages/teleterm/src/ui/services/quickInput/quickInputService.ts
deleted file mode 100644
index 1d114c7bcabc7..0000000000000
--- a/web/packages/teleterm/src/ui/services/quickInput/quickInputService.ts
+++ /dev/null
@@ -1,195 +0,0 @@
-/*
-Copyright 2019 Gravitational, Inc.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { Store, useStore } from 'shared/libs/stores';
-
-import { CommandLauncher } from 'teleterm/ui/commandLauncher';
-import { WorkspacesService } from 'teleterm/ui/services/workspacesService';
-import { ResourcesService } from 'teleterm/ui/services/resources';
-import { ClustersService } from 'teleterm/ui/services/clusters';
-
-import * as parsers from './parsers';
-import * as suggesters from './suggesters';
-import { AutocompleteToken, ParseResult, Suggestion } from './types';
-
-type State = {
- inputValue: string;
- visible: boolean;
-};
-
-export class QuickInputService extends Store {
- private quickCommandParser: parsers.QuickCommandParser;
- lastFocused: WeakRef;
-
- constructor(
- launcher: CommandLauncher,
- clustersService: ClustersService,
- resourcesService: ResourcesService,
- workspacesService: WorkspacesService
- ) {
- super();
- this.lastFocused = new WeakRef(document.createElement('div'));
- this.quickCommandParser = new parsers.QuickCommandParser(launcher);
- this.setState({
- inputValue: '',
- });
-
- const sshLoginSuggester = new suggesters.QuickSshLoginSuggester(
- workspacesService,
- clustersService
- );
- const serverSuggester = new suggesters.QuickServerSuggester(
- workspacesService,
- resourcesService
- );
- const databaseSuggester = new suggesters.QuickDatabaseSuggester(
- workspacesService,
- resourcesService
- );
-
- this.quickCommandParser.registerParserForCommand(
- 'tsh ssh',
- new parsers.QuickTshSshParser(sshLoginSuggester, serverSuggester)
- );
- this.quickCommandParser.registerParserForCommand(
- 'tsh proxy db',
- new parsers.QuickTshProxyDbParser(databaseSuggester)
- );
- this.quickCommandParser.registerParserForCommand(
- 'tsh install',
- new parsers.QuickNoArgumentsParser({ kind: 'command.tsh-install' })
- );
- this.quickCommandParser.registerParserForCommand(
- 'tsh uninstall',
- new parsers.QuickNoArgumentsParser({ kind: 'command.tsh-uninstall' })
- );
- }
-
- state: State = {
- inputValue: '',
- visible: false,
- };
-
- // TODO: There's no "back" in the new command bar. We can probably just remove this method and the
- // behavior related to it?
- goBack = () => {
- this.setState({
- inputValue: '',
- visible: false,
- });
-
- const el = this.lastFocused.deref();
- el?.focus();
- };
-
- show = () => {
- this.setState({
- visible: true,
- });
- };
-
- hide = () => {
- this.setState({
- visible: false,
- });
- };
-
- // Parses the input string and returns AutocompleteResult which includes information about which
- // token we currently show the autocomplete for and what are the autocomplete suggestions (items)
- // to show.
- parse(input: string): ParseResult {
- const parseResult = this.quickCommandParser.parse(input);
-
- // Automatically handle a universal edge case so that each individual suggester doesn't have to
- // care about it.
- const getSuggestionsThenHandleEdgeCase = () =>
- parseResult.getSuggestions().then(suggestions => {
- // Don't show suggestions if the only suggestion completely matches the target token.
- const hasSingleCompleteMatch =
- suggestions.length === 1 &&
- suggestions[0].token === parseResult.targetToken.value;
-
- return hasSingleCompleteMatch ? [] : suggestions;
- });
-
- return {
- ...parseResult,
- getSuggestions: getSuggestionsThenHandleEdgeCase,
- };
- }
-
- // Replaces the token that is being autocompleted with the token from the suggestion.
- // `tsh ssh roo` becomes `tsh ssh root`
- //
- // However, we also preserve anything after the token so that in the future we might effortlessly
- // support cursor index. So `tsh ssh roo --cluster=bar` becomes `tsh ssh root --cluster=bar`.
- pickSuggestion(targetToken: AutocompleteToken, suggestion: Suggestion) {
- const { inputValue } = this.state;
- const insertedToken = suggestion.token + (suggestion.appendToToken || '');
- const newInputValue =
- inputValue.substring(0, targetToken.startIndex) +
- insertedToken +
- inputValue.substring(targetToken.startIndex + targetToken.value.length);
-
- // Keep the autocomplete visible if something was appended to the token. If nothing was appended
- // to the token then we know that we don't have any further suggestions to show.
- //
- // Consider these situations:
- //
- // 1. You type "tsh s" and choose "tsh ssh" from suggestions. The input becomes "tsh ssh " and
- // you see the autocomplete for the SSH login.
- //
- // 2. You type "tsh ssh roo" and choose "root" from suggestions. The input becomes "tsh ssh
- // root@" and you see the autocomplete for the SSH host.
- //
- // 3. You type "tsh ssh root@foo" and choose "foobar" from suggestions. The input becomes "tsh
- // ssh root@foobar". You don't see any further suggestions.
- //
- // In situation 3, it's crucial that we hide the autocomplete, as there might be other servers
- // that match "foobar", but the user already made a conscious choice of picking a specific
- // server.
- const shouldRemainVisible = !!suggestion.appendToToken;
-
- this.setState({
- inputValue: newInputValue,
- visible: shouldRemainVisible,
- });
- }
-
- getInputValue = () => {
- return this.state.inputValue;
- };
-
- setInputValue = (value: string) => {
- this.setState({
- inputValue: value,
- // Changing the input through the UI should always make the autocomplete box visible in case
- // there are any suggestions.
- visible: true,
- });
- };
-
- clearInputValueAndHide = () => {
- this.setState({
- inputValue: '',
- visible: false,
- });
- };
-
- useState() {
- return useStore(this).state;
- }
-}
diff --git a/web/packages/teleterm/src/ui/services/quickInput/suggesters.ts b/web/packages/teleterm/src/ui/services/quickInput/suggesters.ts
deleted file mode 100644
index 43ab423bc89fb..0000000000000
--- a/web/packages/teleterm/src/ui/services/quickInput/suggesters.ts
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
-Copyright 2019 Gravitational, Inc.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { WorkspacesService } from 'teleterm/ui/services/workspacesService';
-import { ResourcesService } from 'teleterm/ui/services/resources';
-import { ClustersService } from 'teleterm/ui/services/clusters';
-
-import {
- SuggestionServer,
- SuggestionSshLogin,
- SuggestionDatabase,
- QuickInputSuggester,
-} from './types';
-
-const limit = 10;
-
-export class QuickSshLoginSuggester
- implements QuickInputSuggester
-{
- constructor(
- private workspacesService: WorkspacesService,
- private clustersService: ClustersService
- ) {}
-
- async getSuggestions(input: string): Promise {
- // TODO(ravicious): Handle the `--cluster` tsh ssh flag.
- const localClusterUri =
- this.workspacesService.getActiveWorkspace()?.localClusterUri;
- if (!localClusterUri) {
- return [];
- }
- const cluster = this.clustersService.findCluster(localClusterUri);
- const allLogins = cluster?.loggedInUser?.sshLoginsList || [];
- let matchingLogins: typeof allLogins;
-
- if (!input) {
- matchingLogins = allLogins;
- } else {
- matchingLogins = allLogins.filter(login =>
- login.startsWith(input.toLowerCase())
- );
- }
-
- return matchingLogins.map(login => ({
- kind: 'suggestion.ssh-login' as const,
- token: login,
- // This allows the user to immediately begin typing the hostname.
- appendToToken: '@',
- data: login,
- }));
- }
-}
-
-export class QuickServerSuggester
- implements QuickInputSuggester
-{
- constructor(
- private workspacesService: WorkspacesService,
- private resourcesService: ResourcesService
- ) {}
-
- async getSuggestions(input: string): Promise {
- // TODO(ravicious): Handle the `--cluster` tsh ssh flag.
- const localClusterUri =
- this.workspacesService.getActiveWorkspace()?.localClusterUri;
- if (!localClusterUri) {
- return [];
- }
- const servers = await this.resourcesService.fetchServers({
- clusterUri: localClusterUri,
- search: input,
- limit,
- sort: { fieldName: 'hostname', dir: 'ASC' },
- });
-
- return servers.agentsList.map(server => ({
- kind: 'suggestion.server' as const,
- token: server.hostname,
- data: server,
- }));
- }
-}
-
-export class QuickDatabaseSuggester
- implements QuickInputSuggester
-{
- constructor(
- private workspacesService: WorkspacesService,
- private resourcesService: ResourcesService
- ) {}
-
- async getSuggestions(input: string): Promise {
- const localClusterUri =
- this.workspacesService.getActiveWorkspace()?.localClusterUri;
- if (!localClusterUri) {
- return [];
- }
- const databases = await this.resourcesService.fetchDatabases({
- clusterUri: localClusterUri,
- search: input,
- limit,
- sort: { fieldName: 'name', dir: 'ASC' },
- });
-
- return databases.agentsList.map(database => ({
- kind: 'suggestion.database' as const,
- token: database.name,
- data: database,
- }));
- }
-}
diff --git a/web/packages/teleterm/src/ui/services/quickInput/types.ts b/web/packages/teleterm/src/ui/services/quickInput/types.ts
deleted file mode 100644
index df85a1042e43b..0000000000000
--- a/web/packages/teleterm/src/ui/services/quickInput/types.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-/**
- * Copyright 2023 Gravitational, Inc
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import type * as tsh from 'teleterm/services/tshd/types';
-
-type SuggestionBase = {
- kind: T;
- token: string;
- appendToToken?: string;
- data: R;
-};
-
-export type SuggestionCmd = SuggestionBase<
- 'suggestion.cmd',
- { displayName: string; description: string }
->;
-
-export type SuggestionSshLogin = SuggestionBase<
- 'suggestion.ssh-login',
- string
-> & { appendToToken: string };
-
-export type SuggestionServer = SuggestionBase<'suggestion.server', tsh.Server>;
-
-export type SuggestionDatabase = SuggestionBase<
- 'suggestion.database',
- tsh.Database
->;
-
-export type Suggestion =
- | SuggestionCmd
- | SuggestionSshLogin
- | SuggestionServer
- | SuggestionDatabase;
-
-export type QuickInputParser = {
- parse(input: string, startIndex: number): ParseResult;
-};
-
-export type ParseResult = {
- // Command includes the result of parsing whatever was parsed so far.
- // This means that in case of `tsh ssh roo`, the command will say that we want to launch `tsh ssh`
- // with `roo` as `loginHost`.
- command: AutocompleteCommand;
- readonly targetToken: AutocompleteToken;
- getSuggestions(): Promise;
-};
-
-export type QuickInputSuggester = {
- getSuggestions(filter: string): Promise;
-};
-
-export type AutocompleteToken = {
- value: string;
- startIndex: number;
-};
-
-type CommandBase = {
- kind: T;
-};
-
-export type AutocompleteUnknownCommand = CommandBase<'command.unknown'>;
-
-export type AutocompleteTshSshCommand = CommandBase<'command.tsh-ssh'> & {
- loginHost: string;
-};
-
-export type AutocompleteTshInstallCommand = CommandBase<'command.tsh-install'>;
-export type AutocompleteTshUninstallCommand =
- CommandBase<'command.tsh-uninstall'>;
-
-export type AutocompleteCommand =
- | AutocompleteUnknownCommand
- | AutocompleteTshSshCommand
- | AutocompleteTshInstallCommand
- | AutocompleteTshUninstallCommand;
diff --git a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.ts b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.ts
index ab809037df5f3..7c96be9216212 100644
--- a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.ts
+++ b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.ts
@@ -15,13 +15,7 @@ limitations under the License.
*/
import { unique } from 'teleterm/ui/utils/uid';
-import {
- ClusterUri,
- DocumentUri,
- ServerUri,
- paths,
- routing,
-} from 'teleterm/ui/uri';
+import { DocumentUri, ServerUri, paths, routing } from 'teleterm/ui/uri';
import {
CreateAccessRequestDocumentOpts,
@@ -36,7 +30,6 @@ import {
DocumentOrigin,
DocumentTshKube,
DocumentTshNode,
- DocumentTshNodeWithLoginHost,
DocumentTshNodeWithServerId,
} from './types';
@@ -130,37 +123,6 @@ export class DocumentsService {
};
}
- /**
- * createTshNodeDocumentFromLoginHost handles creation of the doc when the server URI is not
- * available, for example when executing `tsh ssh user@host` from the command bar.
- *
- * @param clusterUri - the URI of the cluster which should be used for hostname lookup. That is,
- * the command will succeed only if the given cluster has only a single server with the hostname
- * matching `host`.
- * @param loginHost - the "user@host" pair.
- * @param params - additional parameters.
- * @param params.origin - where the document was opened from.
- */
- createTshNodeDocumentFromLoginHost(
- clusterUri: ClusterUri,
- loginHost: string,
- params: { origin: DocumentOrigin }
- ): DocumentTshNodeWithLoginHost {
- const { params: routingParams } = routing.parseClusterUri(clusterUri);
- const uri = routing.getDocUri({ docId: unique() });
-
- return {
- uri,
- kind: 'doc.terminal_tsh_node',
- title: loginHost,
- status: 'connecting',
- rootClusterId: routingParams.rootClusterId,
- leafClusterId: routingParams.leafClusterId,
- loginHost,
- origin: params.origin,
- };
- }
-
/**
* If title is not present in opts, createGatewayDocument will create one based on opts.
*/
diff --git a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts
index a28ed57ba62e3..f11062b034e20 100644
--- a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts
+++ b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts
@@ -43,6 +43,12 @@ export interface DocumentBlank extends DocumentBase {
export type DocumentTshNode =
| DocumentTshNodeWithServerId
+ // DELETE IN 14.0.0
+ //
+ // Logging in to an arbitrary host was removed in 13.0 together with the command bar.
+ // However, there's a slight chance that some users upgrading from 12.x to 13.0 still have
+ // documents with loginHost in the app state (e.g. if the doc failed to connect to the server).
+ // Let's just remove this in 14.0.0 instead to make sure those users can safely upgrade the app.
| DocumentTshNodeWithLoginHost;
interface DocumentTshNodeBase extends DocumentBase {
diff --git a/web/packages/teleterm/src/ui/types.ts b/web/packages/teleterm/src/ui/types.ts
index f1bac8b6f9ab1..7a43982074585 100644
--- a/web/packages/teleterm/src/ui/types.ts
+++ b/web/packages/teleterm/src/ui/types.ts
@@ -18,7 +18,6 @@ import { MainProcessClient, SubscribeToTshdEvent } from 'teleterm/types';
import { ClustersService } from 'teleterm/ui/services/clusters';
import { ModalsService } from 'teleterm/ui/services/modals';
import { TerminalsService } from 'teleterm/ui/services/terminals';
-import { QuickInputService } from 'teleterm/ui/services/quickInput';
import { StatePersistenceService } from 'teleterm/ui/services/statePersistence';
import { CommandLauncher } from 'teleterm/ui/commandLauncher';
import { KeyboardShortcutsService } from 'teleterm/ui/services/keyboardShortcuts';
@@ -38,7 +37,6 @@ export interface IAppContext {
notificationsService: NotificationsService;
terminalsService: TerminalsService;
keyboardShortcutsService: KeyboardShortcutsService;
- quickInputService: QuickInputService;
statePersistenceService: StatePersistenceService;
workspacesService: WorkspacesService;
mainProcessClient: MainProcessClient;