Explorar el Código

rollback boilerplate with expanded chart boilerplate

jusrhee hace 5 años
padre
commit
a9d00c6104

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

@@ -49,6 +49,7 @@ class YamlEditor extends Component<PropsType, StateType> {
             name='codeEditor'
             editorProps={{ $blockScrolling: true }}
             width='100%'
+            height='295px'
             style={{ borderRadius: '5px' }}
           />
         </Editor>
@@ -65,9 +66,10 @@ const Editor = styled.form`
   width: calc(100% - 0px);
   border-radius: 5px;
   border: 1px solid #ffffff22;
-  height: 295px;
-  overflow: auto;
 `;
 
 const Holder = styled.div`
+  .ace_scrollbar {
+    display: none;
+  }
 `;

+ 29 - 7
dashboard/src/main/home/dashboard/Dashboard.tsx

@@ -3,26 +3,39 @@ import styled from 'styled-components';
 import gradient from '../../../assets/gradient.jpg';
 
 import { Context } from '../../../shared/Context';
+import { ChartType } from '../../../shared/types';
 
 import ChartList from './chart/ChartList';
 import NamespaceSelector from './NamespaceSelector';
+import ExpandedChart from './expanded-chart/ExpandedChart';
 
 type PropsType = {
 };
 
 type StateType = {
-  namespace: string
+  namespace: string,
+  currentChart: ChartType | null
 };
 
 export default class Dashboard extends Component<PropsType, StateType> {
   state = {
-    namespace: ''
+    namespace: '',
+    currentChart: null as (ChartType | null)
   }
 
-  render() {
+  renderContents = () => {
     let { currentCluster } = this.context;
 
-    return ( 
+    if (this.state.currentChart) {
+      return (
+        <ExpandedChart
+          currentChart={this.state.currentChart}
+          setCurrentChart={(x: ChartType | null) => this.setState({ currentChart: x })} 
+        />
+      );
+    }
+
+    return (
       <div>
         <TitleSection>
           <ProjectIcon>
@@ -39,14 +52,14 @@ export default class Dashboard extends Component<PropsType, StateType> {
               <i className="material-icons">info</i> Info
             </InfoLabel>
           </TopRow>
-            <Description>Porter dashboard for {currentCluster}.</Description>
+          <Description>Porter dashboard for {currentCluster}.</Description>
         </InfoSection>
 
         <LineBreak />
         
         <ControlRow>
           <Button disabled={true}>
-            <i className="material-icons">add</i> Add a Chart
+            <i className="material-icons">add</i> Deploy a Chart
           </Button>
           <NamespaceSelector
             setNamespace={(namespace) => this.setState({ namespace })}
@@ -58,10 +71,19 @@ export default class Dashboard extends Component<PropsType, StateType> {
         <ChartList
           currentCluster={currentCluster}
           namespace={this.state.namespace}
+          setCurrentChart={(x: ChartType) => this.setState({ currentChart: x })}
         />
       </div>
     );
   }
+
+  render() {
+    return (
+      <div>
+        {this.renderContents()}
+      </div>
+    );
+  }
 }
 
 Dashboard.contextType = Context;
@@ -142,7 +164,7 @@ const Button = styled.div`
     border-radius: 20px;
     display: flex;
     align-items: center;
-    margin-right: 8px;
+    margin-right: 5px;
     justify-content: center;
   }
 `;

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

@@ -29,9 +29,10 @@ export default class NamespaceSelector extends Component<PropsType, StateType> {
 
     api.getNamespaces('<token>', { context: currentCluster }, {}, (err: any, res: any) => {
       if (err) {
-        setCurrentError('Could not read clusters: ' + JSON.stringify(err));
+        // setCurrentError('Could not read clusters: ' + JSON.stringify(err));
+        this.setState({ namespaceOptions: [{ label: 'All', value: '' }] });
       } else {
-        let namespaceOptions: { label: string, value: string }[] = [];
+        let namespaceOptions: { label: string, value: string }[] = [{ label: 'All', value: '' }];
         res.data.items.forEach((x: { metadata: { name: string }}, i: number) => {
           namespaceOptions.push({ label: x.metadata.name, value: x.metadata.name });
         })

+ 9 - 3
dashboard/src/main/home/dashboard/chart/Chart.tsx

@@ -2,9 +2,11 @@ import React, { Component } from 'react';
 import styled from 'styled-components';
 
 import { ChartType } from '../../../../shared/types';
+import { Context } from '../../../../shared/Context';
 
 type PropsType = {
-  chart: ChartType
+  chart: ChartType,
+  setCurrentChart: (c: ChartType) => void
 };
 
 type StateType = {
@@ -33,12 +35,14 @@ export default class Chart extends Component<PropsType, StateType> {
   }
 
   render() {
-    let { chart } = this.props;
+    let { chart, setCurrentChart } = this.props;
+
     return ( 
       <StyledChart
         onMouseEnter={() => this.setState({ expand: true })}
         onMouseLeave={() => this.setState({ expand: false })}
         expand={this.state.expand}
+        onClick={() => setCurrentChart(chart)}
       >
         <Title>
           <IconWrapper>
@@ -71,6 +75,8 @@ export default class Chart extends Component<PropsType, StateType> {
   }
 }
 
+Chart.contextType = Context;
+
 const Version = styled.div`
   position: absolute;
   top: 12px;
@@ -219,7 +225,7 @@ const StyledChart = styled.div`
 
   animation: ${(props: { expand: boolean }) => props.expand ? 'expand' : 'shrink'} 0.12s;
   animation-fill-mode: forwards;
-  animation-timing-function: ease;
+  animation-timing-function: ease-out;
 
   @keyframes expand {
     from { 

+ 39 - 8
dashboard/src/main/home/dashboard/chart/ChartList.tsx

@@ -10,23 +10,26 @@ import Loading from '../../../../components/Loading';
 
 type PropsType = {
   currentCluster: string,
-  namespace: string
+  namespace: string,
+  setCurrentChart: (c: ChartType) => void
 };
 
 type StateType = {
   charts: ChartType[],
-  loading: boolean
+  loading: boolean,
+  error: boolean
 };
 
 export default class ChartList extends Component<PropsType, StateType> {
   state = {
     charts: [] as ChartType[],
     loading: false,
+    error: false,
   }
 
   updateCharts = () => {
     let { setCurrentError, currentCluster } = this.context;
-    
+
     this.setState({ loading: true });
     api.getCharts('<token>', {
       namespace: this.props.namespace,
@@ -38,13 +41,13 @@ export default class ChartList extends Component<PropsType, StateType> {
       statusFilter: ['deployed']
     }, {}, (err: any, res: any) => {
       if (err) {
-        setCurrentError(JSON.stringify(err));
-        this.setState({ loading: false });
+        // setCurrentError(JSON.stringify(err));
+        this.setState({ loading: false, error: true });
       } else {
         if (res.data) {
           this.setState({ charts: res.data });
         }
-        this.setState({ loading: false });
+        this.setState({ loading: false, error: false });
       }
     });
   }
@@ -54,7 +57,10 @@ export default class ChartList extends Component<PropsType, StateType> {
   }
 
   componentDidUpdate(prevProps: PropsType) {
-    if (prevProps !== this.props) {
+
+    // Ret2: Prevents reload when opening ClusterConfigModal
+    if (prevProps.currentCluster !== this.props.currentCluster || 
+      prevProps.namespace !== this.props.namespace) {
       this.updateCharts();
     }
   }
@@ -62,11 +68,21 @@ export default class ChartList extends Component<PropsType, StateType> {
   renderChartList = () => {
     if (this.state.loading) {
       return <LoadingWrapper><Loading /></LoadingWrapper>
+    } else if (this.state.error) {
+      return (
+        <Placeholder>
+          <i className="material-icons">error</i> Error connecting to cluster.
+        </Placeholder>
+      );
     }
 
     return this.state.charts.map((x: ChartType, i: number) => {
       return (
-        <Chart key={i} chart={x} />
+        <Chart
+          key={i}
+          chart={x}
+          setCurrentChart={this.props.setCurrentChart}
+        />
       )
     })
   }
@@ -83,6 +99,21 @@ export default class ChartList extends Component<PropsType, StateType> {
 
 ChartList.contextType = Context;
 
+const Placeholder = styled.div`
+  padding-top: 100px;
+  width: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  color: #ffffff44;
+  font-size: 14px;
+
+  > i {
+    font-size: 18px;
+    margin-right: 12px;
+  }
+`;
+
 const LoadingWrapper = styled.div`
   padding-top: 100px;
 `;

+ 263 - 0
dashboard/src/main/home/dashboard/expanded-chart/ExpandedChart.tsx

@@ -0,0 +1,263 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+import close from '../../../../assets/close.png';
+
+import { ChartType } from '../../../../shared/types';
+import { Context } from '../../../../shared/Context';
+
+import RevisionSection from './RevisionSection';
+
+type PropsType = {
+  currentChart: ChartType,
+  setCurrentChart: (x: ChartType | null) => void
+};
+
+type StateType = {
+  showRevisions: boolean
+};
+
+export default class ExpandedChart extends Component<PropsType, StateType> {
+  state = {
+    showRevisions: false
+  }
+
+  renderIcon = () => {
+    let { currentChart } = this.props;
+
+    if (currentChart.chart.metadata.icon && currentChart.chart.metadata.icon !== '') {
+      return <Icon src={currentChart.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 { currentChart, setCurrentChart } = this.props;
+    let chart = currentChart;
+
+    return ( 
+      <StyledExpandedChart>
+        <TitleSection>
+          <Title>
+            <IconWrapper>
+              {this.renderIcon()}
+            </IconWrapper>
+            {chart.name}
+          </Title>
+          <InfoWrapper>
+            <StatusIndicator>
+              <StatusColor status={chart.info.status} />
+              {chart.info.status}
+            </StatusIndicator>
+
+            <LastDeployed>
+              <Dot>•</Dot>Last deployed {this.readableDate(chart.info.last_deployed)}
+            </LastDeployed>
+          </InfoWrapper>
+
+          <TagWrapper>
+            Namespace
+            <NamespaceTag>
+              {chart.namespace}
+            </NamespaceTag>
+          </TagWrapper>
+        </TitleSection>
+
+        <CloseButton onClick={() => setCurrentChart(null)}>
+          <CloseButtonImg src={close} />
+        </CloseButton>
+
+        <RevisionSection
+          showRevisions={this.state.showRevisions}
+          toggleShowRevisions={() => this.setState({ showRevisions: !this.state.showRevisions })}
+          chart={chart}
+        />
+
+        <ChartSection>
+          <Placeholder>(Under construction)</Placeholder>
+        </ChartSection>
+      </StyledExpandedChart>
+    );
+  }
+}
+
+ExpandedChart.contextType = Context;
+
+const Placeholder = styled.div`
+  color: #ffffff66;
+  padding-bottom: 30px;
+`;
+
+const ChartSection = styled.div`
+  display: flex;
+  margin-top: 20px;
+  border-radius: 5px;
+  flex: 1;
+  width: 100%;
+  background: #ffffff11;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  font-size: 13px;
+`;
+
+const StatusColor = styled.div`
+  margin-bottom: 1px;
+  width: 8px;
+  height: 8px;
+  background: ${(props: { status: string }) => (props.status === 'deployed' ? '#4797ff' : props.status === 'failed' ? "#ed5f85" : "#f5cb42")};
+  border-radius: 20px;
+  margin-right: 16px;
+`;
+
+const Dot = styled.div`
+  margin-right: 9px;
+`;
+
+const InfoWrapper = styled.div`
+  display: flex;
+  align-items: center;
+  margin-left: 6px;
+  margin-top: 22px;
+`;
+
+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: 0px;
+  right: 0px;
+  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 StatusIndicator = styled.div`
+  display: flex;
+  height: 20px;
+  font-size: 13px;
+  flex-direction: row;
+  text-transform: capitalize;
+  align-items: center;
+  font-family: 'Hind Siliguri', sans-serif;
+  color: #aaaabb;
+  animation: fadeIn 0.5s;
+
+  @keyframes fadeIn {
+    from { opacity: 0 }
+    to { opacity: 1 }
+  }
+`;
+
+const Icon = styled.img`
+  width: 100%;
+`;
+
+const IconWrapper = styled.div`
+  color: #efefef;
+  font-size: 16px;
+  height: 20px;
+  width: 20px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  border-radius: 3px;
+  margin-right: 12px;
+
+  > i {
+    font-size: 20px;
+  }
+`;
+
+const Title = styled.div`
+  font-size: 18px;
+  font-weight: 500;
+  display: flex;
+  align-items: center;
+`;
+
+const TitleSection = styled.div`
+  width: 100%;
+  position: relative;
+`;
+
+const CloseButton = styled.div`
+  position: absolute;
+  display: block;
+  width: 40px;
+  height: 40px;
+  padding: 13px 0 12px 0;
+  text-align: center;
+  border-radius: 50%;
+  right: 15px;
+  top: 12px;
+  cursor: pointer;
+  :hover {
+    background-color: #ffffff11;
+  }
+`;
+
+const CloseButtonImg = styled.img`
+  width: 14px;
+  margin: 0 auto;
+`;
+
+const StyledExpandedChart = styled.div`
+  width: calc(100% - 50px);
+  height: calc(100% - 50px);
+  background: red;
+  z-index: 0;
+  position: absolute;
+  top: 25px;
+  left: 25px;;
+  border-radius: 10px;
+  background: #26282f;
+  box-shadow: 0 5px 12px 4px #00000033;
+  animation: floatIn 0.3s;
+  animation-timing-function: ease-out;
+  animation-fill-mode: forwards;
+  padding: 25px; 
+  display: flex;
+  flex-direction: column;
+
+  @keyframes floatIn {
+    from { opacity: 0; transform: translateY(30px) }
+    to { opacity: 1; transform: translateY(0px) }
+  }
+`;

+ 173 - 0
dashboard/src/main/home/dashboard/expanded-chart/RevisionSection.tsx

@@ -0,0 +1,173 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+
+import api from '../../../../shared/api';
+import { ChartType } from '../../../../shared/types';
+
+type PropsType = {
+  showRevisions: boolean,
+  toggleShowRevisions: () => void,
+  chart: ChartType
+};
+
+type StateType = {
+};
+
+const dummyRevisions = [
+  {
+    version: 3,
+    timestamp: 'Monday at 5:00 PM',
+    status: 'deployed'
+  },
+  {
+    version: 2,
+    timestamp: 'Monday at 5:00 PM',
+    status: 'superseded'
+  },
+  {
+    version: 1,
+    timestamp: 'Monday at 5:00 PM',
+    status: 'superseded'
+  }
+]
+
+export default class RevisionSection extends Component<PropsType, StateType> {
+  state = {
+  }
+
+  componentDidMount() {
+    let { chart } = this.props;
+
+    /*
+    api.getRevisions('<token>', {}, { name: chart.name }, (err: any, res: any) => {
+      if (err) {
+        alert(err);
+      } else {
+        console.log(res);
+      }
+    });
+    */
+  }
+
+  renderRevisionList = () => {
+    return dummyRevisions.map((revision: any, i: number) => {
+      return (
+        <Tr key={i}>
+          <Td>{revision.version}</Td>
+          <Td>{revision.timestamp}</Td>
+          <Td>{revision.status}</Td>
+          <Td><RollbackButton disabled={false}>Revert</RollbackButton></Td>
+        </Tr>
+      );
+    });
+  }
+
+  renderExpanded = () => {
+    if (this.props.showRevisions) {
+      return (
+        <RevisionsTable>
+          <Tr>
+            <Th>Revision No.</Th>
+            <Th>Timestamp</Th>
+            <Th>Status</Th>
+            <Th>Rollback</Th>
+          </Tr>
+          {this.renderRevisionList()}
+        </RevisionsTable>
+      )
+    }
+  }
+
+  render() {
+    return (
+      <StyledRevisionSection showRevisions={this.props.showRevisions}>
+        <RevisionHeader
+          showRevisions={this.props.showRevisions}
+          onClick={this.props.toggleShowRevisions}
+        >
+          Current Revision - <Revision>No. {this.props.chart.version}</Revision>
+          <i className="material-icons">expand_more</i>
+        </RevisionHeader>
+
+        {this.renderExpanded()}
+      </StyledRevisionSection>
+    );
+  }
+}
+
+const RollbackButton = styled.div`
+  cursor: pointer;
+  display: flex;
+  border-radius: 3px;
+  align-items: center;
+  justify-content: center;
+  font-weight: 500;
+  height: 21px;
+  font-size: 13px;
+  width: 65px;
+  background: ${(props: { disabled: boolean }) => props.disabled ? '#aaaabbee' :'#616FEEcc'};
+  :hover {
+    background: ${(props: { disabled: boolean }) => props.disabled ? '' : '#505edddd'};
+  }
+`;
+
+const Tr = styled.tr`
+  line-height: 1.8em;
+`;
+
+const Td = styled.td`
+  font-size: 13px;
+  color: #ffffff;
+`;
+
+const Th = styled.td`
+  font-size: 13px;
+  font-weight: 500;
+  color: #aaaabb;
+`;
+
+const RevisionsTable = styled.table`
+  width: 100%;
+  margin-top: 5px;
+  padding-left: 32px;
+  padding-bottom: 20px;
+`;
+
+const Revision = styled.div`
+  color: #ffffff;
+  margin-left: 5px;
+`;
+
+const RevisionHeader = styled.div`
+  color: #ffffff66;
+  display: flex;
+  align-items: center;
+  height: 40px;
+  font-size: 14px;
+  width: 100%;
+  padding-left: 15px;
+  cursor: pointer;
+  :hover {
+    background: #ffffff18;
+    > i {
+      background: #ffffff22;
+    }
+  }
+
+  > i {
+    margin-left: 12px;
+    font-size: 20px;
+    cursor: pointer;
+    border-radius: 20px;
+    transform: ${(props: { showRevisions: boolean }) => props.showRevisions ? 'rotate(180deg)' : ''};
+  }
+`;
+
+const StyledRevisionSection = styled.div`
+  width: 100%;
+  max-height: ${(props: { showRevisions: boolean }) => props.showRevisions ? '250px' : '40px'};
+  background: #ffffff11;
+  margin-top: 25px;
+  border-radius: 5px;
+  overflow-y: auto;
+`;

+ 33 - 0
dashboard/src/main/home/modals/ClusterConfigModal.tsx

@@ -161,6 +161,9 @@ export default class ClusterConfigModal extends Component<PropsType, StateType>
             value={this.state.rawKubeconfig}
             onChange={(e: any) => this.setState({ rawKubeconfig: e })}
           />
+          <UploadButton>
+            <i className="material-icons">cloud_upload</i> Upload Kubeconfig
+          </UploadButton>
           <SaveButton
             text='Save Kubeconfig'
             onClick={this.handleSaveKubeconfig}
@@ -219,6 +222,36 @@ export default class ClusterConfigModal extends Component<PropsType, StateType>
 
 ClusterConfigModal.contextType = Context;
 
+const UploadButton = styled.button`
+  display: flex;
+  align-items: center;
+  position: absolute;
+  bottom: 25px;
+  left: 30px;
+  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: #ffffff11;
+  box-shadow: 0 2px 5px 0 #00000030;
+  cursor: not-allowed;
+  user-select: none;
+  :focus { outline: 0 }
+  :hover {
+    background: #ffffff22;
+  }
+
+  > i {
+    font-size: 18px;
+    margin-right: 12px;
+  }
+`;
+
 const Checkbox = styled.div`
   width: 15px;
   height: 15px;

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

@@ -54,6 +54,10 @@ const getNamespaces = baseApi<{
   context: string
 }>('GET', '/api/k8s/namespaces');
 
+const getRevisions = baseApi<{}, { name: string }>('GET', pathParams => {
+  return `/api/charts/${pathParams.name}/history`;
+});
+
 // Bundle export to allow default api import (api.<method> is more readable)
 export default {
   checkAuth,
@@ -64,5 +68,6 @@ export default {
   updateUser,
   getContexts,
   getCharts,
-  getNamespaces
+  getNamespaces,
+  getRevisions
 }

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

@@ -1,6 +1,8 @@
 import axios from 'axios';
 import qs from 'qs';
 
+axios.defaults.timeout = 1000;
+
 // 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) => {