Przeglądaj źródła

- Adds executions timeline view
- Fixes bug hot-reload on execution causing views to repeat

George Vrancianu 9 lat temu
rodzic
commit
7a35570fd0

+ 27 - 0
src/components/App/App.scss

@@ -462,6 +462,33 @@ button {
     }
   }
 }
+:global(.taskIcon) {
+  width: 16px;
+  height: 16px;
+  display: block;
+  float: left;
+  margin: 1px 8px 0 8px;
+  &:global(.COMPLETED) {
+    background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTZweCIgaGVpZ2h0PSIxNnB4IiB2aWV3Qm94PSIwIDAgMTYgMTYiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+DQogICAgPGcgaWQ9IlN5bWJvbHMiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPg0KICAgICAgICA8ZyBpZD0iSWNvbi1PayI+DQogICAgICAgICAgICA8Y2lyY2xlIGlkPSJPdmFsLTIiIGZpbGw9IiM0Q0Q5NjQiIGN4PSI4IiBjeT0iOCIgcj0iOCI+PC9jaXJjbGU+DQogICAgICAgICAgICA8cG9seWxpbmUgaWQ9IlN0cm9rZS0zIiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgcG9pbnRzPSIxMiA2IDcuMDgzMDc0MTkgMTEgNCA4LjAwMzk3NTQyIj48L3BvbHlsaW5lPg0KICAgICAgICA8L2c+DQogICAgPC9nPg0KPC9zdmc+');
+  }
+  &:global(.RUNNING) {
+    background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTZweCIgaGVpZ2h0PSIxNnB4IiB2aWV3Qm94PSIwIDAgMTYgMTYiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGcgaWQ9Ikdyb3VwLTciIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxjaXJjbGUgaWQ9Ik92YWwtMi1Db3B5IiBmaWxsPSIjQTRBQUI1IiBjeD0iOCIgY3k9IjgiIHI9IjgiPjwvY2lyY2xlPjxwYXRoIGQ9Ik0xNiw4IEMxNiwzLjU4MTcyMiAxMi40MTgyNzgsMCA4LDAgTDgsOCBMMTYsOCBaIiBpZD0iQ29tYmluZWQtU2hhcGUiIGZpbGw9IiMwMDU2QjgiPjwvcGF0aD48Y2lyY2xlIGlkPSJPdmFsLTItQ29weSIgZmlsbD0iI0ZGRkZGRiIgY3g9IjgiIGN5PSI4IiByPSI2Ij48L2NpcmNsZT48L2c+PC9zdmc+');
+    animation: rotate 2s infinite linear;
+  }
+  &:global(.WARNING) {
+    background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTZweCIgaGVpZ2h0PSIxNnB4IiB2aWV3Qm94PSIwIDAgMTYgMTYiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGNpcmNsZSBpZD0iT3ZhbC0yIiBzdHJva2U9Im5vbmUiIGZpbGw9IiNGREMwMkYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgY3g9IjgiIGN5PSI4IiByPSI4Ij48L2NpcmNsZT48cGF0aCBkPSJNOCw4IEw4LDQiIGlkPSJMaW5lLUNvcHkiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIGZpbGw9Im5vbmUiPjwvcGF0aD48cGF0aCBkPSJNOCwxMi4wMjU5NzQgTDgsMTEuMzMzMzMzMyIgaWQ9IkxpbmUtQ29weSIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgZmlsbD0ibm9uZSI+PC9wYXRoPjwvc3ZnPg==');
+  }
+  &:global(.CANCELED) {
+    background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTZweCIgaGVpZ2h0PSIxNnB4IiB2aWV3Qm94PSIwIDAgMTYgMTYiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGNpcmNsZSBpZD0iT3ZhbC0yIiBzdHJva2U9Im5vbmUiIGZpbGw9IiNGREMwMkYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgY3g9IjgiIGN5PSI4IiByPSI4Ij48L2NpcmNsZT48cGF0aCBkPSJNOCw4IEw4LDQiIGlkPSJMaW5lLUNvcHkiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIGZpbGw9Im5vbmUiPjwvcGF0aD48cGF0aCBkPSJNOCwxMi4wMjU5NzQgTDgsMTEuMzMzMzMzMyIgaWQ9IkxpbmUtQ29weSIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgZmlsbD0ibm9uZSI+PC9wYXRoPjwvc3ZnPg==');
+  }
+  &:global(.PENDING) {
+    background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTZweCIgaGVpZ2h0PSIxNnB4IiB2aWV3Qm94PSIwIDAgMTYgMTYiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGNpcmNsZSBpZD0iT3ZhbC0yLUNvcHkiIHN0cm9rZT0ibm9uZSIgZmlsbD0iI0M4Q0NENyIgZmlsbC1ydWxlPSJldmVub2RkIiBjeD0iOCIgY3k9IjgiIHI9IjgiPjwvY2lyY2xlPjxjaXJjbGUgaWQ9Ik92YWwtMi1Db3B5IiBzdHJva2U9Im5vbmUiIGZpbGw9Im5vbmUiIGN4PSI4IiBjeT0iOCIgcj0iNiI+PC9jaXJjbGU+PHBhdGggZD0iTTgsOCBMOCw0IiBpZD0iTGluZS1Db3B5LTIiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIGZpbGw9Im5vbmUiPjwvcGF0aD48cGF0aCBkPSJNOCw4IEwxMCwxMCIgaWQ9IkxpbmUtQ29weS0zIiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBmaWxsPSJub25lIj48L3BhdGg+PC9zdmc+');
+  }
+
+  &:global(.ERROR) {
+    background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTZweCIgaGVpZ2h0PSIxNnB4IiB2aWV3Qm94PSIwIDAgMTYgMTYiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGcgaWQ9IlN5bWJvbHMiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxnIGlkPSJJY29uLUVycm9yIj48Y2lyY2xlIGlkPSJPdmFsLTIiIGZpbGw9IiNFNjI1NjUiIGN4PSI4IiBjeT0iOCIgcj0iOCI+PC9jaXJjbGU+PHBhdGggZD0iTTExLjQyODU3MTQsNC41NzE0Mjg1NyBMNC41NzE0Mjg1NywxMS40Mjg1NzE0IiBpZD0iTGluZSIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCI+PC9wYXRoPjxwYXRoIGQ9Ik0xMS40Mjg1NzE0LDExLjQyODU3MTQgTDQuNTcxNDI4NTcsNC41NzE0Mjg1NyIgaWQ9IkxpbmUtQ29weSIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCI+PC9wYXRoPjwvZz48L2c+PC9zdmc+');
+  }
+}
 :global {
   .icon {
     display: inline-block;

+ 95 - 0
src/components/ExecutionsTimeline/ExecutionsTimeline.js

@@ -0,0 +1,95 @@
+/*
+Copyright (C) 2017  Cloudbase Solutions SRL
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+import React, { Component, PropTypes } from 'react';
+import s from './ExecutionsTimeline.scss';
+import withStyles from 'isomorphic-style-loader/lib/withStyles';
+import Moment from 'react-moment';
+import s2 from '../Tasks/Tasks.scss';
+
+const numberOfExecutions = 5;
+
+class ExecutionsTimeline extends Component {
+
+  static propTypes = {
+    executions: PropTypes.array,
+    currentExecution: PropTypes.object
+  }
+
+  static defaultProps = {
+    executions: null,
+    currentExecution: null
+  }
+
+  constructor(props) {
+    super(props)
+    this.state = {
+      visibleExecutions: []
+    }
+  }
+
+  componentWillMount() {
+    this.componentWillReceiveProps(this.props)
+  }
+
+  componentWillReceiveProps(newProps) {
+    let executions = this.props.executions
+    executions.sort((a, b) => {
+      return a.number - b.number;
+    })
+    let number = newProps.currentExecution.number - 1
+    let visibleExecutions = []
+    let count = 0
+    for (let i = number - ((numberOfExecutions - 1) / 2); i<= number + ((numberOfExecutions - 1) / 2); i++) {
+      visibleExecutions[count] = executions[i]
+      count++
+    }
+    this.setState({
+      visibleExecutions: visibleExecutions
+    })
+  }
+
+  render() {
+    let dotDistance = 100 / (numberOfExecutions + 1)
+    let executions = this.state.visibleExecutions.map((execution, index) => {
+      if (execution) {
+        let style = { left: dotDistance * (index + 1) + "%"}
+        return (
+          <div className={s.executionDot + (index == (numberOfExecutions - 1) / 2 ? (" " + s.current) : "")} style={style}>
+            <div className={"taskIcon " + execution.status}></div>
+            <Moment format="D MMM YYYY" date={execution.created_at} />
+          </div>
+        )
+      } else {
+        return null;
+      }
+
+    })
+    return <div className={s.root}>
+      <div className={s.root}>
+        <div className={s.line}>
+          <div className={s.progress}></div>
+
+        </div>
+        {executions}
+      </div>
+    </div>
+  }
+
+}
+
+export default withStyles(withStyles(ExecutionsTimeline, s2), s);

+ 48 - 0
src/components/ExecutionsTimeline/ExecutionsTimeline.scss

@@ -0,0 +1,48 @@
+/*
+Copyright (C) 2017  Cloudbase Solutions SRL
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+@import '../variables.scss';
+
+.root {
+  position: relative;
+  padding-bottom: 16px;
+  .line {
+    background-color: $execution-line-color;
+    height: 2px;
+    width: 100%;
+    position: absolute;
+    .progress {
+      color: $blue;
+      height: 2px;
+      width: 0px;
+    }
+  }
+  .executionDot {
+    position: absolute;
+    width: 88px;
+    margin-left: -44px;
+    margin-top: -8px;
+    text-align: center;
+    :global(.taskIcon) {
+      float: none;
+      margin: 0 auto;
+    }
+    &.current {
+      font-weight: 500;
+    }
+  }
+}

+ 6 - 0
src/components/ExecutionsTimeline/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "ExecutionsTimeline",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./ExecutionsTimeline.js"
+}

+ 3 - 0
src/components/ReplicaExecutions/ReplicaExecutions.js

@@ -23,8 +23,10 @@ import LoadingIcon from '../LoadingIcon';
 import moment from 'moment';
 import MigrationActions from '../../actions/MigrationActions';
 import Tasks from '../Tasks';
+import ExecutionsTimeline from '../ExecutionsTimeline';
 import {tasksPollTimeout} from '../../config'
 
+
 const title = 'Replica Executions';
 
 class ReplicaExecutions extends Component {
@@ -147,6 +149,7 @@ class ReplicaExecutions extends Component {
               value={this.state ? this.state.currentExecution : null}
               className={s.changeExecutionBtn}
             />
+            <ExecutionsTimeline executions={this.props.migration.executions} currentExecution={this.state.executionRef}/>
             <div className={s.executionsWrapper}>
               <div className={s.leftSide}>
                 <h4>Execution #{this.state.executionRef && this.state.executionRef.number}</h4>

+ 81 - 79
src/components/Tasks/Tasks.js

@@ -61,88 +61,90 @@ class Tasks extends Component {
 
   componentWillReceiveProps(newProps, oldProps) {
     let listItems = []
-    newProps.tasks.forEach((item) => {
-      let latestMessage
-      if (item.progress_updates.length && item.progress_updates[item.progress_updates.length - 1]) {
-        latestMessage = item.progress_updates[item.progress_updates.length - 1].message
-      } else {
-        latestMessage = "-"
-      }
-
-      let progressUpdates = []
-      if (item.progress_updates.length) {
-        let first = true
-
-        if (item.progress_updates[0] !== null) {
-          item.progress_updates.sort((a, b) => moment(a.created_at).isAfter(moment(b.created_at)))
+    if (newProps.tasks) {
+      newProps.tasks.forEach((item) => {
+        let latestMessage
+        if (item.progress_updates.length && item.progress_updates[item.progress_updates.length - 1]) {
+          latestMessage = item.progress_updates[item.progress_updates.length - 1].message
+        } else {
+          latestMessage = "-"
         }
-        for (let i = item.progress_updates.length - 1; i >= 0; i--) {
-          let date = "-"
-          if (item.progress_updates[i]) {
-            date = moment(item.progress_updates[i].created_at).format("YYYY-MM-DD HH:mm:ss")
-
-            progressUpdates.push(
-              <div key={"progress_" + i} className={first ? " first" : ""}>
-                <span>{date}</span>
-                <span>
-                  {item.progress_updates[i] && item.progress_updates[i].message}
-                  {hasProgress(item.progress_updates[i].message) &&
+
+        let progressUpdates = []
+        if (item.progress_updates.length) {
+          let first = true
+
+          if (item.progress_updates[0] !== null) {
+            item.progress_updates.sort((a, b) => moment(a.created_at).isAfter(moment(b.created_at)))
+          }
+          for (let i = item.progress_updates.length - 1; i >= 0; i--) {
+            let date = "-"
+            if (item.progress_updates[i]) {
+              date = moment(item.progress_updates[i].created_at).format("YYYY-MM-DD HH:mm:ss")
+
+              progressUpdates.push(
+                <div key={"progress_" + i} className={first ? " first" : ""}>
+                  <span>{date}</span>
+                  <span>
+                    {item.progress_updates[i] && item.progress_updates[i].message}
+                    {hasProgress(item.progress_updates[i].message) &&
                     <ProgressBar progress={hasProgress(item.progress_updates[i].message)}/>
-                  }
-                </span>
-              </div>)
-            first = false
+                    }
+                  </span>
+                </div>)
+              first = false
+            }
           }
-        }
-        if (progressUpdates.length == 0) {
+          if (progressUpdates.length == 0) {
+            progressUpdates = "N/A"
+          }
+        } else {
           progressUpdates = "N/A"
         }
-      } else {
-        progressUpdates = "N/A"
-      }
-
-      let taskDetails = (<div className={s.taskDetails}>
-        <div className={s.group}>
-          <div className={s.detailTitle}>Status</div>
-          <div className={s.detailValue}><span className={"status-pill " + item.status}>{item.status}</span></div>
-        </div>
-        <div className={s.group}>
-          <div className={s.detailTitle}>ID</div>
-          <div className={s.detailValue}>{item.id}</div>
-        </div>
-        <div className={s.group}>
-          <div className={s.detailTitle}>Exception details</div>
-          <div className={s.detailValue}>
-            {item.exception_details && item.exception_details.length ?
-              (<TextTruncate line={10} text={item.exception_details} truncateText="..." />) : "N/A"}
+
+        let taskDetails = (<div className={s.taskDetails}>
+          <div className={s.group}>
+            <div className={s.detailTitle}>Status</div>
+            <div className={s.detailValue}><span className={"status-pill " + item.status}>{item.status}</span></div>
+          </div>
+          <div className={s.group}>
+            <div className={s.detailTitle}>ID</div>
+            <div className={s.detailValue}>{item.id}</div>
           </div>
-        </div>
-        <div className={s.group}>
-          <div className={s.detailTitle}>Depends on</div>
-          <div className={s.detailValue}>{item.depends_on && item.depends_on[0] ? item.depends_on[0] : "N/A"}</div>
-        </div>
-        <div className={s.group + " " + s.progressUpdates}>
-          <div className={s.detailTitle}>Progress Updates</div>
-          <div className={s.detailValue}>{progressUpdates}</div>
-        </div>
-      </div>)
-
-      let taskType = item.task_type.replace(/_/g, " ").toLowerCase()
-      taskType = taskType.charAt(0).toUpperCase() + taskType.slice(1)
-
-      let newItem = {
-        task_type: (<span>
-          <span className={"taskIcon " + item.status} />
-          <TextTruncate line={1} truncateText="..." text={taskType} />
-        </span>),
-        instance: <TextTruncate line={1} text={item.instance} truncateText="..." />,
-        latest_message: <TextTruncate line={1} truncateText="..." text={latestMessage} />,
-        timestamp: item.updated_at ? moment(item.updated_at).format("YYYY-MM-DD HH:mm:ss") : "-",
-        detailView: taskDetails,
-        openState: item.status === 'RUNNING'
-      }
-      listItems.push(newItem)
-    }, this)
+          <div className={s.group}>
+            <div className={s.detailTitle}>Exception details</div>
+            <div className={s.detailValue}>
+              {item.exception_details && item.exception_details.length ?
+                (<TextTruncate line={10} text={item.exception_details} truncateText="..."/>) : "N/A"}
+            </div>
+          </div>
+          <div className={s.group}>
+            <div className={s.detailTitle}>Depends on</div>
+            <div className={s.detailValue}>{item.depends_on && item.depends_on[0] ? item.depends_on[0] : "N/A"}</div>
+          </div>
+          <div className={s.group + " " + s.progressUpdates}>
+            <div className={s.detailTitle}>Progress Updates</div>
+            <div className={s.detailValue}>{progressUpdates}</div>
+          </div>
+        </div>)
+
+        let taskType = item.task_type.replace(/_/g, " ").toLowerCase()
+        taskType = taskType.charAt(0).toUpperCase() + taskType.slice(1)
+
+        let newItem = {
+          task_type: (<span>
+            <span className={"taskIcon " + item.status}/>
+            <TextTruncate line={1} truncateText="..." text={taskType}/>
+          </span>),
+          instance: <TextTruncate line={1} text={item.instance} truncateText="..."/>,
+          latest_message: <TextTruncate line={1} truncateText="..." text={latestMessage}/>,
+          timestamp: item.updated_at ? moment(item.updated_at).format("YYYY-MM-DD HH:mm:ss") : "-",
+          detailView: taskDetails,
+          openState: item.status === 'RUNNING'
+        }
+        listItems.push(newItem)
+      }, this)
+    }
     this.setState({ listItems: listItems })
   }
 
@@ -150,15 +152,15 @@ class Tasks extends Component {
   render() {
     return (
       <div className={s.root}>
-        { this.state &&
-          <div className={s.container}>
+        { this.state ?
+          (<div className={s.container}>
             <Table
               headerItems={this.headers}
               listItems={this.state ? this.state.listItems : null}
               customClassName={s.table}
               show={this.state !== null}
             />
-          </div>
+          </div>) : <LoadingIcon/>
         }
       </div>
     );

+ 0 - 27
src/components/Tasks/Tasks.scss

@@ -56,33 +56,6 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
       background-color: $task-gray;
     }
   }
-  :global(.taskIcon) {
-    width: 16px;
-    height: 16px;
-    display: block;
-    float: left;
-    margin: 1px 8px 0 8px;
-    &:global(.COMPLETED) {
-      background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTZweCIgaGVpZ2h0PSIxNnB4IiB2aWV3Qm94PSIwIDAgMTYgMTYiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+DQogICAgPGcgaWQ9IlN5bWJvbHMiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPg0KICAgICAgICA8ZyBpZD0iSWNvbi1PayI+DQogICAgICAgICAgICA8Y2lyY2xlIGlkPSJPdmFsLTIiIGZpbGw9IiM0Q0Q5NjQiIGN4PSI4IiBjeT0iOCIgcj0iOCI+PC9jaXJjbGU+DQogICAgICAgICAgICA8cG9seWxpbmUgaWQ9IlN0cm9rZS0zIiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgcG9pbnRzPSIxMiA2IDcuMDgzMDc0MTkgMTEgNCA4LjAwMzk3NTQyIj48L3BvbHlsaW5lPg0KICAgICAgICA8L2c+DQogICAgPC9nPg0KPC9zdmc+');
-    }
-    &:global(.RUNNING) {
-      background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTZweCIgaGVpZ2h0PSIxNnB4IiB2aWV3Qm94PSIwIDAgMTYgMTYiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGcgaWQ9Ikdyb3VwLTciIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxjaXJjbGUgaWQ9Ik92YWwtMi1Db3B5IiBmaWxsPSIjQTRBQUI1IiBjeD0iOCIgY3k9IjgiIHI9IjgiPjwvY2lyY2xlPjxwYXRoIGQ9Ik0xNiw4IEMxNiwzLjU4MTcyMiAxMi40MTgyNzgsMCA4LDAgTDgsOCBMMTYsOCBaIiBpZD0iQ29tYmluZWQtU2hhcGUiIGZpbGw9IiMwMDU2QjgiPjwvcGF0aD48Y2lyY2xlIGlkPSJPdmFsLTItQ29weSIgZmlsbD0iI0ZGRkZGRiIgY3g9IjgiIGN5PSI4IiByPSI2Ij48L2NpcmNsZT48L2c+PC9zdmc+');
-      animation: rotate 2s infinite linear;
-    }
-    &:global(.WARNING) {
-      background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTZweCIgaGVpZ2h0PSIxNnB4IiB2aWV3Qm94PSIwIDAgMTYgMTYiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGNpcmNsZSBpZD0iT3ZhbC0yIiBzdHJva2U9Im5vbmUiIGZpbGw9IiNGREMwMkYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgY3g9IjgiIGN5PSI4IiByPSI4Ij48L2NpcmNsZT48cGF0aCBkPSJNOCw4IEw4LDQiIGlkPSJMaW5lLUNvcHkiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIGZpbGw9Im5vbmUiPjwvcGF0aD48cGF0aCBkPSJNOCwxMi4wMjU5NzQgTDgsMTEuMzMzMzMzMyIgaWQ9IkxpbmUtQ29weSIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgZmlsbD0ibm9uZSI+PC9wYXRoPjwvc3ZnPg==');
-    }
-    &:global(.CANCELED) {
-      background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTZweCIgaGVpZ2h0PSIxNnB4IiB2aWV3Qm94PSIwIDAgMTYgMTYiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGNpcmNsZSBpZD0iT3ZhbC0yIiBzdHJva2U9Im5vbmUiIGZpbGw9IiNGREMwMkYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgY3g9IjgiIGN5PSI4IiByPSI4Ij48L2NpcmNsZT48cGF0aCBkPSJNOCw4IEw4LDQiIGlkPSJMaW5lLUNvcHkiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIGZpbGw9Im5vbmUiPjwvcGF0aD48cGF0aCBkPSJNOCwxMi4wMjU5NzQgTDgsMTEuMzMzMzMzMyIgaWQ9IkxpbmUtQ29weSIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgZmlsbD0ibm9uZSI+PC9wYXRoPjwvc3ZnPg==');
-    }
-    &:global(.PENDING) {
-      background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTZweCIgaGVpZ2h0PSIxNnB4IiB2aWV3Qm94PSIwIDAgMTYgMTYiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGNpcmNsZSBpZD0iT3ZhbC0yLUNvcHkiIHN0cm9rZT0ibm9uZSIgZmlsbD0iI0M4Q0NENyIgZmlsbC1ydWxlPSJldmVub2RkIiBjeD0iOCIgY3k9IjgiIHI9IjgiPjwvY2lyY2xlPjxjaXJjbGUgaWQ9Ik92YWwtMi1Db3B5IiBzdHJva2U9Im5vbmUiIGZpbGw9Im5vbmUiIGN4PSI4IiBjeT0iOCIgcj0iNiI+PC9jaXJjbGU+PHBhdGggZD0iTTgsOCBMOCw0IiBpZD0iTGluZS1Db3B5LTIiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIGZpbGw9Im5vbmUiPjwvcGF0aD48cGF0aCBkPSJNOCw4IEwxMCwxMCIgaWQ9IkxpbmUtQ29weS0zIiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBmaWxsPSJub25lIj48L3BhdGg+PC9zdmc+');
-    }
-
-    &:global(.ERROR) {
-      background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTZweCIgaGVpZ2h0PSIxNnB4IiB2aWV3Qm94PSIwIDAgMTYgMTYiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGcgaWQ9IlN5bWJvbHMiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxnIGlkPSJJY29uLUVycm9yIj48Y2lyY2xlIGlkPSJPdmFsLTIiIGZpbGw9IiNFNjI1NjUiIGN4PSI4IiBjeT0iOCIgcj0iOCI+PC9jaXJjbGU+PHBhdGggZD0iTTExLjQyODU3MTQsNC41NzE0Mjg1NyBMNC41NzE0Mjg1NywxMS40Mjg1NzE0IiBpZD0iTGluZSIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCI+PC9wYXRoPjxwYXRoIGQ9Ik0xMS40Mjg1NzE0LDExLjQyODU3MTQgTDQuNTcxNDI4NTcsNC41NzE0Mjg1NyIgaWQ9IkxpbmUtQ29weSIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCI+PC9wYXRoPjwvZz48L2c+PC9zdmc+');
-    }
-  }
   :global(.row.isOpen) {
     background-color: $task-gray;
   }

+ 1 - 0
src/components/variables.scss

@@ -19,6 +19,7 @@ $color-migration:       $blue;
 
 $task-gray-light: #7F8795;
 $task-gray: #D9DCE3;
+$execution-line-color: #C8CCD7;
 /*
  * Typography
  * ========================================================================== */