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 = (
<>