Skip to content

Commit

Permalink
[v15] Add new empty state for Bots page in the web UI (#47118)
Browse files Browse the repository at this point in the history
  • Loading branch information
avatus committed Oct 4, 2024
1 parent a493e7a commit 392e3a1
Show file tree
Hide file tree
Showing 12 changed files with 1,019 additions and 9 deletions.
141 changes: 141 additions & 0 deletions web/packages/shared/components/EmptyState/EmptyState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/**
* Teleport
* Copyright (C) 2024 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

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: 120px;
--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]};
`;

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]
: '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 @@ -213,6 +213,20 @@ function GuidedTile({ integration }: { integration: BotIntegration }) {
);
}

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 @@ -235,3 +249,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 All @@ -132,11 +152,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 392e3a1

Please sign in to comment.