Quellcode durchsuchen

Merge branch 'frontend-graph' into helm-manifest

sunguroku vor 5 Jahren
Ursprung
Commit
dc9dc8e7ac

+ 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);
   }
 

+ 12 - 0
dashboard/src/main/Login.tsx

@@ -25,6 +25,18 @@ export default class Login extends Component<PropsType, StateType> {
     credentialError: false
   }
 
+  handleKeyDown = (e: any) => {
+    e.key === 'Enter' ? this.handleLogin() : null;
+  }
+
+  componentDidMount() {
+    document.addEventListener("keydown", this.handleKeyDown);
+  }
+
+  componentWillUnmount() {
+    document.removeEventListener("keydown", this.handleKeyDown);
+  }
+
   handleLogin = (): void => {
     let { email, password } = this.state;
     let { authenticate } = this.props;

+ 12 - 0
dashboard/src/main/Register.tsx

@@ -27,6 +27,18 @@ export default class Register extends Component<PropsType, StateType> {
     confirmPasswordError: false
   }
 
+  handleKeyDown = (e: any) => {
+    e.key === 'Enter' ? this.handleRegister() : null;
+  }
+
+  componentDidMount() {
+    document.addEventListener("keydown", this.handleKeyDown);
+  }
+
+  componentWillUnmount() {
+    document.removeEventListener("keydown", this.handleKeyDown);
+  }
+
   handleRegister = (): void => {
     let { email, password, confirmPassword } = this.state;
     let { authenticate } = this.props;

+ 20 - 3
dashboard/src/main/home/Home.tsx

@@ -14,14 +14,20 @@ type PropsType = {
 };
 
 type StateType = {
+  forceSidebar: boolean,
+  showWelcome: boolean
 };
 
 export default class Home extends Component<PropsType, StateType> {
+  state = {
+    forceSidebar: true,
+    showWelcome: false
+  }
 
   renderDashboard = () => {
     let { currentCluster, setCurrentModal, setCurrentModalData } = this.context;
 
-    if (currentCluster === '') {
+    if (currentCluster === '' || this.state.showWelcome) {
       return (
         <DashboardWrapper>
           <Placeholder>
@@ -41,7 +47,14 @@ export default class Home extends Component<PropsType, StateType> {
       return <Loading />
     }
 
-    return <DashboardWrapper><Dashboard currentCluster={currentCluster} /></DashboardWrapper>
+    return (
+      <DashboardWrapper>
+        <Dashboard
+          currentCluster={currentCluster}
+          setSidebar={(x: boolean) => this.setState({ forceSidebar: x })}
+        />
+      </DashboardWrapper>
+    );
   }
 
   render() {
@@ -56,7 +69,11 @@ export default class Home extends Component<PropsType, StateType> {
           <ClusterConfigModal />
         </ReactModal>
 
-        <Sidebar logOut={this.props.logOut} />
+        <Sidebar
+          logOut={this.props.logOut}
+          forceSidebar={this.state.forceSidebar}
+          setWelcome={(x: boolean) => this.setState({ showWelcome: x })}
+        />
         <StyledDashboard>
           {this.renderDashboard()}
         </StyledDashboard>

+ 0 - 2
dashboard/src/main/home/Toolbar.tsx

@@ -4,7 +4,6 @@ import ReactModal from 'react-modal';
 
 import { Context } from '../../shared/Context';
 
-import Sidebar from './sidebar/Sidebar';
 import ClusterConfigModal from './modals/ClusterConfigModal';
 
 type PropsType = {
@@ -27,7 +26,6 @@ export default class Home extends Component<PropsType, StateType> {
           <ClusterConfigModal />
         </ReactModal>
 
-        <Sidebar logOut={this.props.logOut} />
         <DummyDashboard>
           🏗️🏗️🏗️🏗️🏗️
         </DummyDashboard>

+ 4 - 2
dashboard/src/main/home/dashboard/Dashboard.tsx

@@ -11,7 +11,8 @@ import NamespaceSelector from './NamespaceSelector';
 import ExpandedChart from './expanded-chart/ExpandedChart';
 
 type PropsType = {
-  currentCluster: string
+  currentCluster: string,
+  setSidebar: (x: boolean) => void
 };
 
 type StateType = {
@@ -51,7 +52,7 @@ export default class Dashboard extends Component<PropsType, StateType> {
   }
 
   renderContents = () => {
-    let { currentCluster } = this.props;
+    let { currentCluster, setSidebar } = this.props;
 
     if (this.state.currentChart) {
       return (
@@ -59,6 +60,7 @@ export default class Dashboard extends Component<PropsType, StateType> {
           currentChart={this.state.currentChart}
           refreshChart={this.refreshChart}
           setCurrentChart={(x: ChartType | null) => this.setState({ currentChart: x })}
+          setSidebar={setSidebar}
         />
       );
     }

+ 83 - 84
dashboard/src/main/home/dashboard/expanded-chart/ExpandedChart.tsx

@@ -2,36 +2,56 @@ 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,
   setCurrentChart: (x: ChartType | null) => void,
-  refreshChart: () => void
+  refreshChart: () => void,
+  setSidebar: (x: boolean) => void
 };
 
 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 = () => {
@@ -52,14 +72,20 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
   }
 
   renderTabContents = () => {
-    let { currentChart, refreshChart } = this.props;
+    let { currentChart, refreshChart, setSidebar} = 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
+          components={this.state.components}
+          setSidebar={setSidebar}
+        />
+      );
+    } else if (this.state.currentTab === 'list') {
+      return (
+        <ListSection
           currentChart={currentChart}
+          components={this.state.components}
         />
       );
     }
@@ -72,77 +98,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 +106,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>

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

@@ -0,0 +1,53 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+
+import { Context } from '../../../../shared/Context';
+import { ResourceType } from '../../../../shared/types';
+
+import GraphDisplay from './graph/GraphDisplay';
+import Loading from '../../../../components/Loading';
+
+type PropsType = {
+  components: ResourceType[],
+  setSidebar: (x: boolean) => void
+};
+
+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
+          setSidebar={this.props.setSidebar}
+          components={this.props.components}
+          isExpanded={this.state.isExpanded}
+        />
+      );
+    }
+
+    return <Loading offset='-30px' />;
+  }
+
+  render() {
+    return (
+      <StyledGraphSection>
+        {this.renderContents()}
+      </StyledGraphSection>
+    );
+  }
+}
+
+GraphSection.contextType = Context;
+
+const StyledGraphSection = styled.div`
+  width: 100%;
+  height: 100%;
+  background: #ffffff11;
+`;

+ 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;
-`;

+ 4 - 9
dashboard/src/main/home/dashboard/expanded-chart/ResourceItem.tsx

@@ -1,17 +1,12 @@
 import React, { Component } from 'react';
 import styled from 'styled-components';
+import yaml from 'js-yaml';
+
+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,
@@ -28,7 +23,7 @@ type StateType = {
 export default class ResourceItem extends Component<PropsType, StateType> {
   state = {
     expanded: false,
-    RawYAML: JSON.stringify(this.props.resource.RawYAML)
+    RawYAML: yaml.dump(this.props.resource.RawYAML)
   }
 
   renderIcon = (kind: string) => {

+ 44 - 9
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 = 12;
+
 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);
@@ -31,7 +38,7 @@ export default class Edge extends Component<PropsType, StateType> {
     var cx = ((x1 + x2) / 2) - (length / 2);
     var cy = ((y1 + y2) / 2) - (thickness / 2);
     // angle
-    var angle = Math.atan2((y1-y2),(x1-x2))*(180/Math.PI);
+    var angle = Math.atan2((y1 - y2), (x1 - x2)) * (180 / Math.PI);
 
     return (
       <StyledEdge
@@ -39,11 +46,32 @@ export default class Edge extends Component<PropsType, StateType> {
         cx={cx}
         cy={cy}
         angle={angle}
-      />
+        onMouseEnter={() => setCurrentEdge(edge)}
+        onMouseLeave={() => setCurrentEdge(null)}
+        type={edge.type}
+      >
+        {this.state.showArrowHead ? <ArrowHead color={edgeColors[edge.type]} /> : null}
+        <VisibleLine color={edgeColors[edge.type]} />
+      </StyledEdge>
     );
   }
 }
 
+const ArrowHead = styled.div`
+  width: 0; 
+  height: 0;
+  margin-left: 20px;
+  border-top: 5px solid transparent;
+  border-bottom: 5px solid transparent; 
+  border-right: 10px solid ${(props: { color: string }) => props.color ? props.color : '#ffffff66'};
+`;
+
+const VisibleLine = styled.section`
+  height: 2px;
+  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;
-  z-index: 0;
+  cursor: pointer;
+  z-index: ${(props: { type: string, color: string }) => props.type == 'ControlRel' ? '1' : '0'};
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  :hover {
+    > section {
+      box-shadow: 0 0 10px #ffffff;
+    }
+  }
 `;

+ 251 - 69
dashboard/src/main/home/dashboard/expanded-chart/graph/GraphDisplay.tsx

@@ -1,52 +1,47 @@
 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';
+import SelectRegion from './SelectRegion';
 
 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,
+  setSidebar: (x: boolean) => void
 };
 
 type StateType = {
   nodes: NodeType[],
   edges: EdgeType[],
-  activeIds: number[],
-  originX: number | null,
+  activeIds: number[], // IDs of all currently selected nodes
+  originX: number | null, 
   originY: number | null,
   cursorX: number | null,
   cursorY: number | null,
-  deltaX: number | null,
-  deltaY: number | null,
-  panX: number | null,
-  panY: number | null,
-  dragBg: boolean,
-  preventDrag: boolean,
+  deltaX: number | null, // Dragging bg x-displacement
+  deltaY: number | null, // Dragging y-displacement
+  panX: number | null, // Two-finger pan x-displacement 
+  panY: number | null, // Two-finger pan y-displacement
+  anchorX: number | null, // Initial cursorX during region select
+  anchorY: number | null, // Initial cursorY during region select
+  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
   scale: number,
+  showKindLabels: boolean,
+  currentNode: NodeType | null,
+  currentEdge: EdgeType | null,
+  isExpanded: boolean
 };
 
+// TODO: region-based unselect, shift-click, multi-region
 export default class GraphDisplay extends Component<PropsType, StateType> {
   state = {
     nodes: [] as NodeType[],
@@ -60,15 +55,24 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
     deltaY: null as (number | null),
     panX: null as (number | null),
     panY: null as (number | null),
+    anchorX: null as (number | null),
+    anchorY: null as (number | null),
     dragBg: false,
-    preventDrag: false,
-    scale: 0.5
+    preventBgDrag: false,
+    scale: 0.5,
+    showKindLabels: true,
+    currentNode: null as (NodeType | null),
+    currentEdge: null as (EdgeType | null),
+    relocateAllowed: false,
+    isExpanded: false
   }
 
   spaceRef: any = React.createRef();
 
   componentDidMount() {
     let { components } = this.props;
+
+    // Initialize origin
     let height = this.spaceRef.offsetHeight;
     let width = this.spaceRef.offsetWidth;
     this.setState({
@@ -80,54 +84,79 @@ 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 };
+    });
+
+    document.addEventListener("keydown", this.handleKeyDown);
+    document.addEventListener("keyup", this.handleKeyUp);
 
-    let edges = [] as EdgeType[]
+    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({nodes})
+      this.setState({ edges });
+    });
+    this.setState({ nodes });
   }
 
   componentWillUnmount() {
     this.spaceRef.removeEventListener("touchmove", (e: any) => e.preventDefault());
     this.spaceRef.removeEventListener("mousewheel", (e: any) => e.preventDefault());
+    document.removeEventListener("keydown", this.handleKeyDown);
+    document.removeEventListener("keyup", this.handleKeyUp);
+  }
+
+  // Handle shift key for multi-select
+  handleKeyDown = (e: any) => {
+    if (e.key === 'Shift') {
+      this.setState({
+        anchorX: this.state.cursorX,
+        anchorY: this.state.cursorY,
+        relocateAllowed: false
+      });
+    }
+  }
+
+  handleKeyUp = (e: any) => {
+    if (e.key === 'Shift') {
+      this.setState({ anchorX: null, anchorY: null });
+    }
   }
 
   // Push to activeIds if not already present
-  handleClickNode = (id: number) => {
+  handleClickNode = (clickedId: number) => {
     let holding = this.state.activeIds;
-    if (!holding.includes(id)) {
-      holding.push(id);
+    if (!holding.includes(clickedId)) {
+      holding.push(clickedId);
     }
 
     // Track and store offset to grab node from anywhere (must store)
-    let node = this.state.nodes[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;
-    }
+    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;
+        }
+      }
+    });
 
-    this.setState({ activeIds: holding, preventDrag: true });
+    this.setState({ activeIds: holding, preventBgDrag: true, relocateAllowed: true });
   }
 
   handleReleaseNode = () => {
-    this.setState({ activeIds: [], preventDrag: false });
+    this.setState({ activeIds: [], preventBgDrag: false });
 
     // Only update dot position state on release for all active
     let { activeIds, nodes} = this.state;
@@ -138,8 +167,8 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
     }
   }
 
-  onMouseMove = (e: any) => {
-    let { originX, originY, dragBg, preventDrag, scale, panX, panY } = this.state;
+  handleMouseMove = (e: any) => {
+    let { originX, originY, dragBg, preventBgDrag, scale, panX, panY, anchorX, anchorY, nodes, activeIds, relocateAllowed } = this.state;
     
     // Suppress navigation gestures
     if (scale !== 1 || panX !== 0 || panY !== 0) {
@@ -153,13 +182,25 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
     this.setState({ cursorX, cursorY });
 
     // Track delta for dragging background
-    if (dragBg && !preventDrag) {
+    if (dragBg && !preventBgDrag) {
       this.setState({ deltaX: e.movementX, deltaY: e.movementY });
     }
+
+    // Check if within select region 
+    if (anchorX && anchorY) {
+      nodes.forEach((node: NodeType) => {
+        if (node.x > Math.min(anchorX, cursorX) && node.x < Math.max(anchorX, cursorX)
+          && node.y > Math.min(anchorY, cursorY) && node.y < Math.max(anchorY, cursorY)
+        ) {
+          activeIds.push(node.id);
+          this.setState({ activeIds });
+        }
+      });
+    } 
   }
 
   // Handle pan XOR zoom (two-finger gestures count as onWheel)
-  handleOnWheel = (e: any) => {
+  handleWheel = (e: any) => {
 
     // Pinch/zoom sets e.ctrlKey to true
     if (e.ctrlKey) {
@@ -171,20 +212,38 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
     }
   };
 
+  toggleExpanded = () => {
+    this.setState({ isExpanded: !this.state.isExpanded }, () => {
+      this.props.setSidebar(!this.state.isExpanded);
+
+      // Update origin on expand/collapse
+      let height = this.spaceRef.offsetHeight;
+      let width = this.spaceRef.offsetWidth;
+      let nudge = 0;
+      if (!this.state.isExpanded) {
+        nudge = 100;
+      }
+      this.setState({
+        originX: Math.round(width / 2) - nudge,
+        originY: Math.round(height / 2)
+      });  
+    });
+  }
+
   // Pass origin to node for offset
   renderNodes = () => {
-    let { activeIds, originX, originY, cursorX, cursorY, scale, panX, panY } = this.state;
+    let { activeIds, originX, originY, cursorX, cursorY, scale, panX, panY, anchorX, anchorY, relocateAllowed } = this.state;
 
     return this.state.nodes.map((node: NodeType, i: number) => {
 
-      // Update dot position if currently selected
-      if (activeIds.includes(node.id)) {
+      // Update position if not highlighting and active
+      if (activeIds.includes(node.id) && relocateAllowed && !anchorX && !anchorY) {
         node.x = cursorX + node.toCursorX;
         node.y = cursorY + node.toCursorY;
       }
 
       // Apply movement from dragging background
-      if (this.state.dragBg && !this.state.preventDrag) {
+      if (this.state.dragBg && !this.state.preventBgDrag) {
         node.x += this.state.deltaX;
         node.y -= this.state.deltaY;
       }
@@ -196,8 +255,10 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
       }
 
       // Apply pan 
-      node.x -= panConstant * panX;
-      node.y += panConstant * panY;
+      if (this.state.panX !== 0 || this.state.panY !== 0) {
+        node.x -= panConstant * panX;
+        node.y += panConstant * panY;
+      }
       
       return (
         <Node
@@ -208,6 +269,8 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
           nodeMouseDown={() => this.handleClickNode(node.id)}
           nodeMouseUp={this.handleReleaseNode}
           isActive={activeIds.includes(node.id)}
+          showKindLabels={this.state.showKindLabels}
+          setCurrentNode={(node: NodeType) => this.setState({ currentNode: node })}
         />
       );
     });
@@ -224,33 +287,152 @@ 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 })}
         />
       );
     });
   }
 
+  renderSelectRegion = () => {
+    if (this.state.anchorX && this.state.anchorY) {
+      return (
+        <SelectRegion
+          anchorX={this.state.anchorX}
+          anchorY={this.state.anchorY}
+          originX={this.state.originX}
+          originY={this.state.originY}
+          cursorX={this.state.cursorX}
+          cursorY={this.state.cursorY}
+        />
+      );
+    }
+  }
+
   render() {
-    console.log('rendering graph display')
     return (
       <StyledGraphDisplay
+        isExpanded={this.state.isExpanded}
         ref={element => this.spaceRef = element}
-        onMouseMove={this.onMouseMove}
-        onMouseDown={() => this.setState({ dragBg: true })}
-        onMouseUp={() => this.setState({ dragBg: false })}
-        onWheel={this.handleOnWheel}
+        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: [] })}
+        onWheel={this.handleWheel}
       >
         {this.renderNodes()}
         {this.renderEdges()}
+        {this.renderSelectRegion()}
+
+        <ButtonSection>
+          <ToggleLabel
+            onClick={() => this.setState({ showKindLabels: !this.state.showKindLabels })}
+          >
+            <Checkbox checked={this.state.showKindLabels}>
+                <i className="material-icons">done</i>
+            </Checkbox>
+            Show Type
+          </ToggleLabel>
+          <ExpandButton
+            onClick={this.toggleExpanded}
+          >
+            <i className="material-icons">
+              {this.state.isExpanded ? 'close_fullscreen' : 'open_in_full'}
+            </i>
+          </ExpandButton>
+        </ButtonSection>
+        <InfoPanel
+          currentNode={this.state.currentNode}
+          currentEdge={this.state.currentEdge}
+        />
       </StyledGraphDisplay>
     );
   }
 }
 
-const StyledGraphDisplay = styled.div`
+const Checkbox = styled.div`
+  width: 16px;
+  height: 16px;
+  border: 1px solid #ffffff44;
+  margin: 0px 8px 0px 3px;
+  border-radius: 3px;
+  background: ${(props: { checked: boolean }) => props.checked ? '#ffffff22' : ''};
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #ffffff;
+
+  > i {
+    font-size: 12px;
+    padding-left: 0px;
+    display: ${(props: { checked: boolean }) => props.checked ? '' : 'none'};
+  }
+`;
+
+const ToggleLabel = styled.div`
+  font: 12px 'Work Sans';
+  color: #ffffff;
   position: relative;
-  width: 100%;
-  height: 100%;
+  height: 24px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  border-radius: 3px;
+  padding-right: 5px;
+  cursor: pointer;
+  border: 1px solid #ffffff44;
+  :hover {
+    background: #ffffff22;
+
+    > div {
+      background: #ffffff22;
+    }
+  }
+`;
+
+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;
+  }
+`;
+
+const StyledGraphDisplay = styled.div`
   overflow: hidden;
   cursor: move;
+  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' : ''};
 `;

+ 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;
+`;

+ 57 - 42
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,
+  showKindLabels: 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.showKindLabels ? 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;
-  z-index: 1;
-  :hover {
-    background: #555556;
-  }
-
-  > i {
-    color: white;
-    font-size: 18px;
-  }
+  border-radius: 100px;
+  z-index: 3;
 `;

+ 60 - 0
dashboard/src/main/home/dashboard/expanded-chart/graph/SelectRegion.tsx

@@ -0,0 +1,60 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+
+type PropsType = {
+  anchorX: number,
+  anchorY: number,
+  originX: number,
+  originY: number,
+  cursorX: number,
+  cursorY: number
+};
+
+type StateType = {
+};
+
+export default class SelectRegion extends Component<PropsType, StateType> {
+  state = {
+  }
+
+  render() {
+    let { cursorX, cursorY, anchorX, anchorY, originX, originY } = this.props;
+    
+    var x, y, w, h;
+    if (cursorY < anchorY) {
+      y = anchorY;
+    } else {
+      y = cursorY;
+    }
+    if (cursorX < anchorX) {
+      x = cursorX;
+    } else {
+      x = anchorX;
+    }
+
+    w = Math.abs(cursorX - anchorX);
+    h = Math.abs(cursorY - anchorY);
+
+    return (
+      <StyledSelectRegion
+        x={Math.round(originX + x)}
+        y={Math.round(originY - y)}
+        w={w}
+        h={h}
+      />
+    );
+  }
+}
+
+const StyledSelectRegion: any = styled.div.attrs((props: { x: number, y: number, w: number, h: number }) => ({
+  style: {
+    top: props.y + 'px',
+    left: props.x + 'px',
+    width: props.w + 'px',
+    height: props.h + 'px'
+    },
+}))`
+  position: absolute;
+  background: #ffffff22;
+  z-index: 1;
+`;

+ 7 - 6
dashboard/src/main/home/modals/ClusterConfigModal.tsx

@@ -41,7 +41,7 @@ export default class ClusterConfigModal extends Component<PropsType, StateType>
     // Parse kubeconfig to retrieve all possible clusters
     api.getContexts('<token>', {}, { id: userId }, (err: any, res: any) => {
       if (err) {
-        setCurrentError(JSON.stringify(err));
+        // setCurrentError(JSON.stringify(err));
       } else {
         this.setState({ kubeContexts: res.data });
       }
@@ -57,7 +57,7 @@ export default class ClusterConfigModal extends Component<PropsType, StateType>
 
     api.getUser('<token>', {}, { id: userId }, (err: any, res: any) => {
       if (err) {
-        setCurrentError(JSON.stringify(err));
+        // setCurrentError(JSON.stringify(err));
       } else if (res.data.rawKubeConfig !== '') {
         this.setState({ rawKubeconfig: res.data.rawKubeConfig });
       }
@@ -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'
@@ -250,10 +251,10 @@ const UploadButton = styled.button`
 `;
 
 const Checkbox = styled.div`
-  width: 15px;
-  height: 15px;
+  width: 16px;
+  height: 16px;
   border: 1px solid #ffffff44;
-  margin: 0px 15px 0px 12px;
+  margin: 1px 15px 0px 12px;
   border-radius: 3px;
   background: ${(props: { checked: boolean }) => props.checked ? '#ffffff22' : ''};
   display: flex;
@@ -262,7 +263,7 @@ const Checkbox = styled.div`
 
   > i {
     font-size: 12px;
-    padding-left: 1px;
+    padding-left: 0px;
     display: ${(props: { checked: boolean }) => props.checked ? '' : 'none'};
   }
 `;

+ 7 - 3
dashboard/src/main/home/sidebar/ClusterSection.tsx

@@ -10,7 +10,8 @@ import Drawer from './Drawer';
 
 type PropsType = {
   forceCloseDrawer: boolean,
-  releaseDrawer: () => void
+  releaseDrawer: () => void,
+  setWelcome: (x: boolean) => void
 };
 
 type StateType = {
@@ -34,9 +35,12 @@ export default class ClusterSection extends Component<PropsType, StateType> {
     // TODO: query with selected filter once implemented
     api.getContexts('<token>', {}, { id: userId }, (err: any, res: any) => {
       if (err) {
-        setCurrentError('Could not read clusters: ' + JSON.stringify(err));
-      } else {
 
+        // Assume intializing if no contexts
+        this.props.setWelcome(true);
+      } else {
+        this.props.setWelcome(false);
+        
         // TODO: handle uninitialized kubeconfig
         if (res.data) {
 

+ 12 - 2
dashboard/src/main/home/sidebar/Sidebar.tsx

@@ -8,7 +8,9 @@ import { Context } from '../../../shared/Context';
 import ClusterSection from './ClusterSection';
 
 type PropsType = {
-  logOut: () => void
+  logOut: () => void,
+  forceSidebar: boolean,
+  setWelcome: (x: boolean) => void
 };
 
 type StateType = {
@@ -40,6 +42,13 @@ export default class Sidebar extends Component<PropsType, StateType> {
     document.removeEventListener('keyup', this.handleKeyUp);
   }
 
+  // Need to override showDrawer when the sidebar is closed
+  componentDidUpdate(prevProps: PropsType) {
+    if (prevProps.forceSidebar !== this.props.forceSidebar) {
+      this.setState({ showSidebar: this.props.forceSidebar });
+    }
+  }  
+
   handleKeyDown = (e: KeyboardEvent): void => {
     if (e.key === 'Meta' || e.key === 'Control') {
       this.setState({ pressingCtrl: true });
@@ -115,6 +124,7 @@ export default class Sidebar extends Component<PropsType, StateType> {
           <ClusterSection 
             forceCloseDrawer={this.state.forceCloseDrawer} 
             releaseDrawer={() => this.setState({ forceCloseDrawer: false })}
+            setWelcome={this.props.setWelcome}
           />
 
           <BottomSection>
@@ -165,7 +175,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 = {
+  'LabelRel': '#949EFF',
+  'ControlRel': '#fcb603'
+};

+ 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',