[machines] Add clipboard-sk to the Machine ID column header.

Change-Id: I22ef10fd5e4ac3b94073c9e880862390bcb42fcd
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/608118
Commit-Queue: Ravi Mistry <rmistry@google.com>
Auto-Submit: Joe Gregorio <jcgregorio@google.com>
Reviewed-by: Ravi Mistry <rmistry@google.com>
diff --git a/machine/modules/machines-table-sk/machines-table-sk.ts b/machine/modules/machines-table-sk/machines-table-sk.ts
index a2249b9..d101ba7 100644
--- a/machine/modules/machines-table-sk/machines-table-sk.ts
+++ b/machine/modules/machines-table-sk/machines-table-sk.ts
@@ -294,19 +294,49 @@
 
   className: ((machine: FrontendDescription)=> string) | null;
 
-  constructor(name: string, row: (machine: FrontendDescription)=> TemplateResult, compare: compareFunc<FrontendDescription> | null, className: ((machine: FrontendDescription)=> string) | null = null) {
+  computedClipValue: (()=> Promise<string>) | null;
+
+  /** constructor
+   *
+   * name - The displayed name of the column.
+   * row - A function that emits the `td` HTML for each row in this column,
+   * compare - An optional comparison function for sorting based on the values in this column.
+   * className - An optional function to compute the class name to apply to each row's `td`.
+   * computedClipValue - An optional function that computes a value to send to the clipboard. A non-null
+   *    value will cause a clipboard-sk element to be added to the header.
+   */
+  constructor(
+    name: string,
+    row: (machine: FrontendDescription)=> TemplateResult,
+    compare: compareFunc<FrontendDescription> | null,
+    className: ((machine: FrontendDescription)=> string) | null = null,
+    computedClipValue: (()=> Promise<string>) | null = null,
+  ) {
     this.name = name;
     this.row = row;
     this.compare = compare;
     this.className = className;
+    this.computedClipValue = computedClipValue;
   }
 
   // eslint-disable-next-line no-use-before-define
   header(ele: MachinesTableSk): TemplateResult {
-    if (this.compare !== null) {
-      return html`<th>${this.name}&nbsp;${ele.sortArrow(this.compare)}</div></th>`;
+    return html`<th>${this.name}${this.optionalClipboard()}${this.optionalSortArrow(ele)}</div></th>`;
+  }
+
+  // eslint-disable-next-line no-use-before-define
+  optionalSortArrow(ele: MachinesTableSk): TemplateResult {
+    if (this.compare === null) {
+      return html``;
     }
-    return html`<th>${this.name}</th>`;
+    return html`&nbsp;${ele.sortArrow(this.compare)}`;
+  }
+
+  optionalClipboard(): TemplateResult {
+    if (this.computedClipValue === null) {
+      return html``;
+    }
+    return html`&nbsp;<clipboard-sk .calculatedValue=${this.computedClipValue}></clipboard-sk>`;
   }
 
   rowValue(machine: FrontendDescription): TemplateResult {
@@ -361,6 +391,8 @@
         'Machine',
         this.machineLink.bind(this),
         sortByMachineID,
+        null,
+        this.allDisplayedMachineIDs.bind(this),
       ),
       Attached: new Column(
         'Attached',
@@ -463,11 +495,19 @@
     };
   }
 
+  async allDisplayedMachineIDs(): Promise<string> {
+    return this.orderedFilteredRows().map((d: FrontendDescription) => d.Dimensions!.id![0]).join('\n');
+  }
+
+  private orderedFilteredRows(): FrontendDescription[] {
+    const ret = this.filterer.matchingValues();
+    ret.sort(this.sortHistory.compare.bind(this.sortHistory));
+    return ret;
+  }
+
   private tableRows(): TemplateResult[] {
-    const values = this.filterer.matchingValues();
-    values.sort(this.sortHistory.compare.bind(this.sortHistory));
     const ret: TemplateResult[] = [];
-    values.forEach((item) => ret.push(html`<tr>${this.tableRow(item)}</tr>`));
+    this.orderedFilteredRows().forEach((item) => ret.push(html`<tr>${this.tableRow(item)}</tr>`));
     return ret;
   }
 
diff --git a/machine/modules/machines-table-sk/machines-table-sk_test.ts b/machine/modules/machines-table-sk/machines-table-sk_test.ts
index 3731ea0..977cd43 100644
--- a/machine/modules/machines-table-sk/machines-table-sk_test.ts
+++ b/machine/modules/machines-table-sk/machines-table-sk_test.ts
@@ -19,6 +19,7 @@
 }
 
 const setUpElement = async (): Promise<MachinesTableSk> => {
+  await window.customElements.whenDefined('machines-table-sk');
   fetchMock.reset();
   fetchMock.config.overwriteRoutes = true;
   mockMachinesResponse([
@@ -59,6 +60,31 @@
   return element;
 };
 
+const fillWithTwoMachinesReturnedOutOfOrder = async (element: MachinesTableSk) => {
+  fetchMock.reset();
+  fetchMock.config.overwriteRoutes = true;
+  const machine1 = {
+    Dimensions: {
+      id: ['skia-rpi2-rack4-shelf1-002'],
+    },
+  };
+  const machine2 = {
+    Dimensions: {
+      id: ['skia-rpi2-rack4-shelf1-001'],
+    },
+  };
+
+  mockMachinesResponse([
+    machine1,
+    machine2,
+  ]);
+
+  await element.update();
+
+  // Wait for the initial fetch to finish.
+  await fetchMock.flush(true);
+};
+
 describe('machines-table-sk', () => {
   afterEach(() => {
     document.body.innerHTML = '';
@@ -454,4 +480,17 @@
       assert.equal(castFn(a, a), 0, 'sortByPowerCycle');
     });
   });
+
+  describe('allDisplayedMachineIDs', () => {
+    it('returns machine ids in sorted order', async () => {
+      const s = await setUpElement();
+      await fillWithTwoMachinesReturnedOutOfOrder(s);
+      const ids = await s.allDisplayedMachineIDs();
+      const expected = `skia-rpi2-rack4-shelf1-001
+skia-rpi2-rack4-shelf1-002`;
+
+      // Confirm the ids are in machine id sorted order, which is the default order.
+      assert.equal(ids, expected);
+    });
+  });
 });