Adds more specific details to loading state, plus Retry button

This commit is contained in:
Alicia Sykes 2023-07-07 20:59:53 +01:00
parent 7a3977a39b
commit e11d527379

View File

@ -1,7 +1,8 @@
import styled from 'styled-components'; import styled from 'styled-components';
import colors from 'styles/colors'; import colors from 'styles/colors';
import Card from 'components/Form/Card'; import Card from 'components/Form/Card';
import { useState, useEffect } from 'react'; import Heading from 'components/Form/Heading';
import { useState, useEffect, ReactNode } from 'react';
const LoadCard = styled(Card)` const LoadCard = styled(Card)`
@ -58,6 +59,12 @@ const Details = styled.details`
padding: 0.25rem; padding: 0.25rem;
border-radius: 4px; border-radius: 4px;
width: fit-content; width: fit-content;
li b {
cursor: pointer;
}
i {
color: ${colors.textColorSecondary};
}
} }
p.error { p.error {
margin: 0.5rem 0; margin: 0.5rem 0;
@ -105,6 +112,17 @@ const SummaryContainer = styled.div`
} }
`; `;
const ReShowContainer = styled.div`
position: relative;
&.hidden {
height: 0;
overflow: hidden;
margin: 0;
padding: 0;
}
button { background: none;}
`;
const DismissButton = styled.button` const DismissButton = styled.button`
width: fit-content; width: fit-content;
position: absolute; position: absolute;
@ -122,6 +140,32 @@ const DismissButton = styled.button`
} }
`; `;
const FailedJobActionButton = styled.button`
margin: 0.1rem 0.1rem 0.1rem 0.5rem;
background: ${colors.background};
color: ${colors.textColorSecondary};
border: none;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-family: PTMono;
cursor: pointer;
border: 1px solid ${colors.textColorSecondary};
transition: all 0.2s ease-in-out;
&:hover {
color: ${colors.primary};
border: 1px solid ${colors.primary};
}
`;
const ErrorModalContent = styled.div`
p {
margin: 0;
}
pre {
color: ${colors.danger};
}
`;
export type LoadingState = 'success' | 'loading' | 'skipped' | 'error' | 'timed-out'; export type LoadingState = 'success' | 'loading' | 'skipped' | 'error' | 'timed-out';
export interface LoadingJob { export interface LoadingJob {
@ -129,6 +173,7 @@ export interface LoadingJob {
state: LoadingState, state: LoadingState,
error?: string, error?: string,
timeTaken?: number, timeTaken?: number,
retry?: () => void,
} }
const jobNames = [ const jobNames = [
@ -140,20 +185,24 @@ const jobNames = [
'headers', 'headers',
'lighthouse', 'lighthouse',
'location', 'location',
'shodan', 'hosts',
'redirects', 'redirects',
'txt-records', 'txt-records',
'status', 'status',
'ports', 'ports',
'trace-route', 'trace-route',
// 'server-info', 'carbon',
'server-info',
'whois', 'whois',
'features',
'dnssec',
] as const; ] as const;
export const initialJobs = jobNames.map((job: string) => { export const initialJobs = jobNames.map((job: string) => {
return { return {
name: job, name: job,
state: 'loading' as LoadingState, state: 'loading' as LoadingState,
retry: () => {}
} }
}); });
@ -225,18 +274,11 @@ const SummaryText = (props: { state: LoadingJob[], count: number }): JSX.Element
let skippedTasksCount = props.state.filter((val: LoadingJob) => val.state === 'skipped').length; let skippedTasksCount = props.state.filter((val: LoadingJob) => val.state === 'skipped').length;
let successTasksCount = props.state.filter((val: LoadingJob) => val.state === 'success').length; let successTasksCount = props.state.filter((val: LoadingJob) => val.state === 'success').length;
const skippedInfo = skippedTasksCount > 0 ? (<span className="skipped">{skippedTasksCount} skipped</span>) : null; const jobz = (jobCount: number) => `${jobCount} ${jobCount === 1 ? 'job' : 'jobs'}`;
const successInfo = skippedTasksCount > 0 ? (<span className="success">{successTasksCount} successful</span>) : null;
if (failedTasksCount > 0) { const skippedInfo = skippedTasksCount > 0 ? (<span className="skipped">{jobz(skippedTasksCount)} skipped </span>) : null;
return ( const successInfo = successTasksCount > 0 ? (<span className="success">{jobz(successTasksCount)} successful </span>) : null;
<SummaryContainer className="error-info"> const failedInfo = failedTasksCount > 0 ? (<span className="error">{jobz(failedTasksCount)} failed </span>) : null;
<b>{failedTasksCount} Job{failedTasksCount !== 1 ? 's' : ''} Failed</b>
{skippedInfo}
{successInfo}
</SummaryContainer>
);
}
if (loadingTasksCount > 0) { if (loadingTasksCount > 0) {
return ( return (
@ -257,11 +299,15 @@ const SummaryText = (props: { state: LoadingJob[], count: number }): JSX.Element
} }
return ( return (
<SummaryContainer className="misc-info">Other</SummaryContainer> <SummaryContainer className="error-info">
{successInfo}
{skippedInfo}
{failedInfo}
</SummaryContainer>
); );
}; };
const ProgressLoader = (props: { loadStatus: LoadingJob[] }): JSX.Element => { const ProgressLoader = (props: { loadStatus: LoadingJob[], showModal: (err: ReactNode) => void, showJobDocs: (job: string) => void }): JSX.Element => {
const [ hideLoader, setHideLoader ] = useState<boolean>(false); const [ hideLoader, setHideLoader ] = useState<boolean>(false);
const loadStatus = props.loadStatus; const loadStatus = props.loadStatus;
const percentages = calculateLoadingStatePercentages(loadStatus); const percentages = calculateLoadingStatePercentages(loadStatus);
@ -303,9 +349,26 @@ const ProgressLoader = (props: { loadStatus: LoadingJob[] }): JSX.Element => {
} }
}; };
return ( const showErrorModal = (name: string, state: LoadingState, timeTaken: number | undefined, error: string) => {
<LoadCard className={hideLoader ? 'hidden' : ''}> const errorContent = (
<ErrorModalContent>
<Heading as="h3">Error Details for {name}</Heading>
<p>
The {name} job failed with an {state} state after {timeTaken} ms.
The server responded with the following error:
</p>
<pre>{error}</pre>
</ErrorModalContent>
);
props.showModal(errorContent);
};
return (
<>
<ReShowContainer className={!hideLoader ? 'hidden' : ''}>
<DismissButton onClick={() => setHideLoader(false)}>Show Load State</DismissButton>
</ReShowContainer>
<LoadCard className={hideLoader ? 'hidden' : ''}>
<ProgressBarContainer> <ProgressBarContainer>
{Object.keys(percentages).map((state: string | LoadingState) => {Object.keys(percentages).map((state: string | LoadingState) =>
<ProgressBarSegment <ProgressBarSegment
@ -327,12 +390,14 @@ const ProgressLoader = (props: { loadStatus: LoadingJob[] }): JSX.Element => {
<summary>Show Details</summary> <summary>Show Details</summary>
<ul> <ul>
{ {
loadStatus.map(({ name, state, timeTaken }: LoadingJob) => { loadStatus.map(({ name, state, timeTaken, retry, error }: LoadingJob) => {
return ( return (
<li key={name}> <li key={name}>
<b>{getStatusEmoji(state)} {name}</b> <b onClick={() => props.showJobDocs(name)}>{getStatusEmoji(state)} {name}</b>
<span style={{color : barColors[state][0]}}> ({state})</span>. <span style={{color : barColors[state][0]}}> ({state})</span>.
<i>{timeTaken ? ` Took ${timeTaken} ms` : '' }</i> <i>{(timeTaken && state !== 'loading') ? ` Took ${timeTaken} ms` : '' }</i>
{ (retry && state !== 'success' && state !== 'loading') && <FailedJobActionButton onClick={retry}> Retry</FailedJobActionButton> }
{ (error && state === 'error') && <FailedJobActionButton onClick={() => showErrorModal(name, state, timeTaken, error)}> Show Error</FailedJobActionButton> }
</li> </li>
); );
}) })
@ -347,6 +412,7 @@ const ProgressLoader = (props: { loadStatus: LoadingJob[] }): JSX.Element => {
</Details> </Details>
<DismissButton onClick={() => setHideLoader(true)}>Dismiss</DismissButton> <DismissButton onClick={() => setHideLoader(true)}>Dismiss</DismissButton>
</LoadCard> </LoadCard>
</>
); );
} }