Skip to content

Commit

Permalink
[Discovery] Add Finish Component and Tweaks (gravitational#1109)
Browse files Browse the repository at this point in the history
* Renew web session after updating user traits
* Replace timeout with countdown timer for DownloadScript
* Create Finished Component
* Update TestConnection design and output
  • Loading branch information
kimlisa authored Aug 12, 2022
1 parent 47662f8 commit 6462c69
Show file tree
Hide file tree
Showing 18 changed files with 344 additions and 59 deletions.
16 changes: 12 additions & 4 deletions web/packages/teleport/src/Discover/Discover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { SelectResource } from './SelectResource';
import { DownloadScript } from './DownloadScript';
import { LoginTrait } from './LoginTrait';
import { TestConnection } from './TestConnection';
import { Finished } from './Finished';

import type { AgentKind } from './useDiscover';
import type { AgentStepComponent } from './types';
Expand All @@ -42,7 +43,7 @@ export const agentViews: Record<AgentKind, AgentStepComponent[]> = {
db: [],
desktop: [],
kube: [],
node: [SelectResource, DownloadScript, LoginTrait, TestConnection],
node: [SelectResource, DownloadScript, LoginTrait, TestConnection, Finished],
};

export default function Container() {
Expand Down Expand Up @@ -152,6 +153,7 @@ function SideNavAgentConnect({ currentStep }) {
'Configure Resource',
'Configure Role',
'Test Connection',
'',
];

return (
Expand All @@ -178,18 +180,24 @@ function SideNavAgentConnect({ currentStep }) {
<Text bold>Resource Connection</Text>
</Flex>
<Box ml={4} mt={4}>
{agentStepTitles.map((step, index) => {
{agentStepTitles.map((stepTitle, index) => {
let className = '';
if (currentStep > index) {
className = 'checked';
} else if (currentStep === index) {
className = 'active';
}

// All flows will have a finished step that
// does not have a title.
if (!stepTitle) {
return null;
}

return (
<StepsContainer className={className} key={step}>
<StepsContainer className={className} key={stepTitle}>
<Bullet />
{step}
{stepTitle}
</StepsContainer>
);
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,5 @@ const props: State = {
expiry: new Date(),
},
regenerateScriptAndRepoll: () => null,
countdownTime: { minutes: 5, seconds: 0 },
};
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { Header, ActionButtons, TextIcon } from '../Shared';

import { useDownloadScript } from './useDownloadScript';

import type { State } from './useDownloadScript';
import type { State, CountdownTime } from './useDownloadScript';

export default function Container(props: AgentStepProps) {
const ctx = useDiscoverContext();
Expand All @@ -45,6 +45,7 @@ export function DownloadScript({
nextStep,
pollState,
regenerateScriptAndRepoll,
countdownTime,
}: State) {
return (
<Box>
Expand All @@ -70,22 +71,29 @@ export function DownloadScript({
/>

{pollState === 'polling' && (
<TextIcon>
<TextIcon
css={`
white-space: pre;
`}
>
<Icons.Restore fontSize={4} />
Waiting for resource discovery...
{`Waiting for node | ${formatTime(
countdownTime
)} until this script expires`}
</TextIcon>
)}
{pollState === 'success' && (
<TextIcon>
<Icons.CircleCheck ml={1} color="success" />
Successfully discovered resource
Successfully executed
</TextIcon>
)}
{pollState === 'error' && (
<TextIcon>
<Icons.Warning ml={1} color="danger" />
Timed out, failed to discover resource.{' '}
Timeout, script expired{' '}
<ButtonText
ml={2}
onClick={regenerateScriptAndRepoll}
css={`
color: ${({ theme }) => theme.colors.link};
Expand All @@ -95,7 +103,7 @@ export function DownloadScript({
min-height: auto;
`}
>
Generate a new script and try again.
Regenerate Script
</ButtonText>
</TextIcon>
)}
Expand All @@ -114,6 +122,28 @@ function createBashCommand(tokenId: string) {
return `sudo bash -c "$(curl -fsSL ${cfg.getNodeScriptUrl(tokenId)})"`;
}

function formatTime({ minutes, seconds }: CountdownTime) {
let formattedSeconds = seconds.toString();
if (seconds < 10) {
formattedSeconds = `0${seconds}`;
}

let formattedMinutes = minutes.toString();
if (minutes < 10) {
formattedMinutes = `0${minutes}`;
}

let timeNotation = 'minute';
if (!minutes && seconds >= 0) {
timeNotation = 'seconds';
}
if (minutes) {
timeNotation = 'minutes';
}

return `${formattedMinutes}:${formattedSeconds} ${timeNotation}`;
}

const ScriptBox = styled(Box)`
max-width: 800px;
background-color: rgba(255, 255, 255, 0.05);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ exports[`polling error state 1`] = `
min-height: 32px;
font-size: 12px;
padding: 0px 24px;
margin-left: 8px;
}
.c11:active {
Expand Down Expand Up @@ -372,13 +373,13 @@ exports[`polling error state 1`] = `
class="c10 icon icon-warning "
color="danger"
/>
Timed out, failed to discover resource.
Timeout, script expired
<button
class="c11 c12"
kind="text"
>
Generate a new script and try again.
Regenerate Script
</button>
</div>
</div>
Expand Down Expand Up @@ -600,27 +601,28 @@ exports[`polling state 1`] = `
display: flex;
}
.c3 {
box-sizing: border-box;
padding: 16px;
border-radius: 8px;
max-width: 800px;
background-color: rgba(255,255,255,0.05);
border: 2px solid #2F3659;
}
.c9 {
overflow: hidden;
text-overflow: ellipsis;
margin: 0px;
display: flex;
align-items: center;
white-space: pre;
}
.c9 .icon {
margin-right: 8px;
}
.c3 {
box-sizing: border-box;
padding: 16px;
border-radius: 8px;
max-width: 800px;
background-color: rgba(255,255,255,0.05);
border: 2px solid #2F3659;
}
<div
class="c0"
>
Expand Down Expand Up @@ -674,7 +676,7 @@ exports[`polling state 1`] = `
color="light"
font-size="4"
/>
Waiting for resource discovery...
Waiting for node | 05:00 minutes until this script expires
</div>
</div>
<div
Expand Down Expand Up @@ -968,7 +970,7 @@ exports[`polling success state 1`] = `
class="c10 icon icon-checkmark-circle "
color="success"
/>
Successfully discovered resource
Successfully executed
</div>
</div>
<div
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
*/

import { useState, useEffect } from 'react';
import {
addMinutes,
intervalToDuration,
differenceInMilliseconds,
} from 'date-fns';
import useAttempt from 'shared/hooks/useAttemptNext';

import { INTERNAL_RESOURCE_ID_LABEL_KEY } from 'teleport/services/joinToken';
Expand All @@ -24,14 +29,21 @@ import { AgentStepProps } from '../types';

import type { JoinToken } from 'teleport/services/joinToken';

const FIVE_MINUTES_IN_MS = 300000;
const FIVE_MINUTES = 5;
const THREE_SECONDS_IN_MS = 3000;
const ONE_SECOND_IN_MS = 1000;

export function useDownloadScript({ ctx, props }: Props) {
const { attempt, run, setAttempt } = useAttempt('processing');
const [joinToken, setJoinToken] = useState<JoinToken>();
const [pollState, setPollState] = useState<PollState>('polling');

// TODO (lisa) extract count down logic into it's own component.
const [countdownTime, setCountdownTime] = useState<CountdownTime>({
minutes: 5,
seconds: 0,
});

// Responsible for initial join token fetch.
useEffect(() => {
fetchJoinToken();
Expand All @@ -48,7 +60,11 @@ export function useDownloadScript({ ctx, props }: Props) {
const abortController = new AbortController();
const abortSignal = abortController.signal;
let timeoutId;
let intervalId;
let pollingIntervalId;
let countdownIntervalId;

// countdownEndDate takes current date and adds 5 minutes to it.
let countdownEndDate = addMinutes(new Date(), FIVE_MINUTES);

// inFlightReq is a flag to prevent another fetch request when a
// previous fetch request is still in progress. May happen when a
Expand All @@ -57,8 +73,10 @@ export function useDownloadScript({ ctx, props }: Props) {
let inFlightReq;

function cleanUp() {
clearInterval(intervalId);
clearInterval(pollingIntervalId);
clearInterval(countdownIntervalId);
clearTimeout(timeoutId);
setCountdownTime({ minutes: 5, seconds: 0 });
// Cancel any in flight request.
abortController.abort();
}
Expand Down Expand Up @@ -94,20 +112,37 @@ export function useDownloadScript({ ctx, props }: Props) {
});
}

// Start the poller to discover the resource just added.
intervalId = setInterval(
() => fetchNodeMatchingRefResourceId(),
THREE_SECONDS_IN_MS
);
function updateCountdown() {
const start = new Date();
const end = countdownEndDate;
const duration = intervalToDuration({ start, end });

if (differenceInMilliseconds(end, start) <= 0) {
setPollState('error');
cleanUp();
return;
}

setCountdownTime({
minutes: duration.minutes,
seconds: duration.seconds,
});
}

// Set a timeout in case polling continuosly produces
// Set a countdown in case polling continuosly produces
// no results. Which means there is either a network error,
// script is ran unsuccessfully, script has not been ran,
// or resource cannot connect to cluster.
timeoutId = setTimeout(() => {
setPollState('error');
cleanUp();
}, FIVE_MINUTES_IN_MS);
countdownIntervalId = setInterval(
() => updateCountdown(),
ONE_SECOND_IN_MS
);

// Start the poller to discover the resource just added.
pollingIntervalId = setInterval(
() => fetchNodeMatchingRefResourceId(),
THREE_SECONDS_IN_MS
);

return () => {
cleanUp();
Expand Down Expand Up @@ -142,6 +177,7 @@ export function useDownloadScript({ ctx, props }: Props) {
nextStep: props.nextStep,
pollState,
regenerateScriptAndRepoll,
countdownTime,
};
}

Expand All @@ -152,4 +188,9 @@ type Props = {

type PollState = 'polling' | 'success' | 'error';

export type CountdownTime = {
minutes: number;
seconds: number;
};

export type State = ReturnType<typeof useDownloadScript>;
25 changes: 25 additions & 0 deletions web/packages/teleport/src/Discover/Finished/Finished.story.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Copyright 2022 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 { Finished } from './Finished';

export default {
title: 'Teleport/Discover/Finished',
};

export const Loaded = () => <Finished />;
Loading

0 comments on commit 6462c69

Please sign in to comment.