diff --git a/web/packages/teleport/src/Discover/Discover.test.tsx b/web/packages/teleport/src/Discover/Discover.test.tsx
index d484dbf0456e2..aeef68ce57940 100644
--- a/web/packages/teleport/src/Discover/Discover.test.tsx
+++ b/web/packages/teleport/src/Discover/Discover.test.tsx
@@ -25,12 +25,10 @@ import cfg from 'teleport/config';
import { Discover, DiscoverComponent } from 'teleport/Discover/Discover';
import { ResourceViewConfig } from 'teleport/Discover/flow';
import {
+ APPLICATIONS,
DATABASES,
DATABASES_UNGUIDED,
DATABASES_UNGUIDED_DOC,
-} from 'teleport/Discover/SelectResource/databases';
-import {
- APPLICATIONS,
KUBERNETES,
SERVERS,
} from 'teleport/Discover/SelectResource/resources';
diff --git a/web/packages/teleport/src/Discover/Fixtures/databases.tsx b/web/packages/teleport/src/Discover/Fixtures/databases.tsx
index d39910f2efc30..a699e37eb0520 100644
--- a/web/packages/teleport/src/Discover/Fixtures/databases.tsx
+++ b/web/packages/teleport/src/Discover/Fixtures/databases.tsx
@@ -30,7 +30,7 @@ import {
IntegrationStatusCode,
} from 'teleport/services/integrations';
-import { DATABASES } from '../SelectResource/databases';
+import { DATABASES } from '../SelectResource/resources';
import { ResourceKind } from '../Shared';
import { TeleportProvider } from './fixtures';
diff --git a/web/packages/teleport/src/Discover/SelectResource/SelectResource.test.tsx b/web/packages/teleport/src/Discover/SelectResource/SelectResource.test.tsx
index 496f7481a6e62..85fae029a64b8 100644
--- a/web/packages/teleport/src/Discover/SelectResource/SelectResource.test.tsx
+++ b/web/packages/teleport/src/Discover/SelectResource/SelectResource.test.tsx
@@ -37,12 +37,10 @@ import * as userUserContext from 'teleport/User/UserContext';
import { ResourceKind } from '../Shared';
import { resourceKindToPreferredResource } from '../Shared/ResourceKind';
-import {
- filterResources,
- SelectResource,
- sortResources,
-} from './SelectResource';
+import { SelectResource } from './SelectResource';
import { ResourceSpec } from './types';
+import { filterBySupportedPlatformsAndAuthTypes } from './utils/filters';
+import { sortResourcesByPreferences } from './utils/sort';
const setUp = () => {
jest
@@ -85,7 +83,7 @@ const onboardDiscoverNoResources: OnboardDiscover = {
hasVisited: false,
};
-test('sortResources without preferred resources, sorts resources alphabetically with guided resources first', () => {
+test('sortResourcesByPreferences without preferred resources, sorts resources alphabetically with guided resources first', () => {
setUp();
const mockIn: ResourceSpec[] = [
// unguided
@@ -99,7 +97,7 @@ test('sortResources without preferred resources, sorts resources alphabetically
makeResourceSpec({ name: 'costco' }),
];
- const actual = sortResources(
+ const actual = sortResourcesByPreferences(
mockIn,
makeDefaultUserPreferences(),
onboardDiscoverWithResources
@@ -358,7 +356,7 @@ describe('preferred resources', () => {
test.each(testCases)('$name', testCase => {
const preferences = makeDefaultUserPreferences();
preferences.onboard.preferredResources = testCase.preferred;
- const actual = sortResources(
+ const actual = sortResourcesByPreferences(
kindBasedList,
preferences,
onboardDiscoverWithResources
@@ -563,7 +561,7 @@ describe('marketing params', () => {
test.each(testCases)('$name', testCase => {
const preferences = makeDefaultUserPreferences();
preferences.onboard = testCase.preferred;
- const actual = sortResources(
+ const actual = sortResourcesByPreferences(
kindBasedList,
preferences,
onboardDiscoverWithResources
@@ -707,7 +705,7 @@ describe('os sorted resources', () => {
test.each(testCases)('$name', testCase => {
OS.mockReturnValue(testCase.userAgent);
- const actual = sortResources(
+ const actual = sortResourcesByPreferences(
osBasedList,
makeDefaultUserPreferences(),
onboardDiscoverWithResources
@@ -726,7 +724,7 @@ describe('os sorted resources', () => {
];
OS.mockReturnValue(UserAgent.macOS);
- const actual = sortResources(
+ const actual = sortResourcesByPreferences(
mockIn,
makeDefaultUserPreferences(),
onboardDiscoverWithResources
@@ -773,7 +771,7 @@ describe('os sorted resources', () => {
},
};
- const actual = sortResources(
+ const actual = sortResourcesByPreferences(
oneOfEachList,
preferences,
onboardDiscoverWithResources
@@ -853,7 +851,7 @@ describe('sorting 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(
+ const actual = sortResourcesByPreferences(
oneOfEachList,
makeDefaultUserPreferences(),
onboardDiscoverNoResources
@@ -892,7 +890,7 @@ describe('sorting Connect My Computer', () => {
},
};
- const actual = sortResources(
+ const actual = sortResourcesByPreferences(
oneOfEachList,
preferences,
onboardDiscoverNoResources
@@ -935,7 +933,7 @@ describe('sorting Connect My Computer', () => {
platform: Platform.Linux,
});
- const actual = sortResources(
+ const actual = sortResourcesByPreferences(
[
unguidedA,
guidedServerForMatchingPlatformB,
@@ -988,7 +986,7 @@ describe('sorting Connect My Computer', () => {
},
};
- const actual = sortResources(
+ const actual = sortResourcesByPreferences(
[
unguidedA,
guidedServerForMatchingPlatformB,
@@ -1014,7 +1012,7 @@ describe('sorting 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(
+ const actual = sortResourcesByPreferences(
oneOfEachList,
makeDefaultUserPreferences(),
onboardDiscoverWithResources
@@ -1053,7 +1051,7 @@ describe('sorting Connect My Computer', () => {
},
};
- const actual = sortResources(
+ const actual = sortResourcesByPreferences(
oneOfEachList,
preferences,
onboardDiscoverWithResources
@@ -1099,7 +1097,7 @@ describe('sorting Connect My Computer', () => {
},
};
- const actual = sortResources(
+ const actual = sortResourcesByPreferences(
[...oneOfEachList, databaseForAnotherPlatform],
preferences,
onboardDiscoverNoResources
@@ -1195,12 +1193,11 @@ describe('filterResources', () => {
supportedPlatforms: [Platform.macOS],
});
- const result = filterResources(Platform.macOS, 'local', [
- winAndLinux,
- win,
- macosAndLinux,
- macos,
- ]);
+ const result = filterBySupportedPlatformsAndAuthTypes(
+ Platform.macOS,
+ 'local',
+ [winAndLinux, win, macosAndLinux, macos]
+ );
expect(result).toContain(macosAndLinux);
expect(result).toContain(macos);
@@ -1209,24 +1206,28 @@ describe('filterResources', () => {
});
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,
- }),
- ]);
+ const result = filterBySupportedPlatformsAndAuthTypes(
+ 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);
});
@@ -1249,12 +1250,11 @@ describe('filterResources', () => {
supportedAuthTypes: ['local'],
});
- const result = filterResources(Platform.macOS, 'local', [
- ssoAndPasswordless,
- sso,
- localAndPasswordless,
- local,
- ]);
+ const result = filterBySupportedPlatformsAndAuthTypes(
+ Platform.macOS,
+ 'local',
+ [ssoAndPasswordless, sso, localAndPasswordless, local]
+ );
expect(result).toContain(localAndPasswordless);
expect(result).toContain(local);
diff --git a/web/packages/teleport/src/Discover/SelectResource/SelectResource.tsx b/web/packages/teleport/src/Discover/SelectResource/SelectResource.tsx
index 34b0d848933b8..257b2c2d189a0 100644
--- a/web/packages/teleport/src/Discover/SelectResource/SelectResource.tsx
+++ b/web/packages/teleport/src/Discover/SelectResource/SelectResource.tsx
@@ -16,49 +16,29 @@
* along with this program. If not, see .
*/
-import {
- useEffect,
- useMemo,
- useState,
- type ComponentPropsWithoutRef,
-} from 'react';
+import { useEffect, useMemo, useState } from 'react';
import { useHistory, useLocation } from 'react-router';
import styled from 'styled-components';
import { Alert, Box, Flex, Link, P3, Text } from 'design';
import * as Icons from 'design/Icon';
-import { NewTab } from 'design/Icon';
-import { getPlatform, Platform } from 'design/platform';
-import { Resource } from 'gen-proto-ts/teleport/userpreferences/v1/onboard_pb';
-import { UserPreferences } from 'gen-proto-ts/teleport/userpreferences/v1/userpreferences_pb';
+import { getPlatform } from 'design/platform';
import AddApp from 'teleport/Apps/AddApp';
import { FeatureHeader, FeatureHeaderTitle } from 'teleport/components/Layout';
-import { ToolTipNoPermBadge } from 'teleport/components/ToolTipNoPermBadge';
import cfg from 'teleport/config';
-import {
- BASE_RESOURCES,
- getResourcePretitle,
-} from 'teleport/Discover/SelectResource/resources';
-import {
- HeaderSubtitle,
- PermissionsErrorMessage,
- ResourceKind,
-} from 'teleport/Discover/Shared';
-import { resourceKindToPreferredResource } from 'teleport/Discover/Shared/ResourceKind';
+import { BASE_RESOURCES } from 'teleport/Discover/SelectResource/resources';
+import { HeaderSubtitle } from 'teleport/Discover/Shared';
import { storageService } from 'teleport/services/storageService';
-import { Acl, AuthType, OnboardDiscover } from 'teleport/services/user';
import { useUser } from 'teleport/User/UserContext';
import useTeleport from 'teleport/useTeleport';
-import { getMarketingTermMatches } from './getMarketingTermMatches';
-import { DiscoverIcon } from './icons';
-import { SAML_APPLICATIONS } from './resourcesE';
-import {
- PrioritizedResources,
- SearchResource,
- type ResourceSpec,
-} from './types';
+import { SAML_APPLICATIONS } from './resources';
+import { Tile } from './Tile';
+import { SearchResource, type ResourceSpec } from './types';
+import { addHasAccessField } from './utils/checkAccess';
+import { filterBySupportedPlatformsAndAuthTypes } from './utils/filters';
+import { sortResourcesByKind, sortResourcesByPreferences } from './utils/sort';
interface SelectResourceProps {
onSelect: (resource: ResourceSpec) => void;
@@ -89,11 +69,11 @@ export function SelectResource({ onSelect }: SelectResourceProps) {
const platform = getPlatform();
const defaultResources: ResourceSpec[] = useMemo(
() =>
- sortResources(
+ sortResourcesByPreferences(
// Apply access check to each resource.
addHasAccessField(
acl,
- filterResources(
+ filterBySupportedPlatformsAndAuthTypes(
platform,
authType,
getDefaultResources(cfg.isEnterprise)
@@ -193,98 +173,15 @@ export function SelectResource({ onSelect }: SelectResourceProps) {
{resources && resources.length > 0 && (
<>
- {resources.map((r, index) => {
- const title = r.name;
- const pretitle = getResourcePretitle(r);
- const select = () => {
- if (!r.hasAccess) {
- return;
- }
-
- setShowApp(true);
- onSelect(r);
- };
-
- let resourceCardProps: ComponentPropsWithoutRef<
- 'button' | typeof Link
- >;
-
- if (r.kind === ResourceKind.Application && r.isDialog) {
- resourceCardProps = {
- onClick: select,
- onKeyUp: (e: KeyboardEvent) => e.key === 'Enter' && select(),
- role: 'button',
- };
- } else if (r.unguidedLink) {
- resourceCardProps = {
- as: Link,
- href: r.hasAccess ? r.unguidedLink : null,
- target: '_blank',
- style: { textDecoration: 'none' },
- role: 'link',
- };
- } else {
- resourceCardProps = {
- onClick: () => r.hasAccess && onSelect(r),
- onKeyUp: (e: KeyboardEvent) => {
- if (e.key === 'Enter' && r.hasAccess) {
- onSelect(r);
- }
- },
- role: 'button',
- };
- }
-
- // There can be three types of click behavior with the resource cards:
- // 1) If the resource has no interactive UI flow ("unguided"),
- // clicking on the card will take a user to our docs page
- // on a new tab.
- // 2) If the resource is guided, we start the "flow" by
- // taking user to the next step.
- // 3) If the resource is kind 'Application', it will render the legacy
- // popup modal where it shows user to add app manually or automatically.
- return (
-
- {!r.unguidedLink && r.hasAccess && (
- Guided
- )}
- {!r.hasAccess && (
- }
- />
- )}
-
-
-
-
-
- {pretitle && (
-
- {pretitle}
-
- )}
- {r.unguidedLink ? (
-
- {title}
-
- ) : (
- {title}
- )}
-
-
-
- {r.unguidedLink && r.hasAccess ? (
-
- ) : null}
-
- );
- })}
+ {resources.map((r, index) => (
+
+ ))}
Looking for something else?{' '}
@@ -338,301 +235,6 @@ const ClearSearch = ({ onClick }: { onClick(): void }) => {
);
};
-function checkHasAccess(acl: Acl, resourceKind: ResourceKind) {
- const basePerm = acl.tokens.create;
- if (!basePerm) {
- return false;
- }
-
- switch (resourceKind) {
- case ResourceKind.Application:
- return acl.appServers.read && acl.appServers.list;
- case ResourceKind.Database:
- return acl.dbServers.read && acl.dbServers.list;
- case ResourceKind.Desktop:
- return acl.desktops.read && acl.desktops.list;
- case ResourceKind.Kubernetes:
- return acl.kubeServers.read && acl.kubeServers.list;
- case ResourceKind.Server:
- 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;
- }
-}
-
-function sortResourcesByKind(
- resourceKind: SearchResource,
- resources: ResourceSpec[]
-) {
- let sorted: ResourceSpec[] = [];
- switch (resourceKind) {
- case SearchResource.SERVER:
- sorted = [
- ...resources.filter(r => r.kind === ResourceKind.Server),
- ...resources.filter(r => r.kind !== ResourceKind.Server),
- ];
- break;
- case SearchResource.APPLICATION:
- sorted = [
- ...resources.filter(r => r.kind === ResourceKind.Application),
- ...resources.filter(r => r.kind !== ResourceKind.Application),
- ];
- break;
- case SearchResource.DATABASE:
- sorted = [
- ...resources.filter(r => r.kind === ResourceKind.Database),
- ...resources.filter(r => r.kind !== ResourceKind.Database),
- ];
- break;
- case SearchResource.DESKTOP:
- sorted = [
- ...resources.filter(r => r.kind === ResourceKind.Desktop),
- ...resources.filter(r => r.kind !== ResourceKind.Desktop),
- ];
- break;
- case SearchResource.KUBERNETES:
- sorted = [
- ...resources.filter(r => r.kind === ResourceKind.Kubernetes),
- ...resources.filter(r => r.kind !== ResourceKind.Kubernetes),
- ];
- break;
- }
- 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,
- 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);
-
- 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) => {
- 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;
- }
-
- // 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
- const prioritizeUserPlatform = compareAB(r => r.platform === platform);
- if (prioritizeUserPlatform) {
- return prioritizeUserPlatform;
- }
-
- // Display preferred resources second
- if (hasPreferredResources) {
- const prioritizePreferredResource = compareAB(r =>
- preferredResources.includes(resourceKindToPreferredResource(r.kind))
- );
- if (prioritizePreferredResource) {
- return prioritizePreferredResource;
- }
- }
-
- // Display guided resources third
- const prioritizeGuided = compareAB(r => !r.unguidedLink);
- if (prioritizeGuided) {
- return prioritizeGuided;
- }
-
- // Alpha
- return a.name.localeCompare(b.name);
- });
-
- // Sort restricted resources alphabetically
- restricted.sort((a, b) => {
- return a.name.localeCompare(b.name);
- });
-
- // Sort resources that user has access to the
- // top of the list, so it is more visible to
- // the user.
- 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
- *
- * @remarks
- * A user can have preferredResources set via onboarding either from the survey (preferredResources)
- * or various query parameters (marketingParams). We sort the list by the marketingParams if available.
- * If not, we sort by preferred resource type if available.
- * We do not search.
- *
- * @param preferences - Cluster state user preferences
- * @returns PrioritizedResources which is both the resource to prioritize and a boolean value of the value
- *
- */
-function getPrioritizedResources(
- preferences: UserPreferences
-): PrioritizedResources {
- const marketingParams = preferences.onboard.marketingParams;
-
- if (marketingParams) {
- const marketingPriorities = getMarketingTermMatches(marketingParams);
- if (marketingPriorities.length > 0) {
- return {
- hasPreferredResources: true,
- preferredResources: marketingPriorities,
- };
- }
- }
-
- const preferredResources = preferences.onboard.preferredResources || [];
-
- // hasPreferredResources will be false if all resources are selected
- const maxResources = Object.keys(Resource).length / 2 - 1;
- const selectedAll = preferredResources.length === maxResources;
-
- return {
- preferredResources: preferredResources,
- hasPreferredResources: preferredResources.length > 0 && !selectedAll,
- };
-}
-
-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:
- return { ...r, dbMeta: { ...r.dbMeta }, hasAccess };
- default:
- return { ...r, hasAccess };
- }
- });
-}
-
const Grid = styled.div`
display: grid;
grid-template-columns: repeat(auto-fill, 320px);
@@ -640,58 +242,6 @@ const Grid = styled.div`
row-gap: 15px;
`;
-const NewTabInCorner = styled(NewTab)`
- position: absolute;
- top: ${props => props.theme.space[3]}px;
- right: ${props => props.theme.space[3]}px;
- transition: color 0.3s;
-`;
-
-const ResourceCard = styled.button<{ hasAccess?: boolean }>`
- position: relative;
- text-align: left;
- background: ${props => props.theme.colors.spotBackground[0]};
- transition: all 0.3s;
-
- border: none;
- border-radius: 8px;
- padding: 12px;
- color: ${props => props.theme.colors.text.main};
- line-height: inherit;
- font-size: inherit;
- font-family: inherit;
- cursor: pointer;
-
- opacity: ${props => (props.hasAccess ? '1' : '0.45')};
-
- &:focus-visible {
- outline: none;
- box-shadow: 0 0 0 3px ${props => props.theme.colors.brand};
- }
-
- &:hover,
- &:focus-visible {
- background: ${props => props.theme.colors.spotBackground[1]};
-
- ${NewTabInCorner} {
- color: ${props => props.theme.colors.text.slightlyMuted};
- }
- }
-`;
-
-const BadgeGuided = styled.div`
- position: absolute;
- background: ${props => props.theme.colors.brand};
- color: ${props => props.theme.colors.text.primaryInverse};
- padding: 0px 6px;
- border-top-right-radius: 8px;
- border-bottom-left-radius: 8px;
- top: 0px;
- right: 0px;
- font-size: 10px;
- line-height: 24px;
-`;
-
const InputWrapper = styled.div`
border-radius: 200px;
height: 40px;
diff --git a/web/packages/teleport/src/Discover/SelectResource/Tile.tsx b/web/packages/teleport/src/Discover/SelectResource/Tile.tsx
new file mode 100644
index 0000000000000..e86d681b73394
--- /dev/null
+++ b/web/packages/teleport/src/Discover/SelectResource/Tile.tsx
@@ -0,0 +1,183 @@
+/**
+ * Teleport
+ * Copyright (C) 2025 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { type ComponentPropsWithoutRef } from 'react';
+import styled from 'styled-components';
+
+import { Box, Flex, Link, Text } from 'design';
+import { NewTab } from 'design/Icon';
+
+import { ToolTipNoPermBadge } from 'teleport/components/ToolTipNoPermBadge';
+import {
+ PermissionsErrorMessage,
+ ResourceKind,
+} from 'teleport/Discover/Shared';
+
+import { getResourcePretitle } from '.';
+import { DiscoverIcon } from './icons';
+import { type ResourceSpec } from './types';
+
+export function Tile({
+ resourceSpec,
+ onChangeShowApp,
+ onSelectResource,
+}: {
+ resourceSpec: ResourceSpec;
+ onChangeShowApp(b: boolean): void;
+ onSelectResource(r: ResourceSpec): void;
+}) {
+ const title = resourceSpec.name;
+ const pretitle = getResourcePretitle(resourceSpec);
+ const select = () => {
+ if (!resourceSpec.hasAccess) {
+ return;
+ }
+
+ onChangeShowApp(true);
+ onSelectResource(resourceSpec);
+ };
+
+ let resourceCardProps: ComponentPropsWithoutRef<'button' | typeof Link>;
+
+ if (resourceSpec.kind === ResourceKind.Application && resourceSpec.isDialog) {
+ resourceCardProps = {
+ onClick: select,
+ onKeyUp: (e: KeyboardEvent) => e.key === 'Enter' && select(),
+ role: 'button',
+ };
+ } else if (resourceSpec.unguidedLink) {
+ resourceCardProps = {
+ as: Link,
+ href: resourceSpec.hasAccess ? resourceSpec.unguidedLink : null,
+ target: '_blank',
+ style: { textDecoration: 'none' },
+ role: 'link',
+ };
+ } else {
+ resourceCardProps = {
+ onClick: () => resourceSpec.hasAccess && onSelectResource(resourceSpec),
+ onKeyUp: (e: KeyboardEvent) => {
+ if (e.key === 'Enter' && resourceSpec.hasAccess) {
+ onSelectResource(resourceSpec);
+ }
+ },
+ role: 'button',
+ };
+ }
+
+ // There can be three types of click behavior with the resource cards:
+ // 1) If the resource has no interactive UI flow ("unguided"),
+ // clicking on the card will take a user to our docs page
+ // on a new tab.
+ // 2) If the resource is guided, we start the "flow" by
+ // taking user to the next step.
+ // 3) If the resource is kind 'Application', it will render the legacy
+ // popup modal where it shows user to add app manually or automatically.
+ return (
+
+ {!resourceSpec.unguidedLink && resourceSpec.hasAccess && (
+ Guided
+ )}
+ {!resourceSpec.hasAccess && (
+
+
+
+ )}
+
+
+
+
+
+ {pretitle && (
+
+ {pretitle}
+
+ )}
+ {resourceSpec.unguidedLink ? (
+
+ {title}
+
+ ) : (
+ {title}
+ )}
+
+
+
+ {resourceSpec.unguidedLink && resourceSpec.hasAccess ? (
+
+ ) : null}
+
+ );
+}
+
+const NewTabInCorner = styled(NewTab)`
+ position: absolute;
+ top: ${props => props.theme.space[3]}px;
+ right: ${props => props.theme.space[3]}px;
+ transition: color 0.3s;
+`;
+
+const ResourceCard = styled.button<{ hasAccess?: boolean }>`
+ position: relative;
+ text-align: left;
+ background: ${props => props.theme.colors.spotBackground[0]};
+ transition: all 0.3s;
+
+ border: none;
+ border-radius: 8px;
+ padding: 12px;
+ color: ${props => props.theme.colors.text.main};
+ line-height: inherit;
+ font-size: inherit;
+ font-family: inherit;
+ cursor: pointer;
+
+ opacity: ${props => (props.hasAccess ? '1' : '0.45')};
+
+ &:focus-visible {
+ outline: none;
+ box-shadow: 0 0 0 3px ${props => props.theme.colors.brand};
+ }
+
+ &:hover,
+ &:focus-visible {
+ background: ${props => props.theme.colors.spotBackground[1]};
+
+ ${NewTabInCorner} {
+ color: ${props => props.theme.colors.text.slightlyMuted};
+ }
+ }
+`;
+
+const BadgeGuided = styled.div`
+ position: absolute;
+ background: ${props => props.theme.colors.brand};
+ color: ${props => props.theme.colors.text.primaryInverse};
+ padding: 0px 6px;
+ border-top-right-radius: 8px;
+ border-bottom-left-radius: 8px;
+ top: 0px;
+ right: 0px;
+ font-size: 10px;
+ line-height: 24px;
+`;
diff --git a/web/packages/teleport/src/Discover/SelectResource/index.ts b/web/packages/teleport/src/Discover/SelectResource/index.ts
index f253c05ca928d..ab372429f3336 100644
--- a/web/packages/teleport/src/Discover/SelectResource/index.ts
+++ b/web/packages/teleport/src/Discover/SelectResource/index.ts
@@ -17,6 +17,9 @@
*/
export { SelectResource } from './SelectResource';
-export { getResourcePretitle } from './resources';
-export { getDatabaseProtocol, getDefaultDatabasePort } from './databases';
+export {
+ getResourcePretitle,
+ getDatabaseProtocol,
+ getDefaultDatabasePort,
+} from './resources';
export * from './types';
diff --git a/web/packages/teleport/src/Discover/SelectResource/databases.tsx b/web/packages/teleport/src/Discover/SelectResource/resources/databases.tsx
similarity index 99%
rename from web/packages/teleport/src/Discover/SelectResource/databases.tsx
rename to web/packages/teleport/src/Discover/SelectResource/resources/databases.tsx
index a9f3d55112619..d30c280b4c0fb 100644
--- a/web/packages/teleport/src/Discover/SelectResource/databases.tsx
+++ b/web/packages/teleport/src/Discover/SelectResource/resources/databases.tsx
@@ -21,8 +21,8 @@ import { DbProtocol } from 'shared/services/databases';
import { DiscoverEventResource } from 'teleport/services/userEvent';
-import { ResourceKind } from '../Shared/ResourceKind';
-import { DatabaseEngine, DatabaseLocation, ResourceSpec } from './types';
+import { ResourceKind } from '../../Shared/ResourceKind';
+import { DatabaseEngine, DatabaseLocation, ResourceSpec } from '../types';
const baseDatabaseKeywords = ['db', 'database', 'databases'];
const awsKeywords = [...baseDatabaseKeywords, 'aws', 'amazon web services'];
diff --git a/web/packages/teleport/src/Discover/SelectResource/resources/index.ts b/web/packages/teleport/src/Discover/SelectResource/resources/index.ts
new file mode 100644
index 0000000000000..032144296417b
--- /dev/null
+++ b/web/packages/teleport/src/Discover/SelectResource/resources/index.ts
@@ -0,0 +1,21 @@
+/**
+ * Teleport
+ * Copyright (C) 2025 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+export * from './databases';
+export * from './resources';
+export * from './resourcesE';
diff --git a/web/packages/teleport/src/Discover/SelectResource/resources.tsx b/web/packages/teleport/src/Discover/SelectResource/resources/resources.tsx
similarity index 98%
rename from web/packages/teleport/src/Discover/SelectResource/resources.tsx
rename to web/packages/teleport/src/Discover/SelectResource/resources/resources.tsx
index f089fe9dc4db2..56cfb9c66e80b 100644
--- a/web/packages/teleport/src/Discover/SelectResource/resources.tsx
+++ b/web/packages/teleport/src/Discover/SelectResource/resources/resources.tsx
@@ -24,19 +24,19 @@ import {
DiscoverEventResource,
} from 'teleport/services/userEvent';
-import { ResourceKind } from '../Shared/ResourceKind';
-import {
- DATABASES,
- DATABASES_UNGUIDED,
- DATABASES_UNGUIDED_DOC,
-} from './databases';
+import { ResourceKind } from '../../Shared/ResourceKind';
import {
DatabaseEngine,
DatabaseLocation,
KubeLocation,
ResourceSpec,
ServerLocation,
-} from './types';
+} from '../types';
+import {
+ DATABASES,
+ DATABASES_UNGUIDED,
+ DATABASES_UNGUIDED_DOC,
+} from './databases';
const baseServerKeywords = ['server', 'node', 'ssh'];
const awsKeywords = ['aws', 'amazon', 'amazon web services'];
diff --git a/web/packages/teleport/src/Discover/SelectResource/resourcesE.tsx b/web/packages/teleport/src/Discover/SelectResource/resources/resourcesE.tsx
similarity index 95%
rename from web/packages/teleport/src/Discover/SelectResource/resourcesE.tsx
rename to web/packages/teleport/src/Discover/SelectResource/resources/resourcesE.tsx
index 2cba11ef39d34..b6056f4cf344c 100644
--- a/web/packages/teleport/src/Discover/SelectResource/resourcesE.tsx
+++ b/web/packages/teleport/src/Discover/SelectResource/resources/resourcesE.tsx
@@ -19,8 +19,8 @@
import { SamlServiceProviderPreset } from 'teleport/services/samlidp/types';
import { DiscoverEventResource } from 'teleport/services/userEvent';
-import { ResourceKind } from '../Shared';
-import { ResourceSpec } from './types';
+import { ResourceKind } from '../../Shared';
+import { ResourceSpec } from '../types';
export const SAML_APPLICATIONS: ResourceSpec[] = [
{
diff --git a/web/packages/teleport/src/Discover/SelectResource/utils/checkAccess.ts b/web/packages/teleport/src/Discover/SelectResource/utils/checkAccess.ts
new file mode 100644
index 0000000000000..7292e28413c52
--- /dev/null
+++ b/web/packages/teleport/src/Discover/SelectResource/utils/checkAccess.ts
@@ -0,0 +1,65 @@
+/**
+ * Teleport
+ * Copyright (C) 2025 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { Acl } from 'teleport/services/user';
+
+import { ResourceKind } from '../../Shared';
+import { ResourceSpec } from '../types';
+
+function checkHasAccess(acl: Acl, resourceKind: ResourceKind) {
+ const basePerm = acl.tokens.create;
+ if (!basePerm) {
+ return false;
+ }
+
+ switch (resourceKind) {
+ case ResourceKind.Application:
+ return acl.appServers.read && acl.appServers.list;
+ case ResourceKind.Database:
+ return acl.dbServers.read && acl.dbServers.list;
+ case ResourceKind.Desktop:
+ return acl.desktops.read && acl.desktops.list;
+ case ResourceKind.Kubernetes:
+ return acl.kubeServers.read && acl.kubeServers.list;
+ case ResourceKind.Server:
+ 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;
+ }
+}
+
+export function addHasAccessField(
+ acl: Acl,
+ resources: ResourceSpec[]
+): ResourceSpec[] {
+ return resources.map(r => {
+ const hasAccess = checkHasAccess(acl, r.kind);
+ switch (r.kind) {
+ case ResourceKind.Database:
+ return { ...r, dbMeta: { ...r.dbMeta }, hasAccess };
+ default:
+ return { ...r, hasAccess };
+ }
+ });
+}
diff --git a/web/packages/teleport/src/Discover/SelectResource/utils/filters.ts b/web/packages/teleport/src/Discover/SelectResource/utils/filters.ts
new file mode 100644
index 0000000000000..325a85c97d94a
--- /dev/null
+++ b/web/packages/teleport/src/Discover/SelectResource/utils/filters.ts
@@ -0,0 +1,41 @@
+/**
+ * Teleport
+ * Copyright (C) 2025 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { Platform } from 'design/platform';
+
+import { AuthType } from 'teleport/services/user';
+
+import { type ResourceSpec } from '../types';
+
+export function filterBySupportedPlatformsAndAuthTypes(
+ 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;
+ });
+}
diff --git a/web/packages/teleport/src/Discover/SelectResource/utils/sort.ts b/web/packages/teleport/src/Discover/SelectResource/utils/sort.ts
new file mode 100644
index 0000000000000..c43049f632418
--- /dev/null
+++ b/web/packages/teleport/src/Discover/SelectResource/utils/sort.ts
@@ -0,0 +1,262 @@
+/**
+ * Teleport
+ * Copyright (C) 2025 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { getPlatform } from 'design/platform';
+import { Resource } from 'gen-proto-ts/teleport/userpreferences/v1/onboard_pb';
+import { UserPreferences } from 'gen-proto-ts/teleport/userpreferences/v1/userpreferences_pb';
+
+import { OnboardDiscover } from 'teleport/services/user';
+
+import { ResourceKind } from '../../Shared';
+import { resourceKindToPreferredResource } from '../../Shared/ResourceKind';
+import { getMarketingTermMatches } from '../getMarketingTermMatches';
+import { PrioritizedResources, ResourceSpec, SearchResource } from '../types';
+
+function isConnectMyComputerAvailable(
+ accessibleResources: ResourceSpec[]
+): boolean {
+ return !!accessibleResources.find(
+ resource => resource.kind === ResourceKind.ConnectMyComputer
+ );
+}
+
+export function sortResourcesByPreferences(
+ resources: ResourceSpec[],
+ 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);
+
+ 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) => {
+ 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;
+ }
+
+ // 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
+ const prioritizeUserPlatform = compareAB(r => r.platform === platform);
+ if (prioritizeUserPlatform) {
+ return prioritizeUserPlatform;
+ }
+
+ // Display preferred resources second
+ if (hasPreferredResources) {
+ const prioritizePreferredResource = compareAB(r =>
+ preferredResources.includes(resourceKindToPreferredResource(r.kind))
+ );
+ if (prioritizePreferredResource) {
+ return prioritizePreferredResource;
+ }
+ }
+
+ // Display guided resources third
+ const prioritizeGuided = compareAB(r => !r.unguidedLink);
+ if (prioritizeGuided) {
+ return prioritizeGuided;
+ }
+
+ // Alpha
+ return a.name.localeCompare(b.name);
+ });
+
+ // Sort restricted resources alphabetically
+ restricted.sort((a, b) => {
+ return a.name.localeCompare(b.name);
+ });
+
+ // Sort resources that user has access to the
+ // top of the list, so it is more visible to
+ // the user.
+ return [...accessible, ...restricted];
+}
+
+/**
+ * Returns prioritized resources based on user preferences cluster state
+ *
+ * @remarks
+ * A user can have preferredResources set via onboarding either from the survey (preferredResources)
+ * or various query parameters (marketingParams). We sort the list by the marketingParams if available.
+ * If not, we sort by preferred resource type if available.
+ * We do not search.
+ *
+ * @param preferences - Cluster state user preferences
+ * @returns PrioritizedResources which is both the resource to prioritize and a boolean value of the value
+ *
+ */
+function getPrioritizedResources(
+ preferences: UserPreferences
+): PrioritizedResources {
+ const marketingParams = preferences.onboard.marketingParams;
+
+ if (marketingParams) {
+ const marketingPriorities = getMarketingTermMatches(marketingParams);
+ if (marketingPriorities.length > 0) {
+ return {
+ hasPreferredResources: true,
+ preferredResources: marketingPriorities,
+ };
+ }
+ }
+
+ const preferredResources = preferences.onboard.preferredResources || [];
+
+ // hasPreferredResources will be false if all resources are selected
+ const maxResources = Object.keys(Resource).length / 2 - 1;
+ const selectedAll = preferredResources.length === maxResources;
+
+ return {
+ preferredResources: preferredResources,
+ hasPreferredResources: preferredResources.length > 0 && !selectedAll,
+ };
+}
+
+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 sortResourcesByKind(
+ resourceKind: SearchResource,
+ resources: ResourceSpec[]
+) {
+ let sorted: ResourceSpec[] = [];
+ switch (resourceKind) {
+ case SearchResource.SERVER:
+ sorted = [
+ ...resources.filter(r => r.kind === ResourceKind.Server),
+ ...resources.filter(r => r.kind !== ResourceKind.Server),
+ ];
+ break;
+ case SearchResource.APPLICATION:
+ sorted = [
+ ...resources.filter(r => r.kind === ResourceKind.Application),
+ ...resources.filter(r => r.kind !== ResourceKind.Application),
+ ];
+ break;
+ case SearchResource.DATABASE:
+ sorted = [
+ ...resources.filter(r => r.kind === ResourceKind.Database),
+ ...resources.filter(r => r.kind !== ResourceKind.Database),
+ ];
+ break;
+ case SearchResource.DESKTOP:
+ sorted = [
+ ...resources.filter(r => r.kind === ResourceKind.Desktop),
+ ...resources.filter(r => r.kind !== ResourceKind.Desktop),
+ ];
+ break;
+ case SearchResource.KUBERNETES:
+ sorted = [
+ ...resources.filter(r => r.kind === ResourceKind.Kubernetes),
+ ...resources.filter(r => r.kind !== ResourceKind.Kubernetes),
+ ];
+ break;
+ }
+ return sorted;
+}