Jelajahi Sumber

rollback fixed + live update handled

jusrhee 5 tahun lalu
induk
melakukan
e880791d6a

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

@@ -4,6 +4,7 @@ import gradient from '../../../assets/gradient.jpg';
 
 
 import { Context } from '../../../shared/Context';
 import { Context } from '../../../shared/Context';
 import { ChartType } from '../../../shared/types';
 import { ChartType } from '../../../shared/types';
+import api from '../../../shared/api';
 
 
 import ChartList from './chart/ChartList';
 import ChartList from './chart/ChartList';
 import NamespaceSelector from './NamespaceSelector';
 import NamespaceSelector from './NamespaceSelector';
@@ -23,6 +24,22 @@ export default class Dashboard extends Component<PropsType, StateType> {
     currentChart: null as (ChartType | null)
     currentChart: null as (ChartType | null)
   }
   }
 
 
+  // Allows rollback to update the top-level chart
+  refreshChart = () => {
+    let { currentCluster } = this.context;
+    api.getChart('<token>', {
+      namespace: this.state.namespace,
+      context: currentCluster,
+      storage: 'secret'
+    }, { name: this.state.currentChart.name, revision: 0 }, (err: any, res: any) => {
+      if (err) {
+        console.log(err)
+      } else {
+        this.setState({ currentChart: res.data });
+      }
+    });
+  }
+
   renderContents = () => {
   renderContents = () => {
     let { currentCluster } = this.context;
     let { currentCluster } = this.context;
 
 
@@ -30,8 +47,8 @@ export default class Dashboard extends Component<PropsType, StateType> {
       return (
       return (
         <ExpandedChart
         <ExpandedChart
           currentChart={this.state.currentChart}
           currentChart={this.state.currentChart}
-          setCurrentChart={(x: ChartType | null) => this.setState({ currentChart: x })} 
-          namespace={this.state.namespace}
+          refreshChart={this.refreshChart}
+          setCurrentChart={(x: ChartType | null) => this.setState({ currentChart: x })}
         />
         />
       );
       );
     }
     }

+ 12 - 2
dashboard/src/main/home/dashboard/chart/ChartList.tsx

@@ -46,6 +46,8 @@ export default class ChartList extends Component<PropsType, StateType> {
       } else {
       } else {
         if (res.data) {
         if (res.data) {
           this.setState({ charts: res.data });
           this.setState({ charts: res.data });
+        } else {
+          this.setState({ charts: [] });
         }
         }
         this.setState({ loading: false, error: false });
         this.setState({ loading: false, error: false });
       }
       }
@@ -66,14 +68,22 @@ export default class ChartList extends Component<PropsType, StateType> {
   }
   }
 
 
   renderChartList = () => {
   renderChartList = () => {
-    if (this.state.loading) {
+    let { loading, error, charts } = this.state;
+
+    if (loading) {
       return <LoadingWrapper><Loading /></LoadingWrapper>
       return <LoadingWrapper><Loading /></LoadingWrapper>
-    } else if (this.state.error) {
+    } else if (error) {
       return (
       return (
         <Placeholder>
         <Placeholder>
           <i className="material-icons">error</i> Error connecting to cluster.
           <i className="material-icons">error</i> Error connecting to cluster.
         </Placeholder>
         </Placeholder>
       );
       );
+    } else if (charts.length === 0) {
+      return (
+        <Placeholder>
+          <i className="material-icons">category</i> No charts found in this namespace.
+        </Placeholder>
+      );
     }
     }
 
 
     return this.state.charts.map((x: ChartType, i: number) => {
     return this.state.charts.map((x: ChartType, i: number) => {

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

@@ -10,7 +10,7 @@ import RevisionSection from './RevisionSection';
 type PropsType = {
 type PropsType = {
   currentChart: ChartType,
   currentChart: ChartType,
   setCurrentChart: (x: ChartType | null) => void,
   setCurrentChart: (x: ChartType | null) => void,
-  namespace: string
+  refreshChart: () => void
 };
 };
 
 
 type StateType = {
 type StateType = {
@@ -40,7 +40,7 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
   }
   }
 
 
   render() {
   render() {
-    let { currentChart, setCurrentChart } = this.props;
+    let { currentChart, setCurrentChart, refreshChart } = this.props;
     let chart = currentChart;
     let chart = currentChart;
 
 
     return ( 
     return ( 
@@ -79,7 +79,7 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
           showRevisions={this.state.showRevisions}
           showRevisions={this.state.showRevisions}
           toggleShowRevisions={() => this.setState({ showRevisions: !this.state.showRevisions })}
           toggleShowRevisions={() => this.setState({ showRevisions: !this.state.showRevisions })}
           chart={chart}
           chart={chart}
-          namespace={this.props.namespace}
+          refreshChart={refreshChart}
         />
         />
 
 
         <ChartSection>
         <ChartSection>

+ 161 - 15
dashboard/src/main/home/dashboard/expanded-chart/RevisionSection.tsx

@@ -1,5 +1,6 @@
 import React, { Component } from 'react';
 import React, { Component } from 'react';
 import styled from 'styled-components';
 import styled from 'styled-components';
+import loading from '../../../../assets/loading.gif';
 
 
 import api from '../../../../shared/api';
 import api from '../../../../shared/api';
 import { Context } from '../../../../shared/Context';
 import { Context } from '../../../../shared/Context';
@@ -10,23 +11,27 @@ type PropsType = {
   showRevisions: boolean,
   showRevisions: boolean,
   toggleShowRevisions: () => void,
   toggleShowRevisions: () => void,
   chart: ChartType,
   chart: ChartType,
-  namespace: string
+  refreshChart: () => void
 };
 };
 
 
 type StateType = {
 type StateType = {
-  revisions: ChartType[]
+  revisions: ChartType[],
+  rollbackRevision: number | null,
+  loading: boolean
 };
 };
 
 
 export default class RevisionSection extends Component<PropsType, StateType> {
 export default class RevisionSection extends Component<PropsType, StateType> {
   state = {
   state = {
-    revisions: [] as ChartType[]
+    revisions: [] as ChartType[],
+    rollbackRevision: null as (number | null),
+    loading: false
   }
   }
 
 
-  componentDidMount() {
+  refreshHistory = () => {
     let { chart } = this.props;
     let { chart } = this.props;
 
 
     api.getRevisions('<token>', {
     api.getRevisions('<token>', {
-      namespace: this.props.namespace,
+      namespace: chart.namespace,
       context: this.context.currentCluster,
       context: this.context.currentCluster,
       storage: 'secret'
       storage: 'secret'
     }, { name: chart.name }, (err: any, res: any) => {
     }, { name: chart.name }, (err: any, res: any) => {
@@ -38,6 +43,10 @@ export default class RevisionSection extends Component<PropsType, StateType> {
     });
     });
   }
   }
 
 
+  componentDidMount() {
+    this.refreshHistory();
+  }
+
   readableDate = (s: string) => {
   readableDate = (s: string) => {
     let ts = new Date(s);
     let ts = new Date(s);
     let date = ts.toLocaleDateString();
     let date = ts.toLocaleDateString();
@@ -45,19 +54,24 @@ export default class RevisionSection extends Component<PropsType, StateType> {
     return `${time} on ${date}`;
     return `${time} on ${date}`;
   }
   }
 
 
-  handleRollback = (revision: number) => {    
+  handleRollback = () => {
+    let revisionNumber = this.state.rollbackRevision;
+    this.setState({ loading: true, rollbackRevision: null });
+
     api.rollbackChart('<token>', {
     api.rollbackChart('<token>', {
-      namespace: this.props.namespace,
+      namespace: this.props.chart.namespace,
       context: this.context.currentCluster,
       context: this.context.currentCluster,
       storage: 'secret'
       storage: 'secret'
     }, {
     }, {
       name: this.props.chart.name,
       name: this.props.chart.name,
-      revision,
+      revision: revisionNumber
     }, (err: any, res: any) => {
     }, (err: any, res: any) => {
       if (err) {
       if (err) {
         console.log(err)
         console.log(err)
       } else {
       } else {
-        console.log(res)
+        this.setState({ loading: false });
+        this.props.refreshChart();
+        this.refreshHistory();
       }
       }
     });
     });
   }
   }
@@ -72,7 +86,7 @@ export default class RevisionSection extends Component<PropsType, StateType> {
           <Td>
           <Td>
             <RollbackButton
             <RollbackButton
               disabled={revision.version === this.props.chart.version}
               disabled={revision.version === this.props.chart.version}
-              onClick={() => this.handleRollback(revision.version)}
+              onClick={() => this.setState({ rollbackRevision: revision.version })}
             >
             >
               {revision.version === this.props.chart.version ? 'Current' : 'Revert'}
               {revision.version === this.props.chart.version ? 'Current' : 'Revert'}
             </RollbackButton>
             </RollbackButton>
@@ -100,9 +114,41 @@ export default class RevisionSection extends Component<PropsType, StateType> {
     }
     }
   }
   }
 
 
-  render() {
+  renderConfirmOverlay = () => {
+    if (this.state.rollbackRevision) {
+      return (
+        <ConfirmOverlay>
+          {`Are you sure you want to revert to version ${this.state.rollbackRevision}?`}
+          <ButtonRow>
+            <ConfirmButton
+              onClick={() => this.handleRollback()}
+            >
+              Yes
+            </ConfirmButton>
+            <ConfirmButton
+              onClick={() => this.setState({ rollbackRevision: null })}
+            >
+              No
+            </ConfirmButton>
+          </ButtonRow>
+        </ConfirmOverlay>
+      );
+    }
+  }
+
+  renderContents = () => {
+    if (this.state.loading) {
+      return (
+        <LoadingPlaceholder>
+          <StatusWrapper>
+            <LoadingGif src={loading} /> Updating . . .
+          </StatusWrapper>
+        </LoadingPlaceholder>
+      )
+    }
+
     return (
     return (
-      <StyledRevisionSection showRevisions={this.props.showRevisions}>
+      <div>
         <RevisionHeader
         <RevisionHeader
           showRevisions={this.props.showRevisions}
           showRevisions={this.props.showRevisions}
           onClick={this.props.toggleShowRevisions}
           onClick={this.props.toggleShowRevisions}
@@ -111,7 +157,18 @@ export default class RevisionSection extends Component<PropsType, StateType> {
           <i className="material-icons">expand_more</i>
           <i className="material-icons">expand_more</i>
         </RevisionHeader>
         </RevisionHeader>
 
 
-        {this.renderExpanded()}
+        <RevisionList>
+          {this.renderExpanded()}
+        </RevisionList>
+      </div>
+    );
+  }
+
+  render() {
+    return (
+      <StyledRevisionSection showRevisions={this.props.showRevisions}>
+        {this.renderContents()}
+        {this.renderConfirmOverlay()}
       </StyledRevisionSection>
       </StyledRevisionSection>
     );
     );
   }
   }
@@ -119,6 +176,95 @@ export default class RevisionSection extends Component<PropsType, StateType> {
 
 
 RevisionSection.contextType = Context;
 RevisionSection.contextType = Context;
 
 
+const LoadingPlaceholder = styled.div`
+  height: 40px;
+  display: flex;
+  align-items: center;
+  padding-left: 20px;
+`;
+
+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;
+`;
+
+const ConfirmOverlay = styled.div`
+  position: absolute;
+  top: 0px;
+  opacity: 100%;
+  left: 0px;
+  width: 100%;
+  height: 100%;
+  z-index: 999;
+  display: flex;
+  padding-bottom: 30px;
+  align-items: center;
+  justify-content: center;
+  font-family: 'Work Sans', sans-serif;
+  font-size: 18px;
+  font-weight: 500;
+  color: white;
+  flex-direction: column;
+  background: rgb(0,0,0,0.73);
+  opacity: 0;
+  animation: lindEnter 0.2s;
+  animation-fill-mode: forwards;
+
+  @keyframes lindEnter {
+    from { opacity: 0; }
+    to   { opacity: 1; }
+  }
+`;
+
+const ButtonRow = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  width: 180px;
+  margin-top: 30px;
+`;
+
+const ConfirmButton = styled.div`
+  font-size: 18px;
+  padding: 10px 15px;
+  outline: none; 
+  border: 1px solid white;
+  border-radius: 10px; 
+  text-align: center; 
+  width: 80px;
+  cursor: pointer;
+  opacity: 0;
+  font-family: 'Work Sans', sans-serif;
+  font-size: 18px;
+  font-weight: 500;
+  animation: linEnter 0.3s 0.1s;
+  animation-fill-mode: forwards;
+  @keyframes linEnter {
+    from { transform: translateY(20px); opacity: 0; }
+    to   { transform: translateY(0px); opacity: 1; }
+  }
+  :hover {
+    background: white;
+    color: #232323;
+  }
+`;
+
+const RevisionList = styled.div`
+  overflow-y: auto;
+  max-height: 215px;
+`;
+
 const RollbackButton = styled.div`
 const RollbackButton = styled.div`
   cursor: ${(props: { disabled: boolean }) => props.disabled ? 'not-allowed' :'pointer'};
   cursor: ${(props: { disabled: boolean }) => props.disabled ? 'not-allowed' :'pointer'};
   display: flex;
   display: flex;
@@ -191,11 +337,11 @@ const RevisionHeader = styled.div`
 
 
 const StyledRevisionSection = styled.div`
 const StyledRevisionSection = styled.div`
   width: 100%;
   width: 100%;
-  max-height: ${(props: { showRevisions: boolean }) => props.showRevisions ? '250px' : '40px'};
+  max-height: ${(props: { showRevisions: boolean }) => props.showRevisions ? '255px' : '40px'};
   background: #ffffff11;
   background: #ffffff11;
   margin-top: 25px;
   margin-top: 25px;
+  overflow: hidden;
   border-radius: 5px;
   border-radius: 5px;
-  overflow-y: auto;
   animation: ${(props: { showRevisions: boolean }) => props.showRevisions ? 'expandRevisions 0.3s' : ''};
   animation: ${(props: { showRevisions: boolean }) => props.showRevisions ? 'expandRevisions 0.3s' : ''};
   animation-timing-function: ease-out;
   animation-timing-function: ease-out;
   @keyframes expandRevisions {
   @keyframes expandRevisions {

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

@@ -50,6 +50,14 @@ const getCharts = baseApi<{
   statusFilter: string[]
   statusFilter: string[]
 }>('GET', '/api/charts');
 }>('GET', '/api/charts');
 
 
+const getChart = baseApi<{
+  namespace: string,
+  context: string,
+  storage: string
+}, { name: string, revision: number }>('GET', pathParams => {
+  return `/api/charts/${pathParams.name}/${pathParams.revision}`;
+});
+
 const getNamespaces = baseApi<{
 const getNamespaces = baseApi<{
   context: string
   context: string
 }>('GET', '/api/k8s/namespaces');
 }>('GET', '/api/k8s/namespaces');
@@ -80,6 +88,7 @@ export default {
   updateUser,
   updateUser,
   getContexts,
   getContexts,
   getCharts,
   getCharts,
+  getChart,
   getNamespaces,
   getNamespaces,
   getRevisions,
   getRevisions,
   rollbackChart
   rollbackChart

+ 1 - 1
dashboard/src/shared/baseApi.tsx

@@ -1,7 +1,7 @@
 import axios from 'axios';
 import axios from 'axios';
 import qs from 'qs';
 import qs from 'qs';
 
 
-axios.defaults.timeout = 1000;
+axios.defaults.timeout = 2500;
 
 
 // Partial function that accepts a generic params type and returns an api method
 // 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) => {
 export const baseApi = <T extends {}, S = {}>(requestType: string, endpoint: ((pathParams: S) => string) | string) => {