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 <http://www.gnu.org/licenses/>.
  */
 
-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 && (
         <>
           <Grid role="grid">
-            {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 (
-                <ResourceCard
-                  data-testid={r.kind}
-                  key={`${index}${pretitle}${title}`}
-                  hasAccess={r.hasAccess}
-                  aria-label={`${pretitle} ${title}`}
-                  {...resourceCardProps}
-                >
-                  {!r.unguidedLink && r.hasAccess && (
-                    <BadgeGuided>Guided</BadgeGuided>
-                  )}
-                  {!r.hasAccess && (
-                    <ToolTipNoPermBadge
-                      children={<PermissionsErrorMessage resource={r} />}
-                    />
-                  )}
-                  <Flex px={2} alignItems="center" height="48px">
-                    <Flex mr={3} justifyContent="center" width="24px">
-                      <DiscoverIcon name={r.icon} />
-                    </Flex>
-                    <Box>
-                      {pretitle && (
-                        <Text typography="body3" color="text.slightlyMuted">
-                          {pretitle}
-                        </Text>
-                      )}
-                      {r.unguidedLink ? (
-                        <Text bold color="text.main">
-                          {title}
-                        </Text>
-                      ) : (
-                        <Text bold>{title}</Text>
-                      )}
-                    </Box>
-                  </Flex>
-
-                  {r.unguidedLink && r.hasAccess ? (
-                    <NewTabInCorner color="text.muted" size={18} />
-                  ) : null}
-                </ResourceCard>
-              );
-            })}
+            {resources.map((r, index) => (
+              <Tile
+                // TODO(kimlisa): replace with r.id in upcoming PR
+                key={`${index}${r.name}${r.kind}`}
+                resourceSpec={r}
+                onChangeShowApp={setShowApp}
+                onSelectResource={onSelect}
+              />
+            ))}
           </Grid>
           <P3 mt={6}>
             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<ElementType>(
-  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 <http://www.gnu.org/licenses/>.
+ */
+
+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 (
+    <ResourceCard
+      data-testid={resourceSpec.kind}
+      hasAccess={resourceSpec.hasAccess}
+      aria-label={`${pretitle} ${title}`}
+      {...resourceCardProps}
+    >
+      {!resourceSpec.unguidedLink && resourceSpec.hasAccess && (
+        <BadgeGuided>Guided</BadgeGuided>
+      )}
+      {!resourceSpec.hasAccess && (
+        <ToolTipNoPermBadge>
+          <PermissionsErrorMessage resource={resourceSpec} />
+        </ToolTipNoPermBadge>
+      )}
+      <Flex px={2} alignItems="center" height="48px">
+        <Flex mr={3} justifyContent="center" width="24px">
+          <DiscoverIcon name={resourceSpec.icon} />
+        </Flex>
+        <Box>
+          {pretitle && (
+            <Text typography="body3" color="text.slightlyMuted">
+              {pretitle}
+            </Text>
+          )}
+          {resourceSpec.unguidedLink ? (
+            <Text bold color="text.main">
+              {title}
+            </Text>
+          ) : (
+            <Text bold>{title}</Text>
+          )}
+        </Box>
+      </Flex>
+
+      {resourceSpec.unguidedLink && resourceSpec.hasAccess ? (
+        <NewTabInCorner color="text.muted" size={18} />
+      ) : null}
+    </ResourceCard>
+  );
+}
+
+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 <http://www.gnu.org/licenses/>.
+ */
+
+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 <http://www.gnu.org/licenses/>.
+ */
+
+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 <http://www.gnu.org/licenses/>.
+ */
+
+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 <http://www.gnu.org/licenses/>.
+ */
+
+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<ElementType>(
+  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;
+}