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

Merge pull request #49 from porter-dev/frontend-integration

top-level chart info and loading buffer for all api calls
abelanger5 5 лет назад
Родитель
Сommit
3f4412f9d3

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

@@ -22,7 +22,7 @@ export default class Loading extends Component<PropsType, StateType> {
 }
 
 const Spinner = styled.img`
-  width: 25px;
+  width: 20px;
 `;
 
 const StyledLoading = styled.div`

+ 2 - 2
dashboard/src/components/YamlEditor.tsx

@@ -3,7 +3,7 @@ import styled from 'styled-components';
 import AceEditor from 'react-ace';
 
 import 'ace-builds/src-noconflict/mode-yaml';
-import 'ace-builds/src-noconflict/theme-monokai';
+import 'ace-builds/src-noconflict/theme-terminal';
 
 type PropsType = {
   value: string,
@@ -44,7 +44,7 @@ class YamlEditor extends Component<PropsType, StateType> {
           <AceEditor
             mode='yaml'
             value={this.props.value}
-            theme='monokai'
+            theme='terminal'
             onChange={this.props.onChange}
             name='codeEditor'
             editorProps={{ $blockScrolling: true }}

+ 53 - 41
dashboard/src/main/Main.tsx

@@ -1,5 +1,6 @@
 import React, { Component } from 'react';
 import styled, { createGlobalStyle } from 'styled-components';
+import { BrowserRouter, Route, Redirect, Switch } from 'react-router-dom';
 import close from '../assets/close.png';
 
 import api from '../shared/api';
@@ -9,13 +10,13 @@ import Login from './Login';
 import Register from './Register';
 import CurrentError from './CurrentError';
 import Home from './home/Home';
-import { BrowserRouter, Route, Redirect, Switch } from 'react-router-dom';
+import Loading from '../components/Loading';
 
 type PropsType = {
 };
 
 type StateType = {
-  isLoading: boolean,
+  loading: boolean,
   isLoggedIn: boolean,
   initialized: boolean,
 };
@@ -23,7 +24,7 @@ type StateType = {
 export default class Main extends Component<PropsType, StateType> {
   
   state = {
-    isLoading: false,
+    loading: true,
     isLoggedIn : false,
     initialized: (localStorage.getItem("init") == 'true')
   }
@@ -32,10 +33,10 @@ export default class Main extends Component<PropsType, StateType> {
     let { setUserId } = this.context;
     api.checkAuth('', {}, {}, (err: any, res: any) => {
       if (res.data) {
-        setUserId(res.data.id)
-        this.setState({ isLoggedIn: true, initialized: true})
+        setUserId(res.data.id);
+        this.setState({ isLoggedIn: true, initialized: true, loading: false });
       } else {
-        this.setState({ isLoggedIn: false })
+        this.setState({ isLoggedIn: false, loading: false })
       }
     });
   }
@@ -49,46 +50,56 @@ export default class Main extends Component<PropsType, StateType> {
     this.setState({ isLoggedIn: true, initialized: true });
   }
 
+  renderMain = () => {
+    if (this.state.loading) {
+      return <Loading />
+    }
+
+    return (
+      <Switch>
+        <Route path='/login' render={() => {
+          if (!this.state.isLoggedIn) {
+            return <Login authenticate={this.authenticate} />
+          } else {
+            return <Redirect to='/' />
+          }
+        }} />
+
+        <Route path='/register' render={() => {
+          if (!this.state.isLoggedIn) {
+            return <Register authenticate={this.initialize} />
+          } else {
+            return <Redirect to='/' />
+          }
+        }} />
+
+        <Route path='/dashboard' render={() => {
+          if (this.state.isLoggedIn && this.state.initialized) {
+            return <Home logOut={() => this.setState({ isLoggedIn: false, initialized: true })} />
+          } else {
+            return <Redirect to='/' />
+          }
+        }}/>
+
+        <Route path='/' render={() => {
+          if (this.state.isLoggedIn) {
+            return <Redirect to='/dashboard'/>
+          } else if (this.state.initialized) {
+            return <Redirect to='/login'/>
+          } else {
+            return <Redirect to='/register' />
+          }
+        }}/>
+      </Switch>
+    );
+  }
+
   render() {
     return (
       <StyledMain>
         <GlobalStyle />
         <BrowserRouter>
-          <Switch>
-            <Route path='/login' render={() => {
-              if (!this.state.isLoggedIn) {
-                return <Login authenticate={this.authenticate} />
-              } else {
-                return <Redirect to='/' />
-              }
-            }} />
-
-            <Route path='/register' render={() => {
-              if (!this.state.isLoggedIn) {
-                return <Register authenticate={this.initialize} />
-              } else {
-                return <Redirect to='/' />
-              }
-            }} />
-
-            <Route path='/dashboard' render={() => {
-              if (this.state.isLoggedIn && this.state.initialized) {
-                return <Home logOut={() => this.setState({ isLoggedIn: false, initialized: true })} />
-              } else {
-                return <Redirect to='/' />
-              }
-            }}/>
-
-            <Route path='/' render={() => {
-              if (this.state.isLoggedIn) {
-                return <Redirect to='/dashboard'/>
-              } else if (this.state.initialized) {
-                return <Redirect to='/login'/>
-              } else {
-                return <Redirect to='/register' />
-              }
-            }}/>
-          </Switch>
+          {this.renderMain()}
         </BrowserRouter>
         <CurrentError />
       </StyledMain>
@@ -101,6 +112,7 @@ Main.contextType = Context;
 const GlobalStyle = createGlobalStyle`
   * {
     box-sizing: border-box;
+    font-family: 'Work Sans', sans-serif;
   }
 `;
 

+ 22 - 17
dashboard/src/main/home/Home.tsx

@@ -7,6 +7,7 @@ import { Context } from '../../shared/Context';
 import Sidebar from './sidebar/Sidebar';
 import Dashboard from './dashboard/Dashboard';
 import ClusterConfigModal from './modals/ClusterConfigModal';
+import Loading from '../../components/Loading';
 
 type PropsType = {
   logOut: () => void
@@ -18,25 +19,29 @@ type StateType = {
 export default class Home extends Component<PropsType, StateType> {
 
   renderDashboard = () => {
-    if (this.context.currentCluster) {
-      return <DashboardWrapper><Dashboard /></DashboardWrapper>
+    let { currentCluster, setCurrentModal, setCurrentModalData } = this.context;
+
+    if (currentCluster === '') {
+      return (
+        <DashboardWrapper>
+          <Placeholder>
+            <Bold>Porter - Getting Started</Bold><br /><br />
+            1. Navigate to <A onClick={() => setCurrentModal('ClusterConfigModal')}>+ Add a Cluster</A> and provide a kubeconfig. *<br /><br />
+            2. Choose which contexts you would like to use from the <A onClick={() => {
+              setCurrentModal('ClusterConfigModal');
+              setCurrentModalData({ currentTab: 'select' });
+            }}>Select Clusters</A> tab.<br /><br />
+            3. For additional information, please refer to our <A>docs</A>.<br /><br /><br />
+            
+            * Make sure all fields are explicitly declared (e.g., certs and keys).
+          </Placeholder>
+        </DashboardWrapper>
+      );
+    } else if (!currentCluster) {
+      return <Loading />
     }
 
-    return (
-      <DashboardWrapper>
-        <Placeholder>
-          <Bold>Porter - Getting Started</Bold><br /><br />
-          1. Navigate to <A onClick={() => {this.context.setCurrentModal('ClusterConfigModal')}}>+ Add a Cluster</A> and provide a kubeconfig. *<br /><br />
-          2. Choose which contexts you would like to use from the <A onClick={() => {
-            this.context.setCurrentModal('ClusterConfigModal');
-            this.context.setCurrentModalData({ currentTab: 'select' });
-          }}>Select Clusters</A> tab.<br /><br />
-          3. For additional information, please refer to our <A>docs</A>.<br /><br /><br />
-          
-          * Make sure all fields are explicitly declared (e.g., certs and keys).
-        </Placeholder>
-      </DashboardWrapper>
-    );
+    return <DashboardWrapper><Dashboard /></DashboardWrapper>
   }
 
   render() {

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

@@ -3,53 +3,16 @@ import styled from 'styled-components';
 import gradient from '../../../assets/gradient.jpg';
 
 import { Context } from '../../../shared/Context';
-import api from '../../../shared/api';
-import { ChartType } from '../../../shared/types';
 
-import Chart from './chart/Chart';
+import ChartList from './chart/ChartList';
 
 type PropsType = {
 };
 
 type StateType = {
-  charts: ChartType[]
 };
 
 export default class Dashboard extends Component<PropsType, StateType> {
-  state = {
-    charts: [] as ChartType[]
-  }
-
-  componentDidMount() {
-    let { userId, setCurrentError, currentCluster } = this.context;
-    
-    api.getCharts('<token>', {
-      namespace: '',
-      context: currentCluster,
-      storage: 'secret',
-      limit: 20,
-      skip: 0,
-      byDate: false,
-      statusFilter: ['deployed']
-    }, {}, (err: any, res: any) => {
-        if (err) {
-        setCurrentError(JSON.stringify(err));
-      } else {
-        if (res.data) {
-          this.setState({ charts: res.data });
-        }
-      }
-    });
-  }
-
-  renderChartList = () => {
-    return this.state.charts.map((x: ChartType, i: number) => {
-      return (
-        <Chart key={i} chart={x} />
-      )
-    })
-  }
-
   render() {
     let { currentCluster } = this.context;
 
@@ -75,7 +38,7 @@ export default class Dashboard extends Component<PropsType, StateType> {
 
         <LineBreak />
 
-        {this.renderChartList()}
+        <ChartList currentCluster={currentCluster} />
       </div>
     );
   }

+ 152 - 42
dashboard/src/main/home/dashboard/chart/Chart.tsx

@@ -12,37 +12,151 @@ type StateType = {
 
 export default class Chart extends Component<PropsType, StateType> {
   state = {
-    grow: false,
+    expand: false,
+  }
+
+  renderIcon = () => {
+    let { chart } = this.props;
+
+    if (chart.chart.metadata.icon && chart.chart.metadata.icon !== '') {
+      return <Icon src={chart.chart.metadata.icon} />
+    } else {
+      return <i className="material-icons">tonality</i>
+    }
+  }
+
+  readableDate = (s: string) => {
+    let ts = new Date(s);
+    let date = ts.toLocaleDateString();
+    let time = ts.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
+    return `${time} on ${date}`;
   }
 
   render() {
     let { chart } = this.props;
     return ( 
       <StyledChart
-        onMouseEnter={() => this.setState({ grow: true })}
-        onMouseLeave={() => this.setState({ grow: false })}
-        grow={this.state.grow}
+        onMouseEnter={() => this.setState({ expand: true })}
+        onMouseLeave={() => this.setState({ expand: false })}
+        expand={this.state.expand}
       >
         <Title>
-          <i className="material-icons">polymer</i>
+          <IconWrapper>
+            {this.renderIcon()}
+          </IconWrapper>
           {chart.name}
         </Title>
-        <StatusIndicator>
-          <StatusColor status={'Running'} />
-          {chart.info.status}
-        </StatusIndicator>
+
+        <InfoWrapper>
+          <StatusIndicator>
+            <StatusColor status={chart.info.status} />
+            {chart.info.status}
+          </StatusIndicator>
+
+          <LastDeployed>
+            <Dot>•</Dot> Last deployed {this.readableDate(chart.info.last_deployed)}
+          </LastDeployed>
+        </InfoWrapper>
+
+        <Version>v{chart.version}</Version>
+
+        <TagWrapper>
+          Namespace
+          <NamespaceTag>
+            {chart.namespace}
+          </NamespaceTag>
+        </TagWrapper>
       </StyledChart>
     );
   }
 }
 
+const Version = styled.div`
+  position: absolute;
+  top: 12px;
+  right: 12px;
+  font-size: 12px;
+  color: #aaaabb;
+`;
+
+const Dot = styled.div`
+  margin-right: 9px;
+`;
+
+const InfoWrapper = styled.div`
+  display: flex;
+  align-items: center;
+  margin-top: 10px;
+`;
+
+const LastDeployed = styled.div`
+  font-size: 13px;
+  margin-left: 10px;
+  margin-top: -1px;
+  display: flex;
+  align-items: center;
+  color: #aaaabb66;
+`;
+
+const TagWrapper = styled.div`
+  position: absolute;
+  bottom: 12px;
+  right: 12px;
+  height: 20px;
+  font-size: 12px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #ffffff44;
+  border: 1px solid #ffffff44;
+  border-radius: 3px;
+  padding-left: 5px;
+`;
+
+const NamespaceTag = styled.div`
+  height: 20px;
+  margin-left: 6px;
+  color: #aaaabb;
+  background: #ffffff22;
+  border-radius: 3px;
+  font-size: 12px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 0px 6px;
+  padding-left: 7px;
+  border-top-left-radius: 0px;
+  border-bottom-left-radius: 0px;
+`;
+
+const Icon = styled.img`
+  width: 100%;
+`;
+
+const IconWrapper = styled.div`
+  color: #efefef;
+  background: none;
+  font-size: 16px;
+  top: 11px;
+  left: 14px;
+  height: 20px;
+  width: 20px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  border-radius: 3px;
+  position: absolute;
+
+  > i {
+    font-size: 17px;
+    margin-top: -1px;
+  }
+`;
+
 const StatusIndicator = styled.div`
   display: flex;
-  flex: 1;
-  width: 90px;
   height: 20px;
   font-size: 13px;
-  margin-top: 10px;
   flex-direction: row;
   text-transform: capitalize;
   align-items: center;
@@ -61,15 +175,15 @@ const StatusColor = styled.div`
   margin-bottom: 1px;
   width: 8px;
   height: 8px;
-  background: ${(props: { status: string }) => (props.status == 'Running' ? '#4797ff' : props.status == 'Stopped' ? "#ed5f85" : "#f5cb42")};
+  background: ${(props: { status: string }) => (props.status === 'deployed' ? '#4797ff' : props.status === 'failed' ? "#ed5f85" : "#f5cb42")};
   border-radius: 20px;
-  margin-right: 10px;
+  margin-right: 16px;
 `;
 
 const Title = styled.div`
   position: relative;
   text-decoration: none;
-  padding: 16px 35px 12px 43px;
+  padding: 12px 35px 12px 45px;
   font-size: 14px;
   font-family: 'Work Sans', sans-serif;
   font-weight: 500;
@@ -80,20 +194,6 @@ const Title = styled.div`
   text-overflow: ellipsis;
   animation: fadeIn 0.5s;
 
-  > i {
-    color: #efefef;
-    background: none;
-    font-size: 16px;
-    top: 11px;
-    left: 14px;
-
-    padding: 5px 4px;
-    height: 20px;
-    width: 20px;
-    border-radius: 3px;
-    position: absolute;
-  }
-
   >img {
     background: none;
     top: 12px;
@@ -110,8 +210,6 @@ const StyledChart = styled.div`
   cursor: pointer;
   margin-bottom: 25px;
   padding: 1px;
-  padding-top: 3px;
-  padding-bottom: 18px;
   border-radius: 5px;
   box-shadow: 0 5px 8px 0px #00000033;
   position: relative;
@@ -119,41 +217,53 @@ const StyledChart = styled.div`
   width: calc(100% + 2px);
   height: calc(100% + 2px);
 
-  animation: ${(props: { grow: boolean }) => props.grow ? 'grow' : 'shrink'} 0.12s;
+  animation: ${(props: { expand: boolean }) => props.expand ? 'expand' : 'shrink'} 0.12s;
   animation-fill-mode: forwards;
-  animation-timing-function: ease-out;
+  animation-timing-function: ease;
 
-  @keyframes grow {
+  @keyframes expand {
     from { 
       width: calc(100% + 2px); 
-      padding-top: 3px;
-      padding-bottom: 18px;
+      padding-top: 4px;
+      padding-bottom: 15px;
       margin-left: 0px;
       box-shadow: 0 5px 8px 0px #00000033;
+      padding-left: 1px;
+      margin-bottom: 25px;
+      margin-top: 0px;
     }
     to {
       width: calc(100% + 22px);
-      padding-top: 5px;
-      padding-bottom: 22px;
+      padding-top: 7px;
+      padding-bottom: 20px;
       margin-left: -10px; 
       box-shadow: 0 8px 20px 0px #00000030;
+      padding-left: 5px;
+      margin-bottom: 21px;
+      margin-top: -4px;
     }
   }
 
   @keyframes shrink {
     from { 
       width: calc(100% + 22px);
-      padding-top: 5px;
-      padding-bottom: 22px;
+      padding-top: 7px;
+      padding-bottom: 20px;
       margin-left: -10px; 
       box-shadow: 0 8px 20px 0px #00000030;
+      padding-left: 5px;
+      margin-bottom: 21px;
+      margin-top: -4px;
     }
     to {
       width: calc(100% + 2px); 
-      padding-top: 3px;
-      padding-bottom: 18px;
+      padding-top: 4px;
+      padding-bottom: 15px;
       margin-left: 0px; 
       box-shadow: 0 5px 8px 0px #00000033;
+      padding-left: 1px;
+      margin-bottom: 25px;
+      margin-top: 0px;
     }
   }
 `;

+ 91 - 0
dashboard/src/main/home/dashboard/chart/ChartList.tsx

@@ -0,0 +1,91 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+
+import { Context } from '../../../../shared/Context';
+import api from '../../../../shared/api';
+import { ChartType } from '../../../../shared/types';
+
+import Chart from './Chart';
+import Loading from '../../../../components/Loading';
+
+type PropsType = {
+  currentCluster: string
+};
+
+type StateType = {
+  charts: ChartType[],
+  loading: boolean
+};
+
+export default class ChartList extends Component<PropsType, StateType> {
+  state = {
+    charts: [] as ChartType[],
+    loading: false,
+  }
+
+  updateCharts = () => {
+    let { setCurrentError, currentCluster } = this.context;
+    
+    this.setState({ loading: true });
+    api.getCharts('<token>', {
+      namespace: '',
+      context: currentCluster,
+      storage: 'secret',
+      limit: 20,
+      skip: 0,
+      byDate: false,
+      statusFilter: ['deployed']
+    }, {}, (err: any, res: any) => {
+      if (err) {
+        setCurrentError(JSON.stringify(err));
+        this.setState({ loading: false });
+      } else {
+        if (res.data) {
+          this.setState({ charts: res.data });
+        }
+        this.setState({ loading: false });
+      }
+    });
+  }
+
+  componentDidMount() {
+    this.updateCharts();
+  }
+
+  componentDidUpdate(prevProps: PropsType) {
+    if (prevProps !== this.props) {
+      this.updateCharts();
+    }
+  }
+
+  renderChartList = () => {
+    if (this.state.loading) {
+      return <LoadingWrapper><Loading /></LoadingWrapper>
+    }
+
+    return this.state.charts.map((x: ChartType, i: number) => {
+      return (
+        <Chart key={i} chart={x} />
+      )
+    })
+  }
+
+
+  render() {
+    return (
+      <StyledChartList>
+        {this.renderChartList()}
+      </StyledChartList>
+    );
+  }
+}
+
+ChartList.contextType = Context;
+
+const LoadingWrapper = styled.div`
+  padding-top: 100px;
+`;
+
+const StyledChartList = styled.div`
+  padding-bottom: 100px;
+`;

+ 4 - 4
dashboard/src/main/home/sidebar/ClusterSection.tsx

@@ -47,7 +47,7 @@ export default class ClusterSection extends Component<PropsType, StateType> {
             setCurrentCluster(kubeContexts[0].name);
           } else {
             this.setState({ kubeContexts: [] });
-            setCurrentCluster(null);
+            setCurrentCluster('');
           }
         }
       }
@@ -101,7 +101,7 @@ export default class ClusterSection extends Component<PropsType, StateType> {
       return (
         <ClusterSelector showDrawer={showDrawer}>
           <LinkWrapper>
-            <ClusterIcon><i className="material-icons">polymer</i></ClusterIcon>
+            <ClusterIcon><i className="material-icons">device_hub</i></ClusterIcon>
             <ClusterName>{currentCluster}</ClusterName>
           </LinkWrapper>
           <DrawerButton onClick={this.toggleDrawer}>
@@ -227,10 +227,10 @@ const DropdownIcon = styled.span`
 
 const ClusterIcon = styled.div`
   > i {
-    font-size: 16px;
+    font-size: 18px;
     display: flex;
     align-items: center;
-    margin-bottom: -2px;
+    margin-bottom: 0px;
     margin-left: 15px;
     margin-right: 10px;
   }

+ 3 - 3
dashboard/src/main/home/sidebar/Drawer.tsx

@@ -34,7 +34,7 @@ export default class Drawer extends Component<PropsType, StateType> {
             active={kubeContext.name === currentCluster}
             onClick={() => setCurrentCluster(kubeContext.name)}
           >
-            <ClusterIcon><i className="material-icons">polymer</i></ClusterIcon>
+            <ClusterIcon><i className="material-icons">device_hub</i></ClusterIcon>
             <ClusterName>{kubeContext.name}</ClusterName>
           </ClusterOption>
         );
@@ -174,10 +174,10 @@ const CloseButtonImg = styled.img`
 
 const ClusterIcon = styled.div`
   > i {
-    font-size: 16px;
+    font-size: 18px;
     display: flex;
     align-items: center;
-    margin-bottom: -2px;
+    margin-bottom: 0px;
     margin-left: 15px;
     margin-right: 10px;
   }