mirror of
https://github.com/rocky-linux/peridot.git
synced 2024-12-18 08:58:30 +00:00
Update errata visual design and add additional filters
This commit is contained in:
parent
5a7f0693be
commit
6c906cf3e6
@ -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",
|
||||||
|
@ -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);
|
||||||
api.listAdvisories({
|
|
||||||
page,
|
|
||||||
limit: pageSize,
|
|
||||||
filtersSynopsis: filterSynopsis,
|
|
||||||
filtersCve: filterCve,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
if (err || !res) {
|
|
||||||
setAdvisories(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res) {
|
// Request State
|
||||||
setAdvisories(res.advisories);
|
const [page, setPage] = useState(0);
|
||||||
setTotal(parseInt(res.total || '0'));
|
const [pageSize, setPageSize] = useState(25);
|
||||||
}
|
const [filtersKeyword, setFiltersKeyword] = useState<string>();
|
||||||
})().then();
|
const [filterBefore, setFilterBefore] = useState<Date>();
|
||||||
}, 500);
|
const [filterAfter, setFilterAfter] = useState<Date>();
|
||||||
|
const [filtersType, setFiltersType] =
|
||||||
|
useState<keyof typeof ListAdvisoriesFiltersTypeEnum>();
|
||||||
|
const [filtersSeverity, setFiltersSeverity] =
|
||||||
|
useState<keyof typeof ListAdvisoriesFiltersSeverityEnum>();
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
useEffect(() => {
|
||||||
}, [pageSize, page, filterSynopsis, filterCve]);
|
const fetch = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
const [err, res] = await reqap(() =>
|
||||||
|
api.listAdvisories({
|
||||||
|
page,
|
||||||
|
limit: pageSize,
|
||||||
|
filtersKeyword,
|
||||||
|
filtersBefore: filterBefore,
|
||||||
|
filtersAfter: filterAfter,
|
||||||
|
filtersSeverity: filtersSeverity
|
||||||
|
? ListAdvisoriesFiltersSeverityEnum[filtersSeverity]
|
||||||
|
: undefined,
|
||||||
|
filtersType: filtersType
|
||||||
|
? ListAdvisoriesFiltersTypeEnum[filtersType]
|
||||||
|
: undefined,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const columns: GridColDef[] = [
|
setIsLoading(false);
|
||||||
{
|
|
||||||
field: 'id',
|
if (err || !res) {
|
||||||
headerName: 'Advisory',
|
setIsError(true);
|
||||||
width: 150,
|
setAdvisories(undefined);
|
||||||
sortable: false,
|
return;
|
||||||
renderCell: (params) => (
|
}
|
||||||
<Link
|
|
||||||
className="no-underline text-peridot-primary visited:text-purple-500"
|
setIsError(false);
|
||||||
to={`/${params.value}`}
|
|
||||||
>
|
if (res) {
|
||||||
{params.value}
|
setAdvisories(res.advisories);
|
||||||
</Link>
|
setLastUpdated(res.lastUpdated);
|
||||||
),
|
setTotal(parseInt(res.total || '0'));
|
||||||
},
|
}
|
||||||
{
|
};
|
||||||
field: 'synopsis',
|
|
||||||
headerName: 'Synopsis',
|
const timer = setTimeout(() => fetch(), 500);
|
||||||
width: 450,
|
|
||||||
sortable: false,
|
return () => clearTimeout(timer);
|
||||||
},
|
}, [
|
||||||
{
|
pageSize,
|
||||||
field: 'severity',
|
page,
|
||||||
headerName: 'Severity',
|
filtersKeyword,
|
||||||
width: 150,
|
filterBefore,
|
||||||
sortable: false,
|
filterAfter,
|
||||||
renderCell: (params) => severityToBadge(params.value, 'small'),
|
filtersSeverity,
|
||||||
},
|
filtersType,
|
||||||
{
|
]);
|
||||||
field: 'products',
|
|
||||||
headerName: 'Products',
|
// TODO: Figure out why sticky isn't sticking
|
||||||
width: 450,
|
const stickyProps: TableColumnHeaderProps = {
|
||||||
sortable: false,
|
position: 'sticky',
|
||||||
},
|
top: '0px',
|
||||||
{
|
zIndex: '10',
|
||||||
field: 'publish_date',
|
scope: 'col',
|
||||||
headerName: 'Publish date',
|
};
|
||||||
width: 170,
|
|
||||||
sortable: false,
|
const lastPage = total < pageSize ? 0 : Math.ceil(total / pageSize) - 1;
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
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 && (
|
>
|
||||||
<>
|
<Stack
|
||||||
<Container
|
direction={{
|
||||||
maxWidth={false}
|
sm: 'column',
|
||||||
className="flex items-center space-x-4 bg-white"
|
lg: 'row',
|
||||||
style={{ paddingTop: '0.5rem', paddingBottom: '0.5rem' }}
|
}}
|
||||||
|
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)}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
<HStack>
|
||||||
|
<FormControl width="180px" flexShrink={0} flexGrow={1}>
|
||||||
|
<FormLabel fontSize="sm">Type</FormLabel>
|
||||||
|
<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));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl width="180px" flexShrink={0} flexGrow={1}>
|
||||||
|
<FormLabel fontSize="sm">Before</FormLabel>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
variant="filled"
|
||||||
|
background={inputBackground}
|
||||||
|
borderRadius="0"
|
||||||
|
min={filterAfter?.toLocaleDateString('en-ca')}
|
||||||
|
max={new Date().toLocaleDateString('en-ca')}
|
||||||
|
value={filterBefore?.toLocaleDateString('en-ca') || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newVal = e.currentTarget.value;
|
||||||
|
|
||||||
|
if (!newVal) {
|
||||||
|
setFilterBefore(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);
|
||||||
|
|
||||||
|
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"
|
||||||
>
|
>
|
||||||
<Typography variant="overline">Filters</Typography>
|
<IconButton
|
||||||
<TextField
|
aria-label="First Page"
|
||||||
label="Synopsis"
|
icon={<ArrowLeftIcon fontSize="8px" />}
|
||||||
variant="outlined"
|
disabled={page <= 0}
|
||||||
size="small"
|
onClick={() => setPage(0)}
|
||||||
onChange={(e) => setFilterSynopsis(e.target.value)}
|
|
||||||
/>
|
/>
|
||||||
<TextField
|
<IconButton
|
||||||
label="CVE"
|
aria-label="Previous Page"
|
||||||
variant="outlined"
|
icon={<MinusIcon fontSize="8px" />}
|
||||||
size="small"
|
disabled={page <= 0}
|
||||||
onChange={(e) => setFilterCve(e.target.value)}
|
onClick={() => setPage((old) => old - 1)}
|
||||||
/>
|
/>
|
||||||
</Container>
|
<Text
|
||||||
<Divider />
|
fontSize="xs"
|
||||||
<Container maxWidth={false} disableGutters>
|
// borderTop="1px solid"
|
||||||
<DataGrid
|
// borderBottom="1px solid"
|
||||||
autoHeight
|
borderColor="gray.200"
|
||||||
pagination
|
backgroundColor="white"
|
||||||
disableSelectionOnClick
|
lineHeight="24px"
|
||||||
disableDensitySelector
|
px={2}
|
||||||
disableColumnSelector
|
>
|
||||||
disableColumnMenu
|
{(page + 1).toLocaleString()} / {(lastPage + 1).toLocaleString()}
|
||||||
className="bg-white"
|
</Text>
|
||||||
style={{ borderRadius: 0, border: 0 }}
|
<IconButton
|
||||||
rows={advisories.map((advisory: V1Advisory) => ({
|
aria-label="Next Page"
|
||||||
id: advisory.name,
|
icon={<AddIcon fontSize="8px" />}
|
||||||
synopsis: advisory.synopsis,
|
disabled={page >= lastPage}
|
||||||
severity: advisory.severity,
|
onClick={() => setPage((old) => old + 1)}
|
||||||
products: advisory.affectedProducts?.join(', '),
|
|
||||||
publish_date: Intl.DateTimeFormat('en-US', {
|
|
||||||
day: '2-digit',
|
|
||||||
month: 'short',
|
|
||||||
year: 'numeric',
|
|
||||||
}).format(advisory.publishedAt),
|
|
||||||
}))}
|
|
||||||
rowsPerPageOptions={[10, 25, 50, 100]}
|
|
||||||
rowCount={total}
|
|
||||||
paginationMode="server"
|
|
||||||
columns={columns}
|
|
||||||
density="compact"
|
|
||||||
pageSize={pageSize}
|
|
||||||
onPageChange={(page) => setPage(page)}
|
|
||||||
onPageSizeChange={(newPageSize) => setPageSize(newPageSize)}
|
|
||||||
/>
|
/>
|
||||||
</Container>
|
<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',
|
||||||
|
month: 'short',
|
||||||
|
year: 'numeric',
|
||||||
|
}).format(a.publishedAt)}
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</Tbody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
</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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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"
|
||||||
)}
|
>
|
||||||
|
<Box
|
||||||
|
background={`linear-gradient(to right, ${COLOR_RESF_GREEN}, ${COLOR_RESF_BLUE})`}
|
||||||
|
display="flex"
|
||||||
|
flexDirection="row"
|
||||||
|
alignItems="center"
|
||||||
|
py="1"
|
||||||
|
px={4}
|
||||||
>
|
>
|
||||||
<Toolbar className={classes.toolbar}>
|
<Link to="/" className="no-underline text-white">
|
||||||
{inManage && (
|
<HStack flexGrow={1} height="90%" spacing="2">
|
||||||
<IconButton
|
<RESFLogo className="fill-current text-white" />
|
||||||
edge="start"
|
<Text
|
||||||
color="inherit"
|
borderLeft="1px solid"
|
||||||
aria-label="open drawer"
|
pl="2"
|
||||||
onClick={handleDrawerOpen}
|
lineHeight="30px"
|
||||||
className={classnames(
|
fontSize="xl"
|
||||||
classes.menuButton,
|
fontWeight="300"
|
||||||
open && classes.menuButtonHidden
|
color="white"
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<MenuIcon />
|
Product Errata
|
||||||
</IconButton>
|
</Text>
|
||||||
)}
|
</HStack>
|
||||||
<Link to="/" className="no-underline text-white">
|
</Link>
|
||||||
<div
|
</Box>
|
||||||
className={classnames(
|
<Box as="main" flexGrow={1} overflow="auto">
|
||||||
classes.title,
|
|
||||||
'flex items-center space-x-4'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<RESFLogo className="fill-current text-white" />
|
|
||||||
<div className="font-bold text-lg text-white no-underline">
|
|
||||||
Product Errata{inManage && ' (Admin)'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</Toolbar>
|
|
||||||
</AppBar>
|
|
||||||
{inManage && (
|
|
||||||
<Drawer
|
|
||||||
variant="permanent"
|
|
||||||
classes={{
|
|
||||||
paper: classnames(
|
|
||||||
classes.drawerPaper,
|
|
||||||
!open && classes.drawerPaperClose
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
open={open}
|
|
||||||
>
|
|
||||||
<div className={classes.toolbarIcon}>
|
|
||||||
<IconButton onClick={handleDrawerClose}>
|
|
||||||
<ChevronLeftIcon />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
<Divider />
|
|
||||||
</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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
<Breadcrumb mb={4}>
|
||||||
<div className="flex space-x-4 h-full">
|
<BreadcrumbItem>
|
||||||
{severityToBadge(errata.severity)}
|
<BreadcrumbLink as={RouterLink} to="/">
|
||||||
<Chip
|
Product Errata
|
||||||
color="primary"
|
</BreadcrumbLink>
|
||||||
label={`Issued at ${errata.publishedAt?.toLocaleDateString()}`}
|
</BreadcrumbItem>
|
||||||
/>
|
<BreadcrumbItem>
|
||||||
</div>
|
<BreadcrumbLink isCurrentPage>{id}</BreadcrumbLink>
|
||||||
</div>
|
</BreadcrumbItem>
|
||||||
<Card>
|
</Breadcrumb>
|
||||||
<Paper square>
|
{isLoading ? (
|
||||||
<Tabs
|
<Spinner
|
||||||
value={tabValue}
|
m="auto"
|
||||||
indicatorColor="primary"
|
size="xl"
|
||||||
textColor="primary"
|
alignSelf="center"
|
||||||
onChange={handleTabChange}
|
color={COLOR_RESF_GREEN}
|
||||||
aria-label="disabled tabs example"
|
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>
|
||||||
|
) : (
|
||||||
|
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"
|
||||||
>
|
>
|
||||||
<Tab value={0} label="Erratum" />
|
<TabPanels maxWidth="850px" px="2">
|
||||||
<Tab value={1} label="Affected packages" />
|
<TabPanel>
|
||||||
</Tabs>
|
<Heading as="h2" size="md">
|
||||||
</Paper>
|
Topic
|
||||||
{tabValue === 0 && (
|
</Heading>
|
||||||
<CardContent className="max-w-5xl space-y-4">
|
{errata.topic?.split('\n').map((p, i) => (
|
||||||
<div>
|
<Text key={i} mt={2}>
|
||||||
<Typography variant="h6">Synopsis</Typography>
|
{p}
|
||||||
{errata.synopsis}
|
</Text>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Typography variant="h6">Type</Typography>
|
|
||||||
{typeToText(errata.type)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Typography variant="h6">Severity</Typography>
|
|
||||||
{severityToText(errata.severity)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Typography variant="h6">Topic</Typography>
|
|
||||||
{errata.topic?.split('\n').map((x) => (
|
|
||||||
<p>{x}</p>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Typography variant="h6">Description</Typography>
|
|
||||||
{errata.description?.split('\n').map((x) => (
|
|
||||||
<p>{x}</p>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Typography variant="h6">Affected products</Typography>
|
|
||||||
<ul>
|
|
||||||
{errata.affectedProducts?.map((x) => (
|
|
||||||
<li>{x}</li>
|
|
||||||
))}
|
))}
|
||||||
</ul>
|
<Heading as="h2" size="md" mt={4}>
|
||||||
</div>
|
Description
|
||||||
<div>
|
</Heading>
|
||||||
<Typography variant="h6">Fixes</Typography>
|
{errata.description?.split('\n').map((p, i) => (
|
||||||
<ul>
|
<Text key={i} mt={2}>
|
||||||
{errata.fixes?.map((x) => (
|
{p}
|
||||||
<li>
|
</Text>
|
||||||
<a
|
|
||||||
href={x.sourceLink}
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
{x.sourceBy} - {x.ticket}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
))}
|
))}
|
||||||
</ul>
|
</TabPanel>
|
||||||
</div>
|
<TabPanel>
|
||||||
<div>
|
<VStack alignItems="flex-start" spacing="6">
|
||||||
<Typography variant="h6">CVEs</Typography>
|
{Object.keys(errata.rpms || {}).map((product) => (
|
||||||
<ul>
|
<div key={product}>
|
||||||
{errata.cves?.map((x) => {
|
<Heading as="h2" size="lg" mb={4} fontWeight="300">
|
||||||
let text = `${x.name}${
|
{product}
|
||||||
x.sourceBy !== '' && ` (Source: ${x.sourceBy})`
|
</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>
|
||||||
|
))}
|
||||||
|
</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>
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
</TabPanel>
|
||||||
|
</TabPanels>
|
||||||
|
<VStack
|
||||||
|
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}
|
||||||
|
isExternal
|
||||||
|
color={COLOR_RESF_BLUE}
|
||||||
|
>
|
||||||
|
{x.sourceBy} - {x.ticket}
|
||||||
|
</Link>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</UnorderedList>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text fontWeight="bold">CVEs</Text>
|
||||||
|
<UnorderedList>
|
||||||
|
{!!errata.cves?.length ? (
|
||||||
|
errata.cves?.map((x, idx) => {
|
||||||
|
let text = `${x.name}${
|
||||||
|
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
|
||||||
{text}
|
href={x.sourceLink}
|
||||||
</a>
|
isExternal
|
||||||
)}
|
color={COLOR_RESF_BLUE}
|
||||||
</li>
|
>
|
||||||
);
|
{text}
|
||||||
})}
|
</Link>
|
||||||
{errata.cves?.length === 0 && <li>No CVEs</li>}
|
)}
|
||||||
</ul>
|
</ListItem>
|
||||||
</div>
|
);
|
||||||
<div>
|
})
|
||||||
<Typography variant="h6">References</Typography>
|
) : (
|
||||||
<ul>
|
<ListItem>No CVEs</ListItem>
|
||||||
{errata.references?.map((x) => (
|
)}
|
||||||
<li>{x}</li>
|
</UnorderedList>
|
||||||
))}
|
</Box>
|
||||||
{errata.references?.length === 0 && <li>No references</li>}
|
<Box>
|
||||||
</ul>
|
<Text fontWeight="bold">References</Text>
|
||||||
</div>
|
<UnorderedList>
|
||||||
</CardContent>
|
{!!errata.references?.length ? (
|
||||||
)}
|
errata.references?.map((x, idx) => (
|
||||||
{tabValue === 1 && (
|
<ListItem key={idx}>{x}</ListItem>
|
||||||
<CardContent className="max-w-5xl">
|
))
|
||||||
<div className="space-x-4 divide-y py-2">
|
) : (
|
||||||
{Object.keys(errata.rpms || {}).map(product => (
|
<ListItem>No references</ListItem>
|
||||||
<div className="space-y-4">
|
)}
|
||||||
<Typography variant="h6">{product}</Typography>
|
</UnorderedList>
|
||||||
<div>
|
</Box>
|
||||||
<Typography variant="subtitle1">SRPMs</Typography>
|
</VStack>
|
||||||
<ul>
|
</Box>
|
||||||
{errata.rpms[product].nvras
|
</Tabs>
|
||||||
?.filter((x) => x.indexOf('.src.rpm') !== -1)
|
</>
|
||||||
.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>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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')
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
@ -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,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
11
package.json
11
package.json
@ -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",
|
||||||
|
Loading…
Reference in New Issue
Block a user