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
2 changes: 1 addition & 1 deletion web/packages/design/src/ShimmerBox/ShimmerBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const ShimmerWrapper = styled.div`
width: 100%;
height: 100%;
background-color: ${props => props.theme.colors.levels.surface};
border-radius: ${props => props.theme.radii[3]}px;
border-radius: ${props => props.theme.radii[2]}px;
overflow: hidden;
position: relative;
`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ const FilterTypesMenu = ({
// we have a separate state in the filter so we can select a few different things and then click "apply"
const [kinds, setKinds] = useState<string[]>(kindsFromParams || []);
const handleOpen = event => {
setKinds(kindsFromParams);
setAnchorEl(event.currentTarget);
};

Expand Down
55 changes: 55 additions & 0 deletions web/packages/shared/components/UnifiedResources/LoadingCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* Copyright 2023 Gravitational, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import React from 'react';
import { Flex, Box } from 'design';
import { ShimmerBox } from 'design/ShimmerBox';

export function LoadingCard() {
return (
<Flex gap={2} alignItems="start" height="106px" p={3}>
{/* Checkbox */}
<ShimmerBox height="16px" width="16px" />
{/* Image */}
<ShimmerBox height="45px" width="45px" />
<Box flex={1}>
<Flex gap={2} mb={2} justifyContent="space-between">
{/* Name */}
<ShimmerBox
height="24px"
css={`
flex-basis: ${randomNum(100, 30)}%;
`}
/>
{/* Action button */}
<ShimmerBox height="24px" width="90px" />
</Flex>
{/* Description */}
<ShimmerBox height="16px" width={`${randomNum(90, 40)}%`} mb={2} />
{/* Labels */}
<Flex gap={2}>
{new Array(randomNum(4, 0)).fill(null).map((_, i) => (
<ShimmerBox key={i} height="16px" width="60px" />
))}
</Flex>
</Box>
</Flex>
);
}

function randomNum(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Copyright 2023 Gravitational, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import React, { useState, useEffect, Fragment, ReactElement } from 'react';

const DISPLAY_SKELETON_AFTER_MS = 150;

export function LoadingSkeleton(props: {
count: number;
/* Single skeleton item. */
Element: ReactElement;
}) {
const [canDisplay, setCanDisplay] = useState(false);

useEffect(() => {
const displayTimeout = setTimeout(() => {
setCanDisplay(true);
}, DISPLAY_SKELETON_AFTER_MS);
return () => {
clearTimeout(displayTimeout);
};
}, []);

if (!canDisplay) {
return null;
}

return (
<>
{new Array(props.count).fill(undefined).map((_, i) => (
// Using index as key here is ok because these elements never change order
<Fragment key={i}>{props.Element}</Fragment>
))}
</>
);
}
68 changes: 0 additions & 68 deletions web/packages/shared/components/UnifiedResources/ResourceCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import { Box, ButtonIcon, ButtonLink, Flex, Label, Text } from 'design';
import copyToClipboard from 'design/utils/copyToClipboard';
import { StyledCheckbox } from 'design/Checkbox';

import { ShimmerBox } from 'design/ShimmerBox';
import { ResourceIcon, ResourceIconName } from 'design/ResourceIcon';
import { Icon, Copy, Check, PushPinFilled, PushPin } from 'design/Icon';

Expand Down Expand Up @@ -302,66 +301,6 @@ export function ResourceCard({
);
}

type LoadingCardProps = {
delay?: 'none' | 'short' | 'long';
};

const DelayValueMap = {
none: 0,
short: 400, // 0.4s;
long: 600, // 0.6s;
};

function randomNum(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}

export function LoadingCard({ delay = 'none' }: LoadingCardProps) {
const [canDisplay, setCanDisplay] = useState(false);

useEffect(() => {
const displayTimeout = setTimeout(() => {
setCanDisplay(true);
}, DelayValueMap[delay]);
return () => {
clearTimeout(displayTimeout);
};
}, []);

if (!canDisplay) {
return null;
}

return (
<LoadingCardWrapper p={3}>
<Flex gap={2} alignItems="start">
{/* Image */}
<ShimmerBox height="45px" width="45px" />
{/* Name and action button */}
<Box flex={1}>
<Flex gap={2} mb={2} justifyContent="space-between">
<ShimmerBox
height="24px"
css={`
flex-basis: ${randomNum(100, 30)}%;
`}
/>
<ShimmerBox height="24px" width="90px" />
</Flex>
<ShimmerBox height="16px" width={`${randomNum(90, 40)}%`} mb={2} />
<Box>
<Flex gap={2}>
{new Array(randomNum(4, 0)).fill(null).map((_, i) => (
<ShimmerBox key={i} height="16px" width="60px" />
))}
</Flex>
</Box>
</Box>
</Flex>
</LoadingCardWrapper>
);
}

function CopyButton({ name }: { name: string }) {
const copySuccess = 'Copied!';
const copyDefault = 'Click to copy';
Expand Down Expand Up @@ -507,13 +446,6 @@ const MoreLabelsButton = styled(ButtonLink)`
transition: background 150ms;
`;

const LoadingCardWrapper = styled(Box)`
height: 100px;
border: ${props => props.theme.borders[2]}
${props => props.theme.colors.spotBackground[0]};
border-radius: ${props => props.theme.radii[3]}px;
`;

function PinButton({
pinned,
pinningSupport,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
UnifiedResourcesPinning,
useUnifiedResourcesFetch,
} from './UnifiedResources';
import { SharedUnifiedResource } from './types';
import { SharedUnifiedResource, UnifiedResourcesQueryParams } from './types';

export default {
title: 'Shared/UnifiedResources',
Expand Down Expand Up @@ -67,14 +67,24 @@ const story = ({
getClusterPinnedResources: async () => [],
updateClusterPinnedResources: async () => undefined,
},
params,
}: {
fetchFunc: (
params: UrlResourcesParams,
signal: AbortSignal
) => Promise<ResourcesResponse<SharedUnifiedResource['resource']>>;
pinning?: UnifiedResourcesPinning;
params?: Partial<UnifiedResourcesQueryParams>;
}) => {
const params = { sort: { dir: 'ASC', fieldName: 'name' } } as const;
const mergedParams: UnifiedResourcesQueryParams = {
...{
sort: {
dir: 'ASC',
fieldName: 'name',
},
},
...params,
};
return () => {
const { fetch, attempt, resources } = useUnifiedResourcesFetch({
fetchFunc,
Expand All @@ -88,7 +98,7 @@ const story = ({
'kube_cluster',
'windows_desktop',
]}
params={params}
params={mergedParams}
setParams={() => undefined}
pinning={pinning}
updateUnifiedResourcesPreferences={() => undefined}
Expand Down Expand Up @@ -117,6 +127,13 @@ export const List = story({
}),
});

export const NoResults = story({
fetchFunc: async () => ({
agents: [],
}),
params: { search: 'my super long search query' },
});

export const Loading = story({
fetchFunc: (_, signal) =>
new Promise<never>((resolve, reject) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import { Danger } from 'design/Alert';
import './unifiedStyles.css';

import { ResourcesResponse, ResourceLabel } from 'teleport/services/agents';
import { TextIcon } from 'teleport/Discover/Shared';
import {
UnifiedTabPreference,
UnifiedResourcePreferences,
Expand Down Expand Up @@ -62,16 +61,16 @@ import {
} from './cards';

import { ResourceTab } from './ResourceTab';
import { ResourceCard, LoadingCard, PinningSupport } from './ResourceCard';
import { ResourceCard, PinningSupport } from './ResourceCard';
import { FilterPanel } from './FilterPanel';
import { LoadingSkeleton } from './LoadingSkeleton';
import { LoadingCard } from './LoadingCard';

// get 48 resources to start
const INITIAL_FETCH_SIZE = 48;
// increment by 24 every fetch
const FETCH_MORE_SIZE = 24;

const loadingCardArray = new Array(FETCH_MORE_SIZE).fill(undefined);

export const PINNING_NOT_SUPPORTED_MESSAGE =
'This cluster does not support pinning resources. To enable, upgrade to 14.1 or newer.';

Expand Down Expand Up @@ -463,10 +462,12 @@ export function UnifiedResources(props: UnifiedResourcesProps) {
))}
{/* Using index as key here is ok because these elements never change order */}
{(resourcesFetchAttempt.status === 'processing' ||
getPinnedResourcesAttempt.status === 'processing') &&
loadingCardArray.map((_, i) => (
<LoadingCard delay="short" key={i} />
))}
getPinnedResourcesAttempt.status === 'processing') && (
<LoadingSkeleton
count={FETCH_MORE_SIZE}
Element={<LoadingCard />}
/>
)}
</ResourcesContainer>
)}
<div ref={setTrigger} />
Expand Down Expand Up @@ -551,28 +552,37 @@ function NoResults({
query: string;
isPinnedTab: boolean;
}) {
// Prevent `No resources were found for ""` flicker.
if (query) {
return (
<Box p={8} mt={3} mx="auto" maxWidth="720px" textAlign="center">
<TextIcon typography="h3">
<Magnifier />
No {isPinnedTab ? 'pinned ' : ''}resources were found for&nbsp;
<Text
as="span"
bold
css={`
max-width: 270px;
overflow: hidden;
text-overflow: ellipsis;
`}
>
{query}
</Text>
</TextIcon>
</Box>
<Text
typography="h3"
mt={9}
mx="auto"
justifyContent="center"
alignItems="center"
css={`
white-space: nowrap;
`}
as={Flex}
>
<Magnifier mr={2} />
No {isPinnedTab ? 'pinned ' : ''}resources were found for&nbsp;
<Text
as="span"
bold
css={`
max-width: 270px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`}
>
{query}
</Text>
</Text>
);
}

return null;
}

Expand Down