Update errata visual design and add additional filters

This commit is contained in:
Ted Adams 2022-10-31 09:51:22 -07:00
parent 5a7f0693be
commit 6c906cf3e6
9 changed files with 1812 additions and 560 deletions

View File

@ -26,10 +26,13 @@ resf_frontend(
"//common/mui", "//common/mui",
"//common/ui", "//common/ui",
"//tailwind:css", "//tailwind:css",
"@npm//@mui/icons-material", "@npm//@chakra-ui/react",
"@npm//@mui/material", "@npm//@chakra-ui/icons",
"@npm//@mui/styles", "@npm//@emotion/unitless",
"@npm//@mui/x-data-grid", "@npm//framer-motion",
"@npm//framesync",
"@npm//popmotion",
"@npm//style-value-types",
"@npm//await-to-js", "@npm//await-to-js",
"@npm//react", "@npm//react",
"@npm//react-dom", "@npm//react-dom",

View File

@ -30,180 +30,468 @@
* POSSIBILITY OF SUCH DAMAGE. * POSSIBILITY OF SUCH DAMAGE.
*/ */
import React from 'react';
import { import {
DataGrid, AddIcon,
GridColDef, ArrowLeftIcon,
GridColumns, ArrowRightIcon,
GridRowsProp, ChevronDownIcon,
} from '@mui/x-data-grid'; MinusIcon,
SearchIcon,
} from '@chakra-ui/icons';
import { import {
Alert,
AlertDescription,
AlertIcon,
AlertTitle,
Box,
ButtonGroup,
FormControl, FormControl,
InputLabel, FormLabel,
MenuItem, HStack,
IconButton,
Input,
InputGroup,
InputLeftElement,
Select, Select,
CircularProgress, Spinner,
TextField, Stack,
Container, Table,
Typography, TableColumnHeaderProps,
Divider, TableContainer,
} from '@mui/material'; Tbody,
Td,
Text,
Th,
Thead,
Tr,
useColorModeValue,
} from '@chakra-ui/react';
import {
severityToBadge,
severityToText,
typeToBadge,
typeToText,
} from 'apollo/ui/src/enumToText';
import {
ListAdvisoriesFiltersSeverityEnum,
ListAdvisoriesFiltersTypeEnum,
} from 'bazel-bin/apollo/proto/v1/client_typescript';
import {
AdvisorySeverity,
V1Advisory,
V1AdvisoryType,
} from 'bazel-bin/apollo/proto/v1/client_typescript/models';
import { reqap } from 'common/ui/reqap';
import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import {
V1Advisory,
V1ListAdvisoriesResponse,
} from 'bazel-bin/apollo/proto/v1/client_typescript/models';
import { api } from '../api'; import { api } from '../api';
import { reqap } from 'common/ui/reqap'; import { COLOR_RESF_GREEN } from '../styles';
import { severityToBadge, typeToBadge } from 'apollo/ui/src/enumToText';
export const Overview = () => { export const Overview = () => {
// When advisories is set to null that means an error has occurred const inputBackground = useColorModeValue('white', 'gray.800');
// Undefined means loading
const [advisories, setAdvisories] = React.useState<
V1Advisory[] | undefined | null
>();
const [pageSize, setPageSize] = React.useState(25);
const [page, setPage] = React.useState(0);
const [total, setTotal] = React.useState(0);
const [filterSynopsis, setFilterSynopsis] = React.useState<
string | undefined
>();
const [filterCve, setFilterCve] = React.useState<string | undefined>();
React.useEffect(() => { const [advisories, setAdvisories] = useState<V1Advisory[]>();
const timer = setTimeout(() => { const [lastUpdated, setLastUpdated] = useState<Date>();
(async () => { const [total, setTotal] = useState(0);
let err, res: void | V1ListAdvisoriesResponse | undefined; const [isLoading, setIsLoading] = useState(true);
[err, res] = await reqap(() => const [isError, setIsError] = useState(false);
// Request State
const [page, setPage] = useState(0);
const [pageSize, setPageSize] = useState(25);
const [filtersKeyword, setFiltersKeyword] = useState<string>();
const [filterBefore, setFilterBefore] = useState<Date>();
const [filterAfter, setFilterAfter] = useState<Date>();
const [filtersType, setFiltersType] =
useState<keyof typeof ListAdvisoriesFiltersTypeEnum>();
const [filtersSeverity, setFiltersSeverity] =
useState<keyof typeof ListAdvisoriesFiltersSeverityEnum>();
useEffect(() => {
const fetch = async () => {
setIsLoading(true);
const [err, res] = await reqap(() =>
api.listAdvisories({ api.listAdvisories({
page, page,
limit: pageSize, limit: pageSize,
filtersSynopsis: filterSynopsis, filtersKeyword,
filtersCve: filterCve, filtersBefore: filterBefore,
filtersAfter: filterAfter,
filtersSeverity: filtersSeverity
? ListAdvisoriesFiltersSeverityEnum[filtersSeverity]
: undefined,
filtersType: filtersType
? ListAdvisoriesFiltersTypeEnum[filtersType]
: undefined,
}) })
); );
setIsLoading(false);
if (err || !res) { if (err || !res) {
setAdvisories(null); setIsError(true);
setAdvisories(undefined);
return; return;
} }
setIsError(false);
if (res) { if (res) {
setAdvisories(res.advisories); setAdvisories(res.advisories);
setLastUpdated(res.lastUpdated);
setTotal(parseInt(res.total || '0')); setTotal(parseInt(res.total || '0'));
} }
})().then(); };
}, 500);
const timer = setTimeout(() => fetch(), 500);
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [pageSize, page, filterSynopsis, filterCve]); }, [
pageSize,
page,
filtersKeyword,
filterBefore,
filterAfter,
filtersSeverity,
filtersType,
]);
const columns: GridColDef[] = [ // TODO: Figure out why sticky isn't sticking
{ const stickyProps: TableColumnHeaderProps = {
field: 'id', position: 'sticky',
headerName: 'Advisory', top: '0px',
width: 150, zIndex: '10',
sortable: false, scope: 'col',
renderCell: (params) => ( };
<Link
className="no-underline text-peridot-primary visited:text-purple-500" const lastPage = total < pageSize ? 0 : Math.ceil(total / pageSize) - 1;
to={`/${params.value}`}
>
{params.value}
</Link>
),
},
{
field: 'synopsis',
headerName: 'Synopsis',
width: 450,
sortable: false,
},
{
field: 'severity',
headerName: 'Severity',
width: 150,
sortable: false,
renderCell: (params) => severityToBadge(params.value, 'small'),
},
{
field: 'products',
headerName: 'Products',
width: 450,
sortable: false,
},
{
field: 'publish_date',
headerName: 'Publish date',
width: 170,
sortable: false,
},
];
return ( return (
<div className="w-full"> <Box
{advisories === undefined && <CircularProgress />} w="100%"
{advisories === null && ( h="100%"
<h2 className="text-lg text-red-800 font-bold"> display="flex"
Oh no! Something has gone wrong! flexDirection="column"
</h2> p={4}
)} alignItems="stretch"
{advisories && (
<>
<Container
maxWidth={false}
className="flex items-center space-x-4 bg-white"
style={{ paddingTop: '0.5rem', paddingBottom: '0.5rem' }}
> >
<Typography variant="overline">Filters</Typography> <Stack
<TextField direction={{
label="Synopsis" sm: 'column',
variant="outlined" lg: 'row',
size="small" }}
onChange={(e) => setFilterSynopsis(e.target.value)} alignItems={{
sm: 'stretch',
lg: 'flex-end',
}}
>
<InputGroup>
<InputLeftElement>
<SearchIcon />
</InputLeftElement>
<Input
type="search"
aria-label="Keyword search"
placeholder="Keyword Search"
flexGrow={1}
width="200px"
variant="filled"
borderRadius="0"
backgroundColor={inputBackground}
onChange={(e) => setFiltersKeyword(e.target.value)}
/> />
<TextField </InputGroup>
label="CVE" <HStack>
variant="outlined" <FormControl width="180px" flexShrink={0} flexGrow={1}>
size="small" <FormLabel fontSize="sm">Type</FormLabel>
onChange={(e) => setFilterCve(e.target.value)} <Select
aria-label="Type"
placeholder="All"
variant="filled"
background={inputBackground}
borderRadius="0"
value={filtersType}
onChange={(e) => {
if (e.currentTarget.value !== 'Security') {
setFiltersSeverity(undefined);
}
setFiltersType(
e.currentTarget
.value as keyof typeof ListAdvisoriesFiltersTypeEnum
);
}}
>
{Object.keys(ListAdvisoriesFiltersTypeEnum)
.sort((a, b) => a.localeCompare(b))
.filter((a) => a !== 'Unknown')
.map((s) => (
<option key={s} value={s}>
{s}
</option>
))}
</Select>
</FormControl>
{filtersType === 'Security' && (
<FormControl width="180px" flexShrink={0} flexGrow={1}>
<FormLabel fontSize="sm">Severity</FormLabel>
<Select
aria-label="Severity"
placeholder="All"
variant="filled"
background={inputBackground}
borderRadius="0"
value={filtersSeverity}
onChange={(e) =>
setFiltersSeverity(
e.currentTarget
.value as keyof typeof ListAdvisoriesFiltersSeverityEnum
)
}
>
{Object.keys(ListAdvisoriesFiltersSeverityEnum)
.sort((a, b) => a.localeCompare(b))
.filter((a) => a !== 'Unknown')
.map((s) => (
<option key={s} value={s}>
{s}
</option>
))}
</Select>
</FormControl>
)}
</HStack>
<HStack>
<FormControl width="180px" flexShrink={0} flexGrow={1}>
<FormLabel fontSize="sm">After</FormLabel>
<Input
type="date"
variant="filled"
background={inputBackground}
borderRadius="0"
max={
filterBefore
? filterBefore.toLocaleDateString('en-ca')
: new Date().toLocaleDateString('en-ca')
}
value={filterAfter?.toLocaleDateString('en-ca') || ''}
onChange={(e) => {
const newVal = e.currentTarget.value;
console.log(newVal);
if (!newVal) {
setFilterAfter(undefined);
}
const asDate = new Date(newVal);
if (!(asDate instanceof Date) || isNaN(asDate.getTime())) {
// Check value parses as a date
return;
}
const [year, month, date] = newVal.split('-').map(Number);
setFilterAfter(new Date(year, month - 1, date));
}}
/> />
</Container> </FormControl>
<Divider /> <FormControl width="180px" flexShrink={0} flexGrow={1}>
<Container maxWidth={false} disableGutters> <FormLabel fontSize="sm">Before</FormLabel>
<DataGrid <Input
autoHeight type="date"
pagination variant="filled"
disableSelectionOnClick background={inputBackground}
disableDensitySelector borderRadius="0"
disableColumnSelector min={filterAfter?.toLocaleDateString('en-ca')}
disableColumnMenu max={new Date().toLocaleDateString('en-ca')}
className="bg-white" value={filterBefore?.toLocaleDateString('en-ca') || ''}
style={{ borderRadius: 0, border: 0 }} onChange={(e) => {
rows={advisories.map((advisory: V1Advisory) => ({ const newVal = e.currentTarget.value;
id: advisory.name,
synopsis: advisory.synopsis, if (!newVal) {
severity: advisory.severity, setFilterBefore(undefined);
products: advisory.affectedProducts?.join(', '), }
publish_date: Intl.DateTimeFormat('en-US', {
const asDate = new Date(newVal);
if (!(asDate instanceof Date) || isNaN(asDate.getTime())) {
// Check value parses as a date
return;
}
const [year, month, date] = newVal.split('-').map(Number);
setFilterBefore(new Date(year, month - 1, date));
}}
/>
</FormControl>
</HStack>
</Stack>
<HStack my={4} justifyContent="space-between" flexWrap="wrap">
<Text fontStyle="italic" fontSize="xs">
Last updated {lastUpdated?.toLocaleString() || 'never'}
</Text>
<HStack>
<Text fontSize="xs">
Displaying {(page * pageSize + 1).toLocaleString()}-
{Math.min(total, page * pageSize + pageSize).toLocaleString()} of{' '}
{total.toLocaleString()}
</Text>
<ButtonGroup
size="xs"
isAttached
alignItems="stretch"
colorScheme="blackAlpha"
>
<IconButton
aria-label="First Page"
icon={<ArrowLeftIcon fontSize="8px" />}
disabled={page <= 0}
onClick={() => setPage(0)}
/>
<IconButton
aria-label="Previous Page"
icon={<MinusIcon fontSize="8px" />}
disabled={page <= 0}
onClick={() => setPage((old) => old - 1)}
/>
<Text
fontSize="xs"
// borderTop="1px solid"
// borderBottom="1px solid"
borderColor="gray.200"
backgroundColor="white"
lineHeight="24px"
px={2}
>
{(page + 1).toLocaleString()} / {(lastPage + 1).toLocaleString()}
</Text>
<IconButton
aria-label="Next Page"
icon={<AddIcon fontSize="8px" />}
disabled={page >= lastPage}
onClick={() => setPage((old) => old + 1)}
/>
<IconButton
aria-label="Last Page"
icon={<ArrowRightIcon fontSize="8px" />}
disabled={page >= lastPage}
onClick={() => setPage(lastPage)}
/>
</ButtonGroup>
</HStack>
</HStack>
{isLoading ? (
<Spinner
m="auto"
size="xl"
alignSelf="center"
color={COLOR_RESF_GREEN}
thickness="3px"
/>
) : isError ? (
<Alert
status="error"
m="auto"
flexDirection="column"
width="300px"
borderRadius="md"
>
<AlertIcon mr="0" />
<AlertTitle>Something has gone wrong</AlertTitle>
<AlertDescription>Failed to load errata</AlertDescription>
</Alert>
) : (
<Box backgroundColor="white" boxShadow="base">
<TableContainer>
<Table size="sm" variant="striped">
<Thead>
<Tr>
<Th {...stickyProps} width="36px" />
<Th {...stickyProps}>Advisory</Th>
<Th {...stickyProps}>Synopsis</Th>
<Th {...stickyProps}>Type / Severity</Th>
<Th {...stickyProps}>Products</Th>
<Th {...stickyProps}>
<HStack spacing={1}>
<Text>Issue Date</Text>
<ChevronDownIcon />
</HStack>
</Th>
</Tr>
</Thead>
<Tbody>
{!advisories?.length && (
<Tr>
<Td colSpan={6} textAlign="center">
<Text>No rows found</Text>
</Td>
</Tr>
)}
{advisories?.map((a) => (
<Tr key={a.name}>
<Td textAlign="center" pr={0}>
{severityToBadge(a.severity)}
</Td>
<Td>
<Link
className="text-peridot-primary visited:text-purple-500"
to={`/${a.name}`}
>
{a.name}
</Link>
</Td>
<Td>
{a.synopsis?.replace(
/^(Critical|Important|Moderate|Low): /,
''
)}
</Td>
<Td>
{typeToText(a.type)}
{a.type === V1AdvisoryType.Security
? ` / ${severityToText(a.severity)}`
: ''}
</Td>
<Td>{a.affectedProducts?.join(', ')}</Td>
<Td>
{Intl.DateTimeFormat(undefined, {
day: '2-digit', day: '2-digit',
month: 'short', month: 'short',
year: 'numeric', year: 'numeric',
}).format(advisory.publishedAt), }).format(a.publishedAt)}
}))} </Td>
rowsPerPageOptions={[10, 25, 50, 100]} </Tr>
rowCount={total} ))}
paginationMode="server" </Tbody>
columns={columns} </Table>
density="compact" </TableContainer>
pageSize={pageSize} </Box>
onPageChange={(page) => setPage(page)}
onPageSizeChange={(newPageSize) => setPageSize(newPageSize)}
/>
</Container>
</>
)} )}
</div> <HStack justifyContent="flex-end" mt={4}>
<Text as="label" htmlFor="row-count" fontSize="sm">
Rows per page:
</Text>
<Select
id="row-count"
name="row-count"
variant="filled"
backgroundColor={inputBackground}
width="100px"
size="sm"
value={pageSize}
onChange={(e) => {
setPage(0);
setPageSize(Number(e.currentTarget.value));
}}
>
{[10, 25, 50, 100].map((count) => (
<option key={count} value={count}>
{count.toLocaleString()}
</option>
))}
</Select>
</HStack>
</Box>
); );
}; };

View File

@ -30,108 +30,55 @@
* POSSIBILITY OF SUCH DAMAGE. * POSSIBILITY OF SUCH DAMAGE.
*/ */
import React from 'react'; import { Box, HStack, Text } from '@chakra-ui/react';
import {
AppBar,
Toolbar,
Container,
CssBaseline,
Drawer,
Divider,
IconButton,
List,
} from '@mui/material';
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
import MenuIcon from '@mui/icons-material/Menu';
import { useStyles } from '../styles';
import { Switch, Route } from 'react-router';
import { Overview } from './Overview';
import { Link } from 'react-router-dom';
import { RESFLogo } from 'common/ui/RESFLogo'; import { RESFLogo } from 'common/ui/RESFLogo';
import classnames from 'classnames'; import React from 'react';
import { Route, Switch } from 'react-router';
import { Link } from 'react-router-dom';
import { COLOR_RESF_BLUE, COLOR_RESF_GREEN } from '../styles';
import { Overview } from './Overview';
import { ShowErrata } from './ShowErrata'; import { ShowErrata } from './ShowErrata';
export const Root = () => { export const Root = () => {
const [open, setOpen] = React.useState(false);
const classes = useStyles();
const handleDrawerClose = () => {
setOpen(false);
};
const handleDrawerOpen = () => {
setOpen(true);
};
const inManage = location.pathname.startsWith('/manage');
return ( return (
<div className={classes.root}> <Box
<AppBar display="flex"
position="absolute" width="100%"
className={classnames( minHeight="100vh"
inManage && classes.appBar, flexDirection="column"
open && classes.appBarShift alignItems="stretch"
)}
> >
<Toolbar className={classes.toolbar}> <Box
{inManage && ( background={`linear-gradient(to right, ${COLOR_RESF_GREEN}, ${COLOR_RESF_BLUE})`}
<IconButton display="flex"
edge="start" flexDirection="row"
color="inherit" alignItems="center"
aria-label="open drawer" py="1"
onClick={handleDrawerOpen} px={4}
className={classnames(
classes.menuButton,
open && classes.menuButtonHidden
)}
> >
<MenuIcon />
</IconButton>
)}
<Link to="/" className="no-underline text-white"> <Link to="/" className="no-underline text-white">
<div <HStack flexGrow={1} height="90%" spacing="2">
className={classnames(
classes.title,
'flex items-center space-x-4'
)}
>
<RESFLogo className="fill-current text-white" /> <RESFLogo className="fill-current text-white" />
<div className="font-bold text-lg text-white no-underline"> <Text
Product Errata{inManage && ' (Admin)'} borderLeft="1px solid"
</div> pl="2"
</div> lineHeight="30px"
</Link> fontSize="xl"
</Toolbar> fontWeight="300"
</AppBar> color="white"
{inManage && (
<Drawer
variant="permanent"
classes={{
paper: classnames(
classes.drawerPaper,
!open && classes.drawerPaperClose
),
}}
open={open}
> >
<div className={classes.toolbarIcon}> Product Errata
<IconButton onClick={handleDrawerClose}> </Text>
<ChevronLeftIcon /> </HStack>
</IconButton> </Link>
</div> </Box>
<Divider /> <Box as="main" flexGrow={1} overflow="auto">
</Drawer>
)}
<main className={classes.content}>
<div className={classes.appBarSpacer} />
<Switch> <Switch>
<Route path="/" exact component={Overview} /> <Route path="/" exact component={Overview} />
<Route path="/:id" component={ShowErrata} /> <Route path="/:id" component={ShowErrata} />
</Switch> </Switch>
</main> </Box>
</div> </Box>
); );
}; };

View File

@ -30,25 +30,38 @@
* POSSIBILITY OF SUCH DAMAGE. * POSSIBILITY OF SUCH DAMAGE.
*/ */
import React from 'react';
import { import {
V1Advisory, Alert,
V1GetAdvisoryResponse, AlertDescription,
} from 'bazel-bin/apollo/proto/v1/client_typescript'; AlertIcon,
import { reqap } from 'common/ui/reqap'; AlertTitle,
import { api } from '../api'; Box,
import { RouteComponentProps } from 'react-router'; Breadcrumb,
import { BreadcrumbItem,
Card, BreadcrumbLink,
CardContent, Heading,
Chip, HStack,
CircularProgress, Link,
Paper, ListItem,
Spinner,
Tab, Tab,
TabList,
TabPanel,
TabPanels,
Tabs, Tabs,
Typography, Text,
} from '@mui/material'; UnorderedList,
import { severityToBadge, severityToText, typeToText } from 'apollo/ui/src/enumToText'; VStack,
} from '@chakra-ui/react';
import { severityToBadge, typeToText } from 'apollo/ui/src/enumToText';
import { V1Advisory } from 'bazel-bin/apollo/proto/v1/client_typescript';
import { reqap } from 'common/ui/reqap';
import React, { useState } from 'react';
import { RouteComponentProps } from 'react-router';
import { Link as RouterLink } from 'react-router-dom';
import { api } from '../api';
import { COLOR_RESF_BLUE, COLOR_RESF_GREEN } from '../styles';
interface ShowErrataParams { interface ShowErrataParams {
id: string; id: string;
@ -58,182 +71,249 @@ export interface ShowErrataProps
extends RouteComponentProps<ShowErrataParams> {} extends RouteComponentProps<ShowErrataParams> {}
export const ShowErrata = (props: ShowErrataProps) => { export const ShowErrata = (props: ShowErrataProps) => {
const [errata, setErrata] = React.useState< const id = props.match.params.id;
V1Advisory | undefined | null
>(); const [errata, setErrata] = useState<V1Advisory>();
const [tabValue, setTabValue] = React.useState(0); const [isLoading, setIsLoading] = useState(true);
const [isError, setIsError] = useState(false);
React.useEffect(() => { React.useEffect(() => {
(async () => { const fetch = async () => {
let err, res: void | V1GetAdvisoryResponse | undefined; setIsLoading(true);
[err, res] = await reqap(() =>
api.getAdvisory({ id: props.match.params.id }) const [err, res] = await reqap(() => api.getAdvisory({ id }));
);
setIsLoading(false);
if (err || !res) { if (err || !res) {
setErrata(null); setIsError(true);
setErrata(undefined);
return; return;
} }
if (res) { setIsError(false);
setErrata(res.advisory);
}
})().then();
}, []);
const handleTabChange = ({}, val: number) => { setErrata(res.advisory);
setTabValue(val);
}; };
fetch();
}, [id]);
return ( return (
<div> <Box
{errata === undefined && <CircularProgress />} w="100%"
{errata === null && ( h="100%"
<Typography variant="h2" className="text-lg text-red-800 font-bold"> display="flex"
Oh no! Something has gone wrong! flexDirection="column"
</Typography> p={4}
)} alignItems="stretch"
{errata && ( maxWidth="1300px"
<> m="auto"
<div className="flex items-center justify-between mt-4 mb-6">
<Typography variant="h5">{errata.name} </Typography>
<div className="flex space-x-4 h-full">
{severityToBadge(errata.severity)}
<Chip
color="primary"
label={`Issued at ${errata.publishedAt?.toLocaleDateString()}`}
/>
</div>
</div>
<Card>
<Paper square>
<Tabs
value={tabValue}
indicatorColor="primary"
textColor="primary"
onChange={handleTabChange}
aria-label="disabled tabs example"
> >
<Tab value={0} label="Erratum" /> <Breadcrumb mb={4}>
<Tab value={1} label="Affected packages" /> <BreadcrumbItem>
</Tabs> <BreadcrumbLink as={RouterLink} to="/">
</Paper> Product Errata
{tabValue === 0 && ( </BreadcrumbLink>
<CardContent className="max-w-5xl space-y-4"> </BreadcrumbItem>
<div> <BreadcrumbItem>
<Typography variant="h6">Synopsis</Typography> <BreadcrumbLink isCurrentPage>{id}</BreadcrumbLink>
{errata.synopsis} </BreadcrumbItem>
</div> </Breadcrumb>
<div> {isLoading ? (
<Typography variant="h6">Type</Typography> <Spinner
{typeToText(errata.type)} m="auto"
</div> size="xl"
<div> alignSelf="center"
<Typography variant="h6">Severity</Typography> color={COLOR_RESF_GREEN}
{severityToText(errata.severity)} thickness="3px"
</div> />
<div> ) : isError ? (
<Typography variant="h6">Topic</Typography> <Alert
{errata.topic?.split('\n').map((x) => ( status="error"
<p>{x}</p> m="auto"
flexDirection="column"
width="300px"
borderRadius="md"
>
<AlertIcon mr="0" />
<AlertTitle>Something has gone wrong</AlertTitle>
<AlertDescription>Failed to load errata</AlertDescription>
</Alert>
) : (
errata && (
<>
<HStack
alignItems="center"
backgroundColor="white"
py="2"
px="4"
spacing="6"
mb={2}
>
{severityToBadge(errata.severity, 40)}
<VStack alignItems="stretch" spacing="0" flexGrow={1}>
<HStack justifyContent="space-between">
<Text fontSize="lg" fontWeight="bold">
{errata.name}
</Text>
</HStack>
<Text fontSize="sm">{errata.synopsis}</Text>
</VStack>
</HStack>
<Tabs backgroundColor="white" p="2">
<TabList>
<Tab>Erratum</Tab>
<Tab>Affected Packages</Tab>
</TabList>
<Box
display="flex"
flexDir="row"
alignItems="stretch"
flexWrap="wrap"
justifyContent="space-between"
>
<TabPanels maxWidth="850px" px="2">
<TabPanel>
<Heading as="h2" size="md">
Topic
</Heading>
{errata.topic?.split('\n').map((p, i) => (
<Text key={i} mt={2}>
{p}
</Text>
))} ))}
</div> <Heading as="h2" size="md" mt={4}>
<div> Description
<Typography variant="h6">Description</Typography> </Heading>
{errata.description?.split('\n').map((x) => ( {errata.description?.split('\n').map((p, i) => (
<p>{x}</p> <Text key={i} mt={2}>
{p}
</Text>
))} ))}
</div> </TabPanel>
<div> <TabPanel>
<Typography variant="h6">Affected products</Typography> <VStack alignItems="flex-start" spacing="6">
<ul> {Object.keys(errata.rpms || {}).map((product) => (
{errata.affectedProducts?.map((x) => ( <div key={product}>
<li>{x}</li> <Heading as="h2" size="lg" mb={4} fontWeight="300">
{product}
</Heading>
<Heading as="h3" size="md" mt={2}>
SRPMs
</Heading>
<UnorderedList pl="4">
{errata.rpms?.[product]?.nvras
?.filter((x) => x.indexOf('.src.rpm') !== -1)
.map((x) => (
<ListItem key={x}>{x}</ListItem>
))} ))}
</ul> </UnorderedList>
<Heading as="h3" size="md" mt={2}>
RPMs
</Heading>
<UnorderedList pl="4">
{errata.rpms?.[product]?.nvras
?.filter((x) => x.indexOf('.src.rpm') === -1)
.map((x) => (
<ListItem key={x}>{x}</ListItem>
))}
</UnorderedList>
</div> </div>
<div> ))}
<Typography variant="h6">Fixes</Typography> </VStack>
<ul> </TabPanel>
{errata.fixes?.map((x) => ( </TabPanels>
<li> <VStack
<a py="4"
px="8"
alignItems="flex-start"
minWidth="300px"
spacing="5"
flexShrink={0}
backgroundColor="gray.100"
>
<Text>
<b>Issued:</b> {errata.publishedAt?.toLocaleDateString()}
</Text>
<Text>
<b>Type:</b> {typeToText(errata.type)}
</Text>
<Box>
<Text fontWeight="bold">
Affected Product
{(errata.affectedProducts?.length || 0) > 1 ? 's' : ''}
</Text>
<UnorderedList>
{errata.affectedProducts?.map((x, idx) => (
<ListItem key={idx}>{x}</ListItem>
))}
</UnorderedList>
</Box>
<Box>
<Text fontWeight="bold">Fixes</Text>
<UnorderedList>
{errata.fixes?.map((x, idx) => (
<ListItem key={idx}>
<Link
href={x.sourceLink} href={x.sourceLink}
target="_blank" isExternal
color={COLOR_RESF_BLUE}
> >
{x.sourceBy} - {x.ticket} {x.sourceBy} - {x.ticket}
</a> </Link>
</li> </ListItem>
))} ))}
</ul> </UnorderedList>
</div> </Box>
<div> <Box>
<Typography variant="h6">CVEs</Typography> <Text fontWeight="bold">CVEs</Text>
<ul> <UnorderedList>
{errata.cves?.map((x) => { {!!errata.cves?.length ? (
errata.cves?.map((x, idx) => {
let text = `${x.name}${ let text = `${x.name}${
x.sourceBy !== '' && ` (Source: ${x.sourceBy})` x.sourceBy !== '' && ` (Source: ${x.sourceBy})`
}`; }`;
return ( return (
<li> <ListItem key={idx}>
{x.sourceLink === '' ? ( {x.sourceLink === '' ? (
<span>{text}</span> <span>{text}</span>
) : ( ) : (
<a href={x.sourceLink} target="_blank"> <Link
href={x.sourceLink}
isExternal
color={COLOR_RESF_BLUE}
>
{text} {text}
</a> </Link>
)} )}
</li> </ListItem>
); );
})} })
{errata.cves?.length === 0 && <li>No CVEs</li>} ) : (
</ul> <ListItem>No CVEs</ListItem>
</div>
<div>
<Typography variant="h6">References</Typography>
<ul>
{errata.references?.map((x) => (
<li>{x}</li>
))}
{errata.references?.length === 0 && <li>No references</li>}
</ul>
</div>
</CardContent>
)} )}
{tabValue === 1 && ( </UnorderedList>
<CardContent className="max-w-5xl"> </Box>
<div className="space-x-4 divide-y py-2"> <Box>
{Object.keys(errata.rpms || {}).map(product => ( <Text fontWeight="bold">References</Text>
<div className="space-y-4"> <UnorderedList>
<Typography variant="h6">{product}</Typography> {!!errata.references?.length ? (
<div> errata.references?.map((x, idx) => (
<Typography variant="subtitle1">SRPMs</Typography> <ListItem key={idx}>{x}</ListItem>
<ul> ))
{errata.rpms[product].nvras ) : (
?.filter((x) => x.indexOf('.src.rpm') !== -1) <ListItem>No references</ListItem>
.map((x) => (
<li>{x}</li>
))}
</ul>
</div>
<div>
<Typography variant="subtitle1">RPMs</Typography>
<ul>
{errata.rpms[product].nvras
?.filter((x) => x.indexOf('.src.rpm') === -1)
.map((x) => (
<li>{x}</li>
))}
</ul>
</div>
</div>
))}
</div>
</CardContent>
)} )}
</Card> </UnorderedList>
</Box>
</VStack>
</Box>
</Tabs>
</> </>
)
)} )}
</div> </Box>
); );
}; };

View File

@ -30,23 +30,21 @@
* POSSIBILITY OF SUCH DAMAGE. * POSSIBILITY OF SUCH DAMAGE.
*/ */
import 'tailwind/tailwind.css';
import { ChakraProvider } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import CssBaseline from '@mui/material/CssBaseline';
import { Root } from './components/Root'; import { Root } from './components/Root';
import 'tailwind/tailwind.css';
import { PeridotThemeProvider } from 'common/mui/theme';
export const app = () => { export const app = () => {
ReactDOM.render( ReactDOM.render(
<BrowserRouter> <BrowserRouter>
<CssBaseline /> <ChakraProvider>
<PeridotThemeProvider>
<Root /> <Root />
</PeridotThemeProvider> </ChakraProvider>
</BrowserRouter>, </BrowserRouter>,
document.getElementById('root') document.getElementById('root')
); );

View File

@ -29,13 +29,10 @@
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE. * POSSIBILITY OF SUCH DAMAGE.
*/ */
import React from 'react';
import { import { Box, Tag, TagProps } from '@chakra-ui/react';
AdvisorySeverity, import { AdvisorySeverity, V1AdvisoryType } from 'bazel-bin/apollo/proto/v1/client_typescript';
V1AdvisoryType, import React from 'react';
} from 'bazel-bin/apollo/proto/v1/client_typescript';
import Chip from '@mui/material/Chip';
export const severityToText = (severity?: AdvisorySeverity): string => { export const severityToText = (severity?: AdvisorySeverity): string => {
switch (severity) { switch (severity) {
@ -54,26 +51,105 @@ export const severityToText = (severity?: AdvisorySeverity): string => {
export const severityToBadge = ( export const severityToBadge = (
severity?: AdvisorySeverity, severity?: AdvisorySeverity,
size?: 'small', size: number = 20
): React.ReactNode => { ): React.ReactNode => {
let color: 'primary' | 'secondary' | 'success' | 'info' | 'error' | 'warning' = 'success'; return {
[AdvisorySeverity.Critical]: (
switch (severity) { <Box
case AdvisorySeverity.Critical: as="svg"
color = 'error'; version="1.1"
break; id="prefix__Layer_1"
case AdvisorySeverity.Important: xmlns="http://www.w3.org/2000/svg"
color = 'warning'; x="0"
break; y="0"
case AdvisorySeverity.Moderate: viewBox="0 0 24 24"
color = 'secondary'; xmlSpace="preserve"
break; width={`${size}px`}
case AdvisorySeverity.Low: height={`${size}px`}
color = 'primary'; display="inline-block"
break; >
} <g fill="#ED1C24">
<path d="M22.2 19.2l-8.8-16c-.2-.3-.5-.6-.8-.7-.3-.1-.7-.1-1.1 0-.3.1-.6.4-.8.7l-8.8 16c-.3.5-.2 1 0 1.5.3.5.8.7 1.3.8h17.7c.3 0 .5-.1.8-.2.2-.1.4-.3.5-.6.2-.4.2-1 0-1.5zm-18.8.6L12 4.3l8.6 15.5H3.4z" />
return <Chip label={severityToText(severity)} color={color} size={size} variant={size ? 'outlined' : undefined} />; <path d="M12 15.7c-.2 0-.4.1-.6.2-.2.2-.2.4-.2.6v.8c0 .3.2.6.4.7.3.1.6.1.8 0s.4-.4.4-.7v-.8c0-.2-.1-.4-.2-.6-.2-.1-.4-.2-.6-.2zM11.2 9v5c0 .3.2.6.4.7.3.1.6.1.8 0 .3-.1.4-.4.4-.7V9c0-.3-.2-.6-.4-.7-.3-.1-.6-.1-.8 0s-.4.4-.4.7z" />
</g>
</Box>
),
[AdvisorySeverity.Important]: (
<Box
as="svg"
width={`${size}px`}
height={`${size}px`}
display="inline-block"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
>
<g fill="#F7941D">
<path d="M22.2 19.2l-8.8-16c-.2-.3-.5-.6-.8-.7-.3-.1-.7-.1-1.1 0-.3.1-.6.4-.8.7l-8.8 16c-.3.5-.2 1 0 1.5.3.5.8.7 1.3.8h17.7c.3 0 .5-.1.8-.2.2-.1.4-.3.5-.6.2-.4.2-1 0-1.5zm-18.8.6L12 4.3l8.6 15.5H3.4z" />
<path d="M12 15.7c-.2 0-.4.1-.6.2-.2.2-.2.4-.2.6v.8c0 .3.2.6.4.7.3.1.6.1.8 0s.4-.4.4-.7v-.8c0-.2-.1-.4-.2-.6-.2-.1-.4-.2-.6-.2zM11.2 9v5c0 .3.2.6.4.7.3.1.6.1.8 0 .3-.1.4-.4.4-.7V9c0-.3-.2-.6-.4-.7-.3-.1-.6-.1-.8 0s-.4.4-.4.7z" />
</g>
</Box>
),
[AdvisorySeverity.Moderate]: (
<Box
as="svg"
width={`${size}px`}
height={`${size}px`}
display="inline-block"
version="1.1"
id="prefix__Layer_1"
xmlns="http://www.w3.org/2000/svg"
x="0"
y="0"
viewBox="0 0 24 24"
xmlSpace="preserve"
>
<path
fill="#ffc31a"
d="M22.2 19.2l-8.8-16c-.2-.3-.5-.6-.8-.7-.3-.1-.7-.1-1.1 0-.3.1-.6.4-.8.7l-8.8 16c-.3.5-.2 1 0 1.5.3.5.8.7 1.3.8h17.7c.3 0 .5-.1.8-.2.2-.1.4-.3.5-.6.2-.4.2-1 0-1.5zm-18.8.6L12 4.3l8.6 15.5H3.4z"
/>
<path
fill="#ffc31a"
d="M12 15.7c-.2 0-.4.1-.6.2-.2.2-.2.4-.2.6v.8c0 .3.2.6.4.7.3.1.6.1.8 0s.4-.4.4-.7v-.8c0-.2-.1-.4-.2-.6-.2-.1-.4-.2-.6-.2zM11.2 9v5c0 .3.2.6.4.7.3.1.6.1.8 0 .3-.1.4-.4.4-.7V9c0-.3-.2-.6-.4-.7-.3-.1-.6-.1-.8 0s-.4.4-.4.7z"
/>
</Box>
),
[AdvisorySeverity.Low]: (
<Box
as="svg"
width={`${size}px`}
height={`${size}px`}
display="inline-block"
version="1.1"
id="prefix__Layer_1"
xmlns="http://www.w3.org/2000/svg"
x="0"
y="0"
viewBox="0 0 24 24"
xmlSpace="preserve"
>
<g fill="#39B54A">
<path d="M22.2 19.2l-8.8-16c-.2-.3-.5-.6-.8-.7-.3-.1-.7-.1-1.1 0-.3.1-.6.4-.8.7l-8.8 16c-.3.5-.2 1 0 1.5.3.5.8.7 1.3.8h17.7c.3 0 .5-.1.8-.2.2-.1.4-.3.5-.6.2-.4.2-1 0-1.5zm-18.8.6L12 4.3l8.6 15.5H3.4z" />
<path d="M12 15.7c-.2 0-.4.1-.6.2-.2.2-.2.4-.2.6v.8c0 .3.2.6.4.7.3.1.6.1.8 0s.4-.4.4-.7v-.8c0-.2-.1-.4-.2-.6-.2-.1-.4-.2-.6-.2zM11.2 9v5c0 .3.2.6.4.7.3.1.6.1.8 0 .3-.1.4-.4.4-.7V9c0-.3-.2-.6-.4-.7-.3-.1-.6-.1-.8 0s-.4.4-.4.7z" />
</g>
</Box>
),
[AdvisorySeverity.Unknown]: (
<Box
as="svg"
width={`${size}px`}
height={`${size}px`}
display="inline-block"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
>
<g fill="#009444">
<path d="M22 5.6c0-.2 0-.3-.1-.4s-.2-.2-.4-.3L12.3 2h-.5L2.5 4.9c-.1 0-.3.1-.4.2-.1.2-.1.3-.1.5C2 6 1.6 15 6 19.5c.8.8 1.7 1.5 2.8 1.9 1 .4 2.2.6 3.3.6s2.2-.2 3.3-.6c1-.4 2-1.1 2.7-1.9C22.4 14.9 22 5.9 22 5.6zm-5 12.9c-1.3 1.4-3.1 2.1-5 2.1s-3.7-.7-5-2.1C3.6 15 3.4 8 3.4 6.1L12 3.4l8.6 2.7c0 1.9-.2 8.9-3.6 12.4z" />
<path d="M5.4 7c-.2 0-.3.1-.4.3-.1.1-.1.3-.1.4.1 2.1.6 7.2 3.1 9.8 1 1.1 2.4 1.7 3.9 1.6h.1c1.5 0 2.9-.6 3.9-1.6 2.5-2.6 3-7.7 3.2-9.8 0-.2 0-.3-.1-.4s-.2-.2-.4-.3l-6.4-2h-.4L5.4 7zm12.3 1.2c-.2 2.1-.7 6.3-2.7 8.3-.8.8-1.8 1.2-2.9 1.2H12c-1.1 0-2.1-.4-2.9-1.2-2-2.1-2.5-6.2-2.7-8.3L12 6.5l5.7 1.7z" />
<path d="M8.9 12.5l1.4 2.1c.1.2.3.3.6.3.2 0 .4-.1.5-.3l3.6-4.3c.2-.2.2-.5.1-.7-.1-.2-.3-.4-.5-.5-.3 0-.5.1-.7.2L11 13l-.9-1.3c-.1-.2-.3-.3-.5-.3s-.4 0-.6.1c-.2.1-.3.3-.3.5 0 .1.1.3.2.5z" />
</g>
</Box>
),
}[severity || AdvisorySeverity.Unknown];
}; };
export const typeToText = (type?: V1AdvisoryType): string => { export const typeToText = (type?: V1AdvisoryType): string => {
@ -91,14 +167,15 @@ export const typeToText = (type?: V1AdvisoryType): string => {
export const typeToBadge = ( export const typeToBadge = (
type?: V1AdvisoryType, type?: V1AdvisoryType,
size?: 'small', size: TagProps['size'] = 'sm'
): React.ReactNode => { ): React.ReactNode => {
let color: 'info' | 'warning' = 'info'; return (
<Tag
switch (type) { variant="outline"
case V1AdvisoryType.Security: size={size}
color = 'warning'; colorScheme={type === V1AdvisoryType.Security ? 'orange' : 'gray'}
} >
{typeToText(type)}
return <Chip label={typeToText(type)} color={color} size={size} variant={size ? 'outlined' : undefined} />; </Tag>
);
}; };

View File

@ -30,85 +30,5 @@
* POSSIBILITY OF SUCH DAMAGE. * POSSIBILITY OF SUCH DAMAGE.
*/ */
import { makeStyles } from '@mui/styles'; export const COLOR_RESF_GREEN = '#10B981';
export const COLOR_RESF_BLUE = '#1054B9';
const drawerWidth = 240;
export const useStyles = makeStyles((theme) => ({
root: {
display: 'flex',
width: '100%',
},
toolbar: {
paddingRight: 24, // keep right padding when drawer closed
},
toolbarIcon: {
display: 'flex',
alignItems: 'center',
padding: '0 8px',
...theme.mixins.toolbar,
},
appBar: {
zIndex: theme.zIndex.drawer + 1,
transition: theme.transitions.create(['width', 'margin'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
},
appBarShift: {
marginLeft: drawerWidth,
width: `calc(100% - ${drawerWidth}px)`,
transition: theme.transitions.create(['width', 'margin'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
},
menuButton: {
marginRight: 36,
},
menuButtonHidden: {
display: 'none',
},
title: {
flexGrow: 1,
},
drawerPaper: {
position: 'relative',
whiteSpace: 'nowrap',
width: drawerWidth,
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
},
drawerPaperClose: {
overflowX: 'hidden',
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
width: theme.spacing(7),
[theme.breakpoints.up('sm')]: {
width: theme.spacing(9),
},
},
appBarSpacer: theme.mixins.toolbar,
content: {
flexGrow: 1,
height: '100vh',
overflow: 'auto',
},
container: {
paddingTop: theme.spacing(2),
paddingBottom: theme.spacing(4),
},
paper: {
padding: theme.spacing(2),
display: 'flex',
overflow: 'auto',
flexDirection: 'column',
},
fixedHeight: {
height: 240,
},
}));

View File

@ -5,6 +5,9 @@
"description": "", "description": "",
"author": "Mustafa Gezen <mustafa@ctrliq.com>", "author": "Mustafa Gezen <mustafa@ctrliq.com>",
"main": "index.js", "main": "index.js",
"scripts": {
"dev:errata": "URL_API=https://apollo.build.resf.org ibazel run //apollo/ui:apollo.server"
},
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.11.6", "@babel/cli": "^7.11.6",
"@babel/core": "^7.11.6", "@babel/core": "^7.11.6",
@ -16,9 +19,13 @@
"@babel/preset-typescript": "^7.10.4", "@babel/preset-typescript": "^7.10.4",
"@bazel/buildifier": "^5.1.0", "@bazel/buildifier": "^5.1.0",
"@bazel/hide-bazel-files": "^1.7.0", "@bazel/hide-bazel-files": "^1.7.0",
"@bazel/ibazel": "^0.16.2",
"@bazel/typescript": "^3.7.0", "@bazel/typescript": "^3.7.0",
"@chakra-ui/icons": "^1",
"@chakra-ui/react": "^1.0.0",
"@emotion/react": "^11.8.1", "@emotion/react": "^11.8.1",
"@emotion/styled": "^11.8.1", "@emotion/styled": "^11.8.1",
"@emotion/unitless": "^0.8.0",
"@heroicons/react": "^1.0.1", "@heroicons/react": "^1.0.1",
"@loadable/component": "^5.15.2", "@loadable/component": "^5.15.2",
"@mui/icons-material": "^5.2.4", "@mui/icons-material": "^5.2.4",
@ -56,6 +63,8 @@
"evil-dns": "^0.2.0", "evil-dns": "^0.2.0",
"express-openid-connect": "^2.7.2", "express-openid-connect": "^2.7.2",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"framer-motion": "^6",
"framesync": "^6.1.2",
"fs-extra": "^10.1.0", "fs-extra": "^10.1.0",
"glob": "^7.1.7", "glob": "^7.1.7",
"hbs": "^4.1.1", "hbs": "^4.1.1",
@ -66,6 +75,7 @@
"native-url": "^0.3.4", "native-url": "^0.3.4",
"optimize-css-assets-webpack-plugin": "^6.0.1", "optimize-css-assets-webpack-plugin": "^6.0.1",
"path-to-regexp": "^2.4.0", "path-to-regexp": "^2.4.0",
"popmotion": "^11.0.5",
"popper.js": "^1.16.1", "popper.js": "^1.16.1",
"postcss": "^8.3.0", "postcss": "^8.3.0",
"postcss-cli": "^8.3.1", "postcss-cli": "^8.3.1",
@ -82,6 +92,7 @@
"stackframe": "^1.2.0", "stackframe": "^1.2.0",
"strip-ansi": "^6.0.0", "strip-ansi": "^6.0.0",
"style-loader": "^2.0.0", "style-loader": "^2.0.0",
"style-value-types": "^5.1.2",
"styled-components": "^5.3.3", "styled-components": "^5.3.3",
"tailwindcss": "^3.1.8", "tailwindcss": "^3.1.8",
"terser-webpack-plugin": "^5.1.2", "terser-webpack-plugin": "^5.1.2",

942
yarn.lock

File diff suppressed because it is too large Load Diff