From 6f682ef8095390249ec24d54031fa689a4202e0b Mon Sep 17 00:00:00 2001 From: minnakt Date: Sun, 12 Nov 2023 13:50:35 -0500 Subject: [PATCH 1/4] DEVPROD-940: Surface repotracker errors on Project Health page --- cypress/integration/projectHealth/banners.ts | 43 ++++ .../Banners/RepotrackerBanner.test.tsx | 226 ++++++++++++++++++ src/components/Banners/RepotrackerBanner.tsx | 149 ++++++++++++ src/components/Banners/index.ts | 1 + src/gql/generated/types.ts | 81 ++++++- src/gql/mutations/index.ts | 2 + src/gql/mutations/set-last-revision.graphql | 7 + src/gql/queries/index.ts | 2 + src/gql/queries/repotracker-error.graphql | 10 + .../user-project-settings-permissions.graphql | 5 +- src/pages/commits/index.tsx | 3 +- .../CreateDuplicateProjectButton.test.tsx | 12 +- .../CreateDuplicateProjectButton.tsx | 15 +- 13 files changed, 546 insertions(+), 10 deletions(-) create mode 100644 cypress/integration/projectHealth/banners.ts create mode 100644 src/components/Banners/RepotrackerBanner.test.tsx create mode 100644 src/components/Banners/RepotrackerBanner.tsx create mode 100644 src/gql/mutations/set-last-revision.graphql create mode 100644 src/gql/queries/repotracker-error.graphql diff --git a/cypress/integration/projectHealth/banners.ts b/cypress/integration/projectHealth/banners.ts new file mode 100644 index 0000000000..91117f5627 --- /dev/null +++ b/cypress/integration/projectHealth/banners.ts @@ -0,0 +1,43 @@ +describe("banners", () => { + const projectWithRepotrackerError = "/commits/mongodb-mongo-test"; + + describe("repotracker banner", () => { + beforeEach(() => { + cy.visit(projectWithRepotrackerError); + }); + + it("should error if revision is incomplete", () => { + cy.dataCy("repotracker-error-banner").should("be.visible"); + cy.dataCy("repotracker-error-trigger").should("be.visible"); + cy.dataCy("repotracker-error-trigger").click(); + cy.dataCy("repotracker-error-modal").should("be.visible"); + cy.getInputByLabel("Base Revision").type("1234"); + cy.contains("button", "Confirm").should( + "have.attr", + "aria-disabled", + "false" + ); + cy.contains("button", "Confirm").click(); + cy.validateToast("error"); + cy.dataCy("repotracker-error-banner").should("be.visible"); + }); + + it("should be able to clear the repotracker error", () => { + cy.dataCy("repotracker-error-banner").should("be.visible"); + cy.dataCy("repotracker-error-trigger").should("be.visible"); + cy.dataCy("repotracker-error-trigger").click(); + cy.dataCy("repotracker-error-modal").should("be.visible"); + cy.getInputByLabel("Base Revision").type( + "7ad0f0571691fa5063b757762a5b103999290fa8" + ); + cy.contains("button", "Confirm").should( + "have.attr", + "aria-disabled", + "false" + ); + cy.contains("button", "Confirm").click(); + cy.validateToast("success", "Successfully updated merge base revision"); + cy.dataCy("repotracker-error-banner").should("not.exist"); + }); + }); +}); diff --git a/src/components/Banners/RepotrackerBanner.test.tsx b/src/components/Banners/RepotrackerBanner.test.tsx new file mode 100644 index 0000000000..3efb583dee --- /dev/null +++ b/src/components/Banners/RepotrackerBanner.test.tsx @@ -0,0 +1,226 @@ +import { MockedProvider } from "@apollo/client/testing"; +import { RepotrackerBanner } from "components/Banners"; +import { RenderFakeToastContext } from "context/toast/__mocks__"; +import { + UserProjectSettingsPermissionsQuery, + UserProjectSettingsPermissionsQueryVariables, + RepotrackerErrorQuery, + RepotrackerErrorQueryVariables, + SetLastRevisionMutation, + SetLastRevisionMutationVariables, +} from "gql/generated/types"; +import { SET_LAST_REVISION } from "gql/mutations"; +import { + USER_PROJECT_SETTINGS_PERMISSIONS, + REPOTRACKER_ERROR, +} from "gql/queries"; +import { render, screen, userEvent, waitFor } from "test_utils"; +import { ApolloMock } from "types/gql"; + +describe("repotracker banner", () => { + describe("repotracker error does not exist", () => { + it("does not render banner", async () => { + const { Component } = RenderFakeToastContext( + + + + ); + render(); + await waitFor(() => { + expect(screen.queryByDataCy("repotracker-error-banner")).toBeNull(); + }); + }); + }); + + describe("repotracker error exists", () => { + it("renders a banner", async () => { + const { Component } = RenderFakeToastContext( + + + + ); + render(); + await waitFor(() => { + expect(screen.queryByDataCy("repotracker-error-banner")).toBeVisible(); + }); + }); + + it("does not render modal trigger if user is not admin", async () => { + const { Component } = RenderFakeToastContext( + + + + ); + render(); + await waitFor(() => { + expect(screen.queryByDataCy("repotracker-error-banner")).toBeVisible(); + }); + expect(screen.queryByDataCy("repotracker-error-trigger")).toBeNull(); + }); + + it("renders modal trigger if user is admin", async () => { + const { Component } = RenderFakeToastContext( + + + + ); + render(); + await waitFor(() => { + expect(screen.queryByDataCy("repotracker-error-banner")).toBeVisible(); + }); + expect(screen.queryByDataCy("repotracker-error-trigger")).toBeVisible(); + }); + + it("can submit new base revision via modal", async () => { + const user = userEvent.setup(); + const { Component, dispatchToast } = RenderFakeToastContext( + + + + ); + render(); + await waitFor(() => { + expect(screen.queryByDataCy("repotracker-error-banner")).toBeVisible(); + }); + expect(screen.queryByDataCy("repotracker-error-trigger")).toBeVisible(); + + // Open modal. + await user.click(screen.queryByDataCy("repotracker-error-trigger")); + await waitFor(() => { + expect(screen.queryByDataCy("repotracker-error-modal")).toBeVisible(); + }); + + // Submit new base revision. + const confirmButton = screen.getByRole("button", { name: "Confirm" }); + expect(confirmButton).toHaveAttribute("aria-disabled", "true"); + await user.type( + screen.getByLabelText("Base Revision"), + "new_base_revision" + ); + expect(confirmButton).toHaveAttribute("aria-disabled", "false"); + await user.click(confirmButton); + expect(dispatchToast.success).toHaveBeenCalledTimes(1); + }); + }); +}); + +const projectNoError: ApolloMock< + RepotrackerErrorQuery, + RepotrackerErrorQueryVariables +> = { + request: { + query: REPOTRACKER_ERROR, + variables: { + projectIdentifier: "evergreen", + }, + }, + result: { + data: { + project: { + __typename: "Project", + id: "evergreen", + branch: "", + repotrackerError: null, + }, + }, + }, +}; + +const projectWithError: ApolloMock< + RepotrackerErrorQuery, + RepotrackerErrorQueryVariables +> = { + request: { + query: REPOTRACKER_ERROR, + variables: { + projectIdentifier: "evergreen", + }, + }, + result: { + data: { + project: { + __typename: "Project", + id: "evergreen", + branch: "main", + repotrackerError: { + __typename: "RepotrackerError", + exists: true, + invalidRevision: "invalid_revision", + }, + }, + }, + }, +}; + +const adminUser: ApolloMock< + UserProjectSettingsPermissionsQuery, + UserProjectSettingsPermissionsQueryVariables +> = { + request: { + query: USER_PROJECT_SETTINGS_PERMISSIONS, + variables: { projectIdentifier: "evergreen" }, + }, + result: { + data: { + user: { + __typename: "User", + userId: "admin", + permissions: { + __typename: "Permissions", + canCreateProject: true, + projectPermissions: { + __typename: "ProjectPermissions", + admin: true, + }, + }, + }, + }, + }, +}; + +const basicUser: ApolloMock< + UserProjectSettingsPermissionsQuery, + UserProjectSettingsPermissionsQueryVariables +> = { + request: { + query: USER_PROJECT_SETTINGS_PERMISSIONS, + variables: { projectIdentifier: "evergreen" }, + }, + result: { + data: { + user: { + __typename: "User", + userId: "basic", + permissions: { + __typename: "Permissions", + canCreateProject: false, + projectPermissions: { + __typename: "ProjectPermissions", + admin: false, + }, + }, + }, + }, + }, +}; + +const setLastRevision: ApolloMock< + SetLastRevisionMutation, + SetLastRevisionMutationVariables +> = { + request: { + query: SET_LAST_REVISION, + variables: { + projectIdentifier: "evergreen", + revision: "new_base_revision", + }, + }, + result: { + data: { + setLastRevision: { + __typename: "SetLastRevisionPayload", + mergeBaseRevision: "new_base_revision", + }, + }, + }, +}; diff --git a/src/components/Banners/RepotrackerBanner.tsx b/src/components/Banners/RepotrackerBanner.tsx new file mode 100644 index 0000000000..4d904c96c7 --- /dev/null +++ b/src/components/Banners/RepotrackerBanner.tsx @@ -0,0 +1,149 @@ +import { useState } from "react"; +import { useQuery, useMutation } from "@apollo/client"; +import styled from "@emotion/styled"; +import Banner from "@leafygreen-ui/banner"; +import TextInput from "@leafygreen-ui/text-input"; +import { InlineCode } from "@leafygreen-ui/typography"; +import { ConfirmationModal } from "components/ConfirmationModal"; +import { size } from "constants/tokens"; +import { useToastContext } from "context/toast"; +import { + UserProjectSettingsPermissionsQuery, + UserProjectSettingsPermissionsQueryVariables, + RepotrackerErrorQuery, + RepotrackerErrorQueryVariables, + SetLastRevisionMutation, + SetLastRevisionMutationVariables, +} from "gql/generated/types"; +import { SET_LAST_REVISION } from "gql/mutations"; +import { + USER_PROJECT_SETTINGS_PERMISSIONS, + REPOTRACKER_ERROR, +} from "gql/queries"; + +interface RepotrackerBannerProps { + projectIdentifier: string; +} +export const RepotrackerBanner: React.FC = ({ + projectIdentifier, +}) => { + const dispatchToast = useToastContext(); + const [openModal, setOpenModal] = useState(false); + const [baseRevision, setBaseRevision] = useState(""); + + const { data: repotrackerData } = useQuery< + RepotrackerErrorQuery, + RepotrackerErrorQueryVariables + >(REPOTRACKER_ERROR, { + variables: { projectIdentifier }, + }); + const hasRepotrackerError = + repotrackerData?.project?.repotrackerError?.exists; + + const { data: permissionsData } = useQuery< + UserProjectSettingsPermissionsQuery, + UserProjectSettingsPermissionsQueryVariables + >(USER_PROJECT_SETTINGS_PERMISSIONS, { + variables: { projectIdentifier }, + // If there's no repotracker error, there's no need to determine whether the current user is an admin. + skip: !repotrackerData || !hasRepotrackerError, + }); + const isProjectAdmin = + permissionsData?.user?.permissions?.projectPermissions?.admin; + + const [setLastRevision] = useMutation< + SetLastRevisionMutation, + SetLastRevisionMutationVariables + >(SET_LAST_REVISION, { + onCompleted() { + dispatchToast.success( + "Successfully updated merge base revision. The repotracker job has been scheduled to run." + ); + }, + onError: (err) => { + dispatchToast.error( + `Error when attempting to update merge base revision: ${err.message}` + ); + }, + }); + + const resetModal = () => { + setOpenModal(false); + setBaseRevision(""); + }; + + if (!hasRepotrackerError) { + return null; + } + return ( + + + {isProjectAdmin ? ( + + The project was unable to build. Please specify a new base revision + by clicking{" "} + setOpenModal(true)} + > + here + + . + + ) : ( + "The project was unable to build. Please reach out to a project admin to fix." + )} + + { + setLastRevision({ + variables: { projectIdentifier, revision: baseRevision }, + refetchQueries: ["RepotrackerError"], + }); + resetModal(); + }} + open={openModal} + setOpen={setOpenModal} + submitDisabled={!baseRevision.length} + title="Enter New Base Revision" + > + + The current base revision{" "} + + {repotrackerData?.project?.repotrackerError?.invalidRevision} + {" "} + cannot be found on branch '{repotrackerData?.project?.branch} + '. In order to resume tracking the repository, please enter a new + base revision. + + setBaseRevision(e.target.value)} + value={baseRevision} + /> + + + ); +}; + +const BannerContainer = styled.div` + margin-bottom: ${size.s}; +`; + +const ModalDescription = styled.div` + margin-bottom: ${size.xs}; +`; + +const ModalTriggerText = styled.span` + font-weight: bold; + text-decoration-line: underline; + text-underline-offset: 2px; + text-decoration-thickness: 2px; + :hover { + cursor: pointer; + } +`; diff --git a/src/components/Banners/index.ts b/src/components/Banners/index.ts index 3edc7dddba..335d3282fe 100644 --- a/src/components/Banners/index.ts +++ b/src/components/Banners/index.ts @@ -5,3 +5,4 @@ export { SlackNotificationBanner } from "./SlackNotificationBanner"; export { AdminBanner } from "./AdminBanner"; export { PortalBanner } from "./PortalBanner"; export { ProjectBanner } from "./ProjectBanner"; +export { RepotrackerBanner } from "./RepotrackerBanner"; diff --git a/src/gql/generated/types.ts b/src/gql/generated/types.ts index 7ca2e129df..4d6e552931 100644 --- a/src/gql/generated/types.ts +++ b/src/gql/generated/types.ts @@ -1048,6 +1048,7 @@ export type Mutation = { scheduleTasks: Array; scheduleUndispatchedBaseTasks?: Maybe>; setAnnotationMetadataLinks: Scalars["Boolean"]["output"]; + setLastRevision: SetLastRevisionPayload; setPatchPriority?: Maybe; /** setPatchVisibility takes a list of patch ids and a boolean to set the visibility on the my patches queries */ setPatchVisibility: Array; @@ -1273,6 +1274,10 @@ export type MutationSetAnnotationMetadataLinksArgs = { taskId: Scalars["String"]["input"]; }; +export type MutationSetLastRevisionArgs = { + opts: SetLastRevisionInput; +}; + export type MutationSetPatchPriorityArgs = { patchId: Scalars["String"]["input"]; priority: Scalars["Int"]["input"]; @@ -1539,6 +1544,7 @@ export type Permissions = { canCreateProject: Scalars["Boolean"]["output"]; canEditAdminSettings: Scalars["Boolean"]["output"]; distroPermissions: DistroPermissions; + projectPermissions: ProjectPermissions; userId: Scalars["String"]["output"]; }; @@ -1546,6 +1552,10 @@ export type PermissionsDistroPermissionsArgs = { options: DistroPermissionsOptions; }; +export type PermissionsProjectPermissionsArgs = { + options: ProjectPermissionsOptions; +}; + export type PlannerSettings = { __typename?: "PlannerSettings"; commitQueueFactor: Scalars["Int"]["output"]; @@ -1675,6 +1685,7 @@ export type Project = { repo: Scalars["String"]["output"]; repoRefId: Scalars["String"]["output"]; repotrackerDisabled?: Maybe; + repotrackerError?: Maybe; restricted?: Maybe; spawnHostScriptPath: Scalars["String"]["output"]; stepbackDisabled?: Maybe; @@ -1818,6 +1829,16 @@ export type ProjectInput = { workstationConfig?: InputMaybe; }; +export type ProjectPermissions = { + __typename?: "ProjectPermissions"; + admin: Scalars["Boolean"]["output"]; + view: Scalars["Boolean"]["output"]; +}; + +export type ProjectPermissionsOptions = { + projectIdentifier: Scalars["String"]["input"]; +}; + /** ProjectSettings models the settings for a given Project. */ export type ProjectSettings = { __typename?: "ProjectSettings"; @@ -2205,6 +2226,13 @@ export type RepoWorkstationConfig = { setupCommands?: Maybe>; }; +export type RepotrackerError = { + __typename?: "RepotrackerError"; + exists: Scalars["Boolean"]["output"]; + invalidRevision: Scalars["String"]["output"]; + mergeBaseRevision: Scalars["String"]["output"]; +}; + export enum RequiredStatus { MustFail = "MUST_FAIL", MustFinish = "MUST_FINISH", @@ -2272,6 +2300,20 @@ export type SelectorInput = { type: Scalars["String"]["input"]; }; +/** + * SetLastRevisionInput is the input to the setLastRevision mutation. + * It contains information used to fix the repotracker error of a project. + */ +export type SetLastRevisionInput = { + projectIdentifier: Scalars["String"]["input"]; + revision: Scalars["String"]["input"]; +}; + +export type SetLastRevisionPayload = { + __typename?: "SetLastRevisionPayload"; + mergeBaseRevision: Scalars["String"]["output"]; +}; + export type SlackConfig = { __typename?: "SlackConfig"; name?: Maybe; @@ -5037,6 +5079,19 @@ export type ScheduleUndispatchedBaseTasksMutation = { }> | null; }; +export type SetLastRevisionMutationVariables = Exact<{ + projectIdentifier: Scalars["String"]["input"]; + revision: Scalars["String"]["input"]; +}>; + +export type SetLastRevisionMutation = { + __typename?: "Mutation"; + setLastRevision: { + __typename?: "SetLastRevisionPayload"; + mergeBaseRevision: string; + }; +}; + export type SetPatchPriorityMutationVariables = Exact<{ patchId: Scalars["String"]["input"]; priority: Scalars["Int"]["input"]; @@ -7975,6 +8030,24 @@ export type RepoSettingsQuery = { }; }; +export type RepotrackerErrorQueryVariables = Exact<{ + projectIdentifier: Scalars["String"]["input"]; +}>; + +export type RepotrackerErrorQuery = { + __typename?: "Query"; + project: { + __typename?: "Project"; + branch: string; + id: string; + repotrackerError?: { + __typename?: "RepotrackerError"; + exists: boolean; + invalidRevision: string; + } | null; + }; +}; + export type SpawnExpirationInfoQueryVariables = Exact<{ [key: string]: never }>; export type SpawnExpirationInfoQuery = { @@ -8577,7 +8650,7 @@ export type UserPatchesQuery = { }; export type UserProjectSettingsPermissionsQueryVariables = Exact<{ - [key: string]: never; + projectIdentifier: Scalars["String"]["input"]; }>; export type UserProjectSettingsPermissionsQuery = { @@ -8585,7 +8658,11 @@ export type UserProjectSettingsPermissionsQuery = { user: { __typename?: "User"; userId: string; - permissions: { __typename?: "Permissions"; canCreateProject: boolean }; + permissions: { + __typename?: "Permissions"; + canCreateProject: boolean; + projectPermissions: { __typename?: "ProjectPermissions"; admin: boolean }; + }; }; }; diff --git a/src/gql/mutations/index.ts b/src/gql/mutations/index.ts index 6463cce645..57039e9e36 100644 --- a/src/gql/mutations/index.ts +++ b/src/gql/mutations/index.ts @@ -42,6 +42,7 @@ import SAVE_SUBSCRIPTION from "./save-subscription.graphql"; import SCHEDULE_PATCH from "./schedule-patch.graphql"; import SCHEDULE_TASKS from "./schedule-tasks.graphql"; import SCHEDULE_UNDISPATCHED_BASE_TASKS from "./schedule-undispatched-base-tasks.graphql"; +import SET_LAST_REVISION from "./set-last-revision.graphql"; import SET_PATCH_PRIORITY from "./set-patch-priority.graphql"; import SET_PATCH_VISIBILITY from "./set-patch-visibility.graphql"; import SET_TASK_PRIORITY from "./set-task-priority.graphql"; @@ -101,6 +102,7 @@ export { SCHEDULE_PATCH, SCHEDULE_TASKS, SCHEDULE_UNDISPATCHED_BASE_TASKS, + SET_LAST_REVISION, SET_PATCH_PRIORITY, SET_PATCH_VISIBILITY, SET_TASK_PRIORITY, diff --git a/src/gql/mutations/set-last-revision.graphql b/src/gql/mutations/set-last-revision.graphql new file mode 100644 index 0000000000..3834f9c3f2 --- /dev/null +++ b/src/gql/mutations/set-last-revision.graphql @@ -0,0 +1,7 @@ +mutation SetLastRevision($projectIdentifier: String!, $revision: String!) { + setLastRevision( + opts: { projectIdentifier: $projectIdentifier, revision: $revision } + ) { + mergeBaseRevision + } +} diff --git a/src/gql/queries/index.ts b/src/gql/queries/index.ts index f796b6013b..c1e97bad63 100644 --- a/src/gql/queries/index.ts +++ b/src/gql/queries/index.ts @@ -50,6 +50,7 @@ import PROJECTS from "./projects.graphql"; import MY_PUBLIC_KEYS from "./public-keys.graphql"; import REPO_EVENT_LOGS from "./repo-event-logs.graphql"; import REPO_SETTINGS from "./repo-settings.graphql"; +import REPOTRACKER_ERROR from "./repotracker-error.graphql"; import SPAWN_EXPIRATION_INFO from "./spawn-expiration.graphql"; import SPAWN_TASK from "./spawn-task.graphql"; import SPRUCE_CONFIG from "./spruce-config.graphql"; @@ -131,6 +132,7 @@ export { PROJECTS, REPO_EVENT_LOGS, REPO_SETTINGS, + REPOTRACKER_ERROR, SPAWN_EXPIRATION_INFO, SPAWN_TASK, SPRUCE_CONFIG, diff --git a/src/gql/queries/repotracker-error.graphql b/src/gql/queries/repotracker-error.graphql new file mode 100644 index 0000000000..9084474a8f --- /dev/null +++ b/src/gql/queries/repotracker-error.graphql @@ -0,0 +1,10 @@ +query RepotrackerError($projectIdentifier: String!) { + project(projectIdentifier: $projectIdentifier) { + branch + id + repotrackerError { + exists + invalidRevision + } + } +} diff --git a/src/gql/queries/user-project-settings-permissions.graphql b/src/gql/queries/user-project-settings-permissions.graphql index bddd2ea71b..a34a02e2e2 100644 --- a/src/gql/queries/user-project-settings-permissions.graphql +++ b/src/gql/queries/user-project-settings-permissions.graphql @@ -1,7 +1,10 @@ -query UserProjectSettingsPermissions { +query UserProjectSettingsPermissions($projectIdentifier: String!) { user { permissions { canCreateProject + projectPermissions(options: { projectIdentifier: $projectIdentifier }) { + admin + } } userId } diff --git a/src/pages/commits/index.tsx b/src/pages/commits/index.tsx index 058891d163..055959aa78 100644 --- a/src/pages/commits/index.tsx +++ b/src/pages/commits/index.tsx @@ -4,7 +4,7 @@ import styled from "@emotion/styled"; import Cookies from "js-cookie"; import { useParams, useLocation, useNavigate } from "react-router-dom"; import { useProjectHealthAnalytics } from "analytics/projectHealth/useProjectHealthAnalytics"; -import { ProjectBanner } from "components/Banners"; +import { ProjectBanner, RepotrackerBanner } from "components/Banners"; import FilterBadges, { useFilterBadgeQueryParams, } from "components/FilterBadges"; @@ -168,6 +168,7 @@ const Commits = () => { return ( + diff --git a/src/pages/projectSettings/CreateDuplicateProjectButton.test.tsx b/src/pages/projectSettings/CreateDuplicateProjectButton.test.tsx index fe69704242..f342987b1a 100644 --- a/src/pages/projectSettings/CreateDuplicateProjectButton.test.tsx +++ b/src/pages/projectSettings/CreateDuplicateProjectButton.test.tsx @@ -44,7 +44,7 @@ describe("createDuplicateProjectField", () => { > = { request: { query: USER_PROJECT_SETTINGS_PERMISSIONS, - variables: {}, + variables: { projectIdentifier: "evergreen" }, }, result: { data: { @@ -54,6 +54,10 @@ describe("createDuplicateProjectField", () => { permissions: { __typename: "Permissions", canCreateProject: false, + projectPermissions: { + __typename: "ProjectPermissions", + admin: false, + }, }, }, }, @@ -133,7 +137,7 @@ const permissionsMock: ApolloMock< > = { request: { query: USER_PROJECT_SETTINGS_PERMISSIONS, - variables: {}, + variables: { projectIdentifier: "evergreen" }, }, result: { data: { @@ -143,6 +147,10 @@ const permissionsMock: ApolloMock< permissions: { __typename: "Permissions", canCreateProject: true, + projectPermissions: { + __typename: "ProjectPermissions", + admin: true, + }, }, }, }, diff --git a/src/pages/projectSettings/CreateDuplicateProjectButton.tsx b/src/pages/projectSettings/CreateDuplicateProjectButton.tsx index 33c51a3632..3c3c28abb3 100644 --- a/src/pages/projectSettings/CreateDuplicateProjectButton.tsx +++ b/src/pages/projectSettings/CreateDuplicateProjectButton.tsx @@ -3,7 +3,10 @@ import { useQuery } from "@apollo/client"; import { Menu, MenuItem } from "@leafygreen-ui/menu"; import { PlusButton, Size, Variant } from "components/Buttons"; import { zIndex } from "constants/tokens"; -import { UserProjectSettingsPermissionsQuery } from "gql/generated/types"; +import { + UserProjectSettingsPermissionsQuery, + UserProjectSettingsPermissionsQueryVariables, +} from "gql/generated/types"; import { USER_PROJECT_SETTINGS_PERMISSIONS } from "gql/queries"; import { CopyProjectModal } from "./CopyProjectModal"; import { CreateProjectModal } from "./CreateProjectModal"; @@ -34,9 +37,13 @@ export const CreateDuplicateProjectButton: React.FC = ({ projectType, repo, }) => { - const { data } = useQuery( - USER_PROJECT_SETTINGS_PERMISSIONS - ); + const { data } = useQuery< + UserProjectSettingsPermissionsQuery, + UserProjectSettingsPermissionsQueryVariables + >(USER_PROJECT_SETTINGS_PERMISSIONS, { + variables: { projectIdentifier: id }, + }); + const { user: { permissions: { canCreateProject }, From 2452980ac8093c4967e13388ee5a14c0c1c24cbf Mon Sep 17 00:00:00 2001 From: minnakt Date: Sun, 12 Nov 2023 14:11:44 -0500 Subject: [PATCH 2/4] Fix mock for test --- .../CreateDuplicateProjectButton.test.tsx | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/pages/projectSettings/CreateDuplicateProjectButton.test.tsx b/src/pages/projectSettings/CreateDuplicateProjectButton.test.tsx index f342987b1a..1820c2547e 100644 --- a/src/pages/projectSettings/CreateDuplicateProjectButton.test.tsx +++ b/src/pages/projectSettings/CreateDuplicateProjectButton.test.tsx @@ -3,8 +3,10 @@ import { RenderFakeToastContext } from "context/toast/__mocks__"; import { UserProjectSettingsPermissionsQuery, UserProjectSettingsPermissionsQueryVariables, + GithubOrgsQuery, + GithubOrgsQueryVariables, } from "gql/generated/types"; -import { USER_PROJECT_SETTINGS_PERMISSIONS } from "gql/queries"; +import { USER_PROJECT_SETTINGS_PERMISSIONS, GITHUB_ORGS } from "gql/queries"; import { renderWithRouterMatch as render, screen, @@ -25,7 +27,7 @@ const Button = ({ mock?: MockedResponse; projectType?: ProjectType; }) => ( - + { > = { request: { query: USER_PROJECT_SETTINGS_PERMISSIONS, - variables: { projectIdentifier: "evergreen" }, + variables: { projectIdentifier: "my_id" }, }, result: { data: { @@ -137,7 +139,7 @@ const permissionsMock: ApolloMock< > = { request: { query: USER_PROJECT_SETTINGS_PERMISSIONS, - variables: { projectIdentifier: "evergreen" }, + variables: { projectIdentifier: "my_id" }, }, result: { data: { @@ -156,3 +158,18 @@ const permissionsMock: ApolloMock< }, }, }; + +const githubOrgsMock: ApolloMock = { + request: { + query: GITHUB_ORGS, + variables: {}, + }, + result: { + data: { + spruceConfig: { + __typename: "SpruceConfig", + githubOrgs: ["evergreen"], + }, + }, + }, +}; From eceec7d74d56e25a1d84ed60132ff59731522fb1 Mon Sep 17 00:00:00 2001 From: minnakt Date: Tue, 21 Nov 2023 12:17:14 -0500 Subject: [PATCH 3/4] Address feedback --- .../{banners.ts => project_banners.ts} | 2 +- src/components/Banners/PortalBanner.tsx | 12 ++-- src/components/Banners/ProjectBanner.tsx | 3 +- .../Banners/RepotrackerBanner.test.tsx | 4 +- src/components/Banners/RepotrackerBanner.tsx | 61 ++++++++++--------- src/gql/generated/types.ts | 2 +- .../user-project-settings-permissions.graphql | 2 +- .../CreateDuplicateProjectButton.test.tsx | 4 +- 8 files changed, 46 insertions(+), 44 deletions(-) rename cypress/integration/projectHealth/{banners.ts => project_banners.ts} (97%) diff --git a/cypress/integration/projectHealth/banners.ts b/cypress/integration/projectHealth/project_banners.ts similarity index 97% rename from cypress/integration/projectHealth/banners.ts rename to cypress/integration/projectHealth/project_banners.ts index 91117f5627..ed566e49d9 100644 --- a/cypress/integration/projectHealth/banners.ts +++ b/cypress/integration/projectHealth/project_banners.ts @@ -1,4 +1,4 @@ -describe("banners", () => { +describe("project banners", () => { const projectWithRepotrackerError = "/commits/mongodb-mongo-test"; describe("repotracker banner", () => { diff --git a/src/components/Banners/PortalBanner.tsx b/src/components/Banners/PortalBanner.tsx index 4fca2ea9e6..f8c4ff0ab3 100644 --- a/src/components/Banners/PortalBanner.tsx +++ b/src/components/Banners/PortalBanner.tsx @@ -1,10 +1,10 @@ import { createPortal } from "react-dom"; -import { SiteBanner, SiteBannerProps } from "./SiteBanner"; -interface PortalBannerProps extends SiteBannerProps {} -export const PortalBanner: React.FC = ({ text, theme }) => { +interface PortalBannerProps { + banner: React.ReactNode; +} + +export const PortalBanner: React.FC = ({ banner }) => { const bannerContainerEl = document.getElementById("banner-container"); - return bannerContainerEl - ? createPortal(, bannerContainerEl) - : null; + return bannerContainerEl ? createPortal(banner, bannerContainerEl) : null; }; diff --git a/src/components/Banners/ProjectBanner.tsx b/src/components/Banners/ProjectBanner.tsx index c589e76e8b..44e111217f 100644 --- a/src/components/Banners/ProjectBanner.tsx +++ b/src/components/Banners/ProjectBanner.tsx @@ -5,6 +5,7 @@ import { } from "gql/generated/types"; import { PROJECT_BANNER } from "gql/queries"; import { PortalBanner } from "./PortalBanner"; +import { SiteBanner } from "./SiteBanner"; interface ProjectBannerProps { projectIdentifier: string; @@ -23,5 +24,5 @@ export const ProjectBanner: React.FC = ({ if (!text) { return null; } - return ; + return } />; }; diff --git a/src/components/Banners/RepotrackerBanner.test.tsx b/src/components/Banners/RepotrackerBanner.test.tsx index 3efb583dee..c6529280cc 100644 --- a/src/components/Banners/RepotrackerBanner.test.tsx +++ b/src/components/Banners/RepotrackerBanner.test.tsx @@ -170,7 +170,7 @@ const adminUser: ApolloMock< canCreateProject: true, projectPermissions: { __typename: "ProjectPermissions", - admin: true, + edit: true, }, }, }, @@ -196,7 +196,7 @@ const basicUser: ApolloMock< canCreateProject: false, projectPermissions: { __typename: "ProjectPermissions", - admin: false, + edit: false, }, }, }, diff --git a/src/components/Banners/RepotrackerBanner.tsx b/src/components/Banners/RepotrackerBanner.tsx index 4d904c96c7..00048a3ab0 100644 --- a/src/components/Banners/RepotrackerBanner.tsx +++ b/src/components/Banners/RepotrackerBanner.tsx @@ -20,6 +20,7 @@ import { USER_PROJECT_SETTINGS_PERMISSIONS, REPOTRACKER_ERROR, } from "gql/queries"; +import { PortalBanner } from "./PortalBanner"; interface RepotrackerBannerProps { projectIdentifier: string; @@ -38,24 +39,24 @@ export const RepotrackerBanner: React.FC = ({ variables: { projectIdentifier }, }); const hasRepotrackerError = - repotrackerData?.project?.repotrackerError?.exists; + repotrackerData?.project?.repotrackerError?.exists ?? false; const { data: permissionsData } = useQuery< UserProjectSettingsPermissionsQuery, UserProjectSettingsPermissionsQueryVariables >(USER_PROJECT_SETTINGS_PERMISSIONS, { variables: { projectIdentifier }, - // If there's no repotracker error, there's no need to determine whether the current user is an admin. - skip: !repotrackerData || !hasRepotrackerError, + // If there's no repotracker error, there is no need to determine whether the current user is an admin. + skip: !hasRepotrackerError, }); const isProjectAdmin = - permissionsData?.user?.permissions?.projectPermissions?.admin; + permissionsData?.user?.permissions?.projectPermissions?.edit ?? false; const [setLastRevision] = useMutation< SetLastRevisionMutation, SetLastRevisionMutationVariables >(SET_LAST_REVISION, { - onCompleted() { + onCompleted: () => { dispatchToast.success( "Successfully updated merge base revision. The repotracker job has been scheduled to run." ); @@ -76,24 +77,28 @@ export const RepotrackerBanner: React.FC = ({ return null; } return ( - - - {isProjectAdmin ? ( - - The project was unable to build. Please specify a new base revision - by clicking{" "} - setOpenModal(true)} - > - here - - . - - ) : ( - "The project was unable to build. Please reach out to a project admin to fix." - )} - + <> + + {isProjectAdmin ? ( + + The project was unable to build. Please specify a new base + revision by clicking{" "} + setOpenModal(true)} + > + here + + . + + ) : ( + "The project was unable to build. Please reach out to a project admin to fix." + )} + + } + /> = ({ }} open={openModal} setOpen={setOpenModal} - submitDisabled={!baseRevision.length} + submitDisabled={baseRevision.length < 40} title="Enter New Base Revision" > @@ -120,20 +125,16 @@ export const RepotrackerBanner: React.FC = ({ base revision. setBaseRevision(e.target.value)} value={baseRevision} /> - + ); }; -const BannerContainer = styled.div` - margin-bottom: ${size.s}; -`; - const ModalDescription = styled.div` margin-bottom: ${size.xs}; `; diff --git a/src/gql/generated/types.ts b/src/gql/generated/types.ts index 1e266a2ee7..9d4fd7a574 100644 --- a/src/gql/generated/types.ts +++ b/src/gql/generated/types.ts @@ -8666,7 +8666,7 @@ export type UserProjectSettingsPermissionsQuery = { permissions: { __typename?: "Permissions"; canCreateProject: boolean; - projectPermissions: { __typename?: "ProjectPermissions"; admin: boolean }; + projectPermissions: { __typename?: "ProjectPermissions"; edit: boolean }; }; }; }; diff --git a/src/gql/queries/user-project-settings-permissions.graphql b/src/gql/queries/user-project-settings-permissions.graphql index a34a02e2e2..6004031fc2 100644 --- a/src/gql/queries/user-project-settings-permissions.graphql +++ b/src/gql/queries/user-project-settings-permissions.graphql @@ -3,7 +3,7 @@ query UserProjectSettingsPermissions($projectIdentifier: String!) { permissions { canCreateProject projectPermissions(options: { projectIdentifier: $projectIdentifier }) { - admin + edit } } userId diff --git a/src/pages/projectSettings/CreateDuplicateProjectButton.test.tsx b/src/pages/projectSettings/CreateDuplicateProjectButton.test.tsx index 1820c2547e..9465a16799 100644 --- a/src/pages/projectSettings/CreateDuplicateProjectButton.test.tsx +++ b/src/pages/projectSettings/CreateDuplicateProjectButton.test.tsx @@ -58,7 +58,7 @@ describe("createDuplicateProjectField", () => { canCreateProject: false, projectPermissions: { __typename: "ProjectPermissions", - admin: false, + edit: false, }, }, }, @@ -151,7 +151,7 @@ const permissionsMock: ApolloMock< canCreateProject: true, projectPermissions: { __typename: "ProjectPermissions", - admin: true, + edit: true, }, }, }, From 041fafafd4a25862c42c840522059f144bf9fbec Mon Sep 17 00:00:00 2001 From: minnakt Date: Tue, 21 Nov 2023 19:59:53 -0500 Subject: [PATCH 4/4] Fix Jest tests --- .../projectHealth/project_banners.ts | 16 ----------- .../Banners/RepotrackerBanner.test.tsx | 28 ++++++++++++++----- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/cypress/integration/projectHealth/project_banners.ts b/cypress/integration/projectHealth/project_banners.ts index ed566e49d9..aa84717cb8 100644 --- a/cypress/integration/projectHealth/project_banners.ts +++ b/cypress/integration/projectHealth/project_banners.ts @@ -6,22 +6,6 @@ describe("project banners", () => { cy.visit(projectWithRepotrackerError); }); - it("should error if revision is incomplete", () => { - cy.dataCy("repotracker-error-banner").should("be.visible"); - cy.dataCy("repotracker-error-trigger").should("be.visible"); - cy.dataCy("repotracker-error-trigger").click(); - cy.dataCy("repotracker-error-modal").should("be.visible"); - cy.getInputByLabel("Base Revision").type("1234"); - cy.contains("button", "Confirm").should( - "have.attr", - "aria-disabled", - "false" - ); - cy.contains("button", "Confirm").click(); - cy.validateToast("error"); - cy.dataCy("repotracker-error-banner").should("be.visible"); - }); - it("should be able to clear the repotracker error", () => { cy.dataCy("repotracker-error-banner").should("be.visible"); cy.dataCy("repotracker-error-trigger").should("be.visible"); diff --git a/src/components/Banners/RepotrackerBanner.test.tsx b/src/components/Banners/RepotrackerBanner.test.tsx index c6529280cc..b4e0987a67 100644 --- a/src/components/Banners/RepotrackerBanner.test.tsx +++ b/src/components/Banners/RepotrackerBanner.test.tsx @@ -18,6 +18,19 @@ import { render, screen, userEvent, waitFor } from "test_utils"; import { ApolloMock } from "types/gql"; describe("repotracker banner", () => { + beforeEach(() => { + const bannerContainer = document.createElement("div"); + bannerContainer.setAttribute("id", "banner-container"); + const body = document.body as HTMLElement; + body.appendChild(bannerContainer); + }); + + afterEach(() => { + const bannerContainer = document.getElementById("banner-container"); + const body = document.body as HTMLElement; + body.removeChild(bannerContainer); + }); + describe("repotracker error does not exist", () => { it("does not render banner", async () => { const { Component } = RenderFakeToastContext( @@ -74,7 +87,9 @@ describe("repotracker banner", () => { it("can submit new base revision via modal", async () => { const user = userEvent.setup(); const { Component, dispatchToast } = RenderFakeToastContext( - + ); @@ -93,10 +108,7 @@ describe("repotracker banner", () => { // Submit new base revision. const confirmButton = screen.getByRole("button", { name: "Confirm" }); expect(confirmButton).toHaveAttribute("aria-disabled", "true"); - await user.type( - screen.getByLabelText("Base Revision"), - "new_base_revision" - ); + await user.type(screen.getByLabelText("Base Revision"), baseRevision); expect(confirmButton).toHaveAttribute("aria-disabled", "false"); await user.click(confirmButton); expect(dispatchToast.success).toHaveBeenCalledTimes(1); @@ -104,6 +116,8 @@ describe("repotracker banner", () => { }); }); +const baseRevision = "7ad0f0571691fa5063b757762a5b103999290fa8"; + const projectNoError: ApolloMock< RepotrackerErrorQuery, RepotrackerErrorQueryVariables @@ -212,14 +226,14 @@ const setLastRevision: ApolloMock< query: SET_LAST_REVISION, variables: { projectIdentifier: "evergreen", - revision: "new_base_revision", + revision: baseRevision, }, }, result: { data: { setLastRevision: { __typename: "SetLastRevisionPayload", - mergeBaseRevision: "new_base_revision", + mergeBaseRevision: baseRevision, }, }, },