diff --git a/web/packages/design/src/ResourceIcon/assets/laptop.svg b/web/packages/design/src/ResourceIcon/assets/laptop.svg
new file mode 100644
index 0000000000000..ac0d8fe9b88a1
--- /dev/null
+++ b/web/packages/design/src/ResourceIcon/assets/laptop.svg
@@ -0,0 +1,310 @@
+
diff --git a/web/packages/design/src/ResourceIcon/index.tsx b/web/packages/design/src/ResourceIcon/index.tsx
index c0a9c54d89cb0..9c6eb43b48e0d 100644
--- a/web/packages/design/src/ResourceIcon/index.tsx
+++ b/web/packages/design/src/ResourceIcon/index.tsx
@@ -36,6 +36,7 @@ import gcp from './assets/gcp.svg';
import grafana from './assets/grafana.svg';
import jenkins from './assets/jenkins.svg';
import kube from './assets/kube.svg';
+import laptop from './assets/laptop.svg';
import linuxDark from './assets/linux-dark.svg';
import linuxLight from './assets/linux-light.svg';
import mongoDark from './assets/mongo-dark.svg';
@@ -91,6 +92,7 @@ const iconSpecs = {
Grafana: forAllThemes(grafana),
Jenkins: forAllThemes(jenkins),
Kube: forAllThemes(kube),
+ Laptop: forAllThemes(laptop),
Linux: { dark: linuxDark, light: linuxLight },
Mongo: { dark: mongoDark, light: mongoLight },
MysqlLarge: { dark: mysqlLargeDark, light: mysqlLargeLight },
diff --git a/web/packages/teleport/src/Discover/Discover.test.tsx b/web/packages/teleport/src/Discover/Discover.test.tsx
index ec46c1dac965e..d8e40b0860591 100644
--- a/web/packages/teleport/src/Discover/Discover.test.tsx
+++ b/web/packages/teleport/src/Discover/Discover.test.tsx
@@ -47,12 +47,18 @@ import { makeDefaultUserPreferences } from 'teleport/services/userPreferences/us
import { ResourceKind } from './Shared';
+beforeEach(() => {
+ jest.restoreAllMocks();
+});
+
type createProps = {
initialEntry?: string;
preferredResource?: ClusterResource;
};
const create = ({ initialEntry = '', preferredResource }: createProps) => {
+ jest.spyOn(window.navigator, 'userAgent', 'get').mockReturnValue('Macintosh');
+
const defaultPref = makeDefaultUserPreferences();
defaultPref.onboard.preferredResources = preferredResource
? [preferredResource]
@@ -83,9 +89,11 @@ const create = ({ initialEntry = '', preferredResource }: createProps) => {
test('displays all resources by default', () => {
create({});
- expect(screen.getAllByTestId(ResourceKind.Server)).toHaveLength(
- SERVERS.length
- );
+ expect(
+ screen.getAllByTestId(ResourceKind.Server)
+ // TODO(ravicious): Uncomment for v14.2.
+ // .concat(screen.getAllByTestId(ResourceKind.ConnectMyComputer))
+ ).toHaveLength(SERVERS.length);
expect(screen.getAllByTestId(ResourceKind.Desktop)).toHaveLength(
WINDOWS_DESKTOPS.length
);
@@ -122,9 +130,11 @@ describe('location state', () => {
test('displays servers when the location state is server', () => {
create({ initialEntry: 'server' });
- expect(screen.getAllByTestId(ResourceKind.Server)).toHaveLength(
- SERVERS.length
- );
+ expect(
+ screen.getAllByTestId(ResourceKind.Server)
+ // TODO(ravicious): Uncomment for v14.2.
+ // .concat(screen.getAllByTestId(ResourceKind.ConnectMyComputer))
+ ).toHaveLength(SERVERS.length);
// we assert three databases for servers because the naming convention includes "server"
expect(screen.queryAllByTestId(ResourceKind.Database)).toHaveLength(3);
diff --git a/web/packages/teleport/src/Discover/SelectResource/SelectResource.test.tsx b/web/packages/teleport/src/Discover/SelectResource/SelectResource.test.tsx
index 7af80e6654063..e9843681cf04d 100644
--- a/web/packages/teleport/src/Discover/SelectResource/SelectResource.test.tsx
+++ b/web/packages/teleport/src/Discover/SelectResource/SelectResource.test.tsx
@@ -22,10 +22,12 @@ import {
ClusterResource,
OnboardUserPreferences,
} from 'teleport/services/userPreferences/types';
+import { OnboardDiscover } from 'teleport/services/user';
import { ResourceKind } from '../Shared';
+import { resourceKindToPreferredResource } from '../Shared/ResourceKind';
-import { sortResources } from './SelectResource';
+import { filterResources, sortResources } from './SelectResource';
import { ResourceSpec } from './types';
const setUp = () => {
@@ -50,6 +52,25 @@ const makeResourceSpec = (
);
};
+/**
+ * If the user has resources, Connect My Computer is not prioritized when sorting resources.
+ */
+const onboardDiscoverWithResources: OnboardDiscover = {
+ hasResource: true,
+ notified: true,
+ hasVisited: true,
+};
+/**
+ * If the user does not have resources, Connect My Computer is prioritized as long as it was not
+ * filtered out based on supported platforms and auth types and the user either has no preferences
+ * or prefers servers.
+ */
+const onboardDiscoverNoResources: OnboardDiscover = {
+ hasResource: false,
+ notified: true,
+ hasVisited: false,
+};
+
test('sortResources without preferred resources, sorts resources alphabetically with guided resources first', () => {
setUp();
const mockIn: ResourceSpec[] = [
@@ -64,7 +85,11 @@ test('sortResources without preferred resources, sorts resources alphabetically
makeResourceSpec({ name: 'costco' }),
];
- const actual = sortResources(mockIn, makeDefaultUserPreferences());
+ const actual = sortResources(
+ mockIn,
+ makeDefaultUserPreferences(),
+ onboardDiscoverWithResources
+ );
expect(actual).toMatchObject([
// guided and alpha
@@ -319,7 +344,11 @@ describe('preferred resources', () => {
test.each(testCases)('$name', testCase => {
const preferences = makeDefaultUserPreferences();
preferences.onboard.preferredResources = testCase.preferred;
- const actual = sortResources(kindBasedList, preferences);
+ const actual = sortResources(
+ kindBasedList,
+ preferences,
+ onboardDiscoverWithResources
+ );
expect(actual).toMatchObject(testCase.expected);
});
@@ -520,7 +549,11 @@ describe('marketing params', () => {
test.each(testCases)('$name', testCase => {
const preferences = makeDefaultUserPreferences();
preferences.onboard = testCase.preferred;
- const actual = sortResources(kindBasedList, preferences);
+ const actual = sortResources(
+ kindBasedList,
+ preferences,
+ onboardDiscoverWithResources
+ );
expect(actual).toMatchObject(testCase.expected);
});
@@ -660,7 +693,11 @@ describe('os sorted resources', () => {
test.each(testCases)('$name', testCase => {
OS.mockReturnValue(testCase.userAgent);
- const actual = sortResources(osBasedList, makeDefaultUserPreferences());
+ const actual = sortResources(
+ osBasedList,
+ makeDefaultUserPreferences(),
+ onboardDiscoverWithResources
+ );
expect(actual).toMatchObject(testCase.expected);
});
@@ -675,7 +712,11 @@ describe('os sorted resources', () => {
];
OS.mockReturnValue(UserAgent.macOS);
- const actual = sortResources(mockIn, makeDefaultUserPreferences());
+ const actual = sortResources(
+ mockIn,
+ makeDefaultUserPreferences(),
+ onboardDiscoverWithResources
+ );
expect(actual).toMatchObject([
makeResourceSpec({ name: 'Aaaa' }),
makeResourceSpec({
@@ -707,7 +748,9 @@ describe('os sorted resources', () => {
OS.mockReturnValue(UserAgent.macOS);
const preferences = makeDefaultUserPreferences();
preferences.onboard = {
- preferredResources: [2],
+ preferredResources: [
+ resourceKindToPreferredResource(ResourceKind.Server),
+ ],
marketingParams: {
campaign: '',
source: '',
@@ -716,7 +759,11 @@ describe('os sorted resources', () => {
},
};
- const actual = sortResources(oneOfEachList, preferences);
+ const actual = sortResources(
+ oneOfEachList,
+ preferences,
+ onboardDiscoverWithResources
+ );
expect(actual).toMatchObject([
// 1. OS
makeResourceSpec({
@@ -740,3 +787,414 @@ describe('os sorted resources', () => {
]);
});
});
+
+describe('sorting Connect My Computer', () => {
+ let OS: jest.SpyInstance;
+
+ beforeEach(() => {
+ OS = jest.spyOn(window.navigator, 'userAgent', 'get');
+ });
+
+ const connectMyComputer = makeResourceSpec({
+ kind: ResourceKind.ConnectMyComputer,
+ name: 'Connect My Computer',
+ });
+ const noAccessServerForMatchingPlatform = makeResourceSpec({
+ name: 'no access but platform matches',
+ hasAccess: false,
+ platform: Platform.macOS,
+ kind: ResourceKind.Server,
+ });
+ const guidedA = makeResourceSpec({ name: 'guided' });
+ const guidedB = makeResourceSpec({ name: 'guidedB' });
+ const unguidedA = makeResourceSpec({
+ name: 'unguidedA',
+ unguidedLink: 'test.com',
+ });
+ const unguidedB = makeResourceSpec({
+ name: 'unguidedB',
+ unguidedLink: 'test.com',
+ });
+ const platformMatch = makeResourceSpec({
+ name: 'platform match',
+ platform: Platform.macOS,
+ });
+ const server = makeResourceSpec({
+ name: 'server',
+ kind: ResourceKind.Server,
+ });
+
+ const oneOfEachList = [
+ noAccessServerForMatchingPlatform,
+ guidedB,
+ unguidedB,
+ guidedA,
+ unguidedA,
+ platformMatch,
+ server,
+ connectMyComputer,
+ ];
+
+ describe('prioritizing Connect My Computer', () => {
+ it('puts the Connect My Computer resource as the first resource if the user has no preferences', () => {
+ OS.mockReturnValue(UserAgent.macOS);
+
+ const actual = sortResources(
+ oneOfEachList,
+ makeDefaultUserPreferences(),
+ onboardDiscoverNoResources
+ );
+
+ expect(actual).toMatchObject([
+ // 1. Connect My Computer
+ connectMyComputer,
+ // 2. OS
+ platformMatch,
+ // 3. guided
+ guidedA,
+ guidedB,
+ server,
+ // 4. alpha
+ unguidedA,
+ unguidedB,
+ // 5. no access
+ noAccessServerForMatchingPlatform,
+ ]);
+ });
+
+ it('puts the Connect My Computer resource as the first resource if the user prefers servers', () => {
+ OS.mockReturnValue(UserAgent.macOS);
+
+ const preferences = makeDefaultUserPreferences();
+ preferences.onboard = {
+ preferredResources: [
+ resourceKindToPreferredResource(ResourceKind.Server),
+ ],
+ marketingParams: {
+ campaign: '',
+ source: '',
+ medium: '',
+ intent: '',
+ },
+ };
+
+ const actual = sortResources(
+ oneOfEachList,
+ preferences,
+ onboardDiscoverNoResources
+ );
+
+ expect(actual).toMatchObject([
+ // 1. Connect My Computer
+ connectMyComputer,
+ // 2. OS
+ platformMatch,
+ // 3. preferred
+ server,
+ // 4. guided
+ guidedA,
+ guidedB,
+ // 5. alpha
+ unguidedA,
+ unguidedB,
+ // 6. no access is last
+ noAccessServerForMatchingPlatform,
+ ]);
+ });
+
+ it('deprioritizes other server tiles of the matching platform within the guided resources if the user does not prefer servers', () => {
+ OS.mockReturnValue(UserAgent.macOS);
+
+ const guidedServerForMatchingPlatformA = makeResourceSpec({
+ name: 'guided server for matching platform A',
+ kind: ResourceKind.Server,
+ platform: Platform.macOS,
+ });
+ const guidedServerForMatchingPlatformB = makeResourceSpec({
+ name: 'guided server for matching platform B',
+ kind: ResourceKind.Server,
+ platform: Platform.macOS,
+ });
+ const guidedServerForAnotherPlatform = makeResourceSpec({
+ name: 'guided server for another platform',
+ kind: ResourceKind.Server,
+ platform: Platform.Linux,
+ });
+
+ const actual = sortResources(
+ [
+ unguidedA,
+ guidedServerForMatchingPlatformB,
+ guidedServerForMatchingPlatformA,
+ guidedServerForAnotherPlatform,
+ connectMyComputer,
+ ],
+ makeDefaultUserPreferences(),
+ onboardDiscoverNoResources
+ );
+
+ expect(actual).toMatchObject([
+ connectMyComputer,
+ guidedServerForAnotherPlatform,
+ guidedServerForMatchingPlatformA,
+ guidedServerForMatchingPlatformB,
+ unguidedA,
+ ]);
+ });
+
+ it('does not deprioritize server tiles of the matching platform if the user prefers servers,', () => {
+ OS.mockReturnValue(UserAgent.macOS);
+
+ const guidedServerForMatchingPlatformA = makeResourceSpec({
+ name: 'guided server for matching platform A',
+ kind: ResourceKind.Server,
+ platform: Platform.macOS,
+ });
+ const guidedServerForMatchingPlatformB = makeResourceSpec({
+ name: 'guided server for matching platform B',
+ kind: ResourceKind.Server,
+ platform: Platform.macOS,
+ });
+ const guidedServerForAnotherPlatform = makeResourceSpec({
+ name: 'guided server for another platform',
+ kind: ResourceKind.Server,
+ platform: Platform.Linux,
+ });
+
+ const preferences = makeDefaultUserPreferences();
+ preferences.onboard = {
+ preferredResources: [
+ resourceKindToPreferredResource(ResourceKind.Server),
+ ],
+ marketingParams: {
+ campaign: '',
+ source: '',
+ medium: '',
+ intent: '',
+ },
+ };
+
+ const actual = sortResources(
+ [
+ unguidedA,
+ guidedServerForMatchingPlatformB,
+ guidedServerForMatchingPlatformA,
+ guidedServerForAnotherPlatform,
+ connectMyComputer,
+ ],
+ preferences,
+ onboardDiscoverNoResources
+ );
+
+ expect(actual).toMatchObject([
+ connectMyComputer,
+ guidedServerForMatchingPlatformA,
+ guidedServerForMatchingPlatformB,
+ guidedServerForAnotherPlatform,
+ unguidedA,
+ ]);
+ });
+ });
+
+ describe('deprioritizing Connect My Computer', () => {
+ it('puts the Connect My Computer resource as the last guided resource if the user has resources', () => {
+ OS.mockReturnValue(UserAgent.macOS);
+
+ const actual = sortResources(
+ oneOfEachList,
+ makeDefaultUserPreferences(),
+ onboardDiscoverWithResources
+ );
+
+ expect(actual).toMatchObject([
+ // 1. OS
+ platformMatch,
+ // 2. guided
+ guidedA,
+ guidedB,
+ server,
+ // 3. Connect My Computer
+ connectMyComputer,
+ // 4. alpha
+ unguidedA,
+ unguidedB,
+ // 5. no access
+ noAccessServerForMatchingPlatform,
+ ]);
+ });
+
+ it('puts the Connect My Computer resource as the last guided resource if the user has resources, even if the user prefers servers', () => {
+ OS.mockReturnValue(UserAgent.macOS);
+
+ const preferences = makeDefaultUserPreferences();
+ preferences.onboard = {
+ preferredResources: [
+ resourceKindToPreferredResource(ResourceKind.Server),
+ ],
+ marketingParams: {
+ campaign: '',
+ source: '',
+ medium: '',
+ intent: '',
+ },
+ };
+
+ const actual = sortResources(
+ oneOfEachList,
+ preferences,
+ onboardDiscoverWithResources
+ );
+
+ expect(actual).toMatchObject([
+ // 1. OS
+ platformMatch,
+ // 2. preferred
+ server,
+ // 2. guided
+ guidedA,
+ guidedB,
+ // 3. Connect My Computer,
+ connectMyComputer,
+ // 4. alpha
+ unguidedA,
+ unguidedB,
+ // 6. no access is last
+ noAccessServerForMatchingPlatform,
+ ]);
+ });
+
+ it('puts the Connect My Computer resource as the last guided resource if the user has no resources but they prefer other resources than servers', () => {
+ OS.mockReturnValue(UserAgent.macOS);
+
+ const databaseForAnotherPlatform = makeResourceSpec({
+ name: 'database for another platform',
+ kind: ResourceKind.Database,
+ platform: Platform.Linux,
+ });
+
+ const preferences = makeDefaultUserPreferences();
+ preferences.onboard = {
+ preferredResources: [
+ resourceKindToPreferredResource(ResourceKind.Database),
+ ],
+ marketingParams: {
+ campaign: '',
+ source: '',
+ medium: '',
+ intent: '',
+ },
+ };
+
+ const actual = sortResources(
+ [...oneOfEachList, databaseForAnotherPlatform],
+ preferences,
+ onboardDiscoverNoResources
+ );
+
+ expect(actual).toMatchObject([
+ // 1. OS
+ platformMatch,
+ // 2. preferred
+ databaseForAnotherPlatform,
+ // 2. guided
+ guidedA,
+ guidedB,
+ server,
+ // 3. Connect My Computer,
+ connectMyComputer,
+ // 4. alpha
+ unguidedA,
+ unguidedB,
+ // 6. no access is last
+ noAccessServerForMatchingPlatform,
+ ]);
+ });
+ });
+});
+
+describe('filterResources', () => {
+ it('filters out resources based on supportedPlatforms', () => {
+ const winAndLinux = makeResourceSpec({
+ name: 'Filtered out with many supported platforms',
+ supportedPlatforms: [Platform.Windows, Platform.Linux],
+ });
+ const win = makeResourceSpec({
+ name: 'Filtered out with one supported platform',
+ supportedPlatforms: [Platform.Windows],
+ });
+ const macosAndLinux = makeResourceSpec({
+ name: 'Kept with many supported platforms',
+ supportedPlatforms: [Platform.macOS, Platform.Linux],
+ });
+ const macos = makeResourceSpec({
+ name: 'Kept with one supported platform',
+ supportedPlatforms: [Platform.macOS],
+ });
+
+ const result = filterResources(Platform.macOS, 'local', [
+ winAndLinux,
+ win,
+ macosAndLinux,
+ macos,
+ ]);
+
+ expect(result).toContain(macosAndLinux);
+ expect(result).toContain(macos);
+ expect(result).not.toContain(winAndLinux);
+ expect(result).not.toContain(win);
+ });
+
+ it('does not filter out resources with supportedPlatforms and supportedAuthTypes that are missing or empty', () => {
+ const result = filterResources(Platform.macOS, 'local', [
+ makeResourceSpec({
+ name: 'Empty supportedPlatforms',
+ supportedPlatforms: [],
+ }),
+ makeResourceSpec({
+ name: 'Missing supportedPlatforms',
+ supportedPlatforms: undefined,
+ }),
+ makeResourceSpec({
+ name: 'Empty supportedAuthTypes',
+ supportedAuthTypes: [],
+ }),
+ makeResourceSpec({
+ name: 'Missing supportedAuthTypes',
+ supportedAuthTypes: undefined,
+ }),
+ ]);
+
+ expect(result).toHaveLength(4);
+ });
+
+ it('filters out resources based on supportedAuthTypes', () => {
+ const ssoAndPasswordless = makeResourceSpec({
+ name: 'Filtered out with many supported auth types',
+ supportedAuthTypes: ['sso', 'passwordless'],
+ });
+ const sso = makeResourceSpec({
+ name: 'Filtered out with one supported auth type',
+ supportedAuthTypes: ['sso'],
+ });
+ const localAndPasswordless = makeResourceSpec({
+ name: 'Kept with many supported auth types',
+ supportedAuthTypes: ['local', 'passwordless'],
+ });
+ const local = makeResourceSpec({
+ name: 'Kept with one supported auth type',
+ supportedAuthTypes: ['local'],
+ });
+
+ const result = filterResources(Platform.macOS, 'local', [
+ ssoAndPasswordless,
+ sso,
+ localAndPasswordless,
+ local,
+ ]);
+
+ expect(result).toContain(localAndPasswordless);
+ expect(result).toContain(local);
+ expect(result).not.toContain(ssoAndPasswordless);
+ expect(result).not.toContain(sso);
+ });
+});
diff --git a/web/packages/teleport/src/Discover/SelectResource/SelectResource.tsx b/web/packages/teleport/src/Discover/SelectResource/SelectResource.tsx
index 3698219ce7bb3..d7016c93edf33 100644
--- a/web/packages/teleport/src/Discover/SelectResource/SelectResource.tsx
+++ b/web/packages/teleport/src/Discover/SelectResource/SelectResource.tsx
@@ -20,12 +20,11 @@ import { useHistory, useLocation } from 'react-router';
import * as Icons from 'design/Icon';
import styled from 'styled-components';
import { Box, Flex, Link, Text } from 'design';
-
-import { getPlatformType, Platform } from 'design/platform';
+import { getPlatform, Platform } from 'design/platform';
import useTeleport from 'teleport/useTeleport';
import { ToolTipNoPermBadge } from 'teleport/components/ToolTipNoPermBadge';
-import { Acl } from 'teleport/services/user';
+import { Acl, AuthType, OnboardDiscover } from 'teleport/services/user';
import {
Header,
HeaderSubtitle,
@@ -38,6 +37,7 @@ import {
} from 'teleport/Discover/SelectResource/resources';
import AddApp from 'teleport/Apps/AddApp';
import { useUser } from 'teleport/User/UserContext';
+import localStorage from 'teleport/services/localStorage';
import {
ClusterResource,
@@ -94,11 +94,18 @@ export function SelectResource({ onSelect }: SelectResourceProps) {
useEffect(() => {
// Apply access check to each resource.
const userContext = ctx.storeUser.state;
- const { acl } = userContext;
+ const { acl, authType } = userContext;
+ const platform = getPlatform();
+ const resources = addHasAccessField(
+ acl,
+ filterResources(platform, authType, RESOURCES)
+ );
+ const onboardDiscover = localStorage.getOnboardDiscover();
const sortedResources = sortResources(
- makeResourcesWithHasAccessField(acl),
- preferences
+ resources,
+ preferences,
+ onboardDiscover
);
setDefaultResources(sortedResources);
@@ -301,6 +308,10 @@ function checkHasAccess(acl: Acl, resourceKind: ResourceKind) {
return acl.nodes.list;
case ResourceKind.SamlApplication:
return acl.samlIdpServiceProvider.create;
+ case ResourceKind.ConnectMyComputer:
+ // This is probably already true since without this permission the user wouldn't be able to
+ // add any other resource, but let's just leave it for completeness sake.
+ return acl.tokens.create;
default:
return false;
}
@@ -346,67 +357,135 @@ function sortResourcesByKind(
return sorted;
}
+const aBeforeB = -1;
+const aAfterB = 1;
+const aEqualsB = 0;
+
+/**
+ * Evaluates the predicate and prioritizes the element matching the predicate over the element that
+ * doesn't.
+ *
+ * @example
+ * comparePredicate({color: 'green'}, {color: 'red'}, (el) => el.color === 'green') // => -1 (a before b)
+ * comparePredicate({color: 'red'}, {color: 'green'}, (el) => el.color === 'green') // => 1 (a after b)
+ * comparePredicate({color: 'blue'}, {color: 'pink'}, (el) => el.color === 'green') // => 0 (both are equal)
+ */
+function comparePredicate(
+ a: ElementType,
+ b: ElementType,
+ predicate: (resource: ElementType) => boolean
+): -1 | 0 | 1 {
+ const aMatches = predicate(a);
+ const bMatches = predicate(b);
+
+ if (aMatches && !bMatches) {
+ return aBeforeB;
+ }
+
+ if (bMatches && !aMatches) {
+ return aAfterB;
+ }
+
+ return aEqualsB;
+}
+
export function sortResources(
resources: ResourceSpec[],
- preferences: UserPreferences
+ preferences: UserPreferences,
+ onboardDiscover: OnboardDiscover | undefined
) {
const { preferredResources, hasPreferredResources } =
getPrioritizedResources(preferences);
+ const platform = getPlatform();
const sortedResources = [...resources];
const accessible = sortedResources.filter(r => r.hasAccess);
const restricted = sortedResources.filter(r => !r.hasAccess);
- // Sort accessible resources by 1. os 2. preferred 3. guided and 4. alphabetically
+ const hasNoResources = onboardDiscover && !onboardDiscover.hasResource;
+ const prefersServers =
+ hasPreferredResources &&
+ preferredResources.includes(
+ resourceKindToPreferredResource(ResourceKind.Server)
+ );
+ const prefersServersOrNoPreferences =
+ prefersServers || !hasPreferredResources;
+ const shouldShowConnectMyComputerFirst =
+ hasNoResources &&
+ prefersServersOrNoPreferences &&
+ isConnectMyComputerAvailable(accessible);
+
+ // Sort accessible resources by:
+ // 1. os
+ // 2. preferred
+ // 3. guided
+ // 4. alphabetically
+ //
+ // When available on the given platform, Connect My Computer is put either as the first resource
+ // if the user has no resources, otherwise it's at the end of the guided group.
accessible.sort((a, b) => {
- let aPreferred,
- bPreferred = false;
- if (hasPreferredResources) {
- aPreferred = preferredResources.includes(
- resourceKindToPreferredResource(a.kind)
- );
- bPreferred = preferredResources.includes(
- resourceKindToPreferredResource(b.kind)
+ const compareAB = (predicate: (r: ResourceSpec) => boolean) =>
+ comparePredicate(a, b, predicate);
+ const areBothGuided = !a.unguidedLink && !b.unguidedLink;
+
+ // Special cases for Connect My Computer.
+ // Show Connect My Computer tile as the first resource.
+ if (shouldShowConnectMyComputerFirst) {
+ const prioritizeConnectMyComputer = compareAB(
+ r => r.kind === ResourceKind.ConnectMyComputer
);
- }
+ if (prioritizeConnectMyComputer) {
+ return prioritizeConnectMyComputer;
+ }
- let platform: string;
- const platformType = getPlatformType();
- if (platformType.isMac) {
- platform = Platform.macOS;
- }
- if (platformType.isLinux) {
- platform = Platform.Linux;
- }
- if (platformType.isWin) {
- platform = Platform.Windows;
+ // Within the guided group, deprioritize server tiles of the current user platform if Connect
+ // My Computer is available.
+ //
+ // If the user has no resources available in the cluster, we want to nudge them towards
+ // Connect My Computer rather than, say, standalone macOS setup.
+ //
+ // Only do this if the user doesn't explicitly prefer servers. If they prefer servers, we
+ // want the servers for their platform to be displayed in their usual place so that the user
+ // doesn't miss that Teleport supports them.
+ if (!prefersServers && areBothGuided) {
+ const deprioritizeServerForUserPlatform = compareAB(
+ r => !(r.kind == ResourceKind.Server && r.platform === platform)
+ );
+ if (deprioritizeServerForUserPlatform) {
+ return deprioritizeServerForUserPlatform;
+ }
+ }
+ } else if (areBothGuided) {
+ // Show Connect My Computer tile as the last guided resource if the user already added some
+ // resources or they prefer other kinds of resources than servers.
+ const deprioritizeConnectMyComputer = compareAB(
+ r => r.kind !== ResourceKind.ConnectMyComputer
+ );
+ if (deprioritizeConnectMyComputer) {
+ return deprioritizeConnectMyComputer;
+ }
}
// Display platform resources first
- if (a.platform === platform && b.platform !== platform) {
- return -1;
- }
- if (a.platform !== platform && b.platform === platform) {
- return 1;
+ const prioritizeUserPlatform = compareAB(r => r.platform === platform);
+ if (prioritizeUserPlatform) {
+ return prioritizeUserPlatform;
}
// Display preferred resources second
- if (aPreferred && !bPreferred) {
- return -1;
- }
- if (!aPreferred && bPreferred) {
- return 1;
+ if (hasPreferredResources) {
+ const prioritizePreferredResource = compareAB(r =>
+ preferredResources.includes(resourceKindToPreferredResource(r.kind))
+ );
+ if (prioritizePreferredResource) {
+ return prioritizePreferredResource;
+ }
}
// Display guided resources third
- if (!a.unguidedLink && !b.unguidedLink) {
- return a.name.localeCompare(b.name);
- }
- if (!b.unguidedLink) {
- return 1;
- }
- if (!a.unguidedLink) {
- return -1;
+ const prioritizeGuided = compareAB(r => !r.unguidedLink);
+ if (prioritizeGuided) {
+ return prioritizeGuided;
}
// Alpha
@@ -424,6 +503,14 @@ export function sortResources(
return [...accessible, ...restricted];
}
+function isConnectMyComputerAvailable(
+ accessibleResources: ResourceSpec[]
+): boolean {
+ return !!accessibleResources.find(
+ resource => resource.kind === ResourceKind.ConnectMyComputer
+ );
+}
+
/**
* Returns prioritized resources based on user preferences cluster state
*
@@ -464,8 +551,29 @@ function getPrioritizedResources(
};
}
-function makeResourcesWithHasAccessField(acl: Acl): ResourceSpec[] {
- return RESOURCES.map(r => {
+export function filterResources(
+ platform: Platform,
+ authType: AuthType,
+ resources: ResourceSpec[]
+) {
+ return resources.filter(resource => {
+ const resourceSupportsPlatform =
+ !resource.supportedPlatforms?.length ||
+ resource.supportedPlatforms.includes(platform);
+
+ const resourceSupportsAuthType =
+ !resource.supportedAuthTypes?.length ||
+ resource.supportedAuthTypes.includes(authType);
+
+ return resourceSupportsPlatform && resourceSupportsAuthType;
+ });
+}
+
+function addHasAccessField(
+ acl: Acl,
+ resources: ResourceSpec[]
+): ResourceSpec[] {
+ return resources.map(r => {
const hasAccess = checkHasAccess(acl, r.kind);
switch (r.kind) {
case ResourceKind.Database:
diff --git a/web/packages/teleport/src/Discover/SelectResource/resources.tsx b/web/packages/teleport/src/Discover/SelectResource/resources.tsx
index 98fc78e6bcadc..d811ff38b906d 100644
--- a/web/packages/teleport/src/Discover/SelectResource/resources.tsx
+++ b/web/packages/teleport/src/Discover/SelectResource/resources.tsx
@@ -85,6 +85,16 @@ export const SERVERS: ResourceSpec[] = [
event: DiscoverEventResource.Ec2Instance,
nodeMeta: { location: ServerLocation.Aws },
},
+ // TODO(ravicious): Uncomment for v14.2.
+ // {
+ // name: 'Connect My Computer',
+ // kind: ResourceKind.ConnectMyComputer,
+ // keywords: baseServerKeywords + 'connect my computer',
+ // icon: 'Laptop',
+ // event: DiscoverEventResource.Server,
+ // supportedPlatforms: [Platform.macOS, Platform.Linux],
+ // supportedAuthTypes: ['local', 'passwordless'],
+ // },
];
export const APPLICATIONS: ResourceSpec[] = [
diff --git a/web/packages/teleport/src/Discover/SelectResource/types.ts b/web/packages/teleport/src/Discover/SelectResource/types.ts
index 10adfcd1b9af3..a46d192a5b86a 100644
--- a/web/packages/teleport/src/Discover/SelectResource/types.ts
+++ b/web/packages/teleport/src/Discover/SelectResource/types.ts
@@ -17,6 +17,7 @@
import { Platform } from 'design/platform';
import { ClusterResource } from 'teleport/services/userPreferences/types';
+import { AuthType } from 'teleport/services/user';
import { ResourceKind } from '../Shared/ResourceKind';
@@ -34,7 +35,7 @@ export enum DatabaseLocation {
TODO,
}
-// DatabaseEngine represents the db "protocol".
+/** DatabaseEngine represents the db "protocol". */
export enum DatabaseEngine {
Postgres,
AuroraPostgres,
@@ -64,26 +65,52 @@ export interface ResourceSpec {
popular?: boolean;
kind: ResourceKind;
icon: ResourceIconName;
- // keywords are filter words that user may use to search for
- // this resource.
+ /**
+ * keywords are filter words that user may use to search for
+ * this resource.
+ */
keywords: string;
- // hasAccess is a flag to mean that user has
- // the preliminary permissions to add this resource.
+ /**
+ * hasAccess is a flag to mean that user has
+ * the preliminary permissions to add this resource.
+ */
hasAccess?: boolean;
- // unguidedLink is the link out to this resources documentation.
- // It is used as a flag, that when defined, means that
- // this resource is not "guided" (has no UI interactive flow).
+ /**
+ * unguidedLink is the link out to this resources documentation.
+ * It is used as a flag, that when defined, means that
+ * this resource is not "guided" (has no UI interactive flow).
+ */
unguidedLink?: string;
- // isDialog indicates whether the flow for this resource is a popover dialog as opposed to a Discover flow.
- // This is the case for the 'Application' resource.
+ /**
+ * isDialog indicates whether the flow for this resource is a popover dialog as opposed to a
+ * Discover flow. This is the case for the 'Application' resource.
+ */
isDialog?: boolean;
- // event is the expected backend enum event name that describes
- // the type of this resource (e.g. server v. kubernetes),
- // used for usage reporting.
+ /**
+ * event is the expected backend enum event name that describes
+ * the type of this resource (e.g. server v. kubernetes),
+ * used for usage reporting.
+ */
event: DiscoverEventResource;
- // platform indicates a particular platform the resource is associated with.
- // Set this value if the resource should be prioritized based on the platform.
+ /**
+ * platform indicates a particular platform the resource is associated with.
+ * Set this value if the resource should be prioritized based on the platform.
+ */
platform?: Platform;
+ /**
+ * supportedPlatforms indicate particular platforms the resource is available on. The resource
+ * won't be displayed on unsupported platforms.
+ *
+ * An empty array or undefined means that the resource is supported on all platforms.
+ */
+ supportedPlatforms?: Platform[];
+ /**
+ * supportedAuthTypes indicate particular auth types that the resource is available for. The
+ * resource won't be displayed if the user logged in using an unsupported auth type.
+ *
+ * An empty array or undefined means that the resource supports all auth types.
+ */
+ supportedAuthTypes?: AuthType[];
}
export enum SearchResource {
diff --git a/web/packages/teleport/src/Discover/Shared/ResourceKind.ts b/web/packages/teleport/src/Discover/Shared/ResourceKind.ts
index 1c2cd4c77c3c4..bcd16560425fa 100644
--- a/web/packages/teleport/src/Discover/Shared/ResourceKind.ts
+++ b/web/packages/teleport/src/Discover/Shared/ResourceKind.ts
@@ -26,6 +26,7 @@ export enum ResourceKind {
Server,
SamlApplication,
Discovery,
+ ConnectMyComputer,
}
export function resourceKindToJoinRole(kind: ResourceKind): JoinRole {
@@ -42,6 +43,8 @@ export function resourceKindToJoinRole(kind: ResourceKind): JoinRole {
return 'Node';
case ResourceKind.Discovery:
return 'Discovery';
+ case ResourceKind.ConnectMyComputer:
+ return 'Node';
}
}
@@ -61,5 +64,7 @@ export function resourceKindToPreferredResource(
return ClusterResource.RESOURCE_SERVER_SSH;
case ResourceKind.Discovery:
return ClusterResource.RESOURCE_UNSPECIFIED;
+ case ResourceKind.ConnectMyComputer:
+ return ClusterResource.RESOURCE_SERVER_SSH;
}
}