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

[frontend] Add quick export button in Report overview (#2515) #4831

Merged
merged 32 commits into from
Nov 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
6d2be69
[frontend] Add button to quickly export except for Task, Notes and Ex…
CelineSebe Oct 6, 2023
7e44957
[frontend] Add StixCoreObjectFileExportQuery & button export except f…
CelineSebe Oct 16, 2023
441f977
[frontend] recovery of data from stixCoreObject
CelineSebe Oct 17, 2023
1a9ed78
[frontend] Generate export pdf from overview
CelineSebe Oct 18, 2023
ca389ca
[frontend] Add defaultValues
CelineSebe Oct 20, 2023
b205bcd
[frontend] Generate an export except in knowledge tab
CelineSebe Oct 20, 2023
dfd4b42
[frontend] Display quickly button export except in knowledges
CelineSebe Oct 20, 2023
8616e98
[frontend] QuickExport ok
CelineSebe Oct 23, 2023
0b5d0fe
[frontend] QuickExport ok
CelineSebe Oct 23, 2023
efdf9a7
[frontend] Get exported PDF files and creation currentExportUrl in Co…
CelineSebe Oct 24, 2023
d0a0dc5
[frontend] Display list of exported PDF files in Content
CelineSebe Oct 25, 2023
eb5de78
[frontend] Fix delete and download exported PDF files in content entity
CelineSebe Oct 25, 2023
953aa17
[frontend] Bug exportFiles
CelineSebe Oct 27, 2023
1d385e4
[frontend] display selected exported file
lndrtrbn Oct 27, 2023
eec444f
[frontend] some comments for help displaying correct document in viewer
lndrtrbn Oct 27, 2023
6c74358
[frontend] Reverse exportFiles
CelineSebe Oct 30, 2023
63afb1a
[frontend] Get filename with urlParams
CelineSebe Oct 30, 2023
a3a565a
Fix spinner and pdf display
CelineSebe Oct 31, 2023
3e21bc8
Fix spinner display by LandryTrebon
CelineSebe Oct 31, 2023
4a07efa
[frontend] Automatic display of the last exported file
CelineSebe Oct 31, 2023
a8c3d38
[frontend] Clean enableQuickExport and enhance style
CelineSebe Oct 31, 2023
a577cd3
clean code
lndrtrbn Oct 31, 2023
bf8c476
clean code
lndrtrbn Oct 31, 2023
84ec753
clean code
lndrtrbn Oct 31, 2023
36d5d2e
clean code
lndrtrbn Nov 2, 2023
6bcd83f
some styles
lndrtrbn Nov 2, 2023
66fc1c5
[frontend] Fix bugs & converting to TS
CelineSebe Nov 3, 2023
0cf6b10
[frontend] Fix deepscan
CelineSebe Nov 3, 2023
ac30f92
[frontend] Export format disabled
CelineSebe Nov 6, 2023
b4333cc
[frontend] clean code after rebase
CelineSebe Nov 8, 2023
eb62f35
reformat
lndrtrbn Nov 8, 2023
82f138a
[frontend] stop refetch when file is ready
CelineSebe Nov 8, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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();
};
}, []);

CelineSebe marked this conversation as resolved.
Show resolved Hide resolved
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;