From 4b456b95444b231c1096a297d8e99ac56918f3f8 Mon Sep 17 00:00:00 2001 From: Daniel Priori Date: Wed, 28 Aug 2024 18:04:33 +0200 Subject: [PATCH] feat(react-components): fdm support in rule based core (#4706) * add a hook to get all instances from all direct connections and merge all relevant data for each instance - wip * move direct connection with properties hook and adapt the initial functions to support fdm trigger type and styling - wip * adapting styling groups accross components to support fdm styling groups also - wip * insert fdm types for rule type definitions * refactoring expression types * add datetime and boolean expressions statements * refactoring and add property trigger for numeric expression supporting fdm * add optional parameters for the fdm typing type * fix property types * fix date statement and fdm externalid for styling list * fix datetime limit type for between and notbetween * add expression safeguards * refactoring and remove duplicated timeserie ids when traversing expression to get time series * update and export fdm types and some cleanup * add fdm new conditions * stories cleanup * cleanup fdmsdk log * use isLoading flag for the spinner accuracy and some refactoring on loading and caching * reduce complexity a bit * refactoring for some small optimizations * lint fix * export fdm types for rule based * refactoring some rule type definitions to support correct typing data * fix datetimecondition definition * fix boolean condition types and export unique types list for numeric and datetime * add into index * dont request assetids from ts if it is empty * changes from cr * lint fix * use createFdmKey - cr * add createFdmKey for uniqBy --- .../RevealToolbar/RuleBasedOutputsButton.tsx | 42 +- .../RuleBasedOutputsSelector.tsx | 85 +++- .../core/checkBooleanExpressionStatement.ts | 50 +++ .../core/checkDatetimeExpressionStatement.ts | 100 +++++ .../core/checkNumericExpressionStatement.ts | 93 +++++ .../core/checkStringExpressionStatement.ts | 73 ++++ .../hooks/useCreateRuleInstance.tsx | 3 +- .../hooks/useDeleteRuleInstance.tsx | 4 +- .../hooks/useEditRuleInstance.tsx | 3 +- .../src/components/RuleBasedOutputs/types.ts | 225 ++++++++-- .../src/components/RuleBasedOutputs/utils.ts | 393 ++++++++++-------- react-components/src/index.ts | 16 +- .../query/use3dRelatedDirectConnections.ts | 4 +- ...useAll3dDirectConnectionsWithProperties.ts | 146 +++++++ .../useAssetsAndTimeseriesLinkageDataQuery.ts | 4 +- 15 files changed, 999 insertions(+), 242 deletions(-) create mode 100644 react-components/src/components/RuleBasedOutputs/core/checkBooleanExpressionStatement.ts create mode 100644 react-components/src/components/RuleBasedOutputs/core/checkDatetimeExpressionStatement.ts create mode 100644 react-components/src/components/RuleBasedOutputs/core/checkNumericExpressionStatement.ts create mode 100644 react-components/src/components/RuleBasedOutputs/core/checkStringExpressionStatement.ts create mode 100644 react-components/src/query/useAll3dDirectConnectionsWithProperties.ts diff --git a/react-components/src/components/RevealToolbar/RuleBasedOutputsButton.tsx b/react-components/src/components/RevealToolbar/RuleBasedOutputsButton.tsx index fa8460f5e9f..d66060031a5 100644 --- a/react-components/src/components/RevealToolbar/RuleBasedOutputsButton.tsx +++ b/react-components/src/components/RevealToolbar/RuleBasedOutputsButton.tsx @@ -8,13 +8,13 @@ import { Button, Dropdown, Menu, Tooltip as CogsTooltip } from '@cognite/cogs.js import { RuleBasedOutputsSelector } from '../RuleBasedOutputs/RuleBasedOutputsSelector'; import { type EmptyRuleForSelection, - type AssetStylingGroupAndStyleIndex, - type RuleAndEnabled + type RuleAndEnabled, + type AllMappingStylingGroupAndStyleIndex, + type AllRuleBasedStylingGroups } from '../RuleBasedOutputs/types'; import { useTranslation } from '../i18n/I18n'; import { useFetchRuleInstances } from '../RuleBasedOutputs/hooks/useFetchRuleInstances'; import { use3dModels } from '../../hooks/use3dModels'; -import { type AssetStylingGroup } from '../..'; import { type CadModelOptions } from '../Reveal3DResources/types'; import { useAssetMappedNodesForRevisions } from '../CacheProvider/AssetMappingAndNode3DCacheProvider'; import { RuleBasedSelectionItem } from '../RuleBasedOutputs/components/RuleBasedSelectionItem'; @@ -22,7 +22,7 @@ import { generateEmptyRuleForSelection, getRuleBasedById } from '../RuleBasedOut import { useReveal3DResourcesStylingLoading } from '../Reveal3DResources/Reveal3DResourcesInfoContext'; type RuleBasedOutputsButtonProps = { - onRuleSetStylingChanged?: (stylings: AssetStylingGroup[] | undefined) => void; + onRuleSetStylingChanged?: (stylings: AllRuleBasedStylingGroups | undefined) => void; onRuleSetSelectedChanged?: (ruleSet: RuleAndEnabled | undefined) => void; }; export const RuleBasedOutputsButton = ({ @@ -36,7 +36,7 @@ export const RuleBasedOutputsButton = ({ const [currentRuleSetEnabled, setCurrentRuleSetEnabled] = useState(); const [emptyRuleSelected, setEmptyRuleSelected] = useState(); const [currentStylingGroups, setCurrentStylingGroups] = useState< - AssetStylingGroupAndStyleIndex[] | undefined + AllMappingStylingGroupAndStyleIndex[] | undefined >(); const [ruleInstances, setRuleInstances] = useState(); @@ -47,6 +47,8 @@ export const RuleBasedOutputsButton = ({ const [newRuleSetEnabled, setNewRuleSetEnabled] = useState(); const isRuleLoadingFromContext = useReveal3DResourcesStylingLoading(); + const [isAllMappingsFetched, setIsAllMappingsFetched] = useState(false); + const { data: ruleInstancesResult } = useFetchRuleInstances(); useEffect(() => { @@ -64,11 +66,13 @@ export const RuleBasedOutputsButton = ({ useEffect(() => { const hasRuleLoading = - currentStylingGroups !== undefined && - currentStylingGroups.length > 0 && - isRuleLoadingFromContext; + (currentStylingGroups !== undefined && + currentStylingGroups.length > 0 && + isRuleLoadingFromContext) || + !isAllMappingsFetched; + setIsRuleLoading(hasRuleLoading); - }, [isRuleLoadingFromContext, currentStylingGroups]); + }, [isAllMappingsFetched, currentStylingGroups, isRuleLoadingFromContext, newRuleSetEnabled]); const onChange = useCallback( (data: string | undefined): void => { @@ -89,19 +93,28 @@ export const RuleBasedOutputsButton = ({ emptySelection.isEnabled = true; if (onRuleSetStylingChanged !== undefined) onRuleSetStylingChanged(undefined); } - setEmptyRuleSelected(emptySelection); setNewRuleSetEnabled(selectedRule); }, - [ruleInstances, onRuleSetStylingChanged, onRuleSetSelectedChanged] + [ruleInstances, emptyRuleSelected, onRuleSetStylingChanged, onRuleSetSelectedChanged] ); const ruleSetStylingChanged = ( - stylingGroups: AssetStylingGroupAndStyleIndex[] | undefined + stylingGroups: AllMappingStylingGroupAndStyleIndex[] | undefined ): void => { setCurrentStylingGroups(stylingGroups); - const assetStylingGroups = stylingGroups?.map((group) => group.assetStylingGroup); - if (onRuleSetStylingChanged !== undefined) onRuleSetStylingChanged(assetStylingGroups); + const assetStylingGroups = stylingGroups?.map( + (group) => group.assetMappingsStylingGroupAndIndex.assetStylingGroup + ); + const fdmStylingGroups = stylingGroups?.map( + (group) => group.fdmStylingGroupAndStyleIndex.fdmStylingGroup + ); + const allStylingGroups: AllRuleBasedStylingGroups = { + assetStylingGroup: assetStylingGroups ?? [], + fdmStylingGroup: fdmStylingGroups ?? [] + }; + + if (onRuleSetStylingChanged !== undefined) onRuleSetStylingChanged(allStylingGroups); }; if (ruleInstances === undefined || ruleInstances.length === 0) { @@ -158,6 +171,7 @@ export const RuleBasedOutputsButton = ({ {ruleInstances !== undefined && ruleInstances?.length > 0 && ( )} diff --git a/react-components/src/components/RuleBasedOutputs/RuleBasedOutputsSelector.tsx b/react-components/src/components/RuleBasedOutputs/RuleBasedOutputsSelector.tsx index b1e48b76191..ab45eb5f637 100644 --- a/react-components/src/components/RuleBasedOutputs/RuleBasedOutputsSelector.tsx +++ b/react-components/src/components/RuleBasedOutputs/RuleBasedOutputsSelector.tsx @@ -1,10 +1,14 @@ /*! * Copyright 2024 Cognite AS */ -import { useEffect, type ReactElement, useState } from 'react'; +import { useEffect, type ReactElement, useState, useMemo } from 'react'; import { CogniteCadModel } from '@cognite/reveal'; -import { type RuleOutputSet, type AssetStylingGroupAndStyleIndex } from './types'; +import { + type RuleOutputSet, + type AllMappingStylingGroupAndStyleIndex, + type FdmInstanceNodeWithConnectionAndProperties +} from './types'; import { generateRuleBasedOutputs } from './utils'; import { use3dModels } from '../../hooks/use3dModels'; import { type Datapoints, type Asset, type AssetMapping3D } from '@cognite/sdk'; @@ -18,17 +22,21 @@ import { useCreateAssetMappingsMapPerModel } from '../../hooks/useCreateAssetMap import { useExtractUniqueAssetIdsFromMapped } from './hooks/useExtractUniqueAssetIdsFromMapped'; import { useConvertAssetMetadatasToLowerCase } from './hooks/useConvertAssetMetadatasToLowerCase'; import { useExtractTimeseriesIdsFromRuleSet } from './hooks/useExtractTimeseriesIdsFromRuleSet'; +import { useMappedEdgesForRevisions } from '../CacheProvider/NodeCacheProvider'; +import { useAll3dDirectConnectionsWithProperties } from '../../query/useAll3dDirectConnectionsWithProperties'; + +const ruleSetStylingCache = new Map(); export type ColorOverlayProps = { ruleSet: RuleOutputSet | undefined; - onRuleSetChanged?: (currentStylings: AssetStylingGroupAndStyleIndex[] | undefined) => void; + onRuleSetChanged?: (currentStylings: AllMappingStylingGroupAndStyleIndex[] | undefined) => void; + onAllMappingsFetched: (value: boolean) => void; }; -const ruleSetStylingCache = new Map(); - export function RuleBasedOutputsSelector({ ruleSet, - onRuleSetChanged + onRuleSetChanged, + onAllMappingsFetched }: ColorOverlayProps): ReactElement | undefined { if (ruleSet === undefined) return; @@ -42,17 +50,44 @@ export function RuleBasedOutputsSelector({ return { type: 'cad', modelId: model.modelId, revisionId: model.revisionId }; }); - const { data: assetMappings } = useAssetMappedNodesForRevisions(cadModels); + const { data: assetMappings, isLoading: isAssetMappingsLoading } = + useAssetMappedNodesForRevisions(cadModels); const assetIdsFromMapped = useExtractUniqueAssetIdsFromMapped(assetMappings); - const { data: mappedAssets, isFetched } = useAssetsByIdsQuery(assetIdsFromMapped); + const { + data: mappedAssets, + isLoading: isAssetMappedLoading, + isFetched: isAssetMappingsFetched + } = useAssetsByIdsQuery(assetIdsFromMapped); + + const { data: fdmMappedEquipmentEdges, isLoading: isFdmMappingsEdgesLoading } = + useMappedEdgesForRevisions(cadModels, true); + + const fdmConnectionWithNodeAndViewList = useMemo(() => { + return fdmMappedEquipmentEdges !== undefined + ? Array.from(fdmMappedEquipmentEdges.values()).flat() + : []; + }, [fdmMappedEquipmentEdges]); + + const { data: fdmMappings, isLoading: isFdmMappingsLoading } = + useAll3dDirectConnectionsWithProperties(fdmConnectionWithNodeAndViewList); + + const allMappingsLoaded = + !isAssetMappingsLoading && + !isAssetMappedLoading && + !isFdmMappingsEdgesLoading && + !isFdmMappingsLoading; useEffect(() => { - if (isFetched) { + if (isAssetMappingsFetched) { setAllContextualizedAssets(mappedAssets); } - }, [mappedAssets, isFetched]); + }, [mappedAssets, isAssetMappingsFetched]); + + useEffect(() => { + onAllMappingsFetched(allMappingsLoaded); + }, [allMappingsLoaded]); const contextualizedAssetNodes = useConvertAssetMetadatasToLowerCase(allContextualizedAssets); @@ -67,9 +102,10 @@ export function RuleBasedOutputsSelector({ const flatAssetsMappingsListPerModel = useCreateAssetMappingsMapPerModel(models, assetMappings); useEffect(() => { - if (assetMappings === undefined || models === undefined || !isFetched) return; + if ((assetMappings === undefined && fdmMappings === undefined) || models === undefined) return; if (timeseriesExternalIds.length > 0 && isLoadingAssetIdsAndTimeseriesData) return; if (ruleSet === undefined) return; + if (!allMappingsLoaded) return; const ruleBasedInitilization = async (): Promise => { const allStylings = await Promise.all( @@ -81,20 +117,22 @@ export function RuleBasedOutputsSelector({ const flatAssetsMappingsList = flatAssetsMappingsListPerModel.get(model) ?? []; if (flatAssetsMappingsList.length === 0) return []; - const stylings = await initializeRuleBasedOutputs({ - assetMappings: flatAssetsMappingsList, + + const mappingsStylings = await initializeRuleBasedOutputs({ + assetMappings: flatAssetsMappingsList ?? [], + fdmMappings: fdmMappings ?? [], contextualizedAssetNodes, ruleSet, assetIdsAndTimeseries: assetIdsWithTimeseriesData?.assetIdsWithTimeseries ?? [], timeseriesDatapoints: assetIdsWithTimeseriesData?.timeseriesDatapoints ?? [] }); - const filteredStylings = stylings.flat().filter(isDefined); - return filteredStylings; + + return mappingsStylings; }) ); const filteredStylings = allStylings.flat().filter(isDefined).flat(); - ruleSetStylingCache.set(ruleSet.id, filteredStylings); + ruleSetStylingCache.set(ruleSet.id, filteredStylings); if (onRuleSetChanged !== undefined) { onRuleSetChanged(filteredStylings); } @@ -102,29 +140,40 @@ export function RuleBasedOutputsSelector({ if (!ruleSetStylingCache.has(ruleSet.id)) { void ruleBasedInitilization(); } else { + onAllMappingsFetched(true); if (onRuleSetChanged !== undefined) onRuleSetChanged(ruleSetStylingCache.get(ruleSet.id)); } - }, [isLoadingAssetIdsAndTimeseriesData, ruleSet, assetMappings, allContextualizedAssets]); + }, [ + ruleSet, + assetMappings, + fdmMappings, + contextualizedAssetNodes, + assetIdsWithTimeseriesData, + models + ]); return <>; } async function initializeRuleBasedOutputs({ assetMappings, + fdmMappings, contextualizedAssetNodes, ruleSet, assetIdsAndTimeseries, timeseriesDatapoints }: { assetMappings: AssetMapping3D[]; + fdmMappings: FdmInstanceNodeWithConnectionAndProperties[]; contextualizedAssetNodes: Asset[]; ruleSet: RuleOutputSet; assetIdsAndTimeseries: AssetIdsAndTimeseries[]; timeseriesDatapoints: Datapoints[] | undefined; -}): Promise { +}): Promise { const collectionStylings = await generateRuleBasedOutputs({ contextualizedAssetNodes, assetMappings, + fdmMappings, ruleSet, assetIdsAndTimeseries, timeseriesDatapoints diff --git a/react-components/src/components/RuleBasedOutputs/core/checkBooleanExpressionStatement.ts b/react-components/src/components/RuleBasedOutputs/core/checkBooleanExpressionStatement.ts new file mode 100644 index 00000000000..118810a8aea --- /dev/null +++ b/react-components/src/components/RuleBasedOutputs/core/checkBooleanExpressionStatement.ts @@ -0,0 +1,50 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { type FdmPropertyType } from '../../Reveal3DResources/types'; +import { type Expression, type TriggerTypeData } from '../types'; +import { getFdmPropertyTrigger } from '../utils'; + +export const checkBooleanExpressionStatement = ( + triggerTypeData: TriggerTypeData[], + expression: Expression +): boolean | undefined => { + const condition = expression.type === 'booleanExpression' ? expression.condition : undefined; + const trigger = expression.type === 'booleanExpression' ? expression.trigger : undefined; + + let expressionResult: boolean | undefined = false; + + if (condition === undefined || trigger === undefined) return; + + const currentTriggerData = triggerTypeData.find( + (triggerType) => triggerType.type === trigger?.type + ); + + const isFdmTrigger = trigger?.type === 'fdm' && currentTriggerData?.type === 'fdm'; + + if (isFdmTrigger && currentTriggerData.instanceNode.items.length === 0) return; + + const fdmItemsTrigger = + isFdmTrigger && currentTriggerData.instanceNode.items[0] !== undefined + ? currentTriggerData.instanceNode.items[0] + : undefined; + + const fdmPropertyTrigger = isFdmTrigger + ? (fdmItemsTrigger?.properties as FdmPropertyType) + : undefined; + + const propertyTrigger = getFdmPropertyTrigger(fdmPropertyTrigger, trigger); + + switch (condition.type) { + case 'equals': { + expressionResult = propertyTrigger === true; + break; + } + case 'notEquals': { + expressionResult = propertyTrigger === false; + break; + } + } + return expressionResult; +}; diff --git a/react-components/src/components/RuleBasedOutputs/core/checkDatetimeExpressionStatement.ts b/react-components/src/components/RuleBasedOutputs/core/checkDatetimeExpressionStatement.ts new file mode 100644 index 00000000000..a10c6c99d80 --- /dev/null +++ b/react-components/src/components/RuleBasedOutputs/core/checkDatetimeExpressionStatement.ts @@ -0,0 +1,100 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { type FdmPropertyType } from '../../Reveal3DResources/types'; +import { type TriggerTypeData, type DatetimeExpression } from '../types'; +import { getFdmPropertyTrigger } from '../utils'; + +export const checkDatetimeExpressionStatement = ( + triggerTypeData: TriggerTypeData[], + expression: DatetimeExpression +): boolean | undefined => { + const { trigger, condition } = expression; + + let expressionResult: boolean | undefined = false; + + const currentTriggerData = triggerTypeData.find( + (triggerType) => triggerType.type === trigger?.type + ); + + const isFdmTrigger = trigger?.type === 'fdm' && currentTriggerData?.type === 'fdm'; + + if (isFdmTrigger && currentTriggerData.instanceNode.items.length === 0) return; + + const fdmItemsTrigger = + isFdmTrigger && currentTriggerData.instanceNode.items[0] !== undefined + ? currentTriggerData.instanceNode.items[0] + : undefined; + + const fdmPropertyTrigger = isFdmTrigger + ? (fdmItemsTrigger?.properties as FdmPropertyType) + : undefined; + + const propertyTrigger = new Date( + getFdmPropertyTrigger(fdmPropertyTrigger, trigger) ?? '' + ); + + switch (condition.type) { + case 'before': { + const conditionValue = new Date(condition.parameter); + expressionResult = propertyTrigger !== undefined ? propertyTrigger < conditionValue : false; + break; + } + case 'notBefore': { + const conditionValue = new Date(condition.parameter); + expressionResult = propertyTrigger !== undefined ? propertyTrigger >= conditionValue : false; + break; + } + case 'onOrBefore': { + const conditionValue = new Date(condition.parameter); + expressionResult = propertyTrigger !== undefined ? propertyTrigger <= conditionValue : false; + break; + } + case 'between': { + const lowerBound = new Date(condition.lowerBound); + const upperBound = new Date(condition.upperBound); + expressionResult = + propertyTrigger !== undefined + ? lowerBound < propertyTrigger && propertyTrigger < upperBound + : false; + break; + } + case 'notBetween': { + const lowerBound = new Date(condition.lowerBound); + const upperBound = new Date(condition.upperBound); + expressionResult = + propertyTrigger !== undefined + ? !(lowerBound < propertyTrigger && propertyTrigger < upperBound) + : false; + break; + } + case 'after': { + const conditionValue = new Date(condition.parameter); + expressionResult = propertyTrigger !== undefined ? propertyTrigger > conditionValue : false; + break; + } + case 'notAfter': { + const conditionValue = new Date(condition.parameter); + expressionResult = propertyTrigger !== undefined ? propertyTrigger <= conditionValue : false; + break; + } + case 'onOrAfter': { + const conditionValue = new Date(condition.parameter); + expressionResult = propertyTrigger !== undefined ? propertyTrigger >= conditionValue : false; + break; + } + case 'on': { + const conditionValue = new Date(condition.parameter); + expressionResult = propertyTrigger !== undefined ? propertyTrigger === conditionValue : false; + break; + } + case 'notOn': { + const conditionValue = new Date(condition.parameter); + expressionResult = propertyTrigger !== undefined ? propertyTrigger !== conditionValue : false; + break; + } + } + + return expressionResult; +}; diff --git a/react-components/src/components/RuleBasedOutputs/core/checkNumericExpressionStatement.ts b/react-components/src/components/RuleBasedOutputs/core/checkNumericExpressionStatement.ts new file mode 100644 index 00000000000..9da36174909 --- /dev/null +++ b/react-components/src/components/RuleBasedOutputs/core/checkNumericExpressionStatement.ts @@ -0,0 +1,93 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { type FdmPropertyType } from '../../Reveal3DResources/types'; +import { type NumericExpression, type TriggerTypeData } from '../types'; +import { getFdmPropertyTrigger, getTriggerNumericData } from '../utils'; + +export const checkNumericExpressionStatement = ( + triggerTypeData: TriggerTypeData[], + expression: NumericExpression +): boolean | undefined => { + const { trigger, condition } = expression; + + let expressionResult: boolean = false; + let propertyTrigger: number | undefined; + + const currentTriggerData = triggerTypeData.find( + (triggerType) => triggerType.type === trigger?.type + ); + + const isFdmTrigger = trigger?.type === 'fdm' && currentTriggerData?.type === 'fdm'; + + const assetOrTimeseriesDataTrigger = !isFdmTrigger + ? getTriggerNumericData(triggerTypeData, trigger) + : undefined; + + const fdmItemsTrigger = + isFdmTrigger && currentTriggerData.instanceNode.items[0] !== undefined + ? currentTriggerData.instanceNode.items[0] + : undefined; + + const fdmPropertyTrigger = isFdmTrigger + ? (fdmItemsTrigger?.properties as FdmPropertyType) + : undefined; + + if (assetOrTimeseriesDataTrigger === undefined && fdmPropertyTrigger === undefined) return; + + if (isFdmTrigger) { + propertyTrigger = getFdmPropertyTrigger(fdmPropertyTrigger, trigger); + } else { + propertyTrigger = assetOrTimeseriesDataTrigger; + } + + if (propertyTrigger === undefined) return; + + switch (condition.type) { + case 'equals': { + const parameter = condition.parameters[0]; + expressionResult = propertyTrigger === parameter; + break; + } + case 'notEquals': { + const parameter = condition.parameters[0]; + expressionResult = propertyTrigger !== parameter; + break; + } + case 'lessThan': { + const parameter = condition.parameters[0]; + expressionResult = propertyTrigger < parameter; + break; + } + case 'greaterThan': { + const parameter = condition.parameters[0]; + expressionResult = propertyTrigger > parameter; + break; + } + case 'lessThanOrEquals': { + const parameter = condition.parameters[0]; + expressionResult = propertyTrigger <= parameter; + break; + } + case 'greaterThanOrEquals': { + const parameter = condition.parameters[0]; + expressionResult = propertyTrigger >= parameter; + break; + } + case 'within': { + const lower = condition.lowerBoundInclusive; + const upper = condition.upperBoundInclusive; + expressionResult = lower < propertyTrigger && propertyTrigger < upper; + break; + } + case 'outside': { + const lower = condition.lowerBoundExclusive; + const upper = condition.upperBoundExclusive; + expressionResult = propertyTrigger <= lower && upper <= propertyTrigger; + break; + } + } + + return expressionResult; +}; diff --git a/react-components/src/components/RuleBasedOutputs/core/checkStringExpressionStatement.ts b/react-components/src/components/RuleBasedOutputs/core/checkStringExpressionStatement.ts new file mode 100644 index 00000000000..e10cb4f5434 --- /dev/null +++ b/react-components/src/components/RuleBasedOutputs/core/checkStringExpressionStatement.ts @@ -0,0 +1,73 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { type FdmPropertyType } from '../../Reveal3DResources/types'; +import { type StringExpression, type TriggerTypeData } from '../types'; +import { getFdmPropertyTrigger } from '../utils'; + +export const checkStringExpressionStatement = ( + triggerTypeData: TriggerTypeData[], + expression: StringExpression +): boolean | undefined => { + const { trigger, condition } = expression; + + let expressionResult: boolean | undefined = false; + + let propertyTrigger: string | undefined; + + const currentTriggerData = triggerTypeData.find( + (triggerType) => triggerType.type === trigger?.type + ); + + const isMetadataAndAssetTrigger = + trigger?.type === 'metadata' && + currentTriggerData?.type === 'metadata' && + currentTriggerData?.asset !== undefined; + + const isFdmTrigger = trigger?.type === 'fdm' && currentTriggerData?.type === 'fdm'; + + const assetTrigger = isMetadataAndAssetTrigger + ? currentTriggerData?.asset[trigger.type]?.[trigger.key] + : undefined; + + const fdmItemsTrigger = + isFdmTrigger && currentTriggerData.instanceNode.items[0] !== undefined + ? currentTriggerData.instanceNode.items[0] + : undefined; + + const fdmPropertyTrigger = isFdmTrigger + ? (fdmItemsTrigger?.properties as FdmPropertyType) + : undefined; + + if (isMetadataAndAssetTrigger) { + propertyTrigger = assetTrigger; + } else if (isFdmTrigger) { + propertyTrigger = getFdmPropertyTrigger(fdmPropertyTrigger, trigger); + } + + switch (condition.type) { + case 'equals': { + expressionResult = propertyTrigger === condition.parameter; + break; + } + case 'notEquals': { + expressionResult = propertyTrigger !== condition.parameter; + break; + } + case 'contains': { + expressionResult = propertyTrigger?.includes(condition.parameter); + break; + } + case 'startsWith': { + expressionResult = propertyTrigger?.startsWith(condition.parameter); + break; + } + case 'endsWith': { + expressionResult = propertyTrigger?.endsWith(condition.parameter); + break; + } + } + + return expressionResult; +}; diff --git a/react-components/src/components/RuleBasedOutputs/hooks/useCreateRuleInstance.tsx b/react-components/src/components/RuleBasedOutputs/hooks/useCreateRuleInstance.tsx index 54355b99bcf..5ef793f9150 100644 --- a/react-components/src/components/RuleBasedOutputs/hooks/useCreateRuleInstance.tsx +++ b/react-components/src/components/RuleBasedOutputs/hooks/useCreateRuleInstance.tsx @@ -3,9 +3,10 @@ */ import { useFdmSdk } from '../../RevealCanvas/SDKProvider'; import { RULE_BASED_OUTPUTS_VIEW } from '../constants'; -import { type ExternalIdsResultList, type RuleOutputSet } from '../types'; +import { type RuleOutputSet } from '../types'; import { fdmViewsExist } from '../../../utilities/fdmViewsExist'; import { useCallback } from 'react'; +import { type ExternalIdsResultList } from '../../../data-providers/FdmSDK'; export const useCreateRuleInstance = (): (( ruleOutputSet: RuleOutputSet diff --git a/react-components/src/components/RuleBasedOutputs/hooks/useDeleteRuleInstance.tsx b/react-components/src/components/RuleBasedOutputs/hooks/useDeleteRuleInstance.tsx index 9f23e204139..74fffedc1e0 100644 --- a/react-components/src/components/RuleBasedOutputs/hooks/useDeleteRuleInstance.tsx +++ b/react-components/src/components/RuleBasedOutputs/hooks/useDeleteRuleInstance.tsx @@ -4,9 +4,9 @@ import { useCallback } from 'react'; import { useFdmSdk } from '../../RevealCanvas/SDKProvider'; import { RULE_BASED_OUTPUTS_VIEW } from '../constants'; -import { type ExternalIdsResultList, type RuleOutputSet } from '../types'; +import { type RuleOutputSet } from '../types'; import { fdmViewsExist } from '../../../utilities/fdmViewsExist'; -import { type FdmNode } from '../../../data-providers/FdmSDK'; +import { type ExternalIdsResultList, type FdmNode } from '../../../data-providers/FdmSDK'; export const useDeleteRuleInstance = (): (( ruleOutputSet: FdmNode diff --git a/react-components/src/components/RuleBasedOutputs/hooks/useEditRuleInstance.tsx b/react-components/src/components/RuleBasedOutputs/hooks/useEditRuleInstance.tsx index 21f4559ff11..7197de78f57 100644 --- a/react-components/src/components/RuleBasedOutputs/hooks/useEditRuleInstance.tsx +++ b/react-components/src/components/RuleBasedOutputs/hooks/useEditRuleInstance.tsx @@ -3,9 +3,10 @@ */ import { useFdmSdk } from '../../RevealCanvas/SDKProvider'; import { RULE_BASED_OUTPUTS_VIEW } from '../constants'; -import { type ExternalIdsResultList, type RuleOutputSet } from '../types'; +import { type RuleOutputSet } from '../types'; import { fdmViewsExist } from '../../../utilities/fdmViewsExist'; import { useCallback } from 'react'; +import { type ExternalIdsResultList } from '../../../data-providers/FdmSDK'; export const useEditRuleInstance = (): (( ruleOutputSet: RuleOutputSet diff --git a/react-components/src/components/RuleBasedOutputs/types.ts b/react-components/src/components/RuleBasedOutputs/types.ts index 4bdd4311c03..742b0dff87f 100644 --- a/react-components/src/components/RuleBasedOutputs/types.ts +++ b/react-components/src/components/RuleBasedOutputs/types.ts @@ -4,12 +4,17 @@ import { type TreeIndexNodeCollection, type NumericRange } from '@cognite/reveal'; import { type FdmNode, type EdgeItem, type DmsUniqueIdentifier } from '../../data-providers/FdmSDK'; -import { type AssetStylingGroup, type FdmPropertyType } from '../Reveal3DResources/types'; -import { type Datapoints, type Asset, type Timeseries } from '@cognite/sdk'; +import { + type FdmAssetStylingGroup, + type AssetStylingGroup, + type FdmPropertyType +} from '../Reveal3DResources/types'; +import { type Datapoints, type Asset, type Timeseries, type Node3D } from '@cognite/sdk'; +import { type FdmCadConnection } from '../CacheProvider/types'; // =========== RULE BASED OUTPUT DATA MODEL -export type TriggerType = 'timeseries' | 'metadata'; +export type TriggerType = 'timeseries' | 'metadata' | 'fdm'; export type TimeseriesRuleTrigger = { type: 'timeseries'; @@ -21,13 +26,55 @@ export type MetadataRuleTrigger = { key: string; }; +export type FdmRuleTrigger = { + type: 'fdm'; + key: FdmInstanceNodeDataKey; +}; + +export type FdmInstanceNodeDataKey = { + space: string; + externalId: string; + view: Source; + typing: FdmKeyRuleTriggerTyping; + property: string; +}; + export type StringTrigger = MetadataRuleTrigger; +export type BooleanCondition = { + type: 'equals' | 'notEquals'; + parameter: boolean; +}; + export type StringCondition = { type: 'equals' | 'notEquals' | 'contains' | 'startsWith' | 'endsWith'; parameter: string; }; +export type DatetimeCondition = + | { + type: + | 'before' + | 'notBefore' + | 'onOrBefore' + | 'after' + | 'notAfter' + | 'onOrAfter' + | 'on' + | 'notOn'; + parameter: string; + } + | { + type: 'between'; + lowerBound: string; + upperBound: string; + } + | { + type: 'notBetween'; + lowerBound: string; + upperBound: string; + }; + export type NumericCondition = | { type: @@ -52,16 +99,28 @@ export type NumericCondition = export type StringExpression = { type: 'stringExpression'; - trigger: StringTrigger; + trigger: StringTrigger | FdmRuleTrigger; condition: StringCondition; }; export type NumericExpression = { type: 'numericExpression'; - trigger: MetadataRuleTrigger | TimeseriesRuleTrigger; + trigger: MetadataRuleTrigger | TimeseriesRuleTrigger | FdmRuleTrigger; condition: NumericCondition; }; +export type DatetimeExpression = { + type: 'datetimeExpression'; + trigger: FdmRuleTrigger; + condition: DatetimeCondition; +}; + +export type BooleanExpression = { + type: 'booleanExpression'; + trigger: FdmRuleTrigger; + condition: BooleanCondition; +}; + export type ExpressionOperator = | { type: 'or'; @@ -76,7 +135,11 @@ export type ExpressionOperator = expression: Expression; }; -export type ConcreteExpression = StringExpression | NumericExpression; +export type ConcreteExpression = + | StringExpression + | NumericExpression + | DatetimeExpression + | BooleanExpression; export type Expression = ConcreteExpression | ExpressionOperator; @@ -156,6 +219,8 @@ export type FdmRuleOutputSet = { export type ExpressionOperatorsTypes = 'and' | 'or' | 'not'; +export type BooleanConditionTypes = 'equals' | 'notEquals'; + export type StringConditionTypes = 'equals' | 'notEquals' | 'contains' | 'startsWith' | 'endsWith'; export type NumericConditionTypes = @@ -168,6 +233,14 @@ export type NumericConditionTypes = | 'within' | 'outside'; +export type NumericUniqueConditionTypes = + | 'equals' + | 'notEquals' + | 'lessThan' + | 'greaterThan' + | 'lessThanOrEquals' + | 'greaterThanOrEquals'; + export type NumericWithinConditionType = { type: 'within'; lowerBoundInclusive: number; @@ -180,11 +253,31 @@ export type NumericOutsideConditionType = { upperBoundExclusive: number; }; +export type DatetimeConditionTypes = 'between' | 'notBetween' | DatetimeUniqueConditionTypes; + +export type DatetimeUniqueConditionTypes = + | 'before' + | 'notBefore' + | 'onOrBefore' + | 'after' + | 'notAfter' + | 'onOrAfter' + | 'on' + | 'notOn'; + +export type DatetimeBetweenConditionType = { + type: 'between' | 'notBetween'; + lowerBound: string; + upperBound: string; +}; + export type CriteriaTypes = + | BooleanConditionTypes | string | number | NumericWithinConditionType - | NumericOutsideConditionType; + | NumericOutsideConditionType + | DatetimeBetweenConditionType; export type RuleAndStyleIndex = { styleIndex: TreeIndexNodeCollection; @@ -196,6 +289,21 @@ export type AssetStylingGroupAndStyleIndex = { assetStylingGroup: AssetStylingGroup; }; +export type FdmStylingGroupAndStyleIndex = { + styleIndex: TreeIndexNodeCollection; + fdmStylingGroup: FdmAssetStylingGroup; +}; + +export type AllRuleBasedStylingGroups = { + assetStylingGroup: AssetStylingGroup[]; + fdmStylingGroup: FdmAssetStylingGroup[]; +}; + +export type AllMappingStylingGroupAndStyleIndex = { + assetMappingsStylingGroupAndIndex: AssetStylingGroupAndStyleIndex; + fdmStylingGroupAndStyleIndex: FdmStylingGroupAndStyleIndex; +}; + export type NodeAndRange = { treeIndex: number; nodeId: number; @@ -226,27 +334,6 @@ export type ViewQueryFilter = { export type Space = string; -export type ExternalIdsResultList = { - items: Array>; - typing?: Record< - string, - Record< - string, - Record< - string, - { - nullable?: boolean; - autoIncrement?: boolean; - defaultValue?: unknown; - description?: string; - name?: string; - type: { type: string }; - } - > - > - >; -}; - export type NodeItem> = { instanceType: InstanceType; version: number; @@ -271,7 +358,7 @@ export type EmptyRuleForSelectionProps = { isNoSelection: boolean; }; -export type TriggerTypeData = TriggerMetadataType | TriggerTimeseriesType; +export type TriggerTypeData = TriggerMetadataType | TriggerTimeseriesType | TriggerFdmType; export type TriggerMetadataType = { type: 'metadata'; @@ -286,4 +373,84 @@ export type TriggerTimeseriesType = { }; }; +export type TriggerFdmType = { + type: 'fdm'; + instanceNode: FdmInstanceNodeWithConnectionAndProperties; +}; + export type TimeseriesAndDatapoints = Timeseries & Datapoints; + +export type FdmKeyRuleTriggerTyping = Record< + string, + Record< + string, + Record< + string, + { + name: string; + typing: FdmRuleTriggerTyping; + } + > + > +>; + +export type FdmRuleTriggerTyping = { + nullable?: boolean; + autoIncrement?: boolean; + defaultValue?: any; + description?: string; + name?: string; + immutable?: boolean; + container?: { + type?: string; + space?: string; + externalId?: string; + }; + containerPropertyIdentifier?: string; + type: { + collation?: string; + list?: boolean; + type: string; + }; +}; + +export type FdmInstanceWithProperties = NodeItem | EdgeItem; + +export type FdmInstanceWithPropertiesAndTyping = { + items: FdmInstanceWithProperties[]; + typing: FdmTyping; +}; + +export type FdmTyping = Record< + string, + Record< + string, + Record< + string, + { + nullable?: boolean; + autoIncrement?: boolean; + defaultValue?: any; + description?: string; + name?: string; + immutable?: boolean; + type: { collation?: string; list?: boolean; type: string }; + } + > + > +>; + +export type FdmInstanceNodeWithConnectionAndProperties = { + instanceType: 'node'; + version: number; + space: string; + externalId: string; + createdTime: number; + lastUpdatedTime: number; + deletedTime: number; + items: FdmInstanceWithProperties[]; + connection?: FdmCadConnection | undefined; + cadNode?: Node3D | undefined; + view?: Source | undefined; + typing: FdmTyping; +}; diff --git a/react-components/src/components/RuleBasedOutputs/utils.ts b/react-components/src/components/RuleBasedOutputs/utils.ts index c269f2d54e0..77732e2cd31 100644 --- a/react-components/src/components/RuleBasedOutputs/utils.ts +++ b/react-components/src/components/RuleBasedOutputs/utils.ts @@ -4,7 +4,6 @@ import { Color } from 'three'; import { - type StringExpression, type ColorRuleOutput, type RuleOutput, type NumericExpression, @@ -20,67 +19,32 @@ import { type TriggerTypeData, type TimeseriesAndDatapoints, type EmptyRuleForSelection, - type RuleAndEnabled + type RuleAndEnabled, + type FdmStylingGroupAndStyleIndex, + type AllMappingStylingGroupAndStyleIndex, + type FdmRuleTrigger, + type FdmInstanceNodeWithConnectionAndProperties } from './types'; import { NumericRange, TreeIndexNodeCollection, type NodeAppearance } from '@cognite/reveal'; import { type AssetMapping3D, type Asset, type Datapoints } from '@cognite/sdk'; -import { type AssetStylingGroup } from '../Reveal3DResources/types'; +import { + type FdmAssetStylingGroup, + type AssetStylingGroup, + type FdmPropertyType +} from '../Reveal3DResources/types'; import { isDefined } from '../../utilities/isDefined'; import { assertNever } from '../../utilities/assertNever'; import { type AssetIdsAndTimeseries } from '../../data-providers/types'; - -const checkStringExpressionStatement = ( - triggerTypeData: TriggerTypeData[], - expression: StringExpression -): boolean | undefined => { - const { trigger, condition } = expression; - - let expressionResult: boolean | undefined = false; - - const currentTriggerData = triggerTypeData.find( - (triggerType) => triggerType.type === trigger?.type - ); - - const isMetadataAndAssetTrigger = - trigger?.type === 'metadata' && - currentTriggerData?.type === 'metadata' && - currentTriggerData?.asset !== undefined; - - const assetTrigger = isMetadataAndAssetTrigger - ? currentTriggerData?.asset[trigger.type]?.[trigger.key] - : undefined; - - if (assetTrigger === undefined) return; - - switch (condition.type) { - case 'equals': { - expressionResult = assetTrigger === condition.parameter; - break; - } - case 'notEquals': { - expressionResult = assetTrigger !== condition.parameter; - break; - } - case 'contains': { - expressionResult = assetTrigger?.includes(condition.parameter) ?? undefined; - break; - } - case 'startsWith': { - expressionResult = assetTrigger?.startsWith(condition.parameter) ?? undefined; - break; - } - case 'endsWith': { - expressionResult = assetTrigger?.endsWith(condition.parameter) ?? undefined; - break; - } - } - - return expressionResult; -}; - -const getTriggerNumericData = ( +import { uniq } from 'lodash'; +import { type DmsUniqueIdentifier } from '../../data-providers/FdmSDK'; +import { checkNumericExpressionStatement } from './core/checkNumericExpressionStatement'; +import { checkStringExpressionStatement } from './core/checkStringExpressionStatement'; +import { checkDatetimeExpressionStatement } from './core/checkDatetimeExpressionStatement'; +import { checkBooleanExpressionStatement } from './core/checkBooleanExpressionStatement'; + +export const getTriggerNumericData = ( triggerTypeData: TriggerTypeData[], - trigger: MetadataRuleTrigger | TimeseriesRuleTrigger + trigger: MetadataRuleTrigger | TimeseriesRuleTrigger | FdmRuleTrigger ): number | undefined => { const currentTriggerData = triggerTypeData.find( (triggerType) => triggerType.type === trigger?.type @@ -95,7 +59,7 @@ const getTriggerNumericData = ( } }; -const getTriggerTimeseriesNumericData = ( +export const getTriggerTimeseriesNumericData = ( triggerTypeData: TriggerTypeData, trigger: TimeseriesRuleTrigger ): number | undefined => { @@ -111,69 +75,6 @@ const getTriggerTimeseriesNumericData = ( return Number(datapoint); }; -const checkNumericExpressionStatement = ( - triggerTypeData: TriggerTypeData[], - expression: NumericExpression -): boolean | undefined => { - const trigger = expression.trigger; - const condition = expression.condition; - - let expressionResult: boolean = false; - - const dataTrigger = getTriggerNumericData(triggerTypeData, trigger); - - if (dataTrigger === undefined) return; - - switch (condition.type) { - case 'equals': { - const parameter = condition.parameters[0]; - expressionResult = dataTrigger === parameter; - break; - } - case 'notEquals': { - const parameter = condition.parameters[0]; - expressionResult = dataTrigger !== parameter; - break; - } - case 'lessThan': { - const parameter = condition.parameters[0]; - expressionResult = dataTrigger < parameter; - break; - } - case 'greaterThan': { - const parameter = condition.parameters[0]; - expressionResult = dataTrigger > parameter; - break; - } - case 'lessThanOrEquals': { - const parameter = condition.parameters[0]; - expressionResult = dataTrigger <= parameter; - break; - } - case 'greaterThanOrEquals': { - const parameter = condition.parameters[0]; - expressionResult = dataTrigger >= parameter; - break; - } - case 'within': { - const lower = condition.lowerBoundInclusive; - const upper = condition.upperBoundInclusive; - const value = dataTrigger; - expressionResult = lower < value && value < upper; - break; - } - case 'outside': { - const lower = condition.lowerBoundExclusive; - const upper = condition.upperBoundExclusive; - const value = dataTrigger; - expressionResult = value <= lower && upper <= value; - break; - } - } - - return expressionResult; -}; - const getTimeseriesExternalIdFromNumericExpression = ( expression: NumericExpression ): string[] | undefined => { @@ -181,6 +82,8 @@ const getTimeseriesExternalIdFromNumericExpression = ( if (isMetadataTrigger(trigger)) return; + if (isFdmTrigger(trigger)) return; + return [trigger.externalId]; }; @@ -217,6 +120,14 @@ const traverseExpression = ( expressionResult = checkStringExpressionStatement(triggerTypeData, expression); break; } + case 'datetimeExpression': { + expressionResult = checkDatetimeExpressionStatement(triggerTypeData, expression); + break; + } + case 'booleanExpression': { + expressionResult = checkBooleanExpressionStatement(triggerTypeData, expression); + break; + } } expressionResults.push(expressionResult); }); @@ -243,6 +154,8 @@ function forEachExpression( } case 'numericExpression': case 'stringExpression': + case 'datetimeExpression': + case 'booleanExpression': return; default: assertNever(expression); @@ -259,7 +172,12 @@ function getExpressionTriggerTypes(expression: Expression): TriggerType[] { return expression.expressions.flatMap(getExpressionTriggerTypes); } else if (expression.type === 'not') { return getExpressionTriggerTypes(expression.expression); - } else if (expression.type === 'numericExpression' || expression.type === 'stringExpression') { + } else if ( + expression.type === 'numericExpression' || + expression.type === 'stringExpression' || + expression.type === 'datetimeExpression' || + expression.type === 'booleanExpression' + ) { return [expression.trigger.type]; } else { assertNever(expression); @@ -269,16 +187,18 @@ function getExpressionTriggerTypes(expression: Expression): TriggerType[] { export const generateRuleBasedOutputs = async ({ contextualizedAssetNodes, assetMappings, + fdmMappings, ruleSet, assetIdsAndTimeseries, timeseriesDatapoints }: { contextualizedAssetNodes: Asset[]; assetMappings: AssetMapping3D[]; + fdmMappings: FdmInstanceNodeWithConnectionAndProperties[]; ruleSet: RuleOutputSet; assetIdsAndTimeseries: AssetIdsAndTimeseries[]; timeseriesDatapoints: Datapoints[] | undefined; -}): Promise => { +}): Promise => { const outputType = 'color'; // for now it only supports colors as the output const ruleWithOutputs = ruleSet?.rulesWithOutputs; @@ -301,7 +221,7 @@ export const generateRuleBasedOutputs = async ({ if (outputSelected === undefined) return; - return await analyzeNodesAgainstExpression({ + const assetMappingsStylingGroups = await analyzeAssetMappingsAgainstExpression({ contextualizedAssetNodes, assetIdsAndTimeseries, timeseriesDatapoints, @@ -309,6 +229,19 @@ export const generateRuleBasedOutputs = async ({ expression, outputSelected }); + + const fdmMappingsStylingGroups = await analyzeFdmMappingsAgainstExpression({ + fdmMappings, + expression, + outputSelected + }); + + const allStyling: AllMappingStylingGroupAndStyleIndex = { + assetMappingsStylingGroupAndIndex: assetMappingsStylingGroups, + fdmStylingGroupAndStyleIndex: fdmMappingsStylingGroups + }; + + return allStyling; }) ) ).filter(isDefined); @@ -332,7 +265,7 @@ const getRuleOutputFromTypeSelected = ( return outputSelected; }; -const analyzeNodesAgainstExpression = async ({ +const analyzeAssetMappingsAgainstExpression = async ({ contextualizedAssetNodes, assetIdsAndTimeseries, timeseriesDatapoints, @@ -347,55 +280,88 @@ const analyzeNodesAgainstExpression = async ({ expression: Expression; outputSelected: ColorRuleOutput; }): Promise => { - const allTreeNodes = await Promise.all( - contextualizedAssetNodes.map(async (contextualizedAssetNode) => { - const triggerData: TriggerTypeData[] = []; + const allAssetMappingsTreeNodes: AssetMapping3D[][] = []; + + for (const contextualizedAssetNode of contextualizedAssetNodes) { + const triggerData: TriggerTypeData[] = []; + + const metadataTriggerData: TriggerTypeData = { + type: 'metadata', + asset: contextualizedAssetNode + }; + + triggerData.push(metadataTriggerData); + + if ( + timeseriesDatapoints !== undefined && + timeseriesDatapoints.length > 0 && + assetIdsAndTimeseries !== undefined && + assetIdsAndTimeseries.length > 0 + ) { + const timeseriesDataForThisAsset = generateTimeseriesAndDatapointsFromTheAsset({ + contextualizedAssetNode, + assetIdsAndTimeseries, + timeseriesDatapoints + }); - const metadataTriggerData: TriggerTypeData = { - type: 'metadata', - asset: contextualizedAssetNode - }; + if (timeseriesDataForThisAsset.length > 0) { + const timeseriesTriggerData: TriggerTypeData = { + type: 'timeseries', + timeseries: { + timeseriesWithDatapoints: timeseriesDataForThisAsset, + linkedAssets: contextualizedAssetNode + } + }; - triggerData.push(metadataTriggerData); + triggerData.push(timeseriesTriggerData); + } + } - if ( - timeseriesDatapoints !== undefined && - timeseriesDatapoints.length > 0 && - assetIdsAndTimeseries !== undefined && - assetIdsAndTimeseries.length > 0 - ) { - const timeseriesDataForThisAsset = generateTimeseriesAndDatapointsFromTheAsset({ - contextualizedAssetNode, - assetIdsAndTimeseries, - timeseriesDatapoints - }); + const finalGlobalOutputResult = traverseExpression(triggerData, [expression]); - if (timeseriesDataForThisAsset.length > 0) { - const timeseriesTriggerData: TriggerTypeData = { - type: 'timeseries', - timeseries: { - timeseriesWithDatapoints: timeseriesDataForThisAsset, - linkedAssets: contextualizedAssetNode - } - }; + if (finalGlobalOutputResult[0] ?? false) { + const nodesFromThisAsset = assetMappings.filter( + (item) => item.assetId === contextualizedAssetNode.id + ); - triggerData.push(timeseriesTriggerData); - } - } + allAssetMappingsTreeNodes.push(nodesFromThisAsset); + } + } + + const filteredAllAssetMappingsTreeNodes = allAssetMappingsTreeNodes.flat(); + return applyAssetMappingsNodeStyles(filteredAllAssetMappingsTreeNodes, outputSelected); +}; + +const analyzeFdmMappingsAgainstExpression = async ({ + fdmMappings, + expression, + outputSelected +}: { + fdmMappings: FdmInstanceNodeWithConnectionAndProperties[]; + expression: Expression; + outputSelected: ColorRuleOutput; +}): Promise => { + const allFdmtMappingsTreeNodes = await Promise.all( + fdmMappings.map(async (mapping) => { + const triggerData: TriggerTypeData[] = []; + + const fdmTriggerData: TriggerTypeData = { + type: 'fdm', + instanceNode: mapping + }; + + triggerData.push(fdmTriggerData); const finalGlobalOutputResult = traverseExpression(triggerData, [expression]); if (finalGlobalOutputResult[0] ?? false) { - const nodesFromThisAsset = assetMappings.filter( - (item) => item.assetId === contextualizedAssetNode.id - ); - return nodesFromThisAsset; + return mapping; } }) ); - const filteredAllTreeNodes = allTreeNodes.flat().filter(isDefined); - return applyNodeStyles(filteredAllTreeNodes, outputSelected); + const filteredAllFdmMappingsTreeNodes = allFdmtMappingsTreeNodes.flat().filter(isDefined); + return applyFdmMappingsNodeStyles(filteredAllFdmMappingsTreeNodes, outputSelected); }; const generateTimeseriesAndDatapointsFromTheAsset = ({ @@ -411,18 +377,22 @@ const generateTimeseriesAndDatapointsFromTheAsset = ({ (item) => item.assetIds?.externalId === contextualizedAssetNode.externalId ); - const timeseries = timeseriesLinkedToThisAsset?.map((item) => item.timeseries).filter(isDefined); const datapoints = timeseriesDatapoints?.filter((datapoint) => - timeseries?.find((item) => item?.externalId === datapoint.externalId) + timeseriesLinkedToThisAsset?.find( + (item) => item?.timeseries?.externalId === datapoint.externalId + ) ); - const timeseriesData: TimeseriesAndDatapoints[] = timeseries + const timeseriesData: TimeseriesAndDatapoints[] = timeseriesLinkedToThisAsset .map((item) => { - const datapoint = datapoints?.find((datapoint) => datapoint.externalId === item.externalId); + if (item.timeseries === undefined) return undefined; + const datapoint = datapoints?.find( + (datapoint) => datapoint.externalId === item.timeseries?.externalId + ); if (datapoint === undefined) return undefined; const content: TimeseriesAndDatapoints = { - ...item, + ...item.timeseries, ...datapoint }; return content; @@ -455,10 +425,10 @@ export const traverseExpressionToGetTimeseries = ( return timeseriesExternalIdFound?.filter(isDefined) ?? []; }) .flat(); - return timeseriesExternalIdResults; + return uniq(timeseriesExternalIdResults); }; -const applyNodeStyles = ( +const applyAssetMappingsNodeStyles = ( treeNodes: AssetMapping3D[], outputSelected: ColorRuleOutput ): AssetStylingGroupAndStyleIndex => { @@ -469,17 +439,23 @@ const applyNodeStyles = ( const nodeIndexSet = ruleOutputAndStyleIndex.styleIndex.getIndexSet(); nodeIndexSet.clear(); - treeNodes?.forEach((node) => { + + const assetIds: number[] = []; + + for (const node of treeNodes) { const range = new NumericRange(node.treeIndex, node.subtreeSize); nodeIndexSet.addRange(range); - }); + + assetIds.push(node.assetId); + } ruleOutputAndStyleIndex.styleIndex.updateSet(nodeIndexSet); const nodeAppearance: NodeAppearance = { color: new Color(outputSelected.fill) }; + const assetStylingGroup: AssetStylingGroup = { - assetIds: treeNodes.map((node) => node.assetId), + assetIds, style: { cad: nodeAppearance } }; @@ -490,18 +466,74 @@ const applyNodeStyles = ( return stylingGroup; }; +const applyFdmMappingsNodeStyles = ( + treeNodes: FdmInstanceNodeWithConnectionAndProperties[], + outputSelected: ColorRuleOutput +): FdmStylingGroupAndStyleIndex => { + const ruleOutputAndStyleIndex: RuleAndStyleIndex = { + styleIndex: new TreeIndexNodeCollection(), + ruleOutputParams: outputSelected + }; + + const nodeAppearance: NodeAppearance = { + color: new Color(outputSelected.fill) + }; + + const fdmAssetExternalIds: DmsUniqueIdentifier[] = []; + + const nodeIndexSet = ruleOutputAndStyleIndex.styleIndex.getIndexSet(); + nodeIndexSet.clear(); + + for (const node of treeNodes) { + if (node.cadNode === undefined) continue; + + const range = new NumericRange(node.cadNode.treeIndex, node.cadNode.subtreeSize); + nodeIndexSet.addRange(range); + + if (node.connection === undefined) continue; + fdmAssetExternalIds.push({ + space: node.connection?.instance.space, + externalId: node.connection?.instance.externalId + }); + } + + ruleOutputAndStyleIndex.styleIndex.updateSet(nodeIndexSet); + + const fdmStylingGroup: FdmAssetStylingGroup = { + fdmAssetExternalIds, + style: { cad: nodeAppearance } + }; + + const stylingGroup: FdmStylingGroupAndStyleIndex = { + styleIndex: ruleOutputAndStyleIndex.styleIndex, + fdmStylingGroup + }; + return stylingGroup; +}; + const isMetadataTrigger = ( - trigger: MetadataRuleTrigger | TimeseriesRuleTrigger + trigger: MetadataRuleTrigger | TimeseriesRuleTrigger | FdmRuleTrigger ): trigger is MetadataRuleTrigger => { return trigger.type === 'metadata'; }; +const isFdmTrigger = ( + trigger: MetadataRuleTrigger | TimeseriesRuleTrigger | FdmRuleTrigger +): trigger is FdmRuleTrigger => { + return trigger.type === 'fdm'; +}; + const convertExpressionStringMetadataKeyToLowerCase = (expression: Expression): void => { - if (expression.type !== 'stringExpression') { + if ( + expression.type !== 'stringExpression' || + (expression.type === 'stringExpression' && expression.trigger.type === 'fdm') + ) return; - } - expression.trigger.key = expression.trigger.key.toLowerCase(); + expression.trigger.key = + expression.trigger.type === 'metadata' + ? expression.trigger.key.toLowerCase() + : expression.trigger.key; }; export const generateEmptyRuleForSelection = (name: string): EmptyRuleForSelection => { @@ -524,3 +556,18 @@ export const getRuleBasedById = ( ): RuleAndEnabled | undefined => { return ruleInstances?.find((item) => item.rule.properties.id === id); }; + +export function getFdmPropertyTrigger( + fdmPropertyTrigger: FdmPropertyType | undefined, + trigger: FdmRuleTrigger +): T | undefined { + if (fdmPropertyTrigger === undefined) return; + + const space = fdmPropertyTrigger[trigger.key.space]; + const instanceProperties = space?.[ + `${trigger.key.view.externalId}/${trigger.key.view.version}` + ] as FdmPropertyType; + const property = instanceProperties?.[trigger.key.property] as T; + + return property; +} diff --git a/react-components/src/index.ts b/react-components/src/index.ts index 5165d53b1b9..8cf30df7485 100644 --- a/react-components/src/index.ts +++ b/react-components/src/index.ts @@ -143,10 +143,18 @@ export type { RuleOutputSet, TimeseriesRuleTrigger, MetadataRuleTrigger, + FdmRuleTrigger, + FdmKeyRuleTriggerTyping, + FdmRuleTriggerTyping, + FdmInstanceNodeDataKey, StringCondition, NumericCondition, + DatetimeCondition, + BooleanCondition, StringExpression, NumericExpression, + DatetimeExpression, + BooleanExpression, ExpressionOperator, Expression, ConcreteExpression, @@ -157,9 +165,15 @@ export type { ExpressionOperatorsTypes, StringConditionTypes, NumericConditionTypes, + NumericUniqueConditionTypes, NumericWithinConditionType, NumericOutsideConditionType, - CriteriaTypes + DatetimeConditionTypes, + DatetimeUniqueConditionTypes, + DatetimeBetweenConditionType, + BooleanConditionTypes, + CriteriaTypes, + AllRuleBasedStylingGroups } from './components/RuleBasedOutputs/types'; export { RuleBasedOutputsPanel } from './components/RuleBasedOutputs/RuleBasedOutputsPanel'; diff --git a/react-components/src/query/use3dRelatedDirectConnections.ts b/react-components/src/query/use3dRelatedDirectConnections.ts index 71740214153..7205241e552 100644 --- a/react-components/src/query/use3dRelatedDirectConnections.ts +++ b/react-components/src/query/use3dRelatedDirectConnections.ts @@ -58,7 +58,7 @@ export function use3dRelatedDirectConnections( .map((viewList, objectInd) => viewList.map((view) => [objectInd, view] as const)) .flat(); - const [deduplicatedViews, viewToDeduplicatedIndexMap] = createDeduplicatediewToIndexMap( + const [deduplicatedViews, viewToDeduplicatedIndexMap] = createDeduplicatedViewToIndexMap( relatedObjectViewsWithObjectIndex ); @@ -86,7 +86,7 @@ function createViewKey(source: Source): ViewKey { return `${source.externalId}/${source.space}/${source.version}`; } -function createDeduplicatediewToIndexMap( +function createDeduplicatedViewToIndexMap( viewsWithObjectIndex: Array ): [Source[], Map] { const deduplicatedViews: Source[] = []; diff --git a/react-components/src/query/useAll3dDirectConnectionsWithProperties.ts b/react-components/src/query/useAll3dDirectConnectionsWithProperties.ts new file mode 100644 index 00000000000..9d73befa29d --- /dev/null +++ b/react-components/src/query/useAll3dDirectConnectionsWithProperties.ts @@ -0,0 +1,146 @@ +/*! + * Copyright 2023 Cognite AS + */ + +import { type UseQueryResult, useQuery } from '@tanstack/react-query'; +import { useFdmSdk } from '../components/RevealCanvas/SDKProvider'; +import { type FdmConnectionWithNode } from '../components/CacheProvider/types'; +import { type InstanceType } from '@cognite/sdk'; +import { chunk, uniqBy } from 'lodash'; +import { + type FdmInstanceNodeWithConnectionAndProperties, + type FdmInstanceWithPropertiesAndTyping +} from '../components/RuleBasedOutputs/types'; +import { useMemo } from 'react'; +import { createFdmKey } from '../components/CacheProvider/idAndKeyTranslation'; + +export function useAll3dDirectConnectionsWithProperties( + connectionWithNodeAndView: FdmConnectionWithNode[] +): UseQueryResult { + const fdmSdk = useFdmSdk(); + + const connectionKeys = useMemo(() => { + return connectionWithNodeAndView.map((item) => { + const fdmKey = createFdmKey( + item.connection.instance.space, + item.connection.instance.externalId + ); + return fdmKey; + }); + }, [connectionWithNodeAndView]); + + const connectionWithNodeAndViewMap = useMemo(() => { + return new Map( + connectionWithNodeAndView.map((item) => { + const fdmKey = createFdmKey( + item.connection.instance.space, + item.connection.instance.externalId + ); + return [fdmKey, item]; + }) + ); + }, [connectionWithNodeAndView]); + + return useQuery({ + queryKey: [ + 'reveal-react-components', + 'get-all-3d-related-direct-connections', + ...connectionKeys.sort() + ], + queryFn: async () => { + const instanceType: InstanceType = 'node'; + const instancesData = connectionWithNodeAndView.map((item) => { + return { + externalId: item.connection.instance.externalId, + space: item.connection.instance.space, + instanceType + }; + }); + + const instancesViews = connectionWithNodeAndView.map((item) => { + return item.view; + }); + + const uniqueViews = uniqBy(instancesViews, (item) => { + if (item === undefined) { + return ''; + } + const fdmKey = createFdmKey(item?.space, item?.externalId); + return fdmKey; + }); + + const instancesDataChunks = chunk(instancesData, 1000); + + const instancesContent = await Promise.all( + instancesDataChunks.flatMap((chunk) => { + return uniqueViews.map(async (view) => { + return await fdmSdk.getByExternalIds(chunk, view); + }); + }) + ); + + if (instancesContent === undefined) { + return []; + } + + const instancesContentChunks = chunk(instancesContent, 1000); + + const relatedObjectInspectionsResult = await Promise.all( + instancesContentChunks.flatMap((instances) => + instances.flatMap(async (item) => { + const items = item.items.map((fdmId) => ({ + space: fdmId.space, + externalId: fdmId.externalId, + instanceType + })); + return await fdmSdk.inspectInstances({ + inspectionOperations: { involvedViews: {} }, + items + }); + }) + ) + ); + + const instanceItemsAndTyping: FdmInstanceWithPropertiesAndTyping[] = + relatedObjectInspectionsResult + .flatMap((inspectionResult) => + inspectionResult.items.flatMap((inspectionResultItem) => + instancesContent.flatMap((instanceContent) => { + const node: FdmInstanceWithPropertiesAndTyping = { + items: instanceContent.items.filter( + (item) => + item.space === inspectionResultItem.space && + item.externalId === inspectionResultItem.externalId + ), + typing: instanceContent.typing ?? {} + }; + return node; + }) + ) + ) + .filter((item) => item.items.length > 0); + + const instanceWithData = + instanceItemsAndTyping?.flatMap((itemsData) => { + let connectionFound: FdmConnectionWithNode | undefined; + + itemsData.items.every((itemData) => { + const fdmKey = createFdmKey(itemData.space, itemData.externalId); + if (connectionWithNodeAndViewMap.has(fdmKey)) { + connectionFound = connectionWithNodeAndViewMap.get(fdmKey); + return false; + } + return true; + }); + + return { + ...connectionFound, + ...itemsData + }; + }) ?? []; + + return instanceWithData; + }, + enabled: connectionWithNodeAndView.length > 0 + }); +} diff --git a/react-components/src/query/useAssetsAndTimeseriesLinkageDataQuery.ts b/react-components/src/query/useAssetsAndTimeseriesLinkageDataQuery.ts index c57288bca66..e921afd544f 100644 --- a/react-components/src/query/useAssetsAndTimeseriesLinkageDataQuery.ts +++ b/react-components/src/query/useAssetsAndTimeseriesLinkageDataQuery.ts @@ -72,7 +72,9 @@ export function useAssetsAndTimeseriesLinkageDataQuery({ .flat() .filter(isDefined) ?? []; - const assetFromTimeseries = await getAssetsByIds(sdk, assetIdsFound); + const assetFromTimeseries = + assetIdsFound.length > 0 ? await getAssetsByIds(sdk, assetIdsFound) : []; + const assetIdsWithTimeseries = timeseries ?.map((timeseries): AssetIdsAndTimeseries[] | undefined => {