diff --git a/opencti-platform/opencti-front/src/components/dashboard/WidgetPolarArea.tsx b/opencti-platform/opencti-front/src/components/dashboard/WidgetPolarArea.tsx index e3b179f5d13b..a93ea6bf4e0b 100644 --- a/opencti-platform/opencti-front/src/components/dashboard/WidgetPolarArea.tsx +++ b/opencti-platform/opencti-front/src/components/dashboard/WidgetPolarArea.tsx @@ -4,29 +4,37 @@ import { useTheme } from '@mui/styles'; import { ApexOptions } from 'apexcharts'; import { polarAreaChartOptions } from '../../utils/Charts'; import type { Theme } from '../Theme'; +import useDistributionGraphData, { DistributionQueryData } from '../../utils/hooks/useDistributionGraphData'; interface WidgetPolarAreaProps { - data: { - label: string, - value: number - }[] + data: DistributionQueryData + groupBy: string withExport?: boolean readonly?: boolean } const WidgetPolarArea = ({ data, + groupBy, withExport, readonly, }: WidgetPolarAreaProps) => { const theme = useTheme(); + const { buildWidgetLabelsOption, buildWidgetColorsOptions } = useDistributionGraphData(); - const chartData = data.map((n) => n.value); - const labels = data.map((n) => n.label); + const chartData = data.flatMap((n) => (n ? (n.value ?? 0) : [])); + const labels = buildWidgetLabelsOption(data, groupBy); + const colors = buildWidgetColorsOptions(data, groupBy); return ( + withExportPopover: boolean + isReadOnly: boolean +} + +const AuditsPolarAreaComponent = ({ + dataSelection, + queryRef, + withExportPopover, + isReadOnly, +}: AuditsPolarAreaComponentProps) => { + const { auditsDistribution } = usePreloadedQuery( + auditsPolarAreaDistributionQuery, + queryRef, + ); + + if ( + auditsDistribution + && auditsDistribution.length > 0 + ) { + const attributeField = dataSelection[0].attribute || 'entity_type'; + return ( + + ); + } + return ; +}; + +interface AuditsPolarAreaProps { + startDate: string + endDate: string + dataSelection: DashboardWidgetDataSelection[] + parameters: DashboardWidgetParameters + variant: string + height?: CSSProperties['height'] + withExportPopover?: boolean + isReadOnly?: boolean +} + +const AuditsPolarAreaQueyRef = ({ + startDate, + endDate, + dataSelection, + parameters, + height, + variant, + withExportPopover = false, + isReadOnly = false, +}: AuditsPolarAreaProps) => { + const selection = dataSelection[0]; + const { t_i18n } = useFormatter(); + + const queryRef = useQueryLoading( + auditsPolarAreaDistributionQuery, + { + types: ['History', 'Activity'], + field: selection.attribute || 'entity_type', + operation: 'count', + startDate, + endDate, + dateAttribute: + selection.date_attribute && selection.date_attribute.length > 0 + ? selection.date_attribute + : 'timestamp', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore Excepts readonly array as variables but have simple array. + filters: selection.filters, + limit: selection.number ?? 10, + }, + ); + + return ( + + {queryRef ? ( + }> + + + ) : ( + + )} + + ); +}; + +const AuditsPolarArea = (props: AuditsPolarAreaProps) => { + const { t_i18n } = useFormatter(); + const isGrantedToSettings = useGranted([SETTINGS]); + const isEnterpriseEdition = useEnterpriseEdition(); + + if (!isGrantedToSettings || !isEnterpriseEdition) { + const { height, parameters, variant } = props; + return ( + + + + ); + } + return ; +}; + +export default AuditsPolarArea; diff --git a/opencti-platform/opencti-front/src/private/components/common/stix_core_objects/StixCoreObjectsPolarArea.tsx b/opencti-platform/opencti-front/src/private/components/common/stix_core_objects/StixCoreObjectsPolarArea.tsx new file mode 100644 index 000000000000..5d1e17918154 --- /dev/null +++ b/opencti-platform/opencti-front/src/private/components/common/stix_core_objects/StixCoreObjectsPolarArea.tsx @@ -0,0 +1,191 @@ +import { graphql, PreloadedQuery, usePreloadedQuery } from 'react-relay'; +import React, { CSSProperties } from 'react'; +import { StixCoreObjectsPolarAreaDistributionQuery } from '@components/common/stix_core_objects/__generated__/StixCoreObjectsPolarAreaDistributionQuery.graphql'; +import useQueryLoading from '../../../../utils/hooks/useQueryLoading'; +import WidgetContainer from '../../../../components/dashboard/WidgetContainer'; +import WidgetLoader from '../../../../components/dashboard/WidgetLoader'; +import { useFormatter } from '../../../../components/i18n'; +import WidgetPolarArea from '../../../../components/dashboard/WidgetPolarArea'; +import WidgetNoData from '../../../../components/dashboard/WidgetNoData'; +import { DashboardWidgetDataSelection, DashboardWidgetParameters } from '../../../../utils/dashboard'; + +const stixCoreObjectsPolarAreaDistributionQuery = graphql` + query StixCoreObjectsPolarAreaDistributionQuery( + $objectId: [String] + $relationship_type: [String] + $toTypes: [String] + $field: String! + $startDate: DateTime + $endDate: DateTime + $dateAttribute: String + $operation: StatsOperation! + $limit: Int + $order: String + $types: [String] + $filters: FilterGroup + $search: String + ) { + stixCoreObjectsDistribution( + objectId: $objectId + relationship_type: $relationship_type + toTypes: $toTypes + field: $field + startDate: $startDate + endDate: $endDate + dateAttribute: $dateAttribute + operation: $operation + limit: $limit + order: $order + types: $types + filters: $filters + search: $search + ) { + label + value + entity { + ... on BasicObject { + id + entity_type + } + ... on BasicRelationship { + id + entity_type + } + ... on StixObject { + representative { + main + } + } + ... on StixRelationship { + representative { + main + } + } + # use colors when available + ... on Label { + color + } + ... on MarkingDefinition { + x_opencti_color + } + # objects without representative + ... on Creator { + name + } + ... on Group { + name + } + ... on Status { + template { + name + color + } + } + } + } + } +`; + +interface StixCoreObjectsPolarAreaComponentProps { + dataSelection: DashboardWidgetDataSelection[] + queryRef: PreloadedQuery + withExportPopover: boolean + isReadOnly: boolean +} + +const StixCoreObjectsPolarAreaComponent = ({ + dataSelection, + queryRef, + withExportPopover, + isReadOnly, +}: StixCoreObjectsPolarAreaComponentProps) => { + const { stixCoreObjectsDistribution } = usePreloadedQuery( + stixCoreObjectsPolarAreaDistributionQuery, + queryRef, + ); + + if ( + stixCoreObjectsDistribution + && stixCoreObjectsDistribution.length > 0 + ) { + const attributeField = dataSelection[0].attribute || 'entity_type'; + return ( + + ); + } + return ; +}; + +interface StixCoreObjectsPolarAreaProps { + startDate: string + endDate: string + dataSelection: DashboardWidgetDataSelection[] + parameters: DashboardWidgetParameters + variant?: string + height?: CSSProperties['height'] + withExportPopover?: boolean + isReadOnly?: boolean +} + +const StixCoreObjectsPolarArea = ({ + startDate, + endDate, + dataSelection, + parameters, + height, + variant = 'inLine', + withExportPopover = false, + isReadOnly = false, +}: StixCoreObjectsPolarAreaProps) => { + const { t_i18n } = useFormatter(); + + const selection = dataSelection[0]; + const dataSelectionTypes = ['Stix-Core-Object']; + + const queryRef = useQueryLoading( + stixCoreObjectsPolarAreaDistributionQuery, + { + types: dataSelectionTypes, + field: selection.attribute || 'entity_type', + operation: 'count', + startDate, + endDate, + dateAttribute: + selection.date_attribute && selection.date_attribute.length > 0 + ? selection.date_attribute + : 'created_at', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore Excepts readonly array as variables but have simple array. + filters: selection.filters, + limit: selection.number ?? 10, + }, + ); + + return ( + + {queryRef ? ( + }> + + + ) : ( + + )} + + ); +}; + +export default StixCoreObjectsPolarArea; diff --git a/opencti-platform/opencti-front/src/private/components/common/stix_relationships/StixRelationshipsPolarArea.jsx b/opencti-platform/opencti-front/src/private/components/common/stix_relationships/StixRelationshipsPolarArea.jsx index e20d402cd519..e331d2dd6472 100644 --- a/opencti-platform/opencti-front/src/private/components/common/stix_relationships/StixRelationshipsPolarArea.jsx +++ b/opencti-platform/opencti-front/src/private/components/common/stix_relationships/StixRelationshipsPolarArea.jsx @@ -2,7 +2,6 @@ import React from 'react'; import { graphql } from 'react-relay'; import { QueryRenderer } from '../../../../relay/environment'; import { useFormatter } from '../../../../components/i18n'; -import { getMainRepresentative, isFieldForIdentifier } from '../../../../utils/defaultRepresentatives'; import { buildFiltersAndOptionsForWidgets } from '../../../../utils/filters/filtersUtils'; import WidgetNoData from '../../../../components/dashboard/WidgetNoData'; import WidgetLoader from '../../../../components/dashboard/WidgetLoader'; @@ -142,18 +141,15 @@ const StixRelationshipsPolarArea = ({ query={stixRelationshipsPolarAreasDistributionQuery} variables={variables} render={({ props }) => { - if (props && props.stixRelationshipsDistribution && props.stixRelationshipsDistribution.length > 0) { - let data = props.stixRelationshipsDistribution; - if (isFieldForIdentifier(finalField)) { - data = data.map((n) => ({ - ...n, - label: getMainRepresentative(n.entity), - })); - } - // TODO: take into account the entity color to send it to the widget (that shall handle it) + if ( + props + && props.stixRelationshipsDistribution + && props.stixRelationshipsDistribution.length > 0 + ) { return ( diff --git a/opencti-platform/opencti-front/src/private/components/workspaces/dashboards/Dashboard.jsx b/opencti-platform/opencti-front/src/private/components/workspaces/dashboards/Dashboard.jsx index b409c0b6d315..59fbab68f81a 100644 --- a/opencti-platform/opencti-front/src/private/components/workspaces/dashboards/Dashboard.jsx +++ b/opencti-platform/opencti-front/src/private/components/workspaces/dashboards/Dashboard.jsx @@ -5,6 +5,8 @@ import RGL, { WidthProvider } from 'react-grid-layout'; import Paper from '@mui/material/Paper'; import makeStyles from '@mui/styles/makeStyles'; import { v4 as uuid } from 'uuid'; +import AuditsPolarArea from '../../common/audits/AuditsPolarArea'; +import StixCoreObjectsPolarArea from '../../common/stix_core_objects/StixCoreObjectsPolarArea'; import StixRelationshipsPolarArea from '../../common/stix_relationships/StixRelationshipsPolarArea'; import { computerRelativeDate, dayStartDate, parse } from '../../../../utils/Time'; import WorkspaceHeader from '../WorkspaceHeader'; @@ -317,6 +319,18 @@ const DashboardComponent = ({ workspace, noToolbar }) => { isReadOnly={!isWrite} /> ); + case 'polar-area': + return ( + + ); case 'horizontal-bar': if ( widget.dataSelection.length > 1 @@ -655,6 +669,18 @@ const DashboardComponent = ({ workspace, noToolbar }) => { isReadOnly={!isWrite} /> ); + case 'polar-area': + return ( + + ); case 'horizontal-bar': return ( +} + +const PublicStixRelationshipsPolarAreaComponent = ({ + dataSelection, + queryRef, +}: PublicStixRelationshipsPolarAreaComponentProps) => { + const { publicStixCoreObjectsDistribution } = usePreloadedQuery( + publicStixCoreObjectsPolarAreaQuery, + queryRef, + ); + + if ( + publicStixCoreObjectsDistribution + && publicStixCoreObjectsDistribution.length > 0 + ) { + const attributeField = dataSelection[0].attribute || 'entity_type'; + return ( + + ); + } + return ; +}; + +const PublicStixCoreObjectsPolarArea = ({ + uriKey, + widget, + startDate, + endDate, + title, +}: PublicWidgetContainerProps) => { + const { t_i18n } = useFormatter(); + const { id, parameters, dataSelection } = widget; + const queryRef = useQueryLoading( + publicStixCoreObjectsPolarAreaQuery, + { + uriKey, + widgetId: id, + startDate, + endDate, + }, + ); + + return ( + + {queryRef ? ( + }> + + + ) : ( + + )} + + ); +}; + +export default PublicStixCoreObjectsPolarArea; diff --git a/opencti-platform/opencti-front/src/public/components/dashboard/stix_relationships/PublicStixRelationshipsPolarArea.tsx b/opencti-platform/opencti-front/src/public/components/dashboard/stix_relationships/PublicStixRelationshipsPolarArea.tsx index 80e0be1602d9..03e06e046590 100644 --- a/opencti-platform/opencti-front/src/public/components/dashboard/stix_relationships/PublicStixRelationshipsPolarArea.tsx +++ b/opencti-platform/opencti-front/src/public/components/dashboard/stix_relationships/PublicStixRelationshipsPolarArea.tsx @@ -9,7 +9,6 @@ import WidgetContainer from '../../../../components/dashboard/WidgetContainer'; import WidgetLoader from '../../../../components/dashboard/WidgetLoader'; import { PublicStixRelationshipsPolarAreaQuery } from './__generated__/PublicStixRelationshipsPolarAreaQuery.graphql'; import WidgetPolarArea from '../../../../components/dashboard/WidgetPolarArea'; -import { getMainRepresentative, isFieldForIdentifier } from '../../../../utils/defaultRepresentatives'; const publicStixRelationshipsPolarAreaQuery = graphql` query PublicStixRelationshipsPolarAreaQuery( @@ -89,21 +88,10 @@ const PublicStixRelationshipsPolarAreaComponent = ({ && publicStixRelationshipsDistribution.length > 0 ) { const attributeField = dataSelection[0].attribute || 'entity_type'; - - // TODO: take into account the entity color to send it to the widget (that shall handle it) return ( { - if (!item) { - return []; - } - return { - label: isFieldForIdentifier(attributeField) - ? getMainRepresentative(item.entity) - : item.label, - value: item.value ?? 0, - }; - })} + data={[...publicStixRelationshipsDistribution]} + groupBy={attributeField} withExport={false} readonly={true} /> diff --git a/opencti-platform/opencti-front/src/public/components/dashboard/usePublicDashboardWidgets.tsx b/opencti-platform/opencti-front/src/public/components/dashboard/usePublicDashboardWidgets.tsx index e38a7ed550d6..c306181c9e76 100644 --- a/opencti-platform/opencti-front/src/public/components/dashboard/usePublicDashboardWidgets.tsx +++ b/opencti-platform/opencti-front/src/public/components/dashboard/usePublicDashboardWidgets.tsx @@ -30,6 +30,7 @@ import PublicStixRelationshipsHorizontalBars from './stix_relationships/PublicSt import PublicStixDomainObjectBookmarksList from './stix_domain_objects/PublicStixDomainObjectBookmarksList'; import PublicStixRelationshipsMultiHorizontalBars from './stix_relationships/PublicStixRelationshipsMultiHorizontalBars'; import PublicStixRelationshipsPolarArea from './stix_relationships/PublicStixRelationshipsPolarArea'; +import PublicStixCoreObjectsPolarArea from './stix_core_objects/PublicStixCoreObjectsPolarArea'; const usePublicDashboardWidgets = (uriKey: string, config?: PublicManifestConfig) => { const startDate = config?.relativeDate ? computerRelativeDate(config.relativeDate) : config?.startDate; @@ -138,6 +139,15 @@ const usePublicDashboardWidgets = (uriKey: string, config?: PublicManifestConfig widget={widget} /> ); + case 'polar-area': + return ( + + ); case 'heatmap': return ( ({ seriesIndex, w }) => (` +
+ ${w.config.labels[seriesIndex]} +
+`); + /** * @param {Theme} theme * @param {boolean} isTimeSeries @@ -631,76 +646,91 @@ export const radarChartOptions = ( * @param {string[]} labels * @param {function} formatter * @param {string} legendPosition + * @param {string[]} chartColors */ export const polarAreaChartOptions = ( theme, labels, formatter = null, legendPosition = 'bottom', -) => ({ - chart: { - type: 'polarArea', - background: 'transparent', - toolbar: toolbarOptions, - foreColor: theme.palette.text.secondary, - width: '100%', - height: '100%', - }, - theme: { - mode: theme.palette.mode, - }, - colors: colors(theme.palette.mode === 'dark' ? 400 : 600), - labels, - states: { - hover: { - filter: { - type: 'lighten', - value: 0.05, - }, + chartColors = [], +) => { + const temp = theme.palette.mode === 'dark' ? 400 : 600; + let chartFinalColors = chartColors; + if (chartFinalColors.length === 0) { + chartFinalColors = colors(temp); + if (labels.length === 2 && labels[0] === 'true') { + chartFinalColors = [C.green[temp], C.red[temp]]; + } else if (labels.length === 2 && labels[0] === 'false') { + chartFinalColors = [C.red[temp], C.green[temp]]; + } + } + return { + chart: { + type: 'polarArea', + background: 'transparent', + toolbar: toolbarOptions, + foreColor: theme.palette.text.secondary, + width: '100%', + height: '100%', }, - }, - legend: { - show: true, - position: legendPosition, - floating: legendPosition === 'bottom', - fontFamily: '"IBM Plex Sans", sans-serif', - }, - tooltip: { - theme: theme.palette.mode, - }, - fill: { - opacity: 0.5, - }, - yaxis: { - labels: { - formatter: (value) => (formatter ? formatter(value) : value), - style: { - fontFamily: '"IBM Plex Sans", sans-serif', + theme: { + mode: theme.palette.mode, + }, + colors: chartFinalColors, + labels, + states: { + hover: { + filter: { + type: 'lighten', + value: 0.05, + }, }, }, - axisBorder: { - show: false, + legend: { + show: true, + position: legendPosition, + floating: legendPosition === 'bottom', + fontFamily: '"IBM Plex Sans", sans-serif', }, - }, - plotOptions: { - polarArea: { - rings: { - strokeWidth: 1, - strokeColor: - theme.palette.mode === 'dark' - ? 'rgba(255, 255, 255, .1)' - : 'rgba(0, 0, 0, .1)', + tooltip: { + theme: theme.palette.mode, + custom: simpleLabelTooltip(theme), + }, + fill: { + opacity: 0.5, + }, + yaxis: { + labels: { + formatter: (value) => (formatter ? formatter(value) : value), + style: { + fontFamily: '"IBM Plex Sans", sans-serif', + }, }, - spokes: { - strokeWidth: 1, - connectorColors: - theme.palette.mode === 'dark' - ? 'rgba(255, 255, 255, .1)' - : 'rgba(0, 0, 0, .1)', + axisBorder: { + show: false, }, }, - }, -}); + plotOptions: { + polarArea: { + rings: { + strokeWidth: 1, + strokeColor: + theme.palette.mode === 'dark' + ? 'rgba(255, 255, 255, .1)' + : 'rgba(0, 0, 0, .1)', + }, + spokes: { + strokeWidth: 1, + connectorColors: + theme.palette.mode === 'dark' + ? 'rgba(255, 255, 255, .1)' + : 'rgba(0, 0, 0, .1)', + }, + }, + }, + }; +}; /** * @param {Theme} theme @@ -768,6 +798,10 @@ export const donutChartOptions = ( width: 3, colors: [theme.palette.background.paper], }, + tooltip: { + theme: theme.palette.mode, + custom: simpleLabelTooltip(theme), + }, legend: { show: true, position: legendPosition, diff --git a/opencti-platform/opencti-front/src/utils/dashboard.ts b/opencti-platform/opencti-front/src/utils/dashboard.ts new file mode 100644 index 000000000000..f164848f6a01 --- /dev/null +++ b/opencti-platform/opencti-front/src/utils/dashboard.ts @@ -0,0 +1,21 @@ +import { FilterGroup } from './filters/filtersUtils'; + +export interface DashboardWidgetDataSelection { + label?: string + number?: number + attribute?: string + date_attribute?: string + centerLat?: number + centerLng?: number + zoom?: number + isTo?: boolean + filters?: FilterGroup | null +} + +export interface DashboardWidgetParameters { + title?: string + interval?: string + stacked?: boolean + legend?: boolean + distributed?: boolean +} diff --git a/opencti-platform/opencti-front/src/utils/hooks/useDistributionGraphData.ts b/opencti-platform/opencti-front/src/utils/hooks/useDistributionGraphData.ts index 7b3033952fbd..2c4f90959046 100644 --- a/opencti-platform/opencti-front/src/utils/hooks/useDistributionGraphData.ts +++ b/opencti-platform/opencti-front/src/utils/hooks/useDistributionGraphData.ts @@ -22,7 +22,7 @@ type DistributionNode = { } | null, }; -type DistributionQueryData = ReadonlyArray; +export type DistributionQueryData = ReadonlyArray; type Selection = { attribute?: string, @@ -124,9 +124,24 @@ const useDistributionGraphData = () => { }); }; + const buildWidgetColorsOptions = (distributionData: DistributionQueryData, groupBy: string) => { + if ( + !distributionData.at(0)?.entity?.color + && !distributionData.at(0)?.entity?.x_opencti_color + && !distributionData.at(0)?.entity?.template?.color + ) { + return []; + } + return distributionData.map((n) => { + if (!n) return '#000000'; + return getColorFromDistributionNode(n, { attribute: groupBy }); + }); + }; + return { buildWidgetProps, buildWidgetLabelsOption, + buildWidgetColorsOptions, }; };