diff --git a/.evergreen/attach.yml b/.evergreen/attach.yml index 1799ec5d9..7f74528e1 100644 --- a/.evergreen/attach.yml +++ b/.evergreen/attach.yml @@ -167,4 +167,4 @@ functions: command: attach.xunit_results params: files: - - "./ui/${app_dir}/bin/jest/*.xml" + - "./ui/bin/jest/*.xml" diff --git a/.evergreen/shared.yml b/.evergreen/shared.yml index ba3e62eaf..a46336f85 100644 --- a/.evergreen/shared.yml +++ b/.evergreen/shared.yml @@ -67,21 +67,21 @@ functions: params: working_dir: ui/logkeeper background: true - script: go run main/logkeeper.go --localPath _bucketdata + script: go run main/logkeeper.go --localPath ../evergreen/_bucketdata shell: bash env: GOROOT: ${goroot} PATH: ${goroot}/bin:$PATH LK_CORS_ORIGINS: http:\/\/localhost:\d+ - seed-logkeeper: + seed-bucket-data: command: s3.get type: setup params: aws_key: ${AWS_ACCESS_KEY_ID} aws_secret: ${AWS_SECRET_ACCESS_KEY} aws_session_token: ${AWS_SESSION_TOKEN} - extract_to: ui/logkeeper + extract_to: ui/evergreen remote_file: _bucketdata.tar.gz bucket: parsley-test @@ -310,10 +310,10 @@ functions: yarn-test: command: shell.exec params: - working_dir: ui/${app_dir} + working_dir: ui script: | ${PREPARE_SHELL} - yarn test --ci --testPathIgnorePatterns=snapshot.test.ts + yarn test --ci --testPathIgnorePatterns=snapshot.test.ts --selectProjects ${build_variant} yarn-tsc: command: shell.exec @@ -361,7 +361,7 @@ tasks: - func: run-make-background vars: target: local-evergreen - - func: seed-logkeeper + - func: seed-bucket-data - func: run-logkeeper - func: yarn-build - func: yarn-preview @@ -376,7 +376,7 @@ tasks: vars: target: local-evergreen - func: symlink - - func: seed-logkeeper + - func: seed-bucket-data - func: run-logkeeper - func: yarn-build - func: yarn-serve diff --git a/.gitignore b/.gitignore index 3c3629e64..6e9e9ab13 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ node_modules + +# artifacts +bin diff --git a/.graphqlrc b/.graphqlrc new file mode 100644 index 000000000..7385d7966 --- /dev/null +++ b/.graphqlrc @@ -0,0 +1,2 @@ +schema: "apps/*/sdlschema/**/*.graphql" +documents: "apps/*/**/src/gql/**/*.graphql" diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 000000000..372362317 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +yarn lint-staged diff --git a/README.md b/README.md index 8147c26c4..27837d521 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,37 @@ # Evergreen UI The new home of [Spruce](/apps/spruce) and [Parsley](/apps/parsley). + +## Monorepo Tips & Tricks + +Check out the [Yarn Workspaces documentation](https://classic.yarnpkg.com/lang/en/docs/workspaces/) for more. + +### Upgrades + +To upgrade a dependency across workspaces: + +```bash +yarn upgrade-interactive [--latest] [package-name] +``` + +### Scripts + +To run a script in a workspace from root: + +```bash +yarn workspace [workspace-name] run [script-name] +``` + +For example, `yarn workspace spruce run storybook`. + +### Testing + +To run all unit tests across the repository, from root: +```bash +yarn test +``` + +To run a particular workspace's unit tests from root: +```bash +yarn test --selectProjects [workspace-name] +``` diff --git a/apps/parsley/.graphqlrc b/apps/parsley/.graphqlrc deleted file mode 100644 index 58b742a8d..000000000 --- a/apps/parsley/.graphqlrc +++ /dev/null @@ -1,2 +0,0 @@ -schema: "sdlschema/**/*.graphql" -documents: "src/gql/**/*.graphql" diff --git a/apps/parsley/.husky/pre-commit b/apps/parsley/.husky/pre-commit deleted file mode 100755 index 5a182ef10..000000000 --- a/apps/parsley/.husky/pre-commit +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - -yarn lint-staged diff --git a/apps/parsley/jest.setup.ts b/apps/parsley/config/jest/jest.setup.ts similarity index 100% rename from apps/parsley/jest.setup.ts rename to apps/parsley/config/jest/jest.setup.ts diff --git a/apps/parsley/config/jest/setupTests.ts b/apps/parsley/config/jest/setupTests.ts index 47de456c6..1dfda39e9 100644 --- a/apps/parsley/config/jest/setupTests.ts +++ b/apps/parsley/config/jest/setupTests.ts @@ -4,6 +4,7 @@ import "@testing-library/jest-dom/extend-expect"; // The following two variables are dummy values used in auth.test.tsx. process.env.REACT_APP_EVERGREEN_URL = "test-evergreen.com"; process.env.REACT_APP_GRAPHQL_URL = "test-graphql.com"; +process.env.REACT_APP_SPRUCE_URL = "test-spruce.com"; if (process.env.CI) { // Avoid printing debug statements when running tests. diff --git a/apps/parsley/cypress/integration/resmokeLogs/resmoke_evg_all_logView.ts b/apps/parsley/cypress/integration/resmokeLogs/resmoke_evg_all_logView.ts new file mode 100644 index 000000000..82185a72a --- /dev/null +++ b/apps/parsley/cypress/integration/resmokeLogs/resmoke_evg_all_logView.ts @@ -0,0 +1,35 @@ +describe("Basic resmoke log view", () => { + const logLink = + "/resmoke/mongodb_mongo_master_enterprise_amazon_linux2_arm64_all_feature_flags_jsCore_patch_9801cf147ed208ce4c0ff8dff4a97cdb216f4c22_65f06bd09ccd4eaaccca1391_24_03_12_14_51_29/0/job0/all"; + beforeEach(() => { + cy.visit(logLink); + }); + + it("should render resmoke lines", () => { + cy.dataCy("resmoke-row").should("be.visible"); + cy.dataCy("ansii-row").should("not.exist"); + }); + + it("the HTML log button is disabled", () => { + cy.toggleDetailsPanel(true); + cy.dataCy("html-log-button").should("have.attr", "aria-disabled", "true"); + }); + + it("the job logs button has a link to the job logs page", () => { + cy.toggleDetailsPanel(true); + cy.dataCy("job-logs-button").should( + "have.attr", + "href", + "http://localhost:3000/job-logs/mongodb_mongo_master_enterprise_amazon_linux2_arm64_all_feature_flags_jsCore_patch_9801cf147ed208ce4c0ff8dff4a97cdb216f4c22_65f06bd09ccd4eaaccca1391_24_03_12_14_51_29/0/job0", + ); + }); + + it("should show the project, patch, task, and group the breadcrumb", () => { + cy.dataCy("project-breadcrumb") + .contains("mongodb-mongo-master") + .should("be.visible"); + cy.dataCy("version-breadcrumb").contains("Patch 1994").should("be.visible"); + cy.dataCy("task-breadcrumb").contains("jsCore").should("be.visible"); + cy.dataCy("group-breadcrumb").contains("job0").should("be.visible"); + }); +}); diff --git a/apps/parsley/cypress/integration/resmokeLogs/resmoke_evg_test_logView.ts b/apps/parsley/cypress/integration/resmokeLogs/resmoke_evg_test_logView.ts new file mode 100644 index 000000000..715275418 --- /dev/null +++ b/apps/parsley/cypress/integration/resmokeLogs/resmoke_evg_test_logView.ts @@ -0,0 +1,318 @@ +describe("Basic resmoke log view", () => { + const logLink = + "/test/mongodb_mongo_master_rhel80_debug_v4ubsan_all_feature_flags_experimental_concurrency_sharded_with_stepdowns_and_balancer_4_linux_enterprise_361789ed8a613a2dc0335a821ead0ab6205fbdaa_22_09_21_02_53_24/0/1716e11b4f8a4541c5e2faf70affbfab"; + beforeEach(() => { + cy.visit(logLink); + }); + + it("should render resmoke lines", () => { + cy.dataCy("resmoke-row").should("be.visible"); + }); + it("by default should have wrapping turned off and should be able to scroll horizontally", () => { + cy.dataCy("log-row-16").should("be.visible"); + cy.dataCy("log-row-16").isNotContainedInViewport(); + + cy.dataCy("paginated-virtual-list").scrollTo(500, 0, { + ensureScrollable: true, + }); + }); + it("long lines with wrapping turned on should fit on screen", () => { + cy.clickToggle("wrap-toggle", true, "log-viewing"); + cy.dataCy("log-row-16").should("be.visible"); + cy.dataCy("log-row-16").isContainedInViewport(); + }); + it("should still allow horizontal scrolling when there are few logs on screen", () => { + cy.addFilter("Putting spruce/"); + cy.contains("Above & Below").click(); + cy.dataCy("paginated-virtual-list").scrollTo("right"); + }); + + it("log header should show breadcrumbs, including one for the test name", () => { + cy.dataCy("project-breadcrumb").should( + "contain.text", + "mongodb-mongo-master", + ); + + cy.dataCy("version-breadcrumb").should("contain.text", "Patch 973"); + cy.dataCy("version-breadcrumb").trigger("mouseover"); + cy.dataCy("breadcrumb-tooltip").should( + "contain.text", + "SERVER-45720 Create tests for Atlas Workflows", + ); + cy.dataCy("version-breadcrumb").trigger("mouseout"); + + cy.dataCy("task-breadcrumb") + .should("contain.text", "merge-patch") + .should( + "have.attr", + "href", + "http://localhost:9090/task/mongodb_mongo_master_rhel80_debug_v4ubsan_all_feature_flags_experimental_concurrency_sharded_with_stepdowns_and_balancer_4_linux_enterprise_361789ed8a613a2dc0335a821ead0ab6205fbdaa_22_09_21_02_53_24/0?redirect_spruce_users=true", + ); + cy.dataCy("task-status-badge").should("contain.text", "Succeeded"); + + cy.dataCy("test-breadcrumb").should( + "contain.text", + "internal_transactions_kill_sessions.js", + ); + cy.dataCy("test-status-badge").should("contain.text", "Pass"); + }); +}); + +describe("Resmoke syntax highlighting", () => { + // Although it isn't ideal to test for a specific color, this helps us ensure that the color is consistent and deterministic. + const colors = { + black: "rgb(0, 0, 0)", + blue: "rgb(8, 60, 144)", + green: "rgb(0, 163, 92)", + }; + const logLink = + "/test/mongodb_mongo_master_rhel80_debug_v4ubsan_all_feature_flags_experimental_concurrency_sharded_with_stepdowns_and_balancer_4_linux_enterprise_361789ed8a613a2dc0335a821ead0ab6205fbdaa_22_09_21_02_53_24/0/1716e11b4f8a4541c5e2faf70affbfab"; + + beforeEach(() => { + cy.visit(logLink); + }); + it("should not color non-resmoke log lines", () => { + cy.dataCy("log-row-0").within(() => { + cy.dataCy("resmoke-row").should("have.css", "color", colors.black); + }); + }); + it("should color similar resmoke lines with the same color", () => { + cy.dataCy("log-row-20").should("be.visible"); + cy.dataCy("log-row-21").should("be.visible"); + cy.dataCy("log-row-20").should("contain", "[j0:s0:n1]"); + cy.dataCy("log-row-21").should("contain", "[j0:s0:n1]"); + cy.dataCy("log-row-20").within(() => { + cy.dataCy("resmoke-row").should("have.css", "color", colors.blue); + }); + cy.dataCy("log-row-21").within(() => { + cy.dataCy("resmoke-row").should("have.css", "color", colors.blue); + }); + }); + it("should color different resmoke lines with different colors if their resmoke state is different", () => { + cy.dataCy("log-row-19").should("be.visible"); + cy.dataCy("log-row-20").should("be.visible"); + cy.dataCy("log-row-19").should("contain", "[j0:s0:n0]"); + cy.dataCy("log-row-20").should("contain", "[j0:s0:n1]"); + cy.dataCy("log-row-19").within(() => { + cy.dataCy("resmoke-row").should("have.css", "color", colors.green); + }); + cy.dataCy("log-row-20").within(() => { + cy.dataCy("resmoke-row").should("have.css", "color", colors.blue); + }); + }); +}); + +describe("Bookmarking and selecting lines", () => { + const logLink = + "/test/mongodb_mongo_master_rhel80_debug_v4ubsan_all_feature_flags_experimental_concurrency_sharded_with_stepdowns_and_balancer_4_linux_enterprise_361789ed8a613a2dc0335a821ead0ab6205fbdaa_22_09_21_02_53_24/0/1716e11b4f8a4541c5e2faf70affbfab"; + beforeEach(() => { + cy.visit(logLink); + }); + + it("should default to bookmarking 0 and the last log line on load", () => { + cy.location("search").should("equal", "?bookmarks=0,12568"); + cy.dataCy("bookmark-0").should("be.visible"); + cy.dataCy("bookmark-12568").should("be.visible"); + }); + + it("should be able to bookmark and unbookmark log lines", () => { + cy.dataCy("log-row-4").dblclick(); + cy.location("search").should("equal", "?bookmarks=0,4,12568"); + cy.dataCy("bookmark-0").should("be.visible"); + cy.dataCy("bookmark-4").should("be.visible"); + cy.dataCy("bookmark-12568").should("be.visible"); + cy.dataCy("log-row-4").dblclick(); + cy.location("search").should("equal", "?bookmarks=0,12568"); + cy.dataCy("bookmark-4").should("not.exist"); + }); + + it("should be able to set and unset the share line", () => { + cy.dataCy("log-link-5").click(); + cy.location("search").should("equal", "?bookmarks=0,12568&shareLine=5"); + cy.dataCy("bookmark-0").should("be.visible"); + cy.dataCy("bookmark-5").should("be.visible"); + cy.dataCy("bookmark-12568").should("be.visible"); + cy.dataCy("log-link-5").click(); + cy.location("search").should("equal", "?bookmarks=0,12568"); + cy.dataCy("bookmark-5").should("not.exist"); + }); + + it("should be able to copy bookmarks as JIRA format", () => { + cy.dataCy("log-row-10").dblclick({ scrollBehavior: false }); + cy.dataCy("log-row-11").dblclick({ scrollBehavior: false }); + + const logLine0 = + "[fsm_workload_test:internal_transactions_kill_sessions] Fixture status:"; + const logLine10 = + "|ShardedClusterFixture:job0:mongos0 |j0:s0 |20009|73157|"; + const logLine11 = + "|ShardedClusterFixture:job0:mongos1 |j0:s1 |20010|73217|"; + const logLine1638 = `[ContinuousStepdown:job0] Pausing the stepdown thread.`; + + cy.dataCy("details-button").click(); + // Need to fire a real click here because the copy to clipboard + cy.dataCy("jira-button").realClick(); + cy.assertValueCopiedToClipboard( + `{noformat}\n${logLine0}\n...\n${logLine10}\n${logLine11}\n...\n${logLine1638}\n{noformat}`, + ); + }); + + it("should be able to clear bookmarks", () => { + cy.location("search").should("equal", "?bookmarks=0,12568"); + cy.dataCy("clear-bookmarks").click(); + cy.location("search").should("equal", ""); + }); +}); + +describe("Jump to line", () => { + const logLink = + "/test/mongodb_mongo_master_rhel80_debug_v4ubsan_all_feature_flags_experimental_concurrency_sharded_with_stepdowns_and_balancer_4_linux_enterprise_361789ed8a613a2dc0335a821ead0ab6205fbdaa_22_09_21_02_53_24/0/1716e11b4f8a4541c5e2faf70affbfab"; + + it("should be able to use the bookmarks bar to jump to a line when there are no collapsed rows", () => { + cy.visit(logLink); + cy.dataCy("log-row-4").should("be.visible").dblclick({ force: true }); + cy.dataCy("bookmark-4").should("be.visible"); + + cy.dataCy("bookmark-12568").click(); + cy.dataCy("log-row-12568").should("be.visible"); + cy.dataCy("log-row-4").should("not.exist"); + + cy.dataCy("bookmark-4").click(); + cy.dataCy("log-row-4").should("be.visible"); + }); + + it("should be able to use the bookmarks bar to jump to a line when there are collapsed rows", () => { + cy.visit(`${logLink}?filters=100repl_hb`); + cy.dataCy("log-row-30").should("be.visible").dblclick({ force: true }); + cy.url().should("include", "bookmarks=0,30,12568"); + cy.dataCy("bookmark-30").should("be.visible"); + cy.dataCy("bookmark-12568").click(); + cy.dataCy("log-row-12568").should("be.visible"); + cy.dataCy("log-row-30").should("not.exist"); + + cy.dataCy("bookmark-30").click(); + cy.dataCy("log-row-30").should("be.visible"); + }); + + it("visiting a log with a share line should jump to that line on page load", () => { + cy.visit(`${logLink}?shareLine=200`); + cy.dataCy("log-row-200").should("be.visible"); + }); +}); + +describe("expanding collapsed rows", () => { + const logLink = + "/test/mongodb_mongo_master_rhel80_debug_v4ubsan_all_feature_flags_experimental_concurrency_sharded_with_stepdowns_and_balancer_4_linux_enterprise_361789ed8a613a2dc0335a821ead0ab6205fbdaa_22_09_21_02_53_24/0/1716e11b4f8a4541c5e2faf70affbfab?bookmarks=0,12568&filters=100ShardedClusterFixture%253Ajob0"; + beforeEach(() => { + cy.visit(logLink); + }); + + it("should be able to expand collapsed rows", () => { + cy.dataCy("log-row-1").should("not.exist"); + cy.dataCy("log-row-2").should("not.exist"); + cy.dataCy("log-row-3").should("not.exist"); + + cy.dataCy("collapsed-row-1-3").within(() => { + cy.contains("All").click(); + }); + + cy.dataCy("collapsed-row-1-3").should("not.exist"); + cy.dataCy("log-row-1").should("be.visible"); + cy.dataCy("log-row-2").should("be.visible"); + cy.dataCy("log-row-3").should("be.visible"); + }); + + it("should be able to see what rows have been expanded in the drawer", () => { + cy.dataCy("collapsed-row-1-3").within(() => { + cy.contains("All").click(); + }); + cy.toggleDrawer(); + cy.dataCy("expanded-row-1-to-3").should("be.visible"); + }); + + it("should be possible to re-collapse rows through the drawer", () => { + cy.dataCy("collapsed-row-1-3").within(() => { + cy.contains("All").click(); + }); + cy.dataCy("collapsed-row-1-3").should("not.exist"); + + cy.toggleDrawer(); + cy.dataCy("expanded-row-1-to-3").within(() => { + cy.get(`[aria-label="Delete range"]`).click(); + }); + cy.dataCy("collapsed-row-1-3").should("exist"); + }); +}); + +describe("pretty print", () => { + const logLink = + "/test/mongodb_mongo_master_rhel80_debug_v4ubsan_all_feature_flags_experimental_concurrency_sharded_with_stepdowns_and_balancer_4_linux_enterprise_361789ed8a613a2dc0335a821ead0ab6205fbdaa_22_09_21_02_53_24/0/1716e11b4f8a4541c5e2faf70affbfab"; + beforeEach(() => { + cy.setCookie("pretty-print-bookmarks", "true"); + cy.visit(logLink); + }); + + it("should pretty print bookmarks if pretty print is enabled", () => { + const defaultRowHeight = 18; + + cy.dataCy("log-row-19").dblclick({ force: true }); + cy.dataCy("log-row-19") + .invoke("height") + .should("be.greaterThan", defaultRowHeight); + }); +}); + +describe("Sharing lines", () => { + const logLink = + "/test/mongodb_mongo_master_rhel80_debug_v4ubsan_all_feature_flags_experimental_concurrency_sharded_with_stepdowns_and_balancer_4_linux_enterprise_361789ed8a613a2dc0335a821ead0ab6205fbdaa_22_09_21_02_53_24/0/1716e11b4f8a4541c5e2faf70affbfab"; + + beforeEach(() => { + cy.visit(logLink); + }); + + it("should present a share button with a menu when a line is selected", () => { + cy.dataCy("line-index-1").click(); + cy.dataCy("sharing-menu-button").should("be.visible"); + cy.dataCy("sharing-menu-button").click(); + cy.dataCy("sharing-menu").should("be.visible"); + }); + it("shift+click selecting a range of lines should automatically open the sharing menu", () => { + cy.dataCy("line-index-1").click(); + cy.dataCy("line-index-10").click({ shiftKey: true }); + cy.dataCy("sharing-menu").should("be.visible"); + }); + it("should be able to copy the selected lines as JIRA format", () => { + cy.dataCy("line-index-1").click(); + cy.dataCy("line-index-2").click({ shiftKey: true }); + cy.dataCy("sharing-menu").should("be.visible"); + cy.contains("Copy selected contents").should("be.visible"); + // Need to fire a real click here because the copy to clipboard + cy.contains("Copy selected contents").realClick(); + cy.validateToast("success", "Copied 2 lines to clipboard", true); + cy.assertValueCopiedToClipboard( + `{noformat}\n+------------------------------------------+--------+-----+-----+\n|full_name |name |port |pid |\n{noformat}`, + ); + }); + it("should be able to copy a link to the selected lines", () => { + cy.dataCy("line-index-1").click(); + cy.dataCy("line-index-2").click({ shiftKey: true }); + cy.dataCy("sharing-menu").should("be.visible"); + cy.contains("Copy share link to selected lines").should("be.visible"); + // Need to fire a real click here because the copy to clipboard + cy.contains("Copy share link to selected lines").realClick(); + cy.validateToast("success", "Copied link to clipboard", true); + cy.assertValueCopiedToClipboard( + "http://localhost:4173/test/mongodb_mongo_master_rhel80_debug_v4ubsan_all_feature_flags_experimental_concurrency_sharded_with_stepdowns_and_balancer_4_linux_enterprise_361789ed8a613a2dc0335a821ead0ab6205fbdaa_22_09_21_02_53_24/0/1716e11b4f8a4541c5e2faf70affbfab?bookmarks=0%2C12568&selectedLineRange=L1-L2&shareLine=1", + ); + }); + it("should be able to limit the search range to the selected lines", () => { + cy.dataCy("line-index-1").click(); + cy.dataCy("line-index-2").click({ shiftKey: true }); + cy.dataCy("sharing-menu").should("be.visible"); + cy.contains("Only search on range").should("be.visible"); + cy.contains("Only search on range").click(); + cy.toggleDetailsPanel(true); + cy.dataCy("range-lower-bound").should("have.value", "1"); + cy.dataCy("range-upper-bound").should("have.value", "2"); + }); +}); diff --git a/apps/parsley/docs/06-decisions/2024-04-4_determining_log_line_rendering_logic.md b/apps/parsley/docs/06-decisions/2024-04-4_determining_log_line_rendering_logic.md new file mode 100644 index 000000000..7fd10b43a --- /dev/null +++ b/apps/parsley/docs/06-decisions/2024-04-4_determining_log_line_rendering_logic.md @@ -0,0 +1,15 @@ +# 2024-04-04 Determining Log Line Rendering Logic + +* status: accepted +* date: 2024-04-04 +* authors: Arjun Patel + +## Context and Problem Statement + +Raw logs ingested by Parsley require a specific rendering algorithm so the HTML rendered by Parsley is a transformation of the raw log. +Before [DEVPROD-5130](https://jira.mongodb.org/browse/DEVPROD-5130) and [DEVPROD-71](https://jira.mongodb.org/browse/DEVPROD-71), the rendering algorithm was determined by the origin of the log, indicated by the app URL. This strict +requirement makes managing API data, app state, and app URL generation inflexible and tedious. + +## Decision Outcome +After those code changes, the rendering algorithm is available from a GQL API field called `renderingType`. If that value is unavailable, then +the code will fall back to log origin or a default rendering algorithm. \ No newline at end of file diff --git a/apps/parsley/jest.config.js b/apps/parsley/jest.config.ts similarity index 63% rename from apps/parsley/jest.config.js rename to apps/parsley/jest.config.ts index a5978e117..5a8f4b8b8 100644 --- a/apps/parsley/jest.config.js +++ b/apps/parsley/jest.config.ts @@ -1,25 +1,27 @@ -module.exports = { +import type { Config } from "jest"; + +const config: Config = { + displayName: "parsley", collectCoverageFrom: [ "src/**/*.{js,jsx,ts,tsx}", "!/node_modules/", "!/src/{main.tsx,vite-env.d.ts}", ], - coverageReporters: ["text"], - moduleFileExtensions: ["json", "js", "jsx", "ts", "tsx"], + moduleFileExtensions: ["tsx", "ts", "json", "js", "jsx"], moduleNameMapper: { "^uuid$": "/node_modules/uuid/dist/index.js", }, modulePaths: ["/src"], - setupFiles: ["./jest.setup.ts"], - preset: "ts-jest", + setupFiles: ["./config/jest/jest.setup.ts"], + preset: "ts-jest/presets/js-with-ts", resetMocks: true, setupFilesAfterEnv: ["/config/jest/setupTests.ts"], snapshotSerializers: ["@emotion/jest/serializer"], testEnvironment: "jsdom", - testMatch: ["/{src,scripts}/**/*.{spec,test}.{js,jsx,ts,tsx}"], + testMatch: ["/{src,scripts}/**/*.{spec,test}.{ts,tsx}"], transform: { + "^.+\\.[tj]sx?$": ["ts-jest", { isolatedModules: true }], "^.+\\.graphql$": "@graphql-tools/jest-transform", - "^.+\\.(js|jsx|mjs|cjs|ts|tsx)$": "ts-jest", "^.+\\.css$": "/config/jest/cssTransform.js", "^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)": "/config/jest/svgTransform.js", @@ -35,20 +37,6 @@ module.exports = { "filter-obj", ].join("|")})`, ], - watchPlugins: [ - "jest-watch-typeahead/filename", - "jest-watch-typeahead/testname", - ], - - // Set the output directory for generating test results. - reporters: [ - "default", - [ - "jest-junit", - { - outputDirectory: "bin/jest", - outputName: "junit.xml", - }, - ], - ], }; + +export default config; diff --git a/apps/parsley/lint-staged.config.js b/apps/parsley/lint-staged.config.js index da59af68f..9f6b6feb9 100644 --- a/apps/parsley/lint-staged.config.js +++ b/apps/parsley/lint-staged.config.js @@ -1,10 +1,5 @@ +const baseConfig = require("../../lint-staged.base.config"); + module.exports = { - "(?!src/)*.{js,ts,tsx}": ["yarn eslint:staged", "yarn prettier"], // For files that are not in src/, run eslint and prettier - "src/**/*.{js,ts,tsx}": ["yarn eslint:staged", "yarn prettier"], // For files in src/, run eslint and prettier - "cypress/**/*.{js,ts}": ["yarn eslint:staged", "yarn prettier"], // For files in cypress/, run eslint and prettier - "src/gql/**/*.graphql": [ - "yarn eslint:staged", - "yarn prettier --parser graphql", - ], // For GraphQL files, run eslint and prettier - "*.{ts,tsx}": () => "yarn check-types", // For TypeScript files, check types + ...baseConfig, }; diff --git a/apps/parsley/package.json b/apps/parsley/package.json index 2da6e867c..62217fd8f 100644 --- a/apps/parsley/package.json +++ b/apps/parsley/package.json @@ -1,7 +1,7 @@ { "name": "parsley", "private": true, - "version": "2.0.1", + "version": "2.0.6", "scripts": { "bootstrap-logkeeper": "./scripts/bootstrap-logkeeper.sh", "build": "tsc && GIT_SHA=`git rev-parse HEAD` APP_VERSION=$npm_package_version vite build", @@ -24,7 +24,7 @@ "eslint:fix": "yarn eslint:strict --fix", "eslint:staged": "STRICT=1 eslint", "eslint:strict": "STRICT=1 eslint '*.{js,ts,tsx}' 'src/**/*.ts?(x)' 'cypress/**/*.ts' 'src/gql/**/*.graphql'", - "prepare": "husky install", + "prepare": "yarn --cwd='../..' prepare", "prettier": "prettier --write", "preview": "serve -s dist -p 4173", "snapshot": "jest snapshot.test.ts --watchAll=false", @@ -32,8 +32,7 @@ "verify-backend": "./scripts/verify-backend.sh", "storybook": "storybook dev -p 6006", "storybook:build": "env-cmd -e local -r .env-cmdrc.local.json storybook build", - "test": "jest --watchAll=false", - "test:watch": "jest --watch --verbose", + "test": "yarn --cwd='../..' test --selectProjects parsley --watchAll=false", "postversion": "scripts/push-version.sh" }, "dependencies": { @@ -102,7 +101,7 @@ "@testing-library/jest-dom": "5.16.5", "@testing-library/react": "14.1.2", "@testing-library/user-event": "14.5.1", - "@types/jest": "29.5.5", + "@types/jest": "29.5.12", "@types/jest-specific-snapshot": "0.5.9", "@types/js-cookie": "3.0.4", "@types/lodash.debounce": "4.0.7", @@ -134,16 +133,14 @@ "eslint-plugin-sort-keys-plus": "^1.3.1", "eslint-plugin-storybook": "0.6.15", "eslint-plugin-testing-library": "6.0.1", - "husky": "8.0.3", "jest": "29.7.0", "jest-environment-jsdom": "29.6.4", "jest-junit": "15.0.0", "jest-specific-snapshot": "8.0.0", "jest-watch-typeahead": "2.2.2", - "lint-staged": "13.2.2", "prettier": "3.2.2", "prompts": "2.4.2", - "replace-in-file": "7.0.1", + "replace-in-file": "7.1.0", "rollup-plugin-visualizer": "5.9.0", "serve": "14.2.1", "storybook": "7.6.9", diff --git a/apps/parsley/scripts/bootstrap-logkeeper.sh b/apps/parsley/scripts/bootstrap-logkeeper.sh index c2d4337f8..bec3bcc45 100755 --- a/apps/parsley/scripts/bootstrap-logkeeper.sh +++ b/apps/parsley/scripts/bootstrap-logkeeper.sh @@ -1,4 +1,4 @@ -# This file downloads a resmoke log for use with the local logkeeper db +# This file downloads bucket data for task output. RED='\033[0;31m' @@ -14,7 +14,7 @@ if [ ! -d "bin/_bucketdata" ]; then # Use aws cli to download the bucket data echo "Downloading bucket data..." # Try to download the bucket data - aws s3 sync --content-encoding gzip s3://parsley-test/ ./bin + aws s3 cp s3://parsley-test/_bucketdata.tar.gz bin/_bucketdata.tar.gz # Check to see if the download was successful if [ $? -ne 0 ]; then echo "${RED}Failed to download bucket data!${NC}" @@ -26,12 +26,12 @@ if [ ! -d "bin/_bucketdata" ]; then fi # Uncompress the files in the _bucketdata directory echo "Uncompressing bucket data..." - tar -xzf ./bin/_bucketdata.tar.gz -C ./bin/ + tar -xzf bin/_bucketdata.tar.gz -C bin/ # Check to see if the uncompress was successful if [ $? -ne 0 ]; then echo "${RED}Failed to uncompress bucket data!${NC}" echo "Cleaning up _bucketdata directory..." - rm -rf _bucketdata + rm -rf bin/_bucketdata exit 1 fi echo "Finished uncompressing files." @@ -45,6 +45,8 @@ else echo "If you want to download the bucket data again, delete the _bucketdata directory and rerun this script." fi - -echo "Use the following command within the logkeeper directory to start logkeeper:" +echo "Use the following command to start logkeeper:" echo "${YELLOW}LK_CORS_ORIGINS=http:\/\/localhost:\\\d+ LK_EVERGREEN_ORIGIN=http://localhost:8080 LK_PARSLEY_ORIGIN=http://localhost:5173 go run main/logkeeper.go --localPath $PWD/bin/_bucketdata${NC}" + +echo "Create symlink in your local evergreen directory:" +echo "ln -s $PWD/bin/_bucketdata _bucketdata" diff --git a/apps/parsley/scripts/deploy/utils/git/index.ts b/apps/parsley/scripts/deploy/utils/git/index.ts index a5cd5394b..ef57a7a7e 100644 --- a/apps/parsley/scripts/deploy/utils/git/index.ts +++ b/apps/parsley/scripts/deploy/utils/git/index.ts @@ -1,4 +1,5 @@ import { execSync } from "child_process"; +import { resolve } from "path"; /** * `getCommitMessages` returns a string of all commit messages between the currently deployed commit and HEAD. @@ -8,7 +9,7 @@ import { execSync } from "child_process"; const getCommitMessages = (currentlyDeployedCommit: string) => { const commitMessages = execSync( `git log ${currentlyDeployedCommit}..HEAD --oneline -- .`, - { encoding: "utf-8" } + { encoding: "utf-8" }, ).toString(); return commitMessages; }; @@ -19,10 +20,10 @@ const getCommitMessages = (currentlyDeployedCommit: string) => { * @returns - the currently deployed commit */ const getCurrentlyDeployedCommit = () => { - const currentlyDeployedCommit = execSync( - "bash scripts/deploy/get-current-deployed-commit.sh", - { encoding: "utf-8" } - ) + const filePath = resolve(__dirname, "../../get-current-deployed-commit.sh"); + const currentlyDeployedCommit = execSync(`bash ${filePath}`, { + encoding: "utf-8", + }) .toString() .trim(); return currentlyDeployedCommit; diff --git a/apps/parsley/src/analytics/preferences/usePreferencesAnalytics.ts b/apps/parsley/src/analytics/preferences/usePreferencesAnalytics.ts index 91fd5b73b..34e901a26 100644 --- a/apps/parsley/src/analytics/preferences/usePreferencesAnalytics.ts +++ b/apps/parsley/src/analytics/preferences/usePreferencesAnalytics.ts @@ -14,7 +14,8 @@ type Action = | { name: "Toggled Pretty Print"; on: boolean } | { name: "Toggled Filter Logic"; logic: FilterLogic } | { name: "Toggled Expandable Rows"; on: boolean } - | { name: "Toggled Zebra Stripes"; on: boolean }; + | { name: "Toggled Zebra Stripes"; on: boolean } + | { name: "Toggled Jump to Failing Line"; on: boolean }; export const usePreferencesAnalytics = () => useAnalyticsRoot("Preferences"); diff --git a/apps/parsley/src/analytics/useAnalyticAttributes.ts b/apps/parsley/src/analytics/useAnalyticAttributes.ts index dbbc40716..c365c223a 100644 --- a/apps/parsley/src/analytics/useAnalyticAttributes.ts +++ b/apps/parsley/src/analytics/useAnalyticAttributes.ts @@ -5,7 +5,7 @@ export const useAnalyticAttributes = () => { const { newrelic } = window; const { logMetadata } = useLogContext(); - const { logType } = logMetadata || {}; + const { logType, renderingType } = logMetadata || {}; const userId = localStorage.getItem("userId"); @@ -21,5 +21,8 @@ export const useAnalyticAttributes = () => { if (userId !== null) { newrelic.setCustomAttribute("userId", userId); } - }, [userId, logType, newrelic]); + if (renderingType !== undefined) { + newrelic.setCustomAttribute("renderingType", renderingType); + } + }, [userId, logType, newrelic, renderingType]); }; diff --git a/apps/parsley/src/components/DetailsMenu/DetailsMenu.test.tsx b/apps/parsley/src/components/DetailsMenu/DetailsMenu.test.tsx index 7ad7c2d2d..458326890 100644 --- a/apps/parsley/src/components/DetailsMenu/DetailsMenu.test.tsx +++ b/apps/parsley/src/components/DetailsMenu/DetailsMenu.test.tsx @@ -1,25 +1,32 @@ +import { MockedProvider } from "@apollo/client/testing"; import { act, waitFor } from "@testing-library/react"; import { QueryParams } from "constants/queryParams"; import { LogContextProvider, useLogContext } from "context/LogContext"; import { RenderFakeToastContext } from "context/toast/__mocks__"; import { useQueryParam } from "hooks/useQueryParam"; +import { parsleySettingsMock } from "test_data/parsleySettings"; import { renderWithRouterMatch as render, screen, userEvent } from "test_utils"; import { renderComponentWithHook } from "test_utils/TestHooks"; import DetailsMenu from "."; const wrapper = ({ children }: { children: React.ReactNode }) => ( - {children} + + + {children} + + ); -const logs = [ - "line 1", - "line 2", - "line 3", - "line 4", - "line 5", - "line 6", - "line 7", -]; /** * `renderSharingMenu` renders the sharing menu with the default open prop * @returns - hook and utils diff --git a/apps/parsley/src/components/DetailsMenu/DetailsMenuCard/CLIInstructions/CliInstructions.test.tsx b/apps/parsley/src/components/DetailsMenu/DetailsMenuCard/CLIInstructions/CliInstructions.test.tsx index 070fe2803..2ab375eab 100644 --- a/apps/parsley/src/components/DetailsMenu/DetailsMenuCard/CLIInstructions/CliInstructions.test.tsx +++ b/apps/parsley/src/components/DetailsMenu/DetailsMenuCard/CLIInstructions/CliInstructions.test.tsx @@ -87,7 +87,7 @@ describe("cliInstructions", () => { hook.current.setLogMetadata({ buildID: "buildId", execution: "0", - logType: LogTypes.RESMOKE_LOGS, + logType: LogTypes.LOGKEEPER_LOGS, taskID: "taskId", testID: "testId", }); diff --git a/apps/parsley/src/components/DetailsMenu/DetailsMenuCard/CLIInstructions/index.tsx b/apps/parsley/src/components/DetailsMenu/DetailsMenuCard/CLIInstructions/index.tsx index 932844771..be308c384 100644 --- a/apps/parsley/src/components/DetailsMenu/DetailsMenuCard/CLIInstructions/index.tsx +++ b/apps/parsley/src/components/DetailsMenu/DetailsMenuCard/CLIInstructions/index.tsx @@ -29,7 +29,7 @@ const getCLICommand = (logMetadata: LogMetadata) => { return ""; } return `evergreen buildlogger fetch --task_id ${taskID} --execution ${execution} --test_name ${testID}`; - case LogTypes.RESMOKE_LOGS: + case LogTypes.LOGKEEPER_LOGS: if (!buildID) { return ""; } diff --git a/apps/parsley/src/components/DetailsMenu/DetailsMenuCard/DetailsMenu.stories.tsx b/apps/parsley/src/components/DetailsMenu/DetailsMenuCard/DetailsMenu.stories.tsx index 2867f54b5..ee7089e7c 100644 --- a/apps/parsley/src/components/DetailsMenu/DetailsMenuCard/DetailsMenu.stories.tsx +++ b/apps/parsley/src/components/DetailsMenu/DetailsMenuCard/DetailsMenu.stories.tsx @@ -1,12 +1,23 @@ import { useEffect } from "react"; +import { MockedProvider } from "@apollo/client/testing"; import Card from "@leafygreen-ui/card"; import { LogTypes } from "constants/enums"; import { useLogContext } from "context/LogContext"; +import { parsleySettingsMock } from "test_data/parsleySettings"; +import WithToastContext from "test_utils/toast-decorator"; import { CustomMeta, CustomStoryObj } from "test_utils/types"; import DetailsMenu from "."; export default { component: DetailsMenu, + decorators: [ + (Story: () => JSX.Element) => ( + + + + ), + WithToastContext, + ], } satisfies CustomMeta; export const Default: CustomStoryObj = { diff --git a/apps/parsley/src/components/DetailsMenu/DetailsMenuCard/Toggles/JumpToFailingLineToggle/JumpToFailingLineToggle.test.tsx b/apps/parsley/src/components/DetailsMenu/DetailsMenuCard/Toggles/JumpToFailingLineToggle/JumpToFailingLineToggle.test.tsx new file mode 100644 index 000000000..03e9a4ec5 --- /dev/null +++ b/apps/parsley/src/components/DetailsMenu/DetailsMenuCard/Toggles/JumpToFailingLineToggle/JumpToFailingLineToggle.test.tsx @@ -0,0 +1,91 @@ +import { LogTypes } from "constants/enums"; +import { LogContextProvider, useLogContext } from "context/LogContext"; +import { + act, + renderWithRouterMatch as render, + screen, + userEvent, +} from "test_utils"; +import { renderComponentWithHook } from "test_utils/TestHooks"; +import JumpToFailingLineToggle from "."; + +const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} +); + +describe("jump to failing line toggle", () => { + it("should render as checked when 'checked' prop is true", () => { + render(, { + wrapper, + }); + const jumpToFailingLineToggle = screen.getByDataCy( + "jump-to-failing-line-toggle", + ); + expect(jumpToFailingLineToggle).toHaveAttribute("aria-checked", "true"); + }); + + it("should render as unchecked when 'checked' prop is false", () => { + render( + , + { wrapper }, + ); + const jumpToFailingLineToggle = screen.getByDataCy( + "jump-to-failing-line-toggle", + ); + expect(jumpToFailingLineToggle).toHaveAttribute("aria-checked", "false"); + }); + + it("should disable toggle if logType is not Evergreen task logs", () => { + const { Component, hook } = renderComponentWithHook( + useLogContext, + , + ); + render(, { wrapper }); + act(() => { + hook.current.setLogMetadata({ logType: LogTypes.LOGKEEPER_LOGS }); + }); + const jumpToFailingLineToggle = screen.getByDataCy( + "jump-to-failing-line-toggle", + ); + expect(jumpToFailingLineToggle).toHaveAttribute("aria-disabled", "true"); + }); + + it("should enable toggle if logType is Evergreen task logs", () => { + const { Component, hook } = renderComponentWithHook( + useLogContext, + , + ); + render(, { wrapper }); + act(() => { + hook.current.setLogMetadata({ logType: LogTypes.EVERGREEN_TASK_LOGS }); + }); + const jumpToFailingLineToggle = screen.getByDataCy( + "jump-to-failing-line-toggle", + ); + expect(jumpToFailingLineToggle).toHaveAttribute("aria-disabled", "false"); + }); + + it("should call update function with correct parameters, without updating the URL", async () => { + const user = userEvent.setup(); + const updateSettings = jest.fn(); + + const { Component, hook } = renderComponentWithHook( + useLogContext, + , + ); + const { router } = render(, { wrapper }); + act(() => { + hook.current.setLogMetadata({ logType: LogTypes.EVERGREEN_TASK_LOGS }); + }); + + const jumpToFailingLineToggle = screen.getByDataCy( + "jump-to-failing-line-toggle", + ); + await user.click(jumpToFailingLineToggle); + expect(updateSettings).toHaveBeenCalledTimes(1); + expect(updateSettings).toHaveBeenCalledWith({ + jumpToFailingLineEnabled: false, + }); + expect(router.state.location.search).toBe(""); + }); +}); diff --git a/apps/parsley/src/components/DetailsMenu/DetailsMenuCard/Toggles/JumpToFailingLineToggle/index.tsx b/apps/parsley/src/components/DetailsMenu/DetailsMenuCard/Toggles/JumpToFailingLineToggle/index.tsx new file mode 100644 index 000000000..c2f7b3233 --- /dev/null +++ b/apps/parsley/src/components/DetailsMenu/DetailsMenuCard/Toggles/JumpToFailingLineToggle/index.tsx @@ -0,0 +1,35 @@ +import { usePreferencesAnalytics } from "analytics"; +import { LogTypes } from "constants/enums"; +import { useLogContext } from "context/LogContext"; +import { ParsleySettingsInput } from "gql/generated/types"; +import BaseToggle from "../BaseToggle"; + +interface JumpToFailingLineToggleProps { + checked: boolean; + updateSettings: (parsleySettings: ParsleySettingsInput) => void; +} + +const JumpToFailingLineToggle: React.FC = ({ + checked, + updateSettings, +}) => { + const { sendEvent } = usePreferencesAnalytics(); + const { logMetadata } = useLogContext(); + const isTaskLog = logMetadata?.logType === LogTypes.EVERGREEN_TASK_LOGS; + + return ( + { + updateSettings({ jumpToFailingLineEnabled: value }); + sendEvent({ name: "Toggled Jump to Failing Line", on: value }); + }} + tooltip="Toggle scroll to failing line on page load. Only available for task logs." + value={checked} + /> + ); +}; + +export default JumpToFailingLineToggle; diff --git a/apps/parsley/src/components/DetailsMenu/DetailsMenuCard/Toggles/PrettyPrintToggle/PrettyPrintToggle.test.tsx b/apps/parsley/src/components/DetailsMenu/DetailsMenuCard/Toggles/PrettyPrintToggle/PrettyPrintToggle.test.tsx index 5586190f3..d318b47c9 100644 --- a/apps/parsley/src/components/DetailsMenu/DetailsMenuCard/Toggles/PrettyPrintToggle/PrettyPrintToggle.test.tsx +++ b/apps/parsley/src/components/DetailsMenu/DetailsMenuCard/Toggles/PrettyPrintToggle/PrettyPrintToggle.test.tsx @@ -1,5 +1,5 @@ import Cookie from "js-cookie"; -import { LogTypes } from "constants/enums"; +import { LogRenderingTypes } from "constants/enums"; import { LogContextProvider, useLogContext } from "context/LogContext"; import { act, @@ -42,21 +42,21 @@ describe("pretty print toggle", () => { ); render(, { wrapper }); act(() => { - hook.current.setLogMetadata({ logType: LogTypes.EVERGREEN_TASK_LOGS }); + hook.current.setLogMetadata({ renderingType: LogRenderingTypes.Default }); }); const prettyPrintToggle = screen.getByDataCy("pretty-print-toggle"); expect(prettyPrintToggle).toHaveAttribute("aria-disabled", "true"); }); - it("should not disable the toggle if the logType is resmoke", () => { + it("should not disable the toggle if the renderingType is resmoke", () => { const { Component, hook } = renderComponentWithHook( useLogContext, , ); render(, { wrapper }); act(() => { - hook.current.setLogMetadata({ logType: LogTypes.RESMOKE_LOGS }); + hook.current.setLogMetadata({ renderingType: LogRenderingTypes.Resmoke }); }); const prettyPrintToggle = screen.getByDataCy("pretty-print-toggle"); @@ -71,7 +71,7 @@ describe("pretty print toggle", () => { ); const { router } = render(, { wrapper }); act(() => { - hook.current.setLogMetadata({ logType: LogTypes.RESMOKE_LOGS }); + hook.current.setLogMetadata({ renderingType: LogRenderingTypes.Resmoke }); }); const prettyPrintToggle = screen.getByDataCy("pretty-print-toggle"); diff --git a/apps/parsley/src/components/DetailsMenu/DetailsMenuCard/Toggles/PrettyPrintToggle/index.tsx b/apps/parsley/src/components/DetailsMenu/DetailsMenuCard/Toggles/PrettyPrintToggle/index.tsx index 4f505acb9..d2af89bf8 100644 --- a/apps/parsley/src/components/DetailsMenu/DetailsMenuCard/Toggles/PrettyPrintToggle/index.tsx +++ b/apps/parsley/src/components/DetailsMenu/DetailsMenuCard/Toggles/PrettyPrintToggle/index.tsx @@ -1,5 +1,5 @@ import { usePreferencesAnalytics } from "analytics"; -import { LogTypes } from "constants/enums"; +import { LogRenderingTypes } from "constants/enums"; import { useLogContext } from "context/LogContext"; import BaseToggle from "../BaseToggle"; @@ -8,8 +8,8 @@ const PrettyPrintToggle: React.FC = () => { const { logMetadata, preferences } = useLogContext(); const { prettyPrint, setPrettyPrint } = preferences; - const { logType } = logMetadata || {}; - const disablePrettyPrint = logType !== LogTypes.RESMOKE_LOGS; + const disablePrettyPrint = + logMetadata?.renderingType !== LogRenderingTypes.Resmoke; return ( ( ({ "data-cy": dataCy }, ref) => { const [selectedTab, setSelectedTab] = useState(0); + + const { settings, updateSettings } = useParsleySettings(); + const { jumpToFailingLineEnabled = true } = settings ?? {}; + return (

Parsley Settings

@@ -50,6 +57,12 @@ const DetailsMenuCard = forwardRef( + {!isProduction() && ( + + )} diff --git a/apps/parsley/src/components/LogRow/AnsiRow/AnsiRow.stories.tsx b/apps/parsley/src/components/LogRow/AnsiRow/AnsiRow.stories.tsx index 7714daf13..11414fcb2 100644 --- a/apps/parsley/src/components/LogRow/AnsiRow/AnsiRow.stories.tsx +++ b/apps/parsley/src/components/LogRow/AnsiRow/AnsiRow.stories.tsx @@ -1,7 +1,7 @@ import { useEffect } from "react"; import styled from "@emotion/styled"; import LogPane from "components/LogPane"; -import { LogTypes, WordWrapFormat } from "constants/enums"; +import { LogRenderingTypes, LogTypes, WordWrapFormat } from "constants/enums"; import { useLogContext } from "context/LogContext"; import { MultiLineSelectContextProvider } from "context/MultiLineSelectContext"; import WithToastContext from "test_utils/toast-decorator"; @@ -28,7 +28,7 @@ const SingleLineStory = (args: any) => { const { ingestLines, scrollToLine } = useLogContext(); useEffect(() => { - ingestLines(logLines, LogTypes.EVERGREEN_TASK_LOGS); + ingestLines(logLines, LogRenderingTypes.Default); }, []); // eslint-disable-line react-hooks/exhaustive-deps return ( @@ -57,11 +57,13 @@ export const SingleLine: CustomStoryObj = { // Multiple AnsiRows. const MultiLineStory = (args: any) => { - const { ingestLines, preferences, processedLogLines } = useLogContext(); + const { ingestLines, preferences, processedLogLines, setLogMetadata } = + useLogContext(); const { setWrap } = preferences; useEffect(() => { - ingestLines(logLines, LogTypes.EVERGREEN_TASK_LOGS); + setLogMetadata({ logType: LogTypes.EVERGREEN_TASK_LOGS }); + ingestLines(logLines, LogRenderingTypes.Default); }, []); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { @@ -73,7 +75,6 @@ const MultiLineStory = (args: any) => { diff --git a/apps/parsley/src/components/LogRow/BaseRow/SharingMenu/SharingMenu.test.tsx b/apps/parsley/src/components/LogRow/BaseRow/SharingMenu/SharingMenu.test.tsx index 6654edf0b..5eee59432 100644 --- a/apps/parsley/src/components/LogRow/BaseRow/SharingMenu/SharingMenu.test.tsx +++ b/apps/parsley/src/components/LogRow/BaseRow/SharingMenu/SharingMenu.test.tsx @@ -1,3 +1,4 @@ +import { LogTypes } from "constants/enums"; import { LogContextProvider, useLogContext } from "context/LogContext"; import { MultiLineSelectContextProvider, @@ -150,7 +151,7 @@ describe("sharingMenu", () => { }); act(() => { hook.current.useLogContextHook.setLogMetadata({ - isUploadedLog: true, + logType: LogTypes.LOCAL_UPLOAD, }); }); expect(screen.queryByText("Share link to selected lines")).toBeNull(); diff --git a/apps/parsley/src/components/LogRow/BaseRow/SharingMenu/index.tsx b/apps/parsley/src/components/LogRow/BaseRow/SharingMenu/index.tsx index df7276ac5..eb71bb5e6 100644 --- a/apps/parsley/src/components/LogRow/BaseRow/SharingMenu/index.tsx +++ b/apps/parsley/src/components/LogRow/BaseRow/SharingMenu/index.tsx @@ -20,12 +20,11 @@ interface SharingMenuProps { const SharingMenu: React.FC = ({ defaultOpen }) => { const { clearSelection, selectedLines } = useMultiLineSelectContext(); - const { getLine, logMetadata, processedLogLines } = useLogContext(); + const { getLine, isUploadedLog, processedLogLines } = useLogContext(); const [params, setParams] = useQueryParams(); const dispatchToast = useToastContext(); const [open, setOpen] = useState(defaultOpen); const { sendEvent } = useLogWindowAnalytics(); - const { isUploadedLog } = logMetadata || {}; const setMenuOpen = () => { if (open) { sendEvent({ name: "Closed Share Menu" }); diff --git a/apps/parsley/src/components/LogRow/CollapsedRow/CollapsedRow.stories.tsx b/apps/parsley/src/components/LogRow/CollapsedRow/CollapsedRow.stories.tsx index 3c74954f5..bd4195065 100644 --- a/apps/parsley/src/components/LogRow/CollapsedRow/CollapsedRow.stories.tsx +++ b/apps/parsley/src/components/LogRow/CollapsedRow/CollapsedRow.stories.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; import styled from "@emotion/styled"; import LogPane from "components/LogPane"; import { ParsleyRow } from "components/LogRow/RowRenderer"; -import { LogTypes } from "constants/enums"; +import { LogRenderingTypes, LogTypes } from "constants/enums"; import { useLogContext } from "context/LogContext"; import { CustomMeta, CustomStoryObj } from "test_utils/types"; import { ExpandedLine, ExpandedLines } from "types/logs"; @@ -54,11 +54,13 @@ const CollapsedAnsiRowStory = ( wrap: boolean; }, ) => { - const { ingestLines, preferences, processedLogLines } = useLogContext(); + const { ingestLines, preferences, processedLogLines, setLogMetadata } = + useLogContext(); const { setWrap } = preferences; useEffect(() => { - ingestLines(ansiLogLines, LogTypes.EVERGREEN_TASK_LOGS); + setLogMetadata({ logType: LogTypes.EVERGREEN_TASK_LOGS }); + ingestLines(ansiLogLines, LogRenderingTypes.Default); }, []); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { @@ -70,7 +72,6 @@ const CollapsedAnsiRowStory = ( @@ -95,11 +96,13 @@ const CollapsedResmokeRowStory = ( wrap: boolean; }, ) => { - const { ingestLines, preferences, processedLogLines } = useLogContext(); + const { ingestLines, preferences, processedLogLines, setLogMetadata } = + useLogContext(); const { setWrap } = preferences; useEffect(() => { - ingestLines(resmokeLogLines, LogTypes.RESMOKE_LOGS); + setLogMetadata({ logType: LogTypes.LOGKEEPER_LOGS }); + ingestLines(resmokeLogLines, LogRenderingTypes.Resmoke); }, []); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { @@ -111,7 +114,6 @@ const CollapsedResmokeRowStory = ( diff --git a/apps/parsley/src/components/LogRow/ResmokeRow/ResmokeRow.stories.tsx b/apps/parsley/src/components/LogRow/ResmokeRow/ResmokeRow.stories.tsx index 7988200a5..4118df387 100644 --- a/apps/parsley/src/components/LogRow/ResmokeRow/ResmokeRow.stories.tsx +++ b/apps/parsley/src/components/LogRow/ResmokeRow/ResmokeRow.stories.tsx @@ -2,7 +2,7 @@ import { useEffect } from "react"; import styled from "@emotion/styled"; import LogPane from "components/LogPane"; import { ParsleyRow } from "components/LogRow/RowRenderer"; -import { LogTypes, WordWrapFormat } from "constants/enums"; +import { LogRenderingTypes, LogTypes, WordWrapFormat } from "constants/enums"; import { useLogContext } from "context/LogContext"; import { MultiLineSelectContextProvider } from "context/MultiLineSelectContext"; import WithToastContext from "test_utils/toast-decorator"; @@ -28,7 +28,7 @@ const SingleLineStory = (args: any) => { const { getResmokeLineColor, ingestLines, scrollToLine } = useLogContext(); useEffect(() => { - ingestLines(logLines, LogTypes.RESMOKE_LOGS); + ingestLines(logLines, LogRenderingTypes.Resmoke); }, []); // eslint-disable-line react-hooks/exhaustive-deps return ( @@ -60,11 +60,13 @@ export const SingleLine: CustomStoryObj = { // Multiple ResmokeRows. const MultipleLinesStory = (args: any) => { - const { ingestLines, preferences, processedLogLines } = useLogContext(); + const { ingestLines, preferences, processedLogLines, setLogMetadata } = + useLogContext(); const { setPrettyPrint, setWrap } = preferences; useEffect(() => { - ingestLines(logLines, LogTypes.RESMOKE_LOGS); + setLogMetadata({ logType: LogTypes.EVERGREEN_TASK_LOGS }); + ingestLines(logLines, LogRenderingTypes.Resmoke); }, []); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { @@ -80,7 +82,6 @@ const MultipleLinesStory = (args: any) => { diff --git a/apps/parsley/src/components/LogRow/RowRenderer/index.tsx b/apps/parsley/src/components/LogRow/RowRenderer/index.tsx index e05444fc4..fb7c7c49a 100644 --- a/apps/parsley/src/components/LogRow/RowRenderer/index.tsx +++ b/apps/parsley/src/components/LogRow/RowRenderer/index.tsx @@ -1,4 +1,4 @@ -import { LogRenderingTypes, LogTypes } from "constants/enums"; +import { LogRenderingTypes } from "constants/enums"; import { useLogContext } from "context/LogContext"; import { useHighlightParam } from "hooks/useHighlightParam"; import { ProcessedLogLines } from "types/logs"; @@ -9,10 +9,9 @@ import ResmokeRow from "../ResmokeRow"; type RowRendererFunction = (props: { processedLogLines: ProcessedLogLines; - logType: LogTypes; }) => (index: number) => JSX.Element; -const ParsleyRow: RowRendererFunction = ({ logType, processedLogLines }) => { +const ParsleyRow: RowRendererFunction = ({ processedLogLines }) => { const { expandLines, getLine, @@ -83,7 +82,7 @@ const ParsleyRow: RowRendererFunction = ({ logType, processedLogLines }) => { ); }; - result.displayName = `${logType}RowRenderer`; + result.displayName = `${logMetadata?.logType}RowRenderer`; return result; }; diff --git a/apps/parsley/src/components/LogWindow/index.tsx b/apps/parsley/src/components/LogWindow/index.tsx index 66a7df259..bb5cffb25 100644 --- a/apps/parsley/src/components/LogWindow/index.tsx +++ b/apps/parsley/src/components/LogWindow/index.tsx @@ -5,13 +5,9 @@ import LogPane from "components/LogPane"; import { ParsleyRow } from "components/LogRow/RowRenderer"; import SidePanel from "components/SidePanel"; import SubHeader from "components/SubHeader"; -import { LogTypes } from "constants/enums"; import { useLogContext } from "context/LogContext"; -interface LogWindowProps { - logType: LogTypes; -} -const LogWindow: React.FC = ({ logType }) => { +const LogWindow: React.FC = () => { const { clearExpandedLines, collapseLines, @@ -41,7 +37,6 @@ const LogWindow: React.FC = ({ logType }) => { diff --git a/apps/parsley/src/components/SubHeader/EvergreenTaskSubHeader.test.tsx b/apps/parsley/src/components/SubHeader/EvergreenTaskSubHeader.test.tsx index 585216b8b..a7e253a0f 100644 --- a/apps/parsley/src/components/SubHeader/EvergreenTaskSubHeader.test.tsx +++ b/apps/parsley/src/components/SubHeader/EvergreenTaskSubHeader.test.tsx @@ -36,7 +36,7 @@ describe("evergreen task subheader", () => { diff --git a/apps/parsley/src/components/SubHeader/EvergreenTaskSubHeader.tsx b/apps/parsley/src/components/SubHeader/EvergreenTaskSubHeader.tsx index d41a63666..cd3195cd9 100644 --- a/apps/parsley/src/components/SubHeader/EvergreenTaskSubHeader.tsx +++ b/apps/parsley/src/components/SubHeader/EvergreenTaskSubHeader.tsx @@ -1,3 +1,4 @@ +import { useQuery } from "@apollo/client"; import { InlineCode } from "@leafygreen-ui/typography"; import { usePreferencesAnalytics } from "analytics"; import { TaskStatusBadge, TestStatusBadge } from "components/Badge"; @@ -6,35 +7,53 @@ import Icon from "components/Icon"; import { StyledLink } from "components/styles"; import { LogTypes } from "constants/enums"; import { getEvergreenTaskURL } from "constants/externalURLTemplates"; +import { + TestLogUrlAndRenderingTypeQuery, + TestLogUrlAndRenderingTypeQueryVariables, +} from "gql/generated/types"; +import { GET_TEST_LOG_URL_AND_RENDERING_TYPE } from "gql/queries"; import { useTaskQuery } from "hooks/useTaskQuery"; import { shortenGithash, trimStringFromMiddle } from "utils/string"; interface Props { buildID: string; execution: number; + fileName?: string; + groupID?: string; logType?: LogTypes; taskID: string; testID?: string; - fileName?: string; } export const EvergreenTaskSubHeader: React.FC = ({ buildID, execution, fileName, + groupID, logType, taskID, testID, }) => { const { sendEvent } = usePreferencesAnalytics(); - const { loading, task } = useTaskQuery({ + const { loading: isLoadingTask, task: taskData } = useTaskQuery({ buildID, execution, logType, taskID, }); - if (loading || !task) { + const { data: testData, loading: isLoadingTest } = useQuery< + TestLogUrlAndRenderingTypeQuery, + TestLogUrlAndRenderingTypeQueryVariables + >(GET_TEST_LOG_URL_AND_RENDERING_TYPE, { + skip: !(logType === LogTypes.EVERGREEN_TEST_LOGS && testID), + variables: { + execution, + taskID, + testName: `^${testID}$`, + }, + }); + if (isLoadingTask || isLoadingTest || !taskData) { return ( <> @@ -47,20 +66,30 @@ export const EvergreenTaskSubHeader: React.FC = ({ ); } - const { displayName, execution: taskExecution, patchNumber, status, versionMetadata, - } = task; + } = taskData; + const { isPatch, message, projectIdentifier, revision } = versionMetadata; - const currentTest = - task?.tests?.testResults?.find((test) => - test?.logs?.urlRaw?.match(new RegExp(`${testID}`)), - ) ?? null; + let currentTest: { testFile: string; status: string } | null = null; + switch (logType) { + case LogTypes.LOGKEEPER_LOGS: + currentTest = + taskData?.tests?.testResults?.find((test) => + test?.logs?.urlRaw?.match(new RegExp(`${testID}`)), + ) ?? null; + break; + case LogTypes.EVERGREEN_TEST_LOGS: + currentTest = testData?.task?.tests?.testResults?.[0] ?? null; + break; + default: + currentTest = null; + } const breadcrumbs = [ { @@ -115,6 +144,14 @@ export const EvergreenTaskSubHeader: React.FC = ({ }, ] : []), + ...(groupID + ? [ + { + "data-cy": "group-breadcrumb", + text: groupID, + }, + ] + : []), ]; return ( diff --git a/apps/parsley/src/components/SubHeader/SubHeader.stories.tsx b/apps/parsley/src/components/SubHeader/SubHeader.stories.tsx index da0e7fe52..99df6e534 100644 --- a/apps/parsley/src/components/SubHeader/SubHeader.stories.tsx +++ b/apps/parsley/src/components/SubHeader/SubHeader.stories.tsx @@ -77,7 +77,7 @@ const SubheaderWrapper: React.FC = ({ const { setLogMetadata } = useLogContext(); useEffect(() => { - setLogMetadata({ ...metaData, isUploadedLog }); + setLogMetadata({ ...metaData, logType: LogTypes.LOCAL_UPLOAD }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ; diff --git a/apps/parsley/src/components/SubHeader/__snapshots__/SubHeader.stories.storyshot b/apps/parsley/src/components/SubHeader/__snapshots__/SubHeader.stories.storyshot index 3e53bb350..72eef0f4e 100644 --- a/apps/parsley/src/components/SubHeader/__snapshots__/SubHeader.stories.storyshot +++ b/apps/parsley/src/components/SubHeader/__snapshots__/SubHeader.stories.storyshot @@ -10,28 +10,21 @@ exports[`Snapshot Tests SubHeader.stories TaskFileLog 1`] = ` class="css-1xawlld" > - -