Skip to content

Commit 2295ed6

Browse files
authored
Merge pull request #915 from cisagov/VS-Dashboard-Develop
Develop VS Dashboard
2 parents 588aace + ec463bc commit 2295ed6

26 files changed

+1557
-212
lines changed

frontend/src/App.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import { RiskWithSearch } from 'pages/Risk/Risk';
3939
import { StaticsContextProvider } from 'context/StaticsContextProvider';
4040
import { SavedSearchContextProvider } from 'context/SavedSearchContextProvider';
4141
import { FilterDrawerContextProvider } from 'context/FilterDrawerContextProvider';
42-
// import VulnerabilityScan from 'pages/VulnerabilityScanDash/VulnerabilityScan';
42+
import VulnerabilityScan from 'pages/VulnerabilityScanDash/VulnerabilityScan';
4343

4444
API.configure({
4545
endpoints: [
@@ -102,7 +102,7 @@ const App: React.FC = () => (
102102
exact
103103
path="/"
104104
unauth={AuthLogin}
105-
component={RiskWithSearch}
105+
component={VulnerabilityScan}
106106
/>
107107
<Route
108108
exact
@@ -143,10 +143,14 @@ const App: React.FC = () => (
143143
path="/inventory/domains"
144144
component={Domains}
145145
/>
146-
{/* <RouteGuard
146+
<RouteGuard
147+
path="/overview"
148+
component={RiskWithSearch}
149+
/>
150+
<RouteGuard
147151
path="/VSDashboard"
148152
component={VulnerabilityScan}
149-
/> */}
153+
/>
150154
<RouteGuard
151155
path="/inventory/vulnerabilities"
152156
exact
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import React from 'react';
2+
import { Chip } from '@mui/material';
3+
import { Stack } from '@mui/system';
4+
5+
interface GraphChipProps {
6+
data: {
7+
label: string;
8+
onClick: () => void;
9+
}[];
10+
activeLabel: string;
11+
}
12+
const GraphChip: React.FC<GraphChipProps> = ({ data, activeLabel }) => {
13+
const capitalizeFirstLetter = (string: string) => {
14+
return string.charAt(0).toUpperCase() + string.slice(1);
15+
};
16+
return (
17+
<Stack
18+
direction="row"
19+
spacing={1}
20+
alignItems="center"
21+
role="radiogroup"
22+
aria-label="Data selector"
23+
>
24+
{data.map((item, index) => {
25+
const isActive = item.label === activeLabel;
26+
return (
27+
<Chip
28+
key={index}
29+
label={capitalizeFirstLetter(item.label)}
30+
variant={isActive ? 'graphOutlinedActive' : 'graphOutlinedInactive'}
31+
onClick={item.onClick}
32+
role="radio"
33+
aria-checked={isActive}
34+
/>
35+
);
36+
})}
37+
</Stack>
38+
);
39+
};
40+
export default GraphChip;
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import React from 'react';
2+
import {
3+
Box,
4+
Typography,
5+
Link as MuiLink,
6+
TypographyProps
7+
} from '@mui/material';
8+
import { Link as RouterLink } from 'react-router-dom';
9+
import InfoTooltipIcon from './InfoTooltipIcon';
10+
11+
type InfoLabelProps = {
12+
label: string;
13+
viewDetails?: boolean;
14+
link?: string;
15+
typographyVariant?: TypographyProps['variant'];
16+
headingLevel?: 'h2' | 'h3' | 'p';
17+
tooltipContentJson: { content: string; id: string }[];
18+
};
19+
20+
const InfoLabel: React.FC<InfoLabelProps> = ({
21+
label,
22+
viewDetails,
23+
link,
24+
typographyVariant = 'h2',
25+
headingLevel = 'h2',
26+
tooltipContentJson
27+
}) => {
28+
const tooltipContent = (label: string): string => {
29+
const info = tooltipContentJson.find(
30+
(item: { id: string }) => item.id === label
31+
);
32+
return info ? info.content : 'No information available.';
33+
};
34+
return (
35+
<Box
36+
display="flex"
37+
alignItems="center"
38+
justifyContent="space-between"
39+
p={0}
40+
>
41+
<Box display="flex" alignItems="center">
42+
<Typography
43+
variant={typographyVariant}
44+
component={headingLevel}
45+
color="primary.darker"
46+
>
47+
{label}
48+
</Typography>
49+
<InfoTooltipIcon label={label} tooltipContent={tooltipContent(label)} />
50+
</Box>
51+
52+
{viewDetails && link && (
53+
<MuiLink to={link} component={RouterLink}>
54+
<Typography variant="link" component="p" fontWeight="bold">
55+
View Details
56+
</Typography>
57+
</MuiLink>
58+
)}
59+
</Box>
60+
);
61+
};
62+
63+
export default InfoLabel;

frontend/src/pages/VulnerabilityScanDash/InfoTooltipIcon.tsx renamed to frontend/src/components/Dashboard/InfoTooltipIcon.tsx

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
import React from 'react';
22
import { Divider, Paper, Tooltip, IconButton, Typography } from '@mui/material';
33
import InfoOutlined from '@mui/icons-material/InfoOutlined';
4-
import infoIconContent from './infoIconContent.json';
54

65
type InfoTooltipIconProps = {
76
label: string;
7+
// data: { content: string; id: string }[];
8+
tooltipContent: string;
89
};
910

10-
const InfoTooltipIcon: React.FC<InfoTooltipIconProps> = ({ label }) => {
11-
const getTooltipContent = (label: string): string => {
12-
const info = infoIconContent.infoIconContent.find(
13-
(item) => item.id === label
14-
);
15-
return info ? info.content : 'No information available.';
11+
const InfoTooltipIcon: React.FC<InfoTooltipIconProps> = ({
12+
label,
13+
tooltipContent
14+
}) => {
15+
const handleIconButtonClick = (event: {
16+
preventDefault: () => void;
17+
stopPropagation: () => void;
18+
}) => {
19+
event.preventDefault();
20+
event.stopPropagation();
1621
};
1722
const tooltipContentJSX = (
1823
<Paper
@@ -34,7 +39,7 @@ const InfoTooltipIcon: React.FC<InfoTooltipIconProps> = ({ label }) => {
3439
color="neutrals.main"
3540
sx={{ p: 2 }}
3641
>
37-
{getTooltipContent(label)}
42+
{tooltipContent}
3843
</Typography>
3944
</Paper>
4045
);
@@ -56,9 +61,9 @@ const InfoTooltipIcon: React.FC<InfoTooltipIconProps> = ({ label }) => {
5661
}}
5762
>
5863
<IconButton
64+
onClick={handleIconButtonClick}
5965
aria-label={`More information about ${label}`}
6066
disableRipple
61-
disableFocusRipple
6267
sx={{
6368
p: 2,
6469
width: '56px',
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import React from 'react';
2+
import {
3+
Alert,
4+
Box,
5+
Card,
6+
Divider,
7+
Table,
8+
TableBody,
9+
TableCell,
10+
TableHead,
11+
TableRow,
12+
Typography
13+
} from '@mui/material';
14+
import { SxProps } from '@mui/system';
15+
16+
type ColumnConfig<T> = {
17+
key: keyof T;
18+
header: string;
19+
textAlign?: 'left' | 'center' | 'right';
20+
minWidth?: number | string;
21+
render?: (value: T[keyof T], row: T) => React.ReactNode;
22+
headerPadding?: number;
23+
};
24+
25+
type RoundedTableProps<T> = {
26+
columns: ColumnConfig<T>[];
27+
data: T[];
28+
noDataMessage?: string;
29+
tableStyles?: SxProps;
30+
rowHeadStyles?: SxProps;
31+
rowBodyStyles?: SxProps;
32+
cellBodyStyles?: SxProps;
33+
};
34+
35+
const tableSx: SxProps = {
36+
borderCollapse: 'separate',
37+
borderSpacing: '0 16px',
38+
width: '100%',
39+
tableLayout: 'auto',
40+
display: { xs: 'none', sm: 'table' }
41+
};
42+
43+
const rowHeadSx: SxProps = {
44+
'& th': {
45+
border: 'none',
46+
backgroundColor: 'transparent',
47+
pb: 0,
48+
fontSize: '11px',
49+
fontWeight: 600
50+
}
51+
};
52+
53+
const rowBodySx = {
54+
borderRadius: 5,
55+
borderColor: 'gray',
56+
'& td': {
57+
borderLeft: '1px solid #ccc',
58+
backgroundColor: '#fff',
59+
py: '5px',
60+
px: 2
61+
},
62+
'& td:first-of-type': {
63+
borderTopLeftRadius: 8,
64+
borderBottomLeftRadius: 8
65+
},
66+
'& td:last-of-type': {
67+
borderTopRightRadius: 8,
68+
borderBottomRightRadius: 8
69+
}
70+
};
71+
72+
const cellBodySx = {
73+
border: '1px solid',
74+
borderColor: 'neutrals.light',
75+
height: '64px'
76+
};
77+
78+
export default function RoundedTable<T extends Record<string, any>>({
79+
columns,
80+
data,
81+
noDataMessage = 'No data available.',
82+
tableStyles = tableSx,
83+
rowHeadStyles = rowHeadSx,
84+
rowBodyStyles = rowBodySx,
85+
cellBodyStyles = cellBodySx
86+
}: RoundedTableProps<T>) {
87+
if (data.length === 0) {
88+
return (
89+
<Alert severity="info" sx={{ width: '100%', mt: 2 }}>
90+
{noDataMessage}
91+
</Alert>
92+
);
93+
}
94+
95+
return (
96+
<>
97+
<Table sx={tableStyles}>
98+
<TableHead>
99+
<TableRow sx={rowHeadStyles}>
100+
{columns.map((col, colIndex) => (
101+
<TableCell
102+
key={colIndex}
103+
sx={{
104+
minWidth: col.minWidth || '66px',
105+
p: col.headerPadding || 0
106+
}}
107+
align={col.textAlign || 'left'}
108+
>
109+
{col.header}
110+
</TableCell>
111+
))}
112+
</TableRow>
113+
</TableHead>
114+
<TableBody>
115+
{data.map((row, rowIndex) => (
116+
<TableRow key={rowIndex} sx={rowBodyStyles}>
117+
{columns.map((col, colIndex) => (
118+
<TableCell
119+
key={colIndex}
120+
sx={cellBodyStyles}
121+
align={col.textAlign || 'left'}
122+
>
123+
{col.render ? col.render(row[col.key], row) : row[col.key]}
124+
</TableCell>
125+
))}
126+
</TableRow>
127+
))}
128+
</TableBody>
129+
</Table>
130+
<Box sx={{ display: { xs: 'block', sm: 'none' }, textAlign: 'center' }}>
131+
{data.map((row, rowIndex) => (
132+
<Card
133+
key={rowIndex}
134+
sx={{
135+
mb: 2,
136+
pt: 1,
137+
px: 0,
138+
borderColor: 'neutrals.light',
139+
borderRadius: 2
140+
}}
141+
variant="outlined"
142+
>
143+
{columns.map((col, colIndex) => (
144+
<Box key={colIndex}>
145+
<Typography
146+
variant="caption"
147+
color="text.secondary"
148+
align="center"
149+
component="div"
150+
>
151+
{col.header}
152+
</Typography>
153+
<Typography
154+
component="div"
155+
variant="body2"
156+
align="center"
157+
sx={{ py: 2 }}
158+
>
159+
{col.render ? col.render(row[col.key], row) : row[col.key]}
160+
</Typography>
161+
{colIndex < columns.length - 1 && (
162+
<Divider sx={{ my: 1, borderColor: 'neutrals.light' }} />
163+
)}
164+
</Box>
165+
))}
166+
</Card>
167+
))}
168+
</Box>
169+
</>
170+
);
171+
}

0 commit comments

Comments
 (0)