Просмотр исходного кода

graph ux sprint (fullscreen, pop up listview, colored edges, info hover for node + edge, etc.)

jusrhee 5 лет назад
Родитель
Сommit
481dd4450e

+ 3 - 1
dashboard/src/components/Loading.tsx

@@ -3,6 +3,7 @@ import styled from 'styled-components';
 import loading from '../assets/loading.gif';
 
 type PropsType = {
+  offset?: string
 };
 
 type StateType = {
@@ -14,7 +15,7 @@ export default class Loading extends Component<PropsType, StateType> {
 
   render() {
     return (
-      <StyledLoading>
+      <StyledLoading offset={this.props.offset}>
         <Spinner src={loading} />
       </StyledLoading>
     );
@@ -31,4 +32,5 @@ const StyledLoading = styled.div`
   display: flex;
   align-items: center;
   justify-content: center;
+  margin-top: ${(props: { offset?: string }) => props.offset};
 `;

+ 2 - 6
dashboard/src/components/TabSelector.tsx

@@ -7,28 +7,24 @@ export interface selectOption {
 }
 
 type PropsType = {
+  currentTab: string,
   options: selectOption[],
   setCurrentTab: (value: string) => void,
   tabWidth?: string  
 };
 
 type StateType = {
-  currentTab: string
 };
 
 export default class TabSelector extends Component<PropsType, StateType> {
-  state = {
-    currentTab: 'overview', 
-  }
 
   renderLine = (tab: string): JSX.Element | undefined => {
-    if (this.state.currentTab === tab) {
+    if (this.props.currentTab === tab) {
       return <Highlight />
     }
   };
 
   handleTabClick = (value: string) => {
-    this.setState({ currentTab: value });
     this.props.setCurrentTab(value);
   }
 

+ 80 - 82
dashboard/src/main/home/dashboard/expanded-chart/ExpandedChart.tsx

@@ -2,13 +2,15 @@ import React, { Component } from 'react';
 import styled from 'styled-components';
 import close from '../../../../assets/close.png';
 
-import { ChartType } from '../../../../shared/types';
+import { ResourceType, ChartType, StorageType } from '../../../../shared/types';
 import { Context } from '../../../../shared/Context';
+import api from '../../../../shared/api';
 
 import TabSelector from '../../../../components/TabSelector';
 import RevisionSection from './RevisionSection';
 import ValuesYaml from './ValuesYaml';
-import OverviewSection from './OverviewSection';
+import GraphSection from './GraphSection';
+import ListSection from './ListSection';
 
 type PropsType = {
   currentChart: ChartType,
@@ -19,19 +21,36 @@ type PropsType = {
 type StateType = {
   showRevisions: boolean,
   currentTab: string,
-  isExpanded: boolean
+  components: ResourceType[]
 };
 
 const tabOptions = [
-  { label: 'Chart Overview', value: 'overview' },
+  { label: 'Chart Overview', value: 'graph' },
+  { label: 'Search Chart', value: 'list' },
   { label: 'Values Editor', value: 'values' }
 ]
 
 export default class ExpandedChart extends Component<PropsType, StateType> {
   state = {
     showRevisions: false,
-    currentTab: 'overview',
-    isExpanded: false,
+    currentTab: 'graph',
+    components: [] as ResourceType[]
+  }
+
+  componentDidMount() {
+    let { currentCluster, setCurrentError } = this.context;
+    let { currentChart } = this.props;
+    api.getChartComponents('<token>', {
+      namespace: currentChart.namespace,
+      context: currentCluster,
+      storage: StorageType.Secret
+    }, { name: currentChart.name, revision: 0 }, (err: any, res: any) => {
+      if (err) {
+        console.log(err)
+      } else {
+        this.setState({ components: res.data });
+      }
+    });
   }
 
   renderIcon = () => {
@@ -54,12 +73,18 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
   renderTabContents = () => {
     let { currentChart, refreshChart } = this.props;
 
-    if (this.state.currentTab === 'overview') {
+    if (this.state.currentTab === 'graph') {
       return (
-        <OverviewSection
-          toggleExpanded={() => this.setState({ isExpanded: !this.state.isExpanded })}
-          isExpanded={this.state.isExpanded}
+        <GraphSection
           currentChart={currentChart}
+          components={this.state.components}
+        />
+      );
+    } else if (this.state.currentTab === 'list') {
+      return (
+        <ListSection
+          currentChart={currentChart}
+          components={this.state.components}
         />
       );
     }
@@ -72,77 +97,6 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
     );
   }
 
-  renderInfo = () => {
-    let { currentChart, setCurrentChart, refreshChart } = this.props;
-    let chart = currentChart;
-
-    if (!this.state.isExpanded) {
-      return (
-        <HeaderWrapper>
-          <TitleSection>
-            <Title>
-              <IconWrapper>
-                {this.renderIcon()}
-              </IconWrapper>
-              {chart.name}
-            </Title>
-            <InfoWrapper>
-              <StatusIndicator>
-                <StatusColor status={chart.info.status} />
-                {chart.info.status}
-              </StatusIndicator>
-
-              <LastDeployed>
-                <Dot>•</Dot>Last deployed {this.readableDate(chart.info.last_deployed)}
-              </LastDeployed>
-            </InfoWrapper>
-
-            <TagWrapper>
-              Namespace
-            <NamespaceTag>
-                {chart.namespace}
-              </NamespaceTag>
-            </TagWrapper>
-          </TitleSection>
-
-          <CloseButton onClick={() => setCurrentChart(null)}>
-            <CloseButtonImg src={close} />
-          </CloseButton>
-
-          <RevisionSection
-            showRevisions={this.state.showRevisions}
-            toggleShowRevisions={() => this.setState({ showRevisions: !this.state.showRevisions })}
-            chart={chart}
-            refreshChart={refreshChart}
-          />
-
-          <TabSelector
-            options={tabOptions}
-            setCurrentTab={(value: string) => this.setState({ currentTab: value })}
-            tabWidth='120px'
-          />
-        </HeaderWrapper>
-      );
-    }
-
-    return (
-      <HeaderWrapper>
-        <TitleSection>
-          <Title>
-            <IconWrapper>
-              {this.renderIcon()}
-            </IconWrapper>
-            {chart.name}
-          </Title>
-        </TitleSection>
-
-        <CloseButton onClick={() => setCurrentChart(null)}>
-          <CloseButtonImg src={close} />
-        </CloseButton>
-      </HeaderWrapper>
-    );
-  }
-
   render() {
     let { currentChart, setCurrentChart, refreshChart } = this.props;
     let chart = currentChart;
@@ -151,7 +105,51 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
       <div>
         <CloseOverlay onClick={() => setCurrentChart(null)}/>
         <StyledExpandedChart>
-          {this.renderInfo()}
+          <HeaderWrapper>
+            <TitleSection>
+              <Title>
+                <IconWrapper>
+                  {this.renderIcon()}
+                </IconWrapper>
+                {chart.name}
+              </Title>
+              <InfoWrapper>
+                <StatusIndicator>
+                  <StatusColor status={chart.info.status} />
+                  {chart.info.status}
+                </StatusIndicator>
+
+                <LastDeployed>
+                  <Dot>•</Dot>Last deployed {this.readableDate(chart.info.last_deployed)}
+                </LastDeployed>
+              </InfoWrapper>
+
+              <TagWrapper>
+                Namespace
+              <NamespaceTag>
+                  {chart.namespace}
+                </NamespaceTag>
+              </TagWrapper>
+            </TitleSection>
+
+            <CloseButton onClick={() => setCurrentChart(null)}>
+              <CloseButtonImg src={close} />
+            </CloseButton>
+
+            <RevisionSection
+              showRevisions={this.state.showRevisions}
+              toggleShowRevisions={() => this.setState({ showRevisions: !this.state.showRevisions })}
+              chart={chart}
+              refreshChart={refreshChart}
+            />
+
+            <TabSelector
+              options={tabOptions}
+              currentTab={this.state.currentTab}
+              setCurrentTab={(value: string) => this.setState({ currentTab: value })}
+              tabWidth='120px'
+            />
+          </HeaderWrapper>
           <ContentSection>
             {this.renderTabContents()}
           </ContentSection>

+ 93 - 0
dashboard/src/main/home/dashboard/expanded-chart/GraphSection.tsx

@@ -0,0 +1,93 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+import api from '../../../../shared/api';
+
+import { Context } from '../../../../shared/Context';
+import { ResourceType, StorageType, ChartType } from '../../../../shared/types';
+
+import GraphDisplay from './graph/GraphDisplay';
+import Loading from '../../../../components/Loading';
+
+type PropsType = {
+  currentChart: ChartType,
+  components: ResourceType[]
+};
+
+type StateType = {
+  isExpanded: boolean
+};
+
+export default class GraphSection extends Component<PropsType, StateType> {
+  state = {
+    isExpanded: false
+  }
+
+  renderContents = () => {
+    if (this.props.components && this.props.components.length > 0) {
+      return (
+        <GraphDisplay
+          components={this.props.components}
+          isExpanded={this.state.isExpanded}
+        />
+      );
+    }
+
+    return <Loading offset='-30px' />;
+  }
+
+  render() {
+    return (
+      <StyledGraphSection isExpanded={this.state.isExpanded}>
+        {this.renderContents()}
+        <ButtonSection>
+          <ExpandButton
+            onClick={() => this.setState({ isExpanded: !this.state.isExpanded })}
+          >
+            <i className="material-icons">
+              {this.state.isExpanded ? 'close_fullscreen' : 'open_in_full'}
+            </i>
+          </ExpandButton>
+        </ButtonSection>
+      </StyledGraphSection>
+    );
+  }
+}
+
+GraphSection.contextType = Context;
+
+const StyledGraphSection = styled.div`
+  width: ${(props: { isExpanded: boolean }) => props.isExpanded ? '100vw' : '100%'};
+  height: ${(props: { isExpanded: boolean }) => props.isExpanded ? '100vh' : '100%'};
+  background: #202227;
+  position: ${(props: { isExpanded: boolean }) => props.isExpanded ? 'fixed' : 'relative'};
+  top: ${(props: { isExpanded: boolean }) => props.isExpanded ? '-25px' : ''};
+  right: ${(props: { isExpanded: boolean }) => props.isExpanded ? '-25px' : ''};
+`;
+
+const ButtonSection = styled.div`
+  position: absolute;
+  top: 17px;
+  right: 15px;
+  display: flex;
+  align-items: center;
+`;
+
+const ExpandButton = styled.div`
+  width: 24px;
+  height: 24px;
+  cursor: pointer;
+  margin-left: 10px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  border-radius: 3px;
+  border: 1px solid #ffffff44;
+
+  :hover {
+    background: #ffffff44; 
+  }
+
+  > i {
+    font-size: 14px;
+  }
+`;

+ 73 - 0
dashboard/src/main/home/dashboard/expanded-chart/ListSection.tsx

@@ -0,0 +1,73 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+import api from '../../../../shared/api';
+
+import { Context } from '../../../../shared/Context';
+import { ResourceType, StorageType, ChartType } from '../../../../shared/types';
+
+import ResourceItem from './ResourceItem';
+import Loading from '../../../../components/Loading';
+
+type PropsType = {
+  currentChart: ChartType,
+  components: ResourceType[]
+};
+
+type StateType = {
+  showKindLabels: boolean
+};
+
+export default class ListSection extends Component<PropsType, StateType> {
+  state = {
+    showKindLabels: true
+  }
+
+  renderResourceList = () => {
+    return this.props.components.map((resource: ResourceType, i: number) => {
+      return (
+        <ResourceItem
+          key={i}
+          resource={resource}
+          toggleKindLabels={() => this.setState({ showKindLabels: !this.state.showKindLabels })}
+          showKindLabels={this.state.showKindLabels}
+        />
+      );
+    });
+  }
+
+  renderContents = () => {
+    if (this.props.components && this.props.components.length > 0) {
+      return (
+        <ResourceList>
+          {this.renderResourceList()}
+        </ResourceList>
+      );
+    }
+
+    return <Loading offset='-30px' />;
+  }
+
+  render() {
+    return (
+      <StyledListSection>
+        {this.renderContents()}
+      </StyledListSection>
+    );
+  }
+}
+
+ListSection.contextType = Context;
+
+const ResourceList = styled.div`
+  width: 100%;
+  overflow-y: auto;
+  padding-bottom: 150px;
+`;
+
+const StyledListSection = styled.div`
+  width: 100%;
+  height: 100%;
+  background: #ffffff11;
+  display: flex;
+  position: relative;
+`;

+ 0 - 185
dashboard/src/main/home/dashboard/expanded-chart/OverviewSection.tsx

@@ -1,185 +0,0 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
-import api from '../../../../shared/api';
-
-import { Context } from '../../../../shared/Context';
-import { ResourceType, StorageType, ChartType } from '../../../../shared/types';
-
-import ResourceItem from './ResourceItem';
-import GraphDisplay from './graph/GraphDisplay';
-
-type PropsType = {
-  toggleExpanded: () => void,
-  isExpanded: boolean,
-  currentChart: ChartType
-};
-
-type StateType = {
-  viewMode: string,
-  showKindLabels: boolean,
-  components: ResourceType[],
-  isLoaded: boolean
-};
-
-export default class OverviewSection extends Component<PropsType, StateType> {
-  state = {
-    viewMode: 'graph',
-    showKindLabels: true,
-    components: [] as ResourceType[],
-    isLoaded: false
-  }
-
-  componentDidMount() {
-    let { currentCluster, setCurrentError } = this.context;
-    let { currentChart } = this.props;
-    api.getChartComponents('<token>', {
-      namespace: currentChart.namespace,
-      context: currentCluster,
-      storage: StorageType.Secret
-    }, { name: currentChart.name, revision: 0 }, (err: any, res: any) => {
-      if (err) {
-        console.log(err)
-      } else {
-        this.setState({components: res.data}, () => {
-          this.setState({isLoaded: true})
-        })
-      }
-    });
-  }
-
-  renderResourceList = () => {
-    return this.state.components.map((resource: ResourceType, i: number) => {
-      return (
-        <ResourceItem
-          key={i}
-          resource={resource}
-          toggleKindLabels={() => this.setState({ showKindLabels: !this.state.showKindLabels })}
-          showKindLabels={this.state.showKindLabels}
-        />
-      );
-    });
-  }
-
-  renderContents = () => {
-    if (this.state.viewMode === 'list') {
-      return (
-        <ResourceList>
-          {this.renderResourceList()}
-        </ResourceList>
-      )
-    }
-
-    return <GraphDisplay components={this.state.components}/>
-  }
-
-  render() {
-    if (!this.state.isLoaded) {
-      return <StyledOverviewSection>
-        Loading...
-      </StyledOverviewSection>
-    }
-    return (
-      <StyledOverviewSection>
-        {this.renderContents()}
-
-        <ButtonSection>
-          <RadioButtons>
-            <RadioOption
-              nudge={true}
-              selected={this.state.viewMode === 'graph'}
-              onClick={() => this.setState({ viewMode: 'graph' })}
-            >
-              <i className="material-icons">device_hub</i> Graph
-            </RadioOption>
-            <RadioOption
-              selected={this.state.viewMode === 'list'}
-              onClick={() => this.setState({ viewMode: 'list' })}
-            >
-              <i className="material-icons">dehaze</i> List
-            </RadioOption>
-          </RadioButtons>
-          <ExpandButton
-            onClick={this.props.toggleExpanded}
-            isExpanded={this.props.isExpanded}
-          >
-            <i className="material-icons">
-              {this.props.isExpanded ? 'close_fullscreen' : 'open_in_full'}
-            </i>
-          </ExpandButton>
-        </ButtonSection>
-      </StyledOverviewSection>
-    );
-  }
-}
-
-OverviewSection.contextType = Context;
-
-const ResourceList = styled.div`
-  width: 100%;
-  overflow-y: auto;
-  padding-bottom: 150px;
-`;
-
-const RadioOption = styled.div`
-  width: 80px;
-  padding-right: 5px;
-  height: 22px;
-  background: ${(props: { selected: boolean, nudge?: boolean }) => props.selected ? '#6A6C70' : '#424349'};
-  display: flex;
-  align-items: center;
-  cursor: pointer;
-  justify-content: center;
-  
-  > i {
-    margin-top: ${(props: { nudge?: boolean, selected: boolean }) => props.nudge ? '-1px' : ''};
-    font-size: 15px;
-    margin-right: 8px;
-  }
-`;
-
-const RadioButtons = styled.div`
-  display: flex;
-  align-items: center;
-  border-radius: 3px;
-  border: 1px solid #ffffff44;
-  font-size: 12px;
-  font-family: 'Works Sans', sans-serif;
-  overflow: hidden;
-`;
-
-const ButtonSection = styled.div`
-  position: absolute;
-  top: 17px;
-  right: 15px;
-  display: flex;
-  align-items: center;
-`;
-
-const ExpandButton = styled.div`
-  width: 24px;
-  height: 24px;
-  cursor: pointer;
-  margin-left: 10px;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  border-radius: 3px;
-  border: 1px solid #ffffff44;
-  background: ${(props: { isExpanded: boolean }) => props.isExpanded ? '#ffffff44' : ''};
-
-  :hover {
-    background: #ffffff44; 
-  }
-
-  > i {
-    font-size: 14px;
-  }
-`;
-
-const StyledOverviewSection = styled.div`
-  width: 100%;
-  height: 100%;
-  background: #ffffff11;
-  display: flex;
-  position: relative;
-`;

+ 2 - 8
dashboard/src/main/home/dashboard/expanded-chart/ResourceItem.tsx

@@ -1,17 +1,11 @@
 import React, { Component } from 'react';
 import styled from 'styled-components';
 
+import { kindToIcon } from '../../../../shared/rosettaStone';
+
 import { ResourceType } from '../../../../shared/types';
 import YamlEditor from '../../../../components/YamlEditor';
 
-const kindToIcon: any = {
-  'Deployment': 'category',
-  'Pod': 'fiber_manual_record',
-  'Service': 'alt_route',
-  'Ingress': 'sensor_door',
-  'StatefulSet': 'location_city',
-  'Secret': 'vpn_key',
-}
 
 type PropsType = {
   resource: ResourceType,

+ 42 - 7
dashboard/src/main/home/dashboard/expanded-chart/graph/Edge.tsx

@@ -1,26 +1,33 @@
 import React, { Component } from 'react';
 import styled from 'styled-components';
 
+import { edgeColors } from '../../../../../shared/rosettaStone';
+import { EdgeType } from '../../../../../shared/types';
+
+const thickness = 8;
+
 type PropsType = {
   x1: number,
   y1: number,
   x2: number,
   y2: number,
   originX: number,
-  originY: number
+  originY: number,
+  edge: EdgeType,
+  setCurrentEdge: (edge: EdgeType) => void
 };
 
 type StateType = {
+  showArrowHead: boolean
 };
 
-const thickness = 1;
-
 export default class Edge extends Component<PropsType, StateType> {
   state = {
+    showArrowHead: true
   }
 
   render() {
-    let { originX, originY } = this.props;
+    let { originX, originY, edge, setCurrentEdge } = this.props;
     let x1 = Math.round(originX + this.props.x1);
     let x2 = Math.round(originX + this.props.x2);
     let y1 = Math.round(originY - this.props.y1);
@@ -39,11 +46,32 @@ export default class Edge extends Component<PropsType, StateType> {
         cx={cx}
         cy={cy}
         angle={angle}
-      />
+        onMouseEnter={() => setCurrentEdge(edge)}
+        onMouseLeave={() => setCurrentEdge(null)}
+      >
+        <VisibleLine color={edgeColors[edge.type]} />
+        {this.state.showArrowHead ? <ArrowHead color={edgeColors[edge.type]} /> : null}
+      </StyledEdge>
     );
   }
 }
 
+const ArrowHead = styled.div`
+  width: 0; 
+  height: 0; 
+  border-top: 5px solid transparent;
+  border-bottom: 5px solid transparent;
+  margin-right: 20px;
+
+  border-left: 10px solid ${(props: { color: string }) => props.color ? props.color : '#ffffff66'};
+`;
+
+const VisibleLine = styled.section`
+  height: 1px;
+  width: 100%;
+  background: ${(props: { color: string }) => props.color ? props.color : '#ffffff66'};
+`;
+
 const StyledEdge: any = styled.div.attrs((props: any) => ({
   style: {
     top: props.cy + 'px',
@@ -54,7 +82,14 @@ const StyledEdge: any = styled.div.attrs((props: any) => ({
 }))`
   position: absolute;
   height: ${thickness}px;
-  background: #ffffff66;
-  color: #ffffff22;
+  cursor: pointer;
   z-index: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  :hover {
+    > section {
+      box-shadow: 0 0 10px #ffffff;
+    }
+  }
 `;

+ 38 - 29
dashboard/src/main/home/dashboard/expanded-chart/graph/GraphDisplay.tsx

@@ -1,33 +1,18 @@
 import React, { Component } from 'react';
 import styled from 'styled-components';
 
+import { ResourceType, NodeType, EdgeType } from '../../../../../shared/types';
+
 import Node from './Node';
 import Edge from './Edge';
-import { ResourceType } from '../../../../../shared/types';
+import InfoPanel from './InfoPanel';
 
 const zoomConstant = 0.01;
 const panConstant = 0.8;
 
-type NodeType = {
-  id: number,
-  name: string,
-  kind: string,
-  x: number,
-  y: number,
-  w: number,
-  h: number,
-  toCursorX?: number,
-  toCursorY?: number,
-}
-
-type EdgeType = {
-  type: string,
-  source: number,
-  target: number,
-}
-
 type PropsType = {
-  components: ResourceType[]
+  components: ResourceType[],
+  isExpanded: boolean
 };
 
 type StateType = {
@@ -45,6 +30,9 @@ type StateType = {
   dragBg: boolean,
   preventDrag: boolean,
   scale: number,
+  showKind: boolean,
+  currentNode: NodeType | null,
+  currentEdge: EdgeType | null,
 };
 
 export default class GraphDisplay extends Component<PropsType, StateType> {
@@ -62,7 +50,10 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
     panY: null as (number | null),
     dragBg: false,
     preventDrag: false,
-    scale: 0.5
+    scale: 0.5,
+    showKind: true,
+    currentNode: null as (NodeType | null),
+    currentEdge: null as (EdgeType | null)
   }
 
   spaceRef: any = React.createRef();
@@ -80,27 +71,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 nodes = components.map((c: ResourceType) => {
-      return {id: c.ID, name: c.Name, kind: c.Kind, x:0, y:0, w:40, h:40}
-    })
+      return { id: c.ID, name: c.Name, kind: c.Kind, x: 0, y: 0, w: 40, h: 40 }
+    });
 
     let edges = [] as EdgeType[]
     components.map((c: ResourceType) => {
       c.Relations.ControlRels.map((rel: any) => {
         if (rel.Source == c.ID) {
-          edges.push({type: "ControlRel", source: rel.Source, target: rel.Target})
+          edges.push({ type: "ControlRel", source: rel.Source, target: rel.Target })
         }
       })
       c.Relations.LabelRels.map((rel: any) => {
         if (rel.Source == c.ID) {
-          edges.push({type: "LabelRel", source: rel.Source, target: rel.Target})
+          edges.push({ type: "LabelRel", source: rel.Source, target: rel.Target })
         }
       })
 
-      this.setState({edges})
-    })
+      this.setState({ edges })
+    });
     this.setState({nodes})
   }
 
+  // Update origin when expanding/collapsing (can improve w/ resize listener)
+  componentDidUpdate(prevProps: PropsType) {
+    if (this.props.isExpanded !== prevProps.isExpanded) {
+      let height = this.spaceRef.offsetHeight;
+      let width = this.spaceRef.offsetWidth;
+      this.setState({
+        originX: Math.round(width / 2),
+        originY: Math.round(height / 2)
+      });  
+    }
+  }
+
   componentWillUnmount() {
     this.spaceRef.removeEventListener("touchmove", (e: any) => e.preventDefault());
     this.spaceRef.removeEventListener("mousewheel", (e: any) => e.preventDefault());
@@ -208,6 +211,8 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
           nodeMouseDown={() => this.handleClickNode(node.id)}
           nodeMouseUp={this.handleReleaseNode}
           isActive={activeIds.includes(node.id)}
+          showKind={this.state.showKind}
+          setCurrentNode={(node: NodeType) => this.setState({ currentNode: node })}
         />
       );
     });
@@ -224,13 +229,14 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
           y1={this.state.nodes[edge.source].y}
           x2={this.state.nodes[edge.target].x}
           y2={this.state.nodes[edge.target].y}
+          edge={edge}
+          setCurrentEdge={(edge: EdgeType) => this.setState({ currentEdge: edge })}
         />
       );
     });
   }
 
   render() {
-    console.log('rendering graph display')
     return (
       <StyledGraphDisplay
         ref={element => this.spaceRef = element}
@@ -241,6 +247,10 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
       >
         {this.renderNodes()}
         {this.renderEdges()}
+        <InfoPanel
+          currentNode={this.state.currentNode}
+          currentEdge={this.state.currentEdge}
+        />
       </StyledGraphDisplay>
     );
   }
@@ -252,5 +262,4 @@ const StyledGraphDisplay = styled.div`
   height: 100%;
   overflow: hidden;
   cursor: move;
-  background: #202227;
 `;

+ 141 - 0
dashboard/src/main/home/dashboard/expanded-chart/graph/InfoPanel.tsx

@@ -0,0 +1,141 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+
+import { kindToIcon, edgeColors } from '../../../../../shared/rosettaStone';
+import { NodeType, EdgeType} from '../../../../../shared/types';
+
+type PropsType = {
+  currentNode: NodeType,
+  currentEdge: EdgeType
+};
+
+type StateType = {
+};
+
+export default class InfoPanel extends Component<PropsType, StateType> {
+  state = {
+  }
+
+  renderIcon = (kind: string) => {
+
+    let icon = 'tonality';
+    if (Object.keys(kindToIcon).includes(kind)) {
+      icon = kindToIcon[kind]; 
+    }
+    
+    return (
+      <IconWrapper>
+        <i className="material-icons">{icon}</i>
+      </IconWrapper>
+    );
+  }
+
+  renderColorBlock = (type: string) => {
+    return <ColorBlock color={edgeColors[type]} />;
+  }
+
+  renderContents = () => {
+    let { currentNode, currentEdge } = this.props;
+    if (currentNode) {
+      return (
+        <Div>
+          {this.renderIcon(currentNode.kind)}
+          {currentNode.kind}
+          <ResourceName>
+            {currentNode.name}
+          </ResourceName>
+        </Div>
+      );
+    } else if (currentEdge) {
+      return (
+        <EdgeInfo>
+          {this.renderColorBlock(currentEdge.type)}
+          {currentEdge.type}
+        </EdgeInfo>
+      )
+    }
+
+    return (
+      <Div>
+        <IconWrapper>
+          <i className="material-icons">info</i>
+        </IconWrapper>
+        Hover over a node or edge to display info.
+      </Div>
+    )
+  }
+
+  render() {
+    return (
+      <StyledInfoPanel>
+        {this.renderContents()}
+      </StyledInfoPanel>
+    );
+  }
+}
+
+const ColorBlock = styled.div`
+  width: 15px;
+  height: 15px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border-radius: 3px;
+  margin-left: -2px;
+  margin-right: 13px;
+  background: ${(props: { color: string }) => props.color ? props.color : '#ffffff66'};
+`;
+
+const Div = styled.div`
+  display: flex;
+  align-items: center;
+`;
+
+const EdgeInfo = styled.div`
+  display: flex;
+  align-items: center;
+  margin-top: 5px;
+`;
+
+const ResourceName = styled.div`
+  color: #ffffff;
+  margin-left: 10px;
+  text-transform: none;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+`;
+
+const IconWrapper = styled.div`
+  width: 25px;
+  height: 25px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  > i {
+    font-size: 16px;
+    color: #ffffff;
+    margin-right: 14px;
+  }
+`;
+
+const StyledInfoPanel = styled.div`
+  position: absolute;
+  right: 15px;
+  bottom: 15px;
+  color: #ffffff66;
+  height: 40px;
+  width: 400px;
+  max-width: 600px;
+  background: #44444699;
+  border-radius: 3px;
+  padding-left: 20px;
+  display: inline-block;
+  z-index: 999;
+  padding-top: 7px;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  padding-right: 13px;
+`;

+ 56 - 41
dashboard/src/main/home/dashboard/expanded-chart/graph/Node.tsx

@@ -1,24 +1,8 @@
 import React, { Component } from 'react';
 import styled from 'styled-components';
 
-const kindToIcon: any = {
-  'Deployment': 'category',
-  'Pod': 'fiber_manual_record',
-  'Service': 'alt_route',
-  'Ingress': 'sensor_door',
-  'StatefulSet': 'location_city',
-  'Secret': 'vpn_key',
-}
-
-type NodeType = {
-  id: number,
-  name: string,
-  kind: string,
-  x: number,
-  y: number,
-  w: number,
-  h: number
-}
+import { kindToIcon } from '../../../../../shared/rosettaStone';
+import { NodeType } from '../../../../../shared/types';
 
 type PropsType = {
   node: NodeType,
@@ -26,7 +10,9 @@ type PropsType = {
   originY: number,
   nodeMouseDown: () => void,
   nodeMouseUp: () => void,
-  isActive: boolean
+  isActive: boolean,
+  showKind: boolean,
+  setCurrentNode: (node: NodeType) => void,
 };
 
 type StateType = {
@@ -51,30 +37,72 @@ export default class Node extends Component<PropsType, StateType> {
         y={Math.round(originY - y - (h / 2))}
         w={Math.round(w)}
         h={Math.round(h)}
-        onMouseDown={nodeMouseDown}
-        onMouseUp={nodeMouseUp}
         isActive={isActive}
       >
-        <i className="material-icons">{icon}</i>
-        <NodeLabel>{kind}: {name}</NodeLabel>
+        <Kind>
+          {this.props.showKind ? kind : null}
+        </Kind>
+        <NodeBlock 
+          onMouseDown={nodeMouseDown}
+          onMouseUp={nodeMouseUp}
+          onMouseEnter={() => this.props.setCurrentNode(this.props.node)}
+          onMouseLeave={() => this.props.setCurrentNode(null)}
+        >
+          <i className="material-icons">{icon}</i>
+        </NodeBlock>
+        <NodeLabel>
+          {name}
+        </NodeLabel>
       </StyledNode>
     );
   }
 }
 
+const Kind = styled.div`
+  color: #ffffff33;
+  position: absolute;
+  top: -25px;
+  width: 140px;
+  left: -50px;
+  text-align: center;
+  font-size: 13px;
+  font-family: 'Work Sans', sans-serif;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+`;
+
 const NodeLabel = styled.div`
   position: absolute;
   bottom: -25px;
   color: #aaaabb;
-  padding-top: 12px;
-  width: 40px;
-  left: 0px;
+  width: 140px;
+  left: -50px;
   font-size: 13px;
-  white-space: nowrap;
   font-family: 'Work Sans', sans-serif;
+  text-align: center;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+`;
+
+const NodeBlock = styled.div`
+  background: #444446;
+  width: 100%;
+  height: 100%;
   display: flex;
   align-items: center;
   justify-content: center;
+  border-radius: 100px;
+  cursor: grab;
+  :hover {
+    background: #555556;
+  }
+
+  > i {
+    color: white;
+    font-size: 18px;
+  }
 `;
 
 const StyledNode: any = styled.div.attrs((props: NodeType) => ({
@@ -86,21 +114,8 @@ const StyledNode: any = styled.div.attrs((props: NodeType) => ({
   position: absolute;
   width: ${(props: NodeType) => props.w + 'px'};;
   height: ${(props: NodeType) => props.h + 'px'};;
-  background: #444446;
   box-shadow: ${(props: any) => props.isActive ? '0 0 10px #ffffff66' : '0px 0px 10px 2px #00000022'};
-  cursor: grab;
-  border-radius: 5px;
   color: #ffffff22;
-  display: flex;
-  align-items: center;
-  justify-content: center;
+  border-radius: 100px;
   z-index: 1;
-  :hover {
-    background: #555556;
-  }
-
-  > i {
-    color: white;
-    font-size: 18px;
-  }
 `;

+ 1 - 0
dashboard/src/main/home/modals/ClusterConfigModal.tsx

@@ -207,6 +207,7 @@ export default class ClusterConfigModal extends Component<PropsType, StateType>
         </Header>
         <ModalTitle>Connect from Kubeconfig</ModalTitle>
         <TabSelector
+          currentTab={this.state.currentTab}
           options={tabOptions}
           setCurrentTab={(value: string) => this.setState({ currentTab: value })}
           tabWidth='120px'

+ 1 - 1
dashboard/src/main/home/sidebar/Sidebar.tsx

@@ -165,7 +165,7 @@ const NavButton = styled.div`
 const BottomSection = styled.div`
   position: absolute;
   width: 100%;
-  bottom: 12px;
+  bottom: 10px;
 `;
 
 const LogOutButton = styled(NavButton)`

+ 13 - 0
dashboard/src/shared/rosettaStone.tsx

@@ -0,0 +1,13 @@
+export const kindToIcon: any = {
+  'Deployment': 'category',
+  'Pod': 'fiber_manual_record',
+  'Service': 'alt_route',
+  'Ingress': 'sensor_door',
+  'StatefulSet': 'location_city',
+  'Secret': 'vpn_key'
+}
+
+export const edgeColors: any = {
+  'ControlRel': '#fcb603',
+  'LabelRel': '#9a93fa'
+};

+ 19 - 0
dashboard/src/shared/types.tsx

@@ -38,6 +38,25 @@ export interface ResourceType {
   Relations: any
 }
 
+export interface NodeType {
+  id: number,
+  name: string,
+  kind: string,
+  x: number,
+  y: number,
+  w: number,
+  h: number,
+  toCursorX?: number,
+  toCursorY?: number
+}
+
+export interface EdgeType {
+  type: string,
+  source: number,
+  target: number
+}
+
+
 export enum StorageType {
   Secret = 'secret',
   ConfigMap = 'configmap',