From 62d7e06de7cb7442ba7fd12c71bc51d9651f9a98 Mon Sep 17 00:00:00 2001 From: Adrien Servel Date: Fri, 17 May 2024 15:45:27 +0200 Subject: [PATCH] [Frontend] New Datatables :) --- opencti-platform/opencti-front/.eslintrc.js | 1 + opencti-platform/opencti-front/package.json | 3 + .../src/components/FilterIconButton.tsx | 6 +- .../src/components/ItemMarkings.jsx | 8 +- .../src/components/ItemStatus.jsx | 19 +- .../src/components/dataGrid/DataTable.tsx | 185 ++ .../src/components/dataGrid/DataTableBody.tsx | 218 ++ .../dataGrid/DataTableComponent.tsx | 149 ++ .../components/dataGrid/DataTableFilters.tsx | 282 ++ .../components/dataGrid/DataTableHeader.tsx | 170 ++ .../components/dataGrid/DataTableHeaders.tsx | 167 ++ .../src/components/dataGrid/DataTableLine.tsx | 178 ++ .../dataGrid/DataTableWithoutFragment.tsx | 42 + .../src/components/dataGrid/dataTableHooks.ts | 29 + .../src/components/dataGrid/dataTableTypes.ts | 177 ++ .../components/dataGrid/dataTableUtils.tsx | 592 +++++ .../opencti-front/src/private/Index.tsx | 25 +- .../StixCoreObjectsExports.jsx | 10 +- .../components/data/DataTableToolBar.jsx | 2336 +++++++++++++++++ .../src/private/components/data/ToolBar.jsx | 2283 +--------------- .../src/private/components/nav/LeftBar.jsx | 33 +- .../src/private/components/nav/TopBar.tsx | 7 +- .../indicators/IndicatorObservablePopover.jsx | 2 +- .../indicators/IndicatorObservables.jsx | 243 +- .../observations/indicators/Root.jsx | 2 +- .../SettingsMessagesBanner.tsx | 13 +- .../settings/sub_types/SubTypesLines.tsx | 2 +- .../opencti-front/src/static/css/index.css | 4 + .../opencti-front/src/utils/Entity.ts | 2 + .../src/utils/ExportContextProvider.tsx | 4 +- .../opencti-front/src/utils/Number.js | 2 +- .../opencti-front/src/utils/hooks/useBus.ts | 8 +- .../src/utils/hooks/useEntityToggle.ts | 46 +- .../src/utils/hooks/useLocalStorage.ts | 306 ++- .../src/utils/hooks/useLocalStorageModel.ts | 2 +- .../hooks/usePreloadedPaginationFragment.ts | 3 +- 36 files changed, 4972 insertions(+), 2587 deletions(-) create mode 100644 opencti-platform/opencti-front/src/components/dataGrid/DataTable.tsx create mode 100644 opencti-platform/opencti-front/src/components/dataGrid/DataTableBody.tsx create mode 100644 opencti-platform/opencti-front/src/components/dataGrid/DataTableComponent.tsx create mode 100644 opencti-platform/opencti-front/src/components/dataGrid/DataTableFilters.tsx create mode 100644 opencti-platform/opencti-front/src/components/dataGrid/DataTableHeader.tsx create mode 100644 opencti-platform/opencti-front/src/components/dataGrid/DataTableHeaders.tsx create mode 100644 opencti-platform/opencti-front/src/components/dataGrid/DataTableLine.tsx create mode 100644 opencti-platform/opencti-front/src/components/dataGrid/DataTableWithoutFragment.tsx create mode 100644 opencti-platform/opencti-front/src/components/dataGrid/dataTableHooks.ts create mode 100644 opencti-platform/opencti-front/src/components/dataGrid/dataTableTypes.ts create mode 100644 opencti-platform/opencti-front/src/components/dataGrid/dataTableUtils.tsx create mode 100644 opencti-platform/opencti-front/src/private/components/data/DataTableToolBar.jsx diff --git a/opencti-platform/opencti-front/.eslintrc.js b/opencti-platform/opencti-front/.eslintrc.js index 360e72a79783c..95722c71eeaea 100644 --- a/opencti-platform/opencti-front/.eslintrc.js +++ b/opencti-platform/opencti-front/.eslintrc.js @@ -50,6 +50,7 @@ module.exports = { 'custom-rules', ], rules: { + '@typescript-eslint/no-non-null-assertion': 'off', 'custom-rules/classes-rule': 1, 'no-restricted-syntax': 0, 'react/no-unused-prop-types': 0, diff --git a/opencti-platform/opencti-front/package.json b/opencti-platform/opencti-front/package.json index 17ac847b0ad50..29cd40a8fd9b9 100644 --- a/opencti-platform/opencti-front/package.json +++ b/opencti-platform/opencti-front/package.json @@ -21,6 +21,7 @@ "@rjsf/core": "5.18.1", "@rjsf/mui": "5.18.1", "@rjsf/utils": "5.18.1", + "@types/react-beautiful-dnd": "^13.1.8", "analytics": "0.8.11", "apexcharts": "3.48.0", "axios": "1.6.8", @@ -52,10 +53,12 @@ "ramda": "0.29.1", "react": "18.2.0", "react-apexcharts": "1.4.1", + "react-beautiful-dnd": "13.1.1", "react-color": "2.19.3", "react-cookie": "7.1.4", "react-csv": "2.2.2", "react-dom": "18.2.0", + "react-draggable": "4.4.6", "react-force-graph-2d": "1.25.4", "react-force-graph-3d": "1.24.2", "react-grid-layout": "1.4.4", diff --git a/opencti-platform/opencti-front/src/components/FilterIconButton.tsx b/opencti-platform/opencti-front/src/components/FilterIconButton.tsx index bad3cb126d228..40685e087d91a 100644 --- a/opencti-platform/opencti-front/src/components/FilterIconButton.tsx +++ b/opencti-platform/opencti-front/src/components/FilterIconButton.tsx @@ -8,8 +8,8 @@ import { handleFilterHelpers } from '../utils/hooks/useLocalStorage'; import { filterValuesContentQuery } from './FilterValuesContent'; import { FilterValuesContentQuery } from './__generated__/FilterValuesContentQuery.graphql'; -interface FilterIconButtonProps { - availableFilterKeys?: string[]; +export interface FilterIconButtonProps { + availableFilterKeys?: string[] | undefined; filters?: FilterGroup; handleRemoveFilter?: (key: string, op?: string) => void; handleSwitchGlobalMode?: () => void; @@ -20,7 +20,7 @@ interface FilterIconButtonProps { disabledPossible?: boolean; redirection?: boolean; helpers?: handleFilterHelpers; - availableRelationFilterTypes?: Record; + availableRelationFilterTypes?: Record | undefined; entityTypes?: string[]; filtersRestrictions?: FiltersRestrictions; searchContext?: FilterSearchContext diff --git a/opencti-platform/opencti-front/src/components/ItemMarkings.jsx b/opencti-platform/opencti-front/src/components/ItemMarkings.jsx index f0a810f93ff4b..3bf4c872a287c 100644 --- a/opencti-platform/opencti-front/src/components/ItemMarkings.jsx +++ b/opencti-platform/opencti-front/src/components/ItemMarkings.jsx @@ -97,7 +97,7 @@ const StyledBadge = styled(Badge)(() => ({ }, })); -const ItemMarkings = ({ variant, markingDefinitions, limit }) => { +const ItemMarkings = ({ variant, markingDefinitions, limit, handleAddFilter }) => { const markings = markingDefinitions ?? []; const classes = useStyles(); const theme = useTheme(); @@ -134,6 +134,11 @@ const ItemMarkings = ({ variant, markingDefinitions, limit }) => { border, }} label={markingDefinition.definition} + onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + handleAddFilter('objectMarking', markingDefinition.id); + }} /> ); } @@ -238,6 +243,7 @@ const ItemMarkings = ({ variant, markingDefinitions, limit }) => { ItemMarkings.propTypes = { variant: PropTypes.string, limit: PropTypes.number, + handleAddFilter: PropTypes.func, }; export default ItemMarkings; diff --git a/opencti-platform/opencti-front/src/components/ItemStatus.jsx b/opencti-platform/opencti-front/src/components/ItemStatus.jsx index 64026f7cc3bbd..92e205bee72fd 100644 --- a/opencti-platform/opencti-front/src/components/ItemStatus.jsx +++ b/opencti-platform/opencti-front/src/components/ItemStatus.jsx @@ -25,16 +25,30 @@ const styles = () => ({ borderRadius: 4, width: 80, }, + chipInline: { + fontSize: 12, + lineHeight: '10px', + height: 20, + float: 'left', + textTransform: 'uppercase', + borderRadius: 4, + }, }); const ItemStatus = (props) => { - const { classes, t, status, variant, disabled } = props; - const style = variant === 'inList' ? classes.chipInList : classes.chip; + const { classes, t, status, variant, disabled, onClick } = props; + let style = classes.chip; + if (variant === 'inList') { + style = classes.chipInList; + } else if (variant === 'inLine') { + style = classes.chipInline; + } if (status && status.template) { return ( { ItemStatus.propTypes = { classes: PropTypes.object.isRequired, + onClick: PropTypes.func, status: PropTypes.object, variant: PropTypes.string, t: PropTypes.func, diff --git a/opencti-platform/opencti-front/src/components/dataGrid/DataTable.tsx b/opencti-platform/opencti-front/src/components/dataGrid/DataTable.tsx new file mode 100644 index 0000000000000..c4a3599fe0678 --- /dev/null +++ b/opencti-platform/opencti-front/src/components/dataGrid/DataTable.tsx @@ -0,0 +1,185 @@ +import React from 'react'; +import { useSettingsMessagesBannerHeight } from '@components/settings/settings_messages/SettingsMessagesBanner'; +import * as R from 'ramda'; +import DataTableToolBar from '@components/data/DataTableToolBar'; +import makeStyles from '@mui/styles/makeStyles'; +import { OperationType } from 'relay-runtime'; +import DataTableFilters, { DataTableDisplayFilters } from './DataTableFilters'; +import SearchInput from '../SearchInput'; +import type { DataTableProps } from './dataTableTypes'; +import { usePaginationLocalStorage } from '../../utils/hooks/useLocalStorage'; +import useAuth from '../../utils/hooks/useAuth'; +import { useComputeLink, useDataCellHelpers, useDataTable, useDataTableLocalStorage, useDataTableToggle, useLineData } from './dataTableHooks'; +import DataTableComponent from './DataTableComponent'; +import { useFormatter } from '../i18n'; +import { SELECT_COLUMN_SIZE } from './DataTableHeader'; +import type { Theme } from '../Theme'; +import { getDefaultFilterObject } from '../../utils/filters/filtersUtils'; +import { UsePreloadedPaginationFragment } from '../../utils/hooks/usePreloadedPaginationFragment'; +import { FilterIconButtonProps } from '../FilterIconButton'; +import { DataTableVariant } from './dataTableTypes'; + +type OCTIDataTableProps = Pick & { + preloadedPaginationProps: UsePreloadedPaginationFragment, + availableRelationFilterTypes?: FilterIconButtonProps['availableRelationFilterTypes'] + availableEntityTypes?: string[] + availableRelationshipTypes?: string[] + searchContextFinal?: { entityTypes: string[]; elementId?: string[] | undefined; } | undefined + filterExportContext?: { entity_type?: string, entity_id?: string } + additionalHeaderButtons?: React.ReactNode + currentView?: string +}; + +const useStyles = makeStyles((theme) => ({ + toolbar: { + background: theme.palette.background.paper, + width: `calc(( var(--header-table-size) - ${SELECT_COLUMN_SIZE} ) * 1px)`, + }, +})); +const DataTable = (props: OCTIDataTableProps) => { + const { schema } = useAuth(); + const classes = useStyles(); + const formatter = useFormatter(); + + const { + storageKey, + initialValues, + availableFilterKeys: defaultAvailableFilterKeys, + searchContextFinal, + availableEntityTypes, + availableRelationshipTypes, + availableRelationFilterTypes, + preloadedPaginationProps: dataQueryArgs, + additionalFilterKeys, + lineFragment, + filterExportContext, + entityTypes, + toolbarFilters, + variant = DataTableVariant.default, + additionalHeaderButtons, + currentView, + } = props; + + const { + viewStorage: { + searchTerm, + redirectionMode, + numberOfElements, + sortBy, + orderAsc, + }, + helpers, + paginationOptions, + } = usePaginationLocalStorage(storageKey, initialValues!, variant !== DataTableVariant.default); + + const settingsMessagesBannerHeight = useSettingsMessagesBannerHeight(); + + const computedEntityTypes = entityTypes ?? (filterExportContext?.entity_type ? [filterExportContext.entity_type] : []); + let availableFilterKeys = defaultAvailableFilterKeys ?? []; + if (availableFilterKeys.length === 0 && computedEntityTypes) { + const filterKeysMap = new Map(); + computedEntityTypes.forEach((entityType: string) => { + const currentMap = schema.filterKeysSchema.get(entityType); + currentMap?.forEach((value, key) => filterKeysMap.set(key, value)); + }); + availableFilterKeys = R.uniq(Array.from(filterKeysMap.keys())); // keys of the entity type if availableFilterKeys is not specified + } + if (additionalFilterKeys) { + availableFilterKeys = availableFilterKeys.concat(additionalFilterKeys); + } + + const { + selectedElements, + deSelectedElements, + numberOfSelectedElements, + selectAll, + handleClearSelectedElements, + } = useDataTableToggle(storageKey); + + return ( + helpers.handleAddFilterWithEmptyValue(getDefaultFilterObject(id))} + formatter={formatter} + settingsMessagesBannerHeight={settingsMessagesBannerHeight} + storageHelpers={helpers} + redirectionMode={redirectionMode} + numberOfElements={numberOfElements} + onSort={helpers.handleSort} + sortBy={sortBy} + orderAsc={orderAsc} + filtersComponent={( + <> +
+ + +
+ + + )} + dataTableToolBarComponent={( + <> +
+ +
+ + )} + /> + ); +}; + +export default DataTable; diff --git a/opencti-platform/opencti-front/src/components/dataGrid/DataTableBody.tsx b/opencti-platform/opencti-front/src/components/dataGrid/DataTableBody.tsx new file mode 100644 index 0000000000000..ced1fe412aa13 --- /dev/null +++ b/opencti-platform/opencti-front/src/components/dataGrid/DataTableBody.tsx @@ -0,0 +1,218 @@ +import React, { useCallback, useContext, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import makeStyles from '@mui/styles/makeStyles'; +import { createStyles } from '@mui/styles'; +import { Theme as MuiTheme } from '@mui/material/styles/createTheme'; +import DataTableHeaders from './DataTableHeaders'; +import { DataTableContext } from './dataTableUtils'; +import { ColumnSizeVars, DataTableBodyProps, DataTableVariant, LocalStorageColumns } from './dataTableTypes'; +import DataTableLine, { DataTableLineDummy } from './DataTableLine'; +import { SELECT_COLUMN_SIZE } from './DataTableHeader'; + +const useStyles = makeStyles(() => createStyles({ + tableContainer: ({ columnSizeVars, variant }) => ({ + ...columnSizeVars, + height: variant === DataTableVariant.default ? 'calc(var(--table-height) * 1px)' : '500px', + overflowY: 'visible', + }), + linesContainer: ({ variant }) => ({ + height: variant === DataTableVariant.default ? 'calc(var(--table-height, 100%) * 1px - 50px)' : '450px', + width: 'calc(var(--col-table-size, 100%) * 1px)', + overflowY: 'auto', + overflowX: 'hidden', + }), +})); + +const DataTableBody = ({ + columns, + dataQueryArgs, + redirectionMode, + storageHelpers, + settingsMessagesBannerHeight = 0, + hasFilterComponent, + sortBy, + orderAsc, + dataTableToolBarComponent, +}: DataTableBodyProps) => { + const { resolvePath, rootRef, storageKey, setColumns, useDataTable, useDataTableLocalStorage, variant } = useContext(DataTableContext); + + // QUERY PART + const { data: queryData, hasMore, loadMore, isLoading } = useDataTable!(dataQueryArgs); + + const fetchMore = (number = 5) => { + if (!hasMore?.()) { + return; + } + loadMore?.(number); + }; + + const resolvedData = useMemo(() => { + return resolvePath(queryData); + }, [queryData, resolvePath]); + + // TABLE HANDLING + const [resize, setResize] = useState(false); + const [scrollTop, setScrollTop] = useState(0); + const [computeState, setComputeState] = useState(null); + const containerRef = useRef(null); + + const bottomReached = useCallback(() => { + const { scrollHeight, scrollTop: newScrollTop, clientHeight } = computeState as HTMLDivElement; + if (Math.abs(newScrollTop - scrollTop) > 1000 || scrollTop < 100) { + setScrollTop(newScrollTop); + } + if (scrollHeight - newScrollTop - clientHeight < 750 && !isLoading) { + fetchMore(); + } + }, [fetchMore, isLoading]); + + const [localStorageColumns, setLocalStorageColumns] = useDataTableLocalStorage!(`${storageKey}_columns`, {}, true); + + const startsWithSelect = columns.at(0)?.id === 'select'; + const endsWithNavigate = columns.at(-1)?.id === 'navigate'; + let storedSize = 0; + if (startsWithSelect) { + storedSize += SELECT_COLUMN_SIZE; + } + if (endsWithNavigate) { + storedSize += SELECT_COLUMN_SIZE; + } + + // This is intended to improve performance by memoizing the column sizes + const columnSizeVars: ColumnSizeVars = React.useMemo(() => { + const localColumns: LocalStorageColumns = {}; + const colSizes: { [key: string]: number } = { + '--header-select-size': SELECT_COLUMN_SIZE, + '--col-select-size': SELECT_COLUMN_SIZE, + '--header-navigate-size': SELECT_COLUMN_SIZE, + '--col-navigate-size': SELECT_COLUMN_SIZE, + }; + if (!computeState && !containerRef.current) { + return colSizes; + } + const clientWidth = containerRef.current!.clientWidth - storedSize - (variant === DataTableVariant.inline ? 10 : 0); + for (let i = startsWithSelect ? 1 : 0; i < columns.length - (endsWithNavigate ? 1 : 0); i += 1) { + const column = { ...columns[i], ...localStorageColumns[columns[i].id] }; + const shouldCompute = (!column.size || resize) && (column.flexSize && Boolean(computeState)); + let size = column.size ?? 200; + + // We must compute px size for columns + if (shouldCompute) { + size = column.flexSize * (clientWidth / 100); + column.size = size; + } + localColumns[column.id] = { size }; + colSizes[`--header-${column.id}-size`] = size; + colSizes[`--col-${column.id}-size`] = size; + } + if (Object.keys(localColumns).length > 0) { + setResize(false); + } + if (Object.entries(localColumns).some(([id, { size }]) => localStorageColumns[id]?.size !== size)) { + setLocalStorageColumns(localColumns); + setColumns((curr) => { + return curr.map((col) => { + if (localColumns[col.id]) { + return { ...col, size: localColumns[col.id].size }; + } + return col; + }); + }); + } + const columnsSize = Object.values(localColumns).reduce((acc, { size }) => acc + size, 0); + const tableSize = columnsSize + storedSize; + if (variant === DataTableVariant.inline) { + containerRef.current!.style.overflowY = 'hidden'; + } else if (columnsSize > clientWidth) { + containerRef.current!.style.overflowX = 'auto'; + containerRef.current!.style.overflowY = 'hidden'; + } else { + containerRef.current!.style.overflow = 'hidden'; + } + colSizes['--header-table-size'] = tableSize; + colSizes['--col-table-size'] = tableSize; + if (rootRef) { + colSizes['--table-height'] = rootRef.offsetHeight - 42; // SIZE OF CONTAINER - Nb Elements - Line Size + } else { + const rootSize = document.getElementById('root')!.offsetHeight - settingsMessagesBannerHeight; + colSizes['--table-height'] = rootSize - (hasFilterComponent && (document.getElementById('filter-container')!.children.length) ? 240 : 200); + } + + return colSizes; + }, [ + resize, + computeState, + columns, + localStorageColumns, + document.getElementById('filter-container'), + rootRef, + ]); + const classes = useStyles({ columnSizeVars, variant }); + + useLayoutEffect(() => { + const handleResize = () => setResize(true); + const handleStorage = ({ key }: StorageEvent) => setTimeout(() => { + if (key === 'navOpen') { + setResize(true); + } + }, 200); + + let observer: MutationObserver | undefined; + if (hasFilterComponent) { + window.addEventListener('resize', handleResize); + window.addEventListener('storage', handleStorage); + observer = new MutationObserver(() => setResize(true)); + observer.observe(document.getElementById('filter-container')!, { childList: true }); + } + + return () => { + window.removeEventListener('resize', handleResize); + window.removeEventListener('storage', handleStorage); + if (hasFilterComponent && observer) { + observer.disconnect(); + } + }; + }, []); + const effectiveColumns = useMemo(() => columns + .map((col) => ({ ...col, size: localStorageColumns[col.id]?.size })), [columns, localStorageColumns]); + + return ( +
+ +
setComputeState(node)} + onScroll={bottomReached} + className={classes.linesContainer} + > + {computeState && ( + <> + {/* If we have perf issues we should find a way to memoize this */} + {resolvedData.map((row: { id: string }) => { + return ( + + ); + })} + {isLoading && Array(10).fill(0).map((_, idx) => ())} + + )} +
+
+ ); +}; + +export default DataTableBody; diff --git a/opencti-platform/opencti-front/src/components/dataGrid/DataTableComponent.tsx b/opencti-platform/opencti-front/src/components/dataGrid/DataTableComponent.tsx new file mode 100644 index 0000000000000..4b274286034ac --- /dev/null +++ b/opencti-platform/opencti-front/src/components/dataGrid/DataTableComponent.tsx @@ -0,0 +1,149 @@ +import React, { useState } from 'react'; +import * as R from 'ramda'; +import { DataTableLineDummy } from './DataTableLine'; +import DataTableBody from './DataTableBody'; +import { DataTableContext, defaultColumnsMap } from './dataTableUtils'; +import { DataTableColumn, DataTableColumns, DataTableContextProps, DataTableProps, DataTableVariant, LocalStorageColumns } from './dataTableTypes'; +import DataTableHeaders from './DataTableHeaders'; +import { SELECT_COLUMN_SIZE } from './DataTableHeader'; + +const DataTableComponent = ({ + dataColumns, + resolvePath, + storageKey, + initialValues, + availableFilterKeys, + toolbarFilters, + dataQueryArgs, + parametersWithPadding = false, + redirectionModeEnabled = false, + useLineData, + useDataTable, + useDataCellHelpers, + useDataTableToggle, + useComputeLink, + useDataTableLocalStorage, + formatter, + settingsMessagesBannerHeight, + storageHelpers, + filtersComponent, + redirectionMode, + numberOfElements, + onAddFilter, + onSort, + sortBy, + orderAsc, + dataTableToolBarComponent, + variant = DataTableVariant.default, + rootRef, + actions, +}: DataTableProps) => { + const localStorageColumns = useDataTableLocalStorage!(`${storageKey}_columns`, {}, true)[0]; + const toggleHelper = useDataTableToggle!(storageKey); + + const [columns, setColumns] = useState([ + ...(toggleHelper.onToggleEntity ? [{ id: 'select', visible: true } as DataTableColumn] : []), + ...Object.entries(dataColumns).map(([id, column], index) => { + const currentColumn = localStorageColumns?.[id]; + return R.mergeDeepRight(defaultColumnsMap.get(id) as DataTableColumn, { + ...column, + id, + order: currentColumn?.index ?? index, + visible: currentColumn?.visible ?? true, + ...(currentColumn?.size ? { size: currentColumn?.size } : {}), + }); + }), + ...(actions ? [] : [{ id: 'navigate', visible: true } as DataTableColumn]), + ]); + + const clientWidth = document.getElementsByTagName('main')[0].clientWidth - 46; + + const temporaryColumnsSize: { [key: string]: number } = { + '--header-select-size': SELECT_COLUMN_SIZE, + '--col-select-size': SELECT_COLUMN_SIZE, + '--header-navigate-size': SELECT_COLUMN_SIZE, + '--col-navigate-size': SELECT_COLUMN_SIZE, + '--header-table-size': clientWidth, + '--col-table-size': clientWidth, + }; + columns.forEach((col) => { + if (col.visible && col.flexSize) { + const size = col.flexSize * (clientWidth / 100); + temporaryColumnsSize[`--header-${col.id}-size`] = size; + temporaryColumnsSize[`--col-${col.id}-size`] = size; + } + }); + + return ( + visible).sort((a, b) => a.order! - b.order!), + initialValues, + setColumns, + resolvePath, + parametersWithPadding, + redirectionModeEnabled, + toolbarFilters, + useLineData, + useDataTable, + useDataCellHelpers, + useDataTableToggle, + useComputeLink, + useDataTableLocalStorage, + onAddFilter, + onSort, + formatter, + variant, + rootRef, + actions, + } as DataTableContextProps} + > + {filtersComponent ?? ( +
+ {`${numberOfElements?.number}${numberOfElements?.symbol}`}{' '} + {formatter!.t_i18n('entitie(s)')} +
+ )} + + + {Array(10) + .fill(0) + .map((_, idx) => ( + + ))} + + )} + > + visible)} + redirectionMode={redirectionMode} + storageHelpers={storageHelpers} + settingsMessagesBannerHeight={settingsMessagesBannerHeight} + hasFilterComponent={!!filtersComponent} + sortBy={sortBy} + orderAsc={orderAsc} + dataTableToolBarComponent={dataTableToolBarComponent} + /> + +
+ ); +}; + +export default DataTableComponent; diff --git a/opencti-platform/opencti-front/src/components/dataGrid/DataTableFilters.tsx b/opencti-platform/opencti-front/src/components/dataGrid/DataTableFilters.tsx new file mode 100644 index 0000000000000..e6972ad28b0ab --- /dev/null +++ b/opencti-platform/opencti-front/src/components/dataGrid/DataTableFilters.tsx @@ -0,0 +1,282 @@ +import Filters from '@components/common/lists/Filters'; +import React, { useContext, useState } from 'react'; +import makeStyles from '@mui/styles/makeStyles'; +import Tooltip from '@mui/material/Tooltip'; +import { FileDownloadOutlined, SettingsOutlined } from '@mui/icons-material'; +import ToggleButton from '@mui/material/ToggleButton'; +import StixDomainObjectsExports from '@components/common/stix_domain_objects/StixDomainObjectsExports'; +import StixCoreRelationshipsExports from '@components/common/stix_core_relationships/StixCoreRelationshipsExports'; +import StixCoreObjectsExports from '@components/common/stix_core_objects/StixCoreObjectsExports'; +import StixCyberObservablesExports from '@components/observations/stix_cyber_observables/StixCyberObservablesExports'; +import { ToggleButtonGroup } from '@mui/material'; +import DialogTitle from '@mui/material/DialogTitle'; +import DialogContent from '@mui/material/DialogContent'; +import FormControl from '@mui/material/FormControl'; +import InputLabel from '@mui/material/InputLabel'; +import Select from '@mui/material/Select'; +import MenuItem from '@mui/material/MenuItem'; +import DialogActions from '@mui/material/DialogActions'; +import Button from '@mui/material/Button'; +import Dialog from '@mui/material/Dialog'; +import FilterIconButton from '../FilterIconButton'; +import { useFormatter } from '../i18n'; +import { DataTableDisplayFiltersProps, DataTableFiltersProps, DataTableVariant } from './dataTableTypes'; +import { DataTableContext } from './dataTableUtils'; +import { usePaginationLocalStorage } from '../../utils/hooks/useLocalStorage'; +import { export_max_size, isNotEmptyField } from '../../utils/utils'; +import useEntityToggle from '../../utils/hooks/useEntityToggle'; +import Security from '../../utils/Security'; +import { KNOWLEDGE_KNGETEXPORT } from '../../utils/hooks/useGranted'; +import { ExportContext } from '../../utils/ExportContextProvider'; +import Transition from '../Transition'; + +const useStyles = makeStyles(() => ({ + filterContainer: { + minHeight: 10, + marginBottom: 10, + }, + filterInliner: { + display: 'inline-flex', + justifyContent: 'space-between', + flex: 1, + }, + filterHeaderContainer: { + display: 'inline-grid', + gridAutoFlow: 'column', + marginLeft: 10, + gap: 10, + }, + viewsAligner: { + display: 'flex', + alignItems: 'center', + }, +})); + +export const DataTableDisplayFilters = ({ + availableFilterKeys, + availableRelationFilterTypes, + additionalFilterKeys, + entityTypes, +}: DataTableDisplayFiltersProps) => { + const classes = useStyles(); + const { storageKey, initialValues, variant } = useContext(DataTableContext); + const { helpers, viewStorage: { filters } } = usePaginationLocalStorage(storageKey, initialValues!, variant !== DataTableVariant.default); + + return ( +
+ +
+ ); +}; + +const DataTableFilters = ({ + availableFilterKeys, + searchContextFinal, + availableEntityTypes, + availableRelationshipTypes, + availableRelationFilterTypes, + paginationOptions, + filterExportContext, + currentView, + additionalHeaderButtons, +}: DataTableFiltersProps) => { + const { t_i18n } = useFormatter(); + + const [openSettings, setOpenSettings] = useState(false); + + const { + parametersWithPadding, + storageKey, + initialValues, + redirectionModeEnabled, + variant, + } = useContext(DataTableContext); + const { helpers, viewStorage: { numberOfElements, openExports, redirectionMode } } = usePaginationLocalStorage(storageKey, initialValues!, variant !== DataTableVariant.default); + + const { selectedElements } = useEntityToggle(storageKey); + + const classes = useStyles(); + + const exportDisabled = !filterExportContext || (numberOfElements + && ((Object.keys(selectedElements).length > export_max_size + && numberOfElements.number > export_max_size) + || (Object.keys(selectedElements).length === 0 + && numberOfElements.number > export_max_size))); + + return ( + + {availableFilterKeys && availableFilterKeys.length > 0 && ( +
+
+ +
+
+ {isNotEmptyField(numberOfElements) && ( +
+ {`${numberOfElements.number}${numberOfElements.symbol}`}{' '} + {t_i18n('entitie(s)')} +
+ )} + { + if (value && value === 'export') { + helpers.handleToggleExports(); + } else if (value && value === 'settings') { + setOpenSettings(true); + } else if (value && value !== 'export-csv') { + helpers.handleChangeView(value); + } + }} + style={{ margin: '0 0 0 5px' }} + > + {additionalHeaderButtons} + {redirectionModeEnabled && ( + + + + + + )} + {!exportDisabled && ( + + + + + + )} + +
+
+ )} + {filterExportContext + && filterExportContext.entity_type !== 'Stix-Core-Object' + && filterExportContext.entity_type !== 'Stix-Cyber-Observable' + && filterExportContext.entity_type !== 'stix-core-relationship' && ( + + + + )} + {helpers.handleToggleExports && filterExportContext + && filterExportContext.entity_type === 'stix-core-relationship' && ( + + + + )} + {helpers.handleToggleExports && filterExportContext + && filterExportContext.entity_type === 'Stix-Core-Object' && ( + + + + )} + {helpers.handleToggleExports && filterExportContext + && filterExportContext.entity_type === 'Stix-Cyber-Observable' && ( + + + + )} + {redirectionModeEnabled && ( + setOpenSettings(false)} + maxWidth="xs" + fullWidth + > + {t_i18n('List settings')} + + + + {t_i18n('Redirection mode')} + + + + + + + + + )} +
+ ); +}; + +export default DataTableFilters; diff --git a/opencti-platform/opencti-front/src/components/dataGrid/DataTableHeader.tsx b/opencti-platform/opencti-front/src/components/dataGrid/DataTableHeader.tsx new file mode 100644 index 0000000000000..b26dec43f4490 --- /dev/null +++ b/opencti-platform/opencti-front/src/components/dataGrid/DataTableHeader.tsx @@ -0,0 +1,170 @@ +import React, { FunctionComponent, useContext } from 'react'; +import { ArrowDropDown, ArrowDropUp, MoreVert } from '@mui/icons-material'; +import IconButton from '@mui/material/IconButton'; +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; +import SimpleDraggrable from 'react-draggable'; +import makeStyles from '@mui/styles/makeStyles'; +import { createStyles } from '@mui/styles'; +import { Theme as MuiTheme } from '@mui/material/styles/createTheme'; +import { DataTableContext } from './dataTableUtils'; +import { DataTableColumn, DataTableHeaderProps, LocalStorageColumns } from './dataTableTypes'; +import { isNotEmptyField } from '../../utils/utils'; + +export const SELECT_COLUMN_SIZE = 42; + +const useStyles = makeStyles((theme) => createStyles({ + headerContainer: { + position: 'relative', + display: 'flex', + width: ({ column }) => `calc(var(--header-${column?.id}-size) * 1px)`, + fontWeight: 'bold', + justifyContent: 'center', + alignItems: 'center', + height: 40, + }, + headerAligner: { + paddingLeft: 8, + display: 'flex', + alignItems: 'center', + whiteSpace: 'nowrap', + overflow: 'hidden', + cursor: ({ column: { isSortable } }) => (isSortable ? 'pointer' : 'unset'), + }, + aligner: { flexGrow: 1 }, + draggable: { + position: 'absolute', + top: 0, + right: 3, + height: '100%', + width: 3, + background: theme.palette.primary.dark, + cursor: 'col-resize', + userSelect: 'none', + touchAction: 'none', + zIndex: 999, + }, +})); + +const DataTableHeader: FunctionComponent = ({ + column, + anchorEl, + setAnchorEl, + handleClose, + setLocalStorageColumns, + containerRef, + sortBy, + orderAsc, +}) => { + const classes = useStyles({ column }); + + const { + columns, + setColumns, + availableFilterKeys, + onAddFilter, + onSort, + formatter, + } = useContext(DataTableContext); + + const { t_i18n } = formatter!; + + return ( +
+
{ + // Small debounce + (e.target as HTMLDivElement).style.setProperty('pointer-events', 'none'); + setTimeout(() => { + (e.target as HTMLDivElement).style.setProperty('pointer-events', 'auto'); + }, 800); + if (column.isSortable) { + onSort!(column.id, !orderAsc); + } + }} + > + {column.label} + {sortBy && (orderAsc ? : )} +
+ <> +
+ {(column.isSortable || isNotEmptyField(availableFilterKeys)) && ( + <> + setAnchorEl(e.currentTarget)} + > + + + + {/* handleToggleVisibility(column.id)}>{t_i18n('Hide column')} */} + {column.isSortable && ( onSort!(column.id, true)}>{t_i18n('Sort Asc')})} + {column.isSortable && ( onSort!(column.id, false)}>{t_i18n('Sort Desc')})} + {availableFilterKeys?.includes(column.id) && ( + { + onAddFilter!(column.id); + handleClose(); + }} + > + {t_i18n('Add filtering')} + + )} + + + )} + { + if (!containerRef) { + return; + } + const newSize = (column?.size ?? 0) + lastX; + + const effectiveColumns = columns.filter(({ id }) => !['select', 'navigate'].includes(id)); + const currentSize = effectiveColumns.reduce((acc, col) => acc + (col.size ?? 0), 0); + + const currentColIndex = effectiveColumns.findIndex(({ id }) => id === column.id); + const otherColIndex = currentColIndex === effectiveColumns.length - 1 ? currentColIndex - 1 : currentColIndex + 1; + const currentCol = effectiveColumns[currentColIndex]; + + currentCol!.size = newSize; + + const clientWidth = containerRef.current!.clientWidth - (2 * SELECT_COLUMN_SIZE); + + const otherColumn = effectiveColumns[otherColIndex]; + const clientDiff = clientWidth - effectiveColumns.reduce((acc, col) => acc + (col.size ?? 0), 0); + + if (clientDiff > 0) { + const flexSize = (100 * currentCol.size!) / currentSize; + if (otherColumn) { + const otherColumnNewSize = otherColumn.size! - lastX - currentSize + clientWidth; + otherColumn.size = otherColumnNewSize; + otherColumn.flexSize = (otherColumnNewSize * 100) / clientWidth; + } + currentCol.flexSize = flexSize; + } + + setLocalStorageColumns((curr: LocalStorageColumns) => ({ + ...curr, + [column.id]: { ...curr[column.id], size: newSize }, + [otherColumn.id]: { ...curr[otherColumn.id], ...otherColumn }, + })); + const newColumns = [columns.at(0) as DataTableColumn, ...effectiveColumns, columns.at(-1) as DataTableColumn]; + setColumns(newColumns); + }} + > +
+ + +
+ ); +}; + +export default DataTableHeader; diff --git a/opencti-platform/opencti-front/src/components/dataGrid/DataTableHeaders.tsx b/opencti-platform/opencti-front/src/components/dataGrid/DataTableHeaders.tsx new file mode 100644 index 0000000000000..c5d6e42e73bab --- /dev/null +++ b/opencti-platform/opencti-front/src/components/dataGrid/DataTableHeaders.tsx @@ -0,0 +1,167 @@ +import React, { FunctionComponent, useContext, useMemo, useState } from 'react'; +import makeStyles from '@mui/styles/makeStyles'; +import Checkbox from '@mui/material/Checkbox'; +import IconButton from '@mui/material/IconButton'; +import { DragIndicatorOutlined, MoreVert } from '@mui/icons-material'; +import Menu from '@mui/material/Menu'; +import { DragDropContext, Draggable, DraggableLocation, Droppable } from 'react-beautiful-dnd'; +import MenuItem from '@mui/material/MenuItem'; +import { PopoverProps } from '@mui/material/Popover/Popover'; +import { Theme as MuiTheme } from '@mui/material/styles/createTheme'; +import { DataTableColumns, DataTableHeadersProps, LocalStorageColumns } from './dataTableTypes'; +import DataTableHeader from './DataTableHeader'; +import { DataTableContext } from './dataTableUtils'; + +const useStyles = makeStyles((theme) => ({ + headersContainer: { + background: theme.palette.background.paper, + display: 'flex', + width: 'calc(var(--header-table-size) * 1px)', + }, + aligner: { flexGrow: 1 }, +})); + +const DataTableHeaders: FunctionComponent = ({ + containerRef, + effectiveColumns, + dataTableToolBarComponent, + sortBy, + orderAsc, +}) => { + const classes = useStyles(); + const { + storageKey, + columns, + setColumns, + useDataTableToggle, + useDataTableLocalStorage, + actions, + } = useContext(DataTableContext); + + const { + selectAll, + numberOfSelectedElements, + handleToggleSelectAll, + } = useDataTableToggle!(storageKey); + + const [_, setLocalStorageColumns] = useDataTableLocalStorage!(`${storageKey}_columns`, {}, true); + + const [anchorEl, setAnchorEl] = useState(null); + const handleClose = () => setAnchorEl(null); + + const handleToggleVisibility = (columnId: string) => { + const newColumns = [...effectiveColumns]; + const currentColumn = newColumns.find(({ id }) => id === columnId); + currentColumn!.visible = !currentColumn!.visible; + setLocalStorageColumns((curr: LocalStorageColumns) => ({ ...curr, [columnId]: { ...curr[columnId], visible: !currentColumn!.visible } })); + setColumns(newColumns); + }; + + const ordonableColumns = useMemo(() => effectiveColumns.filter(({ id }) => !['select', 'navigate'].includes(id)), [columns]); + + return ( +
+ {effectiveColumns.some(({ id }) => id === 'select') && ( +
+ +
+ )} + {numberOfSelectedElements > 0 ? dataTableToolBarComponent : ( + <> + {effectiveColumns + .filter(({ id }) => !['select', 'navigate'].includes(id)) + .map((column) => ( + + ))} + {(effectiveColumns.some(({ id }) => id === 'navigate') || actions) && ( +
+ )} + {effectiveColumns.some(({ id }) => id === 'todo-navigate') && ( + <> +
+ setAnchorEl(e.currentTarget)} + > + + + + { + const result = Array.from(ordonableColumns); + const [removed] = result.splice(source.index, 1); + result.splice((destination as DraggableLocation).index, 0, removed); + + const newColumns: DataTableColumns = [ + effectiveColumns.at(0), + ...(result.map((c, index) => { + const currentColumn = effectiveColumns.find(({ id }) => id === c.id); + return ({ ...currentColumn, order: index }); + })), + effectiveColumns.at(-1), + ] as DataTableColumns; + + setColumns(newColumns); + setLocalStorageColumns((curr: LocalStorageColumns) => ({ ...curr, [draggableId]: { ...curr[draggableId], order: destination?.index } })); + }} + > + + {(provided) => ( +
+ {ordonableColumns.map((c, index) => ( + + {(item) => ( + + + handleToggleVisibility(c.id)} + checked={c.visible} + /> + {c.label} + + )} + + ))} + {provided.placeholder} +
+ )} +
+
+
+ + )} + + )} +
+ ); +}; + +export default DataTableHeaders; diff --git a/opencti-platform/opencti-front/src/components/dataGrid/DataTableLine.tsx b/opencti-platform/opencti-front/src/components/dataGrid/DataTableLine.tsx new file mode 100644 index 0000000000000..95db3577834fe --- /dev/null +++ b/opencti-platform/opencti-front/src/components/dataGrid/DataTableLine.tsx @@ -0,0 +1,178 @@ +import React, { useContext, useMemo } from 'react'; +import Skeleton from '@mui/material/Skeleton'; +import Checkbox from '@mui/material/Checkbox'; +import IconButton from '@mui/material/IconButton'; +import { KeyboardArrowRightOutlined } from '@mui/icons-material'; +import makeStyles from '@mui/styles/makeStyles'; +import { createStyles } from '@mui/styles'; +import { Theme as MuiTheme } from '@mui/material/styles/createTheme'; +import { useNavigate } from 'react-router-dom'; +import { DataTableContext } from './dataTableUtils'; +import type { DataTableCellProps, DataTableLineProps } from './dataTableTypes'; +import { DataTableColumn } from './dataTableTypes'; + +const useStyles = makeStyles((theme) => createStyles({ + cellContainer: ({ cell }) => ({ + display: 'flex', + borderBottom: `1px solid ${theme.palette.background.nav}`, + width: `calc(var(--col-${cell?.id}-size) * 1px)`, + height: '50px', + alignItems: 'center', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + }), + cellPadding: { + display: 'flex', + paddingLeft: 10, + paddingRight: 10, + width: 'fill-available', + alignItems: 'center', + gap: 3, + }, + dummyContainer: { + display: 'flex', + gap: 8, + }, + row: { + display: 'flex', + '&:hover': { + backgroundColor: + theme.palette.mode === 'dark' + ? 'rgba(255, 255, 255, .2)' + : 'rgba(0, 0, 0, .2)', + }, + }, +})); + +export const DataTableLineDummy = () => { + const classes = useStyles({}); + const { effectiveColumns } = useContext(DataTableContext); + + const lines = useMemo(() => ( + <> + {effectiveColumns.map((column) => ( + + ))} + + ), [effectiveColumns]); + return ( +
+ {lines} +
+ ); +}; + +const DataTableCell = ({ + cell, + data, +}: DataTableCellProps) => { + const classes = useStyles({ cell }); + const { useDataCellHelpers } = useContext(DataTableContext); + + const helpers = useDataCellHelpers!(cell); + + return ( +
+
+ {cell.render?.(data, helpers) ?? (
-
)} +
+
+ ); +}; + +const DataTableLine = ({ row, redirectionMode, storageHelpers, effectiveColumns }: DataTableLineProps) => { + const classes = useStyles({}); + + const { + storageKey, + useLineData, + useDataTableToggle, + useComputeLink, + actions, + } = useContext(DataTableContext); + + const { + selectAll, + deSelectedElements, + selectedElements, + onToggleEntity, + } = useDataTableToggle!(storageKey); + + const data = useLineData!(row); + + const navigate = useNavigate(); + + let link = useComputeLink!(data)!; + if (redirectionMode && redirectionMode !== 'overview') { + link = `${link}/${redirectionMode}`; + } + + const startsWithSelect = effectiveColumns.at(0)?.id === 'select'; + + return ( +
navigate(link)} + > + {startsWithSelect && ( +
+ + onToggleEntity(data, event)} + checked={ + (selectAll + && !((data?.id || 'id') in (deSelectedElements || {}))) + || (data?.id || 'id') in (selectedElements || {}) + } + /> +
+ )} + {effectiveColumns.slice(startsWithSelect ? 1 : 0, actions ? undefined : -1).map((column) => ( + + ))} +
{ + if (actions) { + e.preventDefault(); + e.stopPropagation(); + } + }} + > + {actions && actions(data)} + {effectiveColumns.at(-1)?.id === 'navigate' && ( + navigate(link)}> + + + )} +
+
+ ); +}; + +export default DataTableLine; diff --git a/opencti-platform/opencti-front/src/components/dataGrid/DataTableWithoutFragment.tsx b/opencti-platform/opencti-front/src/components/dataGrid/DataTableWithoutFragment.tsx new file mode 100644 index 0000000000000..e6995c956f32a --- /dev/null +++ b/opencti-platform/opencti-front/src/components/dataGrid/DataTableWithoutFragment.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import type { DataTableProps } from './dataTableTypes'; +import { DataTableVariant } from './dataTableTypes'; +import { useComputeLink, useDataCellHelpers, useDataTableLocalStorage } from './dataTableHooks'; +import DataTableComponent from './DataTableComponent'; +import { useFormatter } from '../i18n'; + +type OCTIDataTableProps = Pick & { + data: never, + globalCount: number +}; + +const DataTable = (props: OCTIDataTableProps) => { + const formatter = useFormatter(); + + const { + data, + variant = DataTableVariant.default, + globalCount, + } = props; + + return ( + ({ data })} + useLineData={(line) => line} + dataQueryArgs={(line: never) => line} + formatter={formatter} + resolvePath={(a) => a} + useDataTableLocalStorage={useDataTableLocalStorage} + useDataTableToggle={() => ({})} + useComputeLink={useComputeLink} + useDataCellHelpers={useDataCellHelpers({}, variant)} + {...props} + /> + ); +}; + +export default DataTable; diff --git a/opencti-platform/opencti-front/src/components/dataGrid/dataTableHooks.ts b/opencti-platform/opencti-front/src/components/dataGrid/dataTableHooks.ts new file mode 100644 index 0000000000000..b3421940ebc06 --- /dev/null +++ b/opencti-platform/opencti-front/src/components/dataGrid/dataTableHooks.ts @@ -0,0 +1,29 @@ +import { useFragment } from 'react-relay'; +import type { GraphQLTaggedNode, OperationType } from 'relay-runtime'; +import type { KeyType } from 'react-relay/relay-hooks/helpers'; +import { DataTableColumn, DataTableVariant, UseDataTable } from './dataTableTypes'; +import usePreloadedPaginationFragment, { UsePreloadedPaginationFragment } from '../../utils/hooks/usePreloadedPaginationFragment'; +import { useFormatter } from '../i18n'; +import useEntityToggle from '../../utils/hooks/useEntityToggle'; +import { computeLink } from '../../utils/Entity'; +import useLocalStorage, { UseLocalStorageHelpers } from '../../utils/hooks/useLocalStorage'; + +export const useLineData = (lineFragment: GraphQLTaggedNode) => (row: KeyType) => useFragment(lineFragment, row); + +export const useDataTable = (args: UsePreloadedPaginationFragment): UseDataTable => usePreloadedPaginationFragment(args) as UseDataTable; + +export const useDataCellHelpers = (storageHelpers: UseLocalStorageHelpers | Record, variant: DataTableVariant) => (column: DataTableColumn) => { + const formatterHelper = useFormatter(); + return { + ...formatterHelper, + storageHelpers, + column, + variant, + }; +}; + +export const useDataTableToggle = useEntityToggle; + +export const useComputeLink = computeLink; + +export const useDataTableLocalStorage = useLocalStorage; diff --git a/opencti-platform/opencti-front/src/components/dataGrid/dataTableTypes.ts b/opencti-platform/opencti-front/src/components/dataGrid/dataTableTypes.ts new file mode 100644 index 0000000000000..8374b25102af1 --- /dev/null +++ b/opencti-platform/opencti-front/src/components/dataGrid/dataTableTypes.ts @@ -0,0 +1,177 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type { Dispatch, MutableRefObject, ReactNode, SetStateAction } from 'react'; +import React from 'react'; +import { GraphQLTaggedNode } from 'react-relay'; +import { PopoverProps } from '@mui/material/Popover/Popover'; +import type { LocalStorage } from '../../utils/hooks/useLocalStorageModel'; +import { FilterGroup } from '../../utils/filters/filtersUtils'; +import { UseLocalStorageHelpers } from '../../utils/hooks/useLocalStorage'; + +export type ColumnSizeVars = Record; + +export type LocalStorageColumn = { size: number, visible?: boolean, index?: number }; +export type LocalStorageColumns = Record; + +export enum DataTableVariant { + default = 'default', + inline = 'inline', +} + +export interface UseDataTable { + data: T[] + hasMore: () => boolean + loadMore: (count: number, options?: Record) => void + isLoading: boolean + isLoadingMore: () => boolean +} + +export interface DataTableColumn { + id: string + isSortable?: boolean + label?: string + size?: number + flexSize: number + render?: (v: any, helpers?: any) => ReactNode + visible?: boolean + order?: number + lastX?: number +} + +export type DataTableColumns = DataTableColumn[]; + +export interface DataTableContextProps { + storageKey: string + columns: DataTableColumns + availableFilterKeys?: string[] | undefined; + effectiveColumns: DataTableColumns + initialValues: DataTableProps['initialValues'] + setColumns: Dispatch> + resolvePath: (data: any) => any + parametersWithPadding: boolean + redirectionModeEnabled?: boolean + toolbarFilters?: FilterGroup + useLineData?: (row: any) => any + useDataTable?: (args: any) => any + useDataCellHelpers?: (cell: DataTableColumn) => any + useDataTableToggle?: (key: string) => { + selectedElements: Record + deSelectedElements: Record + selectAll: boolean + numberOfSelectedElements: number + onToggleEntity: ( + entity: any, + _?: React.SyntheticEvent, + forceRemove?: any[], + ) => void + handleClearSelectedElements: () => void + handleToggleSelectAll: () => void + setSelectedElements: (selectedElements: Record) => void + } | Record + useComputeLink?: (entity: any) => string | null + useDataTableLocalStorage?: (key: string, initialValues?: T, ignoreUri?: boolean) => [T, Dispatch>] + onAddFilter?: (key: string) => void + onSort?: (sortBy: string, orderAsc: boolean) => void + formatter?: Record any> + variant: DataTableVariant + actions?: DataTableProps['actions'] + rootRef?: DataTableProps['rootRef'] +} + +export interface DataTableProps { + dataColumns: Record> + resolvePath: (data: any) => any + storageKey: string + initialValues?: LocalStorage + parametersWithPadding?: boolean + toolbarFilters?: FilterGroup + lineFragment?: GraphQLTaggedNode + dataQueryArgs: any + availableFilterKeys?: string[] | undefined; + redirectionModeEnabled?: boolean + additionalFilterKeys?: string[] + entityTypes?: string[] + settingsMessagesBannerHeight?: number + storageHelpers?: UseLocalStorageHelpers + redirectionMode?: string | undefined + filtersComponent?: ReactNode + dataTableToolBarComponent?: ReactNode + numberOfElements: { number: number, symbol: string } | undefined + onAddFilter?: DataTableContextProps['onAddFilter'] + onSort?: (sortBy: string, orderAsc: boolean) => void + formatter: DataTableContextProps['formatter'] + useDataTableLocalStorage: DataTableContextProps['useDataTableLocalStorage'] + useComputeLink: DataTableContextProps['useComputeLink'] + useDataTableToggle: DataTableContextProps['useDataTableToggle'] + useLineData: DataTableContextProps['useLineData'] + useDataTable: DataTableContextProps['useDataTable'] + useDataCellHelpers: DataTableContextProps['useDataCellHelpers'] + sortBy?: string | undefined + orderAsc?: boolean | undefined + variant?: DataTableVariant + rootRef?: HTMLDivElement + actions?: (row: any) => ReactNode +} + +export interface DataTableBodyProps { + columns: DataTableColumns + redirectionMode: DataTableProps['redirectionMode'] + storageHelpers: DataTableProps['storageHelpers'] + dataQueryArgs: DataTableProps['dataQueryArgs'] + hasFilterComponent: boolean + dataTableToolBarComponent?: ReactNode + sortBy: DataTableProps['sortBy'] + orderAsc: DataTableProps['orderAsc'] + settingsMessagesBannerHeight?: DataTableProps['settingsMessagesBannerHeight'] +} + +export interface DataTableDisplayFiltersProps { + entityTypes?: string[] + additionalFilterKeys?: string[] + availableRelationFilterTypes?: Record | undefined + availableFilterKeys?: string[] | undefined; + paginationOptions: any +} + +export interface DataTableFiltersProps { + availableFilterKeys?: string[] | undefined; + availableRelationFilterTypes?: Record | undefined + availableEntityTypes?: string[] + availableRelationshipTypes?: string[] + searchContextFinal?: { entityTypes: string[]; elementId?: string[] | undefined; } | undefined + filterExportContext?: { entity_type?: string, entity_id?: string } + paginationOptions: any + currentView?: string + additionalHeaderButtons?: ReactNode +} + +export interface DataTableHeadersProps { + containerRef?: MutableRefObject + effectiveColumns: DataTableColumns + sortBy: DataTableProps['sortBy'] + orderAsc: DataTableProps['orderAsc'] + dataTableToolBarComponent: ReactNode +} + +export interface DataTableHeaderProps { + column: DataTableColumn + anchorEl: PopoverProps['anchorEl'] + setAnchorEl: Dispatch> + handleClose: () => void + setLocalStorageColumns: Dispatch> + containerRef?: MutableRefObject + sortBy: boolean + orderAsc: boolean +} + +export interface DataTableLineProps { + row: any + redirectionMode?: string | undefined + effectiveColumns: DataTableColumns + storageHelpers: DataTableProps['storageHelpers'] +} + +export interface DataTableCellProps { + cell: DataTableColumn + data: any + storageHelpers: DataTableProps['storageHelpers'] +} diff --git a/opencti-platform/opencti-front/src/components/dataGrid/dataTableUtils.tsx b/opencti-platform/opencti-front/src/components/dataGrid/dataTableUtils.tsx new file mode 100644 index 0000000000000..5b16cdf706d1e --- /dev/null +++ b/opencti-platform/opencti-front/src/components/dataGrid/dataTableUtils.tsx @@ -0,0 +1,592 @@ +import React from 'react'; +import Chip from '@mui/material/Chip'; +import makeStyles from '@mui/styles/makeStyles'; +import StixCoreObjectLabels from '@components/common/stix_core_objects/StixCoreObjectLabels'; +import Tooltip from '@mui/material/Tooltip'; +import { Link } from 'react-router-dom'; +import type { DataTableColumn, DataTableContextProps } from './dataTableTypes'; +import { DataTableProps, DataTableVariant } from './dataTableTypes'; +import ItemIcon from '../ItemIcon'; +import { hexToRGB, itemColor } from '../../utils/Colors'; +import ItemMarkings from '../ItemMarkings'; +import ItemStatus from '../ItemStatus'; +import { emptyFilled, truncate } from '../../utils/String'; +import ItemPriority from '../ItemPriority'; +import { isNotEmptyField } from '../../utils/utils'; +import RatingField from '../fields/RatingField'; +import ItemConfidence from '../ItemConfidence'; +import ItemPatternType from '../ItemPatternType'; +import type { Theme } from '../Theme'; +import { getMainRepresentative } from '../../utils/defaultRepresentatives'; + +export const DataTableContext = React.createContext({ + parametersWithPadding: false, + storageKey: '', + initialValues: {}, + columns: [], + effectiveColumns: [], + setColumns: () => {}, + resolvePath: () => {}, + variant: DataTableVariant.default, +}); + +const useStyles = makeStyles((theme) => ({ + chipInList: { + fontSize: 12, + height: 20, + float: 'left', + width: 120, + textTransform: 'uppercase', + borderRadius: '0', + }, + chip: { + fontSize: 13, + lineHeight: '12px', + height: 20, + textTransform: 'uppercase', + borderRadius: 4, + cursor: 'pointer', + '&:hover': { + backgroundColor: theme.palette.primary.main, + }, + }, + chipNoLink: { + fontSize: 13, + lineHeight: '12px', + height: 20, + textTransform: 'uppercase', + borderRadius: 4, + }, + positive: { + fontSize: 12, + lineHeight: '12px', + height: 20, + backgroundColor: 'rgba(244, 67, 54, 0.08)', + color: '#f44336', + textTransform: 'uppercase', + borderRadius: '0', + }, + negative: { + fontSize: 12, + lineHeight: '12px', + height: 20, + backgroundColor: 'rgba(76, 175, 80, 0.08)', + color: '#4caf50', + textTransform: 'uppercase', + borderRadius: '0', + }, +})); + +const defaultColumns: DataTableProps['dataColumns'] = { + analyses: { + label: 'Analyses', + flexSize: 8, + isSortable: false, + render: ({ id, entity_type, containersNumber }, { n }) => { + const classes = useStyles(); + const link = `/dashboard/observations/${ + entity_type === 'Artifact' ? 'artifacts' : 'observables' + }/${id}`; + const linkAnalyses = `${link}/analyses`; + return ( + <> + {[ + 'Note', + 'Opinion', + 'Course-Of-Action', + 'Data-Component', + 'Data-Source', + ].includes(entity_type) ? ( + + ) : ( + + )} + + ); + }, + }, + attribute_abstract: { + label: 'Abstract', + flexSize: 25, + isSortable: true, + render: ({ attribute_abstract, content }, { column: { size } }) => { + const data = attribute_abstract || content; + return ({truncate(data, size * 0.113)}); + }, + }, + attribute_count: { + label: 'Nb.', + flexSize: 4, + isSortable: true, + render: ({ attribute_count }) => ({attribute_count}), + }, + channel_types: { + label: 'Types', + flexSize: 20, + isSortable: true, + render: ({ channel_types }, { column: { size } }) => { + const value = channel_types ? channel_types.join(', ') : '-'; + return ({truncate(value, size * 0.113)}); + }, + }, + confidence: { + flexSize: 10, + label: 'Confidence', + isSortable: true, + render: ({ confidence, entity_type }) => ( + + ), + }, + context: { + id: 'context', + label: 'Context', + flexSize: 10, + isSortable: true, + render: ({ context }) => { + const classes = useStyles(); + return ( + + ); + }, + }, + created: { + id: 'created', + label: 'Original creation', + flexSize: 15, + isSortable: true, + render: ({ created }, { fd }) => fd(created), + }, + created_at: { + id: 'created_at', + label: 'Platform creation date', + flexSize: 15, + isSortable: true, + render: ({ created_at }, { fd }) => fd(created_at), + }, + createdBy: { + id: 'createdBy', + label: 'Author', + flexSize: 12, + render: ({ createdBy }) => createdBy?.name ?? '-', + }, + creator: { + id: 'creator', + label: 'Creators', + flexSize: 12, + render: ({ creators }, { column: { size } }) => { + const value = isNotEmptyField(creators) ? creators.map((c: { name: string }) => c.name).join(', ') : '-'; + return ({truncate(value, size * 0.113)}); + }, + }, + entity_type: { + id: 'entity_type', + label: 'Type', + flexSize: 10, + isSortable: false, + render: (data, { t_i18n }) => { + const classes = useStyles(); + return ( + <> + + + + ); + }, + }, + event_types: { + label: 'Types', + flexSize: 20, + isSortable: true, + render: ({ event_types }, { column: { size } }) => { + const value = event_types ? event_types.join(', ') : '-'; + return ({truncate(value, size * 0.113)}); + }, + }, + external_id: { + label: 'External ID', + flexSize: 10, + isSortable: true, + render: ({ external_id }, { column: { size } }) => ({truncate(external_id, size * 0.113)}), + }, + first_observed: { + label: 'First obs.', + flexSize: 14, + isSortable: true, + render: ({ first_observed }, { nsdt }) => nsdt(first_observed), + }, + first_seen: { + label: 'First obs.', + flexSize: 12, + isSortable: true, + render: ({ first_seen }, { nsdt }) => nsdt(first_seen), + }, + fromName: { + label: 'From name', + flexSize: 18, + isSortable: false, + render: ({ from }, { column: { size }, t_i18n }) => { + const value = from ? getMainRepresentative(from) : t_i18n('Restricted'); + return ({truncate(value, size * 0.113)}); + }, + }, + incident_type: { + label: 'Incident type', + flexSize: 9, + isSortable: true, + render: ({ incident_type }, { t_i18n }) => { + const classes = useStyles(); + return ( + + ); + }, + }, + infrastructure_types: { + label: 'Type', + flexSize: 8, + isSortable: true, + render: ({ infrastructure_types }, { t_i18n }) => { + const classes = useStyles(); + return ( + + ); + }, + }, + killChainPhase: { + label: 'Kill chain phase', + flexSize: 15, + isSortable: false, + render: ({ killChainPhases }) => ((killChainPhases && killChainPhases.length > 0) + ? `[${killChainPhases[0].kill_chain_name}] ${killChainPhases[0].phase_name}` + : '-'), + }, + last_observed: { + label: 'Last obs.', + flexSize: 14, + isSortable: true, + render: ({ last_observed }, { nsdt }) => nsdt(last_observed), + }, + last_seen: { + label: 'Last obs.', + flexSize: 12, + isSortable: true, + render: ({ last_seen }, { nsdt }) => nsdt(last_seen), + }, + modified: { + label: 'Modification date', + flexSize: 15, + isSortable: true, + render: ({ modified }, { fd }) => fd(modified), + }, + name: { + id: 'name', + label: 'Name', + flexSize: 25, + isSortable: true, + render: (data, { column: { size } }) => ({truncate(getMainRepresentative(data), size * 0.113)}), + }, + note_types: { + label: 'Type', + flexSize: 10, + isSortable: true, + render: ({ note_types }, { t_i18n }) => { + const classes = useStyles(); + return ( + + ); + }, + }, + number_observed: { + label: 'Nb.', + flexSize: 8, + isSortable: true, + render: ({ number_observed }, { n }) => ({n(number_observed)}), + }, + objectAssignee: { + label: 'Assignees', + flexSize: 10, + isSortable: false, + render: ({ objectAssignee }, { column: { size } }) => { + const value = isNotEmptyField(objectAssignee) ? objectAssignee.map((c: { name: string }) => c.name).join(', ') : '-'; + return ({truncate(value, size * 0.113)}); + }, + }, + objectLabel: { + id: 'objectLabel', + label: 'Labels', + flexSize: 15, + isSortable: false, + render: ({ objectLabel }, { storageHelpers }) => ( + + ), + }, + objectMarking: { + id: 'objectMarking', + label: 'Marking', + flexSize: 8, + render: ({ objectMarking }, { storageHelpers: { handleAddFilter } }) => ( + + ), + }, + observable_value: { + label: 'Value', + flexSize: 20, + isSortable: false, + render: ({ observable_value }, { column: { size } }) => ({truncate(observable_value, size * 0.113)}), + }, + operatingSystem: { + label: 'Operating System', + flexSize: 15, + isSortable: false, + render: ({ operatingSystem }) => ({operatingSystem?.name ?? '-'}), + }, + pattern_type: { + label: 'Pattern type', + flexSize: 10, + isSortable: true, + render: ({ pattern_type }) => (), + }, + priority: { + label: 'Priority', + flexSize: 10, + isSortable: true, + render: ({ priority }, { t_i18n }) => ( + + ), + }, + product: { + label: 'Product', + flexSize: 15, + isSortable: true, + render: ({ product }, { column: { size } }) => ({truncate(product, size * 0.113)}), + }, + published: { + label: 'Date', + flexSize: 10, + isSortable: true, + render: ({ published }, { fd }) => fd(published), + }, + rating: { + label: 'Rating', + flexSize: 10, + isSortable: true, + render: ({ rating }) => ( + + ), + }, + relationship_type: { + label: 'Type', + flexSize: 10, + isSortable: true, + render: ({ relationship_type }, { t_i18n }) => { + const classes = useStyles(); + return ( + + ); + }, + }, + report_types: { + label: 'Type', + flexSize: 10, + isSortable: true, + render: ({ report_types }, { t_i18n }) => { + const classes = useStyles(); + return ( + + ); + }, + }, + result_name: { + label: 'Result name', + flexSize: 15, + isSortable: true, + render: ({ result_name }, { column: { size } }) => ({truncate(result_name, size * 0.113)}), + }, + severity: { + label: 'Severity', + flexSize: 10, + isSortable: true, + render: ({ severity }, { t_i18n }) => ( + + ), + }, + source_name: { + label: 'Source name', + flexSize: 15, + isSortable: true, + render: ({ source_name }, { column: { size } }) => ({truncate(source_name, size * 0.113)}), + }, + start_time: { + label: 'Start date', + flexSize: 15, + isSortable: true, + render: ({ start_time }, { fd }) => fd(start_time), + }, + stop_time: { + label: 'End date', + flexSize: 15, + isSortable: true, + render: ({ stop_time }, { fd }) => fd(stop_time), + }, + submitted: { + label: 'Submission date', + flexSize: 12, + isSortable: true, + render: ({ submitted }, { fd }) => fd(submitted), + }, + toName: { + label: 'To name', + flexSize: 18, + isSortable: false, + render: ({ to }, { column: { size }, t_i18n }) => { + const value = to ? getMainRepresentative(to) : t_i18n('Restricted'); + return ({truncate(value, size * 0.113)}); + }, + }, + url: { + label: 'URL', + flexSize: 45, + isSortable: true, + render: ({ url }, { column: { size } }) => ({truncate(url, size * 0.113)}), + }, + value: { + label: 'Value', + flexSize: 22, + isSortable: false, + render: (node, { column: { size } }) => { + const value = getMainRepresentative(node); + return ({truncate(value, size * 0.113)}); + }, + }, + x_mitre_id: { + label: 'ID', + flexSize: 10, + isSortable: true, + render: ({ x_mitre_id }) => {emptyFilled(x_mitre_id)}, + }, + x_opencti_negative: { + label: 'Qualification', + flexSize: 15, + isSortable: true, + render: ({ x_opencti_negative }, { t_i18n }) => { + const classes = useStyles(); + return ( + + ); + }, + }, + x_opencti_cvss_base_severity: { + label: 'CVSS3 - Severity', + flexSize: 15, + isSortable: true, + render: ({ x_opencti_cvss_base_severity }) => ({x_opencti_cvss_base_severity}), + }, + x_opencti_organization_type: { + label: 'Type', + flexSize: 15, + isSortable: true, + render: ({ x_opencti_organization_type }) => { + const classes = useStyles(); + return ( + + ); + }, + }, + x_opencti_workflow_id: { + label: 'Status', + flexSize: 8, + isSortable: true, + render: ({ status, workflowEnabled }, { variant }) => ( + + ), + }, +}; + +export const defaultColumnsMap = new Map>(Object.entries(defaultColumns)); diff --git a/opencti-platform/opencti-front/src/private/Index.tsx b/opencti-platform/opencti-front/src/private/Index.tsx index 5ef2dd148a750..731432e93a0c1 100644 --- a/opencti-platform/opencti-front/src/private/Index.tsx +++ b/opencti-platform/opencti-front/src/private/Index.tsx @@ -1,8 +1,8 @@ -import React, { Suspense, lazy, useEffect } from 'react'; +import React, { lazy, Suspense, useEffect } from 'react'; import { Route, Routes } from 'react-router-dom'; import Box from '@mui/material/Box'; import CssBaseline from '@mui/material/CssBaseline'; -import { useTheme, makeStyles } from '@mui/styles'; +import { useTheme } from '@mui/styles'; import { boundaryWrapper, NoMatch } from '@components/Error'; import PlatformCriticalAlertDialog from '@components/settings/platform_alerts/PlatformCriticalAlertDialog'; import TopBar from './components/nav/TopBar'; @@ -36,32 +36,29 @@ const RootWorkspaces = lazy(() => import('./components/workspaces/Root')); const RootSettings = lazy(() => import('./components/settings/Root')); const RootAudit = lazy(() => import('./components/settings/activity/audit/Root')); -// Deprecated - https://mui.com/system/styles/basics/ -// Do not use it for new code. -const useStyles = makeStyles((theme: Theme) => ({ - toolbar: theme.mixins.toolbar, -})); - interface IndexProps { settings: RootSettings$data } const Index = ({ settings }: IndexProps) => { const theme = useTheme(); - const classes = useStyles(); const { bannerSettings: { bannerHeight }, } = useAuth(); + const settingsMessagesBannerHeight = useSettingsMessagesBannerHeight(); const boxSx = { flexGrow: 1, - padding: 3, + paddingLeft: 3, + paddingRight: 3, + paddingBottom: 1, transition: theme.transitions.create('width', { easing: theme.transitions.easing.easeInOut, duration: theme.transitions.duration.enteringScreen, }), - overflowX: 'hidden', + overflowY: 'hidden', + minHeight: '100vh', + paddingTop: `calc( 16px + 64px + ${settingsMessagesBannerHeight ?? 0}px)`, // 24 for margin, 48 for top bar }; - const settingsMessagesBannerHeight = useSettingsMessagesBannerHeight(); // Change the theme body attribute when the mode changes in // the palette because some components like CKEditor uses this // body attribute to display correct styles. @@ -94,10 +91,6 @@ const Index = ({ settings }: IndexProps) => { -
}> diff --git a/opencti-platform/opencti-front/src/private/components/common/stix_core_objects/StixCoreObjectsExports.jsx b/opencti-platform/opencti-front/src/private/components/common/stix_core_objects/StixCoreObjectsExports.jsx index e05e0ef9d259e..e72cb4073f778 100644 --- a/opencti-platform/opencti-front/src/private/components/common/stix_core_objects/StixCoreObjectsExports.jsx +++ b/opencti-platform/opencti-front/src/private/components/common/stix_core_objects/StixCoreObjectsExports.jsx @@ -1,6 +1,6 @@ import React from 'react'; +import * as PropTypes from 'prop-types'; import Drawer from '../drawer/Drawer'; - import { QueryRenderer } from '../../../../relay/environment'; import StixCoreObjectsExportsContent, { stixCoreObjectsExportsContentQuery } from './StixCoreObjectsExportsContent'; import { useFormatter } from '../../../../components/i18n'; @@ -37,4 +37,12 @@ const StixCoreObjectsExports = ({ ); }; +StixCoreObjectsExports.propTypes = { + exportContext: PropTypes.object, + paginationOptions: PropTypes.object, + open: PropTypes.bool, + exportType: PropTypes.string, + handleToggle: PropTypes.func, +}; + export default StixCoreObjectsExports; diff --git a/opencti-platform/opencti-front/src/private/components/data/DataTableToolBar.jsx b/opencti-platform/opencti-front/src/private/components/data/DataTableToolBar.jsx new file mode 100644 index 0000000000000..dcf3abd70190a --- /dev/null +++ b/opencti-platform/opencti-front/src/private/components/data/DataTableToolBar.jsx @@ -0,0 +1,2336 @@ +import React, { Component } from 'react'; +import * as PropTypes from 'prop-types'; +import * as R from 'ramda'; +import { Link } from 'react-router-dom'; +import { graphql } from 'react-relay'; +import withTheme from '@mui/styles/withTheme'; +import withStyles from '@mui/styles/withStyles'; +import Toolbar from '@mui/material/Toolbar'; +import MuiSwitch from '@mui/material/Switch'; +import Typography from '@mui/material/Typography'; +import Tooltip from '@mui/material/Tooltip'; +import List from '@mui/material/List'; +import Radio from '@mui/material/Radio'; +import FormControl from '@mui/material/FormControl'; +import InputLabel from '@mui/material/InputLabel'; +import MenuItem from '@mui/material/MenuItem'; +import Select from '@mui/material/Select'; +import ListItem from '@mui/material/ListItem'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction'; +import Table from '@mui/material/Table'; +import TableHead from '@mui/material/TableHead'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableRow from '@mui/material/TableRow'; +import IconButton from '@mui/material/IconButton'; +import { + AddOutlined, + AutoFixHighOutlined, + BrushOutlined, + CancelOutlined, + CenterFocusStrong, + ClearOutlined, + CloseOutlined, + ContentCopyOutlined, + DeleteOutlined, + LanguageOutlined, + LinkOffOutlined, + MergeOutlined, + MoveToInboxOutlined, + RestoreOutlined, + TransformOutlined, +} from '@mui/icons-material'; +import { CloudRefreshOutline, LabelOutline } from 'mdi-material-ui'; +import Autocomplete from '@mui/material/Autocomplete'; +import Drawer from '@mui/material/Drawer'; +import Dialog from '@mui/material/Dialog'; +import DialogContent from '@mui/material/DialogContent'; +import DialogActions from '@mui/material/DialogActions'; +import Button from '@mui/material/Button'; +import Slide from '@mui/material/Slide'; +import Chip from '@mui/material/Chip'; +import DialogTitle from '@mui/material/DialogTitle'; +import Alert from '@mui/material/Alert'; +import TextField from '@mui/material/TextField'; +import Grid from '@mui/material/Grid'; +import Avatar from '@mui/material/Avatar'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Checkbox from '@mui/material/Checkbox'; +import TasksFilterValueContainer from '../../../components/TasksFilterValueContainer'; +import inject18n from '../../../components/i18n'; +import { truncate } from '../../../utils/String'; +import { commitMutation, fetchQuery, MESSAGING$ } from '../../../relay/environment'; +import ItemIcon from '../../../components/ItemIcon'; +import { objectMarkingFieldAllowedMarkingsQuery } from '../common/form/ObjectMarkingField'; +import { identitySearchIdentitiesSearchQuery } from '../common/identities/IdentitySearch'; +import { labelsSearchQuery } from '../settings/LabelsQuery'; +import Security from '../../../utils/Security'; +import { KNOWLEDGE_KNUPDATE, KNOWLEDGE_KNUPDATE_KNDELETE } from '../../../utils/hooks/useGranted'; +import { UserContext } from '../../../utils/hooks/useAuth'; +import { statusFieldStatusesSearchQuery } from '../common/form/StatusField'; +import { hexToRGB } from '../../../utils/Colors'; +import { externalReferencesQueriesSearchQuery } from '../analyses/external_references/ExternalReferencesQueries'; +import StixDomainObjectCreation from '../common/stix_domain_objects/StixDomainObjectCreation'; +import ItemMarkings from '../../../components/ItemMarkings'; +import { findFilterFromKey, removeIdFromFilterGroupObject, serializeFilterGroupForBackend } from '../../../utils/filters/filtersUtils'; +import { getMainRepresentative } from '../../../utils/defaultRepresentatives'; + +const styles = (theme) => ({ + drawerPaper: { + minHeight: '100vh', + width: '50%', + position: 'fixed', + transition: theme.transitions.create('width', { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.enteringScreen, + }), + padding: 0, + }, + header: { + backgroundColor: theme.palette.background.nav, + padding: '20px 20px 20px 60px', + }, + closeButton: { + position: 'absolute', + top: 12, + left: 5, + color: 'inherit', + }, + buttons: { + marginTop: 20, + textAlign: 'right', + }, + button: { + marginLeft: theme.spacing(2), + }, + buttonAdd: { + width: '100%', + height: 20, + }, + container: { + padding: '10px 20px 20px 20px', + }, + aliases: { + margin: '0 7px 7px 0', + }, + title: { + flex: '1 1 100%', + fontSize: '12px', + }, + chipValue: { + margin: 0, + }, + filter: { + margin: '5px 10px 5px 0', + }, + operator: { + fontFamily: 'Consolas, monaco, monospace', + backgroundColor: theme.palette.background.accent, + margin: '5px 10px 5px 0', + }, + step: { + position: 'relative', + width: '100%', + margin: '0 0 20px 0', + padding: 15, + verticalAlign: 'middle', + border: `1px solid ${theme.palette.background.accent}`, + borderRadius: 5, + display: 'flex', + }, + formControl: { + width: '100%', + }, + stepType: { + margin: 0, + paddingRight: 20, + width: '30%', + }, + stepField: { + margin: 0, + paddingRight: 20, + width: '30%', + }, + stepValues: { + paddingRight: 20, + margin: 0, + }, + stepCloseButton: { + position: 'absolute', + top: -20, + right: -20, + }, + icon: { + paddingTop: 4, + display: 'inline-block', + }, + text: { + display: 'inline-block', + flexGrow: 1, + marginLeft: 10, + }, + autoCompleteIndicator: { + display: 'none', + }, +}); + +const notMergableTypes = [ + 'Indicator', + 'Note', + 'Opinion', + 'Label', + 'Case-Template', + 'Task', + 'DeleteOperation', +]; + +const Transition = React.forwardRef((props, ref) => ( + +)); +Transition.displayName = 'TransitionSlide'; + +const toolBarListTaskAddMutation = graphql` + mutation DataTableToolBarListTaskAddMutation($input: ListTaskAddInput!) { + listTaskAdd(input: $input) { + id + type + } + } +`; + +const toolBarQueryTaskAddMutation = graphql` + mutation DataTableToolBarQueryTaskAddMutation($input: QueryTaskAddInput!) { + queryTaskAdd(input: $input) { + id + type + } + } +`; + +const toolBarConnectorsQuery = graphql` + query DataTableToolBarConnectorsQuery($type: String!) { + enrichmentConnectors(type: $type) { + id + name + } + } +`; + +export const maxNumberOfObservablesToCopy = 1000; + +const toolBarContainersQuery = graphql` + query DataTableToolBarContainersQuery($search: String) { + containers( + search: $search + filters: { + mode: and + filters: [{ key: "entity_type", values: ["Container"] }] + filterGroups: [] + } + ) { + edges { + node { + id + entity_type + representative { + main + } + } + } + } + } +`; + +class DataTableToolBar extends Component { + constructor(props) { + super(props); + this.state = { + displayTask: false, + displayUpdate: false, + displayEnrichment: false, + displayRescan: false, + displayMerge: false, + displayAddInContainer: false, + displayPromote: false, + containerCreation: false, + actions: [], + scope: undefined, + actionsInputs: [{}], + keptEntityId: null, + mergingElement: null, + processing: false, + markingDefinitions: [], + labels: [], + identities: [], + containers: [], + statuses: [], + externalReferences: [], + enrichConnectors: [], + enrichSelected: [], + navOpen: localStorage.getItem('navOpen') === 'true', + }; + } + + componentDidMount() { + this.subscription = MESSAGING$.toggleNav.subscribe({ + next: () => this.setState({ navOpen: localStorage.getItem('navOpen') === 'true' }), + }); + } + + componentWillUnmount() { + this.subscription.unsubscribe(); + } + + handleOpenTask() { + this.setState({ displayTask: true }); + } + + handleCloseTask() { + this.setState({ + displayTask: false, + actions: [], + scope: undefined, + keptEntityId: null, + mergingElement: null, + processing: false, + }); + } + + handleOpenUpdate() { + this.setState({ displayUpdate: true }); + } + + handleOpenRescan() { + this.setState({ displayRescan: true }); + } + + handleCloseRescan() { + this.setState({ displayRescan: false }); + } + + handleCloseUpdate() { + this.setState({ displayUpdate: false, actionsInputs: [{}] }); + } + + handleOpenMerge() { + this.setState({ displayMerge: true }); + } + + handleOpenAddInContainer() { + this.setState({ displayAddInContainer: true }); + } + + handleOpenPromote() { + this.setState({ displayPromote: true }); + } + + handleClosePromote() { + this.setState({ displayPromote: false }); + } + + handleOpenEnrichment() { + // Get enrich type + let enrichType; + if (this.props.selectAll) { + enrichType = this.props.type + ?? R.head( + findFilterFromKey( + this.props.filters?.filters ?? [], + 'entity_type', + 'eq', + )?.values ?? [], + ); + } else { + const selected = this.props.selectedElements; + const selectedTypes = R.uniq( + R.map((o) => o.entity_type, R.values(selected || {})), + ); + enrichType = R.head(selectedTypes); + } + // Get available connectors + fetchQuery(toolBarConnectorsQuery, { type: enrichType }) + .toPromise() + .then((data) => { + this.setState({ + displayEnrichment: true, + enrichConnectors: data.enrichmentConnectors ?? [], + enrichSelected: [], + }); + }); + } + + handleCloseEnrichment() { + this.setState({ displayEnrichment: false }); + } + + handleCloseMerge() { + this.setState({ displayMerge: false }); + } + + handleAddStep() { + this.setState({ actionsInputs: R.append({}, this.state.actionsInputs) }); + } + + handleRemoveStep(i) { + const { actionsInputs } = this.state; + actionsInputs.splice(i, 1); + this.setState({ actionsInputs }); + } + + handleLaunchUpdate() { + const { actionsInputs } = this.state; + const actions = R.map( + (n) => ({ + type: n.type, + context: { + field: n.field, + type: n.fieldType, + values: n.values, + options: n.options, + }, + }), + actionsInputs, + ); + this.setState({ actions }, () => { + this.handleCloseUpdate(); + this.handleOpenTask(); + }); + } + + handleChangeActionInput(i, key, event) { + const { value } = event.target; + const { actionsInputs } = this.state; + actionsInputs[i] = R.assoc(key, value, actionsInputs[i] || {}); + if (key === 'field') { + const values = []; + actionsInputs[i] = R.assoc('values', values, actionsInputs[i] || {}); + if ( + value === 'object-marking' + || value === 'object-label' + || value === 'created-by' + || value === 'external-reference' + ) { + actionsInputs[i] = R.assoc( + 'fieldType', + 'RELATION', + actionsInputs[i] || {}, + ); + } else { + actionsInputs[i] = R.assoc( + 'fieldType', + 'ATTRIBUTE', + actionsInputs[i] || {}, + ); + } + } + this.setState({ actionsInputs }); + } + + handleChangeActionInputValues(i, event, value) { + if (event) { + event.stopPropagation(); + event.preventDefault(); + } + const { actionsInputs } = this.state; + actionsInputs[i] = R.assoc( + 'values', + Array.isArray(value) ? value : [value], + actionsInputs[i] || {}, + ); + this.setState({ actionsInputs }); + } + + handleChangeActionInputOptions(i, key, event) { + const { actionsInputs } = this.state; + actionsInputs[i] = R.assoc( + 'options', + R.assoc(key, event.target.checked, actionsInputs[i]?.options || {}), + actionsInputs[i] || {}, + ); + this.setState({ actionsInputs }); + } + + handleChangeActionInputValuesReplace(i, event) { + const { value } = event.target; + const { actionsInputs } = this.state; + actionsInputs[i] = R.assoc( + 'values', + Array.isArray(value) ? value : [value], + actionsInputs[i] || {}, + ); + this.setState({ actionsInputs }); + } + + handleLaunchDelete() { + const actions = [{ type: 'DELETE', context: null }]; + this.setState({ actions }, () => { + this.handleOpenTask(); + }); + } + + handleLaunchRemove() { + const actions = [ + { + type: 'REMOVE', + context: { field: 'container-object', values: [this.props.container] }, + }, + ]; + this.setState({ actions }, () => { + this.handleOpenTask(); + }); + } + + handleChangeKeptEntityId(entityId) { + this.setState({ keptEntityId: entityId }); + } + + handleChangeEnrichSelected(connectorId) { + if (this.state.enrichSelected.includes(connectorId)) { + const filtered = this.state.enrichSelected.filter( + (e) => e !== connectorId, + ); + this.setState({ enrichSelected: filtered }); + } else { + this.setState({ + enrichSelected: [...this.state.enrichSelected, connectorId], + }); + } + } + + handleLaunchRescan() { + const actions = [{ type: 'RULE_ELEMENT_RESCAN' }]; + this.setState({ actions }, () => { + this.handleCloseRescan(); + this.handleOpenTask(); + }); + } + + handleLaunchPromote() { + const actions = [{ type: 'PROMOTE' }]; + this.setState({ actions }, () => { + this.handleClosePromote(); + this.handleOpenTask(); + }); + } + + handleLaunchEnrichment() { + const actions = [ + { type: 'ENRICHMENT', context: { values: this.state.enrichSelected } }, + ]; + this.setState({ actions }, () => { + this.handleCloseEnrichment(); + this.handleOpenTask(); + }); + } + + handleLaunchMerge() { + const { selectedElements } = this.props; + const { keptEntityId } = this.state; + const selectedElementsList = R.values(selectedElements); + const keptElement = keptEntityId + ? R.head(R.filter((n) => n.id === keptEntityId, selectedElementsList)) + : R.head(selectedElementsList); + const filteredStixDomainObjects = keptEntityId + ? R.filter((n) => n.id !== keptEntityId, selectedElementsList) + : R.tail(selectedElementsList); + let scope = 'KNOWLEDGE'; + if ( + selectedElementsList.some( + ({ entity_type }) => entity_type === 'Vocabulary', + ) + ) { + scope = 'SETTINGS'; + } + const actions = [ + { + type: 'MERGE', + context: { values: filteredStixDomainObjects }, + }, + ]; + this.setState({ scope, actions, mergingElement: keptElement }, () => { + this.handleCloseMerge(); + this.handleOpenTask(); + }); + } + + handleLaunchCompleteDelete() { + const actions = [{ type: 'COMPLETE_DELETE', context: null }]; + this.setState({ actions }, () => { + this.handleOpenTask(); + }); + } + + handleLaunchRestore() { + const actions = [{ type: 'RESTORE', context: null }]; + this.setState({ actions }, () => { + this.handleOpenTask(); + }); + } + + titleCopy() { + const { t } = this.props; + if (this.props.numberOfSelectedElements > maxNumberOfObservablesToCopy) { + return `${ + t( + 'Copy disabled: too many selected elements (maximum number of elements for a copy: ', + ) + maxNumberOfObservablesToCopy + })`; + } + return t('Copy'); + } + + submitTask(availableFilterKeys) { + this.setState({ processing: true }); + const { actions, mergingElement, scope } = this.state; + const { + filters, + search, + selectAll, + selectedElements, + deSelectedElements, + numberOfSelectedElements, + handleClearSelectedElements, + t, + } = this.props; + if (numberOfSelectedElements === 0) return; + const jsonFilters = serializeFilterGroupForBackend( + removeIdFromFilterGroupObject(filters, availableFilterKeys), + ); + const finalActions = R.map( + (n) => ({ + type: n.type, + context: n.context + ? { + ...n.context, + values: R.map((o) => o.id || o.value || o, n.context.values), + } + : null, + }), + actions, + ); + if (selectAll) { + commitMutation({ + mutation: toolBarQueryTaskAddMutation, + variables: { + input: { + filters: jsonFilters, + search, + actions: finalActions, + excluded_ids: Object.keys(deSelectedElements || {}), + scope: scope ?? 'KNOWLEDGE', + }, + }, + onCompleted: () => { + handleClearSelectedElements(); + MESSAGING$.notifySuccess( + + {t( + 'The background task has been executed. You can monitor it on', + )}{' '} + + {t('the dedicated page')} + + . + , + ); + this.setState({ processing: false }); + this.handleCloseTask(); + }, + }); + } else { + commitMutation({ + mutation: toolBarListTaskAddMutation, + variables: { + input: { + ids: mergingElement + ? [mergingElement.id] + : Object.keys(selectedElements), + actions: finalActions, + scope: scope ?? 'KNOWLEDGE', + }, + }, + onCompleted: () => { + handleClearSelectedElements(); + MESSAGING$.notifySuccess( + + {t( + 'The background task has been executed. You can monitor it on', + )}{' '} + + {t('the dedicated page')} + + . + , + ); + this.setState({ processing: false }); + this.handleCloseTask(); + }, + }); + } + } + + renderFieldOptions(i) { + const { t } = this.props; + const { actionsInputs } = this.state; + const disabled = R.isNil(actionsInputs[i]?.type) || R.isEmpty(actionsInputs[i]?.type); + let options = []; + if (actionsInputs[i]?.type === 'ADD') { + options = [ + { label: t('Marking definitions'), value: 'object-marking' }, + { label: t('Labels'), value: 'object-label' }, + { label: t('External references'), value: 'external-reference' }, + { label: t('In containers'), value: 'container-object' }, + ]; + } else if (actionsInputs[i]?.type === 'REPLACE') { + options = [ + { label: t('Marking definitions'), value: 'object-marking' }, + { label: t('Labels'), value: 'object-label' }, + { label: t('Author'), value: 'created-by' }, + { label: t('Score'), value: 'x_opencti_score' }, + { label: t('Confidence'), value: 'confidence' }, + { label: t('Description'), value: 'description' }, + ]; + if (this.props.type) { + options.push({ label: t('Status'), value: 'x_opencti_workflow_id' }); + } + } else if (actionsInputs[i]?.type === 'REMOVE') { + options = [ + { label: t('Marking definitions'), value: 'object-marking' }, + { label: t('Labels'), value: 'object-label' }, + { label: t('External references'), value: 'external-reference' }, + ]; + } + return ( + + ); + } + + searchContainers(i, event, newValue) { + if (!event) return; + const { actionsInputs } = this.state; + actionsInputs[i] = R.assoc( + 'inputValue', + newValue && newValue.length > 0 ? newValue : '', + actionsInputs[i], + ); + this.setState({ actionsInputs }); + fetchQuery(toolBarContainersQuery, { + search: newValue && newValue.length > 0 ? newValue : '', + }) + .toPromise() + .then((data) => { + const elements = data.containers.edges.map((e) => e.node); + const containers = elements.map((n) => ({ + label: n.representative.main, + type: n.entity_type, + value: n.id, + })); + this.setState({ containers }); + }); + } + + searchMarkingDefinitions(i, event, newValue) { + if (!event) return; + const { actionsInputs } = this.state; + actionsInputs[i] = R.assoc( + 'inputValue', + newValue && newValue.length > 0 ? newValue : '', + actionsInputs[i], + ); + this.setState({ actionsInputs }); + fetchQuery(objectMarkingFieldAllowedMarkingsQuery) + .toPromise() + .then((data) => { + const markingDefinitions = R.pipe( + R.pathOr([], ['me', 'allowed_marking']), + R.map((n) => ({ + label: n.definition, + value: n.id, + color: n.x_opencti_color, + })), + )(data); + this.setState({ markingDefinitions }); + }); + } + + searchLabels(i, event, newValue) { + if (!event) return; + const { actionsInputs } = this.state; + actionsInputs[i] = R.assoc( + 'inputValue', + newValue && newValue.length > 0 ? newValue : '', + actionsInputs[i], + ); + this.setState({ actionsInputs }); + fetchQuery(labelsSearchQuery, { + search: newValue && newValue.length > 0 ? newValue : '', + }) + .toPromise() + .then((data) => { + const labels = R.pipe( + R.pathOr([], ['labels', 'edges']), + R.map((n) => ({ + label: n.node.value, + value: n.node.id, + color: n.node.color, + })), + )(data); + this.setState({ + labels: R.union(this.state.labels, labels), + }); + }); + } + + searchExternalReferences(i, event, newValue) { + if (!event) return; + const { actionsInputs } = this.state; + actionsInputs[i] = R.assoc( + 'inputValue', + newValue && newValue.length > 0 ? newValue : '', + actionsInputs[i], + ); + this.setState({ actionsInputs }); + fetchQuery(externalReferencesQueriesSearchQuery, { + search: newValue && newValue.length > 0 ? newValue : '', + }) + .toPromise() + .then((data) => { + const externalReferences = R.pipe( + R.pathOr([], ['externalReferences', 'edges']), + R.sortWith([R.ascend(R.path(['node', 'source_name']))]), + R.map((n) => ({ + label: `[${n.node.source_name}] ${truncate( + n.node.description || n.node.external_id, + 150, + )} ${n.node.url && `(${n.node.url})`}`, + value: n.node.id, + })), + )(data); + this.setState({ + externalReferences: R.union( + this.state.externalReferences, + externalReferences, + ), + }); + }); + } + + searchIdentities(i, event, newValue) { + if (!event) return; + const { actionsInputs } = this.state; + actionsInputs[i] = R.assoc( + 'inputValue', + newValue && newValue.length > 0 ? newValue : '', + actionsInputs[i], + ); + this.setState({ actionsInputs }); + fetchQuery(identitySearchIdentitiesSearchQuery, { + types: ['Individual', 'Organization'], + search: newValue && newValue.length > 0 ? newValue : '', + first: 10, + }) + .toPromise() + .then((data) => { + const identities = R.pipe( + R.pathOr([], ['identities', 'edges']), + R.map((n) => ({ + label: n.node.name, + value: n.node.id, + type: n.node.entity_type, + })), + )(data); + this.setState({ + identities: R.union(this.state.identities, identities), + }); + }); + } + + searchStatuses(i, event, newValue) { + if (!event) return; + const { actionsInputs } = this.state; + actionsInputs[i] = R.assoc( + 'inputValue', + newValue && newValue.length > 0 ? newValue : '', + actionsInputs[i], + ); + this.setState({ actionsInputs }); + fetchQuery(statusFieldStatusesSearchQuery, { + first: 10, + filters: { + mode: 'and', + filterGroups: [], + filters: [{ key: 'type', values: [this.props.type] }], + }, + orderBy: 'order', + orderMode: 'asc', + search: newValue && newValue.length > 0 ? newValue : '', + }) + .toPromise() + .then((data) => { + const statuses = R.pipe( + R.pathOr([], ['statuses', 'edges']), + R.map((n) => ({ + label: n.node.template.name, + value: n.node.id, + order: n.node.order, + color: n.node.template.color, + })), + )(data); + this.setState({ statuses: R.union(this.state.statuses, statuses) }); + }); + } + + renderValuesOptions(i) { + const { t, classes } = this.props; + const { actionsInputs } = this.state; + const disabled = R.isNil(actionsInputs[i]?.field) || R.isEmpty(actionsInputs[i]?.field); + switch (actionsInputs[i]?.field) { + case 'container-object': + return ( + <> + this.setState({ containerCreation: false })} + creationCallback={(data) => { + const element = { + label: data.name, + value: data.id, + type: data.entity_type, + }; + this.setState(({ containers }) => ({ + containers: [...(containers ?? []), element], + })); + this.handleChangeActionInputValues(i, null, [ + ...(actionsInputs[i]?.values ?? []), + element, + ]); + }} + /> + option.label ?? ''} + value={actionsInputs[i]?.values || []} + multiple={true} + renderInput={(params) => ( + + )} + noOptionsText={t('No available options')} + options={this.state.containers} + onInputChange={this.searchContainers.bind(this, i)} + inputValue={actionsInputs[i]?.inputValue || ''} + onChange={this.handleChangeActionInputValues.bind(this, i)} + renderOption={(props, option) => ( +
  • +
    + +
    +
    {option.label}
    +
  • + )} + /> + this.setState({ containerCreation: true })} + edge="end" + style={{ position: 'absolute', top: 22, right: 48 }} + size="large" + > + + + + ); + case 'object-marking': + return ( + (option.label ? option.label : '')} + value={actionsInputs[i]?.values || []} + multiple={true} + renderInput={(params) => ( + + )} + noOptionsText={t('No available options')} + options={this.state.markingDefinitions} + onInputChange={this.searchMarkingDefinitions.bind(this, i)} + inputValue={actionsInputs[i]?.inputValue || ''} + onChange={this.handleChangeActionInputValues.bind(this, i)} + renderOption={(props, option) => ( +
  • +
    + +
    +
    {option.label}
    +
  • + )} + /> + ); + case 'object-label': + return ( + (option.label ? option.label : '')} + value={actionsInputs[i]?.values || []} + multiple={true} + renderInput={(params) => ( + + )} + noOptionsText={t('No available options')} + options={this.state.labels} + onInputChange={this.searchLabels.bind(this, i)} + inputValue={actionsInputs[i]?.inputValue || ''} + onChange={this.handleChangeActionInputValues.bind(this, i)} + renderOption={(props, option) => ( +
  • +
    + +
    +
    {option.label}
    +
  • + )} + /> + ); + case 'created-by': + return ( + (option.label ? option.label : '')} + value={actionsInputs[i]?.values ? actionsInputs[i]?.values[0] : ''} + renderInput={(params) => ( + + )} + noOptionsText={t('No available options')} + options={this.state.identities} + onInputChange={this.searchIdentities.bind(this, i)} + inputValue={actionsInputs[i]?.inputValue || ''} + onChange={this.handleChangeActionInputValues.bind(this, i)} + renderOption={(props, option) => ( +
  • +
    + +
    +
    {option.label}
    +
  • + )} + /> + ); + case 'x_opencti_workflow_id': + return ( + (option.label ? option.label : '')} + value={actionsInputs[i]?.values ? actionsInputs[i]?.values[0] : ''} + renderInput={(params) => ( + + )} + noOptionsText={t('No available options')} + options={this.state.statuses} + onInputChange={this.searchStatuses.bind(this, i)} + inputValue={actionsInputs[i]?.inputValue || ''} + onChange={this.handleChangeActionInputValues.bind(this, i)} + renderOption={(props, option) => ( +
  • +
    + + {option.order} + +
    +
    {option.label}
    +
  • + )} + /> + ); + case 'external-reference': + return ( + (option.label ? option.label : '')} + value={actionsInputs[i]?.values || []} + multiple={true} + renderInput={(params) => ( + + )} + noOptionsText={t('No available options')} + options={this.state.externalReferences} + onInputChange={this.searchExternalReferences.bind(this, i)} + inputValue={actionsInputs[i]?.inputValue || ''} + onChange={this.handleChangeActionInputValues.bind(this, i)} + renderOption={(props, option) => ( +
  • +
    + +
    +
    {option.label}
    +
  • + )} + /> + ); + case 'x_opencti_score': + case 'confidence': + return ( + + ); + default: + return ( + + ); + } + } + + areStepValid() { + const { actionsInputs } = this.state; + for (const n of actionsInputs) { + if (!n || !n.type || !n.field || !n.values || n.values.length === 0) { + return false; + } + } + return true; + } + + render() { + const { + t, + n, + classes, + numberOfSelectedElements, + handleClearSelectedElements, + selectedElements, + selectAll, + filters, + search, + theme, + container, + noAuthor, + noMarking, + noWarning, + deleteDisable, + warning, + warningMessage, + mergeDisable, + deleteOperationEnabled, + } = this.props; + const { actions, keptEntityId, mergingElement, actionsInputs } = this.state; + const selectedTypes = R.uniq( + R.map((o) => o.entity_type, R.values(selectedElements || {})), + ); + const typesAreDifferent = selectedTypes.length > 1; + const preventMerge = selectedTypes.at(0) === 'Vocabulary' + && Object.values(selectedElements).some(({ builtIn }) => Boolean(builtIn)); + // region update + const notUpdatableTypes = ['Label', 'Vocabulary', 'Case-Template', 'Task', 'DeleteOperation']; + const entityTypeFilterValues = findFilterFromKey(filters?.filters ?? [], 'entity_type', 'eq')?.values + ?? []; + const typesAreNotUpdatable = R.includes( + R.uniq( + R.map((o) => o.entity_type, R.values(selectedElements || {})), + )[0], + notUpdatableTypes, + ) + || (entityTypeFilterValues.length === 1 + && notUpdatableTypes.includes(entityTypeFilterValues[0])); + // endregion + // region rules + const notScannableTypes = ['Label', 'Vocabulary', 'Case-Template', 'Task', 'DeleteOperation']; + const typesAreNotScannable = R.includes( + R.uniq( + R.map((o) => o.entity_type, R.values(selectedElements || {})), + )[0], + notScannableTypes, + ) + || (entityTypeFilterValues.length === 1 + && notScannableTypes.includes(entityTypeFilterValues[0])); + // endregion + // region enrich + const notEnrichableTypes = ['Label', 'Vocabulary', 'Case-Template', 'Task', 'DeleteOperation']; + const isManualEnrichSelect = !selectAll && selectedTypes.length === 1; + const isAllEnrichSelect = selectAll + && entityTypeFilterValues.length === 1 + && R.head(entityTypeFilterValues) !== 'Stix-Cyber-Observable' + && R.head(entityTypeFilterValues) !== 'Stix-Domain-Object'; + const enrichDisable = notEnrichableTypes.includes(R.head(selectedTypes)) + || (entityTypeFilterValues.length === 1 + && notEnrichableTypes.includes(entityTypeFilterValues[0])) + || (!isManualEnrichSelect && !isAllEnrichSelect); + // endregion + const typesAreNotMergable = R.includes( + R.uniq(R.map((o) => o.entity_type, R.values(selectedElements || {})))[0], + notMergableTypes, + ); + const enableMerge = !typesAreNotMergable && !mergeDisable; + const notAddableTypes = ['Label', 'Vocabulary', 'Case-Template', 'DeleteOperation']; + const typesAreNotAddableInContainer = R.includes( + R.uniq( + R.map((o) => o.entity_type, R.values(selectedElements || {})), + )[0], + notAddableTypes, + ) + || (entityTypeFilterValues.length === 1 + && notScannableTypes.includes(entityTypeFilterValues[0])); + const selectedElementsList = R.values(selectedElements || {}); + const titleCopy = this.titleCopy(); + let keptElement = null; + let newAliases = []; + if (!typesAreNotMergable && !typesAreDifferent) { + keptElement = keptEntityId + ? R.head(selectedElementsList.filter((o) => o.id === keptEntityId)) + : R.head(selectedElementsList); + if (keptElement) { + const names = R.filter( + (o) => o !== keptElement.name, + R.pluck('name', selectedElementsList), + ); + const aliases = !R.isNil(keptElement.aliases) + ? R.filter( + (o) => !R.isNil(o), + R.flatten(R.pluck('aliases', selectedElementsList)), + ) + : R.filter( + (o) => !R.isNil(o), + R.flatten(R.pluck('x_opencti_aliases', selectedElementsList)), + ); + newAliases = R.filter( + (o) => o.length > 0, + R.uniq(R.concat(names, aliases)), + ); + } + } + return ( + + {({ schema }) => { + // region promote filters + const stixCyberObservableTypes = schema.scos.map((sco) => sco.id); + const promotionTypes = stixCyberObservableTypes.concat(['Indicator']); + const observablesFiltered = entityTypeFilterValues.length > 0 + && entityTypeFilterValues.every((id) => stixCyberObservableTypes.includes(id)); + const promotionTypesFiltered = entityTypeFilterValues.length > 0 + && entityTypeFilterValues.every((id) => promotionTypes.includes(id)); + const isManualPromoteSelect = !selectAll + && selectedTypes.length > 0 + && selectedTypes.every((type) => promotionTypes.includes(type)); + const promoteDisable = !isManualPromoteSelect && !promotionTypesFiltered; + const filterKeysMap = new Map(); + entityTypeFilterValues.forEach((entityType) => { + const currentMap = schema.filterKeysSchema.get(entityType); + currentMap?.forEach((value, key) => filterKeysMap.set(key, value)); + }); + const availableFilterKeys = Array.from(filterKeysMap.keys()).concat(['entity_type']); + // endregion + return ( + <> + + + + {numberOfSelectedElements} + {' '} + {t('selected')}{' '} + + + + + + {!typesAreNotUpdatable && ( + + + + + + + + )} + + {({ platformModuleHelpers }) => { + const label = platformModuleHelpers.isRuleEngineEnable() + ? 'Rule rescan' + : 'Rule rescan (engine is disabled)'; + const buttonDisable = typesAreNotScannable + || !platformModuleHelpers.isRuleEngineEnable() + || numberOfSelectedElements === 0 + || this.state.processing; + return typesAreNotScannable ? undefined : ( + + + + + + + + ); + }} + + {this.props.handleCopy && ( + + + maxNumberOfObservablesToCopy + } + onClick={this.props.handleCopy} + color="primary" + size="small" + > + + + + + )} + {!enrichDisable && ( + + + + + + + + )} + {!promoteDisable && ( + + + + + + + + )} + {enableMerge && ( + + + 4 + || preventMerge + || selectAll + || this.state.processing + } + onClick={this.handleOpenMerge.bind(this)} + color="primary" + size="small" + > + + + + + )} + + {deleteOperationEnabled && ( + + + + + + + + + + + + + + + + + )} + {!typesAreNotAddableInContainer && ( + + + + + + + + + + )} + {container && ( + + + + + + + + + + )} + {deleteDisable !== true && ( + + + + + + + + + + )} + + + +
    + {t('Launch a background task')} +
    +
    + + {n(numberOfSelectedElements)} + {' '} + {t('selected element(s)')} +
    +
    + + {numberOfSelectedElements > 1000 && ( + + {t( + "You're targeting more than 1000 entities with this background task, be sure of what you're doing!", + )} + + )} + + + + + # + {t('Step')} + {t('Field')} + {t('Values')} + + + + + + {' '} + + 1 + + + + + + {t('N/A')} + + {selectAll ? ( +
    + {search && search.length > 0 && ( + + + {t('Search')}: {search} +
    + } + /> + {filters.filters.length > 0 && ( + + )} + + )} + + + ) : ( + + {mergingElement + ? truncate( + R.join(', ', [ + getMainRepresentative(mergingElement), + ]), + 80, + ) + : truncate( + R.join( + ', ', + R.map( + (o) => getMainRepresentative(o), + R.values(selectedElements || {}), + ), + ), + 80, + )} + + )} +
    +
    + {R.map((o) => { + const number = actions.indexOf(o); + return ( + + + {' '} + + {number + 2} + + + + + + + {R.pathOr(t('N/A'), ['context', 'field'], o)} + + + {truncate( + R.join( + ', ', + R.map( + (p) => (typeof p === 'string' + ? p + : getMainRepresentative(p)), + R.pathOr([], ['context', 'values'], o), + ), + ), + 80, + )} + + + ); + }, actions)} +
    +
    +
    +
    + + + + +
    + +
    + + + + {t('Update entities')} +
    +
    + {Array(actionsInputs.length) + .fill(0) + .map((_, i) => ( +
    + + + + + + + {t('Action type')} + + + + + + {t('Field')} + {this.renderFieldOptions(i)} + + + + {this.renderValuesOptions(i)} + + +
    + ))} +
    + +
    +
    + +
    +
    +
    + +
    + + + + {t('Merge entities')} +
    +
    + + {t('Selected entities')} + + + {selectedElementsList.map((element) => ( + + + + + +
    + {R.pathOr('', ['createdBy', 'name'], element)} +
    +
    + +
    + + + +
    + ))} +
    + + {t('Merged entity')} + + + {t('Name')} + +
    + {getMainRepresentative(keptElement)} +
    + + {t('Aliases')} + + {newAliases.map((label) => (label.length > 0 ? ( + + ) : ( + '' + )))} + {noAuthor !== true && ( + <> + + {t('Author')} + + {R.pathOr('', ['createdBy', 'name'], keptElement)} + + )} + {noMarking !== true && ( + <> + + {t('Marking')} + + + + )} + {noWarning !== true && ( + <> + + {t( + 'The relations attached to selected entities will be copied to the merged entity.', + )} + + + )} +
    + +
    +
    +
    + +
    + + + + {t('Entity enrichment')} +
    +
    + + {t('Selected connectors')} + + + {this.state.enrichConnectors.length === 0 && ( + + {t('No connector available for the selected entities.')} + + )} + {this.state.enrichConnectors.map((connector) => ( + + + + + + + + + + ))} + +
    + +
    +
    +
    + +
    + + + + + {t('Observables and indicators conversion')} + +
    +
    + {!observablesFiltered && ( +
    + + {t('Indicators')} + + + {t( + 'This action will generate observables from the selected indicators.', + )} + +
    + )} + {observablesFiltered && ( +
    + + {t('Observables')} + + + {t( + 'This action will generate STIX patterns indicators from the selected observables.', + )} + +
    + )} +
    + +
    +
    +
    + +
    + + + + {t('Rule entity rescan')} +
    +
    + + {t('Selected rules')} + + + {t( + 'Element will be rescan with all compatible activated rules', + )} + +
    + +
    +
    +
    + this.setState({ displayAddInContainer: false })} + > + {t('Add in container')} + + this.setState({ containerCreation: false }) + } + creationCallback={(data) => { + const element = { + label: data.name, + value: data.id, + type: data.entity_type, + }; + this.setState(({ containers }) => ({ + containers: [...(containers ?? []), element], + })); + this.handleChangeActionInputValues(0, null, [ + ...(actionsInputs[0]?.values ?? []), + element, + ]); + }} + /> + (option.label ? option.label : '') + } + value={actionsInputs[0]?.values || []} + multiple={true} + renderInput={(params) => ( + + )} + noOptionsText={t('No available options')} + options={this.state.containers} + onInputChange={this.searchContainers.bind(this, 0)} + inputValue={actionsInputs[0]?.inputValue || ''} + onChange={this.handleChangeActionInputValues.bind(this, 0)} + renderOption={(props, option) => ( +
  • +
    + +
    +
    {option.label}
    +
  • + )} + disableClearable + /> + + } + label={t('Also include first neighbours')} + /> + this.setState({ containerCreation: true })} + edge="end" + style={{ position: 'absolute', top: 68, right: 48 }} + size="large" + > + + +
    + + + + +
    + + ); + }} +
    + ); + } +} + +DataTableToolBar.propTypes = { + classes: PropTypes.object, + theme: PropTypes.object, + t: PropTypes.func, + numberOfSelectedElements: PropTypes.number, + selectedElements: PropTypes.object, + deSelectedElements: PropTypes.object, + selectAll: PropTypes.bool, + filters: PropTypes.object, + search: PropTypes.string, + handleClearSelectedElements: PropTypes.func, + variant: PropTypes.string, + container: PropTypes.object, + type: PropTypes.string, + handleCopy: PropTypes.func, + warning: PropTypes.bool, + warningMessage: PropTypes.string, + rightOffset: PropTypes.number, + mergeDisable: PropTypes.bool, + deleteOperationEnabled: PropTypes.bool, +}; + +export default R.compose(inject18n, withTheme, withStyles(styles))(DataTableToolBar); diff --git a/opencti-platform/opencti-front/src/private/components/data/ToolBar.jsx b/opencti-platform/opencti-front/src/private/components/data/ToolBar.jsx index c5fdc491b6782..7a04f2597bfbf 100644 --- a/opencti-platform/opencti-front/src/private/components/data/ToolBar.jsx +++ b/opencti-platform/opencti-front/src/private/components/data/ToolBar.jsx @@ -1,85 +1,13 @@ import React, { Component } from 'react'; import * as PropTypes from 'prop-types'; import * as R from 'ramda'; -import { Link } from 'react-router-dom'; -import { graphql } from 'react-relay'; -import withTheme from '@mui/styles/withTheme'; import withStyles from '@mui/styles/withStyles'; -import Toolbar from '@mui/material/Toolbar'; -import MuiSwitch from '@mui/material/Switch'; -import Typography from '@mui/material/Typography'; -import Tooltip from '@mui/material/Tooltip'; -import List from '@mui/material/List'; -import Radio from '@mui/material/Radio'; -import FormControl from '@mui/material/FormControl'; -import InputLabel from '@mui/material/InputLabel'; -import MenuItem from '@mui/material/MenuItem'; -import Select from '@mui/material/Select'; -import ListItem from '@mui/material/ListItem'; -import ListItemIcon from '@mui/material/ListItemIcon'; -import ListItemText from '@mui/material/ListItemText'; -import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction'; -import Table from '@mui/material/Table'; -import TableHead from '@mui/material/TableHead'; -import TableBody from '@mui/material/TableBody'; -import TableCell from '@mui/material/TableCell'; -import TableContainer from '@mui/material/TableContainer'; -import TableRow from '@mui/material/TableRow'; -import IconButton from '@mui/material/IconButton'; -import { - AddOutlined, - AutoFixHighOutlined, - BrushOutlined, - CancelOutlined, - CenterFocusStrong, - ClearOutlined, - CloseOutlined, - ContentCopyOutlined, - DeleteOutlined, - LanguageOutlined, - LinkOffOutlined, - MergeOutlined, - MoveToInboxOutlined, - RestoreOutlined, - TransformOutlined, -} from '@mui/icons-material'; -import { CloudRefreshOutline, LabelOutline } from 'mdi-material-ui'; -import Autocomplete from '@mui/material/Autocomplete'; import Drawer from '@mui/material/Drawer'; -import Dialog from '@mui/material/Dialog'; -import DialogContent from '@mui/material/DialogContent'; -import DialogActions from '@mui/material/DialogActions'; -import Button from '@mui/material/Button'; import Slide from '@mui/material/Slide'; -import Chip from '@mui/material/Chip'; -import DialogTitle from '@mui/material/DialogTitle'; -import Alert from '@mui/material/Alert'; -import TextField from '@mui/material/TextField'; -import Grid from '@mui/material/Grid'; -import Avatar from '@mui/material/Avatar'; -import FormControlLabel from '@mui/material/FormControlLabel'; -import Checkbox from '@mui/material/Checkbox'; -import TasksFilterValueContainer from '../../../components/TasksFilterValueContainer'; -import inject18n from '../../../components/i18n'; -import { truncate } from '../../../utils/String'; -import { commitMutation, fetchQuery, MESSAGING$ } from '../../../relay/environment'; -import ItemIcon from '../../../components/ItemIcon'; -import { objectMarkingFieldAllowedMarkingsQuery } from '../common/form/ObjectMarkingField'; -import { getMainRepresentative } from '../../../utils/defaultRepresentatives'; -import { identitySearchIdentitiesSearchQuery } from '../common/identities/IdentitySearch'; -import { labelsSearchQuery } from '../settings/LabelsQuery'; -import Security from '../../../utils/Security'; -import { KNOWLEDGE_KNUPDATE, KNOWLEDGE_KNUPDATE_KNDELETE } from '../../../utils/hooks/useGranted'; +import DataTableToolBar from './DataTableToolBar'; import { UserContext } from '../../../utils/hooks/useAuth'; -import { statusFieldStatusesSearchQuery } from '../common/form/StatusField'; -import { hexToRGB } from '../../../utils/Colors'; -import { externalReferencesQueriesSearchQuery } from '../analyses/external_references/ExternalReferencesQueries'; -import StixDomainObjectCreation from '../common/stix_domain_objects/StixDomainObjectCreation'; -import ItemMarkings from '../../../components/ItemMarkings'; -import { findFilterFromKey, serializeFilterGroupForBackend, removeIdAndIncorrectKeysFromFilterGroupObject } from '../../../utils/filters/filtersUtils'; -import PromoteDrawer from './drawers/PromoteDrawer'; -const styles = (theme) => ({ +const styles = () => ({ bottomNav: { padding: 0, zIndex: 1100, @@ -101,1231 +29,45 @@ const styles = (theme) => ({ height: 50, overflow: 'hidden', }, - drawerPaper: { - minHeight: '100vh', - width: '50%', - position: 'fixed', - transition: theme.transitions.create('width', { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.enteringScreen, - }), - padding: 0, - }, - header: { - backgroundColor: theme.palette.background.nav, - padding: '20px 20px 20px 60px', - }, - closeButton: { - position: 'absolute', - top: 12, - left: 5, - color: 'inherit', - }, - buttons: { - marginTop: 20, - textAlign: 'right', - }, - button: { - marginLeft: theme.spacing(2), - }, - buttonAdd: { - width: '100%', - height: 20, - }, - container: { - padding: '10px 20px 20px 20px', - }, - aliases: { - margin: '0 7px 7px 0', - }, - title: { - flex: '1 1 100%', - fontSize: '12px', - }, - chipValue: { - margin: 0, - }, - filter: { - margin: '5px 10px 5px 0', - }, - operator: { - fontFamily: 'Consolas, monaco, monospace', - backgroundColor: theme.palette.background.accent, - margin: '5px 10px 5px 0', - }, - step: { - position: 'relative', - width: '100%', - margin: '0 0 20px 0', - padding: 15, - verticalAlign: 'middle', - border: `1px solid ${theme.palette.primary.main}`, - borderRadius: 4, - display: 'flex', - }, - formControl: { - width: '100%', - }, - stepType: { - margin: 0, - paddingRight: 20, - width: '30%', - }, - stepField: { - margin: 0, - paddingRight: 20, - width: '30%', - }, - stepValues: { - paddingRight: 20, - margin: 0, - }, - stepCloseButton: { - position: 'absolute', - top: -20, - right: -20, - }, - icon: { - paddingTop: 4, - display: 'inline-block', - }, - text: { - display: 'inline-block', - flexGrow: 1, - marginLeft: 10, - }, - autoCompleteIndicator: { - display: 'none', - }, }); -const notMergableTypes = [ - 'Indicator', - 'Note', - 'Opinion', - 'Label', - 'Case-Template', - 'Task', - 'DeleteOperation', -]; - const Transition = React.forwardRef((props, ref) => ( )); Transition.displayName = 'TransitionSlide'; -const toolBarListTaskAddMutation = graphql` - mutation ToolBarListTaskAddMutation($input: ListTaskAddInput!) { - listTaskAdd(input: $input) { - id - type - } - } -`; - -const toolBarQueryTaskAddMutation = graphql` - mutation ToolBarQueryTaskAddMutation($input: QueryTaskAddInput!) { - queryTaskAdd(input: $input) { - id - type - } - } -`; - -const toolBarConnectorsQuery = graphql` - query ToolBarConnectorsQuery($type: String!) { - enrichmentConnectors(type: $type) { - id - name - } - } -`; - export const maxNumberOfObservablesToCopy = 1000; -const toolBarContainersQuery = graphql` - query ToolBarContainersQuery($search: String) { - containers( - search: $search - filters: { - mode: and - filters: [{ key: "entity_type", values: ["Container"] }] - filterGroups: [] - } - ) { - edges { - node { - id - entity_type - representative { - main - } - } - } - } - } -`; - class ToolBar extends Component { constructor(props) { super(props); this.state = { - displayTask: false, - displayUpdate: false, - displayEnrichment: false, - displayRescan: false, - displayMerge: false, - displayAddInContainer: false, - displayPromote: false, - containerCreation: false, - actions: [], - scope: undefined, - actionsInputs: [{}], - keptEntityId: null, - mergingElement: null, - processing: false, - markingDefinitions: [], - labels: [], - identities: [], - containers: [], - statuses: [], - externalReferences: [], - enrichConnectors: [], - enrichSelected: [], navOpen: localStorage.getItem('navOpen') === 'true', }; } - componentDidMount() { - this.subscription = MESSAGING$.toggleNav.subscribe({ - next: () => this.setState({ navOpen: localStorage.getItem('navOpen') === 'true' }), - }); - } - - componentWillUnmount() { - this.subscription.unsubscribe(); - } - - handleOpenTask() { - this.setState({ displayTask: true }); - } - - handleCloseTask() { - this.setState({ - displayTask: false, - actions: [], - scope: undefined, - keptEntityId: null, - mergingElement: null, - processing: false, - }); - } - - handleOpenUpdate() { - this.setState({ displayUpdate: true }); - } - - handleOpenRescan() { - this.setState({ displayRescan: true }); - } - - handleCloseRescan() { - this.setState({ displayRescan: false }); - } - - handleCloseUpdate() { - this.setState({ displayUpdate: false, actionsInputs: [{}] }); - } - - handleOpenMerge() { - this.setState({ displayMerge: true }); - } - - handleOpenAddInContainer() { - this.setState({ displayAddInContainer: true }); - } - - handleOpenPromote() { - this.setState({ displayPromote: true }); - } - - handleClosePromote() { - this.setState({ displayPromote: false }); - } - - handleOpenEnrichment() { - // Get enrich type - let enrichType; - if (this.props.selectAll) { - enrichType = this.props.type - ?? R.head( - findFilterFromKey( - this.props.filters?.filters ?? [], - 'entity_type', - 'eq', - )?.values ?? [], - ); - } else { - const selected = this.props.selectedElements; - const selectedTypes = R.uniq( - R.map((o) => o.entity_type, R.values(selected || {})), - ); - enrichType = R.head(selectedTypes); - } - // Get available connectors - fetchQuery(toolBarConnectorsQuery, { type: enrichType }) - .toPromise() - .then((data) => { - this.setState({ - displayEnrichment: true, - enrichConnectors: data.enrichmentConnectors ?? [], - enrichSelected: [], - }); - }); - } - - handleCloseEnrichment() { - this.setState({ displayEnrichment: false }); - } - - handleCloseMerge() { - this.setState({ displayMerge: false }); - } - - handleAddStep() { - this.setState({ actionsInputs: R.append({}, this.state.actionsInputs) }); - } - - handleRemoveStep(i) { - const { actionsInputs } = this.state; - actionsInputs.splice(i, 1); - this.setState({ actionsInputs }); - } - - handleLaunchUpdate() { - const { actionsInputs } = this.state; - const actions = R.map( - (n) => ({ - type: n.type, - context: { - field: n.field, - type: n.fieldType, - values: n.values, - options: n.options, - }, - }), - actionsInputs, - ); - this.setState({ actions }, () => { - this.handleCloseUpdate(); - this.handleOpenTask(); - }); - } - - handleChangeActionInput(i, key, event) { - const { value } = event.target; - const { actionsInputs } = this.state; - actionsInputs[i] = R.assoc(key, value, actionsInputs[i] || {}); - if (key === 'field') { - const values = []; - actionsInputs[i] = R.assoc('values', values, actionsInputs[i] || {}); - if ( - value === 'object-marking' - || value === 'object-label' - || value === 'created-by' - || value === 'external-reference' - ) { - actionsInputs[i] = R.assoc( - 'fieldType', - 'RELATION', - actionsInputs[i] || {}, - ); - } else { - actionsInputs[i] = R.assoc( - 'fieldType', - 'ATTRIBUTE', - actionsInputs[i] || {}, - ); - } - } - this.setState({ actionsInputs }); - } - - handleChangeActionInputValues(i, event, value) { - if (event) { - event.stopPropagation(); - event.preventDefault(); - } - const { actionsInputs } = this.state; - actionsInputs[i] = R.assoc( - 'values', - Array.isArray(value) ? value : [value], - actionsInputs[i] || {}, - ); - this.setState({ actionsInputs }); - } - - handleChangeActionInputOptions(i, key, event) { - const { actionsInputs } = this.state; - actionsInputs[i] = R.assoc( - 'options', - R.assoc(key, event.target.checked, actionsInputs[i]?.options || {}), - actionsInputs[i] || {}, - ); - this.setState({ actionsInputs }); - } - - handleChangeActionInputValuesReplace(i, event) { - const { value } = event.target; - const { actionsInputs } = this.state; - actionsInputs[i] = R.assoc( - 'values', - Array.isArray(value) ? value : [value], - actionsInputs[i] || {}, - ); - this.setState({ actionsInputs }); - } - - handleLaunchDelete() { - const actions = [{ type: 'DELETE', context: null }]; - this.setState({ actions }, () => { - this.handleOpenTask(); - }); - } - - handleLaunchRemove() { - const actions = [ - { - type: 'REMOVE', - context: { field: 'container-object', values: [this.props.container] }, - }, - ]; - this.setState({ actions }, () => { - this.handleOpenTask(); - }); - } - - handleLaunchCompleteDelete() { - const actions = [{ type: 'COMPLETE_DELETE', context: null }]; - this.setState({ actions }, () => { - this.handleOpenTask(); - }); - } - - handleLaunchRestore() { - const actions = [{ type: 'RESTORE', context: null }]; - this.setState({ actions }, () => { - this.handleOpenTask(); - }); - } - - handleChangeKeptEntityId(entityId) { - this.setState({ keptEntityId: entityId }); - } - - handleChangeEnrichSelected(connectorId) { - if (this.state.enrichSelected.includes(connectorId)) { - const filtered = this.state.enrichSelected.filter( - (e) => e !== connectorId, - ); - this.setState({ enrichSelected: filtered }); - } else { - this.setState({ - enrichSelected: [...this.state.enrichSelected, connectorId], - }); - } - } - - handleLaunchRescan() { - const actions = [{ type: 'RULE_ELEMENT_RESCAN' }]; - this.setState({ actions }, () => { - this.handleCloseRescan(); - this.handleOpenTask(); - }); - } - - handleLaunchPromote() { - const actions = [{ type: 'PROMOTE' }]; - this.setState({ actions }, () => { - this.handleClosePromote(); - this.handleOpenTask(); - }); - } - - handleLaunchEnrichment() { - const actions = [ - { type: 'ENRICHMENT', context: { values: this.state.enrichSelected } }, - ]; - this.setState({ actions }, () => { - this.handleCloseEnrichment(); - this.handleOpenTask(); - }); - } - - handleLaunchMerge() { - const { selectedElements } = this.props; - const { keptEntityId } = this.state; - const selectedElementsList = R.values(selectedElements); - const keptElement = keptEntityId - ? R.head(R.filter((n) => n.id === keptEntityId, selectedElementsList)) - : R.head(selectedElementsList); - const filteredStixDomainObjects = keptEntityId - ? R.filter((n) => n.id !== keptEntityId, selectedElementsList) - : R.tail(selectedElementsList); - let scope = 'KNOWLEDGE'; - if ( - selectedElementsList.some( - ({ entity_type }) => entity_type === 'Vocabulary', - ) - ) { - scope = 'SETTINGS'; - } - const actions = [ - { - type: 'MERGE', - context: { values: filteredStixDomainObjects }, - }, - ]; - this.setState({ scope, actions, mergingElement: keptElement }, () => { - this.handleCloseMerge(); - this.handleOpenTask(); - }); - } - - titleCopy() { - const { t } = this.props; - if (this.props.numberOfSelectedElements > maxNumberOfObservablesToCopy) { - return `${ - t( - 'Copy disabled: too many selected elements (maximum number of elements for a copy: ', - ) + maxNumberOfObservablesToCopy - })`; - } - return t('Copy'); - } - - submitTask(availableFilterKeys) { - this.setState({ processing: true }); - const { actions, mergingElement, scope } = this.state; - const { - filters, - search, - selectAll, - selectedElements, - deSelectedElements, - numberOfSelectedElements, - handleClearSelectedElements, - t, - } = this.props; - if (numberOfSelectedElements === 0) return; - const jsonFilters = serializeFilterGroupForBackend( - removeIdAndIncorrectKeysFromFilterGroupObject(filters, availableFilterKeys), - ); - const finalActions = R.map( - (n) => ({ - type: n.type, - context: n.context - ? { - ...n.context, - values: R.map((o) => o.id || o.value || o, n.context.values), - } - : null, - }), - actions, - ); - if (selectAll) { - commitMutation({ - mutation: toolBarQueryTaskAddMutation, - variables: { - input: { - filters: jsonFilters, - search, - actions: finalActions, - excluded_ids: Object.keys(deSelectedElements || {}), - scope: scope ?? 'KNOWLEDGE', - }, - }, - onCompleted: () => { - handleClearSelectedElements(); - MESSAGING$.notifySuccess( - - {t( - 'The background task has been executed. You can monitor it on', - )}{' '} - - {t('the dedicated page')} - - . - , - ); - this.setState({ processing: false }); - this.handleCloseTask(); - }, - }); - } else { - commitMutation({ - mutation: toolBarListTaskAddMutation, - variables: { - input: { - ids: mergingElement - ? [mergingElement.id] - : Object.keys(selectedElements), - actions: finalActions, - scope: scope ?? 'KNOWLEDGE', - }, - }, - onCompleted: () => { - handleClearSelectedElements(); - MESSAGING$.notifySuccess( - - {t( - 'The background task has been executed. You can monitor it on', - )}{' '} - - {t('the dedicated page')} - - . - , - ); - this.setState({ processing: false }); - this.handleCloseTask(); - }, - }); - } - } - - renderFieldOptions(i) { - const { t } = this.props; - const { actionsInputs } = this.state; - const disabled = R.isNil(actionsInputs[i]?.type) || R.isEmpty(actionsInputs[i]?.type); - let options = []; - if (actionsInputs[i]?.type === 'ADD') { - options = [ - { label: t('Marking definitions'), value: 'object-marking' }, - { label: t('Labels'), value: 'object-label' }, - { label: t('External references'), value: 'external-reference' }, - { label: t('In containers'), value: 'container-object' }, - ]; - } else if (actionsInputs[i]?.type === 'REPLACE') { - options = [ - { label: t('Marking definitions'), value: 'object-marking' }, - { label: t('Labels'), value: 'object-label' }, - { label: t('Author'), value: 'created-by' }, - { label: t('Score'), value: 'x_opencti_score' }, - { label: t('Confidence'), value: 'confidence' }, - { label: t('Description'), value: 'description' }, - ]; - if (this.props.type) { - options.push({ label: t('Status'), value: 'x_opencti_workflow_id' }); - } - } else if (actionsInputs[i]?.type === 'REMOVE') { - options = [ - { label: t('Marking definitions'), value: 'object-marking' }, - { label: t('Labels'), value: 'object-label' }, - { label: t('External references'), value: 'external-reference' }, - ]; - } - return ( - - ); - } - - searchContainers(i, event, newValue) { - if (!event) return; - const { actionsInputs } = this.state; - actionsInputs[i] = R.assoc( - 'inputValue', - newValue && newValue.length > 0 ? newValue : '', - actionsInputs[i], - ); - this.setState({ actionsInputs }); - fetchQuery(toolBarContainersQuery, { - search: newValue && newValue.length > 0 ? newValue : '', - }) - .toPromise() - .then((data) => { - const elements = data.containers.edges.map((e) => e.node); - const containers = elements - .map((n) => ({ - label: n.representative.main, - type: n.entity_type, - value: n.id, - })) - .sort((a, b) => a.label.localeCompare(b.label)) - .sort((a, b) => a.type.localeCompare(b.type)); - this.setState({ containers }); - }); - } - - searchMarkingDefinitions(i, event, newValue) { - if (!event) return; - const { actionsInputs } = this.state; - actionsInputs[i] = R.assoc( - 'inputValue', - newValue && newValue.length > 0 ? newValue : '', - actionsInputs[i], - ); - this.setState({ actionsInputs }); - fetchQuery(objectMarkingFieldAllowedMarkingsQuery) - .toPromise() - .then((data) => { - const markingDefinitions = (data?.me?.allowed_marking ?? []) - .map((n) => ({ - label: n.definition, - value: n.id, - color: n.x_opencti_color, - })) - .sort((a, b) => a.label.localeCompare(b.label)); - this.setState({ markingDefinitions }); - }); - } - - searchLabels(i, event, newValue) { - if (!event) return; - const { actionsInputs } = this.state; - actionsInputs[i] = R.assoc( - 'inputValue', - newValue && newValue.length > 0 ? newValue : '', - actionsInputs[i], - ); - this.setState({ actionsInputs }); - fetchQuery(labelsSearchQuery, { - search: newValue && newValue.length > 0 ? newValue : '', - }) - .toPromise() - .then((data) => { - const labels = (data?.labels?.edges ?? []) - .map((n) => ({ - label: n.node.value, - value: n.node.id, - color: n.node.color, - })) - .sort((a, b) => a.label.localeCompare(b.label)); - this.setState({ - labels: R.union(this.state.labels, labels), - }); - }); - } - - searchExternalReferences(i, event, newValue) { - if (!event) return; - const { actionsInputs } = this.state; - actionsInputs[i] = R.assoc( - 'inputValue', - newValue && newValue.length > 0 ? newValue : '', - actionsInputs[i], - ); - this.setState({ actionsInputs }); - fetchQuery(externalReferencesQueriesSearchQuery, { - search: newValue && newValue.length > 0 ? newValue : '', - }) - .toPromise() - .then((data) => { - const externalReferences = (data?.externalReferences?.edges ?? []) - .map((n) => ({ - label: `[${n.node.source_name}] ${truncate( - n.node.description || n.node.external_id, - 150, - )} ${n.node.url && `(${n.node.url})`}`, - value: n.node.id, - })) - .sort((a, b) => a.label.localeCompare(b.label)); - this.setState({ - externalReferences: R.union( - this.state.externalReferences, - externalReferences, - ), - }); - }); - } - - searchIdentities(i, event, newValue) { - if (!event) return; - const { actionsInputs } = this.state; - actionsInputs[i] = R.assoc( - 'inputValue', - newValue && newValue.length > 0 ? newValue : '', - actionsInputs[i], - ); - this.setState({ actionsInputs }); - fetchQuery(identitySearchIdentitiesSearchQuery, { - types: ['Individual', 'Organization', 'System'], - search: newValue && newValue.length > 0 ? newValue : '', - first: 100, - }) - .toPromise() - .then((data) => { - const identities = (data?.identities?.edges ?? []) - .map((n) => ({ - label: n.node.name, - value: n.node.id, - type: n.node.entity_type, - })) - .sort((a, b) => a.label.localeCompare(b.label)) - .sort((a, b) => a.type.localeCompare(b.type)); - this.setState({ - identities: R.union(this.state.identities, identities), - }); - }); - } - - searchStatuses(i, event, newValue) { - if (!event) return; - const { actionsInputs } = this.state; - actionsInputs[i] = R.assoc( - 'inputValue', - newValue && newValue.length > 0 ? newValue : '', - actionsInputs[i], - ); - this.setState({ actionsInputs }); - fetchQuery(statusFieldStatusesSearchQuery, { - first: 100, - filters: { - mode: 'and', - filterGroups: [], - filters: [{ key: 'type', values: [this.props.type] }], - }, - orderBy: 'order', - orderMode: 'asc', - search: newValue && newValue.length > 0 ? newValue : '', - }) - .toPromise() - .then((data) => { - const statuses = (data?.statuses?.edges ?? []) - .map((n) => ({ - label: n.node.template.name, - value: n.node.id, - order: n.node.order, - color: n.node.template.color, - })) - .sort((a, b) => a.label.localeCompare(b.label)) - .sort((a, b) => a.order - b.order); - this.setState({ statuses: R.union(this.state.statuses, statuses) }); - }); - } - - renderValuesOptions(i) { - const { t, classes } = this.props; - const { actionsInputs } = this.state; - const disabled = R.isNil(actionsInputs[i]?.field) || R.isEmpty(actionsInputs[i]?.field); - switch (actionsInputs[i]?.field) { - case 'container-object': - return ( - <> - this.setState({ containerCreation: false })} - creationCallback={(data) => { - const element = { - label: data.name, - value: data.id, - type: data.entity_type, - }; - this.setState(({ containers }) => ({ - containers: [...(containers ?? []), element], - })); - this.handleChangeActionInputValues(i, null, [ - ...(actionsInputs[i]?.values ?? []), - element, - ]); - }} - /> - option.label ?? ''} - value={actionsInputs[i]?.values || []} - multiple={true} - renderInput={(params) => ( - - )} - noOptionsText={t('No available options')} - options={this.state.containers} - onInputChange={this.searchContainers.bind(this, i)} - inputValue={actionsInputs[i]?.inputValue || ''} - onChange={this.handleChangeActionInputValues.bind(this, i)} - renderOption={(props, option) => ( -
  • -
    - -
    -
    {option.label}
    -
  • - )} - /> - this.setState({ containerCreation: true })} - edge="end" - style={{ position: 'absolute', top: 22, right: 48 }} - size="large" - > - - - - ); - case 'object-marking': - return ( - (option.label ? option.label : '')} - value={actionsInputs[i]?.values || []} - multiple={true} - renderInput={(params) => ( - - )} - noOptionsText={t('No available options')} - options={this.state.markingDefinitions} - onInputChange={this.searchMarkingDefinitions.bind(this, i)} - inputValue={actionsInputs[i]?.inputValue || ''} - onChange={this.handleChangeActionInputValues.bind(this, i)} - renderOption={(props, option) => ( -
  • -
    - -
    -
    {option.label}
    -
  • - )} - /> - ); - case 'object-label': - return ( - (option.label ? option.label : '')} - value={actionsInputs[i]?.values || []} - multiple={true} - renderInput={(params) => ( - - )} - noOptionsText={t('No available options')} - options={this.state.labels} - onInputChange={this.searchLabels.bind(this, i)} - inputValue={actionsInputs[i]?.inputValue || ''} - onChange={this.handleChangeActionInputValues.bind(this, i)} - renderOption={(props, option) => ( -
  • -
    - -
    -
    {option.label}
    -
  • - )} - /> - ); - case 'created-by': - return ( - (option.label ? option.label : '')} - value={actionsInputs[i]?.values || []} - renderInput={(params) => ( - - )} - noOptionsText={t('No available options')} - options={this.state.identities} - onInputChange={this.searchIdentities.bind(this, i)} - inputValue={actionsInputs[i]?.inputValue || ''} - onChange={this.handleChangeActionInputValues.bind(this, i)} - renderOption={(props, option) => ( -
  • -
    - -
    -
    {option.label}
    -
  • - )} - /> - ); - case 'x_opencti_workflow_id': - return ( - (option.label ? option.label : '')} - value={actionsInputs[i]?.values || []} - renderInput={(params) => ( - - )} - noOptionsText={t('No available options')} - options={this.state.statuses} - onInputChange={this.searchStatuses.bind(this, i)} - inputValue={actionsInputs[i]?.inputValue || ''} - onChange={this.handleChangeActionInputValues.bind(this, i)} - renderOption={(props, option) => ( -
  • -
    - - {option.order} - -
    -
    {option.label}
    -
  • - )} - /> - ); - case 'external-reference': - return ( - (option.label ? option.label : '')} - value={actionsInputs[i]?.values || []} - multiple={true} - renderInput={(params) => ( - - )} - noOptionsText={t('No available options')} - options={this.state.externalReferences} - onInputChange={this.searchExternalReferences.bind(this, i)} - inputValue={actionsInputs[i]?.inputValue || ''} - onChange={this.handleChangeActionInputValues.bind(this, i)} - renderOption={(props, option) => ( -
  • -
    - -
    -
    {option.label}
    -
  • - )} - /> - ); - case 'x_opencti_score': - case 'confidence': - return ( - - ); - default: - return ( - - ); - } - } - - areStepValid() { - const { actionsInputs } = this.state; - for (const n of actionsInputs) { - if (!n || !n.type || !n.field || !n.values || n.values.length === 0) { - return false; - } - } - return true; - } - render() { const { - t, - n, classes, numberOfSelectedElements, handleClearSelectedElements, selectedElements, selectAll, filters, - search, - theme, container, variant, - noAuthor, - noMarking, - noWarning, deleteDisable, mergeDisable, deleteOperationEnabled, warning, warningMessage, + type, + noAuthor, + noWarning, + noMarking, } = this.props; - const { actions, keptEntityId, mergingElement, actionsInputs, navOpen } = this.state; + const { navOpen } = this.state; const isOpen = numberOfSelectedElements > 0; - const selectedTypes = R.uniq( - R.map((o) => o.entity_type, R.values(selectedElements || {})), - ); - const typesAreDifferent = selectedTypes.length > 1; - const preventMerge = selectedTypes.at(0) === 'Vocabulary' - && Object.values(selectedElements).some(({ builtIn }) => Boolean(builtIn)); - // region update - const notUpdatableTypes = ['Label', 'Vocabulary', 'Case-Template', 'Task', 'DeleteOperation']; - const entityTypeFilterValues = findFilterFromKey(filters?.filters ?? [], 'entity_type', 'eq')?.values - ?? []; - const typesAreNotUpdatable = R.includes( - R.uniq( - R.map((o) => o.entity_type, R.values(selectedElements || {})), - )[0], - notUpdatableTypes, - ) - || (entityTypeFilterValues.length === 1 - && notUpdatableTypes.includes(entityTypeFilterValues[0])); - // endregion - // region rules - const notScannableTypes = ['Label', 'Vocabulary', 'Case-Template', 'Task', 'DeleteOperation']; - const typesAreNotScannable = R.includes( - R.uniq( - R.map((o) => o.entity_type, R.values(selectedElements || {})), - )[0], - notScannableTypes, - ) - || (entityTypeFilterValues.length === 1 - && notScannableTypes.includes(entityTypeFilterValues[0])); - // endregion - // region enrich - const notEnrichableTypes = ['Label', 'Vocabulary', 'Case-Template', 'Task', 'DeleteOperation']; - const isManualEnrichSelect = !selectAll && selectedTypes.length === 1; - const isAllEnrichSelect = selectAll - && entityTypeFilterValues.length === 1 - && R.head(entityTypeFilterValues) !== 'Stix-Cyber-Observable' - && R.head(entityTypeFilterValues) !== 'Stix-Domain-Object'; - const enrichDisable = notEnrichableTypes.includes(R.head(selectedTypes)) - || (entityTypeFilterValues.length === 1 - && notEnrichableTypes.includes(entityTypeFilterValues[0])) - || (!isManualEnrichSelect && !isAllEnrichSelect); - // endregion - const typesAreNotMergable = R.includes( - R.uniq(R.map((o) => o.entity_type, R.values(selectedElements || {})))[0], - notMergableTypes, - ); - const enableMerge = !typesAreNotMergable && !mergeDisable; - const notAddableTypes = ['Label', 'Vocabulary', 'Case-Template', 'DeleteOperation']; - const typesAreNotAddableInContainer = R.includes( - R.uniq( - R.map((o) => o.entity_type, R.values(selectedElements || {})), - )[0], - notAddableTypes, - ) - || (entityTypeFilterValues.length === 1 - && notScannableTypes.includes(entityTypeFilterValues[0])); - const selectedElementsList = R.values(selectedElements || {}); - const titleCopy = this.titleCopy(); - let keptElement = null; - let newAliases = []; - if (!typesAreNotMergable && !typesAreDifferent) { - keptElement = keptEntityId - ? R.head(selectedElementsList.filter((o) => o.id === keptEntityId)) - : R.head(selectedElementsList); - if (keptElement) { - const names = R.filter( - (o) => o !== keptElement.name, - R.pluck('name', selectedElementsList), - ); - const aliases = !R.isNil(keptElement.aliases) - ? R.filter( - (o) => !R.isNil(o), - R.flatten(R.pluck('aliases', selectedElementsList)), - ) - : R.filter( - (o) => !R.isNil(o), - R.flatten(R.pluck('x_opencti_aliases', selectedElementsList)), - ); - newAliases = R.filter( - (o) => o.length > 0, - R.uniq(R.concat(names, aliases)), - ); - } - } let paperClass; switch (variant) { case 'large': @@ -1339,962 +81,41 @@ class ToolBar extends Component { } return ( - {({ bannerSettings, schema }) => { - // region promote filters - const stixCyberObservableTypes = schema.scos.map((sco) => sco.id).concat('Stix-Cyber-Observable'); - const promotionTypes = stixCyberObservableTypes.concat(['Indicator']); - - const isOnlyStixCyberObservablesTypes = entityTypeFilterValues.length > 0 - && entityTypeFilterValues.every((id) => stixCyberObservableTypes.includes(id)); - - const promotionTypesFiltered = entityTypeFilterValues.length > 0 - && entityTypeFilterValues.every((id) => promotionTypes.includes(id)); - - const isManualPromoteSelect = !selectAll - && selectedTypes.length > 0 - && selectedTypes.every((type) => promotionTypes.includes(type)); - - const promoteEnabled = isManualPromoteSelect || promotionTypesFiltered; - - const entityTypes = selectedTypes.length > 0 ? selectedTypes : [this.props.type ?? 'Stix-Core-Object']; - const filterKeysMap = new Map(); - entityTypes.forEach((entityType) => { - const currentMap = schema.filterKeysSchema.get(entityType); - currentMap?.forEach((value, key) => filterKeysMap.set(key, value)); - }); - const availableFilterKeys = Array.from(filterKeysMap.keys()).concat(['entity_type']); - // endregion - return ( - - - - - {numberOfSelectedElements} - {' '} - {t('selected')}{' '} - - - - - - {!typesAreNotUpdatable && ( - - - - - - - - )} - - {({ platformModuleHelpers }) => { - const label = platformModuleHelpers.isRuleEngineEnable() - ? 'Rule rescan' - : 'Rule rescan (engine is disabled)'; - const buttonDisable = typesAreNotScannable - || !platformModuleHelpers.isRuleEngineEnable() - || numberOfSelectedElements === 0 - || this.state.processing; - return typesAreNotScannable ? undefined : ( - - - - - - - - ); - }} - - {this.props.handleCopy && ( - - - maxNumberOfObservablesToCopy - } - onClick={this.props.handleCopy} - color="primary" - size="small" - > - - - - - )} - {!enrichDisable && ( - - - - - - - - )} - {promoteEnabled && ( - - - - - - - - )} - {enableMerge && ( - - - 4 - || preventMerge - || selectAll - || this.state.processing - } - onClick={this.handleOpenMerge.bind(this)} - color="primary" - size="small" - > - - - - - )} - - {!typesAreNotAddableInContainer && ( - - - - - - - - - - )} - {container && ( - - - - - - - - - - )} - {deleteDisable !== true && ( - - - - - - - - - - )} - {deleteOperationEnabled && ( - - - - - - - - - - - - - - - - - )} - - - -
    - {t('Launch a background task')} -
    -
    - - {n(numberOfSelectedElements)} - {' '} - {t('selected element(s)')} -
    -
    - - {numberOfSelectedElements > 1000 && ( - - {t( - "You're targeting more than 1000 entities with this background task, be sure of what you're doing!", - )} - - )} - - - - - # - {t('Step')} - {t('Field')} - {t('Values')} - - - - - - {' '} - - 1 - - - - - - {t('N/A')} - - {selectAll ? ( -
    - {search && search.length > 0 && ( - - - {t('Search')}: {search} -
    - } - /> - {filters.filters.length > 0 && ( - - )} - - )} - - - ) : ( - - {mergingElement - ? truncate( - R.join(', ', [ - getMainRepresentative(mergingElement), - ]), - 80, - ) - : truncate( - R.join( - ', ', - R.map( - (o) => getMainRepresentative(o), - R.values(selectedElements || {}), - ), - ), - 80, - )} - - )} -
    -
    - {R.map((o) => { - const number = actions.indexOf(o); - return ( - - - {' '} - - {number + 2} - - - - - - - {R.pathOr(t('N/A'), ['context', 'field'], o)} - - - {truncate( - R.join( - ', ', - R.map( - (p) => (typeof p === 'string' - ? p - : getMainRepresentative(p)), - R.pathOr([], ['context', 'values'], o), - ), - ), - 80, - )} - - - ); - }, actions)} -
    -
    -
    -
    - - - - -
    - -
    - - - - {t('Update entities')} -
    -
    - {Array(actionsInputs.length) - .fill(0) - .map((_, i) => ( -
    - - - - - - - {t('Action type')} - - - - - - {t('Field')} - {this.renderFieldOptions(i)} - - - - {this.renderValuesOptions(i)} - - -
    - ))} -
    - -
    -
    - -
    -
    -
    - -
    - - - - {t('Merge entities')} -
    -
    - - {t('Selected entities')} - - - {selectedElementsList.map((element) => ( - - - - - -
    - {R.pathOr('', ['createdBy', 'name'], element)} -
    -
    - -
    - - - -
    - ))} -
    - - {t('Merged entity')} - - - {t('Name')} - -
    - {getMainRepresentative(keptElement)} -
    - - {t('Aliases')} - - {newAliases.map((label) => (label.length > 0 ? ( - - ) : ( - '' - )))} - {noAuthor !== true && ( - <> - - {t('Author')} - - {R.pathOr('', ['createdBy', 'name'], keptElement)} - - )} - {noMarking !== true && ( - <> - - {t('Marking')} - - - - )} - {noWarning !== true && ( - <> - - {t( - 'The relations attached to selected entities will be copied to the merged entity.', - )} - - - )} -
    - -
    -
    -
    - -
    - - - - {t('Entity enrichment')} -
    -
    - - {t('Selected connectors')} - - - {this.state.enrichConnectors.length === 0 && ( - - {t('No connector available for the selected entities.')} - - )} - {this.state.enrichConnectors.map((connector) => ( - - - - - - - - - - ))} - -
    - -
    -
    -
    - - -
    - - - - {t('Rule entity rescan')} -
    -
    - - {t('Selected rules')} - - - {t( - 'Element will be rescan with all compatible activated rules', - )} - -
    - -
    -
    -
    - this.setState({ displayAddInContainer: false })} - > - {t('Add in container')} - - this.setState({ containerCreation: false }) - } - creationCallback={(data) => { - const element = { - label: data.name, - value: data.id, - type: data.entity_type, - }; - this.setState(({ containers }) => ({ - containers: [...(containers ?? []), element], - })); - this.handleChangeActionInputValues(0, null, [ - ...(actionsInputs[0]?.values ?? []), - element, - ]); - }} - /> - (option.label ? option.label : '') - } - value={actionsInputs[0]?.values || []} - multiple={true} - renderInput={(params) => ( - - )} - noOptionsText={t('No available options')} - options={this.state.containers} - onInputChange={this.searchContainers.bind(this, 0)} - inputValue={actionsInputs[0]?.inputValue || ''} - onChange={this.handleChangeActionInputValues.bind(this, 0)} - renderOption={(props, option) => ( -
  • -
    - -
    -
    {option.label}
    -
  • - )} - disableClearable - /> - - } - label={t('Also include first neighbours')} - /> - this.setState({ containerCreation: true })} - edge="end" - style={{ position: 'absolute', top: 68, right: 48 }} - size="large" - > - - -
    - - - - -
    -
    - ); - }} + {({ bannerSettings }) => ( + + + + )}
    ); } @@ -2302,23 +123,25 @@ class ToolBar extends Component { ToolBar.propTypes = { classes: PropTypes.object, - theme: PropTypes.object, - t: PropTypes.func, numberOfSelectedElements: PropTypes.number, + handleClearSelectedElements: PropTypes.func, selectedElements: PropTypes.object, - deSelectedElements: PropTypes.object, selectAll: PropTypes.bool, filters: PropTypes.object, - search: PropTypes.string, - handleClearSelectedElements: PropTypes.func, - variant: PropTypes.string, container: PropTypes.object, + variant: PropTypes.string, + deleteDisable: PropTypes.bool, type: PropTypes.string, - handleCopy: PropTypes.func, warning: PropTypes.bool, warningMessage: PropTypes.string, - rightOffset: PropTypes.number, + deSelectedElements: PropTypes.object, + search: PropTypes.string, + handleCopy: PropTypes.func, + noAuthor: PropTypes.bool, + noMarking: PropTypes.bool, + noWarning: PropTypes.bool, + mergeDisable: PropTypes.bool, deleteOperationEnabled: PropTypes.bool, }; -export default R.compose(inject18n, withTheme, withStyles(styles))(ToolBar); +export default R.compose(withStyles(styles))(ToolBar); diff --git a/opencti-platform/opencti-front/src/private/components/nav/LeftBar.jsx b/opencti-platform/opencti-front/src/private/components/nav/LeftBar.jsx index b1494da1efb42..9600618bc93fe 100644 --- a/opencti-platform/opencti-front/src/private/components/nav/LeftBar.jsx +++ b/opencti-platform/opencti-front/src/private/components/nav/LeftBar.jsx @@ -1,7 +1,6 @@ import React, { useRef, useState } from 'react'; import { Link, useNavigate, useLocation } from 'react-router-dom'; import { createStyles, makeStyles, styled, useTheme } from '@mui/styles'; -import Toolbar from '@mui/material/Toolbar'; import MenuList from '@mui/material/MenuList'; import MenuItem from '@mui/material/MenuItem'; import ListItemIcon from '@mui/material/ListItemIcon'; @@ -102,21 +101,22 @@ import logoFiligranTextLight from '../../../static/images/logo_filigran_text_lig import useEnterpriseEdition from '../../../utils/hooks/useEnterpriseEdition'; import useDimensions from '../../../utils/hooks/useDimensions'; +const SMALL_BAR_WIDTH = 55; +const OPEN_BAR_WIDTH = 180; + // Deprecated - https://mui.com/system/styles/basics/ // Do not use it for new code. const useStyles = makeStyles((theme) => createStyles({ drawerPaper: { - width: 55, + width: SMALL_BAR_WIDTH, minHeight: '100vh', - background: 0, - backgroundColor: theme.palette.background.nav, + background: 'none', overflowX: 'hidden', }, drawerPaperOpen: { - width: 180, + width: OPEN_BAR_WIDTH, minHeight: '100vh', - background: 0, - backgroundColor: theme.palette.background.nav, + background: 'none', overflowX: 'hidden', }, menuItemIcon: { @@ -153,25 +153,25 @@ const useStyles = makeStyles((theme) => createStyles({ fontSize: 12, }, menuCollapseOpen: { - width: 180, + width: OPEN_BAR_WIDTH, height: 35, fontWeight: 500, fontSize: 14, }, menuCollapse: { - width: 55, + width: SMALL_BAR_WIDTH, height: 35, fontWeight: 500, fontSize: 14, }, menuLogoOpen: { - width: 180, + width: OPEN_BAR_WIDTH, height: 35, fontWeight: 500, fontSize: 14, }, menuLogo: { - width: 55, + width: SMALL_BAR_WIDTH, height: 35, fontWeight: 500, fontSize: 14, @@ -244,6 +244,7 @@ const LeftBar = () => { const handleToggle = () => { setSelectedMenu([]); localStorage.setItem('navOpen', String(!navOpen)); + window.dispatchEvent(new StorageEvent('storage', { key: 'navOpen' })); localStorage.setItem('selectedMenu', JSON.stringify([])); setNavOpen(!navOpen); MESSAGING$.toggleNav.next('toggle'); @@ -333,6 +334,7 @@ const LeftBar = () => { } = useAuth(); const settingsMessagesBannerHeight = useSettingsMessagesBannerHeight(); const { dimension } = useDimensions(); + const isMobile = dimension.width < 768; const generateSubMenu = (menu, entries) => { return navOpen ? ( @@ -421,18 +423,21 @@ const LeftBar = () => { paper: navOpen ? classes.drawerPaperOpen : classes.drawerPaper, }} sx={{ - width: navOpen ? 180 : 55, + width: navOpen ? OPEN_BAR_WIDTH : SMALL_BAR_WIDTH, + background: theme.palette.background.gradient, + position: 'sticky', + top: 0, + height: '100vh', transition: theme.transitions.create('width', { easing: theme.transitions.easing.easeInOut, duration: theme.transitions.duration.enteringScreen, }), }} > -
    ((theme) => ({ marginRight: 4, }, menuContainer: { - width: '50%', - float: 'left', + width: '30%', }, barRight: { position: 'absolute', top: 0, right: 13, height: '100%', + display: 'flex', + alignItems: 'center', }, barRightContainer: { float: 'left', - height: '100%', - paddingTop: 12, }, subtitle: { color: theme.palette.text?.secondary, diff --git a/opencti-platform/opencti-front/src/private/components/observations/indicators/IndicatorObservablePopover.jsx b/opencti-platform/opencti-front/src/private/components/observations/indicators/IndicatorObservablePopover.jsx index e4682dd21eb93..0fdba2b2e1dbd 100644 --- a/opencti-platform/opencti-front/src/private/components/observations/indicators/IndicatorObservablePopover.jsx +++ b/opencti-platform/opencti-front/src/private/components/observations/indicators/IndicatorObservablePopover.jsx @@ -90,7 +90,7 @@ class IndicatorObservablePopover extends Component { toId: this.props.observableId, relationship_type: 'based-on', }, - updater: (store) => deleteNodeFromEdge(store, 'observables', this.props.indicatorId, this.props.observableId, { first: 200 }), + updater: (store) => deleteNodeFromEdge(store, 'observables', this.props.indicatorId, this.props.observableId, { first: 100 }), onCompleted: () => { this.handleCloseDelete(); if (this.props.onDelete) { diff --git a/opencti-platform/opencti-front/src/private/components/observations/indicators/IndicatorObservables.jsx b/opencti-platform/opencti-front/src/private/components/observations/indicators/IndicatorObservables.jsx index eae176154a65b..88638ad8c26f2 100644 --- a/opencti-platform/opencti-front/src/private/components/observations/indicators/IndicatorObservables.jsx +++ b/opencti-platform/opencti-front/src/private/components/observations/indicators/IndicatorObservables.jsx @@ -1,198 +1,67 @@ -import React, { Component } from 'react'; -import * as PropTypes from 'prop-types'; -import { compose, includes, append } from 'ramda'; -import { graphql, createFragmentContainer } from 'react-relay'; -import { Link } from 'react-router-dom'; -import withStyles from '@mui/styles/withStyles'; -import List from '@mui/material/List'; -import ListItem from '@mui/material/ListItem'; -import ListItemIcon from '@mui/material/ListItemIcon'; -import ListItemText from '@mui/material/ListItemText'; -import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction'; +import React, { useState } from 'react'; +import { createFragmentContainer, graphql } from 'react-relay'; import Typography from '@mui/material/Typography'; -import Chip from '@mui/material/Chip'; -import inject18n from '../../../../components/i18n'; -import ItemIcon from '../../../../components/ItemIcon'; +import { useFormatter } from '../../../../components/i18n'; import IndicatorAddObservables from './IndicatorAddObservables'; import IndicatorObservablePopover from './IndicatorObservablePopover'; import Security from '../../../../utils/Security'; import { KNOWLEDGE_KNUPDATE } from '../../../../utils/hooks/useGranted'; -import { hexToRGB, itemColor } from '../../../../utils/Colors'; +import DataTableWithoutFragment from '../../../../components/dataGrid/DataTableWithoutFragment'; -const styles = (theme) => ({ - itemHead: { - paddingLeft: 10, - textTransform: 'uppercase', - cursor: 'pointer', - }, - item: { - paddingLeft: 10, - height: 50, - }, - bodyItem: { - height: '100%', - fontSize: 13, - }, - itemIcon: { - color: theme.palette.primary.main, - }, - goIcon: { - position: 'absolute', - right: -10, - }, - inputLabel: { - float: 'left', - }, - sortIcon: { - float: 'left', - margin: '-5px 0 0 15px', - }, - chipInList: { - fontSize: 12, - height: 20, - float: 'left', - width: 120, - textTransform: 'uppercase', - borderRadius: 4, - }, -}); +const IndicatorObservablesComponent = ({ indicator }) => { + const [deleted, setDeleted] = useState([]); + const { t_i18n } = useFormatter(); + const [ref, setRef] = useState(null); -const inlineStyles = { - entity_type: { - float: 'left', - width: '20%', - height: 20, - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - }, - observable_value: { - float: 'left', - width: '50%', - height: 20, - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - }, - created_at: { - float: 'left', - height: 20, - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - }, -}; - -class IndicatorObservablesComponent extends Component { - constructor(props) { - super(props); - this.state = { deleted: [] }; - } + const onDelete = (id) => { + setDeleted([id, ...deleted]); + }; - onDelete(id) { - this.setState({ deleted: append(id, this.state.deleted) }); - } + const observables = indicator.observables.edges.filter((e) => !deleted.includes(e.node.id)) + .map((e) => e.node); + const observablesGlobalCount = indicator.observables.pageInfo.globalCount; - render() { - const { t, fd, classes, indicator } = this.props; - - const observables = indicator.observables.edges.filter((e) => !includes(e.node.id, this.state.deleted)) - .map((e) => e.node); - const observablesGlobalCount = indicator.observables.pageInfo.globalCount; - - return ( -
    - - {t('Based on')} - - - setRef(r)}> + + {t_i18n('Based on')} + + + + +
    + ( + onDelete(observable.id)} /> - -
    - - {observables.map((observable) => ( - - - - - -
    - -
    -
    - {observable.observable_value} -
    -
    - {fd(observable.created_at)} -
    -
    - } - /> - - - - - ))} - - { - observablesGlobalCount > 25 - && - {'See more...'} - - } -
    - ); - } -} - -IndicatorObservablesComponent.propTypes = { - indicator: PropTypes.object, - classes: PropTypes.object, - t: PropTypes.func, - fd: PropTypes.func, - navigate: PropTypes.func, + )} + /> +
    + ); }; const IndicatorObservables = createFragmentContainer( @@ -204,7 +73,7 @@ const IndicatorObservables = createFragmentContainer( name parent_types entity_type - observables(first: 25) { + observables(first: 100) { edges { node { id @@ -224,4 +93,4 @@ const IndicatorObservables = createFragmentContainer( }, ); -export default compose(inject18n, withStyles(styles))(IndicatorObservables); +export default IndicatorObservables; diff --git a/opencti-platform/opencti-front/src/private/components/observations/indicators/Root.jsx b/opencti-platform/opencti-front/src/private/components/observations/indicators/Root.jsx index a197f9fe51a45..429cbf7e3390e 100644 --- a/opencti-platform/opencti-front/src/private/components/observations/indicators/Root.jsx +++ b/opencti-platform/opencti-front/src/private/components/observations/indicators/Root.jsx @@ -88,7 +88,7 @@ class RootIndicator extends Component { <> { if (props) { if (props.indicator) { diff --git a/opencti-platform/opencti-front/src/private/components/settings/settings_messages/SettingsMessagesBanner.tsx b/opencti-platform/opencti-front/src/private/components/settings/settings_messages/SettingsMessagesBanner.tsx index 5ec253b021b60..bf7301f879b64 100644 --- a/opencti-platform/opencti-front/src/private/components/settings/settings_messages/SettingsMessagesBanner.tsx +++ b/opencti-platform/opencti-front/src/private/components/settings/settings_messages/SettingsMessagesBanner.tsx @@ -11,6 +11,7 @@ import useLocalStorage from '../../../../utils/hooks/useLocalStorage'; import useQueryLoading from '../../../../utils/hooks/useQueryLoading'; import { SettingsMessagesBannerQuery } from './__generated__/SettingsMessagesBannerQuery.graphql'; import { MessageFromLocalStorage } from '../../../../utils/hooks/useLocalStorageModel'; +import { isEmptyField } from '../../../../utils/utils'; export const settingsMessagesQuery = graphql` query SettingsMessagesBannerQuery { @@ -53,7 +54,7 @@ const useStyles = makeStyles((theme) => ({ borderLeft: '8px solid #ffc107', color: 'black', width: '100%', - padding: theme.spacing(1), + padding: 4, }, message: { textAlign: 'center', @@ -64,7 +65,7 @@ const useStyles = makeStyles((theme) => ({ button: { color: '#663c00', position: 'absolute', - top: '5px', + top: '1px', right: '8px', }, })); @@ -102,8 +103,8 @@ const extractMessagesToDisplay = ( const ref = React.createRef(); export const useSettingsMessagesBannerHeight = () => { - const [bannerHeight, setBannerHeight] = useState( - ref.current?.clientHeight ?? 0, + const [bannerHeight, setBannerHeight] = useState( + ref.current?.clientHeight as number ?? 0, ); useBus( BANNER_LOCAL_STORAGE_KEY, @@ -116,9 +117,9 @@ export const useSettingsMessagesBannerHeight = () => { ); // At first render, some component might have finished their render while settings message send the dispatch. if (bannerHeight !== ref.current?.clientHeight && ref.current?.clientHeight != null) { - setBannerHeight(ref.current?.clientHeight); + setBannerHeight(ref.current?.clientHeight as number); } - return bannerHeight; + return isEmptyField(bannerHeight) ? 0 : bannerHeight; }; // -- FUNCTION COMPONENT -- diff --git a/opencti-platform/opencti-front/src/private/components/settings/sub_types/SubTypesLines.tsx b/opencti-platform/opencti-front/src/private/components/settings/sub_types/SubTypesLines.tsx index f7586df906a60..4f3dc4f408ea9 100644 --- a/opencti-platform/opencti-front/src/private/components/settings/sub_types/SubTypesLines.tsx +++ b/opencti-platform/opencti-front/src/private/components/settings/sub_types/SubTypesLines.tsx @@ -81,7 +81,7 @@ const SubTypesLines: FunctionComponent = ({ .filter(filterOnSubType) .sort(sortOnSubType); setNumberOfElements({ - number: subTypes.length.toString(), + number: subTypes.length, symbol: '', original: subTypes.length, }); diff --git a/opencti-platform/opencti-front/src/static/css/index.css b/opencti-platform/opencti-front/src/static/css/index.css index 8e68edc54fa6d..d8aa1cc5a3932 100644 --- a/opencti-platform/opencti-front/src/static/css/index.css +++ b/opencti-platform/opencti-front/src/static/css/index.css @@ -2,6 +2,10 @@ scrollbar-width: thin; } +#root{ + height: 100vh; +} + :focus { outline: 0; } diff --git a/opencti-platform/opencti-front/src/utils/Entity.ts b/opencti-platform/opencti-front/src/utils/Entity.ts index 1abc432e2de4d..23c65dca4fc87 100644 --- a/opencti-platform/opencti-front/src/utils/Entity.ts +++ b/opencti-platform/opencti-front/src/utils/Entity.ts @@ -66,6 +66,8 @@ export const resolveLink = (type = 'unknown'): string | null => { return '/dashboard/arsenal/vulnerabilities'; case 'Incident': return '/dashboard/events/incidents'; + case 'stix-sighting-relationship': + return '/dashboard/events/sightings'; case 'Artifact': return '/dashboard/observations/artifacts'; case 'Data-Component': diff --git a/opencti-platform/opencti-front/src/utils/ExportContextProvider.tsx b/opencti-platform/opencti-front/src/utils/ExportContextProvider.tsx index 08baa69a43c2f..e84679fd91cbd 100644 --- a/opencti-platform/opencti-front/src/utils/ExportContextProvider.tsx +++ b/opencti-platform/opencti-front/src/utils/ExportContextProvider.tsx @@ -1,8 +1,8 @@ import React, { Dispatch, ReactNode, useState } from 'react'; export interface ExportContextType { - selectedIds: string[]; - setSelectedIds: Dispatch; + selectedIds: string[] + setSelectedIds?: Dispatch } const defaultContext = { diff --git a/opencti-platform/opencti-front/src/utils/Number.js b/opencti-platform/opencti-front/src/utils/Number.js index da183988e22a2..89c811f5a7595 100644 --- a/opencti-platform/opencti-front/src/utils/Number.js +++ b/opencti-platform/opencti-front/src/utils/Number.js @@ -20,7 +20,7 @@ export const numberFormat = (number, digits = 2) => { } } return { - number: (number / si[i].value).toFixed(digits).replace(rx, '$1'), + number: Number.parseInt((number / si[i].value).toFixed(digits).replace(rx, '$1'), 10), symbol: si[i].symbol, original: number, }; diff --git a/opencti-platform/opencti-front/src/utils/hooks/useBus.ts b/opencti-platform/opencti-front/src/utils/hooks/useBus.ts index 7ef7ef2da8e78..04a9fa7d2d37c 100644 --- a/opencti-platform/opencti-front/src/utils/hooks/useBus.ts +++ b/opencti-platform/opencti-front/src/utils/hooks/useBus.ts @@ -8,13 +8,17 @@ const subscribe = (channel: string, callback: useBusCallback) => { if (!channel || !callback) { return undefined; } - subscribers.push([channel, callback]); + subscribers = [ + ...subscribers, + [channel, callback], + ]; + return () => { subscribers = subscribers.filter((subscriber) => subscriber[1] !== callback); }; }; -export const dispatch = (channel: string, event: any) => { +export const dispatch = (channel: string, event?: any) => { subscribers.filter(([filter]) => filter === channel) .forEach(([_, callback]) => { callback(event); diff --git a/opencti-platform/opencti-front/src/utils/hooks/useEntityToggle.ts b/opencti-platform/opencti-front/src/utils/hooks/useEntityToggle.ts index e79858d83268f..0d9c0ee850633 100644 --- a/opencti-platform/opencti-front/src/utils/hooks/useEntityToggle.ts +++ b/opencti-platform/opencti-front/src/utils/hooks/useEntityToggle.ts @@ -1,5 +1,6 @@ -import React, { useState } from 'react'; +import React, { useCallback, useState } from 'react'; import * as R from 'ramda'; +import useBus from './useBus'; export interface UseEntityToggle { selectedElements: Record; @@ -19,16 +20,30 @@ export interface UseEntityToggle { const useEntityToggle = ( key: string, ): UseEntityToggle => { - const { numberOfElements } = JSON.parse( - window.localStorage.getItem(key) ?? '{}', - ); - const [selectedElements, setSelectedElements] = useState>( - {}, - ); - const [deSelectedElements, setDeSelectedElements] = useState< - Record - >({}); + const { numberOfElements } = JSON.parse(window.localStorage.getItem(key) ?? '{}'); + const [selectAll, setSelectAll] = useState(false); + const [selectedElements, setSelectedElements] = useState>({}); + const [deSelectedElements, setDeSelectedElements] = useState>({}); + + const busKey = `${key}_entityToggle`; + const callback = useCallback((values: { + selectAll?: boolean, + selectedElements?: Record, + deSelectedElements?: Record, + }) => { + if (values.selectAll != null) { + setSelectAll(values.selectAll); + } + if (values.selectedElements != null) { + setSelectedElements(values.selectedElements); + } + if (values.deSelectedElements != null) { + setDeSelectedElements(values.deSelectedElements); + } + }, []); + const dispatch = useBus(busKey, callback); + const onToggleEntity = ( entity: T, event?: React.SyntheticEvent, @@ -58,19 +73,23 @@ const useEntityToggle = ( setSelectAll(false); setSelectedElements(newSelectedElements); setDeSelectedElements({}); + dispatch(busKey, { selectAll: false, selectedElements: newSelectedElements, deSelectedElements: {} }); } else if (entity.id in (selectedElements || {})) { const newSelectedElements = R.omit([entity.id], selectedElements); setSelectAll(false); setSelectedElements(newSelectedElements); + dispatch(busKey, { selectAll: false, selectedElements: newSelectedElements }); } else if (selectAll && entity.id in (deSelectedElements || {})) { const newDeSelectedElements = R.omit([entity.id], deSelectedElements); setDeSelectedElements(newDeSelectedElements); + dispatch(busKey, { deSelectedElements: newDeSelectedElements }); } else if (selectAll) { const newDeSelectedElements = { ...deSelectedElements, [entity.id]: entity, }; setDeSelectedElements(newDeSelectedElements); + dispatch(busKey, { deSelectedElements: newDeSelectedElements }); } else { const newSelectedElements = { ...selectedElements, @@ -78,23 +97,30 @@ const useEntityToggle = ( }; setSelectAll(false); setSelectedElements(newSelectedElements); + dispatch(busKey, { selectAll: false, selectedElements: newSelectedElements }); } }; + const handleToggleSelectAll = () => { setSelectAll(!selectAll); setSelectedElements({}); setDeSelectedElements({}); + dispatch(busKey, { selectAll: !selectAll, selectedElements: {}, deSelectedElements: {} }); }; + const handleClearSelectedElements = () => { setSelectAll(false); setSelectedElements({}); setDeSelectedElements({}); + dispatch(busKey, { selectAll: false, selectedElements: {}, deSelectedElements: {} }); }; + let numberOfSelectedElements = Object.keys(selectedElements).length; if (selectAll) { numberOfSelectedElements = (numberOfElements?.original ?? 0) - Object.keys(deSelectedElements).length; } + return { onToggleEntity, setSelectedElements, diff --git a/opencti-platform/opencti-front/src/utils/hooks/useLocalStorage.ts b/opencti-platform/opencti-front/src/utils/hooks/useLocalStorage.ts index b9e1a840899de..8cafea27a42f6 100644 --- a/opencti-platform/opencti-front/src/utils/hooks/useLocalStorage.ts +++ b/opencti-platform/opencti-front/src/utils/hooks/useLocalStorage.ts @@ -1,5 +1,5 @@ import * as R from 'ramda'; -import { Dispatch, SetStateAction, SyntheticEvent, useState } from 'react'; +import { Dispatch, SetStateAction, SyntheticEvent, useCallback, useState } from 'react'; import { v4 as uuid } from 'uuid'; import { OrderMode, PaginationOptions } from '../../components/list_lines'; import { emptyFilterGroup, Filter, FilterGroup, FilterValue, findFilterFromKey, isFilterGroupNotEmpty, isUniqFilter } from '../filters/filtersUtils'; @@ -16,6 +16,7 @@ import { handleChangeRepresentationFilterUtil, } from '../filters/filtersManageStateUtil'; import { LocalStorage } from './useLocalStorageModel'; +import useBus from './useBus'; import useAuth from './useAuth'; export interface handleFilterHelpers { @@ -31,6 +32,13 @@ export interface handleFilterHelpers { getLatestAddFilterId: () => string | undefined; handleChangeRepresentationFilter: (id: string, oldValue: FilterValue, newValue: FilterValue) => void; } + +export interface NumberOfElements { + number?: number; + symbol?: string; + original?: number; +} + export interface UseLocalStorageHelpers extends handleFilterHelpers { handleSearch: (value: string) => void; handleRemoveFilter: (key: string, op?: string, id?: string) => void; @@ -42,11 +50,7 @@ export interface UseLocalStorageHelpers extends handleFilterHelpers { handleAddSingleValueFilter: (id: string, value?: FilterValue) => void; handleSwitchFilter: HandleAddFilter; handleToggleExports: () => void; - handleSetNumberOfElements: (value: { - number?: number | string; - symbol?: string; - original?: number; - }) => void; + handleSetNumberOfElements: (value: NumberOfElements) => void; handleToggleTypes: (type: string) => void; handleClearTypes: () => void; handleAddProperty: (field: string, value: unknown) => void; @@ -59,7 +63,6 @@ const localStorageToPaginationOptions = ( ): PaginationOptions => { // Remove only display options, not query linked const localOptions = { ...props }; - delete localOptions.redirectionMode; delete localOptions.openExports; delete localOptions.selectAll; delete localOptions.redirectionMode; @@ -94,11 +97,6 @@ export type HandleOperatorFilter = ( op: string, ) => void; -export type UseLocalStorage = [ - value: LocalStorage, - setValue: Dispatch>, -]; - const buildParamsFromHistory = (params: LocalStorage) => { return removeEmptyFields({ filters: @@ -111,6 +109,7 @@ const buildParamsFromHistory = (params: LocalStorage) => { orderAsc: params.orderAsc, timeField: params.timeField, dashboard: params.dashboard, + redirectionMode: params.redirectionMode, types: params.types && params.types.length > 0 ? params.types.join(',') @@ -173,14 +172,14 @@ const setStoredValueToHistory = ( } }; -const useLocalStorage = ( +const useLocalStorage = ( key: string, - initialValue?: LocalStorage, + initialValue?: T, ignoreUri?: boolean, -): UseLocalStorage => { +): [T, Dispatch>] => { // State to store our value // Pass initial state function to useState so logic is only executed once - const [storedValue, setStoredValue] = useState(() => { + const [storedValue, setStoredValue] = useState(() => { if (typeof window === 'undefined') { return initialValue; } @@ -215,17 +214,24 @@ const useLocalStorage = ( throw Error('Error while initializing values in local storage'); } }); + + const dispatch = useBus(key, (v) => { + if (!R.equals(v, storedValue)) { + setStoredValue(v); + } + }); // Return a wrapped version of useState's setter function that ... // ... persists the new value to localStorage. const setValue = ( - value: LocalStorage | ((val: LocalStorage) => LocalStorage), + value: T | ((val: T) => T), ) => { try { // Allow value to be a function so we have same API as useState let valueToStore = value instanceof Function ? value(storedValue) : value; - valueToStore = removeEmptyFields(valueToStore); + valueToStore = removeEmptyFields(valueToStore) as T; // Save state setStoredValue(valueToStore); + dispatch(key, valueToStore); // Save to local storage + re-align uri if needed if (typeof window !== 'undefined') { window.localStorage.setItem(key, JSON.stringify(valueToStore)); @@ -258,19 +264,34 @@ export const usePaginationLocalStorage = ( ignoreUri?: boolean, ): PaginationLocalStorage => { const [viewStorage, setValue] = useLocalStorage(key, initialValue, ignoreUri); + + const callback = useCallback((v: LocalStorage) => { + setValue(v); + }, [viewStorage]); + const dispatch = useBus(`${key}_paginationStorage`, callback); + const paginationOptions = localStorageToPaginationOptions({ count: 25, ...viewStorage }); const { filterKeysSchema } = useAuth().schema; const helpers: UseLocalStorageHelpers = { - handleSearch: (value: string) => setValue((c) => ({ ...c, searchTerm: value })), + handleSearch: (value: string) => { + const newValue = { + ...viewStorage, + searchTerm: value, + }; + setValue(newValue); + dispatch(`${key}_paginationStorage`, newValue); + }, handleRemoveFilterById: (id: string) => { if (viewStorage?.filters) { const { filters } = viewStorage; - setValue((c) => ({ - ...c, + const newValue = { + ...viewStorage, filters: handleRemoveFilterUtil({ filters, id }), latestAddFilterId: undefined, - })); + }; + setValue(newValue); + dispatch(`${key}_paginationStorage`, newValue); } }, handleRemoveFilter: (k: string, op = 'eq', id?: string) => { @@ -295,10 +316,12 @@ export const usePaginationLocalStorage = ( newFilterElement, // remove value=id ], }; - setValue((c) => ({ - ...c, + const newValue = { + ...viewStorage, filters: newBaseFilters, - })); + }; + setValue(newValue); + dispatch(`${key}_paginationStorage`, newValue); } else { // values is empty: remove the filter with key=k and operator=op const newBaseFilters = { ...viewStorage.filters, @@ -307,10 +330,12 @@ export const usePaginationLocalStorage = ( .filter((f) => f.key !== k || f.operator !== op), // remove filter with key=k and operator=op ], }; - setValue((c) => ({ - ...c, + const newValue = { + ...viewStorage, filters: newBaseFilters, - })); + }; + setValue(newValue); + dispatch(`${key}_paginationStorage`, newValue); } } } else { @@ -319,23 +344,34 @@ export const usePaginationLocalStorage = ( filters: viewStorage.filters.filters .filter((f) => f.key !== k || f.operator !== op), // remove filter with key=k and operator=op }; - setValue((c) => ({ - ...c, + const newValue = { + ...viewStorage, filters: newBaseFilters, - })); + }; + setValue(newValue); + dispatch(`${key}_paginationStorage`, newValue); } } }, - handleSort: (field: string, order: boolean) => setValue((c) => ({ - ...c, - sortBy: field, - orderAsc: order, - })), + handleSort: (field: string, order: boolean) => { + const newValue = { + ...viewStorage, + sortBy: field, + orderAsc: order, + }; + setValue(newValue); + dispatch(`${key}_paginationStorage`, newValue); + }, handleAddProperty: (field: string, value: unknown) => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore if (!R.equals(viewStorage[field], value)) { - setValue((c) => ({ ...c, [field]: value })); + const newValue = { + ...viewStorage, + [field]: value, + }; + setValue(newValue); + dispatch(`${key}_paginationStorage`, newValue); } }, handleAddFilter: ( @@ -375,10 +411,12 @@ export const usePaginationLocalStorage = ( ...viewStorage.filters.filters.map((f) => (f.key === k && f.operator === op ? newFilterElement : f)), ], }; - setValue((c) => ({ - ...c, + const newValue = { + ...viewStorage, filters: newBaseFilters, - })); + }; + setValue(newValue); + dispatch(`${key}_paginationStorage`, newValue); } else { const newFilterElement = { id: uuid(), @@ -395,10 +433,12 @@ export const usePaginationLocalStorage = ( filterGroups: [], filters: [newFilterElement], }; - setValue((c) => ({ - ...c, + const newValue = { + ...viewStorage, filters: newBaseFilters, - })); + }; + setValue(newValue); + dispatch(`${key}_paginationStorage`, newValue); } }, handleRemoveRepresentationFilter: ( @@ -407,11 +447,13 @@ export const usePaginationLocalStorage = ( ) => { if (viewStorage?.filters) { const filters = viewStorage?.filters; - setValue((c) => ({ - ...c, + const newValue = { + ...viewStorage, filters: handleRemoveRepresentationFilterUtil({ filters, id, value }), latestAddFilterId: undefined, - })); + }; + setValue(newValue); + dispatch(`${key}_paginationStorage`, newValue); } }, handleAddRepresentationFilter: (id: string, value: string) => { @@ -419,25 +461,28 @@ export const usePaginationLocalStorage = ( const findCorrespondingFilter = viewStorage.filters?.filters.find((f) => id === f.id); if (findCorrespondingFilter && ['objectLabel'].includes(findCorrespondingFilter.key)) { if (viewStorage.filters) { - const { filters } = viewStorage; - setValue((c) => ({ - ...c, + const newValue = { + ...viewStorage, filters: handleChangeOperatorFiltersUtil({ - filters, + filters: viewStorage.filters, id, operator: findCorrespondingFilter.operator === 'not_eq' ? 'not_nil' : 'nil', }), latestAddFilterId: id, - })); + }; + setValue(newValue); + dispatch(`${key}_paginationStorage`, newValue); } } } else if (viewStorage?.filters) { const { filters } = viewStorage; - setValue((c) => ({ - ...c, + const newValue = { + ...viewStorage, filters: handleAddRepresentationFilterUtil({ filters, id, value }), latestAddFilterId: undefined, - })); + }; + setValue(newValue); + dispatch(`${key}_paginationStorage`, newValue); } }, handleChangeRepresentationFilter: (id: string, oldValue: FilterValue, newValue: FilterValue) => { @@ -446,33 +491,41 @@ export const usePaginationLocalStorage = ( return; } if (oldValue && newValue) { - setValue((c) => ({ - ...c, + const newStorageValue = { + ...viewStorage, filters: handleChangeRepresentationFilterUtil({ filters, id, oldValue, newValue }), latestAddFilterId: undefined, - })); + }; + setValue(newStorageValue); + dispatch(`${key}_paginationStorage`, newStorageValue); } else if (oldValue) { - setValue((c) => ({ - ...c, + const newStorageValue = { + ...viewStorage, filters: handleRemoveRepresentationFilterUtil({ filters, id, value: oldValue }), latestAddFilterId: undefined, - })); + }; + setValue(newStorageValue); + dispatch(`${key}_paginationStorage`, newStorageValue); } else if (newValue) { - setValue((c) => ({ - ...c, + const newStorageValue = { + ...viewStorage, filters: handleAddRepresentationFilterUtil({ filters, id, value: newValue }), latestAddFilterId: undefined, - })); + }; + setValue(newStorageValue); + dispatch(`${key}_paginationStorage`, newStorageValue); } }, handleAddSingleValueFilter: (id: string, valueId?: string) => { if (viewStorage?.filters) { const { filters } = viewStorage; - setValue((c) => ({ - ...c, + const newValue = { + ...viewStorage, filters: handleAddSingleValueFilterUtil({ filters, id, valueId }), latestAddFilterId: undefined, - })); + }; + setValue(newValue); + dispatch(`${key}_paginationStorage`, newValue); } }, handleSwitchFilter: ( @@ -501,10 +554,12 @@ export const usePaginationLocalStorage = ( newFilterElement, // add new filter ], }; - setValue((c) => ({ - ...c, + const newValue = { + ...viewStorage, filters: newBaseFilters, - })); + }; + setValue(newValue); + dispatch(`${key}_paginationStorage`, newValue); } else { const newBaseFilters = viewStorage.filters ? { ...viewStorage.filters, @@ -514,10 +569,12 @@ export const usePaginationLocalStorage = ( filterGroups: [], filters: [newFilterElement], }; - setValue((c) => ({ - ...c, + const newValue = { + ...viewStorage, filters: newBaseFilters, - })); + }; + setValue(newValue); + dispatch(`${key}_paginationStorage`, newValue); } }, handleSwitchGlobalMode: () => { @@ -526,74 +583,117 @@ export const usePaginationLocalStorage = ( ...viewStorage.filters, mode: viewStorage.filters.mode === 'and' ? 'or' : 'and', }; - setValue((c) => ({ - ...c, + const newValue = { + ...viewStorage, filters: newBaseFilters, latestAddFilterId: undefined, - })); + }; + setValue(newValue); + dispatch(`${key}_paginationStorage`, newValue); } }, handleSwitchLocalMode: (filter: Filter) => { if (viewStorage?.filters) { const { filters } = viewStorage; - setValue((c) => ({ - ...c, + const newValue = { + ...viewStorage, filters: handleSwitchLocalModeUtil({ filters, filter }), latestAddFilterId: undefined, - })); + }; + setValue(newValue); + dispatch(`${key}_paginationStorage`, newValue); } }, - handleChangeView: (value: string) => setValue((c) => ({ ...c, filters: initialValue.filters ?? emptyFilterGroup, searchTerm: initialValue.searchTerm ?? '', view: value })), - handleToggleExports: () => setValue((c) => ({ ...c, openExports: !c.openExports })), - handleSetNumberOfElements: (nbElements: { number?: number | string; symbol?: string; original?: number; }) => { + handleChangeView: (value: string) => { + const newValue = { + ...viewStorage, + filters: initialValue.filters ?? emptyFilterGroup, + searchTerm: initialValue.searchTerm ?? '', + view: value, + }; + setValue(newValue); + dispatch(`${key}_paginationStorage`, newValue); + }, + handleToggleExports: () => { + const newValue = { ...viewStorage, openExports: !viewStorage.openExports }; + setValue(newValue); + dispatch(`${key}_paginationStorage`, newValue); + }, + handleSetNumberOfElements: (nbElements: { number?: number; symbol?: string; original?: number; }) => { if (!R.equals(nbElements, viewStorage.numberOfElements)) { - setValue((c) => { - const { number, symbol, original } = nbElements; - return { - ...c, - numberOfElements: { - ...c.numberOfElements, - ...(number ? { number } : { number: 0 }), - ...(symbol ? { symbol } : { symbol: '' }), - ...(original ? { original } : { original: 0 }), - }, - }; - }); + const { number, symbol, original } = nbElements; + const newValue = { + ...viewStorage, + numberOfElements: { + ...viewStorage.numberOfElements, + ...(number ? { number } : { number: 0 }), + ...(symbol ? { symbol } : { symbol: '' }), + ...(original ? { original } : { original: 0 }), + }, + }; + setValue(newValue); + dispatch(`${key}_paginationStorage`, newValue); } }, handleToggleTypes: (type: string) => { if (viewStorage.types?.includes(type)) { const newTypes = viewStorage.types.filter((t) => t !== type); - setValue((c) => ({ ...c, types: newTypes })); + const newValue = { + ...viewStorage, + types: newTypes, + }; + setValue(newValue); + dispatch(`${key}_paginationStorage`, newValue); } else { const newTypes = viewStorage.types ? [...viewStorage.types, type] : [type]; - setValue((c) => ({ ...c, types: newTypes })); + const newValue = { + ...viewStorage, + types: newTypes, + }; + setValue(newValue); + dispatch(`${key}_paginationStorage`, newValue); } }, handleClearTypes: () => { - setValue((c) => ({ ...c, types: [] })); + const newValue = { + ...viewStorage, + types: [], + }; + setValue(newValue); + dispatch(`${key}_paginationStorage`, newValue); }, handleClearAllFilters: () => { - setValue((c) => ({ ...c, filters: initialValue.filters ?? emptyFilterGroup, searchTerm: initialValue.searchTerm ?? '' })); + const newValue = { + ...viewStorage, + filters: initialValue.filters ?? emptyFilterGroup, + searchTerm: initialValue.searchTerm ?? '', + numberOfElements: viewStorage.numberOfElements, + }; + setValue(newValue); + dispatch(`${key}_paginationStorage`, newValue); }, handleAddFilterWithEmptyValue: (filter: Filter) => { if (viewStorage?.filters) { const { filters } = viewStorage; - setValue((c) => ({ - ...c, + const newValue = { + ...viewStorage, filters: handleAddFilterWithEmptyValueUtil({ filters, filter }), latestAddFilterId: filter.id, - })); + }; + setValue(newValue); + dispatch(`${key}_paginationStorage`, newValue); } }, handleChangeOperatorFilters: (id: string, operator: string) => { if (viewStorage?.filters) { const { filters } = viewStorage; - setValue((c) => ({ - ...c, + const newValue = { + ...viewStorage, filters: handleChangeOperatorFiltersUtil({ filters, id, operator }), latestAddFilterId: undefined, - })); + }; + setValue(newValue); + dispatch(`${key}_paginationStorage`, newValue); } }, getLatestAddFilterId: () => { diff --git a/opencti-platform/opencti-front/src/utils/hooks/useLocalStorageModel.ts b/opencti-platform/opencti-front/src/utils/hooks/useLocalStorageModel.ts index 04ba420a7730c..97ee84cee3677 100644 --- a/opencti-platform/opencti-front/src/utils/hooks/useLocalStorageModel.ts +++ b/opencti-platform/opencti-front/src/utils/hooks/useLocalStorageModel.ts @@ -11,7 +11,7 @@ export interface MessageFromLocalStorage { } export interface LocalStorage { numberOfElements?: { - number: number | string; + number: number; symbol: string; original?: number; }; diff --git a/opencti-platform/opencti-front/src/utils/hooks/usePreloadedPaginationFragment.ts b/opencti-platform/opencti-front/src/utils/hooks/usePreloadedPaginationFragment.ts index 5e92ded4624c0..3e51745ff5cf0 100644 --- a/opencti-platform/opencti-front/src/utils/hooks/usePreloadedPaginationFragment.ts +++ b/opencti-platform/opencti-front/src/utils/hooks/usePreloadedPaginationFragment.ts @@ -9,7 +9,7 @@ type KeyType = Readonly<{ ' $fragmentSpreads': FragmentType; }>; -interface UsePreloadedPaginationFragment { +export interface UsePreloadedPaginationFragment { queryRef: PreloadedQuery; linesQuery: GraphQLTaggedNode; linesFragment: GraphQLTaggedNode; @@ -46,6 +46,7 @@ const usePreloadedPaginationFragment = < data, hasMore: () => hasNext, isLoadingMore: () => isLoadingNext, + isLoading: isLoadingNext, loadMore: loadNext, }; };