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

live stream deploy status in expanded chart header and revision section, refactor status indicator to shared component

sunguroku 5 лет назад
Родитель
Сommit
2e66e6914d

+ 100 - 0
dashboard/src/components/StatusIndicator.tsx

@@ -0,0 +1,100 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+import loading from '../assets/loading.gif';
+
+type PropsType = {
+    status: string,
+    controllers: Record<string, Record<string, any>>,
+    margin_left: string,
+};
+
+type StateType = {};
+
+// Manages a tab selector and renders the associated view
+export default class StatusIndicator extends Component<PropsType, StateType> {
+  renderStatus = (status: string) => {
+    if (status == 'loading') {
+      return (
+        <div>
+          <Spinner src={loading} />
+        </div>
+      )
+    }
+
+    return (
+      <div>
+        <StatusColor status={status} />
+      </div>
+    )
+  }
+
+  getChartStatus = (chartStatus: string) => {
+    if (chartStatus === 'deployed') {
+      for (var uid in this.props.controllers) {
+        let value = this.props.controllers[uid]
+        let status = this.getAvailability(value.metadata.kind, value)
+        if (!status) {
+          return 'loading'
+        }
+      }
+      return 'deployed'
+    }
+    return chartStatus
+  }
+
+  getAvailability = (kind: string, c: any) => {
+    switch (kind?.toLowerCase()) {
+      case "deployment":
+      case "replicaset":
+        return (c.status.availableReplicas == c.status.replicas)
+      case "statefulset":
+       return (c.status.readyReplicas == c.status.replicas)
+      case "daemonset":
+        return (c.status.numberAvailable == c.status.desiredNumberScheduled)
+      }
+  }
+
+  render() {
+    let status = this.getChartStatus(this.props.status)
+    return (
+    <Status margin_left={this.props.margin_left}>
+        {this.renderStatus(status)}
+        {status}
+    </Status>
+    );
+  }
+}
+
+const Spinner = styled.img`
+  width: 15px;
+  height: 15px;
+  margin-right: 15px;
+  margin-bottom: -1px;
+`;
+
+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 Status = 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;
+  margin-left: ${(props: { margin_left: string}) => props.margin_left};
+
+  @keyframes fadeIn {
+    from { opacity: 0 }
+    to { opacity: 1 }
+  }
+`;

+ 6 - 62
dashboard/src/main/home/cluster-dashboard/chart/Chart.tsx

@@ -3,6 +3,7 @@ import styled from 'styled-components';
 
 import { ChartType, StorageType } from '../../../../shared/types';
 import { Context } from '../../../../shared/Context';
+import StatusIndicator from '../../../../components/StatusIndicator';
 
 type PropsType = {
   chart: ChartType,
@@ -13,27 +14,12 @@ type PropsType = {
 type StateType = {
   expand: boolean,
   update: any[],
-  getAvailability: Function,
 };
 
 export default class Chart extends Component<PropsType, StateType> {
-  getAvailability = (kind: string, c: any) => {
-    switch (kind?.toLowerCase()) {
-      case "deployment":
-      case "replicaset":
-        return (c.status.availableReplicas == c.status.replicas)
-      case "statefulset":
-       return (c.status.readyReplicas == c.status.replicas)
-      case "daemonset":
-        return (c.status.numberAvailable == c.status.desiredNumberScheduled)
-      }
-  }
-
   state = {
     expand: false,
-    controllers: {} as Record<string, boolean>,
     update: [] as any[],
-    getAvailability: this.getAvailability.bind(this),
   }
 
   renderIcon = () => {
@@ -53,23 +39,8 @@ export default class Chart extends Component<PropsType, StateType> {
     return `${time} on ${date}`;
   }
 
-  getChartStatus = (chartStatus: string) => {
-    if (chartStatus === 'deployed') {
-      for (var uid in this.props.controllers) {
-        let value = this.props.controllers[uid]
-        let status = this.getAvailability(value.metadata.kind, value)
-        if (!status) {
-          return 'not ready'
-        }
-      }
-      return 'deployed'
-    }
-    return chartStatus
-  }
-
   render() {
     let { chart, setCurrentChart } = this.props;
-    let status = this.getChartStatus(chart.info.status)
 
     return ( 
       <StyledChart
@@ -87,11 +58,11 @@ export default class Chart extends Component<PropsType, StateType> {
 
         <BottomWrapper>
           <InfoWrapper>
-            <StatusIndicator>
-              <StatusColor status={status} />
-              {status}
-            </StatusIndicator>
-
+            <StatusIndicator
+              controllers={this.props.controllers} 
+              status={chart.info.status}
+              margin_left={'20px'}
+            />
             <LastDeployed>
               <Dot>•</Dot> Last deployed {this.readableDate(chart.info.last_deployed)}
             </LastDeployed>
@@ -206,33 +177,6 @@ const IconWrapper = styled.div`
   }
 `;
 
-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;
-  margin-left: 20px;
-  color: #aaaabb;
-  animation: fadeIn 0.5s;
-
-  @keyframes fadeIn {
-    from { opacity: 0 }
-    to { opacity: 1 }
-  }
-`;
-
-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 Title = styled.div`
   position: relative;
   text-decoration: none;

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

@@ -73,7 +73,6 @@ export default class ChartList extends Component<PropsType, StateType> {
         let event = JSON.parse(evt.data);
         let object = event.Object;
         object.metadata.kind = event.Kind
-        console.log(object)
         let chartKey = this.state.chartLookupTable[object.metadata.uid];
 
         // ignore if updated object does not belong to any chart in the list.
@@ -175,7 +174,6 @@ export default class ChartList extends Component<PropsType, StateType> {
   }
 
   componentDidUpdate(prevProps: PropsType) {
-
     // Ret2: Prevents reload when opening ClusterConfigModal
     if (prevProps.currentCluster !== this.props.currentCluster || 
       prevProps.namespace !== this.props.namespace) {

+ 72 - 65
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -2,6 +2,7 @@ import React, { Component } from 'react';
 import styled from 'styled-components';
 import yaml from 'js-yaml';
 import close from '../../../../assets/close.png';
+import loading from '../../../../assets/loading.gif';
 import _ from 'lodash';
 
 import { ResourceType, ChartType, StorageType, Cluster } from '../../../../shared/types';
@@ -9,6 +10,7 @@ import { Context } from '../../../../shared/Context';
 import api from '../../../../shared/api';
 
 import TabRegion from '../../../../components/TabRegion';
+import StatusIndicator from '../../../../components/StatusIndicator';
 import RevisionSection from './RevisionSection';
 import ValuesYaml from './ValuesYaml';
 import GraphSection from './GraphSection';
@@ -40,12 +42,13 @@ type StateType = {
   saveValuesStatus: string | null,
   forceRefreshRevisions: boolean, // Update revisions after upgrading values
   controllers: Record<string, Record<string, any>>,
+  websockets: Record<string, any>,
 };
 
 export default class ExpandedChart extends Component<PropsType, StateType> {
   state = {
     loading: true,
-    showRevisions: false,
+    showRevisions: true,
     components: [] as ResourceType[],
     podSelectors: [] as string[],
     isPreview: false,
@@ -56,6 +59,7 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
     saveValuesStatus: null as (string | null),
     forceRefreshRevisions: false,
     controllers: {} as Record<string, Record<string, any>>,
+    websockets : {} as Record<string, any>,
   }
 
   // Retrieve full chart data (includes form and values)
@@ -124,6 +128,48 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
     })
   }
   
+  setupWebsocket = (kind: string, chart: ChartType) => {
+    let { currentCluster, currentProject } = this.context;
+    let protocol = process.env.NODE_ENV == 'production' ? 'wss' : 'ws';
+    let ws = new WebSocket(`${protocol}://${process.env.API_SERVER}/api/projects/${currentProject.id}/k8s/${kind}/status?cluster_id=${currentCluster.id}`);
+    ws.onopen = () => {
+      console.log('connected to websocket');
+    }
+
+    ws.onmessage = (evt: MessageEvent) => {
+      let event = JSON.parse(evt.data);
+      let object = event.Object;
+      object.metadata.kind = event.Kind
+      
+      if (!this.state.controllers[object.metadata.uid]) return;
+
+      this.setState({
+        controllers: {
+          ...this.state.controllers,
+          [object.metadata.uid]: object
+        }
+      })
+    }
+
+    ws.onclose = () => {
+      console.log('closing websocket');
+    }
+
+    ws.onerror = (err: ErrorEvent) => {
+      console.log(err);
+      ws.close();
+    }
+
+    return ws;
+  }
+
+  setControllerWebsockets = (controller_types: any[], chart: ChartType) => {
+    let websockets = controller_types.map((kind: string) => {
+      return this.setupWebsocket(kind, chart);
+    })
+    this.setState({ websockets });
+  }
+
   updateResources = () => {
     let { currentCluster, currentProject } = this.context;
     let { currentChart } = this.props;
@@ -147,19 +193,6 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
 
   refreshChart = () => this.getChartData(this.props.currentChart);
 
-  componentDidMount() {
-    this.getChartData(this.props.currentChart);
-    this.getControllers(this.props.currentChart)
-  }
-
-  componentDidUpdate(prevProps: PropsType) {
-    /*
-    if (this.props.currentChart !== prevProps.currentChart) {
-      this.updateResources();
-    }
-    */
-  }
-
   onSubmit = (rawValues: any) => {
     let { currentProject, currentCluster, setCurrentError } = this.context;
 
@@ -341,37 +374,34 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
     return `${time} on ${date}`;
   }
 
-  getChartStatus = (chartStatus: string) => {
-    if (chartStatus === 'deployed') {
+  componentDidMount() {
+    this.getChartData(this.props.currentChart);
+    this.getControllers(this.props.currentChart)
+    this.setControllerWebsockets(
+      ["deployment", "statefulset", "daemonset", "replicaset"],
+      this.props.currentChart 
+    );
+  }
 
-      for (var uid in this.state.controllers) {
-        let value = this.state.controllers[uid]
-        let status = this.getAvailability(value.metadata.kind, value)
-        if (!status) {
-          return 'not ready'
-        }
-      }
-      return 'deployed'
+  componentDidUpdate(prevProps: PropsType) {
+    /*
+    if (this.props.currentChart !== prevProps.currentChart) {
+      this.updateResources();
     }
-    return chartStatus
+    */
   }
 
-  getAvailability = (kind: string, c: any) => {
-    switch (kind?.toLowerCase()) {
-      case "deployment":
-      case "replicaset":
-        return (c.status.availableReplicas == c.status.replicas)
-      case "statefulset":
-       return (c.status.readyReplicas == c.status.replicas)
-      case "daemonset":
-        return (c.status.numberAvailable == c.status.desiredNumberScheduled)
-      }
+  componentWillUnmount() {
+    if (this.state.websockets) {
+      this.state.websockets.forEach((ws: WebSocket) => {
+        ws.close()
+      })
+    }
   }
 
   render() {
     let { currentChart, setCurrentChart } = this.props;
     let chart = currentChart;
-    let status = this.getChartStatus(chart.info.status)
 
     return ( 
       <div>
@@ -384,9 +414,11 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
               </Title>
 
               <InfoWrapper>
-                <StatusIndicator>
-                  <StatusColor status={status} />{status}
-                </StatusIndicator>
+                <StatusIndicator 
+                  controllers={this.state.controllers}
+                  status={chart.info.status}
+                  margin_left={'0px'}
+                />
                 <LastDeployed>
                   <Dot>•</Dot>Last deployed 
                   {' ' + this.readableDate(chart.info.last_deployed)}
@@ -406,6 +438,7 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
               showRevisions={this.state.showRevisions}
               toggleShowRevisions={() => this.setState({ showRevisions: !this.state.showRevisions })}
               chart={chart}
+              status={status}
               refreshChart={this.refreshChart}
               setRevision={this.setRevision}
               forceRefreshRevisions={this.state.forceRefreshRevisions}
@@ -489,15 +522,6 @@ const CloseOverlay = styled.div`
 const HeaderWrapper = styled.div`
 `;
 
-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;
 `;
@@ -550,23 +574,6 @@ const NamespaceTag = styled.div`
   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%;
 `;

+ 21 - 5
dashboard/src/main/home/cluster-dashboard/expanded-chart/RevisionSection.tsx

@@ -16,13 +16,14 @@ type PropsType = {
   setRevision: (x: ChartType, isCurrent?: boolean) => void
   forceRefreshRevisions: boolean,
   refreshRevisionsOff: () => void,
+  status: string,
 };
 
 type StateType = {
   revisions: ChartType[],
   rollbackRevision: number | null,
   loading: boolean,
-  maxVersion: number
+  maxVersion: number,
 };
 
 // TODO: handle refresh when new revision is generated from an old revision
@@ -114,6 +115,20 @@ export default class RevisionSection extends Component<PropsType, StateType> {
     }
   }
 
+  renderStatus = (revision: ChartType) => {
+    if (this.props.chart.version === revision.version && this.props.status == 'loading') {
+      return (
+        <div>
+          {this.props.status}
+          <LoadingGif src={loading} revision={true}/>
+        </div>
+      )
+    } else if (this.props.chart.version === revision.version) {
+      return this.props.status        
+    }
+    return revision.info.status    
+  }
+
   renderRevisionList = () => {
     return this.state.revisions.map((revision: ChartType, i: number) => {
       let isCurrent = revision.version === this.state.maxVersion;
@@ -125,7 +140,7 @@ export default class RevisionSection extends Component<PropsType, StateType> {
         >
           <Td>{revision.version}</Td>
           <Td>{this.readableDate(revision.info.last_deployed)}</Td>
-          <Td>{revision.info.status}</Td>
+          <Td>{this.renderStatus(revision)}</Td>
           <Td>
             <RollbackButton
               disabled={isCurrent}
@@ -164,7 +179,7 @@ export default class RevisionSection extends Component<PropsType, StateType> {
       return (
         <LoadingPlaceholder>
           <StatusWrapper>
-            <LoadingGif src={loading} /> Updating . . .
+            <LoadingGif src={loading} revision={false}/> Updating . . .
           </StatusWrapper>
         </LoadingPlaceholder>
       )
@@ -220,8 +235,9 @@ const LoadingPlaceholder = styled.div`
 const LoadingGif = styled.img`
   width: 15px;
   height: 15px;
-  margin-right: 9px;
-  margin-bottom: 0px;
+  margin-right: ${(props: {revision: boolean}) => props.revision ? '0px' : '9px'};
+  margin-left: ${(props: {revision: boolean}) => props.revision ? '10px' : '0px'};
+  margin-bottom: ${(props: {revision: boolean }) => props.revision ? '-2px' : '0px'};
 `;
 
 const StatusWrapper = styled.div`