Skip to content

Commit

Permalink
[frontend] Add quick export button in Report overview (#2515) (#4831)
Browse files Browse the repository at this point in the history
Co-authored-by: CelineSebe <celine.sebe@filigran.io>
  • Loading branch information
2 people authored and Goumies committed Nov 9, 2023
1 parent e898ec4 commit b0f242d
Show file tree
Hide file tree
Showing 9 changed files with 1,119 additions and 786 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class ReportComponent extends Component {
PopoverComponent={<ReportPopover />}
enableSuggestions={true}
enableQuickSubscription
enableQuickExport
/>
<Grid
container={true}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import useGranted, {
import StixCoreObjectEnrichment from '../stix_core_objects/StixCoreObjectEnrichment';
import StixCoreObjectQuickSubscription from '../stix_core_objects/StixCoreObjectQuickSubscription';
import MarkdownDisplay from '../../../../components/MarkdownDisplay';
import StixCoreObjectFileExport from '../stix_core_objects/StixCoreObjectFileExport';

const useStyles = makeStyles({
title: {
Expand Down Expand Up @@ -473,6 +474,7 @@ const ContainerHeader = (props) => {
enableSuggestions,
onApplied,
enableQuickSubscription,
enableQuickExport,
investigationAddFromContainer,
} = props;
const classes = useStyles();
Expand Down Expand Up @@ -728,12 +730,12 @@ const ContainerHeader = (props) => {
>
{truncate(
container.name
|| container.attribute_abstract
|| container.content
|| container.opinion
|| `${fd(container.first_observed)} - ${fd(
container.last_observed,
)}`,
|| container.attribute_abstract
|| container.content
|| container.opinion
|| `${fd(container.first_observed)} - ${fd(
container.last_observed,
)}`,
80,
)}
</Typography>
Expand Down Expand Up @@ -860,6 +862,9 @@ const ContainerHeader = (props) => {
variant="header"
/>
)}
{enableQuickExport && (
<StixCoreObjectFileExport id={container.id} />
)}
{enableSuggestions && (
<React.Fragment>
<Tooltip title={t('Open the suggestions')}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ FileExportViewerComponentProps
subscription.unsubscribe();
};
}, []);

return (
<Grid item={true} xs={6} style={{ marginTop: 40 }}>
<div style={{ height: '100%' }} className="break">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ Transition.displayName = 'TransitionSlide';

const useStyles = makeStyles<Theme>((theme) => ({
item: {
paddingLeft: 10,
height: 50,
},
itemNested: {
Expand All @@ -58,9 +57,11 @@ const useStyles = makeStyles<Theme>((theme) => ({
},
itemText: {
whiteSpace: 'nowrap',
marginRight: 10,
},
fileName: {
overflow: 'hidden',
textOverflow: 'ellipsis',
marginRight: 10,
},
}));

Expand Down Expand Up @@ -89,6 +90,7 @@ interface FileLineComponentProps {
workNested?: boolean;
isExternalReferenceAttachment?: boolean;
onDelete?: () => void;
onClick?: () => void;
}

const FileLineComponent: FunctionComponent<FileLineComponentProps> = ({
Expand All @@ -102,6 +104,7 @@ const FileLineComponent: FunctionComponent<FileLineComponentProps> = ({
workNested,
isExternalReferenceAttachment,
onDelete,
onClick,
}) => {
const classes = useStyles();
const { t, fld } = useFormatter();
Expand Down Expand Up @@ -228,8 +231,10 @@ const FileLineComponent: FunctionComponent<FileLineComponentProps> = ({
<ListItem
divider={true}
dense={dense}
button={true}
classes={{ root: nested ? classes.itemNested : classes.item }}
rel="noopener noreferrer"
onClick={onClick}
>
<ListItemIcon>
{isProgress && (
Expand All @@ -250,12 +255,16 @@ const FileLineComponent: FunctionComponent<FileLineComponentProps> = ({
</ListItemIcon>
<Tooltip title={!isFail && !isOutdated ? file?.name : ''}>
<ListItemText
primary={<div className={classes.itemText}>{file?.name}</div>}
classes={{
root: classes.itemText,
primary: classes.fileName,
}}
primary={file?.name}
secondary={
<div className={classes.itemText}>
<>
{file?.metaData?.mimetype ?? t('Pending')} (
{fld(file?.lastModified ?? moment())})
</div>
</>
}
/>
</Tooltip>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
import React, { useState } from 'react';
import * as R from 'ramda';
import { map } from 'ramda';
import Tooltip from '@mui/material/Tooltip';
import { FileExportOutline } from 'mdi-material-ui';
import ToggleButton from '@mui/material/ToggleButton';
import { DialogTitle } from '@mui/material';
import Dialog from '@mui/material/Dialog';
import { Field, Form, Formik } from 'formik';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import Button from '@mui/material/Button';
import * as Yup from 'yup';
import MenuItem from '@mui/material/MenuItem';
import { graphql, PreloadedQuery, useMutation, usePreloadedQuery } from 'react-relay';
import { createSearchParams, useNavigate } from 'react-router-dom-v5-compat';
import { FormikHelpers } from 'formik/dist/types';
import { FileManagerExportMutation } from '@components/common/files/__generated__/FileManagerExportMutation.graphql';
import {
StixCoreObjectFileExportQuery,
} from '@components/common/stix_core_objects/__generated__/StixCoreObjectFileExportQuery.graphql';
import {
MarkingDefinitionsLinesSearchQuery$data,
} from '@components/settings/marking_definitions/__generated__/MarkingDefinitionsLinesSearchQuery.graphql';
import { markingDefinitionsLinesSearchQuery } from '../../settings/marking_definitions/MarkingDefinitionsLines';
import { fileManagerExportMutation } from '../files/FileManager';
import useQueryLoading from '../../../../utils/hooks/useQueryLoading';
import Loader, { LoaderVariant } from '../../../../components/Loader';
import { fieldSpacingContainerStyle } from '../../../../utils/field';
import SelectField from '../../../../components/SelectField';
import { useFormatter } from '../../../../components/i18n';
import { MESSAGING$, QueryRenderer } from '../../../../relay/environment';

const stixCoreObjectFileExportQuery = graphql`
query StixCoreObjectFileExportQuery {
connectorsForExport {
id
name
active
connector_scope
updated_at
}
}
`;

const exportValidation = (t: (arg: string) => string) => Yup.object().shape({
format: Yup.string().required(t('This field is required')),
});
interface StixCoreObjectFileExportComponentProps {
queryRef: PreloadedQuery<StixCoreObjectFileExportQuery>
id: string
}

interface FormValues {
format: string;
type: string;
maxMarkingDefinition: string | null;
}

const StixCoreObjectFileExportComponent = ({
queryRef,
id,
}:StixCoreObjectFileExportComponentProps) => {
const navigate = useNavigate();
const { t } = useFormatter();

const data = usePreloadedQuery<StixCoreObjectFileExportQuery>(
stixCoreObjectFileExportQuery,
queryRef,
);
const [commitExport] = useMutation<FileManagerExportMutation>(fileManagerExportMutation);
const [open, setOpen] = useState(false);

const onSubmitExport = (values: FormValues, { setSubmitting, resetForm }: FormikHelpers<FormValues>) => {
const maxMarkingDefinition = values.maxMarkingDefinition === 'none'
? null
: values.maxMarkingDefinition;
commitExport({
variables: {
id,
format: values.format,
exportType: values.type,
maxMarkingDefinition,
},

onCompleted: (exportData) => {
const fileId = exportData.stixCoreObjectEdit?.exportAsk?.[0].id;
setSubmitting(false);
resetForm();
MESSAGING$.notifySuccess('Export successfully started');
navigate({
pathname: `/dashboard/analyses/reports/${id}/content`,
search: fileId ? `?${createSearchParams({ currentFileId: fileId })}` : '',
});
},
});
};

const handleClickOpen = () => {
setOpen(true);
};

const connectorsExport = data.connectorsForExport ?? [];

const exportScopes = R.uniq(connectorsExport.map((c) => c?.connector_scope).flat());

// Handling only pdf for now
const formatValue = 'application/pdf';

const isExportPossible = connectorsExport.some((connector) => {
return connector?.connector_scope?.includes(formatValue) && connector?.active;
});

return (
<div>
<Tooltip
title={
isExportPossible
? t('Generate an export')
: t('No export connector available to generate an export')
}
aria-label="generate-export"
>
<ToggleButton
onClick={() => handleClickOpen()}
disabled={!isExportPossible}
value="quick-export"
aria-haspopup="true"
color="primary"
size="small"
style={{ marginRight: 3 }}
>
<FileExportOutline
fontSize="small"
color={isExportPossible ? 'primary' : 'disabled' }
/>
</ToggleButton>
</Tooltip>
<Formik<FormValues>
enableReinitialize={true}
initialValues={{
format: formatValue,
type: 'full',
maxMarkingDefinition: 'none',
}}
validationSchema={exportValidation(t)}
onSubmit={onSubmitExport}
onReset={() => setOpen(false)}
>
{({ submitForm, handleReset, isSubmitting }) => (
<Form>
<Dialog
PaperProps={{ elevation: 1 }}
open={open}
onClose={() => setOpen(false)}
fullWidth={true}
>
<DialogTitle>{t('Generate an export')}</DialogTitle>
{/* Duplicate code for displaying list of marking in select input. TODO a component */}
<QueryRenderer
query={markingDefinitionsLinesSearchQuery}
variables={{ first: 200 }}
render={({ props }: { props: MarkingDefinitionsLinesSearchQuery$data }) => {
if (props && props.markingDefinitions) {
return (
<DialogContent>
<Field
component={SelectField}
variant="standard"
name="format"
label={t('Export format')}
fullWidth={true}
containerstyle={fieldSpacingContainerStyle}
disabled
>
{exportScopes.map((value, i) => (
<MenuItem
key={i}
value={value ?? ''}
>
{value}
</MenuItem>
))}
</Field>
<Field
component={SelectField}
variant="standard"
name="type"
label={t('Export type')}
fullWidth={true}
containerstyle={fieldSpacingContainerStyle}
>
<MenuItem value="simple">
{t('Simple export (just the entity)')}
</MenuItem>
<MenuItem value="full">
{t('Full export (entity and first neighbours)')}
</MenuItem>
</Field>
<Field
component={SelectField}
variant="standard"
name="maxMarkingDefinition"
label={t('Max marking definition level')}
fullWidth={true}
containerstyle={fieldSpacingContainerStyle}
>
<MenuItem value="none">{t('None')}</MenuItem>
{map(
(markingDefinition) => (
<MenuItem
key={markingDefinition.node.id}
value={markingDefinition.node.id}
>
{markingDefinition.node.definition}
</MenuItem>
),
props.markingDefinitions.edges,
)}
</Field>
</DialogContent>
);
}
return <Loader variant={LoaderVariant.inElement} />;
}}
/>
<DialogActions>
<Button onClick={handleReset} disabled={isSubmitting}>Cancel</Button>
<Button
color="secondary"
onClick={(e) => {
e.preventDefault();
submitForm();
}}
disabled={isSubmitting}
>
{t('Create')}
</Button>
</DialogActions>
</Dialog>
</Form>
)}
</Formik>
</div>
);
};

const StixCoreObjectFileExport = (
{ id }: { id: string },
) => {
const queryRef = useQueryLoading<StixCoreObjectFileExportQuery>(stixCoreObjectFileExportQuery, { id });

return queryRef ? (
<React.Suspense fallback={<Loader variant={LoaderVariant.inElement} />}>
<StixCoreObjectFileExportComponent id={id} queryRef={queryRef} />
</React.Suspense>
) : (
<Loader variant={LoaderVariant.inElement} />
);
};

export default StixCoreObjectFileExport;

0 comments on commit b0f242d

Please sign in to comment.