From 69137bce4dec7148a71ab43a3861f768f5467d23 Mon Sep 17 00:00:00 2001 From: Malik Hadjri <19805673+hadjri@users.noreply.github.com> Date: Thu, 12 Sep 2024 16:32:34 -0400 Subject: [PATCH] DEVPROD-7444 Add task scheduling warnings (#309) --- .../Banners/TaskSchedulingWarningBanner.tsx | 20 ++++ .../ScheduleTasks/testData.ts | 2 + .../components/ScheduleTasksModal/index.tsx | 12 ++ .../VersionRestartModal.tsx | 10 ++ .../spruce/src/constants/externalResources.ts | 2 + apps/spruce/src/gql/generated/types.ts | 21 ++++ .../build-variants-with-children.graphql | 8 ++ .../src/gql/queries/patch-configure.graphql | 5 + .../gql/queries/undispatched-tasks.graphql | 4 + ...ePatchCore_ConfigureTasksDefault.storyshot | 9 +- .../configurePatchCore/index.tsx | 18 +++ .../configurePatchCore/testData.ts | 1 + .../GithubCommitQueueTab.tsx | 2 +- .../tasks/estimatedActivatedTasks.test.ts | 108 ++++++++++++++++++ .../utils/tasks/estimatedActivatedTasks.ts | 98 ++++++++++++++++ 15 files changed, 316 insertions(+), 4 deletions(-) create mode 100644 apps/spruce/src/components/Banners/TaskSchedulingWarningBanner.tsx create mode 100644 apps/spruce/src/utils/tasks/estimatedActivatedTasks.test.ts create mode 100644 apps/spruce/src/utils/tasks/estimatedActivatedTasks.ts diff --git a/apps/spruce/src/components/Banners/TaskSchedulingWarningBanner.tsx b/apps/spruce/src/components/Banners/TaskSchedulingWarningBanner.tsx new file mode 100644 index 000000000..e20c8dd2e --- /dev/null +++ b/apps/spruce/src/components/Banners/TaskSchedulingWarningBanner.tsx @@ -0,0 +1,20 @@ +import Banner from "@leafygreen-ui/banner"; +import { StyledLink } from "components/styles"; +import { taskSchedulingLimitsDocumentationUrl } from "constants/externalResources"; + +interface TaskSchedulingWarningBannerProps { + totalTasks: number; +} +const largeNumFinalizedTasksThreshold = 1000; + +export const TaskSchedulingWarningBanner: React.FC< + TaskSchedulingWarningBannerProps +> = ({ totalTasks }) => + totalTasks >= largeNumFinalizedTasksThreshold ? ( + + This is a large operation, expected to schedule {totalTasks} tasks. Please + confirm that this number of tasks is necessary before continuing. For more + information, please refer to our{" "} + docs. + + ) : null; diff --git a/apps/spruce/src/components/PatchActionButtons/ScheduleTasks/testData.ts b/apps/spruce/src/components/PatchActionButtons/ScheduleTasks/testData.ts index bacc57e9f..c2b35c0a8 100644 --- a/apps/spruce/src/components/PatchActionButtons/ScheduleTasks/testData.ts +++ b/apps/spruce/src/components/PatchActionButtons/ScheduleTasks/testData.ts @@ -19,6 +19,7 @@ const mocks: ApolloMock< version: { __typename: "Version", id: "version_id", + generatedTaskCounts: [], tasks: { __typename: "VersionTasks", data: [ @@ -150,6 +151,7 @@ const mocks: ApolloMock< version: { __typename: "Version", id: "version_empty", + generatedTaskCounts: [], tasks: { __typename: "VersionTasks", data: [], diff --git a/apps/spruce/src/components/ScheduleTasksModal/index.tsx b/apps/spruce/src/components/ScheduleTasksModal/index.tsx index 6e84c71a3..85b46b1a6 100644 --- a/apps/spruce/src/components/ScheduleTasksModal/index.tsx +++ b/apps/spruce/src/components/ScheduleTasksModal/index.tsx @@ -5,6 +5,7 @@ import Checkbox from "@leafygreen-ui/checkbox"; import { Body } from "@leafygreen-ui/typography"; import { Skeleton } from "antd"; import { Accordion } from "components/Accordion"; +import { TaskSchedulingWarningBanner } from "components/Banners/TaskSchedulingWarningBanner"; import { ConfirmationModal } from "components/ConfirmationModal"; import { size } from "constants/tokens"; import { useToastContext } from "context/toast"; @@ -16,6 +17,7 @@ import { } from "gql/generated/types"; import { SCHEDULE_TASKS } from "gql/mutations"; import { UNSCHEDULED_TASKS } from "gql/queries"; +import { sumActivatedTasksInSet } from "utils/tasks/estimatedActivatedTasks"; import { initialState, reducer } from "./reducer"; interface ScheduleTasksModalProps { @@ -70,6 +72,13 @@ export const ScheduleTasksModal: React.FC = ({ dispatch({ type: "ingestData", taskData }); }, [taskData]); + const { generatedTaskCounts = [] } = taskData?.version ?? {}; + + const estimatedActivatedTasksCount = sumActivatedTasksInSet( + selectedTasks, + generatedTaskCounts, + ); + return ( = ({ ); }, )} + )} {!loadingTaskData && !sortedBuildVariantGroups.length && ( diff --git a/apps/spruce/src/components/VersionRestartModal/VersionRestartModal.tsx b/apps/spruce/src/components/VersionRestartModal/VersionRestartModal.tsx index b18ab817b..636fef741 100644 --- a/apps/spruce/src/components/VersionRestartModal/VersionRestartModal.tsx +++ b/apps/spruce/src/components/VersionRestartModal/VersionRestartModal.tsx @@ -7,6 +7,7 @@ import { Skeleton } from "antd"; import { TaskStatus } from "@evg-ui/lib/types/task"; import { useVersionAnalytics } from "analytics"; import { Accordion } from "components/Accordion"; +import { TaskSchedulingWarningBanner } from "components/Banners/TaskSchedulingWarningBanner"; import { ConfirmationModal } from "components/ConfirmationModal"; import { finishedTaskStatuses } from "constants/task"; import { size } from "constants/tokens"; @@ -24,6 +25,7 @@ import { versionSelectedTasks, selectedStrings, } from "hooks/useVersionTaskStatusSelect"; +import { sumActivatedTasksInSelectedTasks } from "utils/tasks/estimatedActivatedTasks"; import VersionTasks from "./VersionTasks"; interface VersionRestartModalProps { @@ -110,6 +112,11 @@ const VersionRestartModal: React.FC = ({ const selectedTotal = selectTasksTotal(selectedTasks || {}); + const { generatedTaskCounts = [] } = version ?? {}; + const estimatedActivatedTasksCount = sumActivatedTasksInSelectedTasks( + selectedTasks || {}, + generatedTaskCounts, + ); return ( = ({
)} + Are you sure you want to restart the {selectedTotal} selected tasks? diff --git a/apps/spruce/src/constants/externalResources.ts b/apps/spruce/src/constants/externalResources.ts index a66b07d1b..09b65153d 100644 --- a/apps/spruce/src/constants/externalResources.ts +++ b/apps/spruce/src/constants/externalResources.ts @@ -50,6 +50,8 @@ export const cliDocumentationUrl = `${wikiBaseUrl}/CLI`; export const containersOnboardingDocumentationUrl = `${wikiBaseUrl}/Containers/Container-Tasks`; +export const taskSchedulingLimitsDocumentationUrl = `${wikiBaseUrl}/Reference/Limits#task-scheduling-limits`; + export const windowsPasswordRulesURL = "https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2003/cc786468(v=ws.10)?redirectedfrom=MSDN"; diff --git a/apps/spruce/src/gql/generated/types.ts b/apps/spruce/src/gql/generated/types.ts index f5ebe1481..d9e225593 100644 --- a/apps/spruce/src/gql/generated/types.ts +++ b/apps/spruce/src/gql/generated/types.ts @@ -5836,7 +5836,17 @@ export type BuildVariantsWithChildrenQuery = { status: string; }> | null; }> | null; + generatedTaskCounts: Array<{ + __typename?: "GeneratedTaskCountResults"; + estimatedTasks: number; + taskId?: string | null; + }>; }> | null; + generatedTaskCounts: Array<{ + __typename?: "GeneratedTaskCountResults"; + estimatedTasks: number; + taskId?: string | null; + }>; }; }; @@ -6845,6 +6855,12 @@ export type ConfigurePatchQuery = { tasks: Array; }>; }> | null; + generatedTaskCounts: Array<{ + __typename?: "GeneratedTaskCountResults"; + buildVariantName?: string | null; + estimatedTasks: number; + taskName?: string | null; + }>; patchTriggerAliases: Array<{ __typename?: "PatchTriggerAlias"; alias: string; @@ -9014,6 +9030,11 @@ export type UndispatchedTasksQuery = { version: { __typename?: "Version"; id: string; + generatedTaskCounts: Array<{ + __typename?: "GeneratedTaskCountResults"; + estimatedTasks: number; + taskId?: string | null; + }>; tasks: { __typename?: "VersionTasks"; data: Array<{ diff --git a/apps/spruce/src/gql/queries/build-variants-with-children.graphql b/apps/spruce/src/gql/queries/build-variants-with-children.graphql index 50571ffa5..acb7904dc 100644 --- a/apps/spruce/src/gql/queries/build-variants-with-children.graphql +++ b/apps/spruce/src/gql/queries/build-variants-with-children.graphql @@ -23,10 +23,18 @@ query BuildVariantsWithChildren($id: String!, $statuses: [String!]!) { } variant } + generatedTaskCounts { + estimatedTasks + taskId + } id project projectIdentifier } + generatedTaskCounts { + estimatedTasks + taskId + } id } } diff --git a/apps/spruce/src/gql/queries/patch-configure.graphql b/apps/spruce/src/gql/queries/patch-configure.graphql index dbed78cf1..88e03f035 100644 --- a/apps/spruce/src/gql/queries/patch-configure.graphql +++ b/apps/spruce/src/gql/queries/patch-configure.graphql @@ -15,6 +15,11 @@ query ConfigurePatch($id: String!) { tasks } } + generatedTaskCounts { + buildVariantName + estimatedTasks + taskName + } patchTriggerAliases { alias childProjectId diff --git a/apps/spruce/src/gql/queries/undispatched-tasks.graphql b/apps/spruce/src/gql/queries/undispatched-tasks.graphql index 6058a5570..e49731497 100644 --- a/apps/spruce/src/gql/queries/undispatched-tasks.graphql +++ b/apps/spruce/src/gql/queries/undispatched-tasks.graphql @@ -1,5 +1,9 @@ query UndispatchedTasks($versionId: String!) { version(versionId: $versionId) { + generatedTaskCounts { + estimatedTasks + taskId + } id tasks( options: { statuses: ["unscheduled"], includeNeverActivatedTasks: true } diff --git a/apps/spruce/src/pages/configurePatch/configurePatchCore/__snapshots__/ConfigurePatchCore_ConfigureTasksDefault.storyshot b/apps/spruce/src/pages/configurePatch/configurePatchCore/__snapshots__/ConfigurePatchCore_ConfigureTasksDefault.storyshot index 49f7f4173..6fde50e65 100644 --- a/apps/spruce/src/pages/configurePatch/configurePatchCore/__snapshots__/ConfigurePatchCore_ConfigureTasksDefault.storyshot +++ b/apps/spruce/src/pages/configurePatch/configurePatchCore/__snapshots__/ConfigurePatchCore_ConfigureTasksDefault.storyshot @@ -1,9 +1,9 @@
+
diff --git a/apps/spruce/src/pages/configurePatch/configurePatchCore/index.tsx b/apps/spruce/src/pages/configurePatch/configurePatchCore/index.tsx index ceef2ddb5..f7a4f6e5e 100644 --- a/apps/spruce/src/pages/configurePatch/configurePatchCore/index.tsx +++ b/apps/spruce/src/pages/configurePatch/configurePatchCore/index.tsx @@ -5,6 +5,7 @@ import Button from "@leafygreen-ui/button"; import { Tab } from "@leafygreen-ui/tabs"; import TextInput from "@leafygreen-ui/text-input"; import { useNavigate } from "react-router-dom"; +import { TaskSchedulingWarningBanner } from "components/Banners/TaskSchedulingWarningBanner"; import { LoadingButton } from "components/Buttons"; import { CodeChanges } from "components/CodeChanges"; import { @@ -33,6 +34,7 @@ import { ProjectBuildVariant, } from "gql/generated/types"; import { SCHEDULE_PATCH } from "gql/mutations"; +import { sumActivatedTasksInVariantsTasks } from "utils/tasks/estimatedActivatedTasks"; import { ConfigureBuildVariants } from "./ConfigureBuildVariants"; import ConfigureTasks from "./ConfigureTasks"; import { ParametersContent } from "./ParametersContent"; @@ -56,6 +58,7 @@ const ConfigurePatchCore: React.FC = ({ patch }) => { author, childPatchAliases, childPatches, + generatedTaskCounts, id, patchTriggerAliases, project, @@ -154,6 +157,12 @@ const ConfigurePatchCore: React.FC = ({ patch }) => { ); } + const estimatedActivatedTasksCount = sumActivatedTasksInVariantsTasks( + selectedBuildVariantTasks, + generatedTaskCounts, + initialPatch.variantsTasks, + ); + return ( <> @@ -187,6 +196,11 @@ const ConfigurePatchCore: React.FC = ({ patch }) => { + + + @@ -348,4 +362,8 @@ const FlexRow = styled.div` gap: ${size.s}; `; +const BannerContainer = styled.div` + margin-bottom: ${size.s}; +`; + export default ConfigurePatchCore; diff --git a/apps/spruce/src/pages/configurePatch/configurePatchCore/testData.ts b/apps/spruce/src/pages/configurePatch/configurePatchCore/testData.ts index 59d67c095..7f361c615 100644 --- a/apps/spruce/src/pages/configurePatch/configurePatchCore/testData.ts +++ b/apps/spruce/src/pages/configurePatch/configurePatchCore/testData.ts @@ -14,6 +14,7 @@ export const patchQuery: ConfigurePatchQuery = { projectIdentifier: "spruce", author: "mohamed.khelif", activated: false, + generatedTaskCounts: [], status: "created", time: { submittedAt: "2020-08-28T15:00:17Z", diff --git a/apps/spruce/src/pages/projectSettings/tabs/GithubCommitQueueTab/GithubCommitQueueTab.tsx b/apps/spruce/src/pages/projectSettings/tabs/GithubCommitQueueTab/GithubCommitQueueTab.tsx index 539e205bc..0d30ef8c5 100644 --- a/apps/spruce/src/pages/projectSettings/tabs/GithubCommitQueueTab/GithubCommitQueueTab.tsx +++ b/apps/spruce/src/pages/projectSettings/tabs/GithubCommitQueueTab/GithubCommitQueueTab.tsx @@ -89,7 +89,7 @@ export const GithubCommitQueueTab: React.FC = ({ return ( <> {!githubWebhooksEnabled && ( - + GitHub features are disabled because the Evergreen GitHub App is not installed on the saved owner/repo. Contact IT to install the App and enable GitHub features. diff --git a/apps/spruce/src/utils/tasks/estimatedActivatedTasks.test.ts b/apps/spruce/src/utils/tasks/estimatedActivatedTasks.test.ts new file mode 100644 index 000000000..69b98b911 --- /dev/null +++ b/apps/spruce/src/utils/tasks/estimatedActivatedTasks.test.ts @@ -0,0 +1,108 @@ +import { GeneratedTaskCountResults, VariantTask } from "gql/generated/types"; +import { versionSelectedTasks } from "hooks/useVersionTaskStatusSelect"; +import { VariantTasksState } from "pages/configurePatch/configurePatchCore/useConfigurePatch/types"; +import { + sumActivatedTasksInSelectedTasks, + sumActivatedTasksInSet, + sumActivatedTasksInVariantsTasks, +} from "./estimatedActivatedTasks"; + +describe("getNumEstimatedActivatedTasks", () => { + const generatedTaskCounts: GeneratedTaskCountResults[] = [ + { taskName: "task1", buildVariantName: "variant1", estimatedTasks: 5 }, + { taskName: "task2", buildVariantName: "variant1", estimatedTasks: 10 }, + { taskName: "task4", buildVariantName: "variant1", estimatedTasks: 20 }, + { taskId: "task1-variant2", estimatedTasks: 100 }, + { taskId: "task2-variant2", estimatedTasks: 50 }, + { taskId: "task4-variant2", estimatedTasks: 25 }, + ]; + + it("should compute the correct number of activated tasks to be created when configuring a patch where some tasks have already been created", () => { + const selectedBuildVariantTasks: VariantTasksState = { + variant1: { + task1: true, + task2: true, + task3: true, + task4: false, + }, + }; + const variantsTasks: Array = [ + { name: "variant1", tasks: ["task1"] }, + ]; + expect( + sumActivatedTasksInVariantsTasks( + selectedBuildVariantTasks, + generatedTaskCounts, + variantsTasks, + ), + ).toBe(12); + }); + it("should compute zero when configuring a patch where all selected tasks have already been created", () => { + const selectedBuildVariantTasks: VariantTasksState = { + variant1: { + task1: true, + task2: true, + task3: true, + }, + }; + const variantsTasks: Array = [ + { name: "variant1", tasks: ["task1"] }, + { name: "variant1", tasks: ["task2"] }, + { name: "variant1", tasks: ["task3"] }, + ]; + expect( + sumActivatedTasksInVariantsTasks( + selectedBuildVariantTasks, + generatedTaskCounts, + variantsTasks, + ), + ).toBe(0); + }); + it("should compute the correct number of activated tasks to be created when configuring a patch where no tasks have already been created", () => { + const selectedBuildVariantTasks: VariantTasksState = { + variant1: { + task1: true, + task2: true, + task3: true, + }, + }; + expect( + sumActivatedTasksInVariantsTasks( + selectedBuildVariantTasks, + generatedTaskCounts, + [], + ), + ).toBe(18); + }); + it("should compute the correct number of activated tasks to be created when scheduling multiple unscheduled tasks", () => { + const set = new Set(["task1-variant2", "task2-variant2", "task3-variant2"]); + expect(sumActivatedTasksInSet(set, generatedTaskCounts)).toBe(153); + }); + it("should compute the correct number of activated tasks to be created when restarting all tasks in a version", () => { + const vsts: versionSelectedTasks = { + version_id: { + "task1-variant2": true, + "task2-variant2": true, + "task3-variant2": true, + }, + }; + expect(sumActivatedTasksInSelectedTasks(vsts, generatedTaskCounts)).toBe( + 153, + ); + }); + it("should compute the correct number of activated tasks to be created when restarting some tasks in a version", () => { + const vsts: versionSelectedTasks = { + version_id: { + "task1-variant2": true, + "task2-variant2": false, + "task3-variant2": true, + }, + }; + expect(sumActivatedTasksInSelectedTasks(vsts, generatedTaskCounts)).toBe( + 102, + ); + }); + it("should compute zero for empty input", () => { + expect(sumActivatedTasksInSelectedTasks({}, [])).toBe(0); + }); +}); diff --git a/apps/spruce/src/utils/tasks/estimatedActivatedTasks.ts b/apps/spruce/src/utils/tasks/estimatedActivatedTasks.ts new file mode 100644 index 000000000..2b0b91fa9 --- /dev/null +++ b/apps/spruce/src/utils/tasks/estimatedActivatedTasks.ts @@ -0,0 +1,98 @@ +import { GeneratedTaskCountResults, VariantTask } from "gql/generated/types"; +import { versionSelectedTasks } from "hooks/useVersionTaskStatusSelect"; +import { VariantTasksState } from "pages/configurePatch/configurePatchCore/useConfigurePatch/types"; + +/** + * The following functions compute the number of tasks that are estimated to be scheduled as a result of a scheduling / restart operation, + * depending on the type of data structure that is in. + */ + +/** + * `sumActivatedTasksInSet` Computes activated tasks for the "Schedule Tasks" modal. + * Sums the input set size with the `estimatedTasks` values from the `GeneratedTaskCounts` array for tasks that are present in the provided `Set`. + * @param taskIdSet - A set of task ID strings. This set defines which tasks to include in the sum. + * @param generatedTaskCounts - An array of objects containing info on the estimated number of tasks that each generator task (a task that calls generate.tasks) + will schedule, including the `taskId`, `buildVariantName`, `taskName`, and `estimatedTasks`. + * @returns The total sum of `estimatedTasks` for tasks that exist in both the `taskIdSet` and the `generatedTaskCounts` array, plus the size of the set. + */ +export const sumActivatedTasksInSet = ( + taskIdSet: Set, + generatedTaskCounts: GeneratedTaskCountResults[], +): number => + generatedTaskCounts.reduce((total, { estimatedTasks, taskId }) => { + if (taskId && taskIdSet.has(taskId)) { + return total + estimatedTasks; + } + return total; + }, taskIdSet.size); + +/** + * `sumActivatedTasksInVariantsTasks` Computes activated tasks for the "Configure Patch" page. + * Sums the number tasks that are both present the provided `VariantTasksState` and NOT present + * in the provided `VariantTask[], with the total `estimatedTasks` from the `GeneratedTaskCounts` array for those tasks`. + * @param selectedTasks - A VariantTasksState object representing the tasks that are currently selected on the page. + * @param generatedTaskCounts - An array of objects containing info on the estimated number of tasks that each generator task + * (a task that calls generate.tasks) will schedule, including the `taskId`, `buildVariantName`, `taskName`, and `estimatedTasks`. + * @param existingVariantsTasks - An array of VariantTask, representing the initial state of selected tasks in the "Configure Patch" page. + * @returns The total sum of tasks that are selected in the `selectedTasks` map and their corresponding `estimatedTasks` that exist in + * the `generatedTaskCounts` array. + */ +export const sumActivatedTasksInVariantsTasks = ( + selectedTasks: VariantTasksState, + generatedTaskCounts: GeneratedTaskCountResults[], + existingVariantsTasks: VariantTask[], +): number => { + // Create a set of existing tasks from existingVariantsTasks + const existingTasks = new Set( + existingVariantsTasks.flatMap((variantTask) => + variantTask.tasks.map((task) => `${variantTask.name}-${task}`), + ), + ); + + // Sum the estimated tasks for selected tasks that are not in existingTasks + return Object.entries(selectedTasks).reduce( + (total, [variant, tasks]) => + Object.entries(tasks).reduce((innerTotal, [taskName, isSelected]) => { + if (isSelected && !existingTasks.has(`${variant}-${taskName}`)) { + const generatedTaskCount = + generatedTaskCounts.find( + (count) => + count.buildVariantName === variant && + count.taskName === taskName, + )?.estimatedTasks || 0; + return innerTotal + 1 + generatedTaskCount; + } + return innerTotal; + }, total), + 0, + ); +}; + +/** + * `sumActivatedTasksInSelectedTasks` Computes tasks for the "Restart Version" modal. + * Sums the number tasks that are both present the provided `versionSelectedTasks`, with the total `estimatedTasks` from the `GeneratedTaskCounts` + * array for those tasks`. + * @param selectedTasks - A versionSelectedTasks object representing the tasks that are currently selected on the page. + * @param generatedTaskCounts - An array of objects containing info on the estimated number of tasks that each generator task (a task that calls generate.tasks) + * will schedule, including the `taskId`, `buildVariantName`, `taskName`, and `estimatedTasks`. + * @returns The total sum of tasks that are selected in the `selectedTasks` map and their corresponding `estimatedTasks` that exist in + * the `generatedTaskCounts` array. + */ +export const sumActivatedTasksInSelectedTasks = ( + selectedTasks: versionSelectedTasks, + generatedTaskCounts: GeneratedTaskCountResults[], +) => + // Sum the total estimated tasks from the selected tasks + Object.entries(selectedTasks).reduce( + (total, [, tasks]) => + Object.entries(tasks).reduce((innerTotal, [taskId, isSelected]) => { + if (isSelected) { + const generatedTaskCount = + generatedTaskCounts.find((count) => count.taskId === taskId) + ?.estimatedTasks || 0; + return innerTotal + 1 + generatedTaskCount; + } + return innerTotal; + }, total), + 0, + );