Connect: Improve search bar readability (#65781)

* Improve results layout

* Do not show matches in resource's secondary properties/labels if there's already a match on the main field

* Adjust stories and add tests

* Change `font-weight` to 400 for better redability

* Add tests for `getVisibleMatches`

* Remove unnecessary trailing spaces
This commit is contained in:
Grzegorz Zdunek
2026-04-20 09:14:49 +00:00
committed by GitHub
parent 1bd317b983
commit b7f317af13
5 changed files with 606 additions and 248 deletions
@@ -23,8 +23,10 @@ import { makeSuccessAttempt } from 'shared/hooks/useAsync';
import Logger, { NullService } from 'teleterm/logger';
import {
makeLabelsList,
makeRetryableError,
makeRootCluster,
makeServer,
} from 'teleterm/services/tshd/testHelpers';
import { AppUpdaterContextProvider } from 'teleterm/ui/AppUpdater';
import { MockAppContextProvider } from 'teleterm/ui/fixtures/MockAppContextProvider';
@@ -389,6 +391,45 @@ it('closes on a click on an unfocusable element outside of the search bar', asyn
expect(screen.queryByRole('menu')).not.toBeInTheDocument();
});
it('shows secondary label matches for terms not already matched by the main field', async () => {
const user = userEvent.setup();
const appContext = setUpContext('/clusters/foo');
const resourceSearchResult = {
kind: 'server' as const,
requiresRequest: false,
resource: makeServer({
hostname: 'ansible',
addr: 'dev-host.internal:3022',
labels: makeLabelsList({ owner: 'an', env: 'dev', creator: 'admin' }),
}),
};
jest
.spyOn(appContext.resourcesService, 'searchResources')
.mockResolvedValue([resourceSearchResult]);
render(
<MockAppContextProvider appContext={appContext}>
<ConnectionsContextProvider>
<VnetContextProvider>
<SearchBarConnected />
</VnetContextProvider>
</ConnectionsContextProvider>
</MockAppContextProvider>
);
const searchbox = await screen.findByRole('searchbox');
await user.type(searchbox, 'ab dev');
expect(await screen.findByText('Connect over SSH')).toBeVisible();
const results = await screen.findByRole('menu');
expect(results).toHaveTextContent('env: dev');
// The result was already matched by the main field ("ansible").
expect(results).not.toHaveTextContent('owner: an');
// This label is omitted because it has no matches.
expect(results).not.toHaveTextContent('creator: admin');
});
const getMockedSearchContext = (): SearchContext.SearchContext => ({
inputValue: 'foo',
filters: [],
@@ -156,6 +156,9 @@ function SearchBar() {
height: 100%;
border: 1px ${props => props.theme.colors.buttons.border.border} solid;
border-radius: ${props => props.theme.radii[2]}px;
* {
font-weight: 400;
}
&:hover {
background: ${props => props.theme.colors.spotBackground[0]};
@@ -19,12 +19,15 @@
import { makeSuccessAttempt } from 'shared/hooks/useAsync';
import {
makeLabelsList,
makeRetryableError,
makeRootCluster,
makeServer,
} from 'teleterm/services/tshd/testHelpers';
import { makeResourceResult } from 'teleterm/ui/Search/testHelpers';
import { ResourceSearchError } from 'teleterm/ui/services/resources';
import { getActionPickerStatus } from './ActionPicker';
import { getActionPickerStatus, getVisibleMatches } from './ActionPicker';
describe('getActionPickerStatus', () => {
describe('some-input search mode', () => {
@@ -203,3 +206,100 @@ describe('getActionPickerStatus', () => {
});
});
});
describe('getVisibleMatches', () => {
it('excludes secondary field and label matches for terms already matched in the main field', () => {
const searchResult = makeResourceResult({
kind: 'server',
resource: makeServer({
labels: makeLabelsList({ env: 'teleport', team: 'platform' }),
}),
resourceMatches: [
{ field: 'hostname', searchTerm: 'teleport' },
{ field: 'addr', searchTerm: 'teleport' },
{ field: 'name', searchTerm: 'platform' },
],
labelMatches: [
{
kind: 'label-name',
labelName: 'env',
searchTerm: 'teleport',
score: 2,
},
{
kind: 'label-name',
labelName: 'team',
searchTerm: 'platform',
score: 3,
},
],
});
const visibleMatches = getVisibleMatches(searchResult, 'hostname');
// Not visible because `addr` only matched a term already matched in `hostname` (main field).
expect(visibleMatches.hasMatchOnField('addr')).toBe(false);
// Visible because `name` matched a different term than the one matched in `hostname`.
expect(visibleMatches.hasMatchOnField('name')).toBe(true);
// Only non-main-field term label matches remain.
expect(visibleMatches.labels.matches).toEqual([
{
kind: 'label-name',
labelName: 'team',
searchTerm: 'platform',
score: 3,
},
]);
expect(visibleMatches.labels.list).toEqual([
{ name: 'team', value: 'platform' },
]);
});
it('sorts visible labels by accumulated score in descending order', () => {
const searchResult = makeResourceResult({
kind: 'server',
resource: makeServer({
labels: makeLabelsList({
low: 'one',
high: 'two',
medium: 'three',
}),
}),
labelMatches: [
{
kind: 'label-name',
labelName: 'medium',
searchTerm: 'm1',
score: 2,
},
{
kind: 'label-value',
labelName: 'high',
searchTerm: 'h1',
score: 3,
},
{
kind: 'label-name',
labelName: 'high',
searchTerm: 'h2',
score: 2,
},
{
kind: 'label-name',
labelName: 'low',
searchTerm: 'l1',
score: 1,
},
],
});
const visibleMatches = getVisibleMatches(searchResult, 'hostname');
expect(visibleMatches.labels.list.map(label => label.name)).toEqual([
'high',
'medium',
'low',
]);
expect(visibleMatches.labels.matches).toHaveLength(4);
});
});
@@ -16,14 +16,17 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { ReactElement, useCallback, useMemo } from 'react';
import React, { ReactElement, ReactNode, useCallback, useMemo } from 'react';
import styled from 'styled-components';
import { Box, ButtonBorder, Label as DesignLabel, Flex, Text } from 'design';
import { Box, ButtonBorder, Flex, Label as DesignLabel, Text } from 'design';
import { makeLabelTag } from 'design/formatters';
import * as icons from 'design/Icon';
import { Cross as CloseIcon } from 'design/Icon';
import { IconProps } from 'design/Icon/Icon';
import { App } from 'gen-proto-ts/teleport/lib/teleterm/v1/app_pb';
import { Cluster } from 'gen-proto-ts/teleport/lib/teleterm/v1/cluster_pb';
import { Label as LabelProto } from 'gen-proto-ts/teleport/lib/teleterm/v1/label_pb';
import { AdvancedSearchToggle } from 'shared/components/AdvancedSearchToggle';
import { Highlight } from 'shared/components/Highlight';
import {
@@ -33,11 +36,11 @@ import {
} from 'shared/hooks/useAsync';
import { isWebApp } from 'teleterm/services/tshd/app';
import * as tsh from 'teleterm/services/tshd/types';
import { useAppContext } from 'teleterm/ui/appContextProvider';
import {
DisplayResults,
isClusterSearchFilter,
LabelMatch,
ResourceMatch,
ResourceSearchResult,
SearchFilter,
@@ -425,7 +428,7 @@ export function getActionPickerStatus({
inputValue: string;
filters: SearchFilter[];
filterActions: SearchAction[];
allClusters: tsh.Cluster[];
allClusters: Cluster[];
resourceSearchAttempt: Attempt<CrossClusterResourceSearchResult>;
}): ActionPickerStatus {
if (!inputValue) {
@@ -539,12 +542,10 @@ function ClusterFilterItem(props: SearchResultItem<SearchResultCluster>) {
<IconAndContent Icon={icons.Lan} iconColor="text.slightlyMuted">
<Text typography="body2">
Search only in{' '}
<strong>
<Highlight
text={props.searchResult.resource.name}
keywords={[props.searchResult.nameMatch]}
/>
</strong>
<Highlight
text={props.searchResult.resource.name}
keywords={[props.searchResult.nameMatch]}
/>
</Text>
</IconAndContent>
);
@@ -564,12 +565,10 @@ function DisplayResultsItem(props: SearchResultItem<DisplayResults>) {
{props.searchResult.value && (
<>
for{' '}
<strong>
<Highlight
keywords={[props.searchResult.value]}
text={props.searchResult.value}
/>
</strong>
<Highlight
keywords={[props.searchResult.value]}
text={props.searchResult.value}
/>
</>
)}
{props.searchResult.documentUri
@@ -577,7 +576,7 @@ function DisplayResultsItem(props: SearchResultItem<DisplayResults>) {
: ' in a new tab'}
</Text>
<Box ml="auto">
<Text typography="body4">
<Text typography="body4" color="text.muted">
{props.getOptionalClusterName(props.searchResult.clusterUri)}
</Text>
</Box>
@@ -611,12 +610,10 @@ function ResourceTypeFilterItem(
>
<Text typography="body2">
Search for{' '}
<strong>
<Highlight
text={resourceTypeToReadableName[props.searchResult.resource]}
keywords={[props.searchResult.nameMatch]}
/>
</strong>
<Highlight
text={resourceTypeToReadableName[props.searchResult.resource]}
keywords={[props.searchResult.nameMatch]}
/>
</Text>
</IconAndContent>
);
@@ -624,78 +621,71 @@ function ResourceTypeFilterItem(
export function ServerItem(props: SearchResultItem<SearchResultServer>) {
const { searchResult } = props;
const server = searchResult.resource;
const hasUuidMatches = searchResult.resourceMatches.some(
match => match.field === 'name'
);
const mainField = 'hostname';
const visibleMatches = getVisibleMatches(searchResult, mainField);
const hasUuidMatches = visibleMatches.hasMatchOnField('name');
const hasAddrMatches = visibleMatches.hasMatchOnField('addr');
return (
<IconAndContent
<ResourceItem
Icon={icons.Server}
iconColor="brand"
iconOpacity={getRequestableResourceIconOpacity(props.searchResult)}
>
<Flex
justifyContent="space-between"
alignItems="center"
flexWrap="wrap"
gap={1}
>
<Text typography="body2">
{props.searchResult.requiresRequest
? 'Request access to server '
: 'Connect over SSH to '}
<strong>
<HighlightField field="hostname" searchResult={searchResult} />
</strong>
</Text>
<Box ml="auto">
<Text typography="body4">
{props.getOptionalClusterName(server.uri)}
</Text>
</Box>
</Flex>
title={<HighlightField field={mainField} searchResult={searchResult} />}
action={
searchResult.requiresRequest
? 'Request access to server'
: 'Connect over SSH'
}
clusterName={props.getOptionalClusterName(searchResult.resource.uri)}
searchResult={props.searchResult}
details={
<Labels labels={visibleMatches.labels}>
{(hasAddrMatches || hasUuidMatches) && (
<ResourceFields>
{hasAddrMatches && (
<span>
<HighlightField field="addr" searchResult={searchResult} />
</span>
)}
<Labels searchResult={searchResult}>
<ResourceFields>
{server.tunnel ? (
<span title="This node is connected to the cluster through a reverse tunnel">
↵ tunnel
</span>
) : (
<span>
<HighlightField field="addr" searchResult={searchResult} />
</span>
{hasUuidMatches && (
<span>
UUID:{' '}
<HighlightField field={'name'} searchResult={searchResult} />
</span>
)}
</ResourceFields>
)}
{hasUuidMatches && (
<span>
UUID:{' '}
<HighlightField field={'name'} searchResult={searchResult} />
</span>
)}
</ResourceFields>
</Labels>
</IconAndContent>
</Labels>
}
/>
);
}
export function DatabaseItem(props: SearchResultItem<SearchResultDatabase>) {
const { searchResult } = props;
const db = searchResult.resource;
const mainField = 'name';
const visibleMatches = getVisibleMatches(searchResult, mainField);
const hasDescMatches = visibleMatches.hasMatchOnField('desc');
const hasTypeMatches = visibleMatches.hasMatchOnField('type');
const hasProtocolMatches = visibleMatches.hasMatchOnField('protocol');
const $resourceFields = (
const $resourceFields = (hasTypeMatches ||
hasProtocolMatches ||
hasDescMatches) && (
<ResourceFields>
<span
css={`
flex-shrink: 0;
`}
>
<HighlightField field="type" searchResult={searchResult} />
/
<HighlightField field="protocol" searchResult={searchResult} />
</span>
{db.desc && (
{(hasTypeMatches || hasProtocolMatches) && (
<span
css={`
flex-shrink: 0;
`}
>
<HighlightField field="type" searchResult={searchResult} />
/
<HighlightField field="protocol" searchResult={searchResult} />
</span>
)}
{hasDescMatches && (
<span
css={`
overflow: hidden;
@@ -710,61 +700,47 @@ export function DatabaseItem(props: SearchResultItem<SearchResultDatabase>) {
);
return (
<IconAndContent
<ResourceItem
Icon={icons.Database}
iconColor="brand"
iconOpacity={getRequestableResourceIconOpacity(props.searchResult)}
>
<Flex
justifyContent="space-between"
alignItems="center"
flexWrap="wrap"
gap={1}
>
<Text typography="body2">
{props.searchResult.requiresRequest
? 'Request access to db '
: 'Set up a db connection to '}
<strong>
<HighlightField field="name" searchResult={searchResult} />
</strong>
</Text>
<Box ml="auto">
<Text typography="body4">{props.getOptionalClusterName(db.uri)}</Text>
</Box>
</Flex>
{/* If the description is long, put the resource fields on a separate line.
title={<HighlightField field={mainField} searchResult={searchResult} />}
searchResult={props.searchResult}
action={
searchResult.requiresRequest
? 'Request access to db'
: 'Set up a db connection'
}
clusterName={props.getOptionalClusterName(searchResult.resource.uri)}
details={
<>
{/* If the description is long, put the resource fields on a separate line.
Otherwise show the resource fields and the labels together in a single line.
*/}
{db.desc.length >= 30 ? (
<>
{$resourceFields}
<Labels searchResult={searchResult} />
{db.desc.length >= 30 ? (
<>
{$resourceFields}
<Labels labels={visibleMatches.labels} />
</>
) : (
<Labels labels={visibleMatches.labels}>{$resourceFields}</Labels>
)}
</>
) : (
<Labels searchResult={searchResult}>{$resourceFields}</Labels>
)}
</IconAndContent>
}
/>
);
}
export function AppItem(props: SearchResultItem<SearchResultApp>) {
const { searchResult } = props;
const app = searchResult.resource;
const mainField = app.friendlyName ? 'friendlyName' : 'name';
const visibleMatches = getVisibleMatches(searchResult, mainField);
const hasAddrWithProtocolMatches =
visibleMatches.hasMatchOnField('addrWithProtocol');
const hasDescMatches = visibleMatches.hasMatchOnField('desc');
const $appName = (
<strong>
<HighlightField
field={app.friendlyName ? 'friendlyName' : 'name'}
searchResult={searchResult}
/>
</strong>
);
const $resourceFields = (app.addrWithProtocol || app.desc) && (
const $resourceFields = (hasAddrWithProtocolMatches || hasDescMatches) && (
<ResourceFields>
{app.addrWithProtocol && (
{hasAddrWithProtocolMatches && (
<span
css={`
flex-shrink: 0;
@@ -776,7 +752,7 @@ export function AppItem(props: SearchResultItem<SearchResultApp>) {
/>
</span>
)}
{app.desc && (
{hasDescMatches && (
<span
css={`
overflow: hidden;
@@ -791,44 +767,32 @@ export function AppItem(props: SearchResultItem<SearchResultApp>) {
);
return (
<IconAndContent
<ResourceItem
Icon={icons.Application}
iconColor="brand"
iconOpacity={getRequestableResourceIconOpacity(props.searchResult)}
>
<Flex
justifyContent="space-between"
alignItems="center"
flexWrap="wrap"
gap={1}
>
<Text typography="body2">
{getAppItemCopy(
$appName,
app,
searchResult.requiresRequest,
props.isVnetSupported
)}
</Text>
<Box ml="auto">
<Text typography="body4">
{props.getOptionalClusterName(app.uri)}
</Text>
</Box>
</Flex>
{/* If the description is long, put the resource fields on a separate line.
title={<HighlightField field={mainField} searchResult={searchResult} />}
searchResult={props.searchResult}
action={getAppItemCopy(
app,
searchResult.requiresRequest,
props.isVnetSupported
)}
clusterName={props.getOptionalClusterName(searchResult.resource.uri)}
details={
<>
{/* If the description is long, put the resource fields on a separate line.
Otherwise, show the resource fields and the labels together in a single line.
*/}
{app.desc.length >= 30 ? (
<>
{$resourceFields}
<Labels searchResult={searchResult} />
{app.desc.length >= 30 ? (
<>
{$resourceFields}
<Labels labels={visibleMatches.labels} />
</>
) : (
<Labels labels={visibleMatches.labels}>{$resourceFields}</Labels>
)}
</>
) : (
<Labels searchResult={searchResult}>{$resourceFields}</Labels>
)}
</IconAndContent>
}
/>
);
}
@@ -836,9 +800,13 @@ export function WindowsDesktopItem(
props: SearchResultItem<SearchResultWindowsDesktop>
) {
const { searchResult } = props;
const windowsDesktop = searchResult.resource;
const mainField = 'name';
const visibleMatches = getVisibleMatches(searchResult, mainField);
const hasAddrWithoutDefaultPortMatches = visibleMatches.hasMatchOnField(
'addrWithoutDefaultPort'
);
const $resourceFields = (
const $resourceFields = hasAddrWithoutDefaultPortMatches && (
<ResourceFields>
<span
css={`
@@ -854,94 +822,118 @@ export function WindowsDesktopItem(
);
return (
<IconAndContent
<ResourceItem
Icon={icons.Desktop}
iconColor="brand"
iconOpacity={getRequestableResourceIconOpacity(props.searchResult)}
>
<Flex
justifyContent="space-between"
alignItems="center"
flexWrap="wrap"
gap={1}
>
<Text typography="body2">
{props.searchResult.requiresRequest
? 'Request access to desktop '
: 'Connect to desktop '}
<strong>
<HighlightField field="name" searchResult={searchResult} />
</strong>
</Text>
<Box ml="auto">
<Text typography="body4">
{props.getOptionalClusterName(windowsDesktop.uri)}
</Text>
</Box>
</Flex>
<Labels searchResult={searchResult}>{$resourceFields}</Labels>
</IconAndContent>
title={<HighlightField field={mainField} searchResult={searchResult} />}
searchResult={props.searchResult}
action={
searchResult.requiresRequest
? 'Request access to desktop'
: 'Connect to desktop'
}
clusterName={props.getOptionalClusterName(searchResult.resource.uri)}
details={
<Labels labels={visibleMatches.labels}>{$resourceFields}</Labels>
}
/>
);
}
function getAppItemCopy(
$appName: React.JSX.Element,
app: App,
requiresRequest: boolean,
isVnetSupported: boolean
) {
): string {
if (requiresRequest) {
return <>Request access to app {$appName}</>;
return 'Request access to the app';
}
if (app.samlApp) {
return <>Log in to {$appName} in the browser</>;
return 'Log in via the browser';
}
if (isWebApp(app) || app.awsConsole) {
return <>Launch {$appName} in the browser</>;
return 'Launch in the browser';
}
// TCP app
if (isVnetSupported) {
return <>Connect with VNet to {$appName}</>;
return 'Connect with VNet';
}
return <>Set up an app connection to {$appName}</>;
return 'Set up an app connection';
}
export function KubeItem(props: SearchResultItem<SearchResultKube>) {
const { searchResult } = props;
const mainField = 'name';
const visibleMatches = getVisibleMatches(searchResult, mainField);
return (
<IconAndContent
<ResourceItem
Icon={icons.Kubernetes}
iconColor="brand"
iconOpacity={getRequestableResourceIconOpacity(props.searchResult)}
>
<Flex
justifyContent="space-between"
alignItems="center"
flexWrap="wrap"
gap={1}
>
<Text typography="body2">
{props.searchResult.requiresRequest
? 'Request access to Kubernetes cluster '
: 'Log in to Kubernetes cluster '}
<strong>
<HighlightField field="name" searchResult={searchResult} />
</strong>
</Text>
<Box ml="auto">
<Text typography="body4">
{props.getOptionalClusterName(searchResult.resource.uri)}
</Text>
</Box>
</Flex>
<Labels searchResult={searchResult} />
</IconAndContent>
title={<HighlightField field={mainField} searchResult={searchResult} />}
searchResult={props.searchResult}
action={
searchResult.requiresRequest
? 'Request access to Kubernetes cluster'
: 'Log in to Kubernetes cluster'
}
clusterName={props.getOptionalClusterName(searchResult.resource.uri)}
details={<Labels labels={visibleMatches.labels} />}
/>
);
}
/**
* Computes secondary matches to display in the item details.
* It omits resource and label matches for terms already matched in the main field,
* reducing duplicate highlights.
*/
export function getVisibleMatches(
searchResult: ResourceSearchResult,
mainField: ResourceMatch<ResourceSearchResult['kind']>['field']
) {
const mainFieldMatchedTerms = new Set(
searchResult.resourceMatches
.filter(match => match.field === mainField)
.map(match => match.searchTerm)
);
const secondaryResourceMatchFields = new Set<
ResourceMatch<ResourceSearchResult['kind']>['field']
>();
for (const match of searchResult.resourceMatches) {
if (!mainFieldMatchedTerms.has(match.searchTerm)) {
secondaryResourceMatchFields.add(match.field);
}
}
const labelMatches = searchResult.labelMatches.filter(
match => !mainFieldMatchedTerms.has(match.searchTerm)
);
const labelScores = new Map<string, number>();
for (const match of labelMatches) {
const currentScore = labelScores.get(match.labelName) || 0;
labelScores.set(match.labelName, currentScore + match.score);
}
const labels = searchResult.resource.labels
.filter(label => labelScores.has(label.name))
.toSorted(
(a, b) =>
// Highest score first.
(labelScores.get(b.name) || 0) - (labelScores.get(a.name) || 0)
);
return {
labels: {
list: labels,
matches: labelMatches,
},
hasMatchOnField: (
field: ResourceMatch<ResourceSearchResult['kind']>['field']
) => secondaryResourceMatchFields.has(field),
};
}
export function NoResultsItem(props: {
clustersWithExpiredCerts: Set<uri.ClusterUri>;
getClusterName: (resourceUri: uri.ClusterOrResourceUri) => string;
@@ -1072,32 +1064,25 @@ export function ResourceSearchErrorsItem(props: {
function Labels(
props: React.PropsWithChildren<{
searchResult: ResourceSearchResult;
labels: {
list: LabelProto[];
matches: LabelMatch[];
};
}>
) {
const { searchResult } = props;
const { labels } = props;
// Label name to score.
const scoreMap: Map<string, number> = new Map();
searchResult.labelMatches.forEach(match => {
const currentScore = scoreMap.get(match.labelName) || 0;
scoreMap.set(match.labelName, currentScore + match.score);
});
const sortedLabelsList = [...searchResult.resource.labels];
sortedLabelsList.sort(
(a, b) =>
// Highest score first.
(scoreMap.get(b.name) || 0) - (scoreMap.get(a.name) || 0)
);
if (!(props.children || labels.list.length)) {
return;
}
return (
<LabelsFlex>
{props.children}
{sortedLabelsList.map(label => (
{labels.list.map(label => (
<Label
key={label.name + label.value}
searchResult={searchResult}
labelMatches={labels.matches}
label={label}
/>
))}
@@ -1123,14 +1108,9 @@ const ResourceFields = styled(Flex).attrs({ gap: 1 })`
font-size: ${props => props.theme.fontSizes[0]}px;
`;
function Label(props: {
searchResult: ResourceSearchResult;
label: tsh.Label;
}) {
const { searchResult: item, label } = props;
const labelMatches = item.labelMatches.filter(
match => match.labelName == label.name
);
function Label(props: { labelMatches: LabelMatch[]; label: LabelProto }) {
let { label, labelMatches } = props;
labelMatches = labelMatches.filter(match => match.labelName == label.name);
const nameMatches = labelMatches
.filter(match => match.kind === 'label-name')
.map(match => match.searchTerm);
@@ -1233,6 +1213,41 @@ function ContentAndAdvancedSearch(
);
}
function ResourceItem(props: {
Icon: React.ComponentType<IconProps>;
searchResult: { requiresRequest: boolean };
title: ReactNode;
action: ReactNode;
clusterName: string;
details?: ReactNode;
}) {
return (
<IconAndContent
Icon={props.Icon}
iconColor="brand"
iconOpacity={getRequestableResourceIconOpacity(props.searchResult)}
>
<Flex
justifyContent="space-between"
alignItems="center"
flexWrap="wrap"
gap={1}
>
<Text typography="body2">{props.title}</Text>
<Text typography="body2" color="text.muted">
{props.action}
</Text>
<Box ml="auto">
<Text typography="body4" color="text.muted">
{props.clusterName}
</Text>
</Box>
</Flex>
{props.details}
</IconAndContent>
);
}
function getRequestableResourceIconOpacity(args: { requiresRequest: boolean }) {
// Unified resources use 0.5 opacity for the requestable resources.
return args.requiresRequest ? 0.5 : 1;
@@ -123,8 +123,36 @@ const SearchResultItems = () => {
const searchResults: SearchResult[] = [
makeResourceResult({
kind: 'server',
resourceMatches: [{ field: 'addr', searchTerm: '10.0.0.175' }],
labelMatches: [
{
kind: 'label-name',
labelName: 'service',
searchTerm: 'service',
score: 40,
},
{
kind: 'label-value',
labelName: 'service',
searchTerm: 'ansible',
score: 60,
},
{
kind: 'label-name',
labelName: 'kernel',
searchTerm: 'kernel',
score: 45,
},
{
kind: 'label-value',
labelName: 'arch',
searchTerm: 'aarch64',
score: 55,
},
],
resource: makeServer({
hostname: 'long-label-list',
addr: '10.0.0.175:3022',
uri: `${clusterUri}/servers/2f96e498-88ec-442f-a25b-569fa915041c`,
name: '2f96e498-88ec-442f-a25b-569fa915041c',
labels: makeLabelsList({
@@ -138,6 +166,14 @@ const SearchResultItems = () => {
}),
makeResourceResult({
kind: 'server',
labelMatches: [
{
kind: 'label-value',
labelName: 'service',
searchTerm: 'ansible',
score: 100,
},
],
resource: makeServer({
hostname: 'short-label-list',
addr: '',
@@ -171,6 +207,7 @@ const SearchResultItems = () => {
}),
makeResourceResult({
kind: 'server',
resourceMatches: [{ field: 'hostname', searchTerm: 'super' }],
resource: makeServer({
hostname:
'super-long-server-name-with-uuid-2f96e498-88ec-442f-a25b-569fa915041c',
@@ -186,6 +223,32 @@ const SearchResultItems = () => {
makeResourceResult({
kind: 'server',
requiresRequest: true,
labelMatches: [
{
kind: 'label-name',
labelName: 'service',
searchTerm: 'service',
score: 40,
},
{
kind: 'label-value',
labelName: 'service',
searchTerm: 'ansible',
score: 60,
},
{
kind: 'label-name',
labelName: 'kernel',
searchTerm: 'kernel',
score: 45,
},
{
kind: 'label-value',
labelName: 'arch',
searchTerm: 'aarch64',
score: 55,
},
],
resource: makeServer({
hostname: 'long-label-list',
uri: `${clusterUri}/servers/2f96e498-88ec-442f-a25b-569fa915041c`,
@@ -201,6 +264,14 @@ const SearchResultItems = () => {
}),
makeResourceResult({
kind: 'app',
labelMatches: [
{
kind: 'label-value',
labelName: 'env',
searchTerm: 'dev',
score: 100,
},
],
resource: makeAppWithAddr({
uri: `${clusterUri}/apps/web-app`,
name: 'web-app',
@@ -217,6 +288,15 @@ const SearchResultItems = () => {
}),
makeResourceResult({
kind: 'app',
resourceMatches: [{ field: 'desc', searchTerm: 'SAML' }],
labelMatches: [
{
kind: 'label-name',
labelName: 'aws/Owner',
searchTerm: 'Owner',
score: 35,
},
],
resource: makeAppWithAddr({
uri: `${clusterUri}/apps/saml-app`,
name: 'saml-app',
@@ -234,6 +314,7 @@ const SearchResultItems = () => {
}),
makeResourceResult({
kind: 'app',
resourceMatches: [{ field: 'addrWithProtocol', searchTerm: 'local' }],
resource: makeAppWithAddr({
uri: `${clusterUri}/apps/no-desc`,
name: 'no-desc',
@@ -249,6 +330,7 @@ const SearchResultItems = () => {
}),
makeResourceResult({
kind: 'app',
resourceMatches: [{ field: 'desc', searchTerm: 'Lorem' }],
resource: makeAppWithAddr({
uri: `${clusterUri}/apps/short-desc`,
name: 'short-desc',
@@ -264,6 +346,15 @@ const SearchResultItems = () => {
}),
makeResourceResult({
kind: 'app',
resourceMatches: [{ field: 'desc', searchTerm: 'dignissim' }],
labelMatches: [
{
kind: 'label-value',
labelName: 'aws/Environment',
searchTerm: 'demo',
score: 50,
},
],
resource: makeAppWithAddr({
uri: `${clusterUri}/apps/long-desc`,
name: 'long-desc',
@@ -279,6 +370,7 @@ const SearchResultItems = () => {
}),
makeResourceResult({
kind: 'app',
resourceMatches: [{ field: 'desc', searchTerm: 'Duis' }],
resource: makeAppWithAddr({
uri: `${clusterUri}/apps/super-long-desc`,
name: 'super-long-desc',
@@ -294,6 +386,15 @@ const SearchResultItems = () => {
}),
makeResourceResult({
kind: 'app',
resourceMatches: [{ field: 'desc', searchTerm: 's' }],
labelMatches: [
{
kind: 'label-name',
labelName: 'access',
searchTerm: 's',
score: 40,
},
],
resource: makeAppWithAddr({
name: 'super-long-app-with-uuid-1f96e498-88ec-442f-a25b-569fa915041c',
desc: 'short-desc',
@@ -311,6 +412,14 @@ const SearchResultItems = () => {
makeResourceResult({
kind: 'app',
requiresRequest: true,
labelMatches: [
{
kind: 'label-name',
labelName: 'access',
searchTerm: 's',
score: 40,
},
],
resource: makeAppWithAddr({
uri: `${clusterUri}/apps/web-app`,
name: 'web-app',
@@ -327,6 +436,18 @@ const SearchResultItems = () => {
}),
makeResourceResult({
kind: 'database',
resourceMatches: [
{ field: 'type', searchTerm: 'self' },
{ field: 'protocol', searchTerm: 'postgres' },
],
labelMatches: [
{
kind: 'label-value',
labelName: 'env',
searchTerm: 'dev',
score: 100,
},
],
resource: makeDatabase({
uri: `${clusterUri}/dbs/no-desc`,
name: 'no-desc',
@@ -345,6 +466,15 @@ const SearchResultItems = () => {
}),
makeResourceResult({
kind: 'database',
resourceMatches: [{ field: 'desc', searchTerm: 'm' }],
labelMatches: [
{
labelName: 'aws/Environment',
score: 40,
kind: 'label-name',
searchTerm: 'm',
},
],
resource: makeDatabase({
uri: `${clusterUri}/dbs/short-desc`,
name: 'short-desc',
@@ -363,6 +493,7 @@ const SearchResultItems = () => {
}),
makeResourceResult({
kind: 'database',
resourceMatches: [{ field: 'desc', searchTerm: 'dignissim' }],
resource: makeDatabase({
uri: `${clusterUri}/dbs/long-desc`,
name: 'long-desc',
@@ -381,6 +512,7 @@ const SearchResultItems = () => {
}),
makeResourceResult({
kind: 'database',
resourceMatches: [{ field: 'desc', searchTerm: 'Duis' }],
resource: makeDatabase({
uri: `${clusterUri}/dbs/super-long-desc`,
name: 'super-long-desc',
@@ -435,6 +567,14 @@ const SearchResultItems = () => {
}),
makeResourceResult({
kind: 'kube',
labelMatches: [
{
kind: 'label-value',
labelName: 'kube',
searchTerm: 'kubersson',
score: 100,
},
],
resource: makeKube({
name: 'short-label-list',
labels: makeLabelsList({
@@ -446,6 +586,26 @@ const SearchResultItems = () => {
}),
makeResourceResult({
kind: 'kube',
labelMatches: [
{
kind: 'label-name',
labelName: 'aws/Environment',
searchTerm: 'Environment',
score: 60,
},
{
kind: 'label-name',
labelName: 'aws/Owner',
searchTerm: 'Owner',
score: 45,
},
{
kind: 'label-value',
labelName: 'kube',
searchTerm: 'kubersson',
score: 70,
},
],
resource: makeKube({
name: 'long-label-list',
uri: `${clusterUri}/kubes/long-label-list`,
@@ -473,6 +633,14 @@ const SearchResultItems = () => {
makeResourceResult({
kind: 'kube',
requiresRequest: true,
labelMatches: [
{
kind: 'label-value',
labelName: 'im-just-a-smol',
searchTerm: 'kube',
score: 40,
},
],
resource: makeKube({
name: 'short-label-list',
labels: makeLabelsList({
@@ -509,10 +677,41 @@ const SearchResultItems = () => {
with: 'little-to-no-labels',
}),
}),
resourceMatches: [
{ field: 'addrWithoutDefaultPort', searchTerm: '192.169.100.50' },
],
labelMatches: [
{
kind: 'label-value',
labelName: 'windowsDesktops',
searchTerm: 'custom',
score: 100,
},
{
kind: 'label-name',
labelName: 'aws/Environment',
searchTerm: 'Environment',
score: 60,
},
{
kind: 'label-name',
labelName: 'aws/Owner',
searchTerm: 'Owner',
score: 45,
},
],
}),
makeResourceResult({
kind: 'windows_desktop',
requiresRequest: true,
labelMatches: [
{
kind: 'label-value',
labelName: 'im-just-a-smol',
searchTerm: 'win',
score: 40,
},
],
resource: makeWindowsDesktopWithoutDefaultPort({
uri: `${clusterUri}/windows_desktops/short-label-list`,
name: 'short-label-list',