Skip to content

Commit a87c67b

Browse files
authored
Web/Teleterm: Add status state to shared unified resources view (#53748)
- Only supported for database atm - Adds info guide side panel for unified resources view in Teleterm
1 parent aba0de0 commit a87c67b

File tree

22 files changed

+793
-246
lines changed

22 files changed

+793
-246
lines changed

web/packages/shared/components/UnifiedResources/CardsView/CardsView.tsx

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -38,24 +38,22 @@ export function CardsView({
3838
}: ResourceViewProps) {
3939
return (
4040
<CardsContainer className="CardsContainer" gap={2}>
41-
{mappedResources.map(({ item, key }) => (
42-
<ResourceCard
43-
key={key}
44-
name={item.name}
45-
ActionButton={item.ActionButton}
46-
primaryIconName={item.primaryIconName}
47-
onLabelClick={onLabelClick}
48-
SecondaryIcon={item.SecondaryIcon}
49-
cardViewProps={item.cardViewProps}
50-
labels={item.labels}
51-
pinned={pinnedResources.includes(key)}
52-
requiresRequest={item.requiresRequest}
53-
pinningSupport={pinningSupport}
54-
selected={selectedResources.includes(key)}
55-
selectResource={() => onSelectResource(key)}
56-
pinResource={() => onPinResource(key)}
57-
/>
58-
))}
41+
{mappedResources.map(
42+
({ item, key, onShowStatusInfo, showingStatusInfo }) => (
43+
<ResourceCard
44+
key={key}
45+
viewItem={item}
46+
onLabelClick={onLabelClick}
47+
pinned={pinnedResources.includes(key)}
48+
pinningSupport={pinningSupport}
49+
selected={selectedResources.includes(key)}
50+
selectResource={() => onSelectResource(key)}
51+
pinResource={() => onPinResource(key)}
52+
onShowStatusInfo={onShowStatusInfo}
53+
showingStatusInfo={showingStatusInfo}
54+
/>
55+
)
56+
)}
5957
{isProcessing && (
6058
<LoadingSkeleton count={FETCH_MORE_SIZE} Element={<LoadingCard />} />
6159
)}

web/packages/shared/components/UnifiedResources/CardsView/ResourceCard.story.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -134,12 +134,9 @@ export const Cards: Story = {
134134
selectResource={() => {}}
135135
selected={false}
136136
pinningSupport={PinningSupport.Supported}
137-
name={res.name}
138-
primaryIconName={res.primaryIconName}
139-
SecondaryIcon={res.SecondaryIcon}
140-
cardViewProps={res.cardViewProps}
141-
labels={res.labels}
142-
ActionButton={res.ActionButton}
137+
onShowStatusInfo={() => null}
138+
showingStatusInfo={false}
139+
viewItem={res}
143140
/>
144141
))}
145142
</Grid>

web/packages/shared/components/UnifiedResources/CardsView/ResourceCard.tsx

Lines changed: 132 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,12 @@ import { CopyButton } from '../shared/CopyButton';
3030
import {
3131
BackgroundColorProps,
3232
getBackgroundColor,
33+
getStatusBackgroundColor,
3334
} from '../shared/getBackgroundColor';
3435
import { PinButton } from '../shared/PinButton';
36+
import { SingleLineBox } from '../shared/SingleLineBox';
3537
import { ResourceItemProps } from '../types';
38+
import { WarningRightEdgeBadgeSvg } from './WarningRightEdgeBadgeSvg';
3639

3740
// Since we do a lot of manual resizing and some absolute positioning, we have
3841
// to put some layout constants in place here.
@@ -51,20 +54,26 @@ const ResTypeIconBox = styled(Box)`
5154
`;
5255

5356
export function ResourceCard({
54-
name,
55-
primaryIconName,
56-
SecondaryIcon,
5757
onLabelClick,
58-
cardViewProps,
59-
ActionButton,
60-
labels,
6158
pinningSupport,
6259
pinned,
6360
pinResource,
6461
selectResource,
65-
requiresRequest,
6662
selected,
67-
}: Omit<ResourceItemProps, 'listViewProps' | 'expandAllLabels'>) {
63+
onShowStatusInfo,
64+
showingStatusInfo,
65+
viewItem,
66+
}: Omit<ResourceItemProps, 'expandAllLabels'>) {
67+
const {
68+
name,
69+
primaryIconName,
70+
SecondaryIcon,
71+
cardViewProps,
72+
ActionButton,
73+
labels,
74+
requiresRequest,
75+
status,
76+
} = viewItem;
6877
const { primaryDesc, secondaryDesc } = cardViewProps;
6978

7079
const [showMoreLabelsButton, setShowMoreLabelsButton] = useState(false);
@@ -157,12 +166,18 @@ export function ResourceCard({
157166
}
158167
};
159168

169+
const hasUnhealthyStatus = status && status !== 'healthy';
170+
160171
return (
161172
<CardContainer
162173
onMouseEnter={() => setHovered(true)}
163174
onMouseLeave={() => setHovered(false)}
175+
showingStatusInfo={showingStatusInfo}
164176
>
165-
<CardOuterContainer showAllLabels={showAllLabels}>
177+
<CardOuterContainer
178+
showAllLabels={showAllLabels}
179+
hasUnhealthyStatus={hasUnhealthyStatus}
180+
>
166181
<CardInnerContainer
167182
ref={innerContainer}
168183
p={3}
@@ -174,6 +189,12 @@ export function ResourceCard({
174189
pinned={pinned}
175190
requiresRequest={requiresRequest}
176191
selected={selected}
192+
showingStatusInfo={showingStatusInfo}
193+
hasUnhealthyStatus={hasUnhealthyStatus}
194+
// we set extra padding to push contents to the right to make
195+
// space for the WarningRightEdgeBadgeIcon.
196+
{...(hasUnhealthyStatus && !showAllLabels && { pr: '35px' })}
197+
{...(hasUnhealthyStatus && showAllLabels && { pr: '7px' })}
177198
>
178199
<HoverTooltip tipContent={selected ? 'Deselect' : 'Select'}>
179200
<CheckboxInput
@@ -242,7 +263,10 @@ export function ResourceCard({
242263
)}
243264
</Flex>
244265
<LabelsContainer showAll={showAllLabels}>
245-
<LabelsInnerContainer ref={labelsInnerContainer}>
266+
<LabelsInnerContainer
267+
ref={labelsInnerContainer}
268+
hasUnhealthyStatus={hasUnhealthyStatus}
269+
>
246270
<MoreLabelsButton
247271
style={{
248272
visibility:
@@ -271,36 +295,88 @@ export function ResourceCard({
271295
</LabelsInnerContainer>
272296
</LabelsContainer>
273297
</Flex>
298+
{hasUnhealthyStatus && !showAllLabels && (
299+
<HoverTooltip tipContent={'Show Connection Issue'} placement="left">
300+
<WarningRightEdgeBadgeIcon onClick={onShowStatusInfo} />
301+
</HoverTooltip>
302+
)}
274303
</CardInnerContainer>
275304
</CardOuterContainer>
305+
{/* This is to let the WarningRightEdgeBadgeIcon stay in place while the
306+
InnerContainer pops out and expands vertically from rendering all
307+
labels. */}
308+
{hasUnhealthyStatus && showAllLabels && <WarningRightEdgeBadgeIcon />}
276309
</CardContainer>
277310
);
278311
}
279312

313+
const WarningRightEdgeBadgeIcon = ({ onClick }: { onClick?(): void }) => {
314+
return (
315+
<Box
316+
onClick={onClick}
317+
css={`
318+
position: absolute;
319+
top: 0;
320+
right: 0;
321+
cursor: pointer;
322+
height: 100%;
323+
`}
324+
>
325+
<WarningRightEdgeBadgeSvg />
326+
</Box>
327+
);
328+
};
329+
280330
/**
281331
* The outer container's purpose is to reserve horizontal space on the resource
282332
* grid. It holds the inner container that normally holds a regular layout of
283333
* the card, and is fully contained inside the outer container. Once the user
284334
* clicks the "more" button, the inner container "pops out" by changing its
285335
* position to absolute.
286336
*
337+
* The card height is fixed to allow the WarningRightEdgeBadgeIcon to stay in
338+
* place when user clicks on "showAllLabels". Without the fixed height, the
339+
* container's height shrinks when the inner container pops out, resulting in
340+
* the svg to jump around (from size difference) and or disappearing.
341+
*
287342
* TODO(bl-nero): Known issue: this doesn't really work well with one-column
288-
* layout; we may need to globally set the card height to fixed size on the
289-
* outer container.
343+
* layout;
290344
*/
291-
const CardContainer = styled(Box)`
345+
const CardContainer = styled(Box)<{
346+
showingStatusInfo: boolean;
347+
}>`
348+
height: 110px;
349+
292350
position: relative;
351+
.resource-health-status-svg {
352+
width: 100%;
353+
height: 100%;
354+
355+
fill: ${p =>
356+
p.showingStatusInfo
357+
? p.theme.colors.interactive.solid.alert.active
358+
: p.theme.colors.interactive.solid.alert.default};
359+
}
360+
&:hover {
361+
.resource-health-status-svg {
362+
fill: ${p => p.theme.colors.interactive.solid.alert.hover};
363+
}
364+
}
293365
`;
294366

295-
const CardOuterContainer = styled(Box)<{ showAllLabels?: boolean }>`
367+
const CardOuterContainer = styled(Box)<{
368+
showAllLabels?: boolean;
369+
hasUnhealthyStatus: boolean;
370+
}>`
296371
border-radius: ${props => props.theme.radii[3]}px;
297372
298373
${props =>
299374
props.showAllLabels &&
300375
css`
301376
position: absolute;
302377
left: 0;
303-
right: 0;
378+
// The padding is required to show the WarningRightEdgeBadgeIcon
379+
right: ${props.hasUnhealthyStatus ? '28px' : 0};
304380
z-index: 1;
305381
`}
306382
transition: all 150ms;
@@ -340,16 +416,40 @@ const CardInnerContainer = styled(Flex)<BackgroundColorProps>`
340416
border-radius: ${props => props.theme.radii[3]}px;
341417
background-color: ${props => getBackgroundColor(props)};
342418
419+
${p =>
420+
p.hasUnhealthyStatus &&
421+
css`
422+
border: 2px solid ${p.theme.colors.interactive.solid.alert.default};
423+
background-color: ${getStatusBackgroundColor({
424+
showingStatusInfo: p.showingStatusInfo,
425+
theme: p.theme,
426+
action: '',
427+
viewType: 'card',
428+
})};
429+
`}
430+
431+
${p =>
432+
p.showingStatusInfo &&
433+
css`
434+
border: 2px solid ${p.theme.colors.interactive.solid.alert.active};
435+
`}
436+
343437
&:hover {
344438
// Make the border invisible instead of removing it, this is to prevent things from shifting due to the size change.
345439
border: ${props => props.theme.borders[2]} rgba(0, 0, 0, 0);
346-
}
347-
`;
348440
349-
const SingleLineBox = styled(Box)`
350-
overflow: hidden;
351-
white-space: nowrap;
352-
text-overflow: ellipsis;
441+
${p =>
442+
p.hasUnhealthyStatus &&
443+
css`
444+
border-color: ${p.theme.colors.interactive.solid.alert.hover};
445+
background-color: ${getStatusBackgroundColor({
446+
showingStatusInfo: p.showingStatusInfo,
447+
theme: p.theme,
448+
action: 'hover',
449+
viewType: 'card',
450+
})};
451+
`}
452+
}
353453
`;
354454

355455
/**
@@ -376,12 +476,16 @@ const StyledLabel = styled(Label)`
376476
* The inner labels container always adapts to the size of labels. Its height
377477
* is measured by the resize observer.
378478
*/
379-
const LabelsInnerContainer = styled(Flex)`
479+
const LabelsInnerContainer = styled(Flex)<{ hasUnhealthyStatus: boolean }>`
380480
position: relative;
381481
flex-wrap: wrap;
382482
align-items: start;
383483
gap: ${props => props.theme.space[1]}px;
384-
padding-right: 60px;
484+
// Padding is required to prevent the more label button to not collide
485+
// with the rendered labels. Just a tiny bit more padding needed to
486+
// accomodate contents getting pushed more to the right when a
487+
// WarningRightEdgeBadgeIcon renders.
488+
padding-right: ${p => (p.hasUnhealthyStatus ? '75px' : '74px')};
385489
`;
386490

387491
/**
@@ -397,10 +501,14 @@ const MoreLabelsButton = styled(ButtonLink)`
397501
margin: ${labelVerticalMargin}px 0;
398502
min-height: 0;
399503
400-
background-color: ${props => getBackgroundColor(props)};
504+
background-color: transparent;
401505
color: ${props => props.theme.colors.text.slightlyMuted};
402506
font-style: italic;
403507
404508
transition: visibility 0s;
405509
transition: background 150ms;
510+
511+
&:hover {
512+
background-color: transparent;
513+
}
406514
`;
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* Teleport
3+
* Copyright (C) 2025 Gravitational, Inc.
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
/**
20+
* This svg is different from existing warning svg's in that it's specifically
21+
* tailored to fit the design of unified ResourceCard.tsx to the right edge
22+
* of the component.
23+
*/
24+
export function WarningRightEdgeBadgeSvg() {
25+
return (
26+
<svg
27+
/**
28+
* "className" is used instead of "fill" to style (color) the svg.
29+
* The svg needs to change style when user hovers anywhere in the
30+
* container the svg is a part of. This required the use of "className"
31+
* to target the svg as part of a container.
32+
*/
33+
className="resource-health-status-svg"
34+
width="32"
35+
height="102"
36+
viewBox="0 0 32 102"
37+
fill="none"
38+
xmlns="http://www.w3.org/2000/svg"
39+
preserveAspectRatio="none"
40+
>
41+
<path d="M24 0H0V2C3.31371 2 6 4.68629 6 8H32C32 3.58172 28.4183 0 24 0Z" />
42+
<path d="M6 7H32V95H6V8Z" />
43+
<path d="M32 94H6C6 97.3137 3.31371 100 0 100V102H24C28.4183 102 32 98.4183 32 94Z" />
44+
<path
45+
d="M19 49.0007C19.2761 49.0007 19.5 49.2245 19.5 49.5007V52.0007C19.5 52.2768 19.2761 52.5007 19 52.5007C18.7239 52.5007 18.5 52.2768 18.5 52.0007V49.5007C18.5 49.2245 18.7239 49.0007 19 49.0007Z"
46+
fill="black"
47+
/>
48+
<path
49+
d="M19 54.8757C19.3452 54.8757 19.625 54.5958 19.625 54.2507C19.625 53.9055 19.3452 53.6257 19 53.6257C18.6548 53.6257 18.375 53.9055 18.375 54.2507C18.375 54.5958 18.6548 54.8757 19 54.8757Z"
50+
fill="black"
51+
/>
52+
<path
53+
fillRule="evenodd"
54+
clipRule="evenodd"
55+
d="M17.7267 45.2292L12.254 54.8064C11.6953 55.7842 12.4013 57.0007 13.5274 57.0007H24.4728C25.5989 57.0007 26.3049 55.7842 25.7462 54.8064L20.2735 45.2292C19.7105 44.2439 18.2897 44.2439 17.7267 45.2292ZM13.1222 55.3025L18.5949 45.7254C18.7741 45.4119 19.2261 45.4119 19.4053 45.7254L24.8779 55.3025C25.0557 55.6136 24.8311 56.0007 24.4728 56.0007H13.5274C13.1691 56.0007 12.9445 55.6136 13.1222 55.3025Z"
56+
fill="black"
57+
/>
58+
</svg>
59+
);
60+
}

0 commit comments

Comments
 (0)