|
|
@@ -1,7 +1,7 @@
|
|
|
import React, { Component } from 'react';
|
|
|
import styled from 'styled-components';
|
|
|
|
|
|
-import { ResourceType, NodeType, EdgeType } from '../../../../../shared/types';
|
|
|
+import { ResourceType, NodeType, EdgeType, ChartType } from '../../../../../shared/types';
|
|
|
|
|
|
import Node from './Node';
|
|
|
import Edge from './Edge';
|
|
|
@@ -15,14 +15,17 @@ type PropsType = {
|
|
|
components: ResourceType[],
|
|
|
isExpanded: boolean,
|
|
|
setSidebar: (x: boolean) => void,
|
|
|
- currentChartName: string
|
|
|
+ currentChart: ChartType,
|
|
|
+
|
|
|
+ // Handle revisions expansion for YAML wrapper
|
|
|
+ showRevisions: boolean
|
|
|
};
|
|
|
|
|
|
type StateType = {
|
|
|
nodes: NodeType[],
|
|
|
edges: EdgeType[],
|
|
|
activeIds: number[], // IDs of all currently selected nodes
|
|
|
- originX: number | null,
|
|
|
+ originX: number | null,
|
|
|
originY: number | null,
|
|
|
cursorX: number | null,
|
|
|
cursorY: number | null,
|
|
|
@@ -32,14 +35,20 @@ type StateType = {
|
|
|
panY: number | null, // Two-finger pan y-displacement
|
|
|
anchorX: number | null, // Initial cursorX during region select
|
|
|
anchorY: number | null, // Initial cursorY during region select
|
|
|
+ nodeClickX: number | null, // Initial cursorX during node click (drag vs click)
|
|
|
+ nodeClickY: number | null, // Initial cursorY during node click (drag vs click)
|
|
|
dragBg: boolean, // Boolean to track if all nodes should move with mouse (bg drag)
|
|
|
- preventBgDrag: boolean, // Prevents bg drag when moving selected with mouse down
|
|
|
- relocateAllowed: boolean, // Suppresses movement of selected when drawing select region
|
|
|
+ preventBgDrag: boolean, // Prevent bg drag when moving selected with mouse down
|
|
|
+ relocateAllowed: boolean, // Suppress movement of selected when drawing select region
|
|
|
scale: number,
|
|
|
showKindLabels: boolean,
|
|
|
+ isExpanded: boolean,
|
|
|
currentNode: NodeType | null,
|
|
|
currentEdge: EdgeType | null,
|
|
|
- isExpanded: boolean
|
|
|
+ openedNode: NodeType | null,
|
|
|
+ suppressCloseNode: boolean, // Still click should close opened unless on a node
|
|
|
+ suppressDisplay: boolean, // Ignore clicks + pan/zoom on InfoPanel or ButtonSection
|
|
|
+ version?: number // Track in localstorage for handling updates when unmounted
|
|
|
};
|
|
|
|
|
|
// TODO: region-based unselect, shift-click, multi-region
|
|
|
@@ -58,14 +67,19 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
|
|
|
panY: null as (number | null),
|
|
|
anchorX: null as (number | null),
|
|
|
anchorY: null as (number | null),
|
|
|
+ nodeClickX: null as (number | null),
|
|
|
+ nodeClickY: null as (number | null),
|
|
|
dragBg: false,
|
|
|
preventBgDrag: false,
|
|
|
+ relocateAllowed: false,
|
|
|
scale: 0.5,
|
|
|
showKindLabels: true,
|
|
|
+ isExpanded: false,
|
|
|
currentNode: null as (NodeType | null),
|
|
|
currentEdge: null as (EdgeType | null),
|
|
|
- relocateAllowed: false,
|
|
|
- isExpanded: false
|
|
|
+ openedNode: null as (NodeType | null),
|
|
|
+ suppressCloseNode: false,
|
|
|
+ suppressDisplay: false
|
|
|
}
|
|
|
|
|
|
spaceRef: any = React.createRef();
|
|
|
@@ -77,7 +91,7 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
|
|
|
}
|
|
|
|
|
|
componentDidMount() {
|
|
|
- let { components } = this.props;
|
|
|
+ let { components, currentChart } = this.props;
|
|
|
|
|
|
// Initialize origin
|
|
|
let height = this.spaceRef.offsetHeight;
|
|
|
@@ -91,21 +105,39 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
|
|
|
this.spaceRef.addEventListener("touchmove", (e: any) => e.preventDefault());
|
|
|
this.spaceRef.addEventListener("mousewheel", (e: any) => e.preventDefault());
|
|
|
|
|
|
- let graph = localStorage.getItem(`charts.${this.props.currentChartName}`)
|
|
|
- let nodes = [] as NodeType[]
|
|
|
- let edges = [] as EdgeType[]
|
|
|
+ document.addEventListener("keydown", this.handleKeyDown);
|
|
|
+ document.addEventListener("keyup", this.handleKeyUp);
|
|
|
|
|
|
+ // Handle graph from localstorage
|
|
|
+ let graph = localStorage.getItem(`charts.${currentChart.name}-${currentChart.version}`);
|
|
|
+ let nodes = [] as NodeType[];
|
|
|
+ let edges = [] as EdgeType[];
|
|
|
if (!graph) {
|
|
|
- nodes = this.createNodes(components)
|
|
|
- edges = this.createEdges(components)
|
|
|
+ nodes = this.createNodes(components);
|
|
|
+ edges = this.createEdges(components);
|
|
|
this.setState({ nodes, edges });
|
|
|
} else {
|
|
|
- let storedState = JSON.parse(localStorage.getItem(`charts.${this.props.currentChartName}`))
|
|
|
- this.setState(storedState)
|
|
|
+ let storedState = JSON.parse(localStorage.getItem(
|
|
|
+ `charts.${currentChart.name}-${currentChart.version}`
|
|
|
+ ));
|
|
|
+ this.setState(storedState);
|
|
|
}
|
|
|
|
|
|
- document.addEventListener("keydown", this.handleKeyDown);
|
|
|
- document.addEventListener("keyup", this.handleKeyUp);
|
|
|
+ window.onbeforeunload = () => {
|
|
|
+ this.storeChart();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Live update on rollback/upgrade
|
|
|
+ componentDidUpdate(prevProps: PropsType) {
|
|
|
+ if (prevProps.components !== this.props.components) {
|
|
|
+ let nodes = [] as NodeType[];
|
|
|
+ let edges = [] as EdgeType[];
|
|
|
+
|
|
|
+ nodes = this.createNodes(this.props.components);
|
|
|
+ edges = this.createEdges(this.props.components);
|
|
|
+ this.setState({ nodes, edges, openedNode: null });
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
createNodes = (components: ResourceType[]) => {
|
|
|
@@ -115,24 +147,24 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
|
|
|
case "ClusterRole":
|
|
|
case "RoleBinding":
|
|
|
case "Role":
|
|
|
- return { id: c.ID, name: c.Name, kind: c.Kind, x: this.getRandomIntBetweenRange(-1000, 0), y: this.getRandomIntBetweenRange(0, 500), w: 40, h: 40 };
|
|
|
+ return { id: c.ID, RawYAML: c.RawYAML, name: c.Name, kind: c.Kind, x: this.getRandomIntBetweenRange(-500, 0), y: this.getRandomIntBetweenRange(0, 250), w: 40, h: 40 };
|
|
|
case "Deployment":
|
|
|
case "StatefulSet":
|
|
|
case "Pod":
|
|
|
case "ServiceAccount":
|
|
|
- return { id: c.ID, name: c.Name, kind: c.Kind, x: this.getRandomIntBetweenRange(0, 1000), y: this.getRandomIntBetweenRange(0, 500), w: 40, h: 40 };
|
|
|
+ return { id: c.ID, RawYAML: c.RawYAML, name: c.Name, kind: c.Kind, x: this.getRandomIntBetweenRange(0, 500), y: this.getRandomIntBetweenRange(0, 250), w: 40, h: 40 };
|
|
|
case "Service":
|
|
|
case "Ingress":
|
|
|
case "ServiceAccount":
|
|
|
- return { id: c.ID, name: c.Name, kind: c.Kind, x: this.getRandomIntBetweenRange(0, 1000), y: this.getRandomIntBetweenRange(-500, 0), w: 40, h: 40 };
|
|
|
+ return { id: c.ID, RawYAML: c.RawYAML, name: c.Name, kind: c.Kind, x: this.getRandomIntBetweenRange(0, 500), y: this.getRandomIntBetweenRange(-250, 0), w: 40, h: 40 };
|
|
|
default:
|
|
|
- return { id: c.ID, name: c.Name, kind: c.Kind, x: this.getRandomIntBetweenRange(-700, 0), y: this.getRandomIntBetweenRange(-500, 0), w: 40, h: 40 };
|
|
|
+ return { id: c.ID, RawYAML: c.RawYAML, name: c.Name, kind: c.Kind, x: this.getRandomIntBetweenRange(-400, 0), y: this.getRandomIntBetweenRange(-250, 0), w: 40, h: 40 };
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
createEdges = (components: ResourceType[]) => {
|
|
|
- let edges = [] as EdgeType[]
|
|
|
+ let edges = [] as EdgeType[];
|
|
|
components.map((c: ResourceType) => {
|
|
|
c.Relations?.ControlRels?.map((rel: any) => {
|
|
|
if (rel.Source == c.ID) {
|
|
|
@@ -150,19 +182,31 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
|
|
|
}
|
|
|
})
|
|
|
});
|
|
|
- return edges
|
|
|
+ return edges;
|
|
|
}
|
|
|
|
|
|
- componentWillUnmount() {
|
|
|
- let graph = this.state;
|
|
|
- console.log("unmounting...", graph)
|
|
|
- // flush non-persistent data
|
|
|
+ storeChart = () => {
|
|
|
+ let { currentChart } = this.props;
|
|
|
+ let graph = JSON.parse(JSON.stringify(this.state));
|
|
|
+
|
|
|
+ // Flush non-persistent data
|
|
|
graph.activeIds = [];
|
|
|
graph.currentNode = null;
|
|
|
graph.currentEdge = null;
|
|
|
graph.isExpanded = false;
|
|
|
+ graph.openedNode = null;
|
|
|
+ graph.suppressDisplay = false;
|
|
|
+ graph.suppressCloseNode = false;
|
|
|
|
|
|
- localStorage.setItem(`charts.${this.props.currentChartName}`, JSON.stringify(graph))
|
|
|
+ localStorage.setItem(
|
|
|
+ `charts.${currentChart.name}-${currentChart.version}`,
|
|
|
+ JSON.stringify(graph)
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ componentWillUnmount() {
|
|
|
+ this.storeChart();
|
|
|
+
|
|
|
this.spaceRef.removeEventListener("touchmove", (e: any) => e.preventDefault());
|
|
|
this.spaceRef.removeEventListener("mousewheel", (e: any) => e.preventDefault());
|
|
|
document.removeEventListener("keydown", this.handleKeyDown);
|
|
|
@@ -201,8 +245,13 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // Push to activeIds if not already present
|
|
|
handleClickNode = (clickedId: number) => {
|
|
|
+ let { cursorX, cursorY } = this.state;
|
|
|
+
|
|
|
+ // Store position for distinguishing click vs drag on release
|
|
|
+ this.setState({ nodeClickX: cursorX, nodeClickY: cursorY, suppressCloseNode: true });
|
|
|
+
|
|
|
+ // Push to activeIds if not already present
|
|
|
let holding = this.state.activeIds;
|
|
|
if (!holding.includes(clickedId)) {
|
|
|
holding.push(clickedId);
|
|
|
@@ -211,28 +260,51 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
|
|
|
// Track and store offset to grab node from anywhere (must store)
|
|
|
this.state.nodes.forEach((node: NodeType) => {
|
|
|
if (this.state.activeIds.includes(node.id)) {
|
|
|
- if (!node.toCursorX && !node.toCursorY) {
|
|
|
- node.toCursorX = node.x - this.state.cursorX;
|
|
|
- node.toCursorY = node.y - this.state.cursorY;
|
|
|
- } else {
|
|
|
- node.toCursorX = 0;
|
|
|
- node.toCursorY = 0;
|
|
|
- }
|
|
|
+ node.toCursorX = node.x - cursorX;
|
|
|
+ node.toCursorY = node.y - cursorY;
|
|
|
}
|
|
|
});
|
|
|
|
|
|
this.setState({ activeIds: holding, preventBgDrag: true, relocateAllowed: true });
|
|
|
}
|
|
|
|
|
|
- handleReleaseNode = () => {
|
|
|
+ handleReleaseNode = (node: NodeType) => {
|
|
|
+ let { cursorX, cursorY, nodeClickX, nodeClickY } = this.state;
|
|
|
this.setState({ activeIds: [], preventBgDrag: false });
|
|
|
|
|
|
- // Only update dot position state on release for all active
|
|
|
- let { activeIds, nodes} = this.state;
|
|
|
- for (var i=0; i < activeIds.length; i++) {
|
|
|
- var a = activeIds[i];
|
|
|
- nodes[a].toCursorX = 0;
|
|
|
- nodes[a].toCursorY = 0;
|
|
|
+ // Distinguish node click vs drag (can't use onClick since drag counts)
|
|
|
+ if (cursorX === nodeClickX && cursorY === nodeClickY) {
|
|
|
+ this.setState({ openedNode: node });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ handleMouseDown = () => {
|
|
|
+ let { cursorX, cursorY } = this.state;
|
|
|
+
|
|
|
+ // Store position for distinguishing click vs drag on release
|
|
|
+ this.setState({ nodeClickX: cursorX, nodeClickY: cursorY });
|
|
|
+
|
|
|
+ this.setState({
|
|
|
+ dragBg: true,
|
|
|
+
|
|
|
+ // Suppress drifting on repeated click
|
|
|
+ deltaX: null,
|
|
|
+ deltaY: null,
|
|
|
+ panX: null,
|
|
|
+ panY: null,
|
|
|
+ scale: 1
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ handleMouseUp = () => {
|
|
|
+ let { cursorX, nodeClickX, cursorY, nodeClickY, suppressCloseNode } = this.state;
|
|
|
+ this.setState({ dragBg: false, activeIds: [] });
|
|
|
+
|
|
|
+ // Distinguish bg click vs drag for setting closing opened node
|
|
|
+ if (!suppressCloseNode && cursorX === nodeClickX && cursorY === nodeClickY) {
|
|
|
+ this.setState({ openedNode: null });
|
|
|
+ } else if (this.state.suppressCloseNode) {
|
|
|
+ this.setState({ suppressCloseNode: false });
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -271,17 +343,21 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
|
|
|
// Handle pan XOR zoom (two-finger gestures count as onWheel)
|
|
|
handleWheel = (e: any) => {
|
|
|
|
|
|
- // Pinch/zoom sets e.ctrlKey to true
|
|
|
- if (e.ctrlKey) {
|
|
|
-
|
|
|
- // Clip deltaY for extreme mousewheel values
|
|
|
- let deltaY = e.deltaY >= 0 ? Math.min(40, e.deltaY) : Math.max(-40, e.deltaY);
|
|
|
+ // Prevent nav gestures if mouse is over InfoPanel or ButtonSection
|
|
|
+ if (!this.state.suppressDisplay) {
|
|
|
|
|
|
- let scale = 1;
|
|
|
- scale -= deltaY * zoomConstant;
|
|
|
- this.setState({ scale, panX: 0, panY: 0 });
|
|
|
- } else {
|
|
|
- this.setState({ panX: e.deltaX, panY: e.deltaY, scale: 1 });
|
|
|
+ // Pinch/zoom sets e.ctrlKey to true
|
|
|
+ if (e.ctrlKey) {
|
|
|
+
|
|
|
+ // Clip deltaY for extreme mousewheel values
|
|
|
+ let deltaY = e.deltaY >= 0 ? Math.min(40, e.deltaY) : Math.max(-40, e.deltaY);
|
|
|
+
|
|
|
+ let scale = 1;
|
|
|
+ scale -= deltaY * zoomConstant;
|
|
|
+ this.setState({ scale, panX: 0, panY: 0 });
|
|
|
+ } else {
|
|
|
+ this.setState({ panX: e.deltaX, panY: e.deltaY, scale: 1 });
|
|
|
+ }
|
|
|
}
|
|
|
};
|
|
|
|
|
|
@@ -340,9 +416,10 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
|
|
|
originX={originX}
|
|
|
originY={originY}
|
|
|
nodeMouseDown={() => this.handleClickNode(node.id)}
|
|
|
- nodeMouseUp={this.handleReleaseNode}
|
|
|
+ nodeMouseUp={() => this.handleReleaseNode(node)}
|
|
|
isActive={activeIds.includes(node.id)}
|
|
|
showKindLabels={this.state.showKindLabels}
|
|
|
+ isOpen={node === this.state.openedNode}
|
|
|
|
|
|
// Parameterized to allow setting to null
|
|
|
setCurrentNode={(node: NodeType) => this.setState({ currentNode: node })}
|
|
|
@@ -390,24 +467,18 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
|
|
|
isExpanded={this.state.isExpanded}
|
|
|
ref={element => this.spaceRef = element}
|
|
|
onMouseMove={this.handleMouseMove}
|
|
|
- onMouseDown={() => this.setState({
|
|
|
- dragBg: true,
|
|
|
-
|
|
|
- // Suppress drifting on repeated click
|
|
|
- deltaX: null,
|
|
|
- deltaY: null,
|
|
|
- panX: null,
|
|
|
- panY: null,
|
|
|
- scale: 1
|
|
|
- })}
|
|
|
- onMouseUp={() => this.setState({ dragBg: false, activeIds: [] })}
|
|
|
+ onMouseDown={this.state.suppressDisplay ? null : this.handleMouseDown}
|
|
|
+ onMouseUp={this.state.suppressDisplay ? null : this.handleMouseUp}
|
|
|
onWheel={this.handleWheel}
|
|
|
>
|
|
|
{this.renderNodes()}
|
|
|
{this.renderEdges()}
|
|
|
{this.renderSelectRegion()}
|
|
|
|
|
|
- <ButtonSection>
|
|
|
+ <ButtonSection
|
|
|
+ onMouseEnter={() => this.setState({ suppressDisplay: true })}
|
|
|
+ onMouseLeave={() => this.setState({ suppressDisplay: false })}
|
|
|
+ >
|
|
|
<ToggleLabel
|
|
|
onClick={() => this.setState({ showKindLabels: !this.state.showKindLabels })}
|
|
|
>
|
|
|
@@ -425,8 +496,17 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
|
|
|
</ExpandButton>
|
|
|
</ButtonSection>
|
|
|
<InfoPanel
|
|
|
+ setSuppressDisplay={(x: boolean) => this.setState({ suppressDisplay: x })}
|
|
|
currentNode={this.state.currentNode}
|
|
|
currentEdge={this.state.currentEdge}
|
|
|
+ openedNode={this.state.openedNode}
|
|
|
+
|
|
|
+ // InfoPanel won't trigger onMouseLeave for unsuppressing if close is clicked
|
|
|
+ closeNode={() => this.setState({ openedNode: null, suppressDisplay: false })}
|
|
|
+
|
|
|
+ // For YAML wrapper to trigger resize
|
|
|
+ isExpanded={this.state.isExpanded}
|
|
|
+ showRevisions={this.props.showRevisions}
|
|
|
/>
|
|
|
</StyledGraphDisplay>
|
|
|
);
|
|
|
@@ -475,10 +555,12 @@ const ToggleLabel = styled.div`
|
|
|
|
|
|
const ButtonSection = styled.div`
|
|
|
position: absolute;
|
|
|
- top: 17px;
|
|
|
+ top: 15px;
|
|
|
right: 15px;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
+ z-index: 999;
|
|
|
+ cursor: pointer;
|
|
|
`;
|
|
|
|
|
|
const ExpandButton = styled.div`
|