Skip to content

Commit

Permalink
Add new empty state for Bots page in the web UI (#46773)
Browse files Browse the repository at this point in the history
* Add new images

* Shared EmptyState components

* Add DisplayTile

* Add Bots EmptyState component

* Use EmptyState and update story/test

* Update images with manual shadow

design team removed shadows from images to make them consistant and
requested manual shadows added for images (and not tile scene)

* Fix fontsize

* Update word
  • Loading branch information
avatus committed Sep 30, 2024
1 parent 553c17a commit 0870fb0
Show file tree
Hide file tree
Showing 11 changed files with 979 additions and 9 deletions.
124 changes: 124 additions & 0 deletions web/packages/shared/components/EmptyState/EmptyState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import styled from 'styled-components';
import { Flex, Text, Box } from 'design';

export const FeatureContainer = styled(Flex)`
@media (min-width: 1662px) {
--feature-slider-width: 612px;
--feature-width: 612px;
--feature-height: 95px;
--feature-preview-scale: scale(0.9);
--feature-text-display: block;
}
@media (max-width: 1662px) {
--feature-slider-width: 512px;
--feature-width: 512px;
--feature-height: 112px;
--feature-preview-scale: scale(0.9);
--feature-text-display: block;
}
@media (max-width: 1563px) {
--feature-slider-width: 412px;
--feature-width: 412px;
--feature-height: 112px;
--feature-preview-scale: scale(0.8);
--feature-text-display: inline;
}
@media (max-width: 1462px) {
--feature-slider-width: 412px;
--feature-width: 412px;
--feature-height: 112px;
--feature-preview-scale: scale(0.8);
--feature-text-display: inline;
}
@media (max-width: 1302px) {
--feature-slider-width: 372px;
--feature-width: 372px;
--feature-height: 112px;
--feature-preview-scale: scale(0.7);
--feature-text-display: inline;
}
`;

export const FeatureSlider = styled.div<{ $currIndex: number }>`
z-index: -1;
position: absolute;
height: var(--feature-height);
width: var(--feature-slider-width);
transition: all 0.3s ease;
border-radius: ${p => p.theme.radii[3]}px;
cursor: pointer;
top: calc(var(--feature-height) * ${p => p.$currIndex});
background-color: ${p =>
p.theme.colors.interactive.tonal.primary[0].background};
`;

export type FeatureProps = {
isSliding: boolean;
title: string;
description: string;
active: boolean;
onClick(): void;
};

export const DetailsTab = ({
active,
onClick,
isSliding,
title,
description,
}: FeatureProps) => {
return (
<Feature $active={active} onClick={onClick} $isSliding={isSliding}>
<Title>{title}</Title>
<Description>{description}</Description>
</Feature>
);
};

export const Title = styled(Text)`
font-weight: bold;
`;

export const Description = styled(Text)`
font-size: ${p => p.theme.fontSizes[1]}px;
`;

export const Feature = styled(Box)<{ $isSliding?: boolean; $active?: boolean }>`
height: var(--feature-height);
line-height: 20px;
padding: ${p => p.theme.space[3]}px;
border-radius: ${p => p.theme.radii[3]}px;
cursor: pointer;
width: var(--feature-width);
background-color: ${p =>
!p.$isSliding && p.$active
? p => p.theme.colors.interactive.tonal.primary[0].background
: 'inherit'};
${Title} {
color: ${p => {
if (p.$isSliding && p.$active) {
return p.theme.colors.buttons.primary.default;
}
return p.$active ? p.theme.colors.buttons.primary.default : 'inherit';
}};
transition: color 0.2s ease-in 0s;
}
&:hover {
background-color: ${p => p.theme.colors.spotBackground[0]};
}
&:hover ${Title} {
color: ${p => p.theme.colors.text.main};
}
`;
19 changes: 19 additions & 0 deletions web/packages/teleport/src/Bots/Add/AddBotsPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,20 @@ function GuidedTile({
);
}

export function DisplayTile({
icon,
title,
}: {
title: string;
icon: JSX.Element;
}) {
return (
<HoverIntegrationTile>
<TileContent icon={icon} title={title} />
</HoverIntegrationTile>
);
}

function TileContent({ icon, title }) {
return (
<>
Expand All @@ -266,3 +280,8 @@ const BadgeGuided = styled.div`
right: 0px;
font-size: 10px;
`;

const HoverIntegrationTile = styled(IntegrationTile)`
background: none;
transition: all 0.1s ease-in;
`;
11 changes: 11 additions & 0 deletions web/packages/teleport/src/Bots/List/Bots.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,25 @@
*/

import React from 'react';
import { MemoryRouter } from 'react-router';

import { botsFixture } from 'teleport/Bots/fixtures';
import { BotList } from 'teleport/Bots/List/BotList';

import { EmptyState } from './EmptyState/EmptyState';

export default {
title: 'Teleport/Bots',
};

export const Empty = () => {
return (
<MemoryRouter>
<EmptyState />
</MemoryRouter>
);
};

export const List = () => {
return (
<BotList
Expand Down
12 changes: 9 additions & 3 deletions web/packages/teleport/src/Bots/List/Bots.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ test('fetches bots on load', async () => {
jest.spyOn(api, 'get').mockResolvedValueOnce({ ...botsApiResponseFixture });
renderWithContext(<Bots />);

expect(screen.getByText('Bots')).toBeInTheDocument();
await waitFor(() => {
expect(
screen.getByText(botsApiResponseFixture.items[0].metadata.name)
Expand All @@ -49,6 +48,15 @@ test('fetches bots on load', async () => {
expect(api.get).toHaveBeenCalledTimes(1);
});

test('shows empty state when bots are empty', async () => {
jest.spyOn(api, 'get').mockResolvedValue({ items: [] });
renderWithContext(<Bots />);

await waitFor(() => {
expect(screen.getByTestId('bots-empty-state')).toBeInTheDocument();
});
});

test('calls edit endpoint', async () => {
jest
.spyOn(api, 'get')
Expand All @@ -57,7 +65,6 @@ test('calls edit endpoint', async () => {
jest.spyOn(api, 'put').mockResolvedValue({});
renderWithContext(<Bots />);

expect(screen.getByText('Bots')).toBeInTheDocument();
await waitFor(() => {
expect(
screen.getByText(botsApiResponseFixture.items[0].metadata.name)
Expand Down Expand Up @@ -89,7 +96,6 @@ test('calls delete endpoint', async () => {
jest.spyOn(api, 'delete').mockResolvedValue({});
renderWithContext(<Bots />);

expect(screen.getByText('Bots')).toBeInTheDocument();
await waitFor(() => {
expect(
screen.getByText(botsApiResponseFixture.items[0].metadata.name)
Expand Down
27 changes: 21 additions & 6 deletions web/packages/teleport/src/Bots/List/Bots.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,14 @@ import useTeleport from 'teleport/useTeleport';

import cfg from 'teleport/config';

import { EmptyState } from './EmptyState/EmptyState';

export function Bots() {
const ctx = useTeleport();
const flags = ctx.getFeatureFlags();
const hasAddBotPermissions = flags.addBots;

const [bots, setBots] = useState<FlatBot[]>();
const [bots, setBots] = useState<FlatBot[]>([]);
const [selectedBot, setSelectedBot] = useState<FlatBot>();
const [selectedRoles, setSelectedRoles] = useState<string[]>();
const { attempt: crudAttempt, run: crudRun } = useAttemptNext();
Expand Down Expand Up @@ -107,6 +109,24 @@ export function Bots() {
setSelectedRoles(null);
}

if (fetchAttempt.status === 'processing') {
return (
<FeatureBox>
<Box textAlign="center" m={10}>
<Indicator />
</Box>
</FeatureBox>
);
}

if (fetchAttempt.status === 'success' && bots.length === 0) {
return (
<FeatureBox>
<EmptyState />
</FeatureBox>
);
}

return (
<FeatureBox>
<FeatureHeader>
Expand Down Expand Up @@ -138,11 +158,6 @@ export function Bots() {
</HoverTooltip>
</Box>
</FeatureHeader>
{fetchAttempt.status == 'processing' && (
<Box textAlign="center" m={10}>
<Indicator />
</Box>
)}
{fetchAttempt.status == 'failed' && (
<Alert kind="danger" children={fetchAttempt.statusText} />
)}
Expand Down
Loading

0 comments on commit 0870fb0

Please sign in to comment.