Merge branch '21204-stable-log-sort-from-wb2-repo' into main. Closes #21204
authorStephen Smith <stephen@curii.com>
Mon, 11 Dec 2023 17:25:43 +0000 (12:25 -0500)
committerStephen Smith <stephen@curii.com>
Mon, 11 Dec 2023 17:25:43 +0000 (12:25 -0500)
Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen@curii.com>

services/workbench2/Makefile
services/workbench2/cypress/integration/process.spec.js
services/workbench2/package.json
services/workbench2/src/store/process-logs-panel/process-logs-panel-actions.ts
services/workbench2/yarn.lock

index 7e70f3ec9d397ddb6af87b4cc1886044bca1dc5f..0d402a63c14465417727843f9239d395c3a8787b 100644 (file)
@@ -174,8 +174,11 @@ packages-in-docker: check-arvados-directory workbench2-build-image
        docker run -t --rm --env ci="true" \
                --env ARVADOS_DIRECTORY=/tmp/arvados \
                --env APP_NAME=${APP_NAME} \
+               --env VERSION="${VERSION}" \
                --env ITERATION=${ITERATION} \
                --env TARGETS="${TARGETS}" \
+               --env MAINTAINER="${MAINTAINER}" \
+               --env DESCRIPTION="${DESCRIPTION}" \
                --env GIT_DISCOVERY_ACROSS_FILESYSTEM=1 \
                -w "/tmp/workbench2" \
                -v ${WORKSPACE}:/tmp/workbench2 \
index 438acbf14dca2608ddf23b9dc3822d4aedbf3dd2..38d9794b18573a0379a3bbba93f53f0b661abe83 100644 (file)
@@ -556,6 +556,53 @@ describe("Process tests", function () {
             });
         });
 
+        it("preserves original ordering of lines within the same log type", function () {
+            const crName = "test_container_request";
+            createContainerRequest(activeUser, crName, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
+                cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [
+                    // Should come first
+                    "2023-07-18T20:14:46.000000000Z A out 1",
+                    // Comes fourth in a contiguous block
+                    "2023-07-18T20:14:48.128642814Z A out 2",
+                    "2023-07-18T20:14:48.128642814Z X out 3",
+                    "2023-07-18T20:14:48.128642814Z A out 4",
+                ]).as("stdout");
+
+                cy.appendLog(adminUser.token, containerRequest.uuid, "stderr.txt", [
+                    // Comes second
+                    "2023-07-18T20:14:47.000000000Z Z err 1",
+                    // Comes third in a contiguous block
+                    "2023-07-18T20:14:48.128642814Z B err 2",
+                    "2023-07-18T20:14:48.128642814Z C err 3",
+                    "2023-07-18T20:14:48.128642814Z Y err 4",
+                    "2023-07-18T20:14:48.128642814Z Z err 5",
+                    "2023-07-18T20:14:48.128642814Z A err 6",
+                ]).as("stderr");
+
+                cy.loginAs(activeUser);
+                cy.goToPath(`/processes/${containerRequest.uuid}`);
+                cy.get("[data-cy=process-details]").should("contain", crName);
+                cy.get("[data-cy=process-logs]").should("contain", "No logs yet");
+
+                cy.getAll("@stdout", "@stderr").then(() => {
+                    // Switch to All logs
+                    cy.get("[data-cy=process-logs-filter]").click();
+                    cy.get("body").contains("li", "All logs").click();
+                    // Verify sorted logs
+                    cy.get("[data-cy=process-logs] pre").eq(0).should("contain", "2023-07-18T20:14:46.000000000Z A out 1");
+                    cy.get("[data-cy=process-logs] pre").eq(1).should("contain", "2023-07-18T20:14:47.000000000Z Z err 1");
+                    cy.get("[data-cy=process-logs] pre").eq(2).should("contain", "2023-07-18T20:14:48.128642814Z B err 2");
+                    cy.get("[data-cy=process-logs] pre").eq(3).should("contain", "2023-07-18T20:14:48.128642814Z C err 3");
+                    cy.get("[data-cy=process-logs] pre").eq(4).should("contain", "2023-07-18T20:14:48.128642814Z Y err 4");
+                    cy.get("[data-cy=process-logs] pre").eq(5).should("contain", "2023-07-18T20:14:48.128642814Z Z err 5");
+                    cy.get("[data-cy=process-logs] pre").eq(6).should("contain", "2023-07-18T20:14:48.128642814Z A err 6");
+                    cy.get("[data-cy=process-logs] pre").eq(7).should("contain", "2023-07-18T20:14:48.128642814Z A out 2");
+                    cy.get("[data-cy=process-logs] pre").eq(8).should("contain", "2023-07-18T20:14:48.128642814Z X out 3");
+                    cy.get("[data-cy=process-logs] pre").eq(9).should("contain", "2023-07-18T20:14:48.128642814Z A out 4");
+                });
+            });
+        });
+
         it("correctly generates sniplines", function () {
             const SNIPLINE = `================ ✀ ================ ✀ ========= Some log(s) were skipped ========= ✀ ================ ✀ ================`;
             const crName = "test_container_request";
index d21d067540ce2b6d18954ed01c36825f56b02c3a..abb204907be5b5ac12aa25c994d4473cb18a2800 100644 (file)
@@ -3,8 +3,8 @@
   "version": "0.1.0",
   "private": true,
   "dependencies": {
-    "@coreui/coreui": "next",
-    "@coreui/react": "next",
+    "@coreui/coreui": "^4.3.2",
+    "@coreui/react": "^4.11.0",
     "@date-io/date-fns": "1",
     "@fortawesome/fontawesome-svg-core": "1.2.28",
     "@fortawesome/free-solid-svg-icons": "5.13.0",
index 4e52431eebadc9a05b240ee530cd29e3390569e4..88b56a2c324379c2b9be44c859da7ad9e05eb28e 100644 (file)
@@ -33,6 +33,12 @@ type FileWithProgress = {
     lastByte: number;
 }
 
+type SortableLine = {
+    logType: LogEventType,
+    timestamp: string;
+    contents: string;
+}
+
 export type ProcessLogsPanelAction = UnionOf<typeof processLogsPanelActions>;
 
 export const setProcessLogsPanelFilter = (filter: string) =>
@@ -257,18 +263,61 @@ const mergeMultilineLoglines = (logFragments: LogFragment[]) => (
  * @returns string[] of merged and sorted log lines
  */
 const mergeSortLogFragments = (logFragments: LogFragment[]): string[] => {
-    const sortableLines = fragmentsToLines(logFragments
-        .filter((fragment) => (!NON_SORTED_LOG_TYPES.includes(fragment.logType))));
+    const sortableFragments = logFragments
+        .filter((fragment) => (!NON_SORTED_LOG_TYPES.includes(fragment.logType)));
 
     const nonSortableLines = fragmentsToLines(logFragments
         .filter((fragment) => (NON_SORTED_LOG_TYPES.includes(fragment.logType)))
         .sort((a, b) => (a.logType.localeCompare(b.logType))));
 
-    return [...nonSortableLines, ...sortableLines.sort(sortLogLines)]
+    return [...nonSortableLines, ...sortLogFragments(sortableFragments)];
 };
 
-const sortLogLines = (a: string, b: string) => {
-    return a.localeCompare(b);
+/**
+ * Performs merge and sort of input log fragment lines
+ * @param logFragments set of sortable log fragments to be merged and sorted
+ * @returns A string array containing all lines, sorted by timestamp and
+ *          preserving line ordering and type grouping when timestamps match
+ */
+const sortLogFragments = (logFragments: LogFragment[]): string[] => {
+    const linesWithType: SortableLine[] = logFragments
+        // Map each logFragment into an array of SortableLine
+        .map((fragment: LogFragment): SortableLine[] => (
+            fragment.contents.map((singleLine: string) => {
+                const timestampMatch = singleLine.match(LOG_TIMESTAMP_PATTERN);
+                const timestamp = timestampMatch && timestampMatch[0] ? timestampMatch[0] : "";
+                return {
+                    logType: fragment.logType,
+                    timestamp: timestamp,
+                    contents: singleLine,
+                };
+            })
+        // Merge each array of SortableLine into single array
+        )).reduce((acc: SortableLine[], lines: SortableLine[]) => (
+            [...acc, ...lines]
+        ), [] as SortableLine[]);
+
+    return linesWithType
+        .sort(sortableLineSortFunc)
+        .map(lineWithType => lineWithType.contents);
+};
+
+/**
+ * Sort func to sort lines
+ *   Preserves original ordering of lines from the same source
+ *   Stably orders lines of differing type but same timestamp
+ *     (produces a block of same-timestamped lines of one type before a block
+ *     of same timestamped lines of another type for readability)
+ *   Sorts all other lines by contents (ie by timestamp)
+ */
+const sortableLineSortFunc = (a: SortableLine, b: SortableLine) => {
+    if (a.logType === b.logType) {
+        return 0;
+    } else if (a.timestamp === b.timestamp) {
+        return a.logType.localeCompare(b.logType);
+    } else {
+        return a.contents.localeCompare(b.contents);
+    }
 };
 
 const fragmentsToLines = (fragments: LogFragment[]): string[] => (
index ef7b9d31eb50f895ec0635f10ac33a9dfe559d24..2e0c4f2a1e2dd0b18d9e71faf93c262cc90f40cd 100644 (file)
@@ -1646,23 +1646,25 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@coreui/coreui@npm:next":
-  version: 5.0.0-alpha.3
-  resolution: "@coreui/coreui@npm:5.0.0-alpha.3"
+"@coreui/coreui@npm:^4.3.2":
+  version: 4.3.2
+  resolution: "@coreui/coreui@npm:4.3.2"
+  dependencies:
+    postcss-combine-duplicated-selectors: ^10.0.3
   peerDependencies:
-    "@popperjs/core": ^2.11.8
-  checksum: 2363ad6be775c6a895a49126a5b9062ffa9ebd0bea6dfb835c1300cd122fb1cf18d85fe647a9c08a3a384caa871e761d8ffb28ea45c7872cb2b034df6527da20
+    "@popperjs/core": ^2.11.6
+  checksum: 88fc70f4f681bb796e1d81ca8472a3d36bfcf92866fc7c6810ead850bc371c99bca123a94abb0fafdf2935972d130005cd62b485406631cfd9abd8f38e14be15
   languageName: node
   linkType: hard
 
-"@coreui/react@npm:next":
-  version: 5.0.0-alpha.3
-  resolution: "@coreui/react@npm:5.0.0-alpha.3"
+"@coreui/react@npm:^4.11.0":
+  version: 4.11.0
+  resolution: "@coreui/react@npm:4.11.0"
   peerDependencies:
-    "@coreui/coreui": ^5.0.0-alpha.2
+    "@coreui/coreui": 4.3.0
     react: ">=17"
     react-dom: ">=17"
-  checksum: efd333cc346307219dcf7fe183eed65305b12e71984bcb940d80a55509d7b92523082e37045bfcb8c4b334920ca185128a9f72f3e8bec69d15cad889cbeda4b4
+  checksum: 75c9394125e41e24fb5855b82cba93c9abeea080f9ee5bcc063ff2e581318b85c5bbef6f2c5300f5fd7a3450743488daa29b4baee6feabec38a009a452876a88
   languageName: node
   linkType: hard
 
@@ -3820,8 +3822,8 @@ __metadata:
   version: 0.0.0-use.local
   resolution: "arvados-workbench-2@workspace:."
   dependencies:
-    "@coreui/coreui": next
-    "@coreui/react": next
+    "@coreui/coreui": ^4.3.2
+    "@coreui/react": ^4.11.0
     "@date-io/date-fns": 1
     "@fortawesome/fontawesome-svg-core": 1.2.28
     "@fortawesome/free-solid-svg-icons": 5.13.0
@@ -14138,6 +14140,17 @@ __metadata:
   languageName: node
   linkType: hard
 
+"postcss-combine-duplicated-selectors@npm:^10.0.3":
+  version: 10.0.3
+  resolution: "postcss-combine-duplicated-selectors@npm:10.0.3"
+  dependencies:
+    postcss-selector-parser: ^6.0.4
+  peerDependencies:
+    postcss: ^8.1.0
+  checksum: 45c3dff41d0cddb510752ed92fe8c7fc66e5cf88f4988314655419d3ecdf1dc66f484a25ee73f4f292da5da851a0fdba0ec4d59bdedeee935d05b26d31d997ed
+  languageName: node
+  linkType: hard
+
 "postcss-convert-values@npm:^4.0.1":
   version: 4.0.1
   resolution: "postcss-convert-values@npm:4.0.1"
@@ -14782,6 +14795,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"postcss-selector-parser@npm:^6.0.4":
+  version: 6.0.13
+  resolution: "postcss-selector-parser@npm:6.0.13"
+  dependencies:
+    cssesc: ^3.0.0
+    util-deprecate: ^1.0.2
+  checksum: f89163338a1ce3b8ece8e9055cd5a3165e79a15e1c408e18de5ad8f87796b61ec2d48a2902d179ae0c4b5de10fccd3a325a4e660596549b040bc5ad1b465f096
+  languageName: node
+  linkType: hard
+
 "postcss-svgo@npm:^4.0.3":
   version: 4.0.3
   resolution: "postcss-svgo@npm:4.0.3"