Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 61 additions & 17 deletions web/packages/teleport/src/UnifiedResources/ResourceCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,14 @@
* limitations under the License.
*/

import React, { useCallback, useState, useLayoutEffect, useRef } from 'react';
import styled from 'styled-components';
import React, {
useCallback,
useState,
useEffect,
useLayoutEffect,
useRef,
} from 'react';
import styled, { css } from 'styled-components';

import {
Box,
Expand Down Expand Up @@ -75,14 +81,16 @@ export function ResourceCard({ resource, onLabelClick }: Props) {
const ResTypeIcon = resourceTypeIcon(resource.kind);
const description = resourceDescription(resource);

const labelsInnerContainer = useRef(null);

const [showMoreLabelsButton, setShowMoreLabelsButton] = useState(false);
const [showAllLabels, setShowAllLabels] = useState(false);
const [numMoreLabels, setNumMoreLabels] = useState(0);

const nameTextRef = useRef<HTMLDivElement | null>(null);
const [isNameOverflowed, setIsNameOverflowed] = useState(false);

const innerContainer = useRef<Element | null>(null);
const labelsInnerContainer = useRef(null);
const nameText = useRef<HTMLDivElement | null>(null);
const collapseTimeout = useRef<ReturnType<typeof setTimeout>>(null);

// This effect installs a resize observer whose purpose is to detect the size
// of the component that contains all the labels. If this component is taller
// than the height of a single label row, we show a "+x more" button.
Expand All @@ -93,8 +101,8 @@ export function ResourceCard({ resource, onLabelClick }: Props) {
// This check will let us know if the name text has overflowed. We do this
// to conditionally render a tooltip for only overflowed names
if (
nameTextRef.current?.scrollWidth >
nameTextRef.current?.parentElement.offsetWidth
nameText.current?.scrollWidth >
nameText.current?.parentElement.offsetWidth
) {
setIsNameOverflowed(true);
} else {
Expand Down Expand Up @@ -132,17 +140,44 @@ export function ResourceCard({ resource, onLabelClick }: Props) {
};
});

// Clear the timeout on unmount to prevent changing a state of an unmounted
// component.
useEffect(() => () => clearTimeout(collapseTimeout.current), []);

const onMoreLabelsClick = () => {
setShowAllLabels(true);
};

const onMouseLeave = () => {
// If the user expanded the labels and then scrolled down enough to hide the
// top of the card, we scroll back up and collapse the labels with a small
// delay to keep the user from losing focus on the card that they were
// looking at. The delay is picked by hand, since there's no (easy) way to
// know when the animation ends.
Comment on lines +152 to +156
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I came to the PR here looking for "why is there a delay for this card closing?" because it was jarring to me, but after reading this comment, I agree with the approach. Thanks for changing my mind

if (
showAllLabels &&
(innerContainer.current?.getBoundingClientRect().top ?? 0) < 0
) {
innerContainer.current?.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
clearTimeout(collapseTimeout.current);
collapseTimeout.current = setTimeout(() => setShowAllLabels(false), 700);
} else {
// Otherwise, we just collapse the labels immediately.
setShowAllLabels(false);
}
};

return (
<CardContainer>
<CardInnerContainer
ref={innerContainer}
p={3}
alignItems="start"
showAllLabels={showAllLabels}
onMouseLeave={() => setShowAllLabels(false)}
onMouseLeave={onMouseLeave}
>
<ResourceIcon name={resIcon} width="45px" height="45px" ml={2} />
{/* MinWidth is important to prevent descriptions from overflowing. */}
Expand All @@ -151,12 +186,12 @@ export function ResourceCard({ resource, onLabelClick }: Props) {
<SingleLineBox flex="1">
{isNameOverflowed ? (
<HoverTooltip tipContent={<>{name}</>}>
<Text ref={nameTextRef} typography="h5" fontWeight={300}>
<Text ref={nameText} typography="h5" fontWeight={300}>
{name}
</Text>
</HoverTooltip>
) : (
<Text ref={nameTextRef} typography="h5" fontWeight={300}>
<Text ref={nameText} typography="h5" fontWeight={300}>
{name}
</Text>
)}
Expand Down Expand Up @@ -345,6 +380,12 @@ const CardContainer = styled(Box)`
position: relative;
`;

const elevatedCardMixin = css`
background-color: ${props => props.theme.colors.levels.elevated};
border-color: ${props => props.theme.colors.levels.elevated};
box-shadow: ${props => props.theme.boxShadow[1]};
`;

/**
* The inner container that normally holds a regular layout of the card, and is
* fully contained inside the outer container. Once the user clicks the "more"
Expand All @@ -359,16 +400,19 @@ const CardInnerContainer = styled(Flex)`
background-color: transparent;

${props =>
props.showAllLabels
? 'position: absolute; left: 0; right: 0; z-index: 1;'
: ''}
props.showAllLabels &&
css`
position: absolute;
left: 0;
right: 0;
z-index: 1;
${elevatedCardMixin}
`}

transition: all 150ms;

${CardContainer}:hover & {
background-color: ${props => props.theme.colors.levels.elevated};
border-color: ${props => props.theme.colors.levels.elevated};
box-shadow: ${props => props.theme.boxShadow[1]};
${elevatedCardMixin}
}

@media (min-width: ${props => props.theme.breakpoints.tablet}px) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,17 @@ export default {
loaders: [mswLoader],
};

const aLotOfLabels = {
...databases[0],
name: 'A DB with a lot of labels',
labels: Array(300)
.fill(0)
.map((_, i) => ({ name: `label-${i}`, value: `value ${i}` })),
};

const allResources = [
...apps,
aLotOfLabels,
...databases,
...kubes,
...desktops,
Expand Down