Skip to content

Commit

Permalink
style: add instances tree (#4582)
Browse files Browse the repository at this point in the history
* style: add instances tree

* refactor: add label to tree view and remove unnecessary map index

* refactor: pass default tree node props instead of manually setting them

* refactor: set default value to isRoot
  • Loading branch information
huygur committed May 31, 2023
1 parent 3e497f2 commit 65bc393
Show file tree
Hide file tree
Showing 8 changed files with 804 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,51 @@
* except in compliance with the proprietary license.
*/

import React from 'react';
import React, {useRef} from 'react';
import {FlowNodeInstancesTree} from '../FlowNodeInstancesTree';
import {observer} from 'mobx-react';
import {Container, PanelHeader} from './styled';
import {flowNodeInstanceStore} from 'modules/stores/flowNodeInstance';
import {Container, NodeContainer, InstanceHistory, PanelHeader} from './styled';
import {processInstanceDetailsDiagramStore} from 'modules/stores/processInstanceDetailsDiagram';
import {TimeStampPill} from './TimeStampPill';
import {modificationsStore} from 'modules/stores/modifications';
import {TreeView} from '@carbon/react';

const FlowNodeInstanceLog: React.FC = observer(() => {
const {instanceExecutionHistory, isInstanceExecutionHistoryAvailable} =
flowNodeInstanceStore;
const {areDiagramDefinitionsAvailable} = processInstanceDetailsDiagramStore;

const flowNodeInstanceRowRef = useRef<HTMLDivElement>(null);
const instanceHistoryRef = useRef<HTMLDivElement>(null);

return (
<Container>
<PanelHeader title="Instance History" size="sm">
{!modificationsStore.isModificationModeEnabled && <TimeStampPill />}
</PanelHeader>
<section>instance history</section>
{areDiagramDefinitionsAvailable && isInstanceExecutionHistoryAvailable ? (
<InstanceHistory
data-testid="instance-history"
ref={instanceHistoryRef}
>
<NodeContainer>
<TreeView
label={`${instanceExecutionHistory!.flowNodeId} instance history`}
hideLabel
>
<FlowNodeInstancesTree
rowRef={flowNodeInstanceRowRef}
scrollableContainerRef={instanceHistoryRef}
flowNodeInstance={instanceExecutionHistory!}
isRoot
/>
</TreeView>
</NodeContainer>
</InstanceHistory>
) : (
<div>skeleton</div>
)}
</Container>
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,24 @@ import {PanelHeader as BasePanelHeader} from 'modules/components/Carbon/PanelHea

const Container = styled.div`
border-right: solid 1px var(--cds-border-subtle-01);
background-color: var(--cds-layer-01);
display: flex;
flex-direction: column;
`;

const PanelHeader = styled(BasePanelHeader)`
justify-content: flex-start;
`;

export {Container, PanelHeader};
const InstanceHistory = styled.div`
position: relative;
height: 100%;
overflow: auto;
`;

const NodeContainer = styled.div`
position: absolute;
width: 100%;
`;

export {NodeContainer, InstanceHistory, PanelHeader, Container};
263 changes: 263 additions & 0 deletions client/src/App/Carbon/ProcessInstance/FlowNodeInstancesTree/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
/*
* Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
* under one or more contributor license agreements. Licensed under a proprietary license.
* See the License.txt file for more information. You may not use this file
* except in compliance with the proprietary license.
*/

import React, {useRef} from 'react';
import {observer} from 'mobx-react';
import {processInstanceDetailsDiagramStore} from 'modules/stores/processInstanceDetailsDiagram';
import {processInstanceDetailsStore} from 'modules/stores/processInstanceDetails';
import {flowNodeSelectionStore} from 'modules/stores/flowNodeSelection';
import {
flowNodeInstanceStore,
FlowNodeInstance,
MAX_INSTANCES_STORED,
} from 'modules/stores/flowNodeInstance';
import {InfiniteScroller} from 'modules/components/InfiniteScroller';
import {tracking} from 'modules/tracking';
import {modificationsStore} from 'modules/stores/modifications';
import {instanceHistoryModificationStore} from 'modules/stores/instanceHistoryModification';
import {TreeNode, NodeName} from './styled';
import {FlowNodeIcon} from 'modules/components/Carbon/FlowNodeIcon';
import {isSubProcess} from 'modules/bpmn-js/utils/isSubProcess';

type Props = {
flowNodeInstance: FlowNodeInstance;
isRoot?: boolean;
rowRef?: React.Ref<HTMLDivElement>;
scrollableContainerRef: React.RefObject<HTMLElement>;
};

const getVisibleChildPlaceholders = ({
isModificationModeEnabled,
flowNodeInstance,
}: {
isModificationModeEnabled: boolean;
flowNodeInstance: FlowNodeInstance;
}) => {
const {state, isPlaceholder, flowNodeId, id} = flowNodeInstance;
if (
!isModificationModeEnabled ||
(!isPlaceholder &&
(state === undefined || !['ACTIVE', 'INCIDENT'].includes(state)))
) {
return [];
}

return instanceHistoryModificationStore.getVisibleChildPlaceholders(
id,
flowNodeId,
isPlaceholder
);
};

const ScrollableNodes: React.FC<
Omit<React.ComponentProps<typeof InfiniteScroller>, 'children'> & {
visibleChildren: FlowNodeInstance[];
}
> = ({
onVerticalScrollEndReach,
onVerticalScrollStartReach,
visibleChildren,
scrollableContainerRef,
...carbonTreeNodeProps
}) => {
return (
<InfiniteScroller
onVerticalScrollEndReach={onVerticalScrollEndReach}
onVerticalScrollStartReach={onVerticalScrollStartReach}
scrollableContainerRef={scrollableContainerRef}
>
<ul>
{visibleChildren.map((childNode: FlowNodeInstance) => {
return (
<FlowNodeInstancesTree
flowNodeInstance={childNode}
key={childNode.id}
scrollableContainerRef={scrollableContainerRef}
{...carbonTreeNodeProps}
/>
);
})}
</ul>
</InfiniteScroller>
);
};

const FlowNodeInstancesTree: React.FC<Props> = observer(
({flowNodeInstance, scrollableContainerRef, isRoot = false, ...rest}) => {
const {fetchSubTree, removeSubTree, getVisibleChildNodes} =
flowNodeInstanceStore;

const isProcessInstance =
flowNodeInstance.id ===
processInstanceDetailsStore.state.processInstance?.id;

const visibleChildNodes = getVisibleChildNodes(flowNodeInstance);

const visibleChildPlaceholders: FlowNodeInstance[] =
getVisibleChildPlaceholders({
isModificationModeEnabled: modificationsStore.isModificationModeEnabled,
flowNodeInstance,
});

const visibleChildren = [...visibleChildNodes, ...visibleChildPlaceholders];

const hasVisibleChildPlaceholders = visibleChildPlaceholders.length > 0;
const hasVisibleChildNodes = visibleChildNodes.length > 0;

const businessObject = isProcessInstance
? processInstanceDetailsDiagramStore.processBusinessObject
: processInstanceDetailsDiagramStore.businessObjects[
flowNodeInstance.flowNodeId
];

const isMultiInstanceBody = flowNodeInstance.type === 'MULTI_INSTANCE_BODY';

const isFoldable =
isMultiInstanceBody || isSubProcess(businessObject) || isRoot;

const hasChildren = flowNodeInstance.isPlaceholder
? isFoldable &&
instanceHistoryModificationStore.hasChildPlaceholders(
flowNodeInstance.id
)
: isFoldable;

const isSelected = flowNodeSelectionStore.isSelected({
flowNodeInstanceId: flowNodeInstance.id,
flowNodeId: flowNodeInstance.flowNodeId,
isMultiInstance: isMultiInstanceBody,
});

const rowRef = useRef<HTMLDivElement>(null);

const expandSubtree = (flowNodeInstance: FlowNodeInstance) => {
if (!flowNodeInstance.isPlaceholder) {
fetchSubTree({treePath: flowNodeInstance.treePath});
} else {
instanceHistoryModificationStore.appendExpandedFlowNodeInstanceIds(
flowNodeInstance.id
);
}
};

const collapseSubtree = (flowNodeInstance: FlowNodeInstance) => {
if (!flowNodeInstance.isPlaceholder) {
removeSubTree({
treePath: flowNodeInstance.treePath,
});
} else {
instanceHistoryModificationStore.removeFromExpandedFlowNodeInstanceIds(
flowNodeInstance.id
);
}
};

const handleEndReach = async (scrollUp: (distance: number) => void) => {
if (flowNodeInstance?.treePath === undefined) {
return;
}
if (flowNodeInstance.treePath !== null) {
const fetchedInstancesCount = await flowNodeInstanceStore.fetchNext(
flowNodeInstance.treePath
);

// This ensures that the container is scrolling up when MAX_INSTANCES_STORED is reached.
if (
fetchedInstancesCount !== undefined &&
flowNodeInstanceStore.state.flowNodeInstances[
flowNodeInstance.treePath
]?.children.length === MAX_INSTANCES_STORED
) {
scrollUp(fetchedInstancesCount * (rowRef.current?.offsetHeight ?? 0));
}
}
};

const nodeName = `${businessObject?.name || flowNodeInstance.flowNodeId}${
isMultiInstanceBody ? ` (Multi Instance)` : ''
}`;

let {rowRef: _, ...carbonTreeNodeProps} = rest;
return (
<TreeNode
{...carbonTreeNodeProps}
selected={isSelected ? [flowNodeInstance.id] : []}
active={isSelected ? flowNodeInstance.id : undefined}
key={flowNodeInstance.id}
id={flowNodeInstance.id}
value={flowNodeInstance.id}
renderIcon={() => (
<FlowNodeIcon
flowNodeInstanceType={flowNodeInstance.type}
diagramBusinessObject={businessObject!}
/>
)}
onSelect={() => {
if (modificationsStore.state.status === 'adding-token') {
modificationsStore.finishAddingToken(
flowNodeInstance.flowNodeId,
flowNodeInstance.id
);
} else {
tracking.track({eventName: 'instance-history-item-clicked'});
flowNodeSelectionStore.selectFlowNode({
flowNodeId: isProcessInstance
? undefined
: flowNodeInstance.flowNodeId,
flowNodeInstanceId: flowNodeInstance.id,
isMultiInstance: isMultiInstanceBody,
isPlaceholder: flowNodeInstance.isPlaceholder,
});
}
}}
onToggle={
isFoldable
? (event) => {
event.stopPropagation();
return (flowNodeInstance.isPlaceholder &&
hasVisibleChildPlaceholders) ||
(!flowNodeInstance.isPlaceholder && hasVisibleChildNodes)
? collapseSubtree(flowNodeInstance)
: expandSubtree(flowNodeInstance);
}
: undefined
}
isExpanded={
flowNodeInstance.isPlaceholder
? hasVisibleChildPlaceholders
: hasVisibleChildNodes
}
label={<NodeName>{nodeName}</NodeName>}
>
{hasChildren ? (
<ScrollableNodes
onVerticalScrollEndReach={handleEndReach}
onVerticalScrollStartReach={async (scrollDown) => {
if (flowNodeInstance.treePath === null) {
return;
}
const fetchedInstancesCount =
await flowNodeInstanceStore.fetchPrevious(
flowNodeInstance.treePath
);

if (fetchedInstancesCount !== undefined) {
scrollDown(
fetchedInstancesCount * (rowRef.current?.offsetHeight ?? 0)
);
}
}}
scrollableContainerRef={scrollableContainerRef}
visibleChildren={visibleChildren}
/>
) : undefined}
</TreeNode>
);
}
);

export {FlowNodeInstancesTree};
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
* under one or more contributor license agreements. Licensed under a proprietary license.
* See the License.txt file for more information. You may not use this file
* except in compliance with the proprietary license.
*/

import styled from 'styled-components';
import {TreeNode as BaseTreeNode} from '@carbon/react';

const TreeNode = styled(BaseTreeNode)`
.cds--tree-node__label {
height: 2rem;
}
`;

const NodeName = styled.span`
margin-left: var(--cds-spacing-02);
`;

export {TreeNode, NodeName};

0 comments on commit 65bc393

Please sign in to comment.