diff --git a/.evergreen.yml b/.evergreen.yml index 0fdcf43a3e..55b090e84a 100644 --- a/.evergreen.yml +++ b/.evergreen.yml @@ -358,7 +358,16 @@ functions: working_dir: spruce script: | echo "Generating .cmdrc.json" - REACT_APP_BUGSNAG_API_KEY=${REACT_APP_BUGSNAG_API_KEY} REACT_APP_SENTRY_DSN=${REACT_APP_SENTRY_DSN} REACT_APP_NEW_RELIC_ACCOUNT_ID=${REACT_APP_NEW_RELIC_ACCOUNT_ID} REACT_APP_NEW_RELIC_AGENT_ID=${REACT_APP_NEW_RELIC_AGENT_ID} REACT_APP_NEW_RELIC_APPLICATION_ID=${REACT_APP_NEW_RELIC_APPLICATION_ID} REACT_APP_NEW_RELIC_LICENSE_KEY=${REACT_APP_NEW_RELIC_LICENSE_KEY} REACT_APP_NEW_RELIC_TRUST_KEY=${REACT_APP_NEW_RELIC_TRUST_KEY} REACT_APP_DEPLOYS_EMAIL=${REACT_APP_DEPLOYS_EMAIL} node scripts/setup-credentials.js + REACT_APP_BUGSNAG_API_KEY=${REACT_APP_BUGSNAG_API_KEY} \ + REACT_APP_SENTRY_DSN=${REACT_APP_SENTRY_DSN} \ + REACT_APP_NEW_RELIC_ACCOUNT_ID=${REACT_APP_NEW_RELIC_ACCOUNT_ID} \ + REACT_APP_NEW_RELIC_AGENT_ID=${REACT_APP_NEW_RELIC_AGENT_ID} \ + REACT_APP_NEW_RELIC_APPLICATION_ID=${REACT_APP_NEW_RELIC_APPLICATION_ID} \ + REACT_APP_NEW_RELIC_LICENSE_KEY=${REACT_APP_NEW_RELIC_LICENSE_KEY} \ + REACT_APP_NEW_RELIC_TRUST_KEY=${REACT_APP_NEW_RELIC_TRUST_KEY} \ + REACT_APP_DEPLOYS_EMAIL=${REACT_APP_DEPLOYS_EMAIL} \ + REACT_APP_HONEYCOMB_BASE_URL=${REACT_APP_HONEYCOMB_BASE_URL} \ + node scripts/setup-credentials.js echo "populating evergreen.yml" cat < .evergreen.yml diff --git a/cypress/integration/projectSettings/project_settings.ts b/cypress/integration/projectSettings/project_settings.ts index 3133cdb623..50047c9bd0 100644 --- a/cypress/integration/projectSettings/project_settings.ts +++ b/cypress/integration/projectSettings/project_settings.ts @@ -211,7 +211,7 @@ describe("Repo Settings", { testIsolation: false }, () => { countCQFields(2); cy.dataCy("cq-enabled-radio-box").children().first().click(); - countCQFields(4); + countCQFields(7); cy.dataCy("error-banner") .contains( diff --git a/package.json b/package.json index e12c9f5fd8..f9a3c9e001 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "spruce", - "version": "3.0.104", + "version": "3.0.106", "private": true, "scripts": { "bootstrap-logkeeper": "./scripts/bootstrap-logkeeper.sh", @@ -125,7 +125,7 @@ "react-dom": "17.0.1", "react-router-dom": "6.11.1", "react-string-replace": "1.1.1", - "react-virtuoso": "^4.3.8", + "react-virtuoso": "^4.3.11", "react-window": "^1.8.9" }, "devDependencies": { diff --git a/scripts/setup-credentials.js b/scripts/setup-credentials.js index 89bb0aa30e..ca6681f5d2 100644 --- a/scripts/setup-credentials.js +++ b/scripts/setup-credentials.js @@ -21,6 +21,7 @@ const production = { REACT_APP_NEW_RELIC_LICENSE_KEY: process.env.REACT_APP_NEW_RELIC_LICENSE_KEY, REACT_APP_NEW_RELIC_TRUST_KEY: process.env.REACT_APP_NEW_RELIC_TRUST_KEY, REACT_APP_DEPLOYS_EMAIL: process.env.REACT_APP_DEPLOYS_EMAIL, + REACT_APP_HONEYCOMB_BASE_URL: process.env.REACT_APP_HONEYCOMB_BASE_URL, }; fs.writeFile(file, JSON.stringify({ production }), (err) => { if (err) { diff --git a/src/analytics/task/useTaskAnalytics.ts b/src/analytics/task/useTaskAnalytics.ts index 10b70d43c1..2727ff251c 100644 --- a/src/analytics/task/useTaskAnalytics.ts +++ b/src/analytics/task/useTaskAnalytics.ts @@ -66,6 +66,8 @@ type Action = | { name: "Click Display Task Link" } | { name: "Click Project Link" } | { name: "Click See History Button" } + | { name: "Click Trace Link" } + | { name: "Click Trace Metrics Link" } | { name: "Submit Previous Commit Selector"; type: CommitType }; interface P extends Properties { diff --git a/src/constants/cookies.ts b/src/constants/cookies.ts index 7bec40c0d4..763d119b5a 100644 --- a/src/constants/cookies.ts +++ b/src/constants/cookies.ts @@ -9,6 +9,7 @@ export const COMMIT_CHART_TYPE_VIEW_OPTIONS_ACCORDION = "commit-chart-view-options-accordion"; export const DISABLE_QUERY_POLLING = "disable-query-polling"; export const SEEN_MIGRATE_GUIDE_CUE = "seen-migrate-guide-cue"; +export const SEEN_HONEYCOMB_GUIDE_CUE = "seen-honeycomb-guide-cue"; export const INCLUDE_COMMIT_QUEUE_PROJECT_PATCHES = "include-commit-queue-project-patches"; export const INCLUDE_COMMIT_QUEUE_USER_PATCHES = diff --git a/src/constants/externalResources.test.ts b/src/constants/externalResources.test.ts index 18f441ca02..309ebb6816 100644 --- a/src/constants/externalResources.test.ts +++ b/src/constants/externalResources.test.ts @@ -2,6 +2,8 @@ import { getLobsterTestLogCompleteUrl, getParsleyBuildLogURL, getParsleyTestLogURL, + getHoneycombTraceUrl, + getHoneycombSystemMetricsUrl, } from "./externalResources"; describe("getLobsterTestLogCompleteUrl", () => { @@ -48,3 +50,27 @@ describe("getParsleyBuildLogURL", () => { expect(getParsleyBuildLogURL("myBuildId")).toBe("/resmoke/myBuildId/all"); }); }); + +describe("getTaskTraceUrl", () => { + it("generates the correct url", () => { + expect( + getHoneycombTraceUrl("abcdef", new Date("2023-07-07T19:08:41")) + ).toBe( + "/datasets/evergreen-agent/trace?trace_id=abcdef&trace_start_ts=1688756921" + ); + }); +}); + +describe("getTaskSystemMetricsUrl", () => { + it("generates the correct url", () => { + expect( + getHoneycombSystemMetricsUrl( + "task_12345", + new Date("2023-07-07T19:08:41"), + new Date("2023-07-07T20:00:00") + ) + ).toBe( + `/datasets/evergreen?query={"calculations":[{"op":"AVG","column":"system.memory.usage.used"},{"op":"AVG","column":"system.cpu.utilization"},{"op":"AVG","column":"system.network.io.transmit"},{"op":"AVG","column":"system.network.io.receive"}],"filters":[{"op":"=","column":"evergreen.task.id","value":"task_12345"}],"start_time":1688756921,"end_time":1688760000,"granularity":15}&omitMissingValues` + ); + }); +}); diff --git a/src/constants/externalResources.ts b/src/constants/externalResources.ts index dc96d4db61..87a2123c3a 100644 --- a/src/constants/externalResources.ts +++ b/src/constants/externalResources.ts @@ -1,7 +1,9 @@ +import { getUnixTime } from "date-fns"; import { LogTypes } from "types/task"; import { environmentVariables } from "utils"; -const { getLobsterURL, getParsleyUrl, getUiUrl } = environmentVariables; +const { getLobsterURL, getParsleyUrl, getUiUrl, getHoneycombBaseURL } = + environmentVariables; export const wikiBaseUrl = "https://docs.devprod.prod.corp.mongodb.com/evergreen"; @@ -36,6 +38,9 @@ export const getJiraImprovementUrl = (jiraHost: string) => export const konamiSoundTrackUrl = "https://www.myinstants.com/media/sounds/mvssf-win.mp3"; +export const githubMergeQueueUrl = + "https://github.blog/changelog/2023-02-08-pull-request-merge-queue-public-beta"; + export const legacyRoutes = { distros: "/distros", hosts: "/spawn", @@ -102,3 +107,31 @@ export const getParsleyBuildLogURL = (buildId: string) => export const getDistroPageUrl = (distroId: string) => `${getUiUrl()}/distros##${distroId}`; + +export const getHoneycombTraceUrl = (traceId: string, startTs: Date) => + `${getHoneycombBaseURL()}/datasets/evergreen-agent/trace?trace_id=${traceId}&trace_start_ts=${getUnixTime( + new Date(startTs) + )}`; + +export const getHoneycombSystemMetricsUrl = ( + taskId: string, + startTs: Date, + endTs: Date +): string => { + const query = { + calculations: [ + { op: "AVG", column: "system.memory.usage.used" }, + { op: "AVG", column: "system.cpu.utilization" }, + { op: "AVG", column: "system.network.io.transmit" }, + { op: "AVG", column: "system.network.io.receive" }, + ], + filters: [{ op: "=", column: "evergreen.task.id", value: taskId }], + start_time: getUnixTime(new Date(startTs)), + end_time: getUnixTime(new Date(endTs)), + granularity: 15, + }; + + return `${getHoneycombBaseURL()}/datasets/evergreen?query=${JSON.stringify( + query + )}&omitMissingValues`; +}; diff --git a/src/gql/fragments/projectSettings/githubCommitQueue.graphql b/src/gql/fragments/projectSettings/githubCommitQueue.graphql index d94d0fe843..bd29f8813d 100644 --- a/src/gql/fragments/projectSettings/githubCommitQueue.graphql +++ b/src/gql/fragments/projectSettings/githubCommitQueue.graphql @@ -2,6 +2,7 @@ fragment ProjectGithubSettings on Project { commitQueue { enabled mergeMethod + mergeQueue message } githubChecksEnabled @@ -17,6 +18,7 @@ fragment RepoGithubSettings on RepoRef { commitQueue { enabled mergeMethod + mergeQueue message } githubChecksEnabled diff --git a/src/gql/generated/types.ts b/src/gql/generated/types.ts index 1e71df9448..be3bf32d64 100644 --- a/src/gql/generated/types.ts +++ b/src/gql/generated/types.ts @@ -3098,6 +3098,7 @@ export type ProjectGithubSettingsFragment = { __typename?: "CommitQueueParams"; enabled?: boolean | null; mergeMethod: string; + mergeQueue: MergeQueue; message: string; }; }; @@ -3115,6 +3116,7 @@ export type RepoGithubSettingsFragment = { __typename?: "RepoCommitQueueParams"; enabled: boolean; mergeMethod: string; + mergeQueue: MergeQueue; message: string; }; }; @@ -3135,6 +3137,7 @@ export type ProjectGithubCommitQueueFragment = { __typename?: "CommitQueueParams"; enabled?: boolean | null; mergeMethod: string; + mergeQueue: MergeQueue; message: string; }; } | null; @@ -3156,6 +3159,7 @@ export type RepoGithubCommitQueueFragment = { __typename?: "RepoCommitQueueParams"; enabled: boolean; mergeMethod: string; + mergeQueue: MergeQueue; message: string; }; } | null; @@ -3177,6 +3181,7 @@ export type ProjectEventGithubCommitQueueFragment = { __typename?: "CommitQueueParams"; enabled?: boolean | null; mergeMethod: string; + mergeQueue: MergeQueue; message: string; }; } | null; @@ -3321,6 +3326,7 @@ export type ProjectSettingsFieldsFragment = { __typename?: "CommitQueueParams"; enabled?: boolean | null; mergeMethod: string; + mergeQueue: MergeQueue; message: string; }; } | null; @@ -3510,6 +3516,7 @@ export type RepoSettingsFieldsFragment = { __typename?: "RepoCommitQueueParams"; enabled: boolean; mergeMethod: string; + mergeQueue: MergeQueue; message: string; }; } | null; @@ -3901,6 +3908,7 @@ export type ProjectEventSettingsFragment = { __typename?: "CommitQueueParams"; enabled?: boolean | null; mergeMethod: string; + mergeQueue: MergeQueue; message: string; }; } | null; @@ -6109,6 +6117,7 @@ export type ProjectEventLogsQuery = { __typename?: "CommitQueueParams"; enabled?: boolean | null; mergeMethod: string; + mergeQueue: MergeQueue; message: string; }; } | null; @@ -6312,6 +6321,7 @@ export type ProjectEventLogsQuery = { __typename?: "CommitQueueParams"; enabled?: boolean | null; mergeMethod: string; + mergeQueue: MergeQueue; message: string; }; } | null; @@ -6530,6 +6540,7 @@ export type ProjectSettingsQuery = { __typename?: "CommitQueueParams"; enabled?: boolean | null; mergeMethod: string; + mergeQueue: MergeQueue; message: string; }; } | null; @@ -6772,6 +6783,7 @@ export type RepoEventLogsQuery = { __typename?: "CommitQueueParams"; enabled?: boolean | null; mergeMethod: string; + mergeQueue: MergeQueue; message: string; }; } | null; @@ -6975,6 +6987,7 @@ export type RepoEventLogsQuery = { __typename?: "CommitQueueParams"; enabled?: boolean | null; mergeMethod: string; + mergeQueue: MergeQueue; message: string; }; } | null; @@ -7177,6 +7190,7 @@ export type RepoSettingsQuery = { __typename?: "RepoCommitQueueParams"; enabled: boolean; mergeMethod: string; + mergeQueue: MergeQueue; message: string; }; } | null; @@ -7616,6 +7630,7 @@ export type TaskQuery = { status: string; timedOut?: boolean | null; timeoutType?: string | null; + traceID?: string | null; type: string; oomTracker: { __typename?: "OomTrackerInfo"; diff --git a/src/gql/queries/get-task.graphql b/src/gql/queries/get-task.graphql index 4a766e4590..e56c555288 100644 --- a/src/gql/queries/get-task.graphql +++ b/src/gql/queries/get-task.graphql @@ -53,6 +53,7 @@ query Task($taskId: String!, $execution: Int) { status timedOut timeoutType + traceID type } displayTask { diff --git a/src/pages/commits/ActiveCommits/BuildVariantCard/BuildVariantCard.test.tsx b/src/pages/commits/ActiveCommits/BuildVariantCard/BuildVariantCard.test.tsx index 968a88a76c..a83e6a84c8 100644 --- a/src/pages/commits/ActiveCommits/BuildVariantCard/BuildVariantCard.test.tsx +++ b/src/pages/commits/ActiveCommits/BuildVariantCard/BuildVariantCard.test.tsx @@ -1,4 +1,8 @@ import { MockedProvider } from "@apollo/client/testing"; +import { + injectGlobalDimStyle, + removeGlobalDimStyle, +} from "pages/commits/ActiveCommits/utils"; import { renderWithRouterMatch as render, screen, @@ -6,7 +10,6 @@ import { waitFor, } from "test_utils"; import { BuildVariantCard } from "."; -import { injectGlobalDimStyle, removeGlobalDimStyle } from "../utils"; jest.mock("../utils"); diff --git a/src/pages/commits/ActiveCommits/BuildVariantCard/index.tsx b/src/pages/commits/ActiveCommits/BuildVariantCard/index.tsx index 8af7e0584a..7216a6684b 100644 --- a/src/pages/commits/ActiveCommits/BuildVariantCard/index.tsx +++ b/src/pages/commits/ActiveCommits/BuildVariantCard/index.tsx @@ -5,12 +5,12 @@ import VisibilityContainer from "components/VisibilityContainer"; import { getVariantHistoryRoute } from "constants/routes"; import { size } from "constants/tokens"; import { StatusCount } from "gql/generated/types"; +import { VariantGroupedTaskStatusBadges } from "pages/commits/ActiveCommits/BuildVariantCard/VariantGroupedTaskStatusBadges"; import { injectGlobalDimStyle, removeGlobalDimStyle, } from "pages/commits/ActiveCommits/utils"; import { TASK_ICON_PADDING } from "pages/commits/constants"; -import { VariantGroupedTaskStatusBadges } from "./VariantGroupedTaskStatusBadges"; import { WaterfallTaskStatusIcon } from "./WaterfallTaskStatusIcon"; type taskList = { diff --git a/src/pages/commits/ActiveCommits/CommitChart/CommitChart.stories.tsx b/src/pages/commits/ActiveCommits/CommitBarChart/CommitBarChart.stories.tsx similarity index 94% rename from src/pages/commits/ActiveCommits/CommitChart/CommitChart.stories.tsx rename to src/pages/commits/ActiveCommits/CommitBarChart/CommitBarChart.stories.tsx index c451a33328..845e71852c 100644 --- a/src/pages/commits/ActiveCommits/CommitChart/CommitChart.stories.tsx +++ b/src/pages/commits/ActiveCommits/CommitBarChart/CommitBarChart.stories.tsx @@ -1,22 +1,22 @@ import styled from "@emotion/styled"; -import { CustomStoryObj, CustomMeta } from "test_utils/types"; -import { ChartTypes, Commits } from "types/commits"; -import { CommitChart } from "."; +import { StoryObj } from "@storybook/react"; import { findMaxGroupedTaskStats, getAllTaskStatsGroupedByColor, -} from "../utils"; +} from "pages/commits/utils"; +import { ChartTypes, Commits } from "types/commits"; +import { CommitBarChart } from "."; export default { - title: "Pages/Commits/Charts", - component: CommitChart, -} satisfies CustomMeta; + title: "Pages/Commits/Charts/ActiveCommit/CommitBarChart", + component: CommitBarChart, +}; -export const Default: CustomStoryObj = { +export const Default: StoryObj = { render: ({ chartType }) => ( {versions.map((item) => ( - { afterEach(() => { @@ -14,7 +14,7 @@ describe("commitChart", () => { it("display right amount of bars", () => { render( - { it("hovering over the chart should open a tooltip", async () => { render( - { it("should show all umbrella statuses (normal and dimmed) and their counts", async () => { render( -
= ({ +export const CommitBarChart: React.VFC = ({ max, chartType, groupedTaskStats, diff --git a/src/pages/commits/ActiveCommits/__snapshots__/ProjectHealth.stories.storyshot b/src/pages/commits/ActiveCommits/__snapshots__/ProjectHealth.stories.storyshot deleted file mode 100644 index d01b9c5748..0000000000 --- a/src/pages/commits/ActiveCommits/__snapshots__/ProjectHealth.stories.storyshot +++ /dev/null @@ -1,1407 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`storybook Storyshots Pages/Commits/Project Health Page Default 1`] = ` -
-
-
-
-
- - - - - Project Health - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 10 -
-
- 8 -
-
- 6 -
-
- 4 -
-
- 2 -
-
- 0 -
-
-
-
-
-
-
-
-
-
-
-
-
- - - - - View options - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-

- - - 987bf57 - - - - - Jul 16, 2021, 3:53 PM - - -

-

- Jonathan Brill - - -

-

- - - - EVG-14901 - - - add ssh key to EditSpawnHos... -

- - more - -
-
-
-
-

- - - a77bd39 - - - - - Jul 13, 2021, 2:51 PM - - -

-

- Mohamed Khelif - - -

-

- Triggered From Git Tag 'v2.11.1': v2.... -

- - more - -
-
-
-
- -
- 1 -
- Inactive -
-
-
-
-
-

- - - 9c1d1eb - - - - - Jul 13, 2021, 2:51 PM - - -

-

- Mohamed Khelif - - -

-

- - - - EVG-14799 - - - Correctly visit configure p... -

- - more - -
-
-
-
-

- - - f7f7f1a - - - - - Jul 13, 2021, 2:51 PM - - -

-

- Sophie Stadler - - -

-

- Remove navigation announcement toast ... -

- - more - -
-
-
-
-

- - - 211b3a0 - - - - - Jul 13, 2021, 2:51 PM - - -

-

- Chaya Malik - - -

-

- Triggered From Git Tag 'v2.11.0': v2.... -

- - more - -
-
-
-
- -
-
-`; diff --git a/src/pages/commits/ActiveCommits/index.tsx b/src/pages/commits/ActiveCommits/index.tsx index 33ec8d4b00..c7d612fee8 100644 --- a/src/pages/commits/ActiveCommits/index.tsx +++ b/src/pages/commits/ActiveCommits/index.tsx @@ -2,37 +2,12 @@ import { useMemo } from "react"; import styled from "@emotion/styled"; import { useProjectHealthAnalytics } from "analytics/projectHealth/useProjectHealthAnalytics"; import CommitChartLabel from "components/CommitChartLabel"; -import { ChartTypes, CommitVersion, BuildVariantDict } from "types/commits"; +import { CommitVersion, BuildVariantDict } from "types/commits"; import { array, string } from "utils"; import { BuildVariantCard } from "./BuildVariantCard"; -import { CommitChart } from "./CommitChart"; -import { ColorCount } from "./utils"; const { convertArrayToObject, arrayUnion } = array; const { shortenGithash } = string; -interface ActiveCommitChartProps { - groupedTaskStats: ColorCount[]; - max: number; - total: number; - chartType: ChartTypes; - eta?: Date; -} - -export const ActiveCommitChart: React.VFC = ({ - groupedTaskStats, - max, - total, - chartType, - eta, -}) => ( - -); interface ActiveCommitLabelProps { version: CommitVersion; diff --git a/src/pages/commits/ActiveCommits/utils.test.ts b/src/pages/commits/ActiveCommits/utils.test.ts index 0c16e196b0..738c41cfd0 100644 --- a/src/pages/commits/ActiveCommits/utils.test.ts +++ b/src/pages/commits/ActiveCommits/utils.test.ts @@ -1,219 +1,13 @@ -import { palette } from "@leafygreen-ui/palette"; -import { taskStatusToCopy } from "constants/task"; import { TaskStatus } from "types/task"; +import { groupedTaskStats, groupedTaskStatsAll } from "../testData"; import { - TASK_ICON_HEIGHT, - TASK_ICON_PADDING, - GROUPED_BADGE_HEIGHT, - GROUPED_BADGE_PADDING, -} from "../constants"; -import { - constructBuildVariantDict, - getAllTaskStatsGroupedByColor, getStatusesWithZeroCount, injectGlobalDimStyle, injectGlobalHighlightStyle, - roundMax, removeGlobalDimStyle, removeGlobalHighlightStyle, } from "./utils"; -const { red, green, yellow, gray, purple } = palette; - -describe("getAllTaskStatsGroupedByColor", () => { - it("grab the taskStatusStats.statusCounts field from all versions, returns mapping between version id to its {grouped task stats, max, total}", () => { - expect(getAllTaskStatsGroupedByColor(versions)).toStrictEqual({ - "12": { - stats: [ - { - count: 8, - statuses: [ - taskStatusToCopy[TaskStatus.TestTimedOut], - taskStatusToCopy[TaskStatus.Failed], - ], - color: red.base, - umbrellaStatus: TaskStatus.FailedUmbrella, - statusCounts: { - [TaskStatus.TestTimedOut]: 6, - [TaskStatus.Failed]: 2, - }, - }, - { - count: 7, - statuses: [ - taskStatusToCopy[TaskStatus.SystemTimedOut], - taskStatusToCopy[TaskStatus.SystemUnresponsive], - ], - color: purple.dark2, - umbrellaStatus: TaskStatus.SystemFailureUmbrella, - statusCounts: { - [TaskStatus.SystemTimedOut]: 5, - [TaskStatus.SystemUnresponsive]: 2, - }, - }, - { - count: 4, - statuses: [taskStatusToCopy[TaskStatus.Dispatched]], - color: yellow.base, - umbrellaStatus: TaskStatus.RunningUmbrella, - statusCounts: { [TaskStatus.Dispatched]: 4 }, - }, - { - count: 2, - statuses: [taskStatusToCopy[TaskStatus.WillRun]], - color: gray.base, - umbrellaStatus: TaskStatus.ScheduledUmbrella, - statusCounts: { [TaskStatus.WillRun]: 2 }, - }, - ], - max: 8, - total: 21, - }, - "13": { - stats: [ - { - count: 6, - statuses: [taskStatusToCopy[TaskStatus.Succeeded]], - color: green.dark1, - umbrellaStatus: TaskStatus.Succeeded, - statusCounts: { [TaskStatus.Succeeded]: 6 }, - }, - { - count: 2, - statuses: [taskStatusToCopy[TaskStatus.Failed]], - color: red.base, - umbrellaStatus: TaskStatus.FailedUmbrella, - statusCounts: { [TaskStatus.Failed]: 2 }, - }, - { - count: 9, - statuses: [ - taskStatusToCopy[TaskStatus.Dispatched], - taskStatusToCopy[TaskStatus.Started], - ], - color: yellow.base, - umbrellaStatus: TaskStatus.RunningUmbrella, - statusCounts: { - [TaskStatus.Dispatched]: 4, - [TaskStatus.Started]: 5, - }, - }, - ], - max: 9, - total: 17, - }, - "14": { - stats: [ - { - count: 4, - statuses: [taskStatusToCopy[TaskStatus.Succeeded]], - color: green.dark1, - umbrellaStatus: TaskStatus.Succeeded, - statusCounts: { [TaskStatus.Succeeded]: 4 }, - }, - { - count: 6, - statuses: [taskStatusToCopy[TaskStatus.TaskTimedOut]], - color: red.base, - umbrellaStatus: TaskStatus.FailedUmbrella, - statusCounts: { [TaskStatus.TaskTimedOut]: 6 }, - }, - { - count: 7, - statuses: [ - taskStatusToCopy[TaskStatus.SystemFailed], - taskStatusToCopy[TaskStatus.SystemUnresponsive], - ], - color: purple.dark2, - umbrellaStatus: TaskStatus.SystemFailureUmbrella, - statusCounts: { - [TaskStatus.SystemFailed]: 5, - [TaskStatus.SystemUnresponsive]: 2, - }, - }, - { - count: 3, - statuses: [taskStatusToCopy[TaskStatus.SetupFailed]], - color: purple.light2, - umbrellaStatus: TaskStatus.SetupFailed, - statusCounts: { [TaskStatus.SetupFailed]: 3 }, - }, - { - count: 3, - statuses: [taskStatusToCopy[TaskStatus.Started]], - color: yellow.base, - umbrellaStatus: TaskStatus.RunningUmbrella, - statusCounts: { started: 3 }, - }, - { - count: 2, - statuses: [taskStatusToCopy[TaskStatus.Unscheduled]], - color: gray.dark1, - umbrellaStatus: TaskStatus.UndispatchedUmbrella, - statusCounts: { [TaskStatus.Unscheduled]: 2 }, - }, - ], - max: 7, - total: 25, - }, - "123": { - stats: [ - { - count: 4, - statuses: [taskStatusToCopy[TaskStatus.Succeeded]], - color: green.dark1, - umbrellaStatus: TaskStatus.Succeeded, - statusCounts: { [TaskStatus.Succeeded]: 4 }, - }, - { - count: 6, - statuses: [taskStatusToCopy[TaskStatus.TaskTimedOut]], - color: red.base, - umbrellaStatus: TaskStatus.FailedUmbrella, - statusCounts: { [TaskStatus.TaskTimedOut]: 6 }, - }, - { - count: 7, - statuses: [ - taskStatusToCopy[TaskStatus.SystemFailed], - taskStatusToCopy[TaskStatus.SystemUnresponsive], - ], - color: purple.dark2, - umbrellaStatus: TaskStatus.SystemFailureUmbrella, - statusCounts: { - [TaskStatus.SystemFailed]: 5, - [TaskStatus.SystemUnresponsive]: 2, - }, - }, - { - count: 3, - statuses: [taskStatusToCopy[TaskStatus.SetupFailed]], - color: purple.light2, - umbrellaStatus: TaskStatus.SetupFailed, - statusCounts: { [TaskStatus.SetupFailed]: 3 }, - }, - { - count: 3, - statuses: [taskStatusToCopy[TaskStatus.Started]], - color: yellow.base, - umbrellaStatus: TaskStatus.RunningUmbrella, - statusCounts: { started: 3 }, - }, - { - count: 2, - statuses: [taskStatusToCopy[TaskStatus.Unscheduled]], - color: gray.dark1, - umbrellaStatus: TaskStatus.UndispatchedUmbrella, - statusCounts: { [TaskStatus.Unscheduled]: 2 }, - }, - ], - max: 7, - total: 25, - }, - }); - }); -}); - describe("getStatusesWithZeroCount", () => { it("return an array of umbrella statuses that have 0 count", () => { expect(getStatusesWithZeroCount(groupedTaskStats)).toStrictEqual([ @@ -240,296 +34,6 @@ describe("getStatusesWithZeroCount", () => { }); }); -describe("constructBuildVariantDict", () => { - it("correctly determines priority, iconHeight, and badgeHeight", () => { - expect(constructBuildVariantDict(versions)).toStrictEqual({ - "enterprise-macos-cxx20": { - iconHeight: TASK_ICON_HEIGHT + TASK_ICON_PADDING * 2, - badgeHeight: GROUPED_BADGE_HEIGHT * 2 + GROUPED_BADGE_PADDING * 2, - priority: 4, - }, - "enterprise-windows-benchmarks": { - iconHeight: TASK_ICON_HEIGHT + TASK_ICON_PADDING * 2, - badgeHeight: 0, - priority: 2, - }, - "enterprise-rhel-80-64-bit-inmem": { - iconHeight: TASK_ICON_HEIGHT + TASK_ICON_PADDING * 2, - badgeHeight: 0, - priority: 1, - }, - }); - }); -}); - -const buildVariant1 = { - displayName: "Enterprise macOS C++20 DEBUG", - variant: "enterprise-macos-cxx20", - tasks: [ - { - status: TaskStatus.WillRun, - id: "auth", - execution: 0, - displayName: "auth", - failedTestCount: 0, - }, - ], -}; - -const buildVariant2 = { - displayName: "~ Enterprise Windows (Benchmarks)", - variant: "enterprise-windows-benchmarks", - tasks: [ - { - status: TaskStatus.Pending, - id: "benchmarks", - execution: 0, - displayName: "benchmarks", - failedTestCount: 0, - }, - ], -}; - -const buildVariant3 = { - displayName: "Enterprise RHEL 8.0 (inMemory)", - variant: "enterprise-rhel-80-64-bit-inmem", - tasks: [ - { - status: TaskStatus.Failed, - id: "fuzzer", - execution: 0, - displayName: "fuzzer", - failedTestCount: 1, - }, - ], -}; - -const buildVariantStat = { - displayName: "Enterprise macOS C++20 DEBUG", - variant: "enterprise-macos-cxx20", - statusCounts: [ - { - count: 4, - status: TaskStatus.Blocked, - }, - { - count: 1, - status: TaskStatus.Succeeded, - }, - { - count: 1, - status: TaskStatus.Failed, - }, - ], -}; - -const versions = [ - { - version: { - id: "123", - projectIdentifier: "mongodb-mongo-master", - createTime: new Date("2021-06-16T23:38:13Z"), - message: "SERVER-57332 Create skeleton InternalDocumentSourceDensify", - order: 39369, - author: "Mohamed Khelif", - revision: "4337c33fa4a0d5c747a1115f0853b5f70e46f112", - taskStatusStats: { - eta: null, - counts: [ - { status: TaskStatus.TaskTimedOut, count: 6 }, - { status: TaskStatus.Succeeded, count: 4 }, - { status: TaskStatus.Started, count: 3 }, - { status: TaskStatus.SystemFailed, count: 5 }, - { status: TaskStatus.Unscheduled, count: 2 }, - { status: TaskStatus.SetupFailed, count: 3 }, - { status: TaskStatus.SystemUnresponsive, count: 2 }, - ], - }, - buildVariants: [buildVariant1], - buildVariantStats: [buildVariantStat], - gitTags: null, - }, - rolledUpVersions: null, - }, - { - version: { - id: "12", - projectIdentifier: "mongodb-mongo-master", - createTime: new Date("2021-06-16T23:38:13Z"), - message: "SERVER-57333 Some complicated server commit", - order: 39368, - author: "Arjun Patel", - revision: "4337c33fa4a0d5c747a1115f0853b5f70e46f112", - taskStatusStats: { - eta: null, - counts: [ - { status: TaskStatus.TestTimedOut, count: 6 }, - { status: TaskStatus.Failed, count: 2 }, - { status: TaskStatus.Dispatched, count: 4 }, - { status: TaskStatus.WillRun, count: 2 }, - { status: TaskStatus.SystemTimedOut, count: 5 }, - { status: TaskStatus.SystemUnresponsive, count: 2 }, - ], - }, - buildVariants: [buildVariant1, buildVariant2], - buildVariantStats: [], - gitTags: null, - }, - rolledUpVersions: null, - }, - { - version: { - id: "13", - projectIdentifier: "mongodb-mongo-master", - createTime: new Date("2021-06-16T23:38:13Z"), - message: "SERVER-57332 Create skeleton InternalDocumentSourceDensify", - order: 39367, - author: "Mohamed Khelif", - revision: "4337c33fa4a0d5c747a1115f0853b5f70e46f112", - taskStatusStats: { - eta: null, - counts: [ - { status: TaskStatus.Succeeded, count: 6 }, - { status: TaskStatus.Failed, count: 2 }, - { status: TaskStatus.Dispatched, count: 4 }, - { status: TaskStatus.Started, count: 5 }, - ], - }, - buildVariants: [buildVariant1, buildVariant2, buildVariant3], - buildVariantStats: [], - gitTags: null, - }, - rolledUpVersions: null, - }, - { - version: { - id: "14", - projectIdentifier: "mongodb-mongo-master", - createTime: new Date("2021-06-16T23:38:13Z"), - message: "SERVER-57333 Some complicated server commit", - order: 39366, - author: "Arjun Patel", - revision: "4337c33fa4a0d5c747a1115f0853b5f70e46f112", - taskStatusStats: { - eta: null, - counts: [ - { status: TaskStatus.TaskTimedOut, count: 6 }, - { status: TaskStatus.Succeeded, count: 4 }, - { status: TaskStatus.Started, count: 3 }, - { status: TaskStatus.SystemFailed, count: 5 }, - { status: TaskStatus.Unscheduled, count: 2 }, - { status: TaskStatus.SetupFailed, count: 3 }, - { status: TaskStatus.SystemUnresponsive, count: 2 }, - ], - }, - buildVariants: [buildVariant1], - buildVariantStats: [], - gitTags: null, - }, - rolledUpVersions: null, - }, -]; - -const groupedTaskStatsAll = [ - { - umbrellaStatus: TaskStatus.Succeeded, - count: 2, - statuses: [TaskStatus.Succeeded], - color: green.dark1, - }, - { - umbrellaStatus: TaskStatus.FailedUmbrella, - count: 1, - statuses: [TaskStatus.Failed], - color: red.base, - }, - { - umbrellaStatus: TaskStatus.SystemFailureUmbrella, - count: 3, - statuses: [TaskStatus.SystemFailed], - color: purple.dark2, - }, - { - umbrellaStatus: TaskStatus.SetupFailed, - count: 5, - statuses: [TaskStatus.SetupFailed], - color: purple.light2, - }, - { - umbrellaStatus: TaskStatus.Undispatched, - count: 1, - statuses: [TaskStatus.Unscheduled], - color: gray.dark1, - }, - { - umbrellaStatus: TaskStatus.RunningUmbrella, - count: 6, - statuses: [TaskStatus.Started], - color: yellow.base, - }, - { - umbrellaStatus: TaskStatus.Dispatched, - count: 7, - statuses: [TaskStatus.Dispatched], - color: gray.dark1, - }, - { - umbrellaStatus: TaskStatus.Inactive, - count: 7, - statuses: [TaskStatus.Inactive], - color: gray.dark1, - }, - { - umbrellaStatus: TaskStatus.ScheduledUmbrella, - count: 7, - statuses: [TaskStatus.WillRun], - color: gray.dark1, - }, - { - umbrellaStatus: TaskStatus.UndispatchedUmbrella, - count: 7, - statuses: [TaskStatus.Unscheduled], - color: gray.dark1, - }, -]; - -const groupedTaskStats = [ - { - umbrellaStatus: TaskStatus.Succeeded, - count: 2, - statuses: [TaskStatus.Succeeded], - color: green.dark1, - }, - { - umbrellaStatus: TaskStatus.Failed, - count: 1, - statuses: [TaskStatus.Failed], - color: red.base, - }, - { - umbrellaStatus: TaskStatus.Unscheduled, - count: 1, - statuses: [TaskStatus.Unscheduled], - color: gray.dark1, - }, - { - umbrellaStatus: TaskStatus.SetupFailed, - count: 5, - statuses: [TaskStatus.SetupFailed], - color: purple.light2, - }, -]; - -describe("roundMax", () => { - it("properly rounds numbers", () => { - expect(roundMax(8)).toBe(10); // 0 <= x < 100 - expect(roundMax(147)).toBe(150); // 100 <= x < 500 - expect(roundMax(712)).toBe(800); // 500 <= x < 1000 - expect(roundMax(1320)).toBe(1500); // 1000 <= x < 5000 - expect(roundMax(6430)).toBe(7000); // 5000 <= x - }); -}); - describe("injectGlobalStyle", () => { it("should properly inject global style using the task identifier", () => { const dimIconStyle = "dim-icon-style"; diff --git a/src/pages/commits/ActiveCommits/utils.ts b/src/pages/commits/ActiveCommits/utils.ts index fe30cda3ab..8b869c5e54 100644 --- a/src/pages/commits/ActiveCommits/utils.ts +++ b/src/pages/commits/ActiveCommits/utils.ts @@ -1,114 +1,7 @@ import { mapTaskStatusToUmbrellaStatus } from "constants/task"; -import { ChartTypes, Commits, BuildVariantDict } from "types/commits"; -import { groupStatusesByUmbrellaStatus } from "utils/statuses"; -import { - TASK_ICONS_PER_ROW, - TASK_ICON_HEIGHT, - TASK_ICON_PADDING, - GROUPED_BADGES_PER_ROW, - GROUPED_BADGE_HEIGHT, - GROUPED_BADGE_PADDING, -} from "../constants"; - -export type ColorCount = { - count: number; - statuses: string[]; - color: string; - umbrellaStatus: string; -}; - -export type GroupedResult = { - stats: ColorCount[]; - max: number; - total: number; -}; - -export const findMaxGroupedTaskStats = (groupedTaskStats: { - [id: string]: GroupedResult; -}) => { - if (Object.keys(groupedTaskStats).length === 0) { - return { max: 0 }; - } - return Object.values(groupedTaskStats).reduce((prev, curr) => - prev.max > curr.max ? prev : curr - ); -}; - -export const getAllTaskStatsGroupedByColor = (versions: Commits) => { - const idToGroupedTaskStats: { [id: string]: GroupedResult } = {}; - versions.forEach(({ version }) => { - if (version != null) { - idToGroupedTaskStats[version.id] = groupStatusesByUmbrellaStatus( - version.taskStatusStats?.counts - ); - } - }); - - return idToGroupedTaskStats; -}; - -export const constructBuildVariantDict = ( - versions: Commits -): BuildVariantDict => { - const buildVariantDict: BuildVariantDict = {}; - - for (let i = 0; i < versions.length; i++) { - const { version } = versions[i]; - - // skip if inactive/unmatching - if (version) { - // Deduplicate build variants and build variant stats by consolidating into a single object. - const allBuildVariants = [ - ...version.buildVariants, - ...version.buildVariantStats, - ].reduce((acc, curr) => { - const { variant } = curr; - acc[variant] = { ...acc[variant], ...curr }; - return acc; - }, {}); - - // Construct build variant dict which will contain information needed for rendering. - Object.values(allBuildVariants).reduce( - (acc, { tasks, statusCounts, variant }) => { - // Determine height to allocate for icons. - let iconHeight = 0; - if (tasks) { - const numRows = Math.ceil(tasks.length / TASK_ICONS_PER_ROW); - const iconContainerHeight = numRows * TASK_ICON_HEIGHT; - const iconContainerPadding = TASK_ICON_PADDING * 2; - iconHeight = iconContainerHeight + iconContainerPadding; - } - - // Determine height to allocate for grouped badges. - let badgeHeight = 0; - if (statusCounts) { - const numRows = Math.ceil( - statusCounts.length / GROUPED_BADGES_PER_ROW - ); - const badgeContainerHeight = numRows * GROUPED_BADGE_HEIGHT; - const badgeContainerPadding = GROUPED_BADGE_PADDING * 2; - badgeHeight = badgeContainerHeight + badgeContainerPadding; - } - - if (acc[variant]) { - if (iconHeight > acc[variant].iconHeight) { - acc[variant].iconHeight = iconHeight; - } - if (badgeHeight > acc[variant].badgeHeight) { - acc[variant].badgeHeight = badgeHeight; - } - acc[variant].priority += 1; - } else { - acc[variant] = { priority: 1, iconHeight, badgeHeight }; - } - return acc; - }, - buildVariantDict - ); - } - } - return buildVariantDict; -}; +import { ColorCount } from "pages/commits/types"; +import { ChartTypes } from "types/commits"; +import { roundMax } from "utils/numbers"; /** * `calculateBarHeight` calculates the height of a single bar in a bar chart. @@ -118,42 +11,21 @@ export const constructBuildVariantDict = ( * @param chartType - the type of chart (percentage or absolute) * @returns the percentage height of the bar */ -export function calculateBarHeight( +const calculateBarHeight = ( value: number, max: number, total: number, chartType: string -) { +) => { if (chartType === ChartTypes.Percentage) { return `${(value / total) * 100}%`; } const roundedMax = roundMax(max); return `${(value / roundedMax) * 100}%`; -} - -export const roundMax = (max: number) => { - if (max < 100) { - // Round up to nearest 10 - return Math.ceil(max / 10) * 10; - } - if (max < 500) { - // Round up to nearest 50 - return Math.ceil(max / 50) * 50; - } - if (max < 1000) { - // Round up to nearest 100 - return Math.ceil(max / 100) * 100; - } - if (max < 5000) { - // Round up to nearest 500 - return Math.ceil(max / 500) * 500; - } - // Else round up to nearest 1000 - return Math.ceil(max / 1000) * 1000; }; // Find zero count statuses for commit chart tooltip -export const getStatusesWithZeroCount = (colors: ColorCount[]) => { +const getStatusesWithZeroCount = (colors: ColorCount[]) => { const availableStatuses = colors.map(({ umbrellaStatus }) => umbrellaStatus); const allStatuses = Object.values(mapTaskStatusToUmbrellaStatus); return Array.from( @@ -164,11 +36,11 @@ export const getStatusesWithZeroCount = (colors: ColorCount[]) => { // Functions for injecting and removing style for hovering on task icons const dimIconStyle = "dim-icon-style"; -export const removeGlobalDimStyle = () => { +const removeGlobalDimStyle = () => { document.getElementById(dimIconStyle)?.remove(); }; -export const injectGlobalDimStyle = () => { +const injectGlobalDimStyle = () => { // Remove style here again because hovering over LG tooltips triggers two consecutive mouseenter events. removeGlobalDimStyle(); @@ -185,11 +57,11 @@ export const injectGlobalDimStyle = () => { // Functions for injecting and removing style for hovering on task icons const taskIconStyle = "task-icon-style"; -export const removeGlobalHighlightStyle = () => { +const removeGlobalHighlightStyle = () => { document.getElementById(taskIconStyle)?.remove(); }; -export const injectGlobalHighlightStyle = (taskIdentifier: string) => { +const injectGlobalHighlightStyle = (taskIdentifier: string) => { // Remove style here again because hovering over LG tooltips triggers two consecutive mouseenter events. removeGlobalHighlightStyle(); @@ -202,3 +74,12 @@ export const injectGlobalHighlightStyle = (taskIdentifier: string) => { `; document.getElementsByTagName("head")[0].appendChild(hoverStyle); }; + +export { + calculateBarHeight, + getStatusesWithZeroCount, + injectGlobalDimStyle, + injectGlobalHighlightStyle, + removeGlobalDimStyle, + removeGlobalHighlightStyle, +}; diff --git a/src/pages/commits/ActiveCommits/ChartToggle.tsx b/src/pages/commits/CommitChart/ChartToggle.tsx similarity index 100% rename from src/pages/commits/ActiveCommits/ChartToggle.tsx rename to src/pages/commits/CommitChart/ChartToggle.tsx diff --git a/src/pages/commits/ActiveCommits/Grid.tsx b/src/pages/commits/CommitChart/Grid.tsx similarity index 100% rename from src/pages/commits/ActiveCommits/Grid.tsx rename to src/pages/commits/CommitChart/Grid.tsx diff --git a/src/pages/commits/ActiveCommits/GridLabel.tsx b/src/pages/commits/CommitChart/GridLabel.tsx similarity index 94% rename from src/pages/commits/ActiveCommits/GridLabel.tsx rename to src/pages/commits/CommitChart/GridLabel.tsx index 4c811e6cce..7b0669122c 100644 --- a/src/pages/commits/ActiveCommits/GridLabel.tsx +++ b/src/pages/commits/CommitChart/GridLabel.tsx @@ -3,10 +3,9 @@ import { palette } from "@leafygreen-ui/palette"; import { zIndex, size } from "constants/tokens"; import { gridHeight } from "pages/commits/constants"; import { ChartTypes } from "types/commits"; -import { array } from "utils"; -import { roundMax } from "./utils"; +import { range } from "utils/array"; +import { roundMax } from "utils/numbers"; -const { range } = array; const { gray } = palette; const percentages = [100, 80, 60, 40, 20, 0]; diff --git a/src/pages/commits/CommitsChart.tsx b/src/pages/commits/CommitChart/index.tsx similarity index 91% rename from src/pages/commits/CommitsChart.tsx rename to src/pages/commits/CommitChart/index.tsx index 650aee15bf..7cbb5320b1 100644 --- a/src/pages/commits/CommitsChart.tsx +++ b/src/pages/commits/CommitChart/index.tsx @@ -6,19 +6,19 @@ import { COMMIT_CHART_TYPE_VIEW_OPTIONS_ACCORDION } from "constants/cookies"; import { size } from "constants/tokens"; import { useQueryParam } from "hooks/useQueryParam"; import { ChartTypes, Commits, ChartToggleQueryParams } from "types/commits"; -import { ChartToggle } from "./ActiveCommits/ChartToggle"; -import { Grid, SolidLine } from "./ActiveCommits/Grid"; -import { GridLabel } from "./ActiveCommits/GridLabel"; -import { - getAllTaskStatsGroupedByColor, - findMaxGroupedTaskStats, -} from "./ActiveCommits/utils"; import { getCommitKey, getCommitWidth, RenderCommitsChart, -} from "./RenderCommit"; -import { FlexRowContainer, CommitWrapper } from "./styles"; +} from "../RenderCommit"; +import { FlexRowContainer, CommitWrapper } from "../styles"; +import { + findMaxGroupedTaskStats, + getAllTaskStatsGroupedByColor, +} from "../utils"; +import { ChartToggle } from "./ChartToggle"; +import { Grid, SolidLine } from "./Grid"; +import { GridLabel } from "./GridLabel"; const DEFAULT_CHART_TYPE = ChartTypes.Absolute; const DEFAULT_OPEN_STATE = true; @@ -29,7 +29,7 @@ interface Props { hasError?: boolean; } -export const CommitsChart: React.VFC = ({ +export const CommitChart: React.VFC = ({ versions, hasTaskFilter, hasError = false, diff --git a/src/pages/commits/commitTypeSelect/index.tsx b/src/pages/commits/CommitTypeSelector/index.tsx similarity index 100% rename from src/pages/commits/commitTypeSelect/index.tsx rename to src/pages/commits/CommitTypeSelector/index.tsx diff --git a/src/pages/commits/CommitsWrapper.tsx b/src/pages/commits/CommitsWrapper.tsx index 7c708b68ed..ccf586ac2c 100644 --- a/src/pages/commits/CommitsWrapper.tsx +++ b/src/pages/commits/CommitsWrapper.tsx @@ -5,8 +5,7 @@ import { palette } from "@leafygreen-ui/palette"; import { Skeleton } from "antd"; import { size } from "constants/tokens"; import { Commits } from "types/commits"; -import { constructBuildVariantDict } from "./ActiveCommits/utils"; -import { CommitsChart } from "./CommitsChart"; +import { CommitChart } from "./CommitChart"; import { getCommitKey, getCommitWidth, @@ -14,6 +13,7 @@ import { RenderCommitsBuildVariants, } from "./RenderCommit"; import { FlexRowContainer, CommitWrapper } from "./styles"; +import { constructBuildVariantDict } from "./utils"; const { white } = palette; @@ -39,7 +39,7 @@ export const CommitsWrapper: React.VFC = ({ }, [versions]); if (error) { - return ; + return ; } if (isLoading) { return ; @@ -47,7 +47,7 @@ export const CommitsWrapper: React.VFC = ({ if (versions) { return ( - + {versions.map((commit) => ( diff --git a/src/pages/commits/RenderCommit.tsx b/src/pages/commits/RenderCommit.tsx index 04079baf94..351f0f1b4e 100644 --- a/src/pages/commits/RenderCommit.tsx +++ b/src/pages/commits/RenderCommit.tsx @@ -1,11 +1,8 @@ import { ChartTypes, Commit, BuildVariantDict } from "types/commits"; -import { - ActiveCommitChart, - ActiveCommitLabel, - BuildVariantContainer, -} from "./ActiveCommits"; -import { GroupedResult } from "./ActiveCommits/utils"; +import { ActiveCommitLabel, BuildVariantContainer } from "./ActiveCommits"; +import { CommitBarChart } from "./ActiveCommits/CommitBarChart"; import { InactiveCommitsLine, InactiveCommitButton } from "./InactiveCommits"; +import { GroupedResult } from "./types"; type ActiveCommitProps = { groupedResult: { [key: string]: GroupedResult }; @@ -28,7 +25,7 @@ const RenderCommitsChart: React.VFC = ({ if (version) { return ( -
10
8
6
4
2
0
{ - + { it("should return an object containing booleans that describe what filters have been applied", () => { expect( @@ -720,3 +733,219 @@ describe("getMainlineCommitsQueryVariables", () => { }); }); }); + +describe("getAllTaskStatsGroupedByColor", () => { + it("grab the taskStatusStats.statusCounts field from all versions, returns mapping between version id to its {grouped task stats, max, total}", () => { + expect(getAllTaskStatsGroupedByColor(versions)).toStrictEqual({ + "12": { + stats: [ + { + count: 8, + statuses: [ + taskStatusToCopy[TaskStatus.TestTimedOut], + taskStatusToCopy[TaskStatus.Failed], + ], + color: red.base, + umbrellaStatus: TaskStatus.FailedUmbrella, + statusCounts: { + [TaskStatus.TestTimedOut]: 6, + [TaskStatus.Failed]: 2, + }, + }, + { + count: 7, + statuses: [ + taskStatusToCopy[TaskStatus.SystemTimedOut], + taskStatusToCopy[TaskStatus.SystemUnresponsive], + ], + color: purple.dark2, + umbrellaStatus: TaskStatus.SystemFailureUmbrella, + statusCounts: { + [TaskStatus.SystemTimedOut]: 5, + [TaskStatus.SystemUnresponsive]: 2, + }, + }, + { + count: 4, + statuses: [taskStatusToCopy[TaskStatus.Dispatched]], + color: yellow.base, + umbrellaStatus: TaskStatus.RunningUmbrella, + statusCounts: { [TaskStatus.Dispatched]: 4 }, + }, + { + count: 2, + statuses: [taskStatusToCopy[TaskStatus.WillRun]], + color: gray.base, + umbrellaStatus: TaskStatus.ScheduledUmbrella, + statusCounts: { [TaskStatus.WillRun]: 2 }, + }, + ], + max: 8, + total: 21, + }, + "13": { + stats: [ + { + count: 6, + statuses: [taskStatusToCopy[TaskStatus.Succeeded]], + color: green.dark1, + umbrellaStatus: TaskStatus.Succeeded, + statusCounts: { [TaskStatus.Succeeded]: 6 }, + }, + { + count: 2, + statuses: [taskStatusToCopy[TaskStatus.Failed]], + color: red.base, + umbrellaStatus: TaskStatus.FailedUmbrella, + statusCounts: { [TaskStatus.Failed]: 2 }, + }, + { + count: 9, + statuses: [ + taskStatusToCopy[TaskStatus.Dispatched], + taskStatusToCopy[TaskStatus.Started], + ], + color: yellow.base, + umbrellaStatus: TaskStatus.RunningUmbrella, + statusCounts: { + [TaskStatus.Dispatched]: 4, + [TaskStatus.Started]: 5, + }, + }, + ], + max: 9, + total: 17, + }, + "14": { + stats: [ + { + count: 4, + statuses: [taskStatusToCopy[TaskStatus.Succeeded]], + color: green.dark1, + umbrellaStatus: TaskStatus.Succeeded, + statusCounts: { [TaskStatus.Succeeded]: 4 }, + }, + { + count: 6, + statuses: [taskStatusToCopy[TaskStatus.TaskTimedOut]], + color: red.base, + umbrellaStatus: TaskStatus.FailedUmbrella, + statusCounts: { [TaskStatus.TaskTimedOut]: 6 }, + }, + { + count: 7, + statuses: [ + taskStatusToCopy[TaskStatus.SystemFailed], + taskStatusToCopy[TaskStatus.SystemUnresponsive], + ], + color: purple.dark2, + umbrellaStatus: TaskStatus.SystemFailureUmbrella, + statusCounts: { + [TaskStatus.SystemFailed]: 5, + [TaskStatus.SystemUnresponsive]: 2, + }, + }, + { + count: 3, + statuses: [taskStatusToCopy[TaskStatus.SetupFailed]], + color: purple.light2, + umbrellaStatus: TaskStatus.SetupFailed, + statusCounts: { [TaskStatus.SetupFailed]: 3 }, + }, + { + count: 3, + statuses: [taskStatusToCopy[TaskStatus.Started]], + color: yellow.base, + umbrellaStatus: TaskStatus.RunningUmbrella, + statusCounts: { started: 3 }, + }, + { + count: 2, + statuses: [taskStatusToCopy[TaskStatus.Unscheduled]], + color: gray.dark1, + umbrellaStatus: TaskStatus.UndispatchedUmbrella, + statusCounts: { [TaskStatus.Unscheduled]: 2 }, + }, + ], + max: 7, + total: 25, + }, + "123": { + stats: [ + { + count: 4, + statuses: [taskStatusToCopy[TaskStatus.Succeeded]], + color: green.dark1, + umbrellaStatus: TaskStatus.Succeeded, + statusCounts: { [TaskStatus.Succeeded]: 4 }, + }, + { + count: 6, + statuses: [taskStatusToCopy[TaskStatus.TaskTimedOut]], + color: red.base, + umbrellaStatus: TaskStatus.FailedUmbrella, + statusCounts: { [TaskStatus.TaskTimedOut]: 6 }, + }, + { + count: 7, + statuses: [ + taskStatusToCopy[TaskStatus.SystemFailed], + taskStatusToCopy[TaskStatus.SystemUnresponsive], + ], + color: purple.dark2, + umbrellaStatus: TaskStatus.SystemFailureUmbrella, + statusCounts: { + [TaskStatus.SystemFailed]: 5, + [TaskStatus.SystemUnresponsive]: 2, + }, + }, + { + count: 3, + statuses: [taskStatusToCopy[TaskStatus.SetupFailed]], + color: purple.light2, + umbrellaStatus: TaskStatus.SetupFailed, + statusCounts: { [TaskStatus.SetupFailed]: 3 }, + }, + { + count: 3, + statuses: [taskStatusToCopy[TaskStatus.Started]], + color: yellow.base, + umbrellaStatus: TaskStatus.RunningUmbrella, + statusCounts: { started: 3 }, + }, + { + count: 2, + statuses: [taskStatusToCopy[TaskStatus.Unscheduled]], + color: gray.dark1, + umbrellaStatus: TaskStatus.UndispatchedUmbrella, + statusCounts: { [TaskStatus.Unscheduled]: 2 }, + }, + ], + max: 7, + total: 25, + }, + }); + }); +}); + +describe("constructBuildVariantDict", () => { + it("correctly determines priority, iconHeight, and badgeHeight", () => { + expect(constructBuildVariantDict(versions)).toStrictEqual({ + "enterprise-macos-cxx20": { + iconHeight: TASK_ICON_HEIGHT + TASK_ICON_PADDING * 2, + badgeHeight: GROUPED_BADGE_HEIGHT * 2 + GROUPED_BADGE_PADDING * 2, + priority: 4, + }, + "enterprise-windows-benchmarks": { + iconHeight: TASK_ICON_HEIGHT + TASK_ICON_PADDING * 2, + badgeHeight: 0, + priority: 2, + }, + "enterprise-rhel-80-64-bit-inmem": { + iconHeight: TASK_ICON_HEIGHT + TASK_ICON_PADDING * 2, + badgeHeight: 0, + priority: 1, + }, + }); + }); +}); diff --git a/src/pages/commits/utils.ts b/src/pages/commits/utils.ts index 58b376c160..627eb38d7b 100644 --- a/src/pages/commits/utils.ts +++ b/src/pages/commits/utils.ts @@ -2,10 +2,23 @@ import { MainlineCommitsQueryVariables, ProjectHealthView, } from "gql/generated/types"; -import { TaskStatus } from "types/task"; +import { BuildVariantDict, Commits } from "types/commits"; import { array } from "utils"; +import { groupStatusesByUmbrellaStatus } from "utils/statuses"; +import { + ALL_NON_FAILING_STATUSES, + FAILED_STATUSES, + GROUPED_BADGES_PER_ROW, + GROUPED_BADGE_HEIGHT, + GROUPED_BADGE_PADDING, + TASK_ICONS_PER_ROW, + TASK_ICON_HEIGHT, + TASK_ICON_PADDING, + impossibleMatch, +} from "./constants"; +import { GroupedResult } from "./types"; -const { arraySetDifference, arrayIntersection } = array; +const { arrayIntersection } = array; interface FilterState { statuses: string[]; @@ -176,30 +189,95 @@ const generateMainlineCommitOptionsFromState = ( }; }; -const FAILED_STATUSES = [ - TaskStatus.Failed, - TaskStatus.TaskTimedOut, - TaskStatus.TestTimedOut, - TaskStatus.KnownIssue, - TaskStatus.SetupFailed, - TaskStatus.SystemFailed, - TaskStatus.SystemTimedOut, - TaskStatus.SystemUnresponsive, - TaskStatus.Aborted, -]; - -const ALL_STATUSES = Object.values(TaskStatus); -const ALL_NON_FAILING_STATUSES = arraySetDifference( - ALL_STATUSES, - FAILED_STATUSES -); -const impossibleMatch = "^\b$"; // this will never match anything +const findMaxGroupedTaskStats = (groupedTaskStats: { + [id: string]: GroupedResult; +}) => { + if (Object.keys(groupedTaskStats).length === 0) { + return { max: 0 }; + } + return Object.values(groupedTaskStats).reduce((prev, curr) => + prev.max > curr.max ? prev : curr + ); +}; + +const getAllTaskStatsGroupedByColor = (versions: Commits) => { + const idToGroupedTaskStats: { [id: string]: GroupedResult } = {}; + versions.forEach(({ version }) => { + if (version != null) { + idToGroupedTaskStats[version.id] = groupStatusesByUmbrellaStatus( + version.taskStatusStats?.counts + ); + } + }); + + return idToGroupedTaskStats; +}; + +const constructBuildVariantDict = (versions: Commits): BuildVariantDict => { + const buildVariantDict: BuildVariantDict = {}; + + for (let i = 0; i < versions.length; i++) { + const { version } = versions[i]; + + // skip if inactive/unmatching + if (version) { + // Deduplicate build variants and build variant stats by consolidating into a single object. + const allBuildVariants = [ + ...version.buildVariants, + ...version.buildVariantStats, + ].reduce((acc, curr) => { + const { variant } = curr; + acc[variant] = { ...acc[variant], ...curr }; + return acc; + }, {}); + + // Construct build variant dict which will contain information needed for rendering. + Object.values(allBuildVariants).reduce( + (acc, { tasks, statusCounts, variant }) => { + // Determine height to allocate for icons. + let iconHeight = 0; + if (tasks) { + const numRows = Math.ceil(tasks.length / TASK_ICONS_PER_ROW); + const iconContainerHeight = numRows * TASK_ICON_HEIGHT; + const iconContainerPadding = TASK_ICON_PADDING * 2; + iconHeight = iconContainerHeight + iconContainerPadding; + } + + // Determine height to allocate for grouped badges. + let badgeHeight = 0; + if (statusCounts) { + const numRows = Math.ceil( + statusCounts.length / GROUPED_BADGES_PER_ROW + ); + const badgeContainerHeight = numRows * GROUPED_BADGE_HEIGHT; + const badgeContainerPadding = GROUPED_BADGE_PADDING * 2; + badgeHeight = badgeContainerHeight + badgeContainerPadding; + } + + if (acc[variant]) { + if (iconHeight > acc[variant].iconHeight) { + acc[variant].iconHeight = iconHeight; + } + if (badgeHeight > acc[variant].badgeHeight) { + acc[variant].badgeHeight = badgeHeight; + } + acc[variant].priority += 1; + } else { + acc[variant] = { priority: 1, iconHeight, badgeHeight }; + } + return acc; + }, + buildVariantDict + ); + } + } + return buildVariantDict; +}; export { - impossibleMatch, getFilterStatus, getMainlineCommitsQueryVariables, - FAILED_STATUSES, - ALL_STATUSES, - ALL_NON_FAILING_STATUSES, + constructBuildVariantDict, + findMaxGroupedTaskStats, + getAllTaskStatsGroupedByColor, }; diff --git a/src/pages/projectSettings/CopyProjectModal.test.tsx b/src/pages/projectSettings/CopyProjectModal.test.tsx index dcc340a777..a64224241b 100644 --- a/src/pages/projectSettings/CopyProjectModal.test.tsx +++ b/src/pages/projectSettings/CopyProjectModal.test.tsx @@ -9,6 +9,7 @@ import { ProjectSettingsQueryVariables, RepoSettingsQuery, RepoSettingsQueryVariables, + MergeQueue, } from "gql/generated/types"; import { COPY_PROJECT } from "gql/mutations"; import { GET_PROJECT_SETTINGS, GET_REPO_SETTINGS } from "gql/queries"; @@ -337,6 +338,7 @@ const projectSettingsMock: ApolloMock< commitQueue: { enabled: true, mergeMethod: "squash", + mergeQueue: MergeQueue.Evergreen, message: "", __typename: "CommitQueueParams", }, diff --git a/src/pages/projectSettings/tabs/EventLogTab/EventLogTab.test.tsx b/src/pages/projectSettings/tabs/EventLogTab/EventLogTab.test.tsx index 41189bc1a2..32aaba42a7 100644 --- a/src/pages/projectSettings/tabs/EventLogTab/EventLogTab.test.tsx +++ b/src/pages/projectSettings/tabs/EventLogTab/EventLogTab.test.tsx @@ -4,6 +4,7 @@ import { ProjectEventLogsQuery, ProjectEventLogsQueryVariables, ProjectHealthView, + MergeQueue, } from "gql/generated/types"; import { GET_PROJECT_EVENT_LOGS } from "gql/queries"; import { renderWithRouterMatch as render, screen, waitFor } from "test_utils"; @@ -144,6 +145,7 @@ const eventLogEntry: ProjectEventLogsQuery["projectEvents"]["eventLogEntries"][0 commitQueue: { enabled: true, mergeMethod: "squash", + mergeQueue: MergeQueue.Evergreen, message: "", __typename: "CommitQueueParams", }, @@ -227,6 +229,7 @@ const eventLogEntry: ProjectEventLogsQuery["projectEvents"]["eventLogEntries"][0 commitQueue: { enabled: true, mergeMethod: "squash", + mergeQueue: MergeQueue.Github, message: "", __typename: "CommitQueueParams", }, diff --git a/src/pages/projectSettings/tabs/GithubCommitQueueTab/getFormSchema.tsx b/src/pages/projectSettings/tabs/GithubCommitQueueTab/getFormSchema.tsx index ca3222e472..558a369671 100644 --- a/src/pages/projectSettings/tabs/GithubCommitQueueTab/getFormSchema.tsx +++ b/src/pages/projectSettings/tabs/GithubCommitQueueTab/getFormSchema.tsx @@ -1,3 +1,5 @@ +import styled from "@emotion/styled"; +import { palette } from "@leafygreen-ui/palette"; import { Description } from "@leafygreen-ui/typography"; import { CardFieldTemplate } from "components/SpruceForm/FieldTemplates"; import widgets from "components/SpruceForm/Widgets"; @@ -7,19 +9,25 @@ import { pullRequestAliasesDocumentationUrl, gitTagAliasesDocumentationUrl, githubChecksAliasesDocumentationUrl, + githubMergeQueueUrl, } from "constants/externalResources"; import { getProjectSettingsRoute, ProjectSettingsTabRoutes, } from "constants/routes"; -import { GithubProjectConflicts } from "gql/generated/types"; +import { size } from "constants/tokens"; +import { GithubProjectConflicts, MergeQueue } from "gql/generated/types"; import { getTabTitle } from "pages/projectSettings/getTabTitle"; +import { environmentVariables } from "utils"; import { GetFormSchema } from "../types"; import { alias, form, ProjectType } from "../utils"; import { githubConflictErrorStyling, sectionHasError } from "./getErrors"; import { GithubTriggerAliasField } from "./GithubTriggerAliasField"; import { FormState } from "./types"; +const { green } = palette; + +const { isProduction } = environmentVariables; const { aliasArray, aliasRowUiSchema, gitTagArray } = alias; const { insertIf, overrideRadioBox, placeholderIf, radioBoxOptions } = form; @@ -194,6 +202,7 @@ export const getFormSchema = ( }, }, dependencies: { + // @ts-expect-error - Allow the BETA badge in Radio Button widget title. enabled: { oneOf: [ { @@ -201,7 +210,6 @@ export const getFormSchema = ( enabled: { enum: [false], }, - message: { type: "string" as "string", title: "Commit Queue Message", @@ -213,6 +221,46 @@ export const getFormSchema = ( enabled: { enum: [true], }, + ...(!isProduction() && { + mergeQueueTitle: { + title: "Merge Queue", + type: "null", + }, + mergeQueue: { + type: "string" as "string", + oneOf: [ + { + type: "string" as "string", + title: "Evergreen", + enum: [MergeQueue.Evergreen], + description: + "Use the standard commit queue owned and maintained by Evergreen.", + }, + { + type: "string" as "string", + title: ( + + GitHub Beta + + ), + enum: [MergeQueue.Github], + description: ( + <> + Use the GitHub merge queue. Read the + documentation{" "} + + here + + . + + ), + }, + ], + }, + }), message: { type: "string" as "string", title: "Commit Queue Message", @@ -453,6 +501,9 @@ export const getFormSchema = ( "the Commit Queue" ), }, + mergeQueue: { + "ui:widget": "radio", + }, message: { "ui:description": "Shown in commit queue CLI commands & web UI", "ui:data-cy": "cq-message-input", @@ -601,3 +652,13 @@ const GitHubChecksAliasesDescription = ( and no aliases are defined on the project or repo page. ); + +const BetaBadge = styled.span` + color: ${green.dark1}; + border: 1px solid ${green.dark1}; + border-radius: ${size.s}; + padding: 0 ${size.xxs}; + font-size: 11px; + text-transform: uppercase; + vertical-align: bottom; +`; diff --git a/src/pages/projectSettings/tabs/GithubCommitQueueTab/transformers.test.ts b/src/pages/projectSettings/tabs/GithubCommitQueueTab/transformers.test.ts index 9b53296383..d7e8fbe360 100644 --- a/src/pages/projectSettings/tabs/GithubCommitQueueTab/transformers.test.ts +++ b/src/pages/projectSettings/tabs/GithubCommitQueueTab/transformers.test.ts @@ -1,4 +1,8 @@ -import { ProjectSettingsInput, RepoSettingsInput } from "gql/generated/types"; +import { + ProjectSettingsInput, + RepoSettingsInput, + MergeQueue, +} from "gql/generated/types"; import { data } from "../testData"; import { alias, ProjectType } from "../utils"; import { formToGql, gqlToForm, mergeProjectRepo } from "./transformers"; @@ -103,6 +107,7 @@ const projectForm: FormState = { commitQueue: { enabled: null, mergeMethod: "", + mergeQueue: MergeQueue.Evergreen, message: "", patchDefinitions: { commitQueueAliasesOverride: true, @@ -141,6 +146,7 @@ const projectResult: Pick = { commitQueue: { enabled: null, mergeMethod: "", + mergeQueue: MergeQueue.Evergreen, message: "", }, }, @@ -249,6 +255,7 @@ const repoForm: FormState = { commitQueue: { enabled: true, mergeMethod: "squash", + mergeQueue: MergeQueue.Github, message: "Commit Queue Message", patchDefinitions: { commitQueueAliasesOverride: true, @@ -269,6 +276,7 @@ const repoResult: Pick = { commitQueue: { enabled: true, mergeMethod: "squash", + mergeQueue: MergeQueue.Github, message: "Commit Queue Message", }, }, @@ -393,6 +401,7 @@ const mergedForm: FormState = { commitQueue: { enabled: null, mergeMethod: "", + mergeQueue: MergeQueue.Evergreen, message: "", patchDefinitions: { commitQueueAliasesOverride: true, diff --git a/src/pages/projectSettings/tabs/GithubCommitQueueTab/transformers.ts b/src/pages/projectSettings/tabs/GithubCommitQueueTab/transformers.ts index 34721bc5dc..c72608cde0 100644 --- a/src/pages/projectSettings/tabs/GithubCommitQueueTab/transformers.ts +++ b/src/pages/projectSettings/tabs/GithubCommitQueueTab/transformers.ts @@ -94,8 +94,9 @@ export const gqlToForm = ((data, options) => { }, commitQueue: { enabled: commitQueue.enabled, - message: commitQueue.message, mergeMethod: commitQueue.mergeMethod, + mergeQueue: commitQueue.mergeQueue, + message: commitQueue.message, patchDefinitions: { commitQueueAliasesOverride: override(commitQueueAliases), commitQueueAliases, @@ -117,7 +118,13 @@ export const formToGql = (( teams: { gitTagAuthorizedTeams, gitTagAuthorizedTeamsOverride }, gitTags, }, - commitQueue: { enabled, message, mergeMethod, patchDefinitions }, + commitQueue: { + enabled, + mergeMethod, + mergeQueue, + message, + patchDefinitions, + }, }, id ) => { @@ -135,8 +142,9 @@ export const formToGql = (( : null, commitQueue: { enabled, - message, mergeMethod, + mergeQueue, + message, }, }; diff --git a/src/pages/projectSettings/tabs/GithubCommitQueueTab/types.ts b/src/pages/projectSettings/tabs/GithubCommitQueueTab/types.ts index bcf566eb82..9db27876bb 100644 --- a/src/pages/projectSettings/tabs/GithubCommitQueueTab/types.ts +++ b/src/pages/projectSettings/tabs/GithubCommitQueueTab/types.ts @@ -1,4 +1,7 @@ -import { ProjectPatchAliasSettingsFragment } from "gql/generated/types"; +import { + ProjectPatchAliasSettingsFragment, + MergeQueue, +} from "gql/generated/types"; import { AliasFormType, ProjectType } from "../utils"; export interface FormState { @@ -52,6 +55,7 @@ export interface FormState { commitQueue: { enabled: boolean | null; mergeMethod: string; + mergeQueue: MergeQueue; message: string; patchDefinitions: { commitQueueAliasesOverride: boolean; diff --git a/src/pages/projectSettings/tabs/testData.ts b/src/pages/projectSettings/tabs/testData.ts index 638519653b..d8d8c96eca 100644 --- a/src/pages/projectSettings/tabs/testData.ts +++ b/src/pages/projectSettings/tabs/testData.ts @@ -3,6 +3,7 @@ import { ProjectHealthView, ProjectSettingsQuery, RepoSettingsQuery, + MergeQueue, } from "gql/generated/types"; const projectBase: ProjectSettingsQuery["projectSettings"] = { @@ -60,6 +61,7 @@ const projectBase: ProjectSettingsQuery["projectSettings"] = { commitQueue: { enabled: null, mergeMethod: "", + mergeQueue: MergeQueue.Evergreen, message: "", }, perfEnabled: true, @@ -182,6 +184,7 @@ const repoBase: RepoSettingsQuery["repoSettings"] = { commitQueue: { enabled: true, mergeMethod: "squash", + mergeQueue: MergeQueue.Github, message: "Commit Queue Message", }, perfEnabled: true, diff --git a/src/pages/task/metadata/Metadata.test.tsx b/src/pages/task/metadata/Metadata.test.tsx index 5efe6cb941..14432b7e42 100644 --- a/src/pages/task/metadata/Metadata.test.tsx +++ b/src/pages/task/metadata/Metadata.test.tsx @@ -50,6 +50,8 @@ describe("metadata", () => { expect(screen.getByDataCy("metadata-eta-timer")).toBeInTheDocument(); expect(screen.getByDataCy("task-metadata-started")).toBeInTheDocument(); expect(screen.queryByDataCy("task-metadata-finished")).toBeNull(); + expect(screen.queryByDataCy("task-trace-link")).toBeNull(); + expect(screen.queryByDataCy("task-metrics-link")).toBeNull(); }); it("renders the metadata card with a succeeded status", () => { @@ -71,6 +73,8 @@ describe("metadata", () => { expect(screen.queryByDataCy("metadata-eta-timer")).toBeNull(); expect(screen.getByDataCy("task-metadata-started")).toBeInTheDocument(); expect(screen.getByDataCy("task-metadata-finished")).toBeInTheDocument(); + expect(screen.getByDataCy("task-trace-link")).toBeInTheDocument(); + expect(screen.getByDataCy("task-metrics-link")).toBeInTheDocument(); }); }); @@ -97,5 +101,6 @@ const taskSucceeded = { ...taskStarted.task, finishTime: addMilliseconds(new Date(), 1228078), status: "succeeded", + details: { ...taskStarted.task.details, traceID: "trace_abcde" }, }, }; diff --git a/src/pages/task/metadata/index.tsx b/src/pages/task/metadata/index.tsx index e27b105589..1963b8d331 100644 --- a/src/pages/task/metadata/index.tsx +++ b/src/pages/task/metadata/index.tsx @@ -1,7 +1,10 @@ +import { useState, useRef } from "react"; import { ApolloError } from "@apollo/client"; import styled from "@emotion/styled"; +import { GuideCue } from "@leafygreen-ui/guide-cue"; import { palette } from "@leafygreen-ui/palette"; import { InlineCode } from "@leafygreen-ui/typography"; +import Cookies from "js-cookie"; import { Link } from "react-router-dom"; import { useTaskAnalytics } from "analytics"; import { @@ -10,7 +13,12 @@ import { MetadataTitle, } from "components/MetadataCard"; import { StyledLink, StyledRouterLink } from "components/styles"; -import { getDistroPageUrl } from "constants/externalResources"; +import { SEEN_HONEYCOMB_GUIDE_CUE } from "constants/cookies"; +import { + getDistroPageUrl, + getHoneycombTraceUrl, + getHoneycombSystemMetricsUrl, +} from "constants/externalResources"; import { getTaskQueueRoute, getTaskRoute, @@ -88,9 +96,18 @@ export const Metadata: React.VFC = ({ const projectIdentifier = project?.identifier; const { author, id: versionID } = versionMetadata ?? {}; const oomTracker = details?.oomTracker; + const taskTrace = details?.traceID; const { id: podId } = pod ?? {}; const isContainerTask = !!podId; const { metadataLinks } = annotation ?? {}; + const [openGuideCue, setOpenGuideCue] = useState( + Cookies.get(SEEN_HONEYCOMB_GUIDE_CUE) !== "true" + ); + const triggerRef = useRef(null); + const onHideCue = () => { + Cookies.set(SEEN_HONEYCOMB_GUIDE_CUE, "true", { expires: 365 }); + setOpenGuideCue(false); + }; return ( @@ -338,6 +355,45 @@ export const Metadata: React.VFC = ({ ))} ) : null} + {taskTrace && startTime && finishTime && ( + + + Finished tasks link to Honeycomb. + + { + onHideCue(); + taskAnalytics.sendEvent({ name: "Click Trace Link" }); + }} + hideExternalIcon={false} + > + Honeycomb Trace + + { + onHideCue(); + taskAnalytics.sendEvent({ name: "Click Trace Metrics Link" }); + }} + hideExternalIcon={false} + > + Honeycomb System Metrics + + + )} ); }; diff --git a/src/utils/environmentVariables.ts b/src/utils/environmentVariables.ts index 1b5945e2b2..a78dfb8495 100644 --- a/src/utils/environmentVariables.ts +++ b/src/utils/environmentVariables.ts @@ -110,6 +110,13 @@ export const getAppVersion = () => process.env.REACT_APP_VERSION || ""; */ export const getReleaseStage = () => process.env.REACT_APP_RELEASE_STAGE || ""; +/** + * `getHoneycombBaseURL()` - Get the base Honeycomb URL from the environment variables + * @returns - Returns the base Honeycomb URL + */ +export const getHoneycombBaseURL = () => + process.env.REACT_APP_HONEYCOMB_BASE_URL || ""; + /** * `getLoginDomain()` - Get the login domain depending on the release stage * @returns - Returns the login domain diff --git a/src/utils/numbers/index.ts b/src/utils/numbers/index.ts index 09fff24d1a..f156a754c1 100644 --- a/src/utils/numbers/index.ts +++ b/src/utils/numbers/index.ts @@ -61,10 +61,37 @@ const cryptoRandom = () => { return randomNumber / 2 ** 32; }; +/** + * `roundMax` rounds up a given maximum value to the nearest specified increment. + * @param max - The maximum value to be rounded up. + * @returns The rounded up value based on the specified increments. + */ +const roundMax = (max: number) => { + if (max < 100) { + // Round up to nearest 10 + return Math.ceil(max / 10) * 10; + } + if (max < 500) { + // Round up to nearest 50 + return Math.ceil(max / 50) * 50; + } + if (max < 1000) { + // Round up to nearest 100 + return Math.ceil(max / 100) * 100; + } + if (max < 5000) { + // Round up to nearest 500 + return Math.ceil(max / 500) * 500; + } + // Else round up to nearest 1000 + return Math.ceil(max / 1000) * 1000; +}; + export { toDecimal, toPercent, formatZeroIndexForDisplay, roundDecimal, cryptoRandom, + roundMax, }; diff --git a/src/utils/numbers/numbers.test.ts b/src/utils/numbers/numbers.test.ts index a25a92f575..3a61624e8c 100644 --- a/src/utils/numbers/numbers.test.ts +++ b/src/utils/numbers/numbers.test.ts @@ -1,4 +1,4 @@ -import { roundDecimal, cryptoRandom } from "."; +import { roundDecimal, cryptoRandom, roundMax } from "."; describe("roundDecimal", () => { it("correctly rounds a decimal to the specified number of places", () => { @@ -19,3 +19,13 @@ describe("cryptoRandom", () => { expect(randomNumber).toBeLessThan(1); }); }); + +describe("roundMax", () => { + it("properly rounds numbers", () => { + expect(roundMax(8)).toBe(10); // 0 <= x < 100 + expect(roundMax(147)).toBe(150); // 100 <= x < 500 + expect(roundMax(712)).toBe(800); // 500 <= x < 1000 + expect(roundMax(1320)).toBe(1500); // 1000 <= x < 5000 + expect(roundMax(6430)).toBe(7000); // 5000 <= x + }); +}); diff --git a/yarn.lock b/yarn.lock index a954b97299..9462f2f4ed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14863,10 +14863,10 @@ react-virtual@^2.10.4: dependencies: "@reach/observe-rect" "^1.1.0" -react-virtuoso@^4.3.8: - version "4.3.8" - resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-4.3.8.tgz#eeeb4112d8477b02bec56d55c8030073302ad465" - integrity sha512-hoZj8Dl1R9fYqtUwA5LjCii1djO4ZNtoYkYsR52ZjdJzdXh2hec1IQ2O+1MZNjLTb4v8ff3hbt34StiHTVDdlg== +react-virtuoso@^4.3.11: + version "4.3.11" + resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-4.3.11.tgz#ab24e707287ef1b4bb5b52f3b14795ba896e9768" + integrity sha512-0YrCvQ5GsIKRcN34GxrzhSJGuMNI+hGxWci5cTVuPQ8QWTEsrKfCyqm7YNBMmV3pu7onG1YVUBo86CyCXdejXg== react-window@^1.8.9: version "1.8.9"