diff --git a/web/packages/teleport/src/Apps/AddApp/AddApp.story.tsx b/web/packages/teleport/src/Apps/AddApp/AddApp.story.tsx index db9ba0c4007ba..4ae3007934307 100644 --- a/web/packages/teleport/src/Apps/AddApp/AddApp.story.tsx +++ b/web/packages/teleport/src/Apps/AddApp/AddApp.story.tsx @@ -16,18 +16,50 @@ * along with this program. If not, see . */ +import { useState } from 'react'; + +import { JoinToken } from 'teleport/services/joinToken'; + import { AddApp } from './AddApp'; export default { - title: 'Teleport/Apps/Add', + title: 'Teleport/Discover/Application/Web', }; -export const Created = () => ( - -); +export const CreatedWithoutLabels = () => { + const [token, setToken] = useState(); + + return ( + { + setToken(props.token); + return Promise.resolve(true); + }} + /> + ); +}; + +export const CreatedWithLabels = () => { + const [token, setToken] = useState(); -export const Loaded = () => { - return ; + return ( + { + setToken(props.token); + return Promise.resolve(true); + }} + /> + ); }; export const Processing = () => ( @@ -72,8 +104,10 @@ const props = { createJoinToken: () => Promise.resolve(null), version: '5.0.0-dev', reset: () => null, + labels: [], + setLabels: () => null, attempt: { - status: '', + status: 'success', statusText: '', } as any, token: { diff --git a/web/packages/teleport/src/Apps/AddApp/AddApp.tsx b/web/packages/teleport/src/Apps/AddApp/AddApp.tsx index b40735fbce53d..7a82293d33a7a 100644 --- a/web/packages/teleport/src/Apps/AddApp/AddApp.tsx +++ b/web/packages/teleport/src/Apps/AddApp/AddApp.tsx @@ -44,6 +44,8 @@ export function AddApp({ setAutomatic, isAuthTypeLocal, token, + labels, + setLabels, }: State & Props) { return ( )} {!automatic && ( diff --git a/web/packages/teleport/src/Apps/AddApp/Automatically.test.tsx b/web/packages/teleport/src/Apps/AddApp/Automatically.test.tsx index 5761abdbcb42f..f8215d02405db 100644 --- a/web/packages/teleport/src/Apps/AddApp/Automatically.test.tsx +++ b/web/packages/teleport/src/Apps/AddApp/Automatically.test.tsx @@ -39,6 +39,8 @@ test('render command only after form submit', async () => { attempt={{ status: 'success' }} onClose={() => {}} onCreate={() => Promise.resolve(true)} + labels={[]} + setLabels={() => null} /> ); diff --git a/web/packages/teleport/src/Apps/AddApp/Automatically.tsx b/web/packages/teleport/src/Apps/AddApp/Automatically.tsx index de6669284f1ce..6e49916ef1261 100644 --- a/web/packages/teleport/src/Apps/AddApp/Automatically.tsx +++ b/web/packages/teleport/src/Apps/AddApp/Automatically.tsx @@ -20,6 +20,7 @@ import { KeyboardEvent, useEffect, useState } from 'react'; import { Alert, + Box, ButtonPrimary, ButtonSecondary, Flex, @@ -33,24 +34,27 @@ import { Attempt } from 'shared/hooks/useAttemptNext'; import TextSelectCopy from 'teleport/components/TextSelectCopy'; import cfg from 'teleport/config'; +import { LabelsCreater } from 'teleport/Discover/Shared'; +import { ResourceLabelTooltip } from 'teleport/Discover/Shared/ResourceLabelTooltip'; +import { ResourceLabel } from 'teleport/services/agents'; import { State } from './useAddApp'; export function Automatically(props: Props) { - const { onClose, attempt, token } = props; + const { onClose, attempt, token, labels, setLabels } = props; const [name, setName] = useState(''); const [uri, setUri] = useState(''); const [cmd, setCmd] = useState(''); useEffect(() => { - if (name && uri) { + if (name && uri && token) { const cmd = createAppBashCommand(token.id, name, uri); setCmd(cmd); } }, [token]); - function handleRegenerate(validator: Validator) { + function onGenerateScript(validator: Validator) { if (!validator.validate()) { return; } @@ -58,25 +62,12 @@ export function Automatically(props: Props) { props.onCreate(name, uri); } - function handleGenerate(validator: Validator) { - if (!validator.validate()) { - return; - } - - const cmd = createAppBashCommand(token.id, name, uri); - setCmd(cmd); - } - function handleEnterPress( e: KeyboardEvent, validator: Validator ) { if (e.key === 'Enter') { - if (cmd) { - handleRegenerate(validator); - } else { - handleGenerate(validator); - } + onGenerateScript(validator); } } @@ -96,6 +87,7 @@ export function Automatically(props: Props) { mr="3" onKeyPress={e => handleEnterPress(e, validator)} onChange={e => setName(e.target.value.toLowerCase())} + disabled={attempt.status === 'processing'} /> handleEnterPress(e, validator)} onChange={e => setUri(e.target.value)} + disabled={attempt.status === 'processing'} /> + + + Add Labels (Optional) + + + + {!cmd && ( Teleport can automatically set up application access. Provide @@ -136,24 +145,13 @@ export function Automatically(props: Props) { )} - {!cmd && ( - handleGenerate(validator)} - > - Generate Script - - )} - {cmd && ( - handleRegenerate(validator)} - > - Regenerate - - )} + onGenerateScript(validator)} + > + {cmd ? 'Regenerate Script' : 'Generate Script'} + ; token: State['token']; attempt: Attempt; + labels: ResourceLabel[]; + setLabels(r: ResourceLabel[]): void; }; diff --git a/web/packages/teleport/src/Apps/AddApp/useAddApp.ts b/web/packages/teleport/src/Apps/AddApp/useAddApp.ts index be04b6cba17fd..cad6afd65c95c 100644 --- a/web/packages/teleport/src/Apps/AddApp/useAddApp.ts +++ b/web/packages/teleport/src/Apps/AddApp/useAddApp.ts @@ -20,6 +20,7 @@ import { useEffect, useState } from 'react'; import useAttempt from 'shared/hooks/useAttemptNext'; +import { ResourceLabel } from 'teleport/services/agents'; import type { JoinToken } from 'teleport/services/joinToken'; import TeleportContext from 'teleport/teleportContext'; @@ -31,14 +32,27 @@ export default function useAddApp(ctx: TeleportContext) { const isEnterprise = ctx.isEnterprise; const [automatic, setAutomatic] = useState(isEnterprise); const [token, setToken] = useState(); + const [labels, setLabels] = useState([]); useEffect(() => { - createToken(); - }, []); + // We don't want to create token on first render + // which defaults to the automatic tab because + // user may want to add labels. + if (!automatic) { + setLabels([]); + // When switching to manual tab, token can be re-used + // if token was already generated from automatic tab. + if (!token) { + createToken(); + } + } + }, [automatic]); function createToken() { return run(() => - ctx.joinTokenService.fetchJoinToken({ roles: ['App'] }).then(setToken) + ctx.joinTokenService + .fetchJoinToken({ roles: ['App'], suggestedLabels: labels }) + .then(setToken) ); } @@ -52,6 +66,8 @@ export default function useAddApp(ctx: TeleportContext) { isAuthTypeLocal, isEnterprise, token, + labels, + setLabels, }; } diff --git a/web/packages/teleport/src/Discover/Shared/ResourceLabelTooltip/ResourceLabelTooltip.tsx b/web/packages/teleport/src/Discover/Shared/ResourceLabelTooltip/ResourceLabelTooltip.tsx index 4feb605ae4692..f0d5ddc8abf5e 100644 --- a/web/packages/teleport/src/Discover/Shared/ResourceLabelTooltip/ResourceLabelTooltip.tsx +++ b/web/packages/teleport/src/Discover/Shared/ResourceLabelTooltip/ResourceLabelTooltip.tsx @@ -37,12 +37,36 @@ export function ResourceLabelTooltip({ resourceKind, toolTipPosition, }: { - resourceKind: 'server' | 'eks' | 'rds' | 'kube' | 'db'; + resourceKind: 'server' | 'eks' | 'rds' | 'kube' | 'db' | 'app'; toolTipPosition?: Position; }) { let tip; switch (resourceKind) { + case 'app': { + tip = ( + <> + Labels allow you to do the following: +
    +
  • + Filter applications by labels when using tsh, tctl, or the web UI. +
  • +
  • + Restrict access to this application with{' '} + + Teleport RBAC + + . Only roles with app_labels that match + these labels will be allowed to access this application. +
  • +
+ + ); + break; + } case 'server': { tip = ( <>