Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: topology group node #2477

Merged
merged 15 commits into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions projects/assets-library/assets/icons/folder.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions projects/assets-library/src/icons/icon-library.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const iconsRootPath = 'assets/icons';
{ key: IconType.EyeVisibilityOff, url: `${iconsRootPath}/eye-visibility-off.svg` },
{ key: IconType.FileCode, url: `${iconsRootPath}/file-code.svg` },
{ key: IconType.Filter, url: `${iconsRootPath}/filter.svg` },
{ key: IconType.Folder, url: `${iconsRootPath}/folder.svg` },
{ key: IconType.Go, url: `${iconsRootPath}/go.svg` },
{ key: IconType.Helm, url: `${iconsRootPath}/helm.svg` },
{ key: IconType.Hypertrace, url: `${iconsRootPath}/hypertrace.svg` },
Expand Down
1 change: 1 addition & 0 deletions projects/assets-library/src/icons/icon-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export const enum IconType {
FileCode = 'svg:file-code',
Filter = 'svg:filter',
First = 'first_page',
Folder = 'svg:folder',
Go = 'svg:go',
Helm = 'svg:helm',
Home = 'home',
Expand Down
3 changes: 3 additions & 0 deletions projects/observability/src/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ export * from './shared/components/topology/renderers/node/topology-node-rendere
export * from './shared/components/topology/renderers/tooltip/topology-tooltip-renderer.service';
export * from './shared/components/topology/topology.component';
export * from './shared/components/topology/topology.module';
export * from './shared/components/topology/d3/layouts/graph-layout';
export * from './shared/dashboard/data/graphql/topology/topology-data-source.model';
export * from './shared/dashboard/data/graphql/topology/metrics/topology-metric-category.model';
export * from './shared/dashboard/data/graphql/topology/metrics/topology-metric-with-category.model';
Expand All @@ -306,8 +307,10 @@ export * from './shared/dashboard/widgets/topology/edge/curved/entity-edge-curve
export * from './shared/dashboard/widgets/topology/node/box/api-node-renderer/api-node-box-renderer.service';
export * from './shared/dashboard/widgets/topology/node/box/backend-node-renderer/backend-node-box-renderer.service';
export * from './shared/dashboard/widgets/topology/node/box/service-node-renderer/service-node-box-renderer.service';
export * from './shared/dashboard/widgets/topology/node/box/group-node/group-node-box-renderer.service';
export * from './shared/dashboard/widgets/topology/tooltip/topology-entity-tooltip.component';
export * from './shared/dashboard/widgets/topology/visibility-updater';
export * from './shared/components/topology/utils/topology-group-node.util';

// Topology Metric
export * from './shared/dashboard/widgets/topology/metric/edge-metric-category';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable max-lines */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

break up this class :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved the group node related code to a util class, but still it is going over around 20 lines 😑

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to break it up further. Can do it in a follow up.

import { ComponentPortal, DomPortalOutlet } from '@angular/cdk/portal';
import {
ApplicationRef,
Expand Down Expand Up @@ -44,6 +45,7 @@
import { ForceLayout } from './layouts/force-layout';
import { GraphLayout } from './layouts/graph-layout';
import { TreeLayout } from './layouts/tree-layout';
import { TopologyGroupNodeUtil } from '../utils/topology-group-node.util';

export class D3Topology implements Topology {
private static readonly CONTAINER_CLASS: string = 'topology-internal-container';
Expand All @@ -62,8 +64,9 @@
protected container?: HTMLDivElement;
protected tooltip?: TopologyTooltip;
protected layout: TopologyLayout;
protected readonly supportGroupNode: boolean;

protected readonly userNodes: TopologyNode[];
protected userNodes: TopologyNode[];
protected readonly nodeRenderer: TopologyNodeRenderer;
protected readonly edgeRenderer: TopologyEdgeRenderer;
protected readonly domRenderer: Renderer2;
Expand All @@ -84,18 +87,21 @@
this.edgeRenderer = config.edgeRenderer;
this.domRenderer = injector.get(Renderer2 as Type<Renderer2>);
this.stateManager = new TopologyStateManager(this.config);
this.supportGroupNode = this.config.supportGroupNode ?? false;
this.topologyData = this.topologyConverter.convertTopology(
this.userNodes,
this.stateManager,
this.nodeRenderer,
this.domRenderer,
undefined,
this.supportGroupNode,
);
this.d3Util = injector.get(D3UtilService);
this.drag = new TopologyNodeDrag(this.d3Util, this.domRenderer);
this.hover = new TopologyHover(this.d3Util, this.domRenderer);
this.click = new TopologyClick(this.d3Util, this.domRenderer);
this.zoom = new TopologyZoom();
this.layout = this.initializeLayout(config.layoutType);
this.layout = this.config.customLayout ?? this.initializeLayout(this.config.layoutType);
}

private initializeLayout(layoutType?: TopologyLayoutType): TopologyLayout {
Expand Down Expand Up @@ -159,8 +165,15 @@
}

private updateLayout(): void {
this.layout.layout(this.topologyData, this.width, this.height);
this.updatePositions();
if (this.supportGroupNode) {
this.runAndDrainCallbacks(this.dataClearCallbacks);
this.collapseGroupNodesIfPresent();
this.layout.layout(this.topologyData, this.width, this.height);
this.drawData(this.topologyData, this.nodeRenderer, this.edgeRenderer);
} else {
this.layout.layout(this.topologyData, this.width, this.height);
this.updatePositions();

Check warning on line 175 in projects/observability/src/shared/components/topology/d3/d3-topology.ts

View check run for this annotation

Codecov / codecov/patch

projects/observability/src/shared/components/topology/d3/d3-topology.ts#L169-L175

Added lines #L169 - L175 were not covered by tests
}
}

private initializeContainer(): HTMLDivElement {
Expand Down Expand Up @@ -227,7 +240,9 @@
this.dataClearCallbacks.push(() => data.nodes.forEach(node => nodeRenderer.destroyNode(node)));

if (this.config.draggableNodes) {
const subscription = this.drag.addDragBehavior(data, nodeRenderer).subscribe(event => this.onNodeDrag(event));
const subscription = this.drag
.addDragBehavior(data, nodeRenderer, this.supportGroupNode)
.subscribe(event => this.onNodeDrag(event));
this.dataClearCallbacks.push(() => subscription.unsubscribe());
}
if (this.config.hoverableNodes) {
Expand Down Expand Up @@ -323,6 +338,7 @@
this.nodeRenderer,
this.domRenderer,
this.topologyData,
this.supportGroupNode,
);
this.layout.layout(this.topologyData, this.width, this.height);
this.drawData(this.topologyData, this.nodeRenderer, this.edgeRenderer);
Expand Down Expand Up @@ -410,6 +426,10 @@
this.neighborhoodFinder.singleNodeNeighborhood(node.userNode),
);

if (this.supportGroupNode) {
this.checkAndHandleGroupNodeClick(node);

Check warning on line 430 in projects/observability/src/shared/components/topology/d3/d3-topology.ts

View check run for this annotation

Codecov / codecov/patch

projects/observability/src/shared/components/topology/d3/d3-topology.ts#L430

Added line #L430 was not covered by tests
}

if (!isNil(this.config.nodeInteractionHandler?.click)) {
this.config.nodeInteractionHandler?.click(node.userNode).subscribe(() => this.resetVisibility());
} else if (this.tooltip) {
Expand All @@ -420,6 +440,35 @@
}
}

private checkAndHandleGroupNodeClick(node: RenderableTopologyNode): void {
const userNode = node.userNode;

Check warning on line 444 in projects/observability/src/shared/components/topology/d3/d3-topology.ts

View check run for this annotation

Codecov / codecov/patch

projects/observability/src/shared/components/topology/d3/d3-topology.ts#L443-L444

Added lines #L443 - L444 were not covered by tests
if (!TopologyGroupNodeUtil.isTopologyGroupNode(userNode)) {
return;

Check warning on line 446 in projects/observability/src/shared/components/topology/d3/d3-topology.ts

View check run for this annotation

Codecov / codecov/patch

projects/observability/src/shared/components/topology/d3/d3-topology.ts#L446

Added line #L446 was not covered by tests
}

this.userNodes = TopologyGroupNodeUtil.getUpdatedNodesOnGroupNodeClick(userNode, this.userNodes);
this.runAndDrainCallbacks(this.dataClearCallbacks);
itssharmasandeep marked this conversation as resolved.
Show resolved Hide resolved
this.convertTopology();
TopologyGroupNodeUtil.updateLayoutForGroupNode(this.topologyData, userNode);
this.drawData(this.topologyData, this.nodeRenderer, this.edgeRenderer);

Check warning on line 453 in projects/observability/src/shared/components/topology/d3/d3-topology.ts

View check run for this annotation

Codecov / codecov/patch

projects/observability/src/shared/components/topology/d3/d3-topology.ts#L449-L453

Added lines #L449 - L453 were not covered by tests
}

private collapseGroupNodesIfPresent(): void {
this.userNodes = TopologyGroupNodeUtil.collapseGroupNodes(this.userNodes);
this.convertTopology();

Check warning on line 458 in projects/observability/src/shared/components/topology/d3/d3-topology.ts

View check run for this annotation

Codecov / codecov/patch

projects/observability/src/shared/components/topology/d3/d3-topology.ts#L456-L458

Added lines #L456 - L458 were not covered by tests
}

private convertTopology(): void {
this.topologyData = this.topologyConverter.convertTopology(

Check warning on line 462 in projects/observability/src/shared/components/topology/d3/d3-topology.ts

View check run for this annotation

Codecov / codecov/patch

projects/observability/src/shared/components/topology/d3/d3-topology.ts#L461-L462

Added lines #L461 - L462 were not covered by tests
this.userNodes,
this.stateManager,
this.nodeRenderer,
this.domRenderer,
this.topologyData,
this.supportGroupNode,
);
}

private onEdgeClick(edge: RenderableTopologyEdge): void {
this.emphasizeTopologyNeighborhood(this.neighborhoodFinder.neighborhoodForEdge(edge.userEdge));

Expand All @@ -445,6 +494,9 @@
});
break;
case 'drag':
if (this.supportGroupNode) {
TopologyGroupNodeUtil.updateLayoutOnGroupNodeDrag(dragEvent, this.topologyData);

Check warning on line 498 in projects/observability/src/shared/components/topology/d3/d3-topology.ts

View check run for this annotation

Codecov / codecov/patch

projects/observability/src/shared/components/topology/d3/d3-topology.ts#L498

Added line #L498 was not covered by tests
}
this.updatePositions();
break;
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
RenderableTopology,
RenderableTopologyNode,
TopologyEdge,
TopologyGroupNode,
TopologyNode,
TopologyNodeRenderer,
} from '../../../topology';
import { TopologyEventBehavior } from '../topology-event-behavior';
import { TopologyGroupNodeUtil } from '../../../utils/topology-group-node.util';

export class TopologyNodeDrag extends TopologyEventBehavior {
/**
Expand All @@ -18,11 +20,26 @@
public addDragBehavior(
topologyData: RenderableTopology<TopologyNode, TopologyEdge>,
nodeRenderer: TopologyNodeRenderer,
supportGroupNode: boolean,
): Observable<TopologyDragEvent> {
if (topologyData.nodes.length === 0) {
return EMPTY;
}
const nodeLookup = this.buildLookupMap(topologyData.nodes, node => nodeRenderer.getElementForNode(node));

let nodeLookup = this.buildLookupMap(topologyData.nodes, node => nodeRenderer.getElementForNode(node));

if (supportGroupNode) {
const childUserNodesOfGroup = topologyData.nodes
.filter(n => TopologyGroupNodeUtil.isTopologyGroupNode(n.userNode))
.flatMap(n => (n.userNode as TopologyGroupNode).children);
const childRenderableNodesOfGroup = topologyData.nodes.filter(n => childUserNodesOfGroup.includes(n.userNode));

Check warning on line 35 in projects/observability/src/shared/components/topology/d3/interactions/drag/topology-node-drag.ts

View check run for this annotation

Codecov / codecov/patch

projects/observability/src/shared/components/topology/d3/interactions/drag/topology-node-drag.ts#L32-L35

Added lines #L32 - L35 were not covered by tests

nodeLookup = this.buildLookupMap(
topologyData.nodes.filter(n => !childRenderableNodesOfGroup.includes(n)),
node => nodeRenderer.getElementForNode(node),

Check warning on line 39 in projects/observability/src/shared/components/topology/d3/interactions/drag/topology-node-drag.ts

View check run for this annotation

Codecov / codecov/patch

projects/observability/src/shared/components/topology/d3/interactions/drag/topology-node-drag.ts#L37-L39

Added lines #L37 - L39 were not covered by tests
);
}

const dragSubect = new Subject<TopologyDragEvent>();

this.d3Utils.selectAll(Array.from(nodeLookup.keys()), this.domRenderer).call(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { RenderableTopology, RenderableTopologyNode, TopologyEdge, TopologyNode } from '../../topology';

export class GraphLayout {
private levelToNodesMap: Map<number, RenderableTopologyNode[]> = new Map<number, RenderableTopologyNode[]>([[0, []]]);
private readonly nodeToLevelMap: Map<RenderableTopologyNode, number> = new Map<RenderableTopologyNode, number>();
protected levelToNodesMap: Map<number, RenderableTopologyNode[]> = new Map<number, RenderableTopologyNode[]>([

Check warning on line 4 in projects/observability/src/shared/components/topology/d3/layouts/graph-layout.ts

View check run for this annotation

Codecov / codecov/patch

projects/observability/src/shared/components/topology/d3/layouts/graph-layout.ts#L4

Added line #L4 was not covered by tests
[0, []],
]);
protected readonly nodeToLevelMap: Map<RenderableTopologyNode, number> = new Map<RenderableTopologyNode, number>();
private readonly layoutConfig: TopologyGraphLayoutConfig = this.getLayoutConfig();

Check warning on line 8 in projects/observability/src/shared/components/topology/d3/layouts/graph-layout.ts

View check run for this annotation

Codecov / codecov/patch

projects/observability/src/shared/components/topology/d3/layouts/graph-layout.ts#L7-L8

Added lines #L7 - L8 were not covered by tests

public layout(topology: RenderableTopology<TopologyNode, TopologyEdge>): void {
this.initialize();
Expand All @@ -12,12 +15,12 @@
this.verticallyCenterAlignNodes();
}

private initialize(): void {
protected initialize(): void {

Check warning on line 18 in projects/observability/src/shared/components/topology/d3/layouts/graph-layout.ts

View check run for this annotation

Codecov / codecov/patch

projects/observability/src/shared/components/topology/d3/layouts/graph-layout.ts#L18

Added line #L18 was not covered by tests
this.levelToNodesMap = new Map([[0, []]]);
this.nodeToLevelMap.clear();
}

private findRootNodes(topology: RenderableTopology<TopologyNode, TopologyEdge>): void {
protected findRootNodes(topology: RenderableTopology<TopologyNode, TopologyEdge>): void {

Check warning on line 23 in projects/observability/src/shared/components/topology/d3/layouts/graph-layout.ts

View check run for this annotation

Codecov / codecov/patch

projects/observability/src/shared/components/topology/d3/layouts/graph-layout.ts#L23

Added line #L23 was not covered by tests
topology.nodes.forEach(node => {
if (node.incoming.length === 0) {
this.nodeToLevelMap.set(node, 0);
Expand All @@ -26,14 +29,14 @@
});
}

private fillNodeAndLevelMaps(): void {
protected fillNodeAndLevelMaps(): void {

Check warning on line 32 in projects/observability/src/shared/components/topology/d3/layouts/graph-layout.ts

View check run for this annotation

Codecov / codecov/patch

projects/observability/src/shared/components/topology/d3/layouts/graph-layout.ts#L32

Added line #L32 was not covered by tests
const goingToBeExploredNodes = [...(this.levelToNodesMap.get(0) ?? [])];
const goingToBeOrAlreadyExploredNodes = new Set<RenderableTopologyNode>([...goingToBeExploredNodes]);

this.levelOrderTraversal(goingToBeExploredNodes, goingToBeOrAlreadyExploredNodes);
}

private levelOrderTraversal(
protected levelOrderTraversal(
goingToBeExploredNodes: RenderableTopologyNode[],
goingToBeOrAlreadyExploredNodes: Set<RenderableTopologyNode>,
): void {
Expand All @@ -60,31 +63,59 @@
this.levelOrderTraversal(goingToBeExploredNodes, goingToBeOrAlreadyExploredNodes);
}

private assignCoordinatesToNodes(): void {
let curX = 1;
protected assignCoordinatesToNodes(): void {
let curX = this.layoutConfig.startX;

Check warning on line 67 in projects/observability/src/shared/components/topology/d3/layouts/graph-layout.ts

View check run for this annotation

Codecov / codecov/patch

projects/observability/src/shared/components/topology/d3/layouts/graph-layout.ts#L66-L67

Added lines #L66 - L67 were not covered by tests

Array.from(this.levelToNodesMap.values()).forEach(nodes => {
let curY = 1;
let curY = this.layoutConfig.startY;

Check warning on line 70 in projects/observability/src/shared/components/topology/d3/layouts/graph-layout.ts

View check run for this annotation

Codecov / codecov/patch

projects/observability/src/shared/components/topology/d3/layouts/graph-layout.ts#L70

Added line #L70 was not covered by tests
let maxWidth = 0;
nodes.forEach(node => {
node.x = curX;
node.y = curY;
curY += (node.renderedData()?.getBoudingBox()?.height ?? 36) + 20;
maxWidth = Math.max(maxWidth, node.renderedData()?.getBoudingBox()?.width ?? 400);
curY +=

Check warning on line 75 in projects/observability/src/shared/components/topology/d3/layouts/graph-layout.ts

View check run for this annotation

Codecov / codecov/patch

projects/observability/src/shared/components/topology/d3/layouts/graph-layout.ts#L75

Added line #L75 was not covered by tests
(node.renderedData()?.getBoudingBox()?.height ?? this.layoutConfig.defaultNodeHeight) +
this.layoutConfig.verticalNodeGap;
maxWidth = Math.max(

Check warning on line 78 in projects/observability/src/shared/components/topology/d3/layouts/graph-layout.ts

View check run for this annotation

Codecov / codecov/patch

projects/observability/src/shared/components/topology/d3/layouts/graph-layout.ts#L78

Added line #L78 was not covered by tests
maxWidth,
node.renderedData()?.getBoudingBox()?.width ?? this.layoutConfig.defaultNodeWidth,
);
});

curX += maxWidth + 240;
curX += maxWidth + this.layoutConfig.horizontalNodeGap;

Check warning on line 84 in projects/observability/src/shared/components/topology/d3/layouts/graph-layout.ts

View check run for this annotation

Codecov / codecov/patch

projects/observability/src/shared/components/topology/d3/layouts/graph-layout.ts#L84

Added line #L84 was not covered by tests
});
}

private verticallyCenterAlignNodes(): void {
protected verticallyCenterAlignNodes(): void {

Check warning on line 88 in projects/observability/src/shared/components/topology/d3/layouts/graph-layout.ts

View check run for this annotation

Codecov / codecov/patch

projects/observability/src/shared/components/topology/d3/layouts/graph-layout.ts#L88

Added line #L88 was not covered by tests
const longestLevelLength = Math.max(...Array.from(this.levelToNodesMap.values()).map(nodes => nodes.length));

Array.from(this.levelToNodesMap.values()).forEach(nodes => {
nodes.forEach(node => {
node.y +=
((longestLevelLength - nodes.length) * ((node.renderedData()?.getBoudingBox()?.height ?? 36) + 20)) / 2;
((longestLevelLength - nodes.length) *
((node.renderedData()?.getBoudingBox()?.height ?? this.layoutConfig.defaultNodeHeight) +
this.layoutConfig.verticalNodeGap)) /
2;
});
});
}

protected getLayoutConfig(): TopologyGraphLayoutConfig {
return {

Check warning on line 103 in projects/observability/src/shared/components/topology/d3/layouts/graph-layout.ts

View check run for this annotation

Codecov / codecov/patch

projects/observability/src/shared/components/topology/d3/layouts/graph-layout.ts#L102-L103

Added lines #L102 - L103 were not covered by tests
horizontalNodeGap: 240,
verticalNodeGap: 20,
startX: 1,
startY: 1,
defaultNodeWidth: 240,
defaultNodeHeight: 36,
};
}
}

export interface TopologyGraphLayoutConfig {
horizontalNodeGap: number;
verticalNodeGap: number;
startX: number;
startY: number;
defaultNodeWidth: number;
defaultNodeHeight: number;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
TopologyDataSpecifier,
TopologyEdgeInteractionHandler,
TopologyEdgeRenderer,
TopologyLayout,
TopologyLayoutType,
TopologyNode,
TopologyNodeInteractionHandler,
Expand Down Expand Up @@ -58,9 +59,18 @@ export class TopologyComponent implements OnChanges, OnDestroy {
@Input()
public shouldAutoZoomToFit?: boolean = false;

@Input()
public draggableNodes?: boolean = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this sounds like it is storing the nodes which are draggable. Could we rename it to allowNodeDrag?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, this was the property already present in the topology config, so I used the same name. do we change at both places or just input?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is fine if it is an existing term


@Input()
public layoutType?: TopologyLayoutType;

@Input()
public customLayout?: TopologyLayout; // This will override `layoutType` property

@Input()
public supportGroupNode?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this as an input or can we extract this info from our custom group node layout property?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The layout and group nodes are different IMO. The current flow looks like this.

  1. We create group nodes like we create entity nodes using the data that we get from the backend.
  2. We pass this data to the topology.
  3. Now topology handles group nodes based on the property.
  4. custom layout (with some business logic) is passed from the parent component and used to position the nodes on some layout logic. The layout may or may not care about the group nodes.

The one thing that we can do here is we can extract this property by checking if the nodes contain a group node or not. If this works I can make the changes


@ViewChild('topologyContainer', { static: true })
private readonly container!: ElementRef;

Expand All @@ -86,9 +96,12 @@ export class TopologyComponent implements OnChanges, OnDestroy {
tooltipRenderer: this.tooltipRenderer,
showBrush: this.showBrush,
shouldAutoZoomToFit: this.shouldAutoZoomToFit,
draggableNodes: this.draggableNodes,
nodeInteractionHandler: this.nodeInteractionHandler,
edgeInteractionHandler: this.edgeInteractionHandler,
layoutType: this.layoutType,
customLayout: this.customLayout,
supportGroupNode: this.supportGroupNode,
});

// Angular doesn't like introducing new child views mid-change detection
Expand Down
Loading