Implement workflow execution UI and logic

This CL adds a provisional workflow execution UI (which will need to be
thoroughly improved in the future), and most importantly the logic for
running workflows and actions inside workflows.

Bug: twpowertools:74
Change-Id: I94944a623a2411bef9d2b5244fea707e69a49790
diff --git a/src/contentScripts/communityConsole/workflows/components/TwptWorkflowDialog.js b/src/contentScripts/communityConsole/workflows/components/TwptWorkflowDialog.js
new file mode 100644
index 0000000..9b9525f
--- /dev/null
+++ b/src/contentScripts/communityConsole/workflows/components/TwptWorkflowDialog.js
@@ -0,0 +1,76 @@
+import '@material/mwc-dialog/mwc-dialog.js';
+import '@material/web/button/text-button.js';
+
+import './TwptWorkflowProgress.js';
+
+import {css, html, LitElement} from 'lit';
+import {createRef, ref} from 'lit/directives/ref.js';
+
+import WorkflowRunner from '../runner.js';
+
+export default class TwptWorkflowDialog extends LitElement {
+  static properties = {
+    open: {type: Boolean},
+    workflow: {type: Object},
+    _runner: {type: Object, state: true},
+  };
+
+  static styles = css`
+    :host {
+      --mdc-dialog-content-ink-color: var(--mdc-theme-on-surface, #000);
+      --mdc-dialog-z-index: 200;
+    }
+
+    .workflow-name {
+      font-weight: 500;
+    }
+  `;
+
+  progressRef = createRef();
+
+  constructor() {
+    super();
+    this.open = false;
+  }
+
+  render() {
+    return html`
+      <mwc-dialog
+          ?open=${this.open}
+          @opening=${this._openingDialog}
+          @closing=${this._closingDialog}
+          heading=${'Running ' + this.workflow?.getName?.() + '...'}>
+        <twpt-workflow-progress ${ref(this.progressRef)}
+            .workflow=${this.workflow}
+            currentThread=${this._runner?.currentThreadIndex}
+            numThreads=${this._runner?.numThreads}
+            currentAction=${this._runner?.currentActionIndex}
+            status=${this._runner?.status}>
+        </twpt-workflow-progress>
+
+        <md-text-button
+            ?disabled=${this._runner?.status !== 'finished'}
+            slot="primaryAction"
+            dialogAction="cancel"
+            label="Close">
+        </md-text-button>
+      </mwc-dialog>
+    `;
+  }
+
+  start() {
+    this._runner =
+        new WorkflowRunner(this.workflow, () => this.requestUpdate());
+    this._runner.start();
+    this.open = true;
+  }
+
+  _openingDialog() {
+    this.open = true;
+  }
+
+  _closingDialog() {
+    this.open = false;
+  }
+}
+window.customElements.define('twpt-workflow-dialog', TwptWorkflowDialog);
diff --git a/src/contentScripts/communityConsole/workflows/components/TwptWorkflowProgress.js b/src/contentScripts/communityConsole/workflows/components/TwptWorkflowProgress.js
new file mode 100644
index 0000000..52245e0
--- /dev/null
+++ b/src/contentScripts/communityConsole/workflows/components/TwptWorkflowProgress.js
@@ -0,0 +1,70 @@
+import '@material/mwc-dialog/mwc-dialog.js';
+import '@material/web/button/filled-button.js';
+import '@material/web/button/text-button.js';
+
+import '../../../../workflows/manager/components/ActionEditor.js';
+
+import {css, html, LitElement} from 'lit';
+import {map} from 'lit/directives/map.js';
+
+export default class TwptWorkflowProgress extends LitElement {
+  static properties = {
+    workflow: {type: Object},
+    currentThread: {type: Number},
+    numThreads: {type: Number},
+    currentAction: {type: Number},
+    status: {type: String},
+  };
+
+  static styles = css`
+    .progressbar-container {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+    }
+  `;
+
+  renderThreadProgress() {
+    // @TODO: Improve this UI when the actions section is complete
+    return html`
+      <div class="progressbar-container">
+        <progress value=${this.currentThread + 1} max=${
+        this.numThreads}></progress>
+        <span>Thread ${this.currentThread + 1}/${this.numThreads}</span>
+      </div>
+      <p style="color: gray;">(Debug information) Status: ${this.status}</p>
+    `;
+  }
+
+  renderActions() {
+    const actions = this.workflow?.getActionsList?.() ?? [];
+    return map(actions, (action, i) => {
+      let status;
+      if (i > this.currentAction)
+        status = 'idle';
+      else if (i == this.currentAction && this.status == 'running')
+        status = 'running';
+      else if (i == this.currentAction && this.status == 'error')
+        status = 'error';
+      else
+        status = 'done';
+
+      return html`
+        <wf-action-editor
+            .action=${action}
+            readOnly
+            step=${i + 1}
+            status=${status}>
+        </wf-action-editor>
+      `;
+    });
+  }
+
+  render() {
+    return [
+      this.renderThreadProgress(),
+      this.renderActions(),
+    ];
+  }
+}
+window.customElements.define('twpt-workflow-progress', TwptWorkflowProgress);
diff --git a/src/contentScripts/communityConsole/workflows/components/index.js b/src/contentScripts/communityConsole/workflows/components/index.js
index 3041e41..1822edc 100644
--- a/src/contentScripts/communityConsole/workflows/components/index.js
+++ b/src/contentScripts/communityConsole/workflows/components/index.js
@@ -1,5 +1,6 @@
-import './TwptWorkflowsMenu.js';
 import './TwptConfirmDialog.js';
+import './TwptWorkflowDialog.js';
+import './TwptWorkflowsMenu.js';
 
 import {css, html, LitElement} from 'lit';
 import {createRef, ref} from 'lit/directives/ref.js';
@@ -52,14 +53,13 @@
   }
 
   _startWorkflow() {
-    // @TODO
-    console.log(`Start workflow ${this._selectedWorkflowUuid}!`);
+    this.workflowDialogRef.value.workflow = this._selectedWorkflow.cloneMessage();
+    this.workflowDialogRef.value.start();
   }
 
   get _selectedWorkflow() {
     for (const w of this._workflows) {
-      if (w.uuid == this._selectedWorkflowUuid)
-        return w.proto;
+      if (w.uuid == this._selectedWorkflowUuid) return w.proto;
     }
 
     return null;