Explorar o código

Merge branch 'frontend-integration' of https://github.com/porter-dev/porter into frontend-integration

mergin
Alexander Belanger %!s(int64=5) %!d(string=hai) anos
pai
achega
e66f269a96

+ 1 - 1
.gitignore

@@ -2,4 +2,4 @@
 .env
 app
 *.db
-
+test.yaml

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

+ 151 - 0
dashboard/src/components/Selector.tsx

@@ -0,0 +1,151 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+
+type PropsType = {
+  activeValue: string,
+  options: { value: string, label: string }[],
+  setActiveValue: (x: string) => void,
+  dropdownLabel: string
+};
+
+type StateType = {
+};
+
+export default class Selector extends Component<PropsType, StateType> {
+  state = {
+    expanded: false
+  }
+
+  renderOptionList = () => {
+    let { options, activeValue, setActiveValue } = this.props;
+    return options.map((option: { value: string, label: string }, i: number) => {
+      return (
+        <Option
+          key={i}
+          selected={option.value === activeValue}
+          onClick={() => setActiveValue(option.value)}
+        >
+          {option.label}
+        </Option>
+      );
+    });
+  }
+
+  renderDropdown = () => {
+    if (this.state.expanded) {
+      return (
+        <div>
+          <CloseOverlay onClick={() => this.setState({ expanded: false })}/>
+          <Dropdown>
+            <DropdownLabel>
+              {this.props.dropdownLabel}
+            </DropdownLabel>
+            {this.renderOptionList()}
+          </Dropdown>
+        </div>
+      )
+    }
+  }
+
+  render() {
+    let { activeValue } = this.props;
+    return (
+      <StyledSelector>
+        <MainSelector
+          onClick={() => this.setState({ expanded: !this.state.expanded })}
+          expanded={this.state.expanded}
+        >
+          <TextWrap>
+            {activeValue === '' ? 'All' : activeValue}
+          </TextWrap>
+          <i className="material-icons">arrow_drop_down</i>
+        </MainSelector>
+        {this.renderDropdown()}
+      </StyledSelector>
+    );
+  }
+}
+
+const TextWrap = styled.div`
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+`;
+
+const DropdownLabel = styled.div`
+  font-size: 13px;
+  color: #ffffff44;
+  font-weight: 500;
+  margin: 10px 13px;
+`;
+
+const Option = styled.div` 
+  width: 100%;
+  border-bottom: 1px solid #ffffff10;
+  height: 35px;
+  font-size: 13px;
+  padding-top: 9px;
+  align-items: center;
+  padding-left: 15px;
+  cursor: pointer;
+  padding-right: 10px;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  background: ${(props: { selected: boolean }) => props.selected ? '#ffffff11' : ''};
+
+  :hover {
+    background: #ffffff22;
+  }
+`;
+
+const CloseOverlay = styled.div`
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100vw;
+  height: 100vh;
+  z-index: 999;
+`;
+
+const Dropdown = styled.div`
+  position: absolute;
+  right: 0;
+  top: calc(100% + 5px);
+  background: #26282f;
+  width: calc(100% + 80px);
+  max-height: 300px;
+  padding-bottom: 20px;
+  border-radius: 3px;
+  z-index: 999;
+  overflow-y: auto;
+  box-shadow: 0 8px 20px 0px #00000055;
+`;
+
+const StyledSelector = styled.div`
+  position: relative;
+`;
+
+const MainSelector = styled.div`
+  width: 150px;
+  height: 30px;
+  border: 1px solid #ffffff66;
+  font-size: 13px;
+  padding: 5px 10px;
+  padding-left: 12px;
+  border-radius: 3px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  cursor: pointer;
+  background: ${(props: { expanded: boolean }) => props.expanded ? '#ffffff33' : '#ffffff11'};
+
+  :hover {
+    background: ${(props: { expanded: boolean }) => props.expanded ? '#ffffff33' : '#ffffff22'};
+  }
+
+  > i {
+    font-size: 20px;
+    transform: ${(props: { expanded: boolean }) => props.expanded ? 'rotate(180deg)' : ''};
+  }
+`;

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

+ 57 - 42
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,10 @@ Main.contextType = Context;
 const GlobalStyle = createGlobalStyle`
   * {
     box-sizing: border-box;
+    font-family: 'Work Sans', sans-serif;
+  }
+  body {
+    background: #202227;
   }
 `;
 
@@ -110,6 +125,6 @@ const StyledMain = styled.div`
   position: fixed;
   top: 0;
   left: 0;
-  background: #24272a;
+  background: #202227;
   color: white;
 `;

+ 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() {

+ 41 - 58
dashboard/src/main/home/dashboard/Dashboard.tsx

@@ -3,51 +3,20 @@ 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';
+import NamespaceSelector from './NamespaceSelector';
 
 type PropsType = {
 };
 
 type StateType = {
-  charts: ChartType[]
+  namespace: string
 };
 
 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} />
-      )
-    })
+    namespace: ''
   }
 
   render() {
@@ -74,8 +43,22 @@ export default class Dashboard extends Component<PropsType, StateType> {
         </InfoSection>
 
         <LineBreak />
-
-        {this.renderChartList()}
+        
+        <ControlRow>
+          <Button disabled={true}>
+            <i className="material-icons">add</i> Add a Chart
+          </Button>
+          <NamespaceSelector
+            setNamespace={(namespace) => this.setState({ namespace })}
+            namespace={this.state.namespace}
+            currentCluster={currentCluster}
+          />
+        </ControlRow>
+
+        <ChartList
+          currentCluster={currentCluster}
+          namespace={this.state.namespace}
+        />
       </div>
     );
   }
@@ -83,6 +66,14 @@ export default class Dashboard extends Component<PropsType, StateType> {
 
 Dashboard.contextType = Context;
 
+const ControlRow = styled.div`
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 35px;
+  padding-left: 0px;
+`;
+
 const TopRow = styled.div`
   display: flex;
   align-items: center;
@@ -112,43 +103,35 @@ const InfoLabel = styled.div`
 const InfoSection = styled.div`
   margin-top: 20px;
   font-family: 'Work Sans', sans-serif;
-  margin-left: 7px;
+  margin-left: 0px;
   margin-bottom: 35px;
 `;
 
-const ButtonWrap = styled.div`
-  display: flex;
-  align-items: center;
-  font-size: 18px;
-  margin-top: 2px;
-  margin-bottom: 25px;
-  color: #00000020;
-`;
-
 const Button = styled.div`
-  min-width: 145px;
-  max-width: 145px;
   display: flex;
-  flex: 1;
   flex-direction: row;
   align-items: center;
   justify-content: space-between;
   font-size: 13px;
   cursor: pointer;
   font-family: 'Work Sans', sans-serif;
-  margin-left: 5px;
   border-radius: 20px;
   color: white;
-  padding: 6px 8px;
+  height: 30px;
+  padding: 0px 8px;
+  padding-bottom: 1px;
   margin-right: 10px;
-  padding-right: 13px;
+  font-weight: 500;
+  padding-right: 15px;
   overflow: hidden;
   white-space: nowrap;
   text-overflow: ellipsis;
+  box-shadow: 0 5px 8px 0px #00000010;
+  cursor: not-allowed;
 
-  background: #616FEEcc;
+  background: ${(props: { disabled: boolean }) => props.disabled ? '#aaaabbee' :'#616FEEcc'};
   :hover {
-    background: #505edddd;
+    background: ${(props: { disabled: boolean }) => props.disabled ? '' : '#505edddd'};
   }
 
   > i {
@@ -159,7 +142,7 @@ const Button = styled.div`
     border-radius: 20px;
     display: flex;
     align-items: center;
-    margin-top: -1px;
+    margin-right: 8px;
     justify-content: center;
   }
 `;
@@ -233,7 +216,7 @@ const Title = styled.div`
   font-size: 20px;
   font-weight: 500;
   font-family: 'Work Sans', sans-serif;
-  margin-left: 20px;
+  margin-left: 18px;
   color: #ffffff;
   white-space: nowrap;
   overflow: hidden;
@@ -247,7 +230,7 @@ const TitleSection = styled.div`
   display: flex;
   flex-direction: row;
   align-items: center;
-  padding-left: 17px;
+  padding-left: 0px;
 
   > i {
     margin-left: 10px;

+ 87 - 0
dashboard/src/main/home/dashboard/NamespaceSelector.tsx

@@ -0,0 +1,87 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+
+import { Context } from '../../../shared/Context';
+import api from '../../../shared/api';
+
+import Selector from '../../../components/Selector';
+
+type PropsType = {
+  setNamespace: (x: string) => void,
+  currentCluster: string,
+  namespace: string
+};
+
+type StateType = {
+  namespaceOptions: { label: string, value: string }[]
+};
+
+
+// TODO: display selected in option dropdown and actually filter!
+
+export default class NamespaceSelector extends Component<PropsType, StateType> {
+  state = {
+    namespaceOptions: [] as { label: string, value: string }[]
+  }
+
+  updateOptions = () => {
+    let { currentCluster, setCurrentError } = this.context;
+
+    api.getNamespaces('<token>', { context: currentCluster }, {}, (err: any, res: any) => {
+      if (err) {
+        setCurrentError('Could not read clusters: ' + JSON.stringify(err));
+      } else {
+        let namespaceOptions: { label: string, value: string }[] = [];
+        res.data.items.forEach((x: { metadata: { name: string }}, i: number) => {
+          namespaceOptions.push({ label: x.metadata.name, value: x.metadata.name });
+        })
+        this.setState({ namespaceOptions });
+      }
+    });
+  }
+
+  componentDidMount() {
+    this.updateOptions();
+  }
+
+  componentDidUpdate(prevProps: PropsType) {
+    if (prevProps !== this.props) {
+      this.updateOptions();
+    }
+  }
+
+  render() {
+    return ( 
+      <StyledNamespaceSelector>
+        <Label>
+          <i className="material-icons">filter_alt</i> Filter
+        </Label>
+        <Selector
+          activeValue={this.props.namespace}
+          setActiveValue={(namespace) => this.props.setNamespace(namespace)}
+          options={this.state.namespaceOptions}
+          dropdownLabel='Namespace:'
+        />
+      </StyledNamespaceSelector>
+    );
+  }
+}
+
+NamespaceSelector.contextType = Context;
+
+const Label = styled.div`
+  display: flex;
+  align-items: center;
+  margin-right: 12px;
+
+  > i {
+    margin-right: 8px;
+    font-size: 18px;
+  }
+`;
+
+const StyledNamespaceSelector = styled.div`
+  display: flex;
+  align-items: center;
+  font-size: 13px;
+`;

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

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

@@ -0,0 +1,92 @@
+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,
+  namespace: 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: this.props.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;
   }

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

@@ -50,6 +50,10 @@ const getCharts = baseApi<{
   statusFilter: string[]
 }>('GET', '/api/charts');
 
+const getNamespaces = baseApi<{
+  context: string
+}>('GET', '/api/k8s/namespaces');
+
 // Bundle export to allow default api import (api.<method> is more readable)
 export default {
   checkAuth,
@@ -60,4 +64,5 @@ export default {
   updateUser,
   getContexts,
   getCharts,
+  getNamespaces
 }

+ 1 - 1
docker/.env

@@ -12,4 +12,4 @@ DB_PASS=porter
 DB_NAME=porter
 COOKIE_SECRETS=secret
 
-QUICK_START=false
+QUICK_START=true

+ 20 - 2
docs/API.md

@@ -208,7 +208,16 @@ User{
 }
 ```
 
-**Successful Response Body**: N/A
+**Successful Response Body**:
+User object with only the id field. Other fields are empty - with values in parantheses.
+```js
+{
+  "id": Int,
+  "email": String ("")
+  "contexts": []String (NULL)
+  "rawKubeConfig": String ("")
+}
+```
 
 **Successful Status Code**: `201`
 
@@ -261,7 +270,16 @@ User{
 }
 ```
 
-**Successful Response Body**: N/A
+**Successful Response Body**:
+User object with only the id field. Other fields are empty - with values in parantheses.
+```js
+{
+  "id": Int,
+  "email": String ("")
+  "contexts": []String (NULL)
+  "rawKubeConfig": String ("")
+}
+```
 
 **Successful Status Code**: `200`
 

+ 0 - 1
server/api/user_handler.go

@@ -343,7 +343,6 @@ func (app *App) sendUserID(w http.ResponseWriter, userID uint) error {
 	resUser := &models.UserExternal{
 		ID: userID,
 	}
-
 	if err := json.NewEncoder(w).Encode(resUser); err != nil {
 		return err
 	}

+ 40 - 4
server/api/user_handler_test.go

@@ -70,6 +70,42 @@ func testUserRequests(t *testing.T, tests []*userTest, canQuery bool) {
 
 // ------------------------- TEST FIXTURES AND FUNCTIONS  ------------------------- //
 
+var authCheckTests = []*userTest{
+	&userTest{
+		initializers: []func(tester *tester){
+			initUserDefault,
+		},
+		msg:       "Auth check successful. User is logged in.",
+		method:    "GET",
+		endpoint:  "/api/auth/check",
+		expStatus: http.StatusOK,
+		body:      "",
+		expBody:   `{"id":1,"email":"","contexts":null,"rawKubeConfig":""}`,
+		useCookie: true,
+		validators: []func(c *userTest, tester *tester, t *testing.T){
+			userBasicBodyValidator,
+		},
+	},
+	&userTest{
+		initializers: []func(tester *tester){
+			initUserDefault,
+		},
+		msg:       "Auth check failure. User is not logged in.",
+		method:    "GET",
+		endpoint:  "/api/auth/check",
+		body:      "",
+		expStatus: http.StatusForbidden,
+		expBody:   http.StatusText(http.StatusForbidden) + "\n",
+		validators: []func(c *userTest, tester *tester, t *testing.T){
+			userBasicBodyValidator,
+		},
+	},
+}
+
+func TestHandleAuthCheck(t *testing.T) {
+	testUserRequests(t, authCheckTests, true)
+}
+
 var createUserTests = []*userTest{
 	&userTest{
 		msg:      "Create user",
@@ -80,7 +116,7 @@ var createUserTests = []*userTest{
 			"password": "hello"
 		}`,
 		expStatus: http.StatusCreated,
-		expBody:   "",
+		expBody:   `{"id":1,"email":"","contexts":null,"rawKubeConfig":""}`,
 	},
 	&userTest{
 		msg:      "Create user invalid email",
@@ -180,7 +216,7 @@ var loginUserTests = []*userTest{
 			"password": "hello"
 		}`,
 		expStatus: http.StatusOK,
-		expBody:   ``,
+		expBody:   `{"id":1,"email":"","contexts":null,"rawKubeConfig":""}`,
 		validators: []func(c *userTest, tester *tester, t *testing.T){
 			userBasicBodyValidator,
 		},
@@ -197,7 +233,7 @@ var loginUserTests = []*userTest{
 			"password": "hello"
 		}`,
 		expStatus: http.StatusOK,
-		expBody:   ``,
+		expBody:   `{"id":1,"email":"","contexts":null,"rawKubeConfig":""}`,
 		useCookie: true,
 		validators: []func(c *userTest, tester *tester, t *testing.T){
 			userBasicBodyValidator,
@@ -670,7 +706,7 @@ func initUserWithContexts(tester *tester) {
 }
 
 func userBasicBodyValidator(c *userTest, tester *tester, t *testing.T) {
-	if body := tester.rr.Body.String(); body != c.expBody {
+	if body := tester.rr.Body.String(); strings.TrimSpace(body) != strings.TrimSpace(c.expBody) {
 		t.Errorf("%s, handler returned wrong body: got %v want %v",
 			c.msg, body, c.expBody)
 	}

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 124
test.yaml


Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio