diff --git a/web/packages/teleport/src/Apps/AddApp/useAddApp.ts b/web/packages/teleport/src/Apps/AddApp/useAddApp.ts index 9fda112960251..94143f9c5662c 100644 --- a/web/packages/teleport/src/Apps/AddApp/useAddApp.ts +++ b/web/packages/teleport/src/Apps/AddApp/useAddApp.ts @@ -36,7 +36,7 @@ export default function useAddApp(ctx: TeleportContext) { function createToken() { return run(() => - ctx.joinTokenService.fetchJoinToken(['App']).then(setToken) + ctx.joinTokenService.fetchJoinToken({ roles: ['App'] }).then(setToken) ); } diff --git a/web/packages/teleport/src/Databases/AddDatabase/useAddDatabase.ts b/web/packages/teleport/src/Databases/AddDatabase/useAddDatabase.ts index 57bf974c9463c..12fc154f733c5 100644 --- a/web/packages/teleport/src/Databases/AddDatabase/useAddDatabase.ts +++ b/web/packages/teleport/src/Databases/AddDatabase/useAddDatabase.ts @@ -31,7 +31,7 @@ export default function useAddDatabase(ctx: TeleportContext) { function createJoinToken() { return run(() => - ctx.joinTokenService.fetchJoinToken(['Db']).then(setToken) + ctx.joinTokenService.fetchJoinToken({ roles: ['Db'] }).then(setToken) ); } diff --git a/web/packages/teleport/src/Discover/Kubernetes/DownloadScript/DownloadScript.story.tsx b/web/packages/teleport/src/Discover/Database/DownloadScript/DownloadScript.story.tsx similarity index 89% rename from web/packages/teleport/src/Discover/Kubernetes/DownloadScript/DownloadScript.story.tsx rename to web/packages/teleport/src/Discover/Database/DownloadScript/DownloadScript.story.tsx index aa88d290e86bf..2ccf7157bf69b 100644 --- a/web/packages/teleport/src/Discover/Kubernetes/DownloadScript/DownloadScript.story.tsx +++ b/web/packages/teleport/src/Discover/Database/DownloadScript/DownloadScript.story.tsx @@ -29,20 +29,29 @@ import { userContext } from 'teleport/mocks/contexts'; import DownloadScript from './DownloadScript'; +const { worker, rest } = window.msw; + export default { - title: 'Teleport/Discover/Kube/DownloadScripts', + title: 'Teleport/Discover/Database/DownloadScript', decorators: [ Story => { // Reset request handlers added in individual stories. - window.msw.worker.resetHandlers(); + worker.resetHandlers(); clearCachedJoinTokenResult(); return ; }, ], }; +export const Init = () => { + return ( + + + + ); +}; + export const Polling = () => { - const { worker, rest } = window.msw; // Use default fetch token handler defined in mocks/handlers worker.use( @@ -51,14 +60,13 @@ export const Polling = () => { }) ); return ( - + ); }; export const PollingSuccess = () => { - const { worker, rest } = window.msw; // Use default fetch token handler defined in mocks/handlers worker.use( @@ -67,14 +75,13 @@ export const PollingSuccess = () => { }) ); return ( - + ); }; export const PollingError = () => { - const { worker, rest } = window.msw; // Use default fetch token handler defined in mocks/handlers worker.use( @@ -83,27 +90,25 @@ export const PollingError = () => { }) ); return ( - + ); }; export const Processing = () => { - const { worker, rest } = window.msw; worker.use( rest.post(cfg.api.joinTokenPath, (req, res, ctx) => { return res(ctx.delay('infinite')); }) ); return ( - + ); }; export const Failed = () => { - const { worker, rest } = window.msw; worker.use( rest.post(cfg.api.joinTokenPath, (req, res, ctx) => { return res.once(ctx.status(500)); @@ -125,7 +130,7 @@ const Provider = props => { {props.children} diff --git a/web/packages/teleport/src/Discover/Database/DownloadScript/DownloadScript.tsx b/web/packages/teleport/src/Discover/Database/DownloadScript/DownloadScript.tsx new file mode 100644 index 0000000000000..34e2313a38433 --- /dev/null +++ b/web/packages/teleport/src/Discover/Database/DownloadScript/DownloadScript.tsx @@ -0,0 +1,373 @@ +/** + * Copyright 2022 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, { useState, Suspense } from 'react'; +import { + Text, + Box, + ButtonSecondary, + Flex, + ButtonIcon, + ButtonText, +} from 'design'; +import * as Icons from 'design/Icon'; +import FieldInput from 'shared/components/FieldInput'; +import Validation, { + useValidation, + Validator, +} from 'shared/components/Validation'; +import { requiredField } from 'shared/components/Validation/rules'; + +import { CatchError } from 'teleport/components/CatchError'; +import { + useJoinToken, + clearCachedJoinTokenResult, +} from 'teleport/Discover/Shared/JoinTokenContext'; +import { usePingTeleport } from 'teleport/Discover/Shared/PingTeleportContext'; +import { CommandWithTimer } from 'teleport/Discover/Shared/CommandWithTimer'; +import { AgentLabel } from 'teleport/services/agents'; +import cfg from 'teleport/config'; +import { Database } from 'teleport/services/databases'; + +import { + ActionButtons, + Header, + HeaderSubtitle, + ResourceKind, + TextIcon, +} from '../../Shared'; + +import type { AgentStepProps } from '../../types'; +import type { Poll } from 'teleport/Discover/Shared/CommandWithTimer'; + +export default function Container( + props: AgentStepProps & { runJoinTokenPromise?: boolean } +) { + const [showScript, setShowScript] = useState(props.runJoinTokenPromise); + const [labels, setLabels] = useState([ + { name: '*', value: '*' }, + ]); + + function handleGenerateCommand(validator: Validator) { + if (!validator.validate()) { + return; + } + + setShowScript(true); + } + + return ( + + {({ validator }) => ( + ( + + + + + Generate Command + + + + + Encountered Error: {props.error.message} + + + null} disableProceed={true} /> + + )} + > + + + + + Generate Command + + null} disableProceed={true} /> + + } + > + {!showScript && ( + + + + handleGenerateCommand(validator)} + > + Generate Command + + null} disableProceed={true} /> + + )} + {showScript && ( + + )} + + + )} + + ); +} + +export function DownloadScript( + props: AgentStepProps & { + labels: AgentLabel[]; + setLabels(l: AgentLabel[]): void; + } +) { + // Fetches join token. + const { joinToken, reloadJoinToken, timeout } = useJoinToken( + ResourceKind.Database, + props.labels + ); + + // Starts resource querying interval. + const { + timedOut: pollingTimedOut, + start, + result, + } = usePingTeleport(); + + function regenerateScriptAndRepoll() { + reloadJoinToken(); + start(); + } + + function handleNextStep() { + props.updateAgentMeta({ + ...props.agentMeta, + resourceName: result.name, + db: result, + }); + } + + let poll: Poll = { state: 'polling' }; + if (pollingTimedOut) { + poll = { + state: 'error', + error: { + reasonContents: [ + <> + The command was not run on the server you were trying to add, + regenerate command and try again. + , + // TODO (lisa): not sure what this message should be. + // <> + // The Teleport Service could not join this Teleport cluster. Check the + // logs for errors by running
+ // kubectl logs -l app=teleport-agent -n {namespace} + // , + ], + }, + }; + } else if (result) { + poll = { state: 'success' }; + } + + return ( + + + + + Regenerate Command + + + + + ); +} + +const Heading = () => { + return ( + <> +
Deploy a Database Service
+ TODO lorem ipsum dolores + + ); +}; + +export const Labels = ({ + labels, + setLabels, + disableAddBtn = false, +}: { + labels: AgentLabel[]; + setLabels(l: AgentLabel[]): void; + disableAddBtn?: boolean; +}) => { + const validator = useValidation() as Validator; + + function addLabel() { + // Prevent adding more rows if there are + // empty input fields. After checking, + // reset the validator so the newly + // added empty input boxes are not + // considered an error. + if (!validator.validate()) { + return; + } + validator.reset(); + setLabels([...labels, { name: '', value: '' }]); + } + + function removeLabel(index: number) { + if (labels.length === 1) { + // Since at least one label is required + // instead of removing the row, clear + // the input and turn on error. + const newList = [...labels]; + newList[index] = { name: '', value: '' }; + setLabels(newList); + + validator.validate(); + return; + } + const newList = [...labels]; + newList.splice(index, 1); + setLabels(newList); + } + + const handleChange = ( + event: React.ChangeEvent, + index: number, + labelField: keyof AgentLabel + ) => { + const { value } = event.target; + const newList = [...labels]; + newList[index] = { ...newList[index], [labelField]: value }; + setLabels(newList); + }; + + return ( + + Labels + + At least one label is required to help this service identify your + database. + + + + Key{' '} + + (required field) + + + + Value{' '} + + (required field) + + + + + {labels.map((label, index) => { + return ( + + + handleChange(e, index, 'name')} + /> + handleChange(e, index, 'value')} + /> + removeLabel(index)} + > + + + + + ); + })} + + + + Add New Label + + + ); +}; + +function createBashCommand(tokenId: string) { + return `sudo bash -c "$(curl -fsSL ${cfg.getDbScriptUrl(tokenId)})"`; +} diff --git a/web/packages/teleport/src/Discover/Database/DownloadScript/index.ts b/web/packages/teleport/src/Discover/Database/DownloadScript/index.ts new file mode 100644 index 0000000000000..f8066fcc9c8d2 --- /dev/null +++ b/web/packages/teleport/src/Discover/Database/DownloadScript/index.ts @@ -0,0 +1,17 @@ +/** + * Copyright 2022 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 { DownloadScript } from './DownloadScript'; diff --git a/web/packages/teleport/src/Discover/Kubernetes/HelmChart/HelmChart.story.tsx b/web/packages/teleport/src/Discover/Kubernetes/HelmChart/HelmChart.story.tsx new file mode 100644 index 0000000000000..00b101f6c9294 --- /dev/null +++ b/web/packages/teleport/src/Discover/Kubernetes/HelmChart/HelmChart.story.tsx @@ -0,0 +1,152 @@ +/** + * Copyright 2022 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 { MemoryRouter } from 'react-router'; + +import { ContextProvider, Context as TeleportContext } from 'teleport'; +import cfg from 'teleport/config'; +import { ResourceKind } from 'teleport/Discover/Shared'; +import { + clearCachedJoinTokenResult, + JoinTokenProvider, +} from 'teleport/Discover/Shared/JoinTokenContext'; +import { PingTeleportProvider } from 'teleport/Discover/Shared/PingTeleportContext'; +import { userContext } from 'teleport/mocks/contexts'; + +import HelmChart from './HelmChart'; + +const { worker, rest } = window.msw; + +export default { + title: 'Teleport/Discover/Kube/HelmChart', + decorators: [ + Story => { + // Reset request handlers added in individual stories. + worker.resetHandlers(); + clearCachedJoinTokenResult(); + return ; + }, + ], +}; + +export const Init = () => { + return ( + + + + ); +}; + +export const Polling = () => { + // Use default fetch token handler defined in mocks/handlers + + worker.use( + rest.get(cfg.api.kubernetesPath, (req, res, ctx) => { + return res(ctx.delay('infinite')); + }) + ); + return ( + + + + ); +}; + +export const PollingSuccess = () => { + // Use default fetch token handler defined in mocks/handlers + + worker.use( + rest.get(cfg.api.kubernetesPath, (req, res, ctx) => { + return res(ctx.json({ items: [{}] })); + }) + ); + return ( + + + + ); +}; + +export const PollingError = () => { + // Use default fetch token handler defined in mocks/handlers + + worker.use( + rest.get(cfg.api.kubernetesPath, (req, res, ctx) => { + return res(ctx.delay('infinite')); + }) + ); + return ( + + + + ); +}; + +export const Processing = () => { + worker.use( + rest.post(cfg.api.joinTokenPath, (req, res, ctx) => { + return res(ctx.delay('infinite')); + }) + ); + return ( + + + + ); +}; + +export const Failed = () => { + worker.use( + rest.post(cfg.api.joinTokenPath, (req, res, ctx) => { + return res.once(ctx.status(500)); + }) + ); + return ( + + + + ); +}; + +const Provider = props => { + const ctx = createTeleportContext(); + + return ( + + + + + {props.children} + + + + + ); +}; + +function createTeleportContext() { + const ctx = new TeleportContext(); + + ctx.isEnterprise = false; + ctx.storeUser.setState(userContext); + + return ctx; +} diff --git a/web/packages/teleport/src/Discover/Kubernetes/DownloadScript/DownloadScript.tsx b/web/packages/teleport/src/Discover/Kubernetes/HelmChart/HelmChart.tsx similarity index 64% rename from web/packages/teleport/src/Discover/Kubernetes/DownloadScript/DownloadScript.tsx rename to web/packages/teleport/src/Discover/Kubernetes/HelmChart/HelmChart.tsx index 39c1a19aa49fe..3dab0dfb51950 100644 --- a/web/packages/teleport/src/Discover/Kubernetes/DownloadScript/DownloadScript.tsx +++ b/web/packages/teleport/src/Discover/Kubernetes/HelmChart/HelmChart.tsx @@ -28,10 +28,9 @@ import { useJoinToken, clearCachedJoinTokenResult, } from 'teleport/Discover/Shared/JoinTokenContext'; -import { Timeout } from 'teleport/Discover/Shared/Timeout'; import useTeleport from 'teleport/useTeleport'; import { usePingTeleport } from 'teleport/Discover/Shared/PingTeleportContext'; -import { PollBox, PollState } from 'teleport/Discover/Shared/PollState'; +import { CommandWithTimer } from 'teleport/Discover/Shared/CommandWithTimer'; import { ActionButtons, @@ -46,12 +45,14 @@ import type { AgentStepProps } from '../../types'; import type { JoinToken } from 'teleport/services/joinToken'; import type { AgentMeta, KubeMeta } from 'teleport/Discover/useDiscover'; import type { Kube } from 'teleport/services/kube'; +import type { Poll } from 'teleport/Discover/Shared/CommandWithTimer'; export default function Container( props: AgentStepProps & { runJoinTokenPromise?: boolean } ) { const [namespace, setNamespace] = useState(''); const [clusterName, setClusterName] = useState(''); + const [showHelmChart, setShowHelmChart] = useState(props.runJoinTokenPromise); return ( // This outer CatchError and Suspense handles @@ -89,91 +90,66 @@ export default function Container( } > - + {!showHelmChart && ( + + + + setShowHelmChart(true)} + namespace={namespace} + setNamespace={setNamespace} + clusterName={clusterName} + setClusterName={setClusterName} + /> + null} disableProceed={true} /> + + )} + {showHelmChart && ( + + )} ); } -export function DownloadScript( +export function HelmChart( props: AgentStepProps & { namespace: string; setNamespace(n: string): void; clusterName: string; setClusterName(c: string): void; - // runJoinTokenPromise is used only for development - // (eg. stories) to bypass user input requirement. - runJoinTokenPromise?: boolean; } ) { - const { runJoinTokenPromise = false } = props; - // runPromise is a flag that when false prevents the `useJoinToken` hook - // from running on the first run. If we let this run in the background on first run, - // and it returns an error, the error is caught by the CatchError - // component which will interrupt the user (rendering the failed state) - // without the user clicking on any buttons. After the user fills out required fields - // and clicks on `handleSubmit`, this flag will always be true. - const [runPromise, setRunPromise] = useState(runJoinTokenPromise); - const [showHelmChart, setShowHelmChart] = useState(false); - const { - joinToken, - reloadJoinToken: fetchJoinToken, - timeout, - } = useJoinToken(ResourceKind.Kubernetes, runPromise); - - function handleSubmit(validator: Validator) { - if (!validator.validate()) { - return; - } - setRunPromise(true); - } - - function reloadJoinToken(validator: Validator) { - if (!validator.validate()) { - return; - } - fetchJoinToken(); - - // Hide chart until we finished fetching a new join token. - setShowHelmChart(false); - } - - React.useEffect(() => { - if (joinToken) { - setShowHelmChart(true); - } - }, [joinToken]); + const { joinToken, reloadJoinToken, timeout } = useJoinToken( + ResourceKind.Kubernetes + ); return ( (joinToken ? reloadJoinToken(v) : handleSubmit(v))} + generateScript={reloadJoinToken} namespace={props.namespace} setNamespace={props.setNamespace} clusterName={props.clusterName} setClusterName={props.setClusterName} hasJoinToken={!!joinToken} /> - {showHelmChart ? ( - - ) : ( - null} disableProceed={true} /> - )} + ); } @@ -218,7 +194,6 @@ const StepOne = () => { }; const StepTwo = ({ - handleSubmit, namespace, setNamespace, clusterName, @@ -226,16 +201,23 @@ const StepTwo = ({ hasJoinToken, error, onRetry, + generateScript, }: { error?: Error; onRetry?(): void; - handleSubmit?(v: Validator): void; + generateScript?(): void; namespace: string; setNamespace(n: string): void; clusterName: string; setClusterName(c: string): void; hasJoinToken?: boolean; }) => { + function handleSubmit(validator: Validator) { + if (!validator.validate()) { + return; + } + generateScript(); + } return ( Step 2 @@ -273,7 +255,7 @@ const StepTwo = ({ width="200px" type="submit" // Let user re-try on error - disabled={!error && !handleSubmit} + disabled={!error && !generateScript} onClick={() => (error ? onRetry() : handleSubmit(validator))} > {hasJoinToken ? 'Regenerate Command' : 'Generate Command'} @@ -338,11 +320,26 @@ const InstallHelmChart = ({ // Starts resource querying interval. const { timedOut: pollingTimedOut, result } = usePingTeleport(); - let pollState: PollState = 'polling'; + let poll: Poll = { state: 'polling' }; if (pollingTimedOut) { - pollState = 'error'; + poll = { + state: 'error', + error: { + reasonContents: [ + <> + The command was not run on the server you were trying to add, + regenerate command and try again. + , + <> + The Teleport Service could not join this Teleport cluster. Check the + logs for errors by running
+ kubectl logs -l app=teleport-agent -n {namespace} + , + ], + }, + }; } else if (result) { - pollState = 'success'; + poll = { state: 'success' }; } function handleOnProceed() { @@ -356,88 +353,36 @@ const InstallHelmChart = ({ return ( <> - - Step 3 - - Run the command below on the server running your Kubernetes cluster. - May take up to a minute for the Teleport Service to join after running - the command. - - - - - {pollState === 'polling' && ( - - - - - )} - {pollState === 'success' && ( - - - The Teleport Service successfully join this Teleport cluster - - )} - {pollState === 'error' && } - + + Step 3 + + Run the command below on the server running your Kubernetes + cluster. May take up to a minute for the Teleport Service to join + after running the command. + + + } + /> ); }; -const TimeoutError = ({ namespace }: { namespace: string }) => { - return ( - - - - We could not detect the Teleport Service you were trying to add - - - Possible reasons - -
    -
  • - The command was not run on the server you were trying to add, - regenerate command and try again. -
  • -
  • - The Teleport Service could not join this Teleport cluster. Check the - logs for errors by running
    - kubectl logs -l app=teleport-agent -n {namespace} -
  • -
-
- ); -}; - const StyledBox = styled(Box)` max-width: 800px; background-color: rgba(255, 255, 255, 0.05); diff --git a/web/packages/teleport/src/Discover/Kubernetes/DownloadScript/index.ts b/web/packages/teleport/src/Discover/Kubernetes/HelmChart/index.ts similarity index 89% rename from web/packages/teleport/src/Discover/Kubernetes/DownloadScript/index.ts rename to web/packages/teleport/src/Discover/Kubernetes/HelmChart/index.ts index d1a2634637f28..7aec926ba123d 100644 --- a/web/packages/teleport/src/Discover/Kubernetes/DownloadScript/index.ts +++ b/web/packages/teleport/src/Discover/Kubernetes/HelmChart/index.ts @@ -14,6 +14,6 @@ * limitations under the License. */ -import DownloadScript from './DownloadScript'; +import HelmChart from './HelmChart'; -export { DownloadScript }; +export { HelmChart }; diff --git a/web/packages/teleport/src/Discover/Kubernetes/index.tsx b/web/packages/teleport/src/Discover/Kubernetes/index.tsx index 92ab4e57d7842..4596ad6d8a301 100644 --- a/web/packages/teleport/src/Discover/Kubernetes/index.tsx +++ b/web/packages/teleport/src/Discover/Kubernetes/index.tsx @@ -22,8 +22,8 @@ import { Finished, ResourceKind } from 'teleport/Discover/Shared'; import { Resource } from 'teleport/Discover/flow'; import { KubeWrapper } from './KubeWrapper'; -import { DownloadScript } from './DownloadScript'; import { SetupAccess } from './SetupAccess'; +import { HelmChart } from './HelmChart'; import { TestConnection } from './TestConnection'; export const KubernetesResource: Resource = { @@ -42,7 +42,7 @@ export const KubernetesResource: Resource = { }, { title: 'Configure Resource', - component: DownloadScript, + component: HelmChart, }, { title: 'Set Up Access', diff --git a/web/packages/teleport/src/Discover/Server/DownloadScript/DownloadScript.story.test.tsx b/web/packages/teleport/src/Discover/Server/DownloadScript/DownloadScript.story.test.tsx deleted file mode 100644 index f77a56c5f0d8c..0000000000000 --- a/web/packages/teleport/src/Discover/Server/DownloadScript/DownloadScript.story.test.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright 2022 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 { render } from 'design/utils/testing'; - -import { - Polling, - PollingSuccess, - PollingError, - Failed, -} from './DownloadScript.story'; - -test('polling state', () => { - const { container } = render(); - expect(container.firstChild).toMatchSnapshot(); -}); - -test('polling success state', () => { - const { container } = render(); - expect(container.firstChild).toMatchSnapshot(); -}); - -test('polling error state', () => { - const { container } = render(); - expect(container.firstChild).toMatchSnapshot(); -}); - -test('failed', () => { - const { container } = render(); - expect(container.firstChild).toMatchSnapshot(); -}); diff --git a/web/packages/teleport/src/Discover/Server/DownloadScript/DownloadScript.story.tsx b/web/packages/teleport/src/Discover/Server/DownloadScript/DownloadScript.story.tsx index d248d95cf0a8c..2662822bec1d1 100644 --- a/web/packages/teleport/src/Discover/Server/DownloadScript/DownloadScript.story.tsx +++ b/web/packages/teleport/src/Discover/Server/DownloadScript/DownloadScript.story.tsx @@ -15,61 +15,130 @@ */ import React from 'react'; - import { MemoryRouter } from 'react-router'; -import { DownloadScript } from './DownloadScript'; -import { State } from './useDownloadScript'; +import { ContextProvider, Context as TeleportContext } from 'teleport'; +import cfg from 'teleport/config'; +import { + clearCachedJoinTokenResult, + JoinTokenProvider, +} from 'teleport/Discover/Shared/JoinTokenContext'; +import { PingTeleportProvider } from 'teleport/Discover/Shared/PingTeleportContext'; +import { userContext } from 'teleport/Main/fixtures'; +import { ResourceKind } from 'teleport/Discover/Shared'; + +import DownloadScript from './DownloadScript'; + +const { worker, rest } = window.msw; export default { - title: 'Teleport/Discover/Server/DownloadScript', + title: 'Teleport/Discover/Server/DownloadScripts', + decorators: [ + Story => { + // Reset request handlers added in individual stories. + worker.resetHandlers(); + clearCachedJoinTokenResult(); + return ; + }, + ], +}; + +export const Polling = () => { + // Use default fetch token handler defined in mocks/handlers + + worker.use( + rest.get(cfg.api.nodesPath, (req, res, ctx) => { + return res(ctx.delay('infinite')); + }) + ); + return ( + + + + ); +}; + +export const PollingSuccess = () => { + // Use default fetch token handler defined in mocks/handlers + + worker.use( + rest.get(cfg.api.nodesPath, (req, res, ctx) => { + return res(ctx.json({ items: [{}] })); + }) + ); + return ( + + + + ); +}; + +export const PollingError = () => { + // Use default fetch token handler defined in mocks/handlers + + worker.use( + rest.get(cfg.api.nodesPath, (req, res, ctx) => { + return res(ctx.delay('infinite')); + }) + ); + return ( + + + + ); +}; + +export const Processing = () => { + worker.use( + rest.post(cfg.api.joinTokenPath, (req, res, ctx) => { + return res(ctx.delay('infinite')); + }) + ); + return ( + + + + ); +}; + +export const Failed = () => { + worker.use( + rest.post(cfg.api.joinTokenPath, (req, res, ctx) => { + return res.once(ctx.status(500)); + }) + ); + return ( + + + + ); }; -export const Polling = () => ( - - - -); - -export const PollingSuccess = () => ( - - - -); - -export const PollingError = () => ( - - - -); - -export const Processing = () => ( - - - -); - -export const Failed = () => ( - - - -); - -const props: State = { - attempt: { - status: 'success', - statusText: '', - }, - pollState: 'polling', - nextStep: () => null, - joinToken: { - id: 'some-join-token-hash', - expiryText: '4 hours', - expiry: new Date(), - }, - regenerateScriptAndRepoll: () => null, - countdownTime: { minutes: 5, seconds: 0 }, +const Provider = props => { + const ctx = createTeleportContext(); + + return ( + + + + + {props.children} + + + + + ); }; + +function createTeleportContext() { + const ctx = new TeleportContext(); + + ctx.isEnterprise = false; + ctx.storeUser.setState(userContext); + + return ctx; +} diff --git a/web/packages/teleport/src/Discover/Server/DownloadScript/DownloadScript.tsx b/web/packages/teleport/src/Discover/Server/DownloadScript/DownloadScript.tsx index 0a513f8bae417..be288fcebda05 100644 --- a/web/packages/teleport/src/Discover/Server/DownloadScript/DownloadScript.tsx +++ b/web/packages/teleport/src/Discover/Server/DownloadScript/DownloadScript.tsx @@ -14,188 +14,185 @@ * limitations under the License. */ -import React from 'react'; -import styled from 'styled-components'; -import { Text, Box, Indicator } from 'design'; +import React, { Suspense } from 'react'; +import { Box, Indicator } from 'design'; import * as Icons from 'design/Icon'; import cfg from 'teleport/config'; -import TextSelectCopy from 'teleport/components/TextSelectCopy'; -import useTeleport from 'teleport/useTeleport'; +import { CatchError } from 'teleport/components/CatchError'; +import { + useJoinToken, + clearCachedJoinTokenResult, +} from 'teleport/Discover/Shared/JoinTokenContext'; +import { usePingTeleport } from 'teleport/Discover/Shared/PingTeleportContext'; +import { JoinToken } from 'teleport/services/joinToken'; +import { CommandWithTimer } from 'teleport/Discover/Shared/CommandWithTimer'; +import { + PollBox, + PollState, +} from 'teleport/Discover/Shared/CommandWithTimer/CommandWithTimer'; import { AgentStepProps } from '../../types'; - import { ActionButtons, ButtonBlueText, Header, HeaderSubtitle, Mark, + ResourceKind, TextIcon, } from '../../Shared'; -import { useDownloadScript } from './useDownloadScript'; - -import type { State, CountdownTime } from './useDownloadScript'; +import type { Node } from 'teleport/services/nodes'; +import type { Poll } from 'teleport/Discover/Shared/CommandWithTimer'; export default function Container(props: AgentStepProps) { - const ctx = useTeleport(); - const state = useDownloadScript({ ctx, props }); + return ( + ( + + )} + > + + + + } + > + + + + ); +} + +export function DownloadScript(props: AgentStepProps) { + // Fetches join token. + const { joinToken, reloadJoinToken, timeout } = useJoinToken( + ResourceKind.Server + ); + // Starts resource querying interval. + const { timedOut: pollingTimedOut, start, result } = usePingTeleport(); + + function regenerateScriptAndRepoll() { + reloadJoinToken(); + start(); + } - return ; + let poll: Poll = { + state: 'polling', + customStateDesc: 'Waiting for Teleport SSH Service', + }; + if (pollingTimedOut) { + poll = { + state: 'error', + error: { + customErrContent: ( + <> + We could not detect the server you were trying to add.{' '} + + Generate a new command + + + ), + reasonContents: [ + <>The command was not run on the server you were trying to add, + <> + The Teleport SSH Service could not join this Teleport cluster. Check + the logs for errors by running
+ journalctl status teleport + , + ], + }, + }; + } else if (result) { + poll = { + state: 'success', + customStateDesc: 'The server successfully joined this Teleport cluster', + }; + } + + function handleNextStep() { + props.updateAgentMeta({ + ...props.agentMeta, + // Node is an oddity in that the hostname is the more + // user identifiable resource name and what user expects + // as the resource name. + resourceName: result.hostname, + node: result, + }); + } + + return ( + <> +
Configure Resource
+ + Install and configure the Teleport SSH Service. +
+ Run the following command on the server you want to add. +
+ + + + ); } -export function DownloadScript({ - attempt, - joinToken, +const Template = ({ nextStep, pollState, - regenerateScriptAndRepoll, - countdownTime, -}: State) { + children, +}: { + nextStep(): void; + pollState?: PollState; + children: React.ReactNode; +}) => { return ( - + <>
Configure Resource
Install and configure the Teleport SSH Service.
Run the following command on the server you want to add.
- - Command - {attempt.status === 'processing' && ( - - - - )} - {attempt.status === 'failed' && ( - <> - - - Encountered Error: {attempt.statusText} - - - Refetch a command - - - )} - {attempt.status === 'success' && ( - <> - - {pollState === 'polling' && ( - - - {`Waiting for Teleport SSH Service | ${formatTime( - countdownTime - )}`} - - )} - {pollState === 'success' && ( - - - The server successfully joined this Teleport cluster - - )} - {pollState === 'error' && ( - - )} - - )} - + {children} -
+ ); -} +}; function createBashCommand(tokenId: string) { return `sudo bash -c "$(curl -fsSL ${cfg.getNodeScriptUrl(tokenId)})"`; } -function formatTime({ minutes, seconds }: CountdownTime) { - const formattedSeconds = String(seconds).padStart(2, '0'); - const formattedMinutes = String(minutes).padStart(2, '0'); - - let timeNotation = 'minute'; - if (!minutes && seconds >= 0) { - timeNotation = 'seconds'; - } - if (minutes) { - timeNotation = 'minutes'; - } - - return `${formattedMinutes}:${formattedSeconds} ${timeNotation}`; -} - -const ScriptBox = styled(Box)` - max-width: 800px; - background-color: rgba(255, 255, 255, 0.05); - border: 2px solid - ${props => { - switch (props.pollState) { - case 'error': - return props.theme.colors.danger; - case 'success': - return props.theme.colors.success; - default: - // polling - return '#2F3659'; - } - }}; -`; - -const TimeoutError = ({ - regenerateScriptAndRepoll, -}: { +export type State = { + joinToken: JoinToken; + nextStep(): void; regenerateScriptAndRepoll(): void; -}) => { - return ( - - - - We could not detect the server you were trying to add{' '} - - Generate a new command - - - - Possible reasons - -
    -
  • The command was not run on the server you were trying to add
  • -
  • - The Teleport SSH Service could not join this Teleport cluster. Check - the logs for errors by running
    - journalctl status teleport -
  • -
-
- ); + poll: Poll; + pollTimeout: number; }; diff --git a/web/packages/teleport/src/Discover/Server/DownloadScript/__snapshots__/DownloadScript.story.test.tsx.snap b/web/packages/teleport/src/Discover/Server/DownloadScript/__snapshots__/DownloadScript.story.test.tsx.snap deleted file mode 100644 index 46f6e74f8edd4..0000000000000 --- a/web/packages/teleport/src/Discover/Server/DownloadScript/__snapshots__/DownloadScript.story.test.tsx.snap +++ /dev/null @@ -1,1289 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`failed 1`] = ` -.c0 { - box-sizing: border-box; -} - -.c9 { - box-sizing: border-box; - margin-top: 24px; -} - -.c7 { - line-height: 1.5; - margin: 0; - display: inline-flex; - justify-content: center; - align-items: center; - box-sizing: border-box; - border: none; - border-radius: 4px; - cursor: pointer; - font-family: inherit; - font-weight: 600; - outline: none; - position: relative; - text-align: center; - text-decoration: none; - text-transform: uppercase; - transition: all 0.3s; - -webkit-font-smoothing: antialiased; - background: none; - text-transform: none; - color: rgba(255,255,255,0.87); - min-height: 32px; - font-size: 12px; - padding: 0px 24px; - margin-left: 8px; -} - -.c7:active { - opacity: 0.56; -} - -.c7:hover, -.c7:focus { - background: none; - text-decoration: underline; -} - -.c7:disabled { - background: none; - color: rgba(255,255,255,0.3); -} - -.c10 { - line-height: 1.5; - margin: 0; - display: inline-flex; - justify-content: center; - align-items: center; - box-sizing: border-box; - border: none; - border-radius: 4px; - cursor: pointer; - font-family: inherit; - font-weight: 600; - outline: none; - position: relative; - text-align: center; - text-decoration: none; - text-transform: uppercase; - transition: all 0.3s; - -webkit-font-smoothing: antialiased; - background: #512FC9; - color: rgba(255,255,255,0.87); - min-height: 32px; - font-size: 12px; - padding: 0px 24px; - margin-right: 16px; - width: 165px; -} - -.c10:active { - opacity: 0.56; -} - -.c10:hover, -.c10:focus { - background: #651FFF; -} - -.c10:active { - background: #354AA4; -} - -.c10:disabled { - background: rgba(255,255,255,0.12); - color: rgba(255,255,255,0.3); -} - -.c11 { - line-height: 1.5; - margin: 0; - display: inline-flex; - justify-content: center; - align-items: center; - box-sizing: border-box; - border: none; - border-radius: 4px; - cursor: pointer; - font-family: inherit; - font-weight: 600; - outline: none; - position: relative; - text-align: center; - text-decoration: none; - text-transform: uppercase; - transition: all 0.3s; - -webkit-font-smoothing: antialiased; - background: #222C59; - color: rgba(255,255,255,0.87); - min-height: 32px; - font-size: 12px; - padding: 0px 24px; - margin-top: 16px; - width: 165px; -} - -.c11:active { - opacity: 0.56; -} - -.c11:hover, -.c11:focus { - background: #2C3A73; -} - -.c11:disabled { - background: rgba(255,255,255,0.12); - color: rgba(255,255,255,0.3); -} - -.c6 { - display: inline-block; - transition: color 0.3s; - margin-left: 4px; - color: #f50057; -} - -.c1 { - overflow: hidden; - text-overflow: ellipsis; - font-weight: 600; - font-size: 18px; - margin: 0px; - margin-top: 4px; - margin-bottom: 4px; -} - -.c2 { - overflow: hidden; - text-overflow: ellipsis; - margin: 0px; - margin-bottom: 32px; -} - -.c4 { - overflow: hidden; - text-overflow: ellipsis; - font-weight: 600; - margin: 0px; -} - -.c8 { - color: #03a9f4; - font-weight: normal; - padding-left: 0; - font-size: inherit; - min-height: auto; -} - -.c5 { - overflow: hidden; - text-overflow: ellipsis; - margin: 0px; - margin-bottom: 16px; - margin-top: 8px; - display: flex; - align-items: center; -} - -.c5 .icon { - margin-right: 8px; -} - -.c3 { - box-sizing: border-box; - padding: 16px; - height: auto; - border-radius: 8px; - max-width: 800px; - background-color: rgba(255,255,255,0.05); - border: 2px solid #f50057; -} - -
-
- Configure Resource -
-
- Install and configure the Teleport SSH Service. -
- Run the following command on the server you want to add. -
-
-
- Command -
-
- - Encountered Error: - some error message -
- -
-
- - - - Exit - -
-
-`; - -exports[`polling error state 1`] = ` -.c0 { - box-sizing: border-box; -} - -.c7 { - box-sizing: border-box; - margin-right: 4px; -} - -.c16 { - box-sizing: border-box; - margin-top: 24px; -} - -.c8 { - line-height: 1.5; - margin: 0; - display: inline-flex; - justify-content: center; - align-items: center; - box-sizing: border-box; - border: none; - border-radius: 4px; - cursor: pointer; - font-family: inherit; - font-weight: 600; - outline: none; - position: relative; - text-align: center; - text-decoration: none; - text-transform: uppercase; - transition: all 0.3s; - -webkit-font-smoothing: antialiased; - background: #512FC9; - color: rgba(255,255,255,0.87); - min-height: 32px; - font-size: 12px; - padding: 0px 24px; -} - -.c8:active { - opacity: 0.56; -} - -.c8:hover, -.c8:focus { - background: #651FFF; -} - -.c8:active { - background: #354AA4; -} - -.c8:disabled { - background: rgba(255,255,255,0.12); - color: rgba(255,255,255,0.3); -} - -.c11 { - line-height: 1.5; - margin: 0; - display: inline-flex; - justify-content: center; - align-items: center; - box-sizing: border-box; - border: none; - border-radius: 4px; - cursor: pointer; - font-family: inherit; - font-weight: 600; - outline: none; - position: relative; - text-align: center; - text-decoration: none; - text-transform: uppercase; - transition: all 0.3s; - -webkit-font-smoothing: antialiased; - background: none; - text-transform: none; - color: rgba(255,255,255,0.87); - min-height: 32px; - font-size: 12px; - padding: 0px 24px; - margin-left: 4px; -} - -.c11:active { - opacity: 0.56; -} - -.c11:hover, -.c11:focus { - background: none; - text-decoration: underline; -} - -.c11:disabled { - background: none; - color: rgba(255,255,255,0.3); -} - -.c17 { - line-height: 1.5; - margin: 0; - display: inline-flex; - justify-content: center; - align-items: center; - box-sizing: border-box; - border: none; - border-radius: 4px; - cursor: pointer; - font-family: inherit; - font-weight: 600; - outline: none; - position: relative; - text-align: center; - text-decoration: none; - text-transform: uppercase; - transition: all 0.3s; - -webkit-font-smoothing: antialiased; - background: #512FC9; - color: rgba(255,255,255,0.87); - min-height: 32px; - font-size: 12px; - padding: 0px 24px; - margin-right: 16px; - width: 165px; -} - -.c17:active { - opacity: 0.56; -} - -.c17:hover, -.c17:focus { - background: #651FFF; -} - -.c17:active { - background: #354AA4; -} - -.c17:disabled { - background: rgba(255,255,255,0.12); - color: rgba(255,255,255,0.3); -} - -.c18 { - line-height: 1.5; - margin: 0; - display: inline-flex; - justify-content: center; - align-items: center; - box-sizing: border-box; - border: none; - border-radius: 4px; - cursor: pointer; - font-family: inherit; - font-weight: 600; - outline: none; - position: relative; - text-align: center; - text-decoration: none; - text-transform: uppercase; - transition: all 0.3s; - -webkit-font-smoothing: antialiased; - background: #222C59; - color: rgba(255,255,255,0.87); - min-height: 32px; - font-size: 12px; - padding: 0px 24px; - margin-top: 16px; - width: 165px; -} - -.c18:active { - opacity: 0.56; -} - -.c18:hover, -.c18:focus { - background: #2C3A73; -} - -.c18:disabled { - background: rgba(255,255,255,0.12); - color: rgba(255,255,255,0.3); -} - -.c10 { - display: inline-block; - transition: color 0.3s; - margin-left: 4px; - color: #f50057; -} - -.c1 { - overflow: hidden; - text-overflow: ellipsis; - font-weight: 600; - font-size: 18px; - margin: 0px; - margin-top: 4px; - margin-bottom: 4px; -} - -.c2 { - overflow: hidden; - text-overflow: ellipsis; - margin: 0px; - margin-bottom: 32px; -} - -.c4 { - overflow: hidden; - text-overflow: ellipsis; - font-weight: 600; - margin: 0px; -} - -.c13 { - overflow: hidden; - text-overflow: ellipsis; - font-weight: 600; - margin: 0px; - margin-top: 24px; -} - -.c5 { - box-sizing: border-box; - margin-bottom: 4px; - margin-top: 8px; - padding: 8px; - background-color: #010B1C; - border-radius: 4px; - display: flex; - align-items: center; - justify-content: space-between; -} - -.c6 { - box-sizing: border-box; - margin-right: 8px; - display: flex; -} - -.c12 { - color: #03a9f4; - font-weight: normal; - padding-left: 0; - font-size: inherit; - min-height: auto; -} - -.c15 { - padding: 2px 5px; - border-radius: 6px; - background-color: rgb(255 255 255 / 17%); - color: inherit; -} - -.c9 { - overflow: hidden; - text-overflow: ellipsis; - margin: 0px; - display: flex; - align-items: center; -} - -.c9 .icon { - margin-right: 8px; -} - -.c3 { - box-sizing: border-box; - padding: 16px; - height: auto; - border-radius: 8px; - max-width: 800px; - background-color: rgba(255,255,255,0.05); - border: 2px solid #f50057; -} - -.c14 { - margin-top: 6px; - margin-bottom: 0; -} - -
-
- Configure Resource -
-
- Install and configure the Teleport SSH Service. -
- Run the following command on the server you want to add. -
-
-
- Command -
-
-
-
- $ -
-
- sudo bash -c "$(curl -fsSL http://localhost/scripts/some-join-token-hash/install-node.sh)" -
-
- -
-
-
- - We could not detect the server you were trying to add - - -
-
- Possible reasons -
-
    -
  • - The command was not run on the server you were trying to add -
  • -
  • - The Teleport SSH Service could not join this Teleport cluster. Check the logs for errors by running -
    - - journalctl status teleport - -
  • -
-
-
-
- - - - Exit - -
-
-`; - -exports[`polling state 1`] = ` -.c0 { - box-sizing: border-box; -} - -.c7 { - box-sizing: border-box; - margin-right: 4px; -} - -.c11 { - box-sizing: border-box; - margin-top: 24px; -} - -.c8 { - line-height: 1.5; - margin: 0; - display: inline-flex; - justify-content: center; - align-items: center; - box-sizing: border-box; - border: none; - border-radius: 4px; - cursor: pointer; - font-family: inherit; - font-weight: 600; - outline: none; - position: relative; - text-align: center; - text-decoration: none; - text-transform: uppercase; - transition: all 0.3s; - -webkit-font-smoothing: antialiased; - background: #512FC9; - color: rgba(255,255,255,0.87); - min-height: 32px; - font-size: 12px; - padding: 0px 24px; -} - -.c8:active { - opacity: 0.56; -} - -.c8:hover, -.c8:focus { - background: #651FFF; -} - -.c8:active { - background: #354AA4; -} - -.c8:disabled { - background: rgba(255,255,255,0.12); - color: rgba(255,255,255,0.3); -} - -.c12 { - line-height: 1.5; - margin: 0; - display: inline-flex; - justify-content: center; - align-items: center; - box-sizing: border-box; - border: none; - border-radius: 4px; - cursor: pointer; - font-family: inherit; - font-weight: 600; - outline: none; - position: relative; - text-align: center; - text-decoration: none; - text-transform: uppercase; - transition: all 0.3s; - -webkit-font-smoothing: antialiased; - background: #512FC9; - color: rgba(255,255,255,0.87); - min-height: 32px; - font-size: 12px; - padding: 0px 24px; - margin-right: 16px; - width: 165px; -} - -.c12:active { - opacity: 0.56; -} - -.c12:hover, -.c12:focus { - background: #651FFF; -} - -.c12:active { - background: #354AA4; -} - -.c12:disabled { - background: rgba(255,255,255,0.12); - color: rgba(255,255,255,0.3); -} - -.c13 { - line-height: 1.5; - margin: 0; - display: inline-flex; - justify-content: center; - align-items: center; - box-sizing: border-box; - border: none; - border-radius: 4px; - cursor: pointer; - font-family: inherit; - font-weight: 600; - outline: none; - position: relative; - text-align: center; - text-decoration: none; - text-transform: uppercase; - transition: all 0.3s; - -webkit-font-smoothing: antialiased; - background: #222C59; - color: rgba(255,255,255,0.87); - min-height: 32px; - font-size: 12px; - padding: 0px 24px; - margin-top: 16px; - width: 165px; -} - -.c13:active { - opacity: 0.56; -} - -.c13:hover, -.c13:focus { - background: #2C3A73; -} - -.c13:disabled { - background: rgba(255,255,255,0.12); - color: rgba(255,255,255,0.3); -} - -.c10 { - display: inline-block; - transition: color 0.3s; - color: #FFFFFF; - font-size: 18px; -} - -.c1 { - overflow: hidden; - text-overflow: ellipsis; - font-weight: 600; - font-size: 18px; - margin: 0px; - margin-top: 4px; - margin-bottom: 4px; -} - -.c2 { - overflow: hidden; - text-overflow: ellipsis; - margin: 0px; - margin-bottom: 32px; -} - -.c4 { - overflow: hidden; - text-overflow: ellipsis; - font-weight: 600; - margin: 0px; -} - -.c5 { - box-sizing: border-box; - margin-bottom: 4px; - margin-top: 8px; - padding: 8px; - background-color: #010B1C; - border-radius: 4px; - display: flex; - align-items: center; - justify-content: space-between; -} - -.c6 { - box-sizing: border-box; - margin-right: 8px; - display: flex; -} - -.c3 { - box-sizing: border-box; - padding: 16px; - height: auto; - border-radius: 8px; - max-width: 800px; - background-color: rgba(255,255,255,0.05); - border: 2px solid #2F3659; -} - -.c9 { - overflow: hidden; - text-overflow: ellipsis; - margin: 0px; - display: flex; - align-items: center; - white-space: pre; -} - -.c9 .icon { - margin-right: 8px; -} - -
-
- Configure Resource -
-
- Install and configure the Teleport SSH Service. -
- Run the following command on the server you want to add. -
-
-
- Command -
-
-
-
- $ -
-
- sudo bash -c "$(curl -fsSL http://localhost/scripts/some-join-token-hash/install-node.sh)" -
-
- -
-
- - Waiting for Teleport SSH Service | 05:00 minutes -
-
-
- - - - Exit - -
-
-`; - -exports[`polling success state 1`] = ` -.c0 { - box-sizing: border-box; -} - -.c7 { - box-sizing: border-box; - margin-right: 4px; -} - -.c11 { - box-sizing: border-box; - margin-top: 24px; -} - -.c8 { - line-height: 1.5; - margin: 0; - display: inline-flex; - justify-content: center; - align-items: center; - box-sizing: border-box; - border: none; - border-radius: 4px; - cursor: pointer; - font-family: inherit; - font-weight: 600; - outline: none; - position: relative; - text-align: center; - text-decoration: none; - text-transform: uppercase; - transition: all 0.3s; - -webkit-font-smoothing: antialiased; - background: #512FC9; - color: rgba(255,255,255,0.87); - min-height: 32px; - font-size: 12px; - padding: 0px 24px; -} - -.c8:active { - opacity: 0.56; -} - -.c8:hover, -.c8:focus { - background: #651FFF; -} - -.c8:active { - background: #354AA4; -} - -.c8:disabled { - background: rgba(255,255,255,0.12); - color: rgba(255,255,255,0.3); -} - -.c12 { - line-height: 1.5; - margin: 0; - display: inline-flex; - justify-content: center; - align-items: center; - box-sizing: border-box; - border: none; - border-radius: 4px; - cursor: pointer; - font-family: inherit; - font-weight: 600; - outline: none; - position: relative; - text-align: center; - text-decoration: none; - text-transform: uppercase; - transition: all 0.3s; - -webkit-font-smoothing: antialiased; - background: #512FC9; - color: rgba(255,255,255,0.87); - min-height: 32px; - font-size: 12px; - padding: 0px 24px; - margin-right: 16px; - width: 165px; -} - -.c12:active { - opacity: 0.56; -} - -.c12:hover, -.c12:focus { - background: #651FFF; -} - -.c12:active { - background: #354AA4; -} - -.c12:disabled { - background: rgba(255,255,255,0.12); - color: rgba(255,255,255,0.3); -} - -.c13 { - line-height: 1.5; - margin: 0; - display: inline-flex; - justify-content: center; - align-items: center; - box-sizing: border-box; - border: none; - border-radius: 4px; - cursor: pointer; - font-family: inherit; - font-weight: 600; - outline: none; - position: relative; - text-align: center; - text-decoration: none; - text-transform: uppercase; - transition: all 0.3s; - -webkit-font-smoothing: antialiased; - background: #222C59; - color: rgba(255,255,255,0.87); - min-height: 32px; - font-size: 12px; - padding: 0px 24px; - margin-top: 16px; - width: 165px; -} - -.c13:active { - opacity: 0.56; -} - -.c13:hover, -.c13:focus { - background: #2C3A73; -} - -.c13:disabled { - background: rgba(255,255,255,0.12); - color: rgba(255,255,255,0.3); -} - -.c10 { - display: inline-block; - transition: color 0.3s; - margin-left: 4px; - color: #00bfa5; -} - -.c1 { - overflow: hidden; - text-overflow: ellipsis; - font-weight: 600; - font-size: 18px; - margin: 0px; - margin-top: 4px; - margin-bottom: 4px; -} - -.c2 { - overflow: hidden; - text-overflow: ellipsis; - margin: 0px; - margin-bottom: 32px; -} - -.c4 { - overflow: hidden; - text-overflow: ellipsis; - font-weight: 600; - margin: 0px; -} - -.c5 { - box-sizing: border-box; - margin-bottom: 4px; - margin-top: 8px; - padding: 8px; - background-color: #010B1C; - border-radius: 4px; - display: flex; - align-items: center; - justify-content: space-between; -} - -.c6 { - box-sizing: border-box; - margin-right: 8px; - display: flex; -} - -.c9 { - overflow: hidden; - text-overflow: ellipsis; - margin: 0px; - display: flex; - align-items: center; -} - -.c9 .icon { - margin-right: 8px; -} - -.c3 { - box-sizing: border-box; - padding: 16px; - height: auto; - border-radius: 8px; - max-width: 800px; - background-color: rgba(255,255,255,0.05); - border: 2px solid #00bfa5; -} - -
-
- Configure Resource -
-
- Install and configure the Teleport SSH Service. -
- Run the following command on the server you want to add. -
-
-
- Command -
-
-
-
- $ -
-
- sudo bash -c "$(curl -fsSL http://localhost/scripts/some-join-token-hash/install-node.sh)" -
-
- -
-
- - The server successfully joined this Teleport cluster -
-
-
- - - - Exit - -
-
-`; diff --git a/web/packages/teleport/src/Discover/Server/DownloadScript/useDownloadScript.tsx b/web/packages/teleport/src/Discover/Server/DownloadScript/useDownloadScript.tsx deleted file mode 100644 index 398ba84815fb8..0000000000000 --- a/web/packages/teleport/src/Discover/Server/DownloadScript/useDownloadScript.tsx +++ /dev/null @@ -1,199 +0,0 @@ -/** - * Copyright 2022 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 { useState, useEffect } from 'react'; -import { - addMinutes, - intervalToDuration, - differenceInMilliseconds, -} from 'date-fns'; -import useAttempt from 'shared/hooks/useAttemptNext'; - -import { INTERNAL_RESOURCE_ID_LABEL_KEY } from 'teleport/services/joinToken'; -import TeleportContext from 'teleport/teleportContext'; - -import { AgentStepProps } from '../../types'; - -import type { JoinToken } from 'teleport/services/joinToken'; - -const FIVE_MINUTES = 5; -const THREE_SECONDS_IN_MS = 3000; -const ONE_SECOND_IN_MS = 1000; - -export function useDownloadScript({ ctx, props }: Props) { - const { attempt, run, setAttempt } = useAttempt('processing'); - const [joinToken, setJoinToken] = useState(); - const [pollState, setPollState] = useState('polling'); - - // TODO (lisa) extract count down logic into it's own component. - const [countdownTime, setCountdownTime] = useState({ - minutes: 5, - seconds: 0, - }); - - // Responsible for initial join token fetch. - useEffect(() => { - fetchJoinToken(); - }, []); - - // Responsible for initiating polling and - // timeout on join token change. - useEffect(() => { - if (!joinToken) return; - - setPollState('polling'); - - // abortController is required to cancel in flight request. - const abortController = new AbortController(); - const abortSignal = abortController.signal; - let timeoutId; - let pollingIntervalId; - let countdownIntervalId; - - // countdownEndDate takes current date and adds 5 minutes to it. - let countdownEndDate = addMinutes(new Date(), FIVE_MINUTES); - - // inFlightReq is a flag to prevent another fetch request when a - // previous fetch request is still in progress. May happen when a - // previous fetch request is taking longer than the polling - // interval time. - let inFlightReq; - - function cleanUp() { - clearInterval(pollingIntervalId); - clearInterval(countdownIntervalId); - clearTimeout(timeoutId); - setCountdownTime({ minutes: 5, seconds: 0 }); - // Cancel any in flight request. - abortController.abort(); - } - - function fetchNodeMatchingRefResourceId() { - if (inFlightReq) return; - - inFlightReq = ctx.nodeService - .fetchNodes( - ctx.storeUser.getClusterId(), - { - search: `${INTERNAL_RESOURCE_ID_LABEL_KEY} ${joinToken.internalResourceId}`, - limit: 1, - }, - abortSignal - ) - .then(res => { - if (res.agents.length > 0) { - setPollState('success'); - props.updateAgentMeta({ - ...props.agentMeta, - // Node is an oddity in that the hostname is the more - // user friendly text. - resourceName: res.agents[0].hostname, - node: res.agents[0], - }); - cleanUp(); - } - }) - // Polling related errors are ignored. - // The most likely cause of error would be network issues - // and aborting in flight request. - .catch(() => {}) - .finally(() => { - inFlightReq = null; // reset flag - }); - } - - function updateCountdown() { - const start = new Date(); - const end = countdownEndDate; - const duration = intervalToDuration({ start, end }); - - if (differenceInMilliseconds(end, start) <= 0) { - setPollState('error'); - cleanUp(); - return; - } - - setCountdownTime({ - minutes: duration.minutes, - seconds: duration.seconds, - }); - } - - // Set a countdown in case polling continuosly produces - // no results. Which means there is either a network error, - // script is ran unsuccessfully, script has not been ran, - // or resource cannot connect to cluster. - countdownIntervalId = setInterval( - () => updateCountdown(), - ONE_SECOND_IN_MS - ); - - // Start the poller to discover the resource just added. - pollingIntervalId = setInterval( - () => fetchNodeMatchingRefResourceId(), - THREE_SECONDS_IN_MS - ); - - return () => { - cleanUp(); - }; - }, [joinToken]); - - function fetchJoinToken() { - run(() => - ctx.joinTokenService.fetchJoinToken(['Node'], 'token').then(token => { - // Probably will never happen, but just in case, otherwise - // querying for the resource can return a false positive. - if (!token.internalResourceId) { - setAttempt({ - status: 'failed', - statusText: - 'internal resource ID is required to discover the newly added resource, but none was provided', - }); - return; - } - setJoinToken(token); - }) - ); - } - - function regenerateScriptAndRepoll() { - fetchJoinToken(); - } - - return { - attempt, - joinToken, - nextStep: props.nextStep, - pollState, - regenerateScriptAndRepoll, - countdownTime, - }; -} - -type Props = { - ctx: TeleportContext; - props: AgentStepProps; -}; - -type PollState = 'polling' | 'success' | 'error'; - -export type CountdownTime = { - minutes: number; - seconds: number; -}; - -export type State = ReturnType; diff --git a/web/packages/teleport/src/Discover/Server/ServerWrapper.tsx b/web/packages/teleport/src/Discover/Server/ServerWrapper.tsx new file mode 100644 index 0000000000000..8dc17fd6964f7 --- /dev/null +++ b/web/packages/teleport/src/Discover/Server/ServerWrapper.tsx @@ -0,0 +1,44 @@ +/** + * Copyright 2022 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 { PingTeleportProvider } from 'teleport/Discover/Shared/PingTeleportContext'; +import { JoinTokenProvider } from 'teleport/Discover/Shared/JoinTokenContext'; + +import { ResourceKind } from '../Shared'; + +const PING_TIMEOUT = 1000 * 60 * 5; // 5 minutes +const PING_INTERVAL = 1000 * 3; // 3 seconds +export const SCRIPT_TIMEOUT = 1000 * 60 * 5; // 5 minutes + +export function ServerWrapper(props: ServerWrapperProps) { + return ( + + + {props.children} + + + ); +} + +interface ServerWrapperProps { + children: React.ReactNode; +} diff --git a/web/packages/teleport/src/Discover/Server/SetupAccess/index.ts b/web/packages/teleport/src/Discover/Server/SetupAccess/index.ts index 3f7016de4273d..b93f097a21046 100644 --- a/web/packages/teleport/src/Discover/Server/SetupAccess/index.ts +++ b/web/packages/teleport/src/Discover/Server/SetupAccess/index.ts @@ -14,5 +14,5 @@ * limitations under the License. */ -import LoginTrait from './SetupAccess'; -export { LoginTrait }; +import SetupAccess from './SetupAccess'; +export { SetupAccess }; diff --git a/web/packages/teleport/src/Discover/Server/index.tsx b/web/packages/teleport/src/Discover/Server/index.tsx index 98b691407b158..3f2b1ca7d1d50 100644 --- a/web/packages/teleport/src/Discover/Server/index.tsx +++ b/web/packages/teleport/src/Discover/Server/index.tsx @@ -20,13 +20,18 @@ import { Server } from 'design/Icon'; import { Resource } from 'teleport/Discover/flow'; import { DownloadScript } from 'teleport/Discover/Server/DownloadScript'; -import { LoginTrait } from 'teleport/Discover/Server/SetupAccess'; +import { SetupAccess } from 'teleport/Discover/Server/SetupAccess'; import { TestConnection } from 'teleport/Discover/Server/TestConnection'; import { ResourceKind, Finished } from 'teleport/Discover/Shared'; +import { ServerWrapper } from './ServerWrapper'; + export const ServerResource: Resource = { kind: ResourceKind.Server, icon: , + wrapper: (component: React.ReactNode) => ( + {component} + ), shouldPrompt(currentStep) { // do not prompt on exit if they're selecting a resource return currentStep !== 0; @@ -41,7 +46,7 @@ export const ServerResource: Resource = { }, { title: 'Set Up Access', - component: LoginTrait, + component: SetupAccess, }, { title: 'Test Connection', diff --git a/web/packages/teleport/src/Discover/Shared/CommandWithTimer/CommandWithTimer.story.test.tsx b/web/packages/teleport/src/Discover/Shared/CommandWithTimer/CommandWithTimer.story.test.tsx new file mode 100644 index 0000000000000..e84be4af5ac8d --- /dev/null +++ b/web/packages/teleport/src/Discover/Shared/CommandWithTimer/CommandWithTimer.story.test.tsx @@ -0,0 +1,50 @@ +/** + * Copyright 2022 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 { render } from 'design/utils/testing'; + +import * as stories from './CommandWithTimer.story'; + +test('render default polling', async () => { + const { container } = render(); + expect(container).toMatchSnapshot(); +}); + +test('render default polling success', async () => { + const { container } = render(); + expect(container).toMatchSnapshot(); +}); + +test('render default polling error', async () => { + const { container } = render(); + expect(container).toMatchSnapshot(); +}); + +test('render custom polling', async () => { + const { container } = render(); + expect(container).toMatchSnapshot(); +}); + +test('render custom polling success', async () => { + const { container } = render(); + expect(container).toMatchSnapshot(); +}); + +test('render custom polling error', async () => { + const { container } = render(); + expect(container).toMatchSnapshot(); +}); diff --git a/web/packages/teleport/src/Discover/Shared/CommandWithTimer/CommandWithTimer.story.tsx b/web/packages/teleport/src/Discover/Shared/CommandWithTimer/CommandWithTimer.story.tsx new file mode 100644 index 0000000000000..c0ce890216241 --- /dev/null +++ b/web/packages/teleport/src/Discover/Shared/CommandWithTimer/CommandWithTimer.story.tsx @@ -0,0 +1,74 @@ +/** + * Copyright 2022 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 { CommandWithTimer } from './CommandWithTimer'; + +export default { + title: 'Teleport/Discover/Shared/CommandWithTimer', +}; + +export const DefaultPolling = () => ( + +); + +export const DefaultPollingSuccess = () => ( + +); + +export const DefaultPollingError = () => ( + error reason 1, <>error reason 2] }, + }} + /> +); + +export const CustomPolling = () => ( + Custom Header Component} + /> +); + +export const CustomPollingSuccess = () => ( + +); + +export const CustomPollingError = () => ( + error reason 1, <>error reason 2], + customErrContent: <>custom error content, + }, + }} + /> +); + +const props = { + command: 'some kind of command', + pollingTimeout: 100000, +}; diff --git a/web/packages/teleport/src/Discover/Shared/CommandWithTimer/CommandWithTimer.tsx b/web/packages/teleport/src/Discover/Shared/CommandWithTimer/CommandWithTimer.tsx new file mode 100644 index 0000000000000..843cea591bf2c --- /dev/null +++ b/web/packages/teleport/src/Discover/Shared/CommandWithTimer/CommandWithTimer.tsx @@ -0,0 +1,123 @@ +// Copyright 2022 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 styled from 'styled-components'; +import { Text, Box } from 'design'; +import * as Icons from 'design/Icon'; + +import { TextSelectCopyMulti } from 'teleport/components/TextSelectCopy'; +import { Timeout } from 'teleport/Discover/Shared/Timeout'; +import { TextIcon } from 'teleport/Discover/Shared'; + +export const CommandWithTimer = ({ + command, + poll, + pollingTimeout, + header, +}: Props) => { + return ( + + {header || Command} + + + + {poll.state === 'polling' && ( + + + + + )} + {poll.state === 'success' && ( + + + {poll.customStateDesc || + 'The Teleport Service successfully join this Teleport cluster'} + + )} + {poll.state === 'error' && ( + + + + {poll.error.customErrContent || + 'We could not detect the Teleport Service you were trying to add'} + + + Possible reasons + +
    + {poll.error.reasonContents.map((content, index) => ( +
  • {content}
  • + ))} +
+
+ )} +
+ ); +}; + +type PollError = { + reasonContents: React.ReactNode[]; + customErrContent?: React.ReactNode; +}; + +export type PollState = 'polling' | 'success' | 'error'; + +export type Poll = { + state: 'polling' | 'success' | 'error'; + customStateDesc?: string; + // error only needs to be defined when + // poll state is 'error'. + error?: PollError; +}; + +export const PollBox = styled(Box)` + max-width: 800px; + background-color: rgba(255, 255, 255, 0.05); + padding: ${props => `${props.theme.space[3]}px`}; + border-radius: ${props => `${props.theme.space[2]}px`}; + border: 2px solid + ${props => { + switch (props.pollState) { + case 'error': + return props.theme.colors.danger; + case 'success': + return props.theme.colors.success; + default: + // polling + return '#2F3659'; + } + }}; +`; + +export type Props = { + command: string; + poll: Poll; + pollingTimeout: number; + header?: React.ReactNode; +}; diff --git a/web/packages/teleport/src/Discover/Shared/CommandWithTimer/__snapshots__/CommandWithTimer.story.test.tsx.snap b/web/packages/teleport/src/Discover/Shared/CommandWithTimer/__snapshots__/CommandWithTimer.story.test.tsx.snap new file mode 100644 index 0000000000000..bfe1db9da51ef --- /dev/null +++ b/web/packages/teleport/src/Discover/Shared/CommandWithTimer/__snapshots__/CommandWithTimer.story.test.tsx.snap @@ -0,0 +1,1520 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`render custom polling 1`] = ` +.c1 { + box-sizing: border-box; + margin-bottom: 4px; + margin-top: 8px; +} + +.c4 { + box-sizing: border-box; + padding-bottom: 0px; + padding-top: 8px; +} + +.c6 { + box-sizing: border-box; + margin-right: 4px; +} + +.c8 { + line-height: 1.5; + margin: 0; + display: inline-flex; + justify-content: center; + align-items: center; + box-sizing: border-box; + border: none; + border-radius: 4px; + cursor: pointer; + font-family: inherit; + font-weight: 600; + outline: none; + position: relative; + text-align: center; + text-decoration: none; + text-transform: uppercase; + transition: all 0.3s; + -webkit-font-smoothing: antialiased; + background: #222C59; + color: rgba(255,255,255,0.87); + min-height: 32px; + font-size: 12px; + padding: 0px 24px; +} + +.c8:active { + opacity: 0.56; +} + +.c8:hover, +.c8:focus { + background: #2C3A73; +} + +.c8:disabled { + background: rgba(255,255,255,0.12); + color: rgba(255,255,255,0.3); +} + +.c11 { + display: inline-block; + transition: color 0.3s; + color: #FFFFFF; +} + +.c13 { + display: inline-block; + transition: color 0.3s; + color: #FFFFFF; + font-size: 18px; +} + +.c5 { + box-sizing: border-box; + display: flex; +} + +.c10 .icon-check { + display: none; +} + +.c10 .icon-copy { + display: block; +} + +.c10.copied .icon-check { + display: block; +} + +.c10.copied .icon-copy { + display: none; +} + +.c9 { + height: 28px; + width: 28px; + border-radius: 20px; + min-height: auto; + padding: 0; + margin-top: -4px; +} + +.c3 { + box-sizing: border-box; + margin-right: 4px; + white-space: pre; + word-break: break-all; + font-size: 12px; + font-family: "Droid Sans Mono","monospace",monospace,"Droid Sans Fallback"; + overflow: scroll; + line-height: 20px; +} + +.c2 { + box-sizing: border-box; + padding-bottom: 8px; + padding-left: 16px; + padding-right: 48px; + padding-top: 8px; + background-color: #010B1C; + border-radius: 4px; + position: relative; +} + +.c7 { + box-sizing: border-box; + padding-right: 16px; + position: absolute; + right: 0px; +} + +.c0 { + box-sizing: border-box; + margin-top: 24px; + padding: 16px; + border-radius: 8px; + max-width: 800px; + background-color: rgba(255,255,255,0.05); + padding: 16px; + border-radius: 8px; + border: 2px solid #2F3659; +} + +.c12 { + overflow: hidden; + text-overflow: ellipsis; + margin: 0px; + display: flex; + align-items: center; + white-space: pre; +} + +.c12 .icon { + margin-right: 8px; +} + +
+
+
+ Custom Header Component +
+
+
+
+
+
+
+
+ $ +
+
+ some kind of command +
+
+
+ +
+
+
+
+
+
+
+ + + custom polling text | + + 00 + : + 00 + +
+
+
+`; + +exports[`render custom polling error 1`] = ` +.c2 { + box-sizing: border-box; + margin-bottom: 4px; + margin-top: 8px; +} + +.c5 { + box-sizing: border-box; + padding-bottom: 0px; + padding-top: 8px; +} + +.c7 { + box-sizing: border-box; + margin-right: 4px; +} + +.c13 { + box-sizing: border-box; +} + +.c9 { + line-height: 1.5; + margin: 0; + display: inline-flex; + justify-content: center; + align-items: center; + box-sizing: border-box; + border: none; + border-radius: 4px; + cursor: pointer; + font-family: inherit; + font-weight: 600; + outline: none; + position: relative; + text-align: center; + text-decoration: none; + text-transform: uppercase; + transition: all 0.3s; + -webkit-font-smoothing: antialiased; + background: #222C59; + color: rgba(255,255,255,0.87); + min-height: 32px; + font-size: 12px; + padding: 0px 24px; +} + +.c9:active { + opacity: 0.56; +} + +.c9:hover, +.c9:focus { + background: #2C3A73; +} + +.c9:disabled { + background: rgba(255,255,255,0.12); + color: rgba(255,255,255,0.3); +} + +.c12 { + display: inline-block; + transition: color 0.3s; + color: #FFFFFF; +} + +.c15 { + display: inline-block; + transition: color 0.3s; + margin-left: 4px; + color: #f50057; +} + +.c1 { + overflow: hidden; + text-overflow: ellipsis; + font-weight: 600; + margin: 0px; +} + +.c16 { + overflow: hidden; + text-overflow: ellipsis; + font-weight: 600; + margin: 0px; + margin-top: 24px; +} + +.c6 { + box-sizing: border-box; + display: flex; +} + +.c11 .icon-check { + display: none; +} + +.c11 .icon-copy { + display: block; +} + +.c11.copied .icon-check { + display: block; +} + +.c11.copied .icon-copy { + display: none; +} + +.c10 { + height: 28px; + width: 28px; + border-radius: 20px; + min-height: auto; + padding: 0; + margin-top: -4px; +} + +.c4 { + box-sizing: border-box; + margin-right: 4px; + white-space: pre; + word-break: break-all; + font-size: 12px; + font-family: "Droid Sans Mono","monospace",monospace,"Droid Sans Fallback"; + overflow: scroll; + line-height: 20px; +} + +.c3 { + box-sizing: border-box; + padding-bottom: 8px; + padding-left: 16px; + padding-right: 48px; + padding-top: 8px; + background-color: #010B1C; + border-radius: 4px; + position: relative; +} + +.c8 { + box-sizing: border-box; + padding-right: 16px; + position: absolute; + right: 0px; +} + +.c14 { + overflow: hidden; + text-overflow: ellipsis; + margin: 0px; + display: flex; + align-items: center; +} + +.c14 .icon { + margin-right: 8px; +} + +.c0 { + box-sizing: border-box; + margin-top: 24px; + padding: 16px; + border-radius: 8px; + max-width: 800px; + background-color: rgba(255,255,255,0.05); + padding: 16px; + border-radius: 8px; + border: 2px solid #f50057; +} + +.c17 { + margin-top: 6px; + margin-bottom: 0; +} + +
+
+
+ Command +
+
+
+
+
+
+
+
+ $ +
+
+ some kind of command +
+
+
+ +
+
+
+
+
+
+
+
+ + custom error content +
+
+ Possible reasons +
+
    +
  • + error reason 1 +
  • +
  • + error reason 2 +
  • +
+
+
+
+`; + +exports[`render custom polling success 1`] = ` +.c2 { + box-sizing: border-box; + margin-bottom: 4px; + margin-top: 8px; +} + +.c5 { + box-sizing: border-box; + padding-bottom: 0px; + padding-top: 8px; +} + +.c7 { + box-sizing: border-box; + margin-right: 4px; +} + +.c9 { + line-height: 1.5; + margin: 0; + display: inline-flex; + justify-content: center; + align-items: center; + box-sizing: border-box; + border: none; + border-radius: 4px; + cursor: pointer; + font-family: inherit; + font-weight: 600; + outline: none; + position: relative; + text-align: center; + text-decoration: none; + text-transform: uppercase; + transition: all 0.3s; + -webkit-font-smoothing: antialiased; + background: #222C59; + color: rgba(255,255,255,0.87); + min-height: 32px; + font-size: 12px; + padding: 0px 24px; +} + +.c9:active { + opacity: 0.56; +} + +.c9:hover, +.c9:focus { + background: #2C3A73; +} + +.c9:disabled { + background: rgba(255,255,255,0.12); + color: rgba(255,255,255,0.3); +} + +.c12 { + display: inline-block; + transition: color 0.3s; + color: #FFFFFF; +} + +.c14 { + display: inline-block; + transition: color 0.3s; + margin-left: 4px; + color: #00bfa5; +} + +.c1 { + overflow: hidden; + text-overflow: ellipsis; + font-weight: 600; + margin: 0px; +} + +.c6 { + box-sizing: border-box; + display: flex; +} + +.c11 .icon-check { + display: none; +} + +.c11 .icon-copy { + display: block; +} + +.c11.copied .icon-check { + display: block; +} + +.c11.copied .icon-copy { + display: none; +} + +.c10 { + height: 28px; + width: 28px; + border-radius: 20px; + min-height: auto; + padding: 0; + margin-top: -4px; +} + +.c4 { + box-sizing: border-box; + margin-right: 4px; + white-space: pre; + word-break: break-all; + font-size: 12px; + font-family: "Droid Sans Mono","monospace",monospace,"Droid Sans Fallback"; + overflow: scroll; + line-height: 20px; +} + +.c3 { + box-sizing: border-box; + padding-bottom: 8px; + padding-left: 16px; + padding-right: 48px; + padding-top: 8px; + background-color: #010B1C; + border-radius: 4px; + position: relative; +} + +.c8 { + box-sizing: border-box; + padding-right: 16px; + position: absolute; + right: 0px; +} + +.c13 { + overflow: hidden; + text-overflow: ellipsis; + margin: 0px; + display: flex; + align-items: center; +} + +.c13 .icon { + margin-right: 8px; +} + +.c0 { + box-sizing: border-box; + margin-top: 24px; + padding: 16px; + border-radius: 8px; + max-width: 800px; + background-color: rgba(255,255,255,0.05); + padding: 16px; + border-radius: 8px; + border: 2px solid #00bfa5; +} + +
+
+
+ Command +
+
+
+
+
+
+
+
+ $ +
+
+ some kind of command +
+
+
+ +
+
+
+
+
+
+
+ + custom polling success text +
+
+
+`; + +exports[`render default polling 1`] = ` +.c2 { + box-sizing: border-box; + margin-bottom: 4px; + margin-top: 8px; +} + +.c5 { + box-sizing: border-box; + padding-bottom: 0px; + padding-top: 8px; +} + +.c7 { + box-sizing: border-box; + margin-right: 4px; +} + +.c9 { + line-height: 1.5; + margin: 0; + display: inline-flex; + justify-content: center; + align-items: center; + box-sizing: border-box; + border: none; + border-radius: 4px; + cursor: pointer; + font-family: inherit; + font-weight: 600; + outline: none; + position: relative; + text-align: center; + text-decoration: none; + text-transform: uppercase; + transition: all 0.3s; + -webkit-font-smoothing: antialiased; + background: #222C59; + color: rgba(255,255,255,0.87); + min-height: 32px; + font-size: 12px; + padding: 0px 24px; +} + +.c9:active { + opacity: 0.56; +} + +.c9:hover, +.c9:focus { + background: #2C3A73; +} + +.c9:disabled { + background: rgba(255,255,255,0.12); + color: rgba(255,255,255,0.3); +} + +.c12 { + display: inline-block; + transition: color 0.3s; + color: #FFFFFF; +} + +.c14 { + display: inline-block; + transition: color 0.3s; + color: #FFFFFF; + font-size: 18px; +} + +.c1 { + overflow: hidden; + text-overflow: ellipsis; + font-weight: 600; + margin: 0px; +} + +.c6 { + box-sizing: border-box; + display: flex; +} + +.c11 .icon-check { + display: none; +} + +.c11 .icon-copy { + display: block; +} + +.c11.copied .icon-check { + display: block; +} + +.c11.copied .icon-copy { + display: none; +} + +.c10 { + height: 28px; + width: 28px; + border-radius: 20px; + min-height: auto; + padding: 0; + margin-top: -4px; +} + +.c4 { + box-sizing: border-box; + margin-right: 4px; + white-space: pre; + word-break: break-all; + font-size: 12px; + font-family: "Droid Sans Mono","monospace",monospace,"Droid Sans Fallback"; + overflow: scroll; + line-height: 20px; +} + +.c3 { + box-sizing: border-box; + padding-bottom: 8px; + padding-left: 16px; + padding-right: 48px; + padding-top: 8px; + background-color: #010B1C; + border-radius: 4px; + position: relative; +} + +.c8 { + box-sizing: border-box; + padding-right: 16px; + position: absolute; + right: 0px; +} + +.c0 { + box-sizing: border-box; + margin-top: 24px; + padding: 16px; + border-radius: 8px; + max-width: 800px; + background-color: rgba(255,255,255,0.05); + padding: 16px; + border-radius: 8px; + border: 2px solid #2F3659; +} + +.c13 { + overflow: hidden; + text-overflow: ellipsis; + margin: 0px; + display: flex; + align-items: center; + white-space: pre; +} + +.c13 .icon { + margin-right: 8px; +} + +
+
+
+ Command +
+
+
+
+
+
+
+
+ $ +
+
+ some kind of command +
+
+
+ +
+
+
+
+
+
+
+ + + Waiting for Teleport Service | + + 00 + : + 00 + +
+
+
+`; + +exports[`render default polling error 1`] = ` +.c2 { + box-sizing: border-box; + margin-bottom: 4px; + margin-top: 8px; +} + +.c5 { + box-sizing: border-box; + padding-bottom: 0px; + padding-top: 8px; +} + +.c7 { + box-sizing: border-box; + margin-right: 4px; +} + +.c13 { + box-sizing: border-box; +} + +.c9 { + line-height: 1.5; + margin: 0; + display: inline-flex; + justify-content: center; + align-items: center; + box-sizing: border-box; + border: none; + border-radius: 4px; + cursor: pointer; + font-family: inherit; + font-weight: 600; + outline: none; + position: relative; + text-align: center; + text-decoration: none; + text-transform: uppercase; + transition: all 0.3s; + -webkit-font-smoothing: antialiased; + background: #222C59; + color: rgba(255,255,255,0.87); + min-height: 32px; + font-size: 12px; + padding: 0px 24px; +} + +.c9:active { + opacity: 0.56; +} + +.c9:hover, +.c9:focus { + background: #2C3A73; +} + +.c9:disabled { + background: rgba(255,255,255,0.12); + color: rgba(255,255,255,0.3); +} + +.c12 { + display: inline-block; + transition: color 0.3s; + color: #FFFFFF; +} + +.c15 { + display: inline-block; + transition: color 0.3s; + margin-left: 4px; + color: #f50057; +} + +.c1 { + overflow: hidden; + text-overflow: ellipsis; + font-weight: 600; + margin: 0px; +} + +.c16 { + overflow: hidden; + text-overflow: ellipsis; + font-weight: 600; + margin: 0px; + margin-top: 24px; +} + +.c6 { + box-sizing: border-box; + display: flex; +} + +.c11 .icon-check { + display: none; +} + +.c11 .icon-copy { + display: block; +} + +.c11.copied .icon-check { + display: block; +} + +.c11.copied .icon-copy { + display: none; +} + +.c10 { + height: 28px; + width: 28px; + border-radius: 20px; + min-height: auto; + padding: 0; + margin-top: -4px; +} + +.c4 { + box-sizing: border-box; + margin-right: 4px; + white-space: pre; + word-break: break-all; + font-size: 12px; + font-family: "Droid Sans Mono","monospace",monospace,"Droid Sans Fallback"; + overflow: scroll; + line-height: 20px; +} + +.c3 { + box-sizing: border-box; + padding-bottom: 8px; + padding-left: 16px; + padding-right: 48px; + padding-top: 8px; + background-color: #010B1C; + border-radius: 4px; + position: relative; +} + +.c8 { + box-sizing: border-box; + padding-right: 16px; + position: absolute; + right: 0px; +} + +.c14 { + overflow: hidden; + text-overflow: ellipsis; + margin: 0px; + display: flex; + align-items: center; +} + +.c14 .icon { + margin-right: 8px; +} + +.c0 { + box-sizing: border-box; + margin-top: 24px; + padding: 16px; + border-radius: 8px; + max-width: 800px; + background-color: rgba(255,255,255,0.05); + padding: 16px; + border-radius: 8px; + border: 2px solid #f50057; +} + +.c17 { + margin-top: 6px; + margin-bottom: 0; +} + +
+
+
+ Command +
+
+
+
+
+
+
+
+ $ +
+
+ some kind of command +
+
+
+ +
+
+
+
+
+
+
+
+ + We could not detect the Teleport Service you were trying to add +
+
+ Possible reasons +
+
    +
  • + error reason 1 +
  • +
  • + error reason 2 +
  • +
+
+
+
+`; + +exports[`render default polling success 1`] = ` +.c2 { + box-sizing: border-box; + margin-bottom: 4px; + margin-top: 8px; +} + +.c5 { + box-sizing: border-box; + padding-bottom: 0px; + padding-top: 8px; +} + +.c7 { + box-sizing: border-box; + margin-right: 4px; +} + +.c9 { + line-height: 1.5; + margin: 0; + display: inline-flex; + justify-content: center; + align-items: center; + box-sizing: border-box; + border: none; + border-radius: 4px; + cursor: pointer; + font-family: inherit; + font-weight: 600; + outline: none; + position: relative; + text-align: center; + text-decoration: none; + text-transform: uppercase; + transition: all 0.3s; + -webkit-font-smoothing: antialiased; + background: #222C59; + color: rgba(255,255,255,0.87); + min-height: 32px; + font-size: 12px; + padding: 0px 24px; +} + +.c9:active { + opacity: 0.56; +} + +.c9:hover, +.c9:focus { + background: #2C3A73; +} + +.c9:disabled { + background: rgba(255,255,255,0.12); + color: rgba(255,255,255,0.3); +} + +.c12 { + display: inline-block; + transition: color 0.3s; + color: #FFFFFF; +} + +.c14 { + display: inline-block; + transition: color 0.3s; + margin-left: 4px; + color: #00bfa5; +} + +.c1 { + overflow: hidden; + text-overflow: ellipsis; + font-weight: 600; + margin: 0px; +} + +.c6 { + box-sizing: border-box; + display: flex; +} + +.c11 .icon-check { + display: none; +} + +.c11 .icon-copy { + display: block; +} + +.c11.copied .icon-check { + display: block; +} + +.c11.copied .icon-copy { + display: none; +} + +.c10 { + height: 28px; + width: 28px; + border-radius: 20px; + min-height: auto; + padding: 0; + margin-top: -4px; +} + +.c4 { + box-sizing: border-box; + margin-right: 4px; + white-space: pre; + word-break: break-all; + font-size: 12px; + font-family: "Droid Sans Mono","monospace",monospace,"Droid Sans Fallback"; + overflow: scroll; + line-height: 20px; +} + +.c3 { + box-sizing: border-box; + padding-bottom: 8px; + padding-left: 16px; + padding-right: 48px; + padding-top: 8px; + background-color: #010B1C; + border-radius: 4px; + position: relative; +} + +.c8 { + box-sizing: border-box; + padding-right: 16px; + position: absolute; + right: 0px; +} + +.c13 { + overflow: hidden; + text-overflow: ellipsis; + margin: 0px; + display: flex; + align-items: center; +} + +.c13 .icon { + margin-right: 8px; +} + +.c0 { + box-sizing: border-box; + margin-top: 24px; + padding: 16px; + border-radius: 8px; + max-width: 800px; + background-color: rgba(255,255,255,0.05); + padding: 16px; + border-radius: 8px; + border: 2px solid #00bfa5; +} + +
+
+
+ Command +
+
+
+
+
+
+
+
+ $ +
+
+ some kind of command +
+
+
+ +
+
+
+
+
+
+
+ + The Teleport Service successfully join this Teleport cluster +
+
+
+`; diff --git a/web/packages/teleport/src/Discover/Shared/CommandWithTimer/index.ts b/web/packages/teleport/src/Discover/Shared/CommandWithTimer/index.ts new file mode 100644 index 0000000000000..7222a13a06740 --- /dev/null +++ b/web/packages/teleport/src/Discover/Shared/CommandWithTimer/index.ts @@ -0,0 +1,19 @@ +/** + * Copyright 2022 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 { CommandWithTimer } from './CommandWithTimer'; + +export type { Poll } from './CommandWithTimer'; diff --git a/web/packages/teleport/src/Discover/Shared/Finished/Finished.story.tsx b/web/packages/teleport/src/Discover/Shared/Finished/Finished.story.tsx index b90b3a0b163c1..aca86ca31e25e 100644 --- a/web/packages/teleport/src/Discover/Shared/Finished/Finished.story.tsx +++ b/web/packages/teleport/src/Discover/Shared/Finished/Finished.story.tsx @@ -18,15 +18,15 @@ import React from 'react'; import { ResourceKind } from 'teleport/Discover/Shared'; -import { Finished } from './Finished'; +import { Finished as Component } from './Finished'; import type { AgentStepProps } from '../../types'; export default { - title: 'Teleport/Discover/Finished', + title: 'Teleport/Discover/Shared', }; -export const Loaded = () => ; +export const Finished = () => ; const props: AgentStepProps = { agentMeta: { resourceName: 'some-resource-name' } as any, diff --git a/web/packages/teleport/src/Discover/Shared/JoinTokenContext.tsx b/web/packages/teleport/src/Discover/Shared/JoinTokenContext.tsx index 35cd840788ffd..7d336e2ae6b51 100644 --- a/web/packages/teleport/src/Discover/Shared/JoinTokenContext.tsx +++ b/web/packages/teleport/src/Discover/Shared/JoinTokenContext.tsx @@ -7,6 +7,7 @@ import { resourceKindToJoinRole, } from 'teleport/Discover/Shared/ResourceKind'; +import type { AgentLabel } from 'teleport/services/agents'; import type { JoinToken, JoinMethod } from 'teleport/services/joinToken'; interface JoinTokenContextState { @@ -79,7 +80,7 @@ export function useJoinTokenValue() { export function useJoinToken( resourceKind: ResourceKind, - runNow = true, + agentMatcherLabel: AgentLabel[] = [], joinMethod: JoinMethod = 'token' ): { joinToken: JoinToken; @@ -96,9 +97,11 @@ export function useJoinToken( cachedJoinTokenResult = { promise: ctx.joinTokenService .fetchJoinToken( - [resourceKindToJoinRole(resourceKind)], - joinMethod, - [], + { + roles: [resourceKindToJoinRole(resourceKind)], + method: joinMethod, + agentMatcherLabel, + }, abortController.signal ) .then(token => { @@ -131,14 +134,6 @@ export function useJoinToken( }; }, []); - if (!runNow) - return { - joinToken: null, - reloadJoinToken: run, - timedOut: false, - timeout: 0, - }; - if (cachedJoinTokenResult) { if (cachedJoinTokenResult.error) { throw cachedJoinTokenResult.error; diff --git a/web/packages/teleport/src/Discover/Shared/PingTeleportContext.tsx b/web/packages/teleport/src/Discover/Shared/PingTeleportContext.tsx index 499a3cfd7a3c5..8520980d8650a 100644 --- a/web/packages/teleport/src/Discover/Shared/PingTeleportContext.tsx +++ b/web/packages/teleport/src/Discover/Shared/PingTeleportContext.tsx @@ -61,10 +61,11 @@ export function PingTeleportProvider(props: { ); case ResourceKind.Kubernetes: return ctx.kubeService.fetchKubernetes(clusterId, request, signal); + case ResourceKind.Database: + return ctx.databaseService.fetchDatabases(clusterId, request, signal); // TODO (when we start implementing them) // the fetch XXX needs a param defined for abort signal // case 'app': - // case 'db': } } diff --git a/web/packages/teleport/src/Discover/Shared/PollState.tsx b/web/packages/teleport/src/Discover/Shared/PollState.tsx deleted file mode 100644 index 9021fb9e8cf48..0000000000000 --- a/web/packages/teleport/src/Discover/Shared/PollState.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright 2022 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 styled from 'styled-components'; -import { Box } from 'design'; - -export type PollState = 'polling' | 'success' | 'error'; - -export const PollBox = styled(Box)` - max-width: 800px; - background-color: rgba(255, 255, 255, 0.05); - padding: ${props => `${props.theme.space[3]}px`}; - border-radius: ${props => `${props.theme.space[2]}px`}; - border: 2px solid - ${props => { - switch (props.pollState) { - case 'error': - return props.theme.colors.danger; - case 'success': - return props.theme.colors.success; - default: - // polling - return '#2F3659'; - } - }}; -`; diff --git a/web/packages/teleport/src/Discover/Shared/SelectCreatable/SelectCreatable.story.tsx b/web/packages/teleport/src/Discover/Shared/SelectCreatable/SelectCreatable.story.tsx index cf726211fa9cb..fafa5fdcd7a48 100644 --- a/web/packages/teleport/src/Discover/Shared/SelectCreatable/SelectCreatable.story.tsx +++ b/web/packages/teleport/src/Discover/Shared/SelectCreatable/SelectCreatable.story.tsx @@ -19,13 +19,13 @@ import React, { useState } from 'react'; import { SelectCreatable as Component, Option } from './SelectCreatable'; export default { - title: 'Teleport/Discover/SelectCreatable', + title: 'Teleport/Discover/Shared/SelectCreatable', }; const data = ['apple', 'banana', 'carrot']; const fixedData = ['pumpkin', 'watermelon']; -export const SelectCreatable = () => { +export const SelectCreatableWithoutFixed = () => { const [fruitInputValue, setFruitInputValue] = useState(''); const [fruits, setFruits] = useState(() => { return data.map(l => ({ diff --git a/web/packages/teleport/src/Discover/Shared/SetupAccess/SetupAccessWrapper.story.tsx b/web/packages/teleport/src/Discover/Shared/SetupAccess/SetupAccessWrapper.story.tsx index 9cb5d9aedb115..a3ad065d70507 100644 --- a/web/packages/teleport/src/Discover/Shared/SetupAccess/SetupAccessWrapper.story.tsx +++ b/web/packages/teleport/src/Discover/Shared/SetupAccess/SetupAccessWrapper.story.tsx @@ -22,7 +22,7 @@ import { SetupAccessWrapper } from './SetupAccessWrapper'; import type { Props } from './SetupAccessWrapper'; export default { - title: 'Teleport/Discover/SetupAccessContainer', + title: 'Teleport/Discover/Shared/SetupAccessContainer', }; export const HasAccessAndTraits = () => ( diff --git a/web/packages/teleport/src/config.ts b/web/packages/teleport/src/config.ts index f9f45f4d3f483..12d297f362273 100644 --- a/web/packages/teleport/src/config.ts +++ b/web/packages/teleport/src/config.ts @@ -154,6 +154,7 @@ const cfg = { trustedClustersPath: '/v1/webapi/trustedcluster/:name?', joinTokenPath: '/v1/webapi/token', + dbScriptPath: '/scripts/:token/install-database.sh', nodeScriptPath: '/scripts/:token/install-node.sh', appNodeScriptPath: '/scripts/:token/install-app.sh?name=:name&uri=:uri', @@ -272,6 +273,10 @@ const cfg = { return cfg.baseUrl + generatePath(cfg.api.nodeScriptPath, { token }); }, + getDbScriptUrl(token: string) { + return cfg.baseUrl + generatePath(cfg.api.dbScriptPath, { token }); + }, + getConfigureADUrl(token: string) { return cfg.baseUrl + generatePath(cfg.api.configureADPath, { token }); }, diff --git a/web/packages/teleport/src/services/agents/agents.test.ts b/web/packages/teleport/src/services/agents/agents.test.ts index 5d4d3b474c101..3e5a613e35c53 100644 --- a/web/packages/teleport/src/services/agents/agents.test.ts +++ b/web/packages/teleport/src/services/agents/agents.test.ts @@ -18,6 +18,7 @@ import api from 'teleport/services/api'; import cfg from 'teleport/config'; import { agentService } from './agents'; +import { makeLabelMapOfStrArrs } from './make'; import type { ConnectionDiagnosticRequest } from './types'; @@ -60,3 +61,17 @@ test('createConnectionDiagnostic request', () => { }, }); }); + +test('correct makeLabelMapOfStrArrs', () => { + // Test empty param. + let result = makeLabelMapOfStrArrs(); + expect(result).toStrictEqual({}); + + // Test with param. + result = makeLabelMapOfStrArrs([ + { name: 'os', value: 'mac' }, + { name: 'os', value: 'linux' }, + { name: 'env', value: 'prod' }, + ]); + expect(result).toStrictEqual({ os: ['mac', 'linux'], env: ['prod'] }); +}); diff --git a/web/packages/teleport/src/services/agents/make.ts b/web/packages/teleport/src/services/agents/make.ts index 94d634b2bca20..f20cdf8208f2a 100644 --- a/web/packages/teleport/src/services/agents/make.ts +++ b/web/packages/teleport/src/services/agents/make.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import type { ConnectionDiagnostic, ConnectionDiagnosticTrace } from './types'; +import type { + ConnectionDiagnostic, + ConnectionDiagnosticTrace, + AgentLabel, +} from './types'; export function makeConnectionDiagnostic(json: any): ConnectionDiagnostic { json = json || {}; @@ -40,3 +44,19 @@ function makeTraces(traces: any): ConnectionDiagnosticTrace[] { error: t.error, })); } + +// makeLabelMapOfStrArrs converts an array of type AgentLabel into +// a map of string arrays eg: {"os": ["mac", "linux"]} which is the +// data model expected in the backend for labels. +export function makeLabelMapOfStrArrs(labels: AgentLabel[] = []) { + const m: Record = {}; + + labels.forEach(label => { + if (!m[label.name]) { + m[label.name] = []; + } + m[label.name].push(label.value); + }); + + return m; +} diff --git a/web/packages/teleport/src/services/databases/databases.ts b/web/packages/teleport/src/services/databases/databases.ts index 578c90708e58e..4c94f76dc3d5c 100644 --- a/web/packages/teleport/src/services/databases/databases.ts +++ b/web/packages/teleport/src/services/databases/databases.ts @@ -24,17 +24,20 @@ import makeDatabase from './makeDatabase'; class DatabaseService { fetchDatabases( clusterId: string, - params: UrlResourcesParams + params: UrlResourcesParams, + signal?: AbortSignal ): Promise> { - return api.get(cfg.getDatabasesUrl(clusterId, params)).then(json => { - const items = json?.items || []; - - return { - agents: items.map(makeDatabase), - startKey: json?.startKey, - totalCount: json?.totalCount, - }; - }); + return api + .get(cfg.getDatabasesUrl(clusterId, params), signal) + .then(json => { + const items = json?.items || []; + + return { + agents: items.map(makeDatabase), + startKey: json?.startKey, + totalCount: json?.totalCount, + }; + }); } } diff --git a/web/packages/teleport/src/services/joinToken/joinToken.test.ts b/web/packages/teleport/src/services/joinToken/joinToken.test.ts new file mode 100644 index 0000000000000..bb4e95ffc6859 --- /dev/null +++ b/web/packages/teleport/src/services/joinToken/joinToken.test.ts @@ -0,0 +1,63 @@ +/** + * Copyright 2022 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 api from 'teleport/services/api'; +import cfg from 'teleport/config'; + +import JoinTokenService from './joinToken'; + +import type { JoinTokenRequest } from './types'; + +test('fetchJoinToken with an empty request properly sets defaults', () => { + const svc = new JoinTokenService(); + jest.spyOn(api, 'post').mockResolvedValue(null); + + // Test with all empty fields. + svc.fetchJoinToken({} as any); + expect(api.post).toHaveBeenCalledWith( + cfg.getJoinTokenUrl(), + { + roles: undefined, + join_method: 'token', + allow: [], + agent_matcher_labels: {}, + }, + null + ); +}); + +test('fetchJoinToken request fields are set as requested', () => { + const svc = new JoinTokenService(); + jest.spyOn(api, 'post').mockResolvedValue(null); + + const mock: JoinTokenRequest = { + roles: ['Node'], + rules: [{ awsAccountId: '1234', awsArn: 'xxxx' }], + method: 'iam', + agentMatcherLabel: [{ name: 'env', value: 'dev' }], + }; + svc.fetchJoinToken(mock); + expect(api.post).toHaveBeenCalledWith( + cfg.getJoinTokenUrl(), + { + roles: ['Node'], + join_method: 'iam', + allow: [{ aws_account: '1234', aws_arn: 'xxxx' }], + agent_matcher_labels: { env: ['dev'] }, + }, + null + ); +}); diff --git a/web/packages/teleport/src/services/joinToken/joinToken.ts b/web/packages/teleport/src/services/joinToken/joinToken.ts index 0a8350ac65299..38c1dd85e8a82 100644 --- a/web/packages/teleport/src/services/joinToken/joinToken.ts +++ b/web/packages/teleport/src/services/joinToken/joinToken.ts @@ -17,27 +17,24 @@ limitations under the License. import api from 'teleport/services/api'; import cfg from 'teleport/config'; +import { makeLabelMapOfStrArrs } from '../agents/make'; + import makeJoinToken from './makeJoinToken'; -import { JoinToken, JoinMethod, JoinRole, JoinRule } from './types'; +import { JoinToken, JoinRule, JoinTokenRequest } from './types'; class JoinTokenService { fetchJoinToken( - // roles is a list of join roles, since there can be more than - // one role associated with a token. - roles: JoinRole[], - joinMethod: JoinMethod = 'token', - // rules is a list of allow rules associated with the join token - // and the node using this token must match one of the rules. - rules: JoinRule[] = [], + req: JoinTokenRequest, signal: AbortSignal = null ): Promise { return api .post( cfg.getJoinTokenUrl(), { - roles, - join_method: joinMethod, - allow: makeAllowField(rules), + roles: req.roles, + join_method: req.method || 'token', + allow: makeAllowField(req.rules || []), + agent_matcher_labels: makeLabelMapOfStrArrs(req.agentMatcherLabel), }, signal ) @@ -45,7 +42,7 @@ class JoinTokenService { } } -function makeAllowField(rules: JoinRule[]) { +function makeAllowField(rules: JoinRule[] = []) { return rules.map(rule => ({ aws_account: rule.awsAccountId, aws_arn: rule.awsArn, diff --git a/web/packages/teleport/src/services/joinToken/types.ts b/web/packages/teleport/src/services/joinToken/types.ts index 26e9a868835d2..4ce9b5d8bdca7 100644 --- a/web/packages/teleport/src/services/joinToken/types.ts +++ b/web/packages/teleport/src/services/joinToken/types.ts @@ -55,3 +55,18 @@ export type JoinRule = { // awsArn is used for the IAM join method. awsArn?: string; }; + +export type JoinTokenRequest = { + // roles is a list of join roles, since there can be more than + // one role associated with a token. + roles: JoinRole[]; + // rules is a list of allow rules associated with the join token + // and the node using this token must match one of the rules. + rules?: JoinRule[]; + // agentMatcherLabel is a set of labels to be used by agents to match + // on resources. When an agent uses this token, the agent should + // monitor resources that match those labels. For databases, this + // means adding the labels to `db_service.resources.labels`. + agentMatcherLabel?: AgentLabel[]; + method?: JoinMethod; +};