Przeglądaj źródła

kubeconfig implementation with assumed endpoints + save error handling

jusrhee 5 lat temu
rodzic
commit
1918c56fac

BIN
dashboard/src/assets/loading.gif


+ 116 - 0
dashboard/src/components/SaveButton.tsx

@@ -0,0 +1,116 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+import loading from '../assets/loading.gif';
+
+type PropsType = {
+  text: string,
+  onClick: () => void,
+  disabled?: boolean,
+  status?: string | null
+};
+
+type StateType = {
+};
+
+export default class SaveButton extends Component<PropsType, StateType> {
+
+  renderStatus = () => {
+    if (this.props.status) {
+      if (this.props.status === 'successful') {
+        return (
+          <StatusWrapper successful={true}>
+            <i className="material-icons">done</i> Successfully updated
+          </StatusWrapper>
+        );
+      } else if (this.props.status === 'loading') {
+        return (
+          <StatusWrapper successful={false}>
+            <LoadingGif src={loading} /> Updating . . .
+          </StatusWrapper>
+        );
+      }
+
+      return (
+        <StatusWrapper successful={false}>
+          <i className="material-icons">error_outline</i> Could not update
+        </StatusWrapper>
+      );
+    }
+  }
+
+  render() {
+    return (
+      <ButtonWrapper>
+        {this.renderStatus()}
+        <Button 
+          disabled={this.props.disabled}
+          onClick={this.props.onClick}
+        >
+          {this.props.text}
+        </Button>
+      </ButtonWrapper>
+    );
+  }
+}
+
+const LoadingGif = styled.img`
+  width: 15px;
+  height: 15px;
+  margin-right: 9px;
+  margin-bottom: 0px;
+`;
+
+const StatusWrapper = styled.div`
+  display: flex;
+  align-items: center;
+  font-family: 'Work Sans', sans-serif;
+  font-size: 13px;
+  color: #ffffff55;
+  margin-right: 25px;
+
+  > i {
+    font-size: 18px;
+    margin-right: 5px;
+    color: ${(props: { successful: boolean }) => props.successful ? '#4797ff' : '#fcba03'};
+  }
+
+  animation: statusFloatIn 0.5s;
+  animation-fill-mode: forwards;
+
+  @keyframes statusFloatIn {
+    from {
+      opacity: 0; transform: translateY(10px);
+    }
+    to {
+      opacity: 1; transform: translateY(0px);
+    }
+  }
+`;
+
+const ButtonWrapper = styled.div`
+  display: flex;
+  align-items: center;
+  position: absolute;
+  bottom: 25px;
+  right: 27px;
+`;
+
+const Button = styled.button`
+  height: 40px;
+  font-size: 13px;
+  font-weight: 500;
+  font-family: 'Work Sans', sans-serif;
+  color: white;
+  padding: 6px 20px 7px 20px;
+  text-align: left;
+  border: 0;
+  border-radius: 5px;
+  background: ${(props) => (!props.disabled ? '#616FEEcc' : '#bbd')};
+  box-shadow: ${(props) => (!props.disabled ? '0 2px 5px 0 #00000030' : 'none')};
+  cursor: ${(props) => (!props.disabled ? 'pointer' : 'default')};
+  user-select: none;
+  :focus { outline: 0 }
+  :hover {
+    background: ${(props) => (!props.disabled ? '#616FEEff' : '#bbd')};
+  }
+`;

+ 10 - 13
dashboard/src/components/YamlEditor.tsx

@@ -6,21 +6,18 @@ import 'ace-builds/src-noconflict/mode-yaml';
 import 'ace-builds/src-noconflict/theme-monokai';
 
 type PropsType = {
+  value: string,
+  onChange: (e: any) => void,
 }
 
-class YamlEditor extends Component {
-  constructor(props: PropsType) {
-    super(props);
-    this.state = {
-      yaml: ``,
-    }
-    this.handleChange = this.handleChange.bind(this);
-    this.handleSubmit = this.handleSubmit.bind(this);
-  }
+type StateType = {
+}
+
+class YamlEditor extends Component<PropsType, StateType> {
 
   // 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: string) => {
+  checkYaml = () => {
     /*
     yamlLint.lint(y).then(() => {
       alert('Valid YAML file.');
@@ -36,7 +33,7 @@ class YamlEditor extends Component {
   }
 
   handleSubmit = (e: any) => {
-    this.checkYaml('dummyText');
+    this.checkYaml();
     e.preventDefault();
   }
 
@@ -46,12 +43,12 @@ class YamlEditor extends Component {
         <Editor onSubmit={this.handleSubmit}>
           <AceEditor
             mode='yaml'
+            value={this.props.value}
             theme='monokai'
-            onChange={this.handleChange}
+            onChange={this.props.onChange}
             name='codeEditor'
             editorProps={{ $blockScrolling: true }}
             width='100%'
-            defaultValue={`# If you are using certificate files, include those explicitly`}
             style={{ borderRadius: '5px' }}
           />
         </Editor>

+ 2 - 2
dashboard/src/main/Main.tsx

@@ -20,7 +20,7 @@ type StateType = {
 export default class Main extends Component<PropsType, StateType> {
   state = {
     isLoading: false,
-    isLoggedIn: false,
+    isLoggedIn: true,
     uninitialized: true,
   };
 
@@ -105,7 +105,7 @@ const ErrorText = styled.div`
 const CurrentError = styled.div`
   position: fixed;
   bottom: 20px;
-  width: 220px;
+  width: 300px;
   left: 17px;
   padding: 15px;
   padding-right: 0px;

+ 1 - 1
dashboard/src/main/Register.tsx

@@ -66,7 +66,7 @@ export default class Register extends Component<PropsType, StateType> {
     let { confirmPasswordError } = this.state;
     if (confirmPasswordError) {
       return (
-        <ErrorHelper><div />Password does not match</ErrorHelper>
+        <ErrorHelper><div />Passwords do not match</ErrorHelper>
       );
     }
   }

+ 82 - 45
dashboard/src/main/home/modals/ClusterConfigModal.tsx

@@ -1,20 +1,33 @@
 import React, { Component } from 'react';
 import styled from 'styled-components';
-import { textChangeRangeIsUnchanged } from 'typescript';
 import close from '../../../assets/close.png';
 
+import api from '../../../shared/api';
 import { Context } from '../../../shared/Context';
-import YamlEditor from '../../../components/YamlEditor';
-
-type ClusterOption = {
-  name: string,
-  selected: boolean,
-}
+import { ClusterConfig } from '../../../shared/types';
 
-const dummyClusters: ClusterOption[]  = [
-  { name: 'happy-lil-trees', selected: true },
-  { name: 'joyous-petite-rocks', selected: false },
-  { name: 'friendly-small-bush', selected: false }
+import YamlEditor from '../../../components/YamlEditor';
+import SaveButton from '../../../components/SaveButton';
+
+const dummyClusters: ClusterConfig[]  = [
+  { 
+    name: 'happy-lil-trees', 
+    server: 'idc',
+    context: 'idk',
+    user: 'jusrhee'
+  },
+  { 
+    name: 'joyous-petite-rocks', 
+    server: 'idc',
+    context: 'idk',
+    user: 'jusrhee'
+  },
+  { 
+    name: 'friendly-small-bush', 
+    server: 'idc',
+    context: 'idk',
+    user: 'jusrhee'
+  }
 ];
 
 type PropsType = {
@@ -22,17 +35,31 @@ type PropsType = {
 
 type StateType = {
   currentTab: string,
-  clusters: ClusterOption[]
+  clusters: ClusterConfig[],
+  selected: boolean[],
+  rawKubeconfig: string,
+  saveKubeconfigStatus: string | null,
 };
 
 export default class ClusterConfigModal extends Component<PropsType, StateType> {
   state = {
     currentTab: 'kubeconfig',
-    clusters: new Array<ClusterOption>(),
+    clusters: [] as ClusterConfig[],
+    selected: [] as boolean[],
+    rawKubeconfig: '# If you are using certificate files, include those explicitly',
+    saveKubeconfigStatus: null,
   };
 
   componentDidMount() {
-    this.setState({ clusters: dummyClusters });
+    let { setCurrentError } = this.context;
+
+    api.getUser('<token>', {}, { id: 0 }, (err: any, res: any) => {      
+      if (err) {
+        setCurrentError(JSON.stringify(err));
+      } else {
+        this.setState({ rawKubeconfig: res.data.rawKubeConfig });
+      }
+    });
   }
 
   renderLine = (tab: string): JSX.Element | undefined => {
@@ -42,17 +69,17 @@ export default class ClusterConfigModal extends Component<PropsType, StateType>
   };
 
   toggleCluster = (i: number): void => {
-    let newClusters = this.state.clusters;
-    newClusters[i].selected = !this.state.clusters[i].selected;
-    this.setState({ clusters: newClusters });
+    let newSelected = this.state.selected;
+    newSelected[i] = !this.state.selected[i];
+    this.setState({ selected: newSelected });
   };
 
   renderClusterList = (): JSX.Element[] | JSX.Element => {
     if (this.state.clusters.length > 0) {
-      return this.state.clusters.map((cluster: ClusterOption, i) => {
+      return this.state.clusters.map((cluster: ClusterConfig, i) => {
         return (
           <Row key={i} onClick={() => this.toggleCluster(i)}>
-            <Checkbox checked={cluster.selected}>
+            <Checkbox checked={this.state.selected[i]}>
               <i className="material-icons">done</i>
             </Checkbox>
             {cluster.name}
@@ -71,14 +98,43 @@ export default class ClusterConfigModal extends Component<PropsType, StateType>
       </Placeholder>
     );
   };
+
+  handleSaveKubeconfig = () => {
+    let { rawKubeconfig } = this.state;
+    let { setCurrentError } = this.context;
+
+    this.setState({ saveKubeconfigStatus: 'loading' });
+    api.updateRawKubeconfig(
+      '<token>',
+      { rawKubeconfig },
+      { id: 0 },
+      (err: any, res: any) => {
+        if (err) {
+          this.setState({ saveKubeconfigStatus: 'error' });
+        } else {
+          this.setState({ 
+            rawKubeconfig: res.data.rawKubeConfig,
+            saveKubeconfigStatus: 'successful'
+          });
+        }
+      }
+    );
+  }
   
   renderTabContents = (): JSX.Element => {
     if (this.state.currentTab === 'kubeconfig') {
       return (
         <div>
           <Subtitle>Copy and paste your kubeconfig below</Subtitle>
-          <YamlEditor />
-          <Button>Save Kubeconfig</Button>
+          <YamlEditor 
+            value={this.state.rawKubeconfig}
+            onChange={(e: any) => this.setState({ rawKubeconfig: e })}
+          />
+          <SaveButton
+            text='Save Kubeconfig'
+            onClick={this.handleSaveKubeconfig}
+            status={this.state.saveKubeconfigStatus}
+          />
         </div>
       )
     }
@@ -89,7 +145,11 @@ export default class ClusterConfigModal extends Component<PropsType, StateType>
         <ClusterList>
           {this.renderClusterList()}
         </ClusterList>
-        <Button disabled={true}>Save Selected</Button>
+        <SaveButton
+          text='Save Selected'
+          disabled={true}
+          onClick={() => alert('unimplemented')}
+        />
       </div>
     )
   };
@@ -187,29 +247,6 @@ const ClusterList = styled.div`
   border: 1px solid #ffffff22;
 `;
 
-const Button = styled.button`
-  position: absolute;
-  bottom: 25px;
-  right: 27px;
-  height: 40px;
-  font-size: 13px;
-  font-weight: 500;
-  font-family: 'Work Sans', sans-serif;
-  color: white;
-  padding: 6px 20px 7px 20px;
-  text-align: left;
-  border: 0;
-  border-radius: 5px;
-  background: ${(props) => (!props.disabled ? '#616FEEcc' : '#bbd')};
-  box-shadow: ${(props) => (!props.disabled ? '0 2px 5px 0 #00000030' : 'none')};
-  cursor: ${(props) => (!props.disabled ? 'pointer' : 'default')};
-  user-select: none;
-  :focus { outline: 0 }
-  :hover {
-    background: ${(props) => (!props.disabled ? '#616FEEff' : '#bbd')};
-  }
-`;
-
 const Subtitle = styled.div`
   padding: 15px 0px;
   font-family: 'Work Sans', sans-serif;

+ 39 - 9
dashboard/src/main/home/sidebar/ClusterSection.tsx

@@ -4,6 +4,7 @@ import drawerBg from '../../../assets/drawer-bg.png';
 
 import api from '../../../shared/api';
 import { Context } from '../../../shared/Context';
+import { ClusterConfig } from '../../../shared/types';
 
 import Drawer from './Drawer';
 
@@ -16,28 +17,52 @@ type StateType = {
   configExists: boolean,
   showDrawer: boolean,
   initializedDrawer: boolean,
-  clusters: any[]
+  clusters: any[],
+  activeIndex: number,
 };
 
+const dummyClusters: ClusterConfig[]  = [
+  { 
+    name: 'happy-ol-trees', 
+    server: 'idc',
+    context: 'idk',
+    user: 'jusrhee'
+  },
+  { 
+    name: 'joyous-petite-rocks', 
+    server: 'idc',
+    context: 'idk',
+    user: 'jusrhee'
+  },
+  { 
+    name: 'friendly-small-bush', 
+    server: 'idc',
+    context: 'idk',
+    user: 'jusrhee'
+  }
+];
+
 export default class ClusterSection extends Component<PropsType, StateType> {
 
   // Need to track initialized for animation mounting
   state = {
-    configExists: false,
+    configExists: true,
     showDrawer: false,
     initializedDrawer: false,
-    clusters: [],
+    clusters: [] as ClusterConfig[],
+    activeIndex: 0,
   };
 
   componentDidMount() {
+    // TODO: remove
+    // this.setState({ clusters: dummyClusters });
+
     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 });
       }
     });
@@ -66,22 +91,27 @@ export default class ClusterSection extends Component<PropsType, StateType> {
         <Drawer
           toggleDrawer={this.toggleDrawer}
           showDrawer={this.state.showDrawer}
+          clusters={this.state.clusters}
+          activeIndex={this.state.activeIndex}
+          setActiveIndex={(i: number): void => this.setState({ activeIndex: i })}
         />
       );
     }
   };
 
   renderContents = (): JSX.Element => {
-    if (this.state.configExists) {
+    let { clusters, activeIndex, showDrawer } = this.state;
+
+    if (clusters.length > 0) {
       return (
-        <ClusterSelector showDrawer={this.state.showDrawer}>
+        <ClusterSelector showDrawer={showDrawer}>
           <LinkWrapper>
             <ClusterIcon><i className="material-icons">polymer</i></ClusterIcon>
-            <ClusterName>happy-lil-trees</ClusterName>
+            <ClusterName>{clusters[activeIndex].name}</ClusterName>
           </LinkWrapper>
           <DrawerButton onClick={this.toggleDrawer}>
             <BgAccent src={drawerBg} />
-            <DropdownIcon showDrawer={this.state.showDrawer}>
+            <DropdownIcon showDrawer={showDrawer}>
               <i className="material-icons">arrow_drop_down</i>
             </DropdownIcon>
           </DrawerButton>

+ 15 - 16
dashboard/src/main/home/sidebar/Drawer.tsx

@@ -3,36 +3,34 @@ import styled from 'styled-components';
 import close from '../../../assets/close.png';
 
 import { Context } from '../../../shared/Context';
+import { ClusterConfig } from '../../../shared/types';
 
 type PropsType = {
+  toggleDrawer: () => void,
   showDrawer: boolean,
-  toggleDrawer: () => void
+  clusters: ClusterConfig[],
+  activeIndex: number,
+  setActiveIndex: (i: number) => void
 };
 
 type StateType = {
 };
 
-type ClusterOption = {
-  name: string
-};
-
-const dummyClusters: ClusterOption[]  = [
-  { name: 'happy-lil-trees' },
-  { name: 'joyous-petite-rocks' },
-  { name: 'friendly-small-bush' }
-];
-
 export default class Drawer extends Component<PropsType, StateType> {
 
   renderClusterList = (): JSX.Element[] => {
-    return dummyClusters.map((cluster, i) => {
+    return this.props.clusters.map((cluster, i) => {
       /*
       let active = this.context.activeProject &&
         this.context.activeProject.namespace == val.namespace; 
       */
 
       return (
-        <ClusterOption key={i}>
+        <ClusterOption 
+          key={i}
+          active={i === this.props.activeIndex}
+          onClick={() => this.props.setActiveIndex(i)}
+        >
           <ClusterIcon><i className="material-icons">polymer</i></ClusterIcon>
           <ClusterName>{cluster.name}</ClusterName>
         </ClusterOption>
@@ -97,10 +95,10 @@ const InitializeButton = styled.div`
   color: #ffffff;
   padding-bottom: 3px;
   cursor: pointer;
-  background: #ffffff11;
+  background: #ffffff22;
 
   :hover {
-    background: #ffffff22;
+    background: #ffffff33;
   }
 `;
 
@@ -118,8 +116,9 @@ const ClusterOption = styled.div`
   overflow: hidden;
   text-overflow: ellipsis;
   cursor: pointer;
+  background: ${(props: { active: boolean }) => props.active ? '#ffffff22' : ''};
   :hover {
-    background: #ffffff18;
+    background: #ffffff33;
   }
 `;
 

+ 5 - 0
dashboard/src/shared/Context.tsx

@@ -1,4 +1,5 @@
 import React, { Component } from 'react';
+import { isNullishCoalesce } from 'typescript';
 
 type ContextProps = {
 }
@@ -28,6 +29,10 @@ class ContextProvider extends Component {
     currentError: null,
     setCurrentError: (currentError: string): void => {
       this.setState({ currentError });
+    },
+    currentCluster: null,
+    setCurrentCluster: (currentCluster: string): void => {
+      this.setState({ currentCluster });
     }
   };
 

+ 13 - 1
dashboard/src/shared/api.tsx

@@ -21,7 +21,17 @@ const logInUser = baseApi<{
 
 const logOutUser = baseApi<{}>('GET', '/api/logout');
 
-const getClusters = baseApi<{}, { id: number }>('GET', (pathParams) => {
+const getUser = baseApi<{}, { id: number }>('GET', pathParams => {
+  return `/api/users/${pathParams.id}`;
+});
+
+const updateRawKubeconfig = baseApi<{
+  rawKubeconfig: string
+}, { id: number }>('PUT', pathParams => {
+  return `/api/users/${pathParams.id}`;
+});
+
+const getClusters = baseApi<{}, { id: number }>('GET', pathParams => {
   return `/api/users/${pathParams.id}/clusters`;
 });
 
@@ -30,5 +40,7 @@ export default {
   registerUser,
   logInUser,
   logOutUser,
+  getUser,
+  updateRawKubeconfig,
   getClusters
 }

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

@@ -0,0 +1,6 @@
+export interface ClusterConfig {
+  name: string,
+  server: string,
+  context: string,
+  user: string
+}