Brings all the changes together in results page

This commit is contained in:
Alicia Sykes 2023-07-07 21:00:22 +01:00
parent e11d527379
commit 83272e4536
2 changed files with 232 additions and 83 deletions

View File

@ -1,26 +1,30 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { LoadingState } from 'components/misc/ProgressBar'; import { LoadingState } from 'components/misc/ProgressBar';
import { AddressType } from 'utils/address-type-checker'; import { AddressType } from 'utils/address-type-checker';
type UpdateLoadingJobsFunction = (job: string, newState: LoadingState, error?: string) => void;
interface AddressInfo {
address: string | undefined;
addressType: AddressType;
expectedAddressTypes: AddressType[];
}
interface UseIpAddressProps<ResultType = any> { interface UseIpAddressProps<ResultType = any> {
addressInfo: AddressInfo; // Unique identifier for this job type
updateLoadingJobs: UpdateLoadingJobsFunction;
jobId: string; jobId: string;
fetchRequest: () => Promise<ResultType>; // The actual fetch request
fetchRequest: () => Promise<ResultType>;
// Function to call to update the loading state in parent
updateLoadingJobs: (job: string, newState: LoadingState, error?: string, retry?: (data?: any) => void | null, data?: any) => void;
addressInfo: {
// The hostname/ip address that we're checking
address: string | undefined;
// The type of address (e.g. url, ipv4)
addressType: AddressType;
// The valid address types for this job
expectedAddressTypes: AddressType[];
};
} }
type ResultType = any; type ResultType = any;
type ReturnType = [ResultType | undefined, React.Dispatch<React.SetStateAction<ResultType | undefined>>]; type ReturnType = [ResultType | undefined, (data?: any) => void];
const useMotherOfAllHooks = <ResultType = any>(params: UseIpAddressProps<ResultType>): ReturnType => { const useMotherOfAllHooks = <ResultType = any>(params: UseIpAddressProps<ResultType>): ReturnType => {
// Destructure params // Destructure params
@ -30,6 +34,47 @@ const useMotherOfAllHooks = <ResultType = any>(params: UseIpAddressProps<ResultT
// Build useState that will be returned // Build useState that will be returned
const [result, setResult] = useState<ResultType>(); const [result, setResult] = useState<ResultType>();
// Fire off the HTTP fetch request, then set results and update loading / error state
const doTheFetch = () => {
return fetchRequest()
.then((res: any) => {
if (!res) {
updateLoadingJobs(jobId, 'error', res.error, reset);
throw new Error('No response');
}
if (res.error) {
updateLoadingJobs(jobId, 'error', res.error, reset);
throw new Error(res.error);
}
// All went to plan, set results and mark as done
setResult(res);
updateLoadingJobs(jobId, 'success', '', undefined, res);
})
.catch((err) => {
// Something fucked up, log the error
updateLoadingJobs(jobId, 'error', err.message, reset);
throw err;
})
}
// For when the user manually re-triggers the job
const reset = (data: any) => {
// If data is provided, then update state
if (data && !(data instanceof Event) && !data?._reactName) {
setResult(data);
} else { // Otherwise, trigger a data re-fetch
updateLoadingJobs(jobId, 'loading');
const fetchyFetch = doTheFetch();
const toastOptions = {
pending: `Updating Data (${jobId})`,
success: `Completed (${jobId})`,
error: `Failed to update (${jobId})`,
};
// Initiate fetch, and show progress toast
toast.promise(fetchyFetch, toastOptions).catch(() => {});
}
};
useEffect(() => { useEffect(() => {
// Still waiting for this upstream, cancel job // Still waiting for this upstream, cancel job
if (!address || !addressType) { if (!address || !addressType) {
@ -37,26 +82,17 @@ const useMotherOfAllHooks = <ResultType = any>(params: UseIpAddressProps<ResultT
} }
// This job isn't needed for this address type, cancel job // This job isn't needed for this address type, cancel job
if (!expectedAddressTypes.includes(addressType)) { if (!expectedAddressTypes.includes(addressType)) {
// updateLoadingJobs(jobId, 'skipped'); if (addressType !== 'empt') updateLoadingJobs(jobId, 'skipped');
return; return;
} }
// Initiate fetch request, set results and update loading / error state // Initiate the data fetching process
fetchRequest() doTheFetch().catch(() => {});
.then((res) => {
// All went to plan, set results and mark as done
setResult(res);
updateLoadingJobs(jobId, 'success');
})
.catch((err) => {
// Something fucked up, log the error
updateLoadingJobs(jobId, 'error', err.message);
});
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [address, addressType]); }, [address, addressType]);
return [result, setResult]; return [result, reset];
}; };
export default useMotherOfAllHooks; export default useMotherOfAllHooks;

View File

@ -1,13 +1,15 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback, ReactNode } from 'react';
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import styled from 'styled-components'; import styled from 'styled-components';
import { ToastContainer } from 'react-toastify';
import colors from 'styles/colors'; import colors from 'styles/colors';
import Heading from 'components/Form/Heading'; import Heading from 'components/Form/Heading';
import Card from 'components/Form/Card'; import Card from 'components/Form/Card';
import ErrorBoundary from 'components/misc/ErrorBoundary'; import Modal from 'components/Form/Modal';
import Footer from 'components/misc/Footer'; import Footer from 'components/misc/Footer';
import { RowProps } from 'components/Form/Row'; import { RowProps } from 'components/Form/Row';
import docs from 'utils/docs';
import ServerLocationCard from 'components/Results/ServerLocation'; import ServerLocationCard from 'components/Results/ServerLocation';
@ -27,8 +29,12 @@ import TxtRecordCard from 'components/Results/TxtRecords';
import ServerStatusCard from 'components/Results/ServerStatus'; import ServerStatusCard from 'components/Results/ServerStatus';
import OpenPortsCard from 'components/Results/OpenPorts'; import OpenPortsCard from 'components/Results/OpenPorts';
import TraceRouteCard from 'components/Results/TraceRoute'; import TraceRouteCard from 'components/Results/TraceRoute';
import CarbonFootprintCard from 'components/Results/CarbonFootprint';
import SiteFeaturesCard from 'components/Results/SiteFeatures';
import DnsSecCard from 'components/Results/DnsSec';
import ProgressBar, { LoadingJob, LoadingState, initialJobs } from 'components/misc/ProgressBar'; import ProgressBar, { LoadingJob, LoadingState, initialJobs } from 'components/misc/ProgressBar';
import ActionButtons from 'components/misc/ActionButtons';
import keys from 'utils/get-keys'; import keys from 'utils/get-keys';
import { determineAddressType, AddressType } from 'utils/address-type-checker'; import { determineAddressType, AddressType } from 'utils/address-type-checker';
@ -69,6 +75,24 @@ const Header = styled(Card)`
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
`; `;
const JobDocsContainer = styled.div`
p.doc-desc, p.doc-uses, ul {
margin: 0.25rem auto 1.5rem auto;
}
ul {
padding: 0 0.5rem 0 1rem;
}
ul li a {
color: ${colors.primary};
}
h4 {
border-top: 1px solid ${colors.primary};
color: ${colors.primary};
opacity: 0.75;
padding: 0.5rem 0;
}
`;
const Results = (): JSX.Element => { const Results = (): JSX.Element => {
const startTime = new Date().getTime(); const startTime = new Date().getTime();
@ -77,28 +101,44 @@ const Results = (): JSX.Element => {
const [ loadingJobs, setLoadingJobs ] = useState<LoadingJob[]>(initialJobs); const [ loadingJobs, setLoadingJobs ] = useState<LoadingJob[]>(initialJobs);
const updateLoadingJobs = useCallback((job: string, newState: LoadingState, error?: string) => { const [modalOpen, setModalOpen] = useState(false);
const timeTaken = new Date().getTime() - startTime; const [modalContent, setModalContent] = useState<ReactNode>(<></>);
const updateLoadingJobs = useCallback((job: string, newState: LoadingState, error?: string, retry?: () => void, data?: any) => {
const now = new Date();
const timeTaken = now.getTime() - startTime;
setLoadingJobs((prevJobs) => { setLoadingJobs((prevJobs) => {
const newJobs = prevJobs.map((loadingJob: LoadingJob) => { const newJobs = prevJobs.map((loadingJob: LoadingJob) => {
if (loadingJob.name === job) { if (loadingJob.name === job) {
return { ...loadingJob, error, state: newState, timeTaken }; return { ...loadingJob, error, state: newState, timeTaken, retry };
} }
return loadingJob; return loadingJob;
}); });
const timeString = `[${now.getHours().toString().padStart(2, '0')}:`
+`${now.getMinutes().toString().padStart(2, '0')}:`
+ `${now.getSeconds().toString().padStart(2, '0')}]`;
if (newState === 'success') { if (newState === 'success') {
console.log( console.log(
`%cFetch Success - ${job}%c\n\nThe ${job} job succeeded in ${timeTaken}ms`, `%cFetch Success - ${job}%c\n\n${timeString}%c The ${job} job succeeded in ${timeTaken}ms`
`background: ${colors.success}; color: ${colors.background}; padding: 4px 8px; font-size: 16px;`, + `\n%cRun %cwindow.webCheck['${job}']%c to inspect the raw the results`,
`background:${colors.success};color:${colors.background};padding: 4px 8px;font-size:16px;`,
`font-weight: bold; color: ${colors.success};`,
`color: ${colors.success};`, `color: ${colors.success};`,
`color: #1d8242;`,`color: #1d8242;text-decoration:underline;`,`color: #1d8242;`,
); );
if (!(window as any).webCheck) (window as any).webCheck = {};
if (data) (window as any).webCheck[job] = data;
} }
if (newState === 'error') { if (newState === 'error') {
console.log( console.log(
`%cFetch Error - ${job}%c\n\nThe ${job} job failed with the following error:%c\n${error}`, `%cFetch Error - ${job}%c\n\n${timeString}%c The ${job} job failed `
+`after ${timeTaken}ms, with the following error:%c\n${error}`,
`background: ${colors.danger}; padding: 4px 8px; font-size: 16px;`, `background: ${colors.danger}; padding: 4px 8px; font-size: 16px;`,
`font-weight: bold; color: ${colors.danger};`,
`color: ${colors.danger};`, `color: ${colors.danger};`,
`color: ${colors.warning};`, `color: ${colors.warning};`,
); );
@ -108,11 +148,13 @@ const Results = (): JSX.Element => {
}, []); }, []);
useEffect(() => { useEffect(() => {
setAddressType(determineAddressType(address || '')); if (!addressType || addressType === 'empt') {
setAddressType(determineAddressType(address || ''));
}
if (addressType === 'ipV4' && address) { if (addressType === 'ipV4' && address) {
setIpAddress(address); setIpAddress(address);
} }
}, []); }, []);
const urlTypeOnly = ['url'] as AddressType[]; // Many jobs only run with these address types const urlTypeOnly = ['url'] as AddressType[]; // Many jobs only run with these address types
@ -127,7 +169,7 @@ const Results = (): JSX.Element => {
}); });
// Fetch and parse SSL certificate info // Fetch and parse SSL certificate info
const [sslResults] = useMotherHook({ const [sslResults, updateSslResults] = useMotherHook({
jobId: 'ssl', jobId: 'ssl',
updateLoadingJobs, updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
@ -135,7 +177,7 @@ const Results = (): JSX.Element => {
}); });
// Fetch and parse cookies info // Fetch and parse cookies info
const [cookieResults] = useMotherHook<{cookies: Cookie[]}>({ const [cookieResults, updateCookieResults] = useMotherHook<{cookies: Cookie[]}>({
jobId: 'cookies', jobId: 'cookies',
updateLoadingJobs, updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
@ -145,7 +187,7 @@ const Results = (): JSX.Element => {
}); });
// Fetch and parse crawl rules from robots.txt // Fetch and parse crawl rules from robots.txt
const [robotsTxtResults] = useMotherHook<{robots: RowProps[]}>({ const [robotsTxtResults, updateRobotsTxtResults] = useMotherHook<{robots: RowProps[]}>({
jobId: 'robots-txt', jobId: 'robots-txt',
updateLoadingJobs, updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
@ -155,7 +197,7 @@ const Results = (): JSX.Element => {
}); });
// Fetch and parse headers // Fetch and parse headers
const [headersResults] = useMotherHook({ const [headersResults, updateHeadersResults] = useMotherHook({
jobId: 'headers', jobId: 'headers',
updateLoadingJobs, updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
@ -163,7 +205,7 @@ const Results = (): JSX.Element => {
}); });
// Fetch and parse DNS records // Fetch and parse DNS records
const [dnsResults] = useMotherHook({ const [dnsResults, updateDnsResults] = useMotherHook({
jobId: 'dns', jobId: 'dns',
updateLoadingJobs, updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
@ -171,7 +213,7 @@ const Results = (): JSX.Element => {
}); });
// Fetch and parse Lighthouse performance data // Fetch and parse Lighthouse performance data
const [lighthouseResults] = useMotherHook({ const [lighthouseResults, updateLighthouseResults] = useMotherHook({
jobId: 'lighthouse', jobId: 'lighthouse',
updateLoadingJobs, updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
@ -181,7 +223,7 @@ const Results = (): JSX.Element => {
}); });
// Get IP address location info // Get IP address location info
const [locationResults] = useMotherHook<ServerLocation>({ const [locationResults, updateLocationResults] = useMotherHook<ServerLocation>({
jobId: 'location', jobId: 'location',
updateLoadingJobs, updateLoadingJobs,
addressInfo: { address: ipAddress, addressType: 'ipV4', expectedAddressTypes: ['ipV4', 'ipV6'] }, addressInfo: { address: ipAddress, addressType: 'ipV4', expectedAddressTypes: ['ipV4', 'ipV6'] },
@ -192,8 +234,8 @@ const Results = (): JSX.Element => {
// Get hostnames and associated domains from Shodan // Get hostnames and associated domains from Shodan
const [shoadnResults] = useMotherHook<ShodanResults>({ const [shoadnResults, updateShodanResults] = useMotherHook<ShodanResults>({
jobId: 'shodan', jobId: 'hosts',
updateLoadingJobs, updateLoadingJobs,
addressInfo: { address: ipAddress, addressType: 'ipV4', expectedAddressTypes: ['ipV4', 'ipV6'] }, addressInfo: { address: ipAddress, addressType: 'ipV4', expectedAddressTypes: ['ipV4', 'ipV6'] },
fetchRequest: () => fetch(`https://api.shodan.io/shodan/host/${ipAddress}?key=${keys.shodan}`) fetchRequest: () => fetch(`https://api.shodan.io/shodan/host/${ipAddress}?key=${keys.shodan}`)
@ -203,7 +245,7 @@ const Results = (): JSX.Element => {
// Check for open ports // Check for open ports
const [portsResults] = useMotherHook({ const [portsResults, updatePortsResults] = useMotherHook({
jobId: 'ports', jobId: 'ports',
updateLoadingJobs, updateLoadingJobs,
addressInfo: { address: ipAddress, addressType: 'ipV4', expectedAddressTypes: ['ipV4', 'ipV6'] }, addressInfo: { address: ipAddress, addressType: 'ipV4', expectedAddressTypes: ['ipV4', 'ipV6'] },
@ -212,7 +254,7 @@ const Results = (): JSX.Element => {
}); });
// Fetch and parse domain whois results // Fetch and parse domain whois results
const [whoIsResults] = useMotherHook<Whois>({ const [whoIsResults, updateWhoIsResults] = useMotherHook<Whois>({
jobId: 'whois', jobId: 'whois',
updateLoadingJobs, updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
@ -222,17 +264,17 @@ const Results = (): JSX.Element => {
}); });
// Fetch and parse built-with results // Fetch and parse built-with results
const [technologyResults] = useMotherHook<TechnologyGroup[]>({ // const [technologyResults, updateTechnologyResults] = useMotherHook<TechnologyGroup[]>({
jobId: 'built-with', // jobId: 'built-with',
updateLoadingJobs, // updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, // addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
fetchRequest: () => fetch(`https://api.builtwith.com/v21/api.json?KEY=${keys.builtWith}&LOOKUP=${address}`) // fetchRequest: () => fetch(`https://api.builtwith.com/v21/api.json?KEY=${keys.builtWith}&LOOKUP=${address}`)
.then(res => res.json()) // .then(res => res.json())
.then(res => makeTechnologies(res)), // .then(res => makeTechnologies(res)),
}); // });
// Fetches DNS TXT records // Fetches DNS TXT records
const [txtRecordResults] = useMotherHook({ const [txtRecordResults, updateTxtRecordResults] = useMotherHook({
jobId: 'txt-records', jobId: 'txt-records',
updateLoadingJobs, updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
@ -240,7 +282,7 @@ const Results = (): JSX.Element => {
}); });
// Fetches URL redirects // Fetches URL redirects
const [redirectResults] = useMotherHook({ const [redirectResults, updateRedirectResults] = useMotherHook({
jobId: 'redirects', jobId: 'redirects',
updateLoadingJobs, updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
@ -248,7 +290,7 @@ const Results = (): JSX.Element => {
}); });
// Get current status and response time of server // Get current status and response time of server
const [serverStatusResults] = useMotherHook({ const [serverStatusResults, updateServerStatusResults] = useMotherHook({
jobId: 'status', jobId: 'status',
updateLoadingJobs, updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
@ -256,13 +298,37 @@ const Results = (): JSX.Element => {
}); });
// Get trace route for a given hostname // Get trace route for a given hostname
const [traceRouteResults] = useMotherHook({ const [traceRouteResults, updateTraceRouteResults] = useMotherHook({
jobId: 'trace-route', jobId: 'trace-route',
updateLoadingJobs, updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
fetchRequest: () => fetch(`/trace-route?url=${address}`).then(res => res.json()), fetchRequest: () => fetch(`/trace-route?url=${address}`).then(res => res.json()),
}); });
// Fetch carbon footprint data for a given site
const [carbonResults, updateCarbonResults] = useMotherHook({
jobId: 'carbon',
updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
fetchRequest: () => fetch(`/get-carbon?url=${address}`).then(res => res.json()),
});
// Get site features from BuiltWith
const [siteFeaturesResults, updateSiteFeaturesResults] = useMotherHook({
jobId: 'features',
updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
fetchRequest: () => fetch(`/site-features?url=${address}`).then(res => res.json()),
});
// Get DNSSEC info
const [dnsSecResults, updateDnsSecResults] = useMotherHook({
jobId: 'dnssec',
updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
fetchRequest: () => fetch(`/dns-sec?url=${address}`).then(res => res.json()),
});
/* Cancel remaining jobs after 10 second timeout */ /* Cancel remaining jobs after 10 second timeout */
useEffect(() => { useEffect(() => {
const checkJobs = () => { const checkJobs = () => {
@ -288,24 +354,66 @@ const Results = (): JSX.Element => {
// A list of state sata, corresponding component and title for each card // A list of state sata, corresponding component and title for each card
const resultCardData = [ const resultCardData = [
{ title: 'Server Location', result: locationResults, Component: ServerLocationCard }, { id: 'location', title: 'Server Location', result: locationResults, Component: ServerLocationCard, refresh: updateLocationResults },
{ title: 'SSL Info', result: sslResults, Component: SslCertCard }, { id: 'ssl', title: 'SSL Info', result: sslResults, Component: SslCertCard, refresh: updateSslResults },
{ title: 'Headers', result: headersResults, Component: HeadersCard }, { id: 'dns', title: 'Headers', result: headersResults, Component: HeadersCard, refresh: updateHeadersResults },
{ title: 'Host Names', result: shoadnResults?.hostnames, Component: HostNamesCard }, { id: 'hosts', title: 'Host Names', result: shoadnResults?.hostnames, Component: HostNamesCard, refresh: updateShodanResults },
{ title: 'Domain Info', result: whoIsResults, Component: WhoIsCard }, { id: 'whois', title: 'Domain Info', result: whoIsResults, Component: WhoIsCard, refresh: updateWhoIsResults },
{ title: 'DNS Records', result: dnsResults, Component: DnsRecordsCard }, { id: 'dns', title: 'DNS Records', result: dnsResults, Component: DnsRecordsCard, refresh: updateDnsResults },
{ title: 'Performance', result: lighthouseResults, Component: LighthouseCard }, { id: 'lighthouse', title: 'Performance', result: lighthouseResults, Component: LighthouseCard, refresh: updateLighthouseResults },
{ title: 'Cookies', result: cookieResults, Component: CookiesCard }, { id: 'cookies', title: 'Cookies', result: cookieResults, Component: CookiesCard, refresh: updateCookieResults },
{ title: 'Trace Route', result: traceRouteResults, Component: TraceRouteCard }, { id: 'trace-route', title: 'Trace Route', result: traceRouteResults, Component: TraceRouteCard, refresh: updateTraceRouteResults },
{ title: 'Screenshot', result: lighthouseResults?.fullPageScreenshot?.screenshot, Component: ScreenshotCard }, { id: '', title: 'Screenshot', result: lighthouseResults?.fullPageScreenshot?.screenshot, Component: ScreenshotCard, refresh: updateLighthouseResults },
{ title: 'Technologies', result: technologyResults, Component: BuiltWithCard }, // { title: 'Technologies', result: technologyResults, Component: BuiltWithCard, refresh: updateTechnologyResults },
{ title: 'Crawl Rules', result: robotsTxtResults, Component: RobotsTxtCard }, { id: 'robots-txt', title: 'Crawl Rules', result: robotsTxtResults, Component: RobotsTxtCard, refresh: updateRobotsTxtResults },
{ title: 'Server Info', result: shoadnResults?.serverInfo, Component: ServerInfoCard }, { id: 'server-info', title: 'Server Info', result: shoadnResults?.serverInfo, Component: ServerInfoCard, refresh: updateShodanResults },
{ title: 'Redirects', result: redirectResults, Component: RedirectsCard }, { id: 'redirects', title: 'Redirects', result: redirectResults, Component: RedirectsCard, refresh: updateRedirectResults },
{ title: 'TXT Records', result: txtRecordResults, Component: TxtRecordCard }, { id: 'txt-records', title: 'TXT Records', result: txtRecordResults, Component: TxtRecordCard, refresh: updateTxtRecordResults },
{ title: 'Server Status', result: serverStatusResults, Component: ServerStatusCard }, { id: 'status', title: 'Server Status', result: serverStatusResults, Component: ServerStatusCard, refresh: updateServerStatusResults },
{ title: 'Open Ports', result: portsResults, Component: OpenPortsCard }, { id: 'ports', title: 'Open Ports', result: portsResults, Component: OpenPortsCard, refresh: updatePortsResults },
{ id: 'carbon', title: 'Carbon Footprint', result: carbonResults, Component: CarbonFootprintCard, refresh: updateCarbonResults },
{ id: 'features', title: 'Site Features', result: siteFeaturesResults, Component: SiteFeaturesCard, refresh: updateSiteFeaturesResults },
{ id: 'dnssec', title: 'DNSSEC', result: dnsSecResults, Component: DnsSecCard, refresh: updateDnsSecResults },
]; ];
const MakeActionButtons = (title: string, refresh: () => void, showInfo: (id: string) => void): ReactNode => {
const actions = [
{ label: `Info about ${title}`, onClick: showInfo, icon: 'ⓘ'},
{ label: `Re-fetch ${title} data`, onClick: refresh, icon: '↻'},
];
return (
<ActionButtons actions={actions} />
);
};
const showInfo = (id: string) => {
const doc = docs.filter((doc: any) => doc.id === id)[0] || null;
setModalContent(
doc? (<JobDocsContainer>
<Heading as="h3" size="medium" color={colors.primary}>{doc.title}</Heading>
<Heading as="h4" size="small">About</Heading>
<p className="doc-desc">{doc.description}</p>
<Heading as="h4" size="small">Use Cases</Heading>
<p className="doc-uses">{doc.use}</p>
<Heading as="h4" size="small">Links</Heading>
<ul>
{doc.resources.map((resource: string, index: number) => (
<li id={`link-${index}`}><a target="_blank" rel="noreferrer" href={resource}>{resource}</a></li>
))}
</ul>
</JobDocsContainer>)
: (
<JobDocsContainer>
<p>No Docs provided for this widget yet</p>
</JobDocsContainer>
));
setModalOpen(true);
};
const showErrorModal = (content: ReactNode) => {
setModalContent(content);
setModalOpen(true);
};
return ( return (
<ResultsOuter> <ResultsOuter>
@ -321,20 +429,25 @@ const Results = (): JSX.Element => {
</Heading> </Heading>
} }
</Header> </Header>
<ProgressBar loadStatus={loadingJobs} /> <ProgressBar loadStatus={loadingJobs} showModal={showErrorModal} showJobDocs={showInfo} />
<ResultsContent> <ResultsContent>
{ {
resultCardData.map(({ title, result, Component }) => ( resultCardData.map(({ id, title, result, refresh, Component }, index: number) => (
(result) ? ( (result && !result.error) ? (
<ErrorBoundary title={title} key={title}> <Component
<Component {...result} /> key={`${title}-${index}`}
</ErrorBoundary> data={{...result}}
title={title}
actionButtons={refresh ? MakeActionButtons(title, refresh, () => showInfo(id)) : undefined}
/>
) : <></> ) : <></>
)) ))
} }
</ResultsContent> </ResultsContent>
<Footer /> <Footer />
<Modal isOpen={modalOpen} closeModal={()=> setModalOpen(false)}>{modalContent}</Modal>
<ToastContainer limit={3} draggablePercent={60} autoClose={2500} theme="dark" position="bottom-right" />
</ResultsOuter> </ResultsOuter>
); );
} }