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

Merge branch 'master' of https://github.com/porter-dev/porter into api-users

mergin master
Alexander Belanger 5 лет назад
Родитель
Сommit
026d54a4ce

Разница между файлами не показана из-за своего большого размера
+ 80 - 719
dashboard/package-lock.json


+ 4 - 2
dashboard/package.json

@@ -7,12 +7,13 @@
     "@testing-library/react": "^9.3.2",
     "@testing-library/user-event": "^7.1.2",
     "@types/jest": "^24.0.0",
-    "@types/node": "^12.0.0",
+    "@types/node": "^12.12.62",
     "@types/react": "^16.9.49",
     "@types/react-dom": "^16.9.8",
     "@types/react-modal": "^3.10.6",
     "@types/styled-components": "^5.1.3",
     "ace-builds": "^1.4.12",
+    "axios": "^0.20.0",
     "react": "^16.13.1",
     "react-ace": "^9.1.3",
     "react-dom": "^16.13.1",
@@ -41,5 +42,6 @@
       "last 1 firefox version",
       "last 1 safari version"
     ]
-  }
+  },
+  "devDependencies": {}
 }

+ 3 - 25
dashboard/src/App.tsx

@@ -1,10 +1,8 @@
 import React, { Component } from 'react';
 import styled, { createGlobalStyle } from 'styled-components';
 
-import { ContextProvider } from './Context';
-import Login from './main/Login';
-import Register from './main/Register';
-import Home from './main/home/Home';
+import { ContextProvider } from './shared/Context';
+import Main from './main/Main';
 
 type PropsType = {
 };
@@ -16,30 +14,10 @@ type StateType = {
 };
 
 export default class App extends Component<PropsType, StateType> {
-  state = {
-    isLoading: false,
-    isLoggedIn: false,
-    uninitialized: true,
-  }
-
-  renderContents = (): JSX.Element => {
-    if (this.state.isLoading) {
-      return <h1>Loading...</h1>
-    } else if (this.state.isLoggedIn) {
-      return <Home logOut={() => this.setState({ isLoggedIn: false })} />
-    } else if (this.state.uninitialized) {
-      return <Register authenticate={() => this.setState({ isLoggedIn: true })} />
-    }
-    return <Login authenticate={() => this.setState({ isLoggedIn: true })} />
-  }
-
   render() {
     return (
       <ContextProvider>
-        <StyledApp>
-          <GlobalStyle />
-          {this.renderContents()}
-        </StyledApp>
+        <Main />
       </ContextProvider>
     );
   }

+ 0 - 0
dashboard/src/templates/Boilerplate.tsx → dashboard/src/components/Boilerplate.tsx


+ 0 - 0
dashboard/src/lib/YamlEditor.tsx → dashboard/src/components/YamlEditor.tsx


+ 0 - 13
dashboard/src/index.css

@@ -1,13 +0,0 @@
-body {
-  margin: 0;
-  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
-    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
-    sans-serif;
-  -webkit-font-smoothing: antialiased;
-  -moz-osx-font-smoothing: grayscale;
-}
-
-code {
-  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
-    monospace;
-}

+ 0 - 1
dashboard/src/index.tsx

@@ -1,6 +1,5 @@
 import React from 'react';
 import ReactDOM from 'react-dom';
-import './index.css';
 import App from './App';
 
 ReactDOM.render(

+ 0 - 74
dashboard/src/lib/YamlEditor.jsx

@@ -1,74 +0,0 @@
-import React from 'react';
-import styled from 'styled-components';
-import AceEditor from 'react-ace';
-
-import 'ace-builds/src-noconflict/mode-yaml';
-import 'ace-builds/src-noconflict/theme-monokai';
-
-class YamlEditor extends React.Component {
-  constructor(props) {
-    super(props);
-    this.state = {
-      yaml: ``,
-    }
-    this.handleChange = this.handleChange.bind(this);
-    this.handleSubmit = this.handleSubmit.bind(this);
-  }
-
-  // Uses the yaml-lint library to determine if a given string is valid yaml.
-  // If the code is invalid, it returns an error message detailing what went wrong.
-  checkYaml = (y) => {
-    /*
-    yamlLint.lint(y).then(() => {
-      alert('Valid YAML file.');
-    }).catch((error) => {
-      alert(error.message);
-    });
-    */
-  }
-
-  // Calls checkYaml and passes in the value from the textarea
-  handleChange = (e) => {
-    this.setState({ yaml: e });
-  }
-  handleSubmit = (e) => {
-    this.checkYaml(this.state.yaml);
-    e.preventDefault();
-  }
-
-  render() {
-    return (
-      <Holder>
-        <Editor onSubmit={this.handleSubmit}>
-          <AceEditor
-            mode='yaml'
-            theme='monokai'
-            onChange={this.handleChange}
-            name='codeEditor'
-            editorProps={{ $blockScrolling: true }}
-            width='100%'
-            defaultValue={`# If you are using certificate files, include those explicitly`}
-            style={{ 
-              borderRadius: '5px',
-            }}
-          />
-        </Editor>
-      </Holder>
-    );
-  }
-}
-
-export default YamlEditor;
-
-const Editor = styled.form`
-  margin-top: 0px;
-  margin-bottom: 12px;
-  width: calc(100% - 0px);
-  border-radius: 5px;
-  border: 1px solid #ffffff22;
-  height: 295px;
-  overflow: auto;
-`;
-
-const Holder = styled.div`
-`;

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

@@ -1,25 +1,116 @@
-import React, { Component } from 'react';
+import React, { ChangeEvent, Component } from 'react';
 import styled from 'styled-components';
-
 import logo from '../assets/logo.png';
 
+import api from '../shared/api';
+import { emailRegex } from '../shared/regex';
+import { Context } from '../shared/Context';
+
 type PropsType = {
   authenticate: () => void
 };
 
-export default class Login extends Component<PropsType> {
+type StateType = {
+  email: string,
+  password: string,
+  emailError: boolean,
+  credentialError: boolean
+};
+
+export default class Login extends Component<PropsType, StateType> {
+  state = {
+    email: '',
+    password: '',
+    emailError: false,
+    credentialError: false
+  }
+
+  handleLogin = (): void => {
+    let { email, password } = this.state;
+    let { authenticate } = this.props;
+    let { setCurrentError } = this.context;
+
+    // Check for valid input
+    if (!emailRegex.test(email)) {
+      this.setState({ emailError: true });
+    } else {
+      
+      // Attempt user login
+      api.logInUser('<token>', {
+        email: email,
+        password: password
+      }, {}, (err: any, res: any) => {
+        // TODO: case and set credential error
+
+        console.log(err)
+        err ? setCurrentError(JSON.stringify(err)) : authenticate();
+      });
+    }
+  }
+
+  renderEmailError = () => {
+    let { emailError } = this.state;
+    if (emailError) {
+      return (
+        <ErrorHelper><div />Please enter a valid email</ErrorHelper>
+      );
+    }
+  }
+
+  renderCredentialError = () => {
+    let { credentialError } = this.state;
+    if (credentialError) {
+      return (
+        <ErrorHelper><div />Incorrect email or password</ErrorHelper>
+      );
+    }
+  }
+
   render() {
+    let { email, password, credentialError, emailError } = this.state;
+
     return (
       <StyledLogin>
         <LoginPanel>
-          <GradientBg />
+          <OverflowWrapper>
+            <GradientBg />
+          </OverflowWrapper>
           <FormWrapper>
             <Logo src={logo} />
             <Line />
             <Prompt>Log in to Porter</Prompt>
-            <Input placeholder='Username' />
-            <Input type='password' placeholder='Password' />
-            <Button onClick={this.props.authenticate}>Continue</Button>
+            <InputWrapper>
+              <Input 
+                type='email' 
+                placeholder='Email'
+                value={email}
+                onChange={(e: ChangeEvent<HTMLInputElement>) => 
+                  this.setState({ 
+                    email: e.target.value,
+                    emailError: false,
+                    credentialError: false
+                  })
+                }
+                valid={!credentialError && !emailError}
+              />
+              {this.renderEmailError()}
+            </InputWrapper>
+            <InputWrapper>
+              <Input 
+                type='password' 
+                placeholder='Password'
+                value={password}
+                onChange={(e: ChangeEvent<HTMLInputElement>) => 
+                  this.setState({ 
+                    password: e.target.value, 
+                    credentialError: false 
+                  })
+                }
+                valid={!credentialError}
+              />
+              {this.renderCredentialError()}
+            </InputWrapper>
+            <Button onClick={this.handleLogin}>Continue</Button>
           </FormWrapper>
         </LoginPanel>
       </StyledLogin>
@@ -27,6 +118,46 @@ export default class Login extends Component<PropsType> {
   }
 }
 
+Login.contextType = Context;
+
+const OverflowWrapper = styled.div`
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+  border-radius: 10px;
+`;
+
+const ErrorHelper = styled.div`
+  position: absolute;
+  right: -185px;
+  top: 8px;
+  height: 30px;
+  width: 170px;
+  user-select: none;
+  background: #272731;
+  font-family: 'Work Sans', sans-serif;
+  font-size: 12px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #ff3b62;
+  border-radius: 3px;
+
+  > div {
+    background: #272731;
+    height: 15px;
+    width: 15px;
+    position: absolute;
+    left: -3px;
+    top: 7px;
+    transform: rotate(45deg);
+    z-index: -1;
+  }
+`;
+
 const Line = styled.div`
   height: 3px;
   width: 100px;
@@ -51,6 +182,10 @@ const Button = styled.button`
   font-size: 14px;
 `;
 
+const InputWrapper = styled.div`
+  position: relative;
+`;
+
 const Input = styled.input`
   width: 200px;
   font-family: 'Work Sans', sans-serif;
@@ -59,7 +194,7 @@ const Input = styled.input`
   padding: 8px;
   background: #ffffff12;
   color: #ffffff;
-  border: 0;
+  border: ${(props: { valid?: boolean }) => props.valid ? '0' : '1px solid #ff3b62'};
   border-radius: 2px;
   font-size: 14px
 `;
@@ -74,10 +209,7 @@ const Prompt = styled.div`
 const Logo = styled.img`
   width: 150px;
   margin-top: 63px;
-<<<<<<< HEAD
   user-select: none;
-=======
->>>>>>> 00bdb9028faac5bb0c17eabe2b90abe0400be933
 `;
 
 const FormWrapper = styled.div`
@@ -96,6 +228,8 @@ const GradientBg = styled.div`
   width: 180%;
   height: 180%;
   position: absolute;
+  top: -40%;
+  left: -40%;
   animation: flip 6s infinite linear;
   @keyframes flip {
     from { transform: rotate(0deg); }
@@ -109,7 +243,6 @@ const LoginPanel = styled.div`
   background: white;
   margin-top: -20px;
   border-radius: 10px;
-  overflow: hidden;
   display: flex;
   justify-content: center;
   position: relative;

+ 147 - 0
dashboard/src/main/Main.tsx

@@ -0,0 +1,147 @@
+import React, { Component } from 'react';
+import styled, { createGlobalStyle } from 'styled-components';
+import close from '../assets/close.png';
+
+import { Context } from '../shared/Context';
+
+import Login from './Login';
+import Register from './Register';
+import Home from './home/Home';
+
+type PropsType = {
+};
+
+type StateType = {
+  isLoading: boolean,
+  isLoggedIn: boolean
+  uninitialized: boolean,
+};
+
+export default class Main extends Component<PropsType, StateType> {
+  state = {
+    isLoading: false,
+    isLoggedIn: false,
+    uninitialized: true,
+  };
+
+  componentDidMount() {
+    // Check if Porter has already been initialized
+    if (false) {
+      // Check if user is logged in
+      if (false) {
+        this.setState({ isLoggedIn: true });
+      }
+    }
+  }
+
+  renderContents = (): JSX.Element => {
+    if (this.state.isLoading) {
+      return <h1>Loading...</h1>
+    } else if (this.state.isLoggedIn) {
+      return <Home logOut={() => this.setState({ isLoggedIn: false })} />
+    } else if (this.state.uninitialized) {
+      return <Register authenticate={() => this.setState({ isLoggedIn: true })} />
+    }
+    return <Login authenticate={() => this.setState({ isLoggedIn: true })} />
+  };
+
+  renderCurrentError = (): JSX.Element | undefined => {
+    if (this.context.currentError) {
+      return (
+        <CurrentError>
+          <ErrorText>Error: {this.context.currentError}</ErrorText>
+          <CloseButton onClick={() => { this.context.setCurrentError(null) }}>
+            <CloseButtonImg src={close} />
+          </CloseButton>
+        </CurrentError>
+      );
+    }
+  }
+
+  render() {
+    return (
+      <StyledMain>
+        <GlobalStyle />
+        {this.renderContents()}
+        {this.renderCurrentError()}
+      </StyledMain>
+    );
+  }
+}
+
+Main.contextType = Context;
+
+const CloseButton = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 30px;
+  height: 30px;
+  border-radius: 50%;
+  margin-left: 10px;
+  cursor: pointer;
+  :hover {
+    background-color: #ffffff11;
+  }
+`;
+
+const CloseButtonImg = styled.img`
+  width: 10px;
+`;
+
+const GlobalStyle = createGlobalStyle`
+  * {
+    box-sizing: border-box;
+  }
+`;
+
+const ErrorText = styled.div`
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  width: calc(100% - 50px);
+`;
+
+const CurrentError = styled.div`
+  position: fixed;
+  bottom: 20px;
+  width: 220px;
+  left: 17px;
+  padding: 15px;
+  padding-right: 0px;
+  font-family: 'Work Sans', sans-serif;
+  height: 50px;
+  font-size: 13px;
+  border-radius: 3px;
+  background: #383842dd;
+  border: 1px solid #ffffff55;
+  display: flex;
+  align-items: center;
+
+  > i {
+    font-size: 18px;
+    margin-right: 10px;
+  }
+
+  animation: floatIn 0.5s;
+  animation-fill-mode: forwards;
+
+  @keyframes floatIn {
+    from {
+      opacity: 0; transform: translateY(20px);
+    }
+    to {
+      opacity: 1; transform: translateY(0px);
+    }
+  }
+`;
+
+const StyledMain = styled.div`
+  height: 100vh;
+  width: 100vw;
+  position: fixed;
+  top: 0;
+  left: 0;
+  background: #24272a;
+  color: white;
+`;

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

@@ -1,31 +1,134 @@
-import React, { Component } from 'react';
+import React, { ChangeEvent, Component, useContext } from 'react';
 import styled from 'styled-components';
-
 import logo from '../assets/logo.png';
 
+import api from '../shared/api';
+import { emailRegex } from '../shared/regex';
+import { Context } from '../shared/Context';
+
 type PropsType = {
   authenticate: () => void
 };
 
-export default class Register extends Component<PropsType> {
-  handleRegister = () => {
-    // TODO: Register user
+type StateType = {
+  email: string,
+  password: string,
+  confirmPassword: string,
+  emailError: boolean,
+  confirmPasswordError: boolean
+};
+
+export default class Register extends Component<PropsType, StateType> {
+  state = {
+    email: '',
+    password: '',
+    confirmPassword: '',
+    emailError: false,
+    confirmPasswordError: false
+  }
+
+  handleRegister = (): void => {
+    let { email, password, confirmPassword } = this.state;
+    let { authenticate } = this.props;
+    let { setCurrentError } = this.context;
+
+    if (!emailRegex.test(email)) {
+      this.setState({ emailError: true });
+    }
+
+    if (confirmPassword !== password) {
+      this.setState({ confirmPasswordError: true });
+    }
+    
+    // Check for valid input
+    if (emailRegex.test(email) && confirmPassword === password) {
+
+      // Attempt user registration
+      api.registerUser('', {
+        email: email,
+        password: password
+      }, {}, (err: any, res: any) => {
+        err ? setCurrentError(JSON.stringify(err)) : authenticate();
+      });
+    } 
+  };
 
-    this.props.authenticate();
+  renderEmailError = () => {
+    let { emailError } = this.state;
+    if (emailError) {
+      return (
+        <ErrorHelper><div />Please enter a valid email</ErrorHelper>
+      );
+    }
+  }
+
+  renderConfirmPasswordError = () => {
+    let { confirmPasswordError } = this.state;
+    if (confirmPasswordError) {
+      return (
+        <ErrorHelper><div />Password does not match</ErrorHelper>
+      );
+    }
   }
 
   render() {
+    let { 
+      email,
+      password, 
+      confirmPassword, 
+      emailError,
+      confirmPasswordError
+    } = this.state;
+
     return (
       <StyledRegister>
         <LoginPanel>
-          <GradientBg />
+          <OverflowWrapper>
+            <GradientBg />
+          </OverflowWrapper>
           <FormWrapper>
             <Logo src={logo} />
             <Line />
             <Prompt>Create Account</Prompt>
-            <Input placeholder='Username' />
-            <Input type='password' placeholder='Password' />
-            <Input type='password' placeholder='Confirm Password' />
+            <InputWrapper>
+              <Input 
+                type='email'
+                placeholder='Email'
+                value={email}
+                onChange={(e: ChangeEvent<HTMLInputElement>) => 
+                  this.setState({ email: e.target.value, emailError: false })
+                }
+                valid={!emailError}
+              />
+              {this.renderEmailError()}
+            </InputWrapper>
+            <Input 
+              type='password' 
+              placeholder='Password'
+              value={password}
+              onChange={(e: ChangeEvent<HTMLInputElement>) => 
+                this.setState({ 
+                  password: e.target.value,
+                  confirmPasswordError: false
+                })
+              }
+              valid={true}
+            />
+            <InputWrapper>
+              <Input 
+                type='password' 
+                placeholder='Confirm Password'
+                value={confirmPassword}
+                onChange={(e: ChangeEvent<HTMLInputElement>) => 
+                  this.setState({ 
+                    confirmPassword: e.target.value, 
+                    confirmPasswordError: false 
+                  })
+                }
+                valid={!confirmPasswordError}
+              />
+              {this.renderConfirmPasswordError()}
+            </InputWrapper>
             <Button onClick={this.handleRegister}>Continue</Button>
           </FormWrapper>
         </LoginPanel>
@@ -34,6 +137,50 @@ export default class Register extends Component<PropsType> {
   }
 }
 
+Register.contextType = Context;
+
+const OverflowWrapper = styled.div`
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+  border-radius: 10px;
+`;
+
+const InputWrapper = styled.div`
+  position: relative;
+`;
+
+const ErrorHelper = styled.div`
+  position: absolute;
+  right: -185px;
+  top: 8px;
+  height: 30px;
+  width: 170px;
+  user-select: none;
+  background: #272731;
+  font-family: 'Work Sans', sans-serif;
+  font-size: 12px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #ff3b62;
+  border-radius: 3px;
+
+  > div {
+    background: #272731;
+    height: 15px;
+    width: 15px;
+    position: absolute;
+    left: -3px;
+    top: 7px;
+    transform: rotate(45deg);
+    z-index: -1;
+  }
+`;
+
 const Line = styled.div`
   height: 3px;
   width: 100px;
@@ -66,7 +213,7 @@ const Input = styled.input`
   padding: 8px;
   background: #ffffff12;
   color: #ffffff;
-  border: 0;
+  border: ${(props: { valid?: boolean }) => props.valid ? '0' : '1px solid #ff3b62'};
   border-radius: 2px;
   font-size: 14px
 `;
@@ -100,6 +247,8 @@ const GradientBg = styled.div`
   width: 180%;
   height: 180%;
   position: absolute;
+  top: -40%;
+  left: -40%;
   animation: flip 6s infinite linear;
   @keyframes flip {
     from { transform: rotate(0deg); }
@@ -113,7 +262,6 @@ const LoginPanel = styled.div`
   background: white;
   margin-top: -20px;
   border-radius: 10px;
-  overflow: hidden;
   display: flex;
   justify-content: center;
   position: relative;

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

@@ -2,7 +2,8 @@ import React, { Component } from 'react';
 import styled from 'styled-components';
 import ReactModal from 'react-modal';
 
-import { Context } from '../../Context';
+import { Context } from '../../shared/Context';
+
 import Sidebar from './sidebar/Sidebar';
 import ClusterConfigModal from './modals/ClusterConfigModal';
 
@@ -13,7 +14,7 @@ type PropsType = {
 type StateType = {
 };
 
-export default class Home extends Component<PropsType> {
+export default class Home extends Component<PropsType, StateType> {
   render() {
     return (
       <StyledHome>

+ 14 - 13
dashboard/src/main/home/modals/ClusterConfigModal.tsx

@@ -3,8 +3,8 @@ import styled from 'styled-components';
 import { textChangeRangeIsUnchanged } from 'typescript';
 import close from '../../../assets/close.png';
 
-import { Context } from '../../../Context';
-import YamlEditor from '../../../lib/YamlEditor';
+import { Context } from '../../../shared/Context';
+import YamlEditor from '../../../components/YamlEditor';
 
 type ClusterOption = {
   name: string,
@@ -29,25 +29,25 @@ export default class ClusterConfigModal extends Component<PropsType, StateType>
   state = {
     currentTab: 'kubeconfig',
     clusters: new Array<ClusterOption>(),
-  }
+  };
 
   componentDidMount() {
     this.setState({ clusters: dummyClusters });
   }
 
-  renderLine = (tab: string) => {
-    if (this.state.currentTab == tab) {
+  renderLine = (tab: string): JSX.Element | undefined => {
+    if (this.state.currentTab === tab) {
       return <Highlight />
     }
-  }
+  };
 
-  toggleCluster = (i: number) => {
+  toggleCluster = (i: number): void => {
     let newClusters = this.state.clusters;
     newClusters[i].selected = !this.state.clusters[i].selected;
     this.setState({ clusters: newClusters });
-  }
+  };
 
-  renderClusterList = () => {
+  renderClusterList = (): JSX.Element[] | JSX.Element => {
     if (this.state.clusters.length > 0) {
       return this.state.clusters.map((cluster: ClusterOption, i) => {
         return (
@@ -70,9 +70,9 @@ export default class ClusterConfigModal extends Component<PropsType, StateType>
         first
       </Placeholder>
     );
-  }
+  };
   
-  renderTabContents = () => {
+  renderTabContents = (): JSX.Element => {
     if (this.state.currentTab === 'kubeconfig') {
       return (
         <div>
@@ -92,7 +92,7 @@ export default class ClusterConfigModal extends Component<PropsType, StateType>
         <Button disabled={true}>Save Selected</Button>
       </div>
     )
-  }
+  };
 
   render() {
     return (
@@ -149,7 +149,8 @@ const Row = styled.div`
   display: flex;
   align-items: center;
   color: #ffffff;
-  font-size: 14px;
+  font-size: 13px;
+  font-family: 'Work Sans', sans-serif;
   cursor: pointer;
   
   :hover {

+ 29 - 8
dashboard/src/main/home/sidebar/ClusterSection.tsx

@@ -2,7 +2,9 @@ import React, { Component } from 'react';
 import styled from 'styled-components';
 import drawerBg from '../../../assets/drawer-bg.png';
 
-import { Context } from '../../../Context';
+import api from '../../../shared/api';
+import { Context } from '../../../shared/Context';
+
 import Drawer from './Drawer';
 
 type PropsType = {
@@ -11,15 +13,34 @@ type PropsType = {
 };
 
 type StateType = {
+  configExists: boolean,
+  showDrawer: boolean,
+  initializedDrawer: boolean,
+  clusters: any[]
 };
 
 export default class ClusterSection extends Component<PropsType, StateType> {
 
   // Need to track initialized for animation mounting
   state = {
-    configExists: true,
+    configExists: false,
     showDrawer: false,
     initializedDrawer: false,
+    clusters: [],
+  };
+
+  componentDidMount() {
+    let { setCurrentError } = this.context;
+
+    api.getClusters('<token>', {}, { id: 0 }, (err: any, res: any) => {      
+      if (err) {
+        setCurrentError(JSON.stringify(err));
+      } else {
+        // TODO: need a separate query for checking if config has been set
+
+        this.setState({ clusters: res.data.clusters });
+      }
+    });
   }
 
   // Need to override showDrawer when the sidebar is closed
@@ -32,14 +53,14 @@ export default class ClusterSection extends Component<PropsType, StateType> {
     }
   }
   
-  toggleDrawer = () => {
+  toggleDrawer = (): void => {
     if (!this.state.initializedDrawer) {
       this.setState({ initializedDrawer: true });
     }
     this.setState({ showDrawer: !this.state.showDrawer });
-  }
+  };
 
-  renderDrawer = () => {
+  renderDrawer = (): JSX.Element | undefined => {
     if (this.state.initializedDrawer) {
       return (
         <Drawer
@@ -48,9 +69,9 @@ export default class ClusterSection extends Component<PropsType, StateType> {
         />
       );
     }
-  }
+  };
 
-  renderContents = () => {
+  renderContents = (): JSX.Element => {
     if (this.state.configExists) {
       return (
         <ClusterSelector showDrawer={this.state.showDrawer}>
@@ -73,7 +94,7 @@ export default class ClusterSection extends Component<PropsType, StateType> {
         <Plus>+</Plus> Add a Cluster
       </InitializeButton>
     )
-  }
+  };
 
   render() {
     return (

+ 10 - 7
dashboard/src/main/home/sidebar/Drawer.tsx

@@ -2,16 +2,19 @@ import React, { Component } from 'react';
 import styled from 'styled-components';
 import close from '../../../assets/close.png';
 
-import { Context } from '../../../Context';
+import { Context } from '../../../shared/Context';
 
 type PropsType = {
   showDrawer: boolean,
   toggleDrawer: () => void
 };
 
+type StateType = {
+};
+
 type ClusterOption = {
   name: string
-}
+};
 
 const dummyClusters: ClusterOption[]  = [
   { name: 'happy-lil-trees' },
@@ -19,9 +22,9 @@ const dummyClusters: ClusterOption[]  = [
   { name: 'friendly-small-bush' }
 ];
 
-export default class Drawer extends Component<PropsType> {
+export default class Drawer extends Component<PropsType, StateType> {
 
-  renderClusterList = () => {
+  renderClusterList = (): JSX.Element[] => {
     return dummyClusters.map((cluster, i) => {
       /*
       let active = this.context.activeProject &&
@@ -35,15 +38,15 @@ export default class Drawer extends Component<PropsType> {
         </ClusterOption>
       );
     });
-  }
+  };
 
-  renderCloseOverlay = () => {
+  renderCloseOverlay = (): JSX.Element | undefined => {
     if (this.props.showDrawer) {
       return (
         <CloseOverlay onClick={this.props.toggleDrawer} />
       );
     }
-  }
+  };
 
   render() {
     return (

+ 36 - 11
dashboard/src/main/home/sidebar/Sidebar.tsx

@@ -2,13 +2,24 @@ import React, { Component } from 'react';
 import styled from 'styled-components';
 import gradient from '../../../assets/grad.jpg';
 
+import api from '../../../shared/api';
+import { Context } from '../../../shared/Context';
+
 import ClusterSection from './ClusterSection';
 
 type PropsType = {
   logOut: () => void
 };
 
-export default class Sidebar extends Component<PropsType> {
+type StateType = {
+  showSidebar: boolean,
+  initializedSidebar: boolean,
+  pressingCtrl: boolean,
+  showTooltip: boolean,
+  forceCloseDrawer: boolean
+};
+
+export default class Sidebar extends Component<PropsType, StateType> {
 
   // Need closeDrawer to hide drawer on sidebar close
   state = {
@@ -17,7 +28,7 @@ export default class Sidebar extends Component<PropsType> {
     pressingCtrl: false,
     showTooltip: false,
     forceCloseDrawer: false,
-  }
+  };
 
   componentDidMount() {
     document.addEventListener('keydown', this.handleKeyDown);
@@ -29,23 +40,23 @@ export default class Sidebar extends Component<PropsType> {
     document.removeEventListener('keyup', this.handleKeyUp);
   }
 
-  handleKeyDown = (e: any): void => {
-    if (e.keyCode === 17 || e.keyCode === 91 || e.keyCode === 93) {
+  handleKeyDown = (e: KeyboardEvent): void => {
+    if (e.key === 'Meta' || e.key === 'Control') {
       this.setState({ pressingCtrl: true });
     } else if (e.keyCode === 220 && this.state.pressingCtrl) {
       this.toggleSidebar();
     }
-  }
+  };
 
-  handleKeyUp = (e: any): void => {
-    if (e.keyCode === 17 || e.keyCode === 91 || e.keyCode === 93) {
+  handleKeyUp = (e: KeyboardEvent): void => {
+    if (e.key === 'Meta' || e.key === 'Control') {
       this.setState({ pressingCtrl: false });
     }
-  }
+  };
 
   toggleSidebar = (): void => {
     this.setState({ showSidebar: !this.state.showSidebar, forceCloseDrawer: true });
-  }
+  };
 
   renderPullTab = (): JSX.Element | undefined => {
     if (!this.state.showSidebar) {
@@ -55,7 +66,7 @@ export default class Sidebar extends Component<PropsType> {
         </PullTab>
       );
     }
-  }
+  };
 
   renderTooltip = (): JSX.Element | undefined => {
     if (this.state.showTooltip) {
@@ -63,6 +74,18 @@ export default class Sidebar extends Component<PropsType> {
         <Tooltip>⌘/CTRL + \</Tooltip>
       );
     }
+  };
+
+  handleLogout = (): void => {
+    let { logOut } = this.props;
+    let { setCurrentError } = this.context;
+
+    // Attempt user logout
+    api.logOutUser('<token>', {}, {}, (err: any, res: any) => {
+      // TODO: case and set logout error
+      
+      err ? setCurrentError(JSON.stringify(err)) : logOut();
+    });
   }
 
   // SidebarBg is separate to cover retracted drawer
@@ -94,7 +117,7 @@ export default class Sidebar extends Component<PropsType> {
             releaseDrawer={() => this.setState({ forceCloseDrawer: false })}
           />
 
-          <LogOutButton onClick={this.props.logOut}>
+          <LogOutButton onClick={this.handleLogout}>
             Log Out <i className="material-icons">keyboard_return</i>
           </LogOutButton>
         </StyledSidebar>
@@ -103,6 +126,8 @@ export default class Sidebar extends Component<PropsType> {
   }
 }
 
+Sidebar.contextType = Context;
+
 const NavButton = styled.div`
   display: block;
   position: relative;

+ 4 - 0
dashboard/src/Context.tsx → dashboard/src/shared/Context.tsx

@@ -24,6 +24,10 @@ class ContextProvider extends Component {
     currentModal: null,
     setCurrentModal: (currentModal: string): void => {
       this.setState({ currentModal });
+    },
+    currentError: null,
+    setCurrentError: (currentError: string): void => {
+      this.setState({ currentError });
     }
   };
 

+ 34 - 0
dashboard/src/shared/api.tsx

@@ -0,0 +1,34 @@
+import axios from 'axios';
+import { baseApi } from './baseApi';
+
+/**
+ * Generic api call format
+ * @param {string} token - Bearer token.
+ * @param {Object} params - Query params.
+ * @param {Object} pathParams - Path params.
+ * @param {(err: Object, res: Object) => void} callback - Callback function.
+ */
+
+const registerUser = baseApi<{ 
+  email: string, 
+  password: string
+}>('POST', '/api/register');
+
+const logInUser = baseApi<{
+  email: string,
+  password: string
+}>('POST', '/api/login');
+
+const logOutUser = baseApi<{}>('GET', '/api/logout');
+
+const getClusters = baseApi<{}, { id: number }>('GET', (pathParams) => {
+  return `/api/users/${pathParams.id}/clusters`;
+});
+
+// Bundle export to allow default api import
+export default {
+  registerUser,
+  logInUser,
+  logOutUser,
+  getClusters
+}

+ 55 - 0
dashboard/src/shared/baseApi.tsx

@@ -0,0 +1,55 @@
+import axios from 'axios';
+
+// Partial function that accepts a generic params type and returns an api method
+export const baseApi = <T extends {}, S = {}>(requestType: string, endpoint: ((pathParams: S) => string) | string) => {
+  return (token: string, params: T, pathParams: S, callback?: (err: any, res: any) => void) => {
+
+    // Generate endpoint literal
+    let endpointString: ((pathParams: S) => string) | string;
+    if (typeof endpoint === 'string') {
+      endpointString = endpoint;
+    } else {
+      endpointString = endpoint(pathParams);
+    }
+
+    // Handle request type (can refactor)
+    if (requestType === 'POST') {
+      axios.post(`https://${(process as any).env.API_SERVER + endpointString}`, params, {
+        headers: {
+          Authorization: `Bearer ${token}`
+        }
+      })
+      .then(res => {
+        callback && callback(null, res.data);
+      })
+      .catch(err => {
+        callback && callback(err, null);
+      });
+    } else if (requestType === 'PUT') {
+      axios.put(`https://${(process as any).env.API_SERVER + endpointString}`, params, {
+        headers: {
+          Authorization: `Bearer ${token}`
+        }
+      })
+      .then(res => {
+        callback && callback(null, res.data);
+      })
+      .catch(err => {
+        callback && callback(err, null);
+      });
+    } else {
+      axios.get(`https://${(process as any).env.API_SERVER + endpoint}`, {
+        headers: {
+          Authorization: `Bearer ${token}`
+        },
+        params
+      })
+      .then(res => {
+        callback && callback(null, res.data);
+      })
+      .catch(err => {
+        callback && callback(err, null);
+      });
+    }
+  }
+}

+ 1 - 0
dashboard/src/shared/regex.tsx

@@ -0,0 +1 @@
+export const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;

Некоторые файлы не были показаны из-за большого количества измененных файлов