From d7841a1a94e0b9a5da48ef4fd2bca532c80e97c9 Mon Sep 17 00:00:00 2001 From: Landry Trebon <33682259+lndrtrbn@users.noreply.github.com> Date: Wed, 8 Nov 2023 11:39:55 +0100 Subject: [PATCH] [frontend] Add quick export button in Report overview (#2515) (#4831) Co-authored-by: CelineSebe --- .../components/analyses/reports/Report.jsx | 1 + .../common/containers/ContainerHeader.jsx | 17 +- .../common/files/FileExportViewer.tsx | 1 + .../components/common/files/FileLine.tsx | 19 +- .../StixCoreObjectFileExport.tsx | 262 +++++ .../StixDomainObjectContent.jsx | 390 +++++--- .../StixDomainObjectContentFiles.jsx | 264 ++--- .../components/data/tasks/TasksList.jsx | 943 ++++++++---------- .../opencti-front/src/utils/Localization.js | 8 +- 9 files changed, 1119 insertions(+), 786 deletions(-) create mode 100644 opencti-platform/opencti-front/src/private/components/common/stix_core_objects/StixCoreObjectFileExport.tsx diff --git a/opencti-platform/opencti-front/src/private/components/analyses/reports/Report.jsx b/opencti-platform/opencti-front/src/private/components/analyses/reports/Report.jsx index 9fe8537db9f0..c3c41cd752d5 100644 --- a/opencti-platform/opencti-front/src/private/components/analyses/reports/Report.jsx +++ b/opencti-platform/opencti-front/src/private/components/analyses/reports/Report.jsx @@ -32,6 +32,7 @@ class ReportComponent extends Component { PopoverComponent={} enableSuggestions={true} enableQuickSubscription + enableQuickExport /> { enableSuggestions, onApplied, enableQuickSubscription, + enableQuickExport, investigationAddFromContainer, } = props; const classes = useStyles(); @@ -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, )} @@ -860,6 +862,9 @@ const ContainerHeader = (props) => { variant="header" /> )} + {enableQuickExport && ( + + )} {enableSuggestions && ( diff --git a/opencti-platform/opencti-front/src/private/components/common/files/FileExportViewer.tsx b/opencti-platform/opencti-front/src/private/components/common/files/FileExportViewer.tsx index 2f0e2b009089..f2b623c774b2 100644 --- a/opencti-platform/opencti-front/src/private/components/common/files/FileExportViewer.tsx +++ b/opencti-platform/opencti-front/src/private/components/common/files/FileExportViewer.tsx @@ -48,6 +48,7 @@ FileExportViewerComponentProps subscription.unsubscribe(); }; }, []); + return (
diff --git a/opencti-platform/opencti-front/src/private/components/common/files/FileLine.tsx b/opencti-platform/opencti-front/src/private/components/common/files/FileLine.tsx index 080399c7328c..a00c19704c03 100644 --- a/opencti-platform/opencti-front/src/private/components/common/files/FileLine.tsx +++ b/opencti-platform/opencti-front/src/private/components/common/files/FileLine.tsx @@ -49,7 +49,6 @@ Transition.displayName = 'TransitionSlide'; const useStyles = makeStyles((theme) => ({ item: { - paddingLeft: 10, height: 50, }, itemNested: { @@ -58,9 +57,11 @@ const useStyles = makeStyles((theme) => ({ }, itemText: { whiteSpace: 'nowrap', + marginRight: 10, + }, + fileName: { overflow: 'hidden', textOverflow: 'ellipsis', - marginRight: 10, }, })); @@ -89,6 +90,7 @@ interface FileLineComponentProps { workNested?: boolean; isExternalReferenceAttachment?: boolean; onDelete?: () => void; + onClick?: () => void; } const FileLineComponent: FunctionComponent = ({ @@ -102,6 +104,7 @@ const FileLineComponent: FunctionComponent = ({ workNested, isExternalReferenceAttachment, onDelete, + onClick, }) => { const classes = useStyles(); const { t, fld } = useFormatter(); @@ -228,8 +231,10 @@ const FileLineComponent: FunctionComponent = ({ {isProgress && ( @@ -250,12 +255,16 @@ const FileLineComponent: FunctionComponent = ({ {file?.name}
} + classes={{ + root: classes.itemText, + primary: classes.fileName, + }} + primary={file?.name} secondary={ -
+ <> {file?.metaData?.mimetype ?? t('Pending')} ( {fld(file?.lastModified ?? moment())}) -
+ } />
diff --git a/opencti-platform/opencti-front/src/private/components/common/stix_core_objects/StixCoreObjectFileExport.tsx b/opencti-platform/opencti-front/src/private/components/common/stix_core_objects/StixCoreObjectFileExport.tsx new file mode 100644 index 000000000000..1eb37d6574b2 --- /dev/null +++ b/opencti-platform/opencti-front/src/private/components/common/stix_core_objects/StixCoreObjectFileExport.tsx @@ -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 + 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, + queryRef, + ); + const [commitExport] = useMutation(fileManagerExportMutation); + const [open, setOpen] = useState(false); + + const onSubmitExport = (values: FormValues, { setSubmitting, resetForm }: FormikHelpers) => { + 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 ( +
+ + handleClickOpen()} + disabled={!isExportPossible} + value="quick-export" + aria-haspopup="true" + color="primary" + size="small" + style={{ marginRight: 3 }} + > + + + + + enableReinitialize={true} + initialValues={{ + format: formatValue, + type: 'full', + maxMarkingDefinition: 'none', + }} + validationSchema={exportValidation(t)} + onSubmit={onSubmitExport} + onReset={() => setOpen(false)} + > + {({ submitForm, handleReset, isSubmitting }) => ( +
+ setOpen(false)} + fullWidth={true} + > + {t('Generate an export')} + {/* Duplicate code for displaying list of marking in select input. TODO a component */} + { + if (props && props.markingDefinitions) { + return ( + + + {exportScopes.map((value, i) => ( + + {value} + + ))} + + + + {t('Simple export (just the entity)')} + + + {t('Full export (entity and first neighbours)')} + + + + {t('None')} + {map( + (markingDefinition) => ( + + {markingDefinition.node.definition} + + ), + props.markingDefinitions.edges, + )} + + + ); + } + return ; + }} + /> + + + + + +
+ )} + +
+ ); +}; + +const StixCoreObjectFileExport = ( + { id }: { id: string }, +) => { + const queryRef = useQueryLoading(stixCoreObjectFileExportQuery, { id }); + + return queryRef ? ( + }> + + + ) : ( + + ); +}; + +export default StixCoreObjectFileExport; diff --git a/opencti-platform/opencti-front/src/private/components/common/stix_domain_objects/StixDomainObjectContent.jsx b/opencti-platform/opencti-front/src/private/components/common/stix_domain_objects/StixDomainObjectContent.jsx index 9c406d04feb0..1b69ee0ef8e8 100644 --- a/opencti-platform/opencti-front/src/private/components/common/stix_domain_objects/StixDomainObjectContent.jsx +++ b/opencti-platform/opencti-front/src/private/components/common/stix_domain_objects/StixDomainObjectContent.jsx @@ -16,6 +16,7 @@ import { pdfjs, Document, Page } from 'react-pdf'; import 'react-pdf/dist/esm/Page/TextLayer.css'; import 'react-pdf/dist/esm/Page/AnnotationLayer.css'; import ReactMde from 'react-mde'; +import { interval } from 'rxjs'; import inject18n from '../../../../components/i18n'; import StixDomainObjectContentFiles, { stixDomainObjectContentFilesUploadStixDomainObjectMutation, @@ -33,6 +34,7 @@ import Loader from '../../../../components/Loader'; import StixDomainObjectContentBar from './StixDomainObjectContentBar'; import { isEmptyField } from '../../../../utils/utils'; import MarkdownDisplay from '../../../../components/MarkdownDisplay'; +import { FIVE_SECONDS } from '../../../../utils/Time'; pdfjs.GlobalWorkerOptions.workerSrc = `${APP_BASE_PATH}/static/ext/pdf.worker.js`; @@ -106,6 +108,8 @@ const styles = () => ({ }, }); +const interval$ = interval(FIVE_SECONDS); + const stixDomainObjectContentUploadExternalReferenceMutation = graphql` mutation StixDomainObjectContentUploadExternalReferenceMutation( $id: ID! @@ -138,7 +142,7 @@ const stixDomainObjectContentUploadExternalReferenceMutation = graphql` } `; -const sortByLastModified = R.sortBy(R.prop('name')); +const sortByLastModified = R.sortBy(R.prop('lastModified')); const getFiles = (stixDomainObject) => { const importFiles = stixDomainObject.importFiles?.edges?.filter((n) => !!n?.node) @@ -153,17 +157,44 @@ const getFiles = (stixDomainObject) => { return result; }; +const getExportFiles = (stixDomainObject) => { + const exportFiles = stixDomainObject.exportFiles?.edges?.filter((n) => !!n?.node) + .map((n) => n.node) ?? []; + return sortByLastModified([...exportFiles].filter((n) => { + return (['application/pdf'].includes(n.metaData.mimetype) || n.uploadStatus === 'progress'); + })); +}; + class StixDomainObjectContentComponent extends Component { constructor(props) { super(props); + const params = buildViewParamsFromUrlAndStorage( props.history, props.location, `view-stix-domain-object-content-${props.stixDomainObject.id}`, ); + const files = getFiles(props.stixDomainObject); + const exportFiles = getExportFiles(props.stixDomainObject); + + let currentFileId = R.head(files)?.id; + let isLoading = false; + let onProgressExportFileName; + if (params.currentFileId) { + const onProgressExportFile = exportFiles.find((file) => ( + (file.id === params.currentFileId) + && (file.uploadStatus === 'progress') + )); + if (onProgressExportFile) { + isLoading = true; + onProgressExportFileName = onProgressExportFile.name; + } + currentFileId = params.currentFileId; + } + this.state = { - currentFileId: R.propOr(R.head(files)?.id, 'currentFileId', params), + currentFileId, totalPdfPageNumber: null, currentPdfPageNumber: 1, pdfViewerZoom: 1.2, @@ -172,6 +203,8 @@ class StixDomainObjectContentComponent extends Component { currentContent: props.t('Write something awesome...'), navOpen: localStorage.getItem('navOpen') === 'true', changed: false, + isLoading, + onProgressExportFileName, }; } @@ -186,22 +219,23 @@ class StixDomainObjectContentComponent extends Component { loadFileContent() { const { stixDomainObject } = this.props; - const files = getFiles(stixDomainObject); + const files = [...getFiles(stixDomainObject), ...getExportFiles(stixDomainObject)]; this.setState({ isLoading: true }, () => { const { currentFileId } = this.state; if (!currentFileId) { return this.setState({ isLoading: false }); } - const currentFile = R.head( - R.filter((n) => n.id === currentFileId, files), - ); + const currentFile = files.find((f) => f.id === currentFileId); const currentFileType = currentFile && currentFile.metaData.mimetype; + if (currentFileType === 'application/pdf') { return this.setState({ isLoading: false }); } + const url = `${APP_BASE_PATH}/storage/view/${encodeURIComponent( currentFileId, )}`; + return Axios.get(url).then((res) => { const content = res.data; return this.setState({ @@ -217,11 +251,40 @@ class StixDomainObjectContentComponent extends Component { this.subscriptionToggle = MESSAGING$.toggleNav.subscribe({ next: () => this.setState({ navOpen: localStorage.getItem('navOpen') === 'true' }), }); - this.loadFileContent(); + this.subscription = interval$.subscribe(() => { + this.props.relay.refetch(); + }); + + const { stixDomainObject } = this.props; + const { currentFileId } = this.state; + const files = [...getFiles(stixDomainObject), ...getExportFiles(stixDomainObject)]; + const currentFile = files.find((f) => f.id === currentFileId); + + if (currentFile?.uploadStatus !== 'progress') { + this.loadFileContent(); + } + } + + componentDidUpdate() { + const { onProgressExportFileName } = this.state; + const { stixDomainObject } = this.props; + const exportFiles = getExportFiles(stixDomainObject); + + if (onProgressExportFileName) { + const exportFile = exportFiles.find((file) => file.name === onProgressExportFileName); + if (exportFile?.uploadStatus === 'complete') { + this.handleSelectFile(exportFile.id); + this.setState({ + onProgressExportFileName: undefined, + }); + this.subscription.unsubscribe(); + } + } } componentWillUnmount() { this.subscriptionToggle.unsubscribe(); + this.subscription.unsubscribe(); } handleSelectFile(fileId) { @@ -255,6 +318,7 @@ class StixDomainObjectContentComponent extends Component { handleZoomOut() { this.setState({ pdfViewerZoom: this.state.pdfViewerZoom - 0.2 }, () => this.saveView()); } + // END OF PDF SECTION prepareSaveFile() { @@ -384,6 +448,7 @@ class StixDomainObjectContentComponent extends Component { render() { const { classes, stixDomainObject, t } = this.props; + const { currentFileId, totalPdfPageNumber, @@ -393,173 +458,178 @@ class StixDomainObjectContentComponent extends Component { navOpen, changed, } = this.state; + const files = getFiles(stixDomainObject); + const exportFiles = getExportFiles(stixDomainObject); const currentUrl = currentFileId && `${APP_BASE_PATH}/storage/view/${encodeURIComponent(currentFileId)}`; const currentGetUrl = currentFileId && `${APP_BASE_PATH}/storage/get/${encodeURIComponent(currentFileId)}`; - const currentFile = currentFileId && R.head(R.filter((n) => n.id === currentFileId, files)); + + const currentFile = currentFileId && [...files, ...exportFiles].find((n) => n.id === currentFileId); const currentFileType = currentFile && currentFile.metaData.mimetype; + const { innerHeight } = window; const height = innerHeight - 190; + return (
- {currentFileType === 'text/plain' && ( -
- -
- {isLoading ? ( - - ) : ( - + ) : ( + <> + {currentFileType === 'text/plain' && ( +
+ - )} -
-
- )} - {currentFileType === 'text/html' && ( -
- -
- { - this.onHtmlFieldChange(editor.getData()); - }} - /> -
-
- )} - {currentFileType === 'text/markdown' && ( -
- -
- {isLoading ? ( - - ) : ( - Promise.resolve( +
+ +
+
+ )} + {currentFileType === 'text/html' && ( +
+ +
+ { + this.onHtmlFieldChange(editor.getData()); + }} + /> +
+
+ )} + {currentFileType === 'text/markdown' && ( +
+ +
+ Promise.resolve( , - ) - } - l18n={{ - write: t('Write'), - preview: t('Preview'), - uploadingImage: t('Uploading image'), - pasteDropSelect: t('Paste'), - }} + ) + } + l18n={{ + write: t('Write'), + preview: t('Preview'), + uploadingImage: t('Uploading image'), + pasteDropSelect: t('Paste'), + }} + /> +
+
+ )} + + {currentFileType === 'application/pdf' && ( +
+ - )} -
-
- )} - {currentFileType === 'application/pdf' && ( -
- -
- } - file={currentUrl} +
+ } + file={currentUrl} + > + {Array.from(new Array(totalPdfPageNumber), (el, index) => ( + + ))} + +
+
+ )} + {!currentFile && ( +
- {Array.from(new Array(totalPdfPageNumber), (el, index) => ( - - ))} - -
-
- )} - {!currentFile && ( -
-
+
{t('No file selected.')} -
-
+
+
+ )} + )} +
); } @@ -624,6 +697,21 @@ const StixDomainObjectContent = createRefetchContainer( } } } + exportFiles(first: 1000) @connection(key: "Pagination_exportFiles") { + edges { + node { + id + name + uploadStatus + lastModified + lastModifiedSinceMin + metaData { + mimetype + } + ...FileLine_file + } + } + } externalReferences { edges { node { diff --git a/opencti-platform/opencti-front/src/private/components/common/stix_domain_objects/StixDomainObjectContentFiles.jsx b/opencti-platform/opencti-front/src/private/components/common/stix_domain_objects/StixDomainObjectContentFiles.jsx index 3139a980b7b8..cb1c0739cfe4 100644 --- a/opencti-platform/opencti-front/src/private/components/common/stix_domain_objects/StixDomainObjectContentFiles.jsx +++ b/opencti-platform/opencti-front/src/private/components/common/stix_domain_objects/StixDomainObjectContentFiles.jsx @@ -29,10 +29,11 @@ import { graphql } from 'react-relay'; import Divider from '@mui/material/Divider'; import inject18n from '../../../../components/i18n'; import FileUploader from '../files/FileUploader'; -import { FileLineDeleteMutation } from '../files/FileLine'; +import FileLine, { FileLineDeleteMutation } from '../files/FileLine'; import { commitMutation } from '../../../../relay/environment'; import TextField from '../../../../components/TextField'; -import withHooksSettingsMessagesBannerHeight from '../../settings/settings_messages/withHooksSettingsMessagesBannerHeight'; +import withHooksSettingsMessagesBannerHeight + from '../../settings/settings_messages/withHooksSettingsMessagesBannerHeight'; const styles = (theme) => ({ drawerPaper: { @@ -59,6 +60,11 @@ const styles = (theme) => ({ padding: '0 15px 0 15px', }, toolbar: theme.mixins.toolbar, + subHeader: { + display: 'flex', + alignItems: 'center', + gap: 5, + }, }); export const stixDomainObjectContentFilesUploadStixDomainObjectMutation = graphql` @@ -144,22 +150,6 @@ class StixDomainObjectContentFiles extends Component { this.handleCloseCreate(); } - prepareSaveFile() { - const fragment = this.state.currentFile.id.split('/'); - const currentName = R.last(fragment); - const currentId = fragment[fragment.length - 2]; - const currentType = fragment[fragment.length - 3]; - const isExternalReference = currentType === 'External-Reference'; - const content = this.state.currentContent; - const blob = new Blob([content], { - type: this.state.currentFile.metaData.mimetype, - }); - const file = new File([blob], currentName, { - type: this.state.currentFile.metaData.mimetype, - }); - return { currentId, isExternalReference, file }; - } - onSubmit(values, { setSubmitting, resetForm }) { const { t, stixDomainObjectId } = this.props; let { name } = values; @@ -206,6 +196,8 @@ class StixDomainObjectContentFiles extends Component { currentFileId, onFileChange, settingsMessagesBannerHeight, + exportFiles, + handleSelectExportFile, } = this.props; const { deleting, displayCreate } = this.state; const textFiles = files.filter((n) => n.metaData.mimetype === 'text/plain'); @@ -216,6 +208,7 @@ class StixDomainObjectContentFiles extends Component { const pdfFiles = files.filter( (n) => n.metaData.mimetype === 'application/pdf', ); + return ( -
{t('PDF files')}
+
{t('Uploaded PDF files')}
{pdfFiles.length > 0 ? pdfFiles.map((file) => ( - - - - } - > - - - - - - )) + + + + } + > + +
+ +
+
+ +
+ )).reverse() : this.renderNoFiles()} + + +
{t('Exported PDF files ')}
+ + } + > + {exportFiles.map((file) => { + return ( + file && ( + + ) + ); + }).reverse()} + + {exportFiles.length === 0 && this.renderNoFiles()} +
+ {textFiles.length > 0 ? textFiles.map((file) => ( - - - - } - > - - - - - + + + + } + > + + + + + )) : this.renderNoFiles()} @@ -360,29 +394,32 @@ class StixDomainObjectContentFiles extends Component { > {htmlFiles.length > 0 ? htmlFiles.map((file) => ( - - - - } - > - - - - - + + + + } + > + + + + + )) : this.renderNoFiles()} @@ -417,29 +454,32 @@ class StixDomainObjectContentFiles extends Component { > {markdownFiles.length > 0 ? markdownFiles.map((file) => ( - - - - } - > - - - - - + + + + } + > + + + + + )) : this.renderNoFiles()} @@ -497,6 +537,8 @@ StixDomainObjectContentFiles.propTypes = { currentFileId: PropTypes.string, handleSelectFile: PropTypes.func, onFileChange: PropTypes.func, + exportFiles: PropTypes.array, + handleSelectExportFile: PropTypes.func, }; export default R.compose( diff --git a/opencti-platform/opencti-front/src/private/components/data/tasks/TasksList.jsx b/opencti-platform/opencti-front/src/private/components/data/tasks/TasksList.jsx index a4f4fe1266f8..be29c764a634 100644 --- a/opencti-platform/opencti-front/src/private/components/data/tasks/TasksList.jsx +++ b/opencti-platform/opencti-front/src/private/components/data/tasks/TasksList.jsx @@ -1,8 +1,6 @@ -import React, { Component } from 'react'; -import * as PropTypes from 'prop-types'; +import React, { useState } from 'react'; import * as R from 'ramda'; -import { createRefetchContainer, graphql } from 'react-relay'; -import withStyles from '@mui/styles/withStyles'; +import { graphql, useFragment } from 'react-relay'; import Grid from '@mui/material/Grid'; import Typography from '@mui/material/Typography'; import LinearProgress from '@mui/material/LinearProgress'; @@ -19,40 +17,18 @@ import TableContainer from '@mui/material/TableContainer'; import TableHead from '@mui/material/TableHead'; import TableRow from '@mui/material/TableRow'; import Slide from '@mui/material/Slide'; -import { interval } from 'rxjs'; import { Delete } from 'mdi-material-ui'; import Chip from '@mui/material/Chip'; +import makeStyles from '@mui/styles/makeStyles'; import TaskStatus from '../../../../components/TaskStatus'; -import inject18n from '../../../../components/i18n'; -import { FIVE_SECONDS } from '../../../../utils/Time'; +import { useFormatter } from '../../../../components/i18n'; import { truncate } from '../../../../utils/String'; import { commitMutation, MESSAGING$ } from '../../../../relay/environment'; import Security from '../../../../utils/Security'; import { KNOWLEDGE_KNUPDATE_KNDELETE } from '../../../../utils/hooks/useGranted'; import TaskScope from '../../../../components/TaskScope'; -const interval$ = interval(FIVE_SECONDS); - -const styles = (theme) => ({ - container: { - margin: 0, - }, - editButton: { - position: 'fixed', - bottom: 30, - right: 230, - }, - gridContainer: { - marginBottom: 20, - }, - title: { - float: 'left', - textTransform: 'uppercase', - }, - popover: { - float: 'right', - marginTop: '-13px', - }, +const useStyles = makeStyles((theme) => ({ paper: { height: '100%', minHeight: '100%', @@ -61,29 +37,10 @@ const styles = (theme) => ({ borderRadius: 6, position: 'relative', }, - card: { - width: '100%', - marginBottom: 20, - borderRadius: 6, - position: 'relative', - }, - chip: { - height: 30, - float: 'left', - margin: '0 10px 10px 0', - backgroundColor: '#607d8b', - }, - number: { - fontWeight: 600, - fontSize: 18, - }, progress: { borderRadius: 5, height: 10, }, - chipValue: { - margin: 0, - }, filter: { margin: '5px 10px 5px 0', }, @@ -92,7 +49,7 @@ const styles = (theme) => ({ backgroundColor: theme.palette.background.accent, margin: '5px 10px 5px 0', }, -}); +})); const Transition = React.forwardRef((props, ref) => ( @@ -105,45 +62,104 @@ export const tasksListTaskDeletionMutation = graphql` } `; -class TasksListComponent extends Component { - constructor(props) { - super(props); - this.state = { - displayMessages: false, - displayErrors: false, - messages: [], - errors: [], - }; - } - - componentDidMount() { - this.subscription = interval$.subscribe(() => { - this.props.relay.refetch(this.props.options); - }); - } - - componentWillUnmount() { - this.subscription.unsubscribe(); - } - - handleOpenMessages(messages) { - this.setState({ displayMessages: true, messages }); +export const tasksListQuery = graphql` + query TasksListQuery( + $count: Int + $orderBy: BackgroundTasksOrdering + $orderMode: OrderingMode + $includeAuthorities: Boolean + $filters: [BackgroundTasksFiltering] + ) { + ...TasksList_data + @arguments( + count: $count + orderBy: $orderBy + orderMode: $orderMode + includeAuthorities: $includeAuthorities + filters: $filters + ) } +`; - handleCloseMessages() { - this.setState({ displayMessages: false, messages: [] }); +const TasksListFragment = graphql` + fragment TasksList_data on Query + @argumentDefinitions( + count: { type: "Int" } + orderBy: { type: "BackgroundTasksOrdering", defaultValue: created_at } + orderMode: { type: "OrderingMode", defaultValue: desc } + includeAuthorities: { type: "Boolean", defaultValue: true } + filters: { type: "[BackgroundTasksFiltering]" } + ) { + backgroundTasks( + first: $count + orderBy: $orderBy + orderMode: $orderMode + includeAuthorities: $includeAuthorities + filters: $filters + ) { + edges { + node { + id + type + initiator { + name + } + actions { + type + context { + field + type + values + } + } + created_at + last_execution_date + completed + task_expected_number + task_processed_number + errors { + id + timestamp + message + } + ... on ListTask { + task_ids + scope + } + ... on QueryTask { + task_filters + task_search + scope + } + } + } + } } +`; +const TasksList = ({ data }) => { + const classes = useStyles(); + const { t, nsdt, n } = useFormatter(); + const [displayMessages, setDisplayMessages] = useState(false); + const [displayErrors, setDisplayErrors] = useState(false); + const [messages, setMessages] = useState([]); + const [errors, setErrors] = useState([]); + const { backgroundTasks } = useFragment(TasksListFragment, data); + const handleCloseMessages = () => { + setDisplayMessages(false); + setMessages([]); + }; - handleOpenErrors(errors) { - this.setState({ displayErrors: true, errors }); - } + const handleOpenErrors = (err) => { + setDisplayErrors(true); + setErrors(err); + }; - handleCloseErrors() { - this.setState({ displayErrors: false, errors: [] }); - } + const handleCloseErrors = () => { + setDisplayErrors(false); + setErrors([]); + }; - // eslint-disable-next-line class-methods-use-this - handleDeleteTask(taskId) { + const handleDeleteTask = (taskId) => { commitMutation({ mutation: tasksListTaskDeletionMutation, variables: { @@ -153,466 +169,375 @@ class TasksListComponent extends Component { MESSAGING$.notifySuccess('The task has been deleted'); }, }); - } + }; - render() { - const { classes, data, t, nsdt, n } = this.props; - const tasks = data?.backgroundTasks?.edges ?? []; - return ( -
- {tasks.length === 0 && ( + const tasks = backgroundTasks?.edges ?? []; + return ( +
+ {tasks.length === 0 && ( + +
+ + {t('No task')} + +
+
+ )} + {tasks.map((taskEdge) => { + const task = taskEdge.node; + let status = ''; + if (task.completed) { + status = 'complete'; + } else if (task.task_processed_number > 0) { + status = 'progress'; + } else { + status = 'wait'; + } + let filters = null; + let listIds = ''; + if (task.task_filters) { + filters = JSON.parse(task.task_filters); + } else if (task.task_ids) { + listIds = truncate(R.join(', ', task.task_ids), 60); + } + return ( -
- - {t('No task')} - -
-
- )} - {tasks.map((taskEdge) => { - const task = taskEdge.node; - let status = ''; - if (task.completed) { - status = 'complete'; - } else if (task.task_processed_number > 0) { - status = 'progress'; - } else { - status = 'wait'; - } - let filters = null; - let listIds = ''; - if (task.task_filters) { - filters = JSON.parse(task.task_filters); - } else if (task.task_ids) { - listIds = truncate(R.join(', ', task.task_ids), 60); - } - return ( - - - - - - - {t('Targeted entities')} ({n(task.task_expected_number)} - ) - - {task.task_search && ( - - - {t('Search')}:{' '} - {task.task_search} -
- } - /> - - - )} - {task.type !== 'RULE' - && (filters ? ( - R.map((currentFilter) => { - const label = `${truncate( - t(`filter_${currentFilter[0]}`), - 20, - )}`; - const localFilterMode = currentFilter[0].endsWith( - 'not_eq', - ) - ? t('AND') - : t('OR'); - const values = ( - - {R.map( - (o) => ( - - {o.value && o.value.length > 0 - ? truncate(o.value, 15) - : t('No label')}{' '} - {R.last(currentFilter[1]).value - !== o.value && ( - {localFilterMode} - )}{' '} - - ), - currentFilter[1], - )} - - ); - return ( - - - {label}: {values} -
- } - /> - {R.last(R.toPairs(filters))[0] - !== currentFilter[0] && ( - - )} - - ); - }, R.toPairs(filters)) - ) : ( - - {t('List of entities')}:{' '} - {listIds} -
- } - /> - ))} - {task.type === 'RULE' && ( + + + + + + {t('Targeted entities')} ({n(task.task_expected_number)} + ) + + {task.task_search && ( + {t('All rule targets')}} + label={ +
+ {t('Search')}:{' '} + {task.task_search} +
+ } /> - )} -
- - - {t('Actions')} - - {task.type === 'RULE' && ( {t('APPLY RULE')}} + label={t('AND')} /> - )} - {task.actions - && R.map( - (action) => ( -
+ + )} + {task.type !== 'RULE' + && (filters ? ( + R.map((currentFilter) => { + const label = `${truncate( + t(`filter_${currentFilter[0]}`), + 20, + )}`; + const localFilterMode = currentFilter[0].endsWith( + 'not_eq', + ) + ? t('AND') + : t('OR'); + const values = ( + + {R.map( + (o) => ( + + {o.value && o.value.length > 0 + ? truncate(o.value, 15) + : t('No label')}{' '} + {R.last(currentFilter[1]).value + !== o.value && ( + {localFilterMode} + )}{' '} + + ), + currentFilter[1], + )} + + ); + return ( + + {label}: {values} +
+ } /> - {action.context && ( - - {action.context.field && ( - - - {action.context.field} - - :{' '} - - )} - {truncate( - R.join( - ', ', - action.context.values || [], - ), - 80, - )} - - } - /> + {R.last(R.toPairs(filters))[0] + !== currentFilter[0] && ( + )} + + ); + }, R.toPairs(filters)) + ) : ( + + {t('List of entities')}:{' '} + {listIds} - ), - task.actions, - )} -
+ } + /> + ))} + {task.type === 'RULE' && ( + {t('All rule targets')}} + /> + )} +
+ + + {t('Actions')} + + {task.type === 'RULE' && ( + {t('APPLY RULE')}} + /> + )} + {task.actions + && R.map( + (action) => ( +
+ + {action.context && ( + + {action.context.field && ( + + + {action.context.field} + + :{' '} + + )} + {truncate( + R.join( + ', ', + action.context.values || [], + ), + 80, + )} +
+ } + /> + )} + + ), + task.actions, + )}
- - - - - {t('Initiator')} - - {task.initiator?.name} - - - - {t('Task start time')} - - {nsdt(task.created_at)} - - - - {task.completed - ? t('Task end time') - : t('Task last execution time')} - - {nsdt(task.last_execution_date)} - - {(task.scope ?? task.type) - && - - {t('Scope')} - - - - } - - - {t('Status')} - - - - + + + + + + {t('Initiator')} + + {task.initiator?.name} + + + + {t('Task start time')} + + {nsdt(task.created_at)} + + + + {task.completed + ? t('Task end time') + : t('Task last execution time')} + + {nsdt(task.last_execution_date)} + + {(task.scope ?? task.type) + && - {t('Progress')} + {t('Scope')} - + + } + + + {t('Status')} + + + + + + {t('Progress')} + + - + {task.scope // if task.scope exists = it is list task or a query task + ? - {task.scope // if task.scope exists = it is list task or a query task - ? - : - - - } - - - ); - })} - - - - - - - - {t('Timestamp')} - {t('Message')} + + } + + + ); + })} + + + + +
+ + + {t('Timestamp')} + {t('Message')} + + + + {messages.map((message) => ( + + {nsdt(message.timestamp)} + {message.message} - - - {this.state.messages.map((message) => ( - - {nsdt(message.timestamp)} - {message.message} - - ))} - -
-
-
-
- - - -
- - - - - - - - {t('Timestamp')} - {t('Message')} - {t('Source')} + ))} + +
+
+
+
+ + + +
+ + + + + + + + {t('Timestamp')} + {t('Message')} + {t('Source')} + + + + {errors.map((error) => ( + + {nsdt(error.timestamp)} + {error.message} + {error.source} - - - {this.state.errors.map((error) => ( - - {nsdt(error.timestamp)} - {error.message} - {error.source} - - ))} - -
-
-
-
- - - -
- - ); - } -} - -TasksListComponent.propTypes = { - data: PropTypes.object, - options: PropTypes.object, - classes: PropTypes.object, - t: PropTypes.func, + ))} + + + + + + + + + + + ); }; -export const tasksListQuery = graphql` - query TasksListQuery( - $count: Int - $orderBy: BackgroundTasksOrdering - $orderMode: OrderingMode - $includeAuthorities: Boolean - $filters: [BackgroundTasksFiltering] - ) { - ...TasksList_data - @arguments( - count: $count - orderBy: $orderBy - orderMode: $orderMode - includeAuthorities: $includeAuthorities - filters: $filters - ) - } -`; - -const TasksList = createRefetchContainer( - TasksListComponent, - { - data: graphql` - fragment TasksList_data on Query - @argumentDefinitions( - count: { type: "Int" } - orderBy: { type: "BackgroundTasksOrdering", defaultValue: created_at } - orderMode: { type: "OrderingMode", defaultValue: desc } - includeAuthorities: { type: "Boolean", defaultValue: true } - filters: { type: "[BackgroundTasksFiltering]" } - ) { - backgroundTasks( - first: $count - orderBy: $orderBy - orderMode: $orderMode - includeAuthorities: $includeAuthorities - filters: $filters - ) { - edges { - node { - id - type - initiator { - name - } - actions { - type - context { - field - type - values - } - } - created_at - last_execution_date - completed - task_expected_number - task_processed_number - errors { - id - timestamp - message - } - ... on ListTask { - task_ids - scope - } - ... on QueryTask { - task_filters - task_search - scope - } - } - } - } - } - `, - }, - tasksListQuery, -); - -export default R.compose(inject18n, withStyles(styles))(TasksList); +export default TasksList; diff --git a/opencti-platform/opencti-front/src/utils/Localization.js b/opencti-platform/opencti-front/src/utils/Localization.js index 5f1b639a1773..53c5251e072b 100644 --- a/opencti-platform/opencti-front/src/utils/Localization.js +++ b/opencti-platform/opencti-front/src/utils/Localization.js @@ -348,7 +348,7 @@ const i18n = { 'No files in this category.': 'Sin archivos en esta categoría.', 'Text files': 'Ficheros de texto', 'HTML files': 'Ficheros HTML', - 'PDF files': 'Ficheros PDF', + 'Uploaded PDF files': 'Subido ficheros PDF', 'Markdown files': 'Ficheros Markdown', 'sighted in/at': 'visto en', 'auto:': 'auto :', @@ -2567,7 +2567,7 @@ const i18n = { 'No files in this category.': 'Aucun fichier dans cette catégorie.', 'Text files': 'Fichiers texte', 'HTML files': 'Fichiers HTML', - 'PDF files': 'Fichiers PDF', + 'Uploaded PDF files': 'Fichiers PDF téléversés', 'Markdown files': 'Fichiers Markdown', 'sighted in/at': 'détecté dans/en', 'auto:': 'auto:', @@ -4738,7 +4738,7 @@ const i18n = { 'このカテゴリーに該当するファイルはありません。', 'Text files': 'テキストファイル', 'HTML files': 'HTMLファイル', - 'PDF files': 'PDFファイル', + 'Uploaded PDF files': 'アップロードされた PDF ファイル', 'Markdown files': 'マークダウンファイル', 'sighted in/at': '目撃された場所', 'auto:': '自動: ', @@ -6823,7 +6823,7 @@ const i18n = { 'No files in this category.': '此类别中没有文件。', 'Text files': '文本文件', 'HTML files': 'HTML 文件', - 'PDF files': 'PDF 文件', + 'Uploaded PDF files': '上传的 PDF 文件', 'Markdown files': '降价文件', 'sighted in/at': '目击在', 'auto:': '自动:',