Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

261 UI: Update All Domains & All Vulnerabilities Tables #291

Merged
merged 17 commits into from
Jun 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/src/api/domains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
import S3Client from '../tasks/s3-client';
import * as Papa from 'papaparse';

const PAGE_SIZE = parseInt(process.env.PAGE_SIZE ?? '') || 25;
const PAGE_SIZE = 15;

class DomainFilters {
@IsString()
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/hooks/useDomainApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ interface ApiResponse {
url?: string;
}

const PAGE_SIZE = 25;
const PAGE_SIZE = 15;

export const useDomainApi = (showAll?: boolean) => {
const { currentOrganization, apiPost, apiGet } = useAuthContext();
Expand Down
212 changes: 166 additions & 46 deletions frontend/src/pages/Domains/Domains.tsx
Original file line number Diff line number Diff line change
@@ -1,69 +1,149 @@
import React, { useCallback, useState, useMemo, useRef } from 'react';
import { TableInstance } from 'react-table';
import React, { useCallback, useState, useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import { Query } from 'types';
import { Table, Paginator, Subnav } from 'components';
import { Subnav } from 'components';
import { Domain } from 'types';
import { createColumns } from './columns';
import { useAuthContext } from 'context';
import classes from './styles.module.scss';
import { useDomainApi } from 'hooks';
import { Box, Stack } from '@mui/system';
import { Alert, Button, IconButton, Paper } from '@mui/material';
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import { DataGrid, GridColDef, GridRenderCellParams } from '@mui/x-data-grid';
import CustomToolbar from 'components/DataGrid/CustomToolbar';
import { differenceInCalendarDays, parseISO } from 'date-fns';

const PAGE_SIZE = 15;

export interface DomainRow {
id: string;
organizationName: string;
name: string;
ip: string;
ports: string[];
service: string[];
vulnerabilities: (string | null)[];
updatedAt: string;
createdAt: string;
}

export const Domains: React.FC = () => {
const { showAllOrganizations } = useAuthContext();
const tableRef = useRef<TableInstance<Domain>>(null);
const columns = useMemo(() => createColumns(), []);
const [domains, setDomains] = useState<Domain[]>([]);
const [totalResults, setTotalResults] = useState(0);

const { listDomains } = useDomainApi(showAllOrganizations);
const history = useHistory();

const fetchDomains = useCallback(
async (q: Query<Domain>) => {
try {
const { domains, count } = await listDomains(q);
if (domains.length === 0) return;
setDomains(domains);
setTotalResults(count);
setPaginationModel((prevState) => ({
...prevState,
page: q.page - 1,
pageSize: q.pageSize ?? PAGE_SIZE,
pageCount: Math.ceil(count / (q.pageSize ?? PAGE_SIZE))
}));
} catch (e) {
console.error(e);
}
},
[listDomains]
);

const fetchDomainsExport = async (): Promise<string> => {
const { sortBy, filters } = tableRef.current?.state ?? {};
try {
const { url } = await listDomains(
{
sort: sortBy ?? [],
page: 1,
pageSize: -1,
filters: filters ?? []
},
true
);
return url!;
} catch (e) {
console.error(e);
return '';
}
};
const resetDomains = useCallback(() => {
fetchDomains({
page: 1,
pageSize: PAGE_SIZE,
sort: [],
filters: []
});
}, [fetchDomains]);

const renderPagination = (table: TableInstance<Domain>) => (
<Paginator
table={table}
totalResults={totalResults}
export={{
name: 'domains',
getDataToExport: fetchDomainsExport
}}
/>
);
//Code for new table//
const [paginationModel, setPaginationModel] = useState({
page: 0,
pageSize: PAGE_SIZE,
pageCount: 0,
sort: [],
filters: []
});

useEffect(() => {
fetchDomains({
page: 1,
pageSize: PAGE_SIZE,
sort: [],
filters: []
});
}, [fetchDomains]);

const domRows: DomainRow[] = domains.map((domain) => ({
id: domain.id,
organizationName: domain.organization.name,
name: domain.name,
ip: domain.ip,
ports: [domain.services.map((service) => service.port).join(', ')],
service: domain.services.map((service) =>
service.products.map((p) => p.name).join(', ')
),
vulnerabilities: domain.vulnerabilities.map((vuln) => vuln.cve),
updatedAt: `${differenceInCalendarDays(
Date.now(),
parseISO(domain.updatedAt)
)} days ago`,
createdAt: `${differenceInCalendarDays(
Date.now(),
parseISO(domain.createdAt)
)} days ago`
}));

const domCols: GridColDef[] = [
{
field: 'organizationName',
headerName: 'Organization',
minWidth: 100,
flex: 1
},
{ field: 'name', headerName: 'Domain', minWidth: 100, flex: 2 },
{ field: 'ip', headerName: 'IP', minWidth: 50, flex: 1 },
{ field: 'ports', headerName: 'Ports', minWidth: 100, flex: 1 },
{ field: 'service', headerName: 'Services', minWidth: 100, flex: 2 },
{
field: 'vulnerabilities',
headerName: 'Vulnerabilities',
minWidth: 100,
flex: 2
},
{ field: 'updatedAt', headerName: 'Updated At', minWidth: 50, flex: 1 },
{ field: 'createdAt', headerName: 'Created At', minWidth: 50, flex: 1 },

{
field: 'view',
headerName: 'Details',
minWidth: 100,
flex: 0.5,
renderCell: (cellValues: GridRenderCellParams) => {
return (
<IconButton
aria-label={`View details for ${cellValues.row.name}`}
tabIndex={cellValues.tabIndex}
color="primary"
onClick={() =>
history.push('/inventory/domain/' + cellValues.row.id)
}
>
<OpenInNewIcon />
</IconButton>
);
}
}
];

return (
<div className={classes.root}>
<div>
<Subnav
items={[
{ title: 'Search Results', path: '/inventory', exact: true },
Expand All @@ -72,15 +152,55 @@ export const Domains: React.FC = () => {
]}
></Subnav>
<br></br>
<Table<Domain>
renderPagination={renderPagination}
tableRef={tableRef}
columns={columns}
data={domains}
pageCount={Math.ceil(totalResults / PAGE_SIZE)}
fetchData={fetchDomains}
pageSize={PAGE_SIZE}
/>
<Box mb={3} mt={3} display="flex" justifyContent="center">
{domains?.length === 0 ? (
<Stack direction="row" spacing={2}>
<Paper elevation={2}>
<Alert severity="warning"> Unable to load domains.</Alert>
</Paper>
<Button
onClick={resetDomains}
variant="contained"
color="primary"
sx={{ width: 'fit-content' }}
>
Retry
</Button>
</Stack>
) : (
<Paper elevation={2} sx={{ width: '90%' }}>
<DataGrid
rows={domRows}
rowCount={totalResults}
columns={domCols}
slots={{ toolbar: CustomToolbar }}
paginationMode="server"
paginationModel={paginationModel}
onPaginationModelChange={(model) => {
fetchDomains({
page: model.page + 1,
pageSize: model.pageSize,
sort: paginationModel.sort,
filters: paginationModel.filters
});
}}
filterMode="server"
onFilterModelChange={(model) => {
fetchDomains({
page: 1,
pageSize: paginationModel.pageSize,
sort: paginationModel.sort,
filters: model.items.map((item) => ({
id: item.field,
value: item.value
}))
});
}}
pageSizeOptions={[15, 30, 50, 100]}
/>
</Paper>
)}
</Box>
</div>
);
};
Loading
Loading