diff --git a/cypress/integration/projectHealth/commits.ts b/cypress/integration/projectHealth/commits.ts index 0f65820e67..cc7c9c8309 100644 --- a/cypress/integration/projectHealth/commits.ts +++ b/cypress/integration/projectHealth/commits.ts @@ -2,40 +2,43 @@ describe("commits page", () => { beforeEach(() => { cy.visit("/commits/spruce"); }); - it("should present a default view with only failing task icons visible", () => { - cy.dataCy("waterfall-task-status-icon").should("exist"); - cy.dataCy("waterfall-task-status-icon") - .should("be.visible") - .should("have.length", 2); - cy.dataCy("waterfall-task-status-icon").should( - "have.attr", - "aria-label", - "failed icon" - ); - cy.dataCy("grouped-task-status-badge").should("not.exist"); - }); - it("shows all icons and no badges when the view is toggled", () => { - cy.dataCy("waterfall-task-status-icon").should("exist"); + describe("view", () => { + it("should present a default view with only failing task icons visible", () => { + cy.dataCy("waterfall-task-status-icon").should("exist"); + cy.dataCy("waterfall-task-status-icon") + .should("be.visible") + .should("have.length", 2); + cy.dataCy("waterfall-task-status-icon").should( + "have.attr", + "aria-label", + "failed icon" + ); + cy.dataCy("grouped-task-status-badge").should("not.exist"); + }); + + it("shows all icons and no badges when the view is toggled", () => { + cy.dataCy("waterfall-task-status-icon").should("exist"); - cy.dataCy("view-all").click(); - cy.dataCy("waterfall-task-status-icon") - .should("be.visible") - .should("have.length", 50); - cy.dataCy("grouped-task-status-badge").should("have.length", 0); - cy.location("search").should("contain", "view=ALL"); + cy.dataCy("view-all").click(); + cy.dataCy("waterfall-task-status-icon") + .should("be.visible") + .should("have.length", 50); + cy.dataCy("grouped-task-status-badge").should("have.length", 0); + cy.location("search").should("contain", "view=ALL"); - cy.dataCy("view-failed").click(); - cy.dataCy("waterfall-task-status-icon") - .should("be.visible") - .should("have.length", 2); - }); + cy.dataCy("view-failed").click(); + cy.dataCy("waterfall-task-status-icon") + .should("be.visible") + .should("have.length", 2); + }); - it("shows all icons when loaded with the view all query param", () => { - cy.visit(`/commits/spruce?view=ALL`); - cy.dataCy("waterfall-task-status-icon") - .should("be.visible") - .should("have.length", 50); + it("shows all icons when loaded with the view all query param", () => { + cy.visit(`/commits/spruce?view=ALL`); + cy.dataCy("waterfall-task-status-icon") + .should("be.visible") + .should("have.length", 50); + }); }); it("should be able to collapse/expand commit graph which retains state when paginating", () => { @@ -97,21 +100,7 @@ describe("commits page", () => { cy.dataCy("prev-page-button").click(); cy.dataCy("prev-page-button").should("have.attr", "aria-disabled", "true"); }); - it("should only show matching requester filters", () => { - cy.dataCy("requester-select").click(); - cy.dataCy("requester-select-options").should("be.visible"); - cy.dataCy("requester-select-options").within(() => { - cy.getInputByLabel("Git Tag").should("exist"); - cy.getInputByLabel("Git Tag").should("not.be.checked"); - cy.getInputByLabel("Git Tag").check({ force: true }); - }); - cy.dataCy("requester-select").click(); - cy.dataCy("requester-select-options").should("not.exist"); - cy.dataCy("commit-label").should("exist"); - cy.dataCy("commit-label").each(($el) => { - cy.wrap($el).should("contain.text", "Git Tag"); - }); - }); + it("resizing the page adjusts the number of commits rendered", () => { cy.visit("/commits/spruce"); cy.dataCy("commit-chart-container").should("have.length", 9); @@ -120,94 +109,7 @@ describe("commits page", () => { cy.viewport(1280, 1024); cy.dataCy("commit-chart-container").should("have.length", 6); }); - describe("task filtering", () => { - beforeEach(() => { - cy.visit("/commits/spruce"); - }); - it("applying an `all` status filter should show all matching tasks with non failing tasks grouped", () => { - cy.dataCy("project-task-status-select").should("exist"); - cy.dataCy("project-task-status-select").click(); - cy.dataCy("project-task-status-select-options").should("be.visible"); - cy.dataCy("project-task-status-select-options").within(() => { - cy.getInputByLabel("All").should("exist"); - cy.getInputByLabel("All").should("not.be.checked"); - cy.getInputByLabel("All").check({ force: true }); - }); - cy.dataCy("project-task-status-select").click(); - cy.dataCy("project-task-status-select-options").should("not.exist"); - cy.dataCy("grouped-task-status-badge").should("have.length", 9); - cy.dataCy("waterfall-task-status-icon").should("have.length", 2); - }); - it("applying a status filter should only show matching tasks", () => { - cy.dataCy("project-task-status-select").should("exist"); - cy.dataCy("project-task-status-select").click(); - cy.dataCy("project-task-status-select-options").should("be.visible"); - cy.dataCy("project-task-status-select-options").within(() => { - cy.getInputByLabel("Succeeded").should("exist"); - cy.getInputByLabel("Succeeded").should("not.be.checked"); - cy.getInputByLabel("Succeeded").check({ force: true }); - }); - cy.dataCy("project-task-status-select").click(); - cy.dataCy("project-task-status-select-options").should("not.exist"); - cy.dataCy("grouped-task-status-badge").should("have.length", 9); - cy.dataCy("grouped-task-status-badge").should( - "contain.text", - "Succeeded" - ); - cy.dataCy("waterfall-task-status-icon").should("not.exist"); - }); - it("applying a build variant filter should show all task statuses by default", () => { - cy.getInputByLabel("Add New Filter").type("Ubuntu").type("{enter}"); - cy.dataCy("filter-badge").should("have.length", 1); - cy.dataCy("filter-badge").should("have.text", "buildVariants: Ubuntu"); - cy.location("search").should("contain", "?buildVariants=Ubuntu"); - cy.dataCy("grouped-task-status-badge").should("have.length", 9); - cy.dataCy("waterfall-task-status-icon").should("have.length", 2); - cy.dataCy("waterfall-task-status-icon").should( - "have.attr", - "aria-label", - "failed icon" - ); - }); - it("applying a task filter should show all task icons instead of groupings", () => { - cy.contains("button", "Build Variant").should("exist"); - cy.contains("button", "Build Variant").click({ force: true }); - cy.get("li").contains("Task").should("be.visible"); - cy.get("li").contains("Task").click(); - cy.getInputByLabel("Add New Filter").type(".").type("{enter}"); - cy.dataCy("grouped-task-status-badge").should("not.exist"); - cy.dataCy("waterfall-task-status-icon").should("have.length", 51); - cy.dataCy("waterfall-task-status-icon") - .get("[aria-label='failed icon']") - .should("exist"); - cy.dataCy("waterfall-task-status-icon") - .get("[aria-label='failed icon']") - .should("have.length", 2); - cy.dataCy("waterfall-task-status-icon") - .get("[aria-label='success icon']") - .should("exist"); - cy.dataCy("waterfall-task-status-icon") - .get("[aria-label='success icon']") - .should("have.length", 49); - }); - it("should hide commits that don't match applied filters", () => { - cy.dataCy("project-task-status-select").should("exist"); - cy.dataCy("project-task-status-select").click(); - cy.dataCy("project-task-status-select-options").should("be.visible"); - cy.dataCy("project-task-status-select-options").within(() => { - cy.getInputByLabel("Failed").should("exist"); - cy.getInputByLabel("Failed").should("not.be.checked"); - cy.getInputByLabel("Failed").check({ force: true }); - }); - cy.dataCy("project-task-status-select").click(); - cy.dataCy("project-task-status-select-options").should("not.exist"); - cy.dataCy("grouped-task-status-badge").should("not.exist"); - cy.dataCy("inactive-commits-button").should("have.length", 5); - cy.dataCy("inactive-commits-button").each(($el) => { - cy.wrap($el).should("contain.text", "Unmatching"); - }); - }); - }); + describe("inactive / unmatching commit tooltips", () => { beforeEach(() => { cy.visit("/commits/spruce"); @@ -295,4 +197,60 @@ describe("commits page", () => { }); }); }); + + describe("search by git commit", () => { + const revision = "aac24c894994e44a2dadc6db40b46eb82e41f2cc"; + + const searchCommit = (commitHash: string) => { + cy.dataCy("waterfall-menu").click(); + cy.dataCy("git-commit-search").click(); + cy.dataCy("git-commit-search-modal").should("be.visible"); + cy.getInputByLabel("Git Commit Hash").type(commitHash); + cy.contains("button", "Submit").click(); + cy.location("search").should("contain", `revision=${commitHash}`); + }; + + beforeEach(() => { + cy.visit("/commits/spruce"); + }); + + it("should show an error toast if the commit could not be found", () => { + searchCommit(revision.slice(3, 12)); + cy.validateToast("error"); + }); + + it("should jump to the given commit", () => { + searchCommit(revision); + cy.get("[data-selected='true']").should("be.visible"); + cy.get("[data-selected='true']").should( + "contain.text", + revision.substring(0, 7) + ); + }); + + it("should clear any applied filters and skip numbers", () => { + cy.visit( + "/commits/spruce?buildVariants=ubuntu&skipOrderNumber=1231&taskNames=codegen&view=FAILED" + ); + searchCommit(revision); + cy.location("search").should("not.contain", "buildVariants"); + cy.location("search").should("not.contain", "taskNames"); + cy.location("search").should("not.contain", "skipOrderNumber"); + }); + + it("should be possible to paginate from the given commit", () => { + searchCommit(revision); + cy.get("[data-selected='true']").should("be.visible"); + + cy.dataCy("next-page-button").click(); + cy.get("[data-selected='true']").should("not.exist"); + cy.dataCy("prev-page-button").click(); + cy.get("[data-selected='true']").should("be.visible"); + + cy.dataCy("prev-page-button").click(); + cy.get("[data-selected='true']").should("not.exist"); + cy.dataCy("next-page-button").click(); + cy.get("[data-selected='true']").should("be.visible"); + }); + }); }); diff --git a/cypress/integration/projectHealth/filtering.ts b/cypress/integration/projectHealth/filtering.ts new file mode 100644 index 0000000000..d43128f1ae --- /dev/null +++ b/cypress/integration/projectHealth/filtering.ts @@ -0,0 +1,107 @@ +describe("filtering", () => { + beforeEach(() => { + cy.visit("/commits/spruce"); + }); + + it("should only show matching requester filters", () => { + cy.dataCy("requester-select").click(); + cy.dataCy("requester-select-options").should("be.visible"); + cy.dataCy("requester-select-options").within(() => { + cy.getInputByLabel("Git Tag").should("exist"); + cy.getInputByLabel("Git Tag").should("not.be.checked"); + cy.getInputByLabel("Git Tag").check({ force: true }); + }); + cy.dataCy("requester-select").click(); + cy.dataCy("requester-select-options").should("not.exist"); + cy.dataCy("commit-label").should("exist"); + cy.dataCy("commit-label").each(($el) => { + cy.wrap($el).should("contain.text", "Git Tag"); + }); + }); + + describe("task filtering", () => { + it("applying an `all` status filter should show all matching tasks with non failing tasks grouped", () => { + cy.dataCy("project-task-status-select").should("exist"); + cy.dataCy("project-task-status-select").click(); + cy.dataCy("project-task-status-select-options").should("be.visible"); + cy.dataCy("project-task-status-select-options").within(() => { + cy.getInputByLabel("All").should("exist"); + cy.getInputByLabel("All").should("not.be.checked"); + cy.getInputByLabel("All").check({ force: true }); + }); + cy.dataCy("project-task-status-select").click(); + cy.dataCy("project-task-status-select-options").should("not.exist"); + cy.dataCy("grouped-task-status-badge").should("have.length", 9); + cy.dataCy("waterfall-task-status-icon").should("have.length", 2); + }); + it("applying a status filter should only show matching tasks", () => { + cy.dataCy("project-task-status-select").should("exist"); + cy.dataCy("project-task-status-select").click(); + cy.dataCy("project-task-status-select-options").should("be.visible"); + cy.dataCy("project-task-status-select-options").within(() => { + cy.getInputByLabel("Succeeded").should("exist"); + cy.getInputByLabel("Succeeded").should("not.be.checked"); + cy.getInputByLabel("Succeeded").check({ force: true }); + }); + cy.dataCy("project-task-status-select").click(); + cy.dataCy("project-task-status-select-options").should("not.exist"); + cy.dataCy("grouped-task-status-badge").should("have.length", 9); + cy.dataCy("grouped-task-status-badge").should( + "contain.text", + "Succeeded" + ); + cy.dataCy("waterfall-task-status-icon").should("not.exist"); + }); + it("applying a build variant filter should show all task statuses by default", () => { + cy.getInputByLabel("Add New Filter").type("Ubuntu{enter}"); + cy.dataCy("filter-badge").should("have.length", 1); + cy.dataCy("filter-badge").should("have.text", "buildVariants: Ubuntu"); + cy.location("search").should("contain", "?buildVariants=Ubuntu"); + cy.dataCy("grouped-task-status-badge").should("have.length", 9); + cy.dataCy("waterfall-task-status-icon").should("have.length", 2); + cy.dataCy("waterfall-task-status-icon").should( + "have.attr", + "aria-label", + "failed icon" + ); + }); + it("applying a task filter should show all task icons instead of groupings", () => { + cy.contains("button", "Build Variant").should("exist"); + cy.contains("button", "Build Variant").click({ force: true }); + cy.get("li").contains("Task").should("be.visible"); + cy.get("li").contains("Task").click(); + cy.getInputByLabel("Add New Filter").type(".{enter}"); + cy.dataCy("grouped-task-status-badge").should("not.exist"); + cy.dataCy("waterfall-task-status-icon").should("have.length", 51); + cy.dataCy("waterfall-task-status-icon") + .get("[aria-label='failed icon']") + .should("exist"); + cy.dataCy("waterfall-task-status-icon") + .get("[aria-label='failed icon']") + .should("have.length", 2); + cy.dataCy("waterfall-task-status-icon") + .get("[aria-label='success icon']") + .should("exist"); + cy.dataCy("waterfall-task-status-icon") + .get("[aria-label='success icon']") + .should("have.length", 49); + }); + it("should hide commits that don't match applied filters", () => { + cy.dataCy("project-task-status-select").should("exist"); + cy.dataCy("project-task-status-select").click(); + cy.dataCy("project-task-status-select-options").should("be.visible"); + cy.dataCy("project-task-status-select-options").within(() => { + cy.getInputByLabel("Failed").should("exist"); + cy.getInputByLabel("Failed").should("not.be.checked"); + cy.getInputByLabel("Failed").check({ force: true }); + }); + cy.dataCy("project-task-status-select").click(); + cy.dataCy("project-task-status-select-options").should("not.exist"); + cy.dataCy("grouped-task-status-badge").should("not.exist"); + cy.dataCy("inactive-commits-button").should("have.length", 5); + cy.dataCy("inactive-commits-button").each(($el) => { + cy.wrap($el).should("contain.text", "Unmatching"); + }); + }); + }); +}); diff --git a/src/analytics/projectHealth/useProjectHealthAnalytics.ts b/src/analytics/projectHealth/useProjectHealthAnalytics.ts index e24d96a98c..6ec3a4f01d 100644 --- a/src/analytics/projectHealth/useProjectHealthAnalytics.ts +++ b/src/analytics/projectHealth/useProjectHealthAnalytics.ts @@ -38,6 +38,8 @@ type Action = name: "Toggle commit chart label tooltip"; } | { name: "Open Notification Modal" } + | { name: "Open Git Commit Search Modal" } + | { name: "Search for commit"; commit: string } | { name: "Add Notification"; subscription: SaveSubscriptionForUserMutationVariables["subscription"]; diff --git a/src/components/ButtonDropdown.tsx b/src/components/ButtonDropdown.tsx index 957598d776..4de498adbe 100644 --- a/src/components/ButtonDropdown.tsx +++ b/src/components/ButtonDropdown.tsx @@ -9,6 +9,8 @@ interface Props { dropdownItems: JSX.Element[]; size?: "default" | "small" | "large"; "data-cy"?: string; + open?: boolean; + setOpen?: (open: boolean) => void; } export const ButtonDropdown: React.FC = ({ @@ -16,6 +18,8 @@ export const ButtonDropdown: React.FC = ({ disabled = false, dropdownItems, loading = false, + open = undefined, + setOpen = undefined, size = "small", }) => ( = ({ data-cy="card-dropdown" popoverZIndex={zIndex.popover} adjustOnMutation + open={open} + setOpen={setOpen} > {dropdownItems} diff --git a/src/gql/generated/types.ts b/src/gql/generated/types.ts index f6500cdbe0..509c49c47a 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"]; @@ -1819,6 +1829,16 @@ export type ProjectInput = { workstationConfig?: InputMaybe; }; +export type ProjectPermissions = { + __typename?: "ProjectPermissions"; + edit: 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"; @@ -2280,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; diff --git a/src/pages/commits/CommitChart/index.tsx b/src/pages/commits/CommitChart/index.tsx index edf16c39dd..0f50b8a915 100644 --- a/src/pages/commits/CommitChart/index.tsx +++ b/src/pages/commits/CommitChart/index.tsx @@ -26,14 +26,9 @@ const DEFAULT_OPEN_STATE = true; interface Props { versions?: Commits; hasTaskFilter?: boolean; - hasError?: boolean; } -export const CommitChart: React.FC = ({ - hasError = false, - hasTaskFilter, - versions, -}) => { +export const CommitChart: React.FC = ({ hasTaskFilter, versions }) => { const [chartOpen, setChartOpen] = useQueryParam( ChartToggleQueryParams.chartOpen, DEFAULT_OPEN_STATE @@ -70,7 +65,7 @@ export const CommitChart: React.FC = ({ const onToggleAccordion = ({ isVisible }) => Cookies.set(COMMIT_CHART_TYPE_VIEW_OPTIONS_ACCORDION, isVisible.toString()); - return hasError ? ( + return !versions ? ( diff --git a/src/pages/commits/CommitsWrapper.tsx b/src/pages/commits/CommitsWrapper.tsx index bc00586e33..02bad5e401 100644 --- a/src/pages/commits/CommitsWrapper.tsx +++ b/src/pages/commits/CommitsWrapper.tsx @@ -1,5 +1,4 @@ import { useMemo } from "react"; -import { ApolloError } from "@apollo/client"; import styled from "@emotion/styled"; import { palette } from "@leafygreen-ui/palette"; import { Skeleton } from "antd"; @@ -9,6 +8,7 @@ import { CommitChart } from "./CommitChart"; import { getCommitKey, getCommitWidth, + isCommitSelected, RenderCommitsLabel, RenderCommitsBuildVariants, } from "./RenderCommit"; @@ -19,17 +19,17 @@ const { white } = palette; interface CommitsWrapperProps { versions: Commits; - error?: ApolloError; isLoading: boolean; hasTaskFilter: boolean; hasFilters: boolean; + revision?: string; } export const CommitsWrapper: React.FC = ({ - error, hasFilters, hasTaskFilter, isLoading, + revision, versions, }) => { const buildVariantDict = useMemo(() => { @@ -38,12 +38,12 @@ export const CommitsWrapper: React.FC = ({ } }, [versions]); - if (error) { - return ; - } if (isLoading) { return ; } + if (!versions) { + return ; + } if (versions) { return ( @@ -54,6 +54,8 @@ export const CommitsWrapper: React.FC = ({ @@ -65,6 +67,7 @@ export const CommitsWrapper: React.FC = ({ = ({ name: "Paginate", direction: "previous", }); - // 0 is the first page so we can just omit the query param updateQueryParams({ [MainlineCommitQueryParams.SkipOrderNumber]: - prevPageOrderNumber > 0 ? prevPageOrderNumber.toString() : undefined, + prevPageOrderNumber.toString(), }); }; return ( diff --git a/src/pages/commits/ProjectHealth.stories.tsx b/src/pages/commits/ProjectHealth.stories.tsx index a9c8e96531..1d102138c4 100644 --- a/src/pages/commits/ProjectHealth.stories.tsx +++ b/src/pages/commits/ProjectHealth.stories.tsx @@ -59,7 +59,6 @@ const RenderCommitsWrapper = ({
{ throw new Error("Commit type not found"); }; -export { RenderCommitsChart, RenderCommitsLabel, getCommitKey, getCommitWidth }; +const isCommitSelected = (commit: Commit, revision: string) => { + const { rolledUpVersions, version } = commit; + + if (!revision) { + return false; + } + + if (version?.revision.startsWith(revision)) { + return true; + } + + if ( + rolledUpVersions && + rolledUpVersions.some((v) => v.revision.startsWith(revision)) + ) { + return true; + } + + return false; +}; + +export { + RenderCommitsChart, + RenderCommitsLabel, + getCommitKey, + getCommitWidth, + isCommitSelected, +}; diff --git a/src/pages/commits/WaterfallMenu/AddNotification.tsx b/src/pages/commits/WaterfallMenu/AddNotification.tsx index 381fe4c87d..1f44e1eb77 100644 --- a/src/pages/commits/WaterfallMenu/AddNotification.tsx +++ b/src/pages/commits/WaterfallMenu/AddNotification.tsx @@ -6,7 +6,13 @@ import { NotificationModal } from "components/Notifications"; import { waterfallTriggers } from "constants/triggers"; import { subscriptionMethods } from "types/subscription"; -export const AddNotification: React.FC = () => { +interface AddNotificationProps { + setMenuOpen: (open: boolean) => void; +} + +export const AddNotification: React.FC = ({ + setMenuOpen, +}) => { const { projectIdentifier } = useParams<{ projectIdentifier: string }>(); const [isModalVisible, setIsModalVisible] = useState(false); const { sendEvent } = useProjectHealthAnalytics({ page: "Commit chart" }); @@ -23,7 +29,10 @@ export const AddNotification: React.FC = () => { setIsModalVisible(false)} + onCancel={() => { + setIsModalVisible(false); + setMenuOpen(false); + }} resourceId={projectIdentifier} sendAnalyticsEvent={(subscription) => sendEvent({ name: "Add Notification", subscription }) diff --git a/src/pages/commits/WaterfallMenu/GitCommitSearch.tsx b/src/pages/commits/WaterfallMenu/GitCommitSearch.tsx new file mode 100644 index 0000000000..51a41d6fcb --- /dev/null +++ b/src/pages/commits/WaterfallMenu/GitCommitSearch.tsx @@ -0,0 +1,76 @@ +import { useState } from "react"; +import styled from "@emotion/styled"; +import TextInput from "@leafygreen-ui/text-input"; +import { Description } from "@leafygreen-ui/typography"; +import { useProjectHealthAnalytics } from "analytics/projectHealth/useProjectHealthAnalytics"; +import { DropdownItem } from "components/ButtonDropdown"; +import { ConfirmationModal } from "components/ConfirmationModal"; +import { size } from "constants/tokens"; +import { useQueryParams } from "hooks/useQueryParam"; +import { MainlineCommitQueryParams } from "types/commits"; + +interface GitCommitSearchProps { + setMenuOpen: (open: boolean) => void; +} + +export const GitCommitSearch: React.FC = ({ + setMenuOpen, +}) => { + const { sendEvent } = useProjectHealthAnalytics({ page: "Commit chart" }); + const [, setQueryParams] = useQueryParams(); + + const [modalOpen, setModalOpen] = useState(false); + const [commitHash, setCommitHash] = useState(""); + + const onCancel = () => { + setModalOpen(false); + setMenuOpen(false); + }; + + const onConfirm = () => { + sendEvent({ name: "Search for commit", commit: commitHash }); + setQueryParams({ + [MainlineCommitQueryParams.Revision]: commitHash, + }); + onCancel(); + }; + + return ( + <> + { + setModalOpen(true); + sendEvent({ name: "Open Git Commit Search Modal" }); + }} + > + Search by Git Hash + + + + Note: This is an experimental feature that works best without any + filters. Searching for a git hash will clear all applied filters. + + setCommitHash(e.target.value.trim())} + onKeyPress={(e) => e.key === "Enter" && onConfirm()} + value={commitHash} + /> + + + ); +}; + +const StyledDescription = styled(Description)` + margin-bottom: ${size.xs}; +`; diff --git a/src/pages/commits/WaterfallMenu/index.tsx b/src/pages/commits/WaterfallMenu/index.tsx index 9cd1d93441..8b31987f31 100644 --- a/src/pages/commits/WaterfallMenu/index.tsx +++ b/src/pages/commits/WaterfallMenu/index.tsx @@ -1,14 +1,23 @@ +import { useState } from "react"; import { ButtonDropdown } from "components/ButtonDropdown"; import { AddNotification } from "./AddNotification"; +import { GitCommitSearch } from "./GitCommitSearch"; export const WaterfallMenu: React.FC = () => { - const dropdownItems = []; + const [menuOpen, setMenuOpen] = useState(false); + + const dropdownItems = [ + , + , + ]; return ( ); }; diff --git a/src/pages/commits/index.tsx b/src/pages/commits/index.tsx index 058891d163..67d3fe2783 100644 --- a/src/pages/commits/index.tsx +++ b/src/pages/commits/index.tsx @@ -107,7 +107,14 @@ const Commits = () => { const skipOrderNumberParam = getString( parsed[MainlineCommitQueryParams.SkipOrderNumber] ); - const skipOrderNumber = parseInt(skipOrderNumberParam, 10) || undefined; + const revisionParam = getString(parsed[MainlineCommitQueryParams.Revision]); + + const parsedSkipNum = parseInt(skipOrderNumberParam, 10); + const skipOrderNumber = Number.isNaN(parsedSkipNum) + ? undefined + : parsedSkipNum; + const revision = revisionParam.length ? revisionParam : undefined; + const filterState = { statuses: statusFilters, variants: variantFilters, @@ -115,22 +122,25 @@ const Commits = () => { requesters: requesterFilters, view: viewFilter || ProjectHealthView.Failed, }; + const variables = getMainlineCommitsQueryVariables({ mainlineCommitOptions: { projectIdentifier, skipOrderNumber, limit, + revision, }, filterState, }); const { hasFilters, hasTasks } = getFilterStatus(filterState); - const { data, error, loading, refetch, startPolling, stopPolling } = useQuery< + const { data, loading, refetch, startPolling, stopPolling } = useQuery< MainlineCommitsQuery, MainlineCommitsQueryVariables >(MAINLINE_COMMITS, { skip: !projectIdentifier || isResizing, + errorPolicy: "all", fetchPolicy: "cache-and-network", variables, pollInterval: DEFAULT_POLL_INTERVAL, @@ -220,7 +230,7 @@ const Commits = () => {
` +export const CommitWrapper = styled.div<{ + width: number; + selected?: boolean; +}>` + background-color: ${({ selected }) => + selected ? blue.light3 : "transparent"}; width: ${({ width }) => width}px; min-width: ${({ width }) => width * 0.75}px; + align-self: stretch; margin: 0px ${size.xs}; &:first-of-type { diff --git a/src/pages/commits/utils.test.ts b/src/pages/commits/utils.test.ts index 3ab7b13afb..4e185f4790 100644 --- a/src/pages/commits/utils.test.ts +++ b/src/pages/commits/utils.test.ts @@ -80,6 +80,7 @@ describe("getMainlineCommitsQueryVariables", () => { projectIdentifier: "projectIdentifier", limit: 5, skipOrderNumber: 0, + revision: "revision", }, filterState: { statuses: [], @@ -93,6 +94,7 @@ describe("getMainlineCommitsQueryVariables", () => { limit: 5, projectIdentifier: "projectIdentifier", skipOrderNumber: 0, + revision: "revision", shouldCollapse: false, requesters: [], }); @@ -104,6 +106,7 @@ describe("getMainlineCommitsQueryVariables", () => { projectIdentifier: "projectIdentifier", limit: 5, skipOrderNumber: 0, + revision: "revision", }, filterState: { statuses: [], @@ -117,6 +120,7 @@ describe("getMainlineCommitsQueryVariables", () => { limit: 5, projectIdentifier: "projectIdentifier", skipOrderNumber: 0, + revision: "revision", shouldCollapse: true, requesters: [], }); @@ -126,6 +130,7 @@ describe("getMainlineCommitsQueryVariables", () => { projectIdentifier: "projectIdentifier", limit: 5, skipOrderNumber: 0, + revision: "revision", }, filterState: { statuses: [TaskStatus.Succeeded], @@ -139,6 +144,7 @@ describe("getMainlineCommitsQueryVariables", () => { limit: 5, projectIdentifier: "projectIdentifier", skipOrderNumber: 0, + revision: "revision", shouldCollapse: true, requesters: [], }); @@ -148,6 +154,7 @@ describe("getMainlineCommitsQueryVariables", () => { projectIdentifier: "projectIdentifier", limit: 5, skipOrderNumber: 0, + revision: "revision", }, filterState: { statuses: [], @@ -161,6 +168,7 @@ describe("getMainlineCommitsQueryVariables", () => { limit: 5, projectIdentifier: "projectIdentifier", skipOrderNumber: 0, + revision: "revision", shouldCollapse: true, requesters: [], }); @@ -170,6 +178,7 @@ describe("getMainlineCommitsQueryVariables", () => { projectIdentifier: "projectIdentifier", limit: 5, skipOrderNumber: 0, + revision: "revision", }, filterState: { statuses: [], @@ -183,6 +192,7 @@ describe("getMainlineCommitsQueryVariables", () => { limit: 5, projectIdentifier: "projectIdentifier", skipOrderNumber: 0, + revision: "revision", shouldCollapse: true, requesters: [], }); @@ -196,6 +206,7 @@ describe("getMainlineCommitsQueryVariables", () => { projectIdentifier: "projectIdentifier", limit: 5, skipOrderNumber: 0, + revision: "", }, filterState: { statuses: [], @@ -217,6 +228,7 @@ describe("getMainlineCommitsQueryVariables", () => { projectIdentifier: "projectIdentifier", limit: 5, skipOrderNumber: 0, + revision: "", }, filterState: { statuses: [TaskStatus.Failed], @@ -238,6 +250,7 @@ describe("getMainlineCommitsQueryVariables", () => { projectIdentifier: "projectIdentifier", limit: 5, skipOrderNumber: 0, + revision: "", }, filterState: { statuses: [], @@ -263,6 +276,7 @@ describe("getMainlineCommitsQueryVariables", () => { projectIdentifier: "projectIdentifier", limit: 5, skipOrderNumber: 0, + revision: "", }, filterState: { statuses: [], @@ -285,6 +299,7 @@ describe("getMainlineCommitsQueryVariables", () => { projectIdentifier: "projectIdentifier", limit: 5, skipOrderNumber: 0, + revision: "", }, filterState: { statuses: [TaskStatus.Failed], @@ -305,6 +320,7 @@ describe("getMainlineCommitsQueryVariables", () => { projectIdentifier: "projectIdentifier", limit: 5, skipOrderNumber: 0, + revision: "", }, filterState: { statuses: [], @@ -325,6 +341,7 @@ describe("getMainlineCommitsQueryVariables", () => { projectIdentifier: "projectIdentifier", limit: 5, skipOrderNumber: 0, + revision: "", }, filterState: { statuses: [], @@ -349,6 +366,7 @@ describe("getMainlineCommitsQueryVariables", () => { projectIdentifier: "projectIdentifier", limit: 5, skipOrderNumber: 0, + revision: "", }, filterState: { statuses: [], @@ -373,6 +391,7 @@ describe("getMainlineCommitsQueryVariables", () => { projectIdentifier: "projectIdentifier", limit: 5, skipOrderNumber: 0, + revision: "", }, filterState: { statuses: [], @@ -397,6 +416,7 @@ describe("getMainlineCommitsQueryVariables", () => { projectIdentifier: "projectIdentifier", limit: 5, skipOrderNumber: 0, + revision: "", }, filterState: { statuses: [TaskStatus.Succeeded], @@ -421,6 +441,7 @@ describe("getMainlineCommitsQueryVariables", () => { projectIdentifier: "projectIdentifier", limit: 5, skipOrderNumber: 0, + revision: "", }, filterState: { statuses: [TaskStatus.Succeeded], @@ -445,6 +466,7 @@ describe("getMainlineCommitsQueryVariables", () => { projectIdentifier: "projectIdentifier", limit: 5, skipOrderNumber: 0, + revision: "", }, filterState: { statuses: [TaskStatus.Failed, TaskStatus.Succeeded], @@ -468,6 +490,7 @@ describe("getMainlineCommitsQueryVariables", () => { projectIdentifier: "projectIdentifier", limit: 5, skipOrderNumber: 0, + revision: "", }, filterState: { statuses: [], @@ -489,6 +512,7 @@ describe("getMainlineCommitsQueryVariables", () => { projectIdentifier: "projectIdentifier", limit: 5, skipOrderNumber: 0, + revision: "", }, filterState: { statuses: [TaskStatus.Succeeded], @@ -512,6 +536,7 @@ describe("getMainlineCommitsQueryVariables", () => { projectIdentifier: "projectIdentifier", limit: 5, skipOrderNumber: 0, + revision: "", }, filterState: { statuses: [], @@ -538,6 +563,7 @@ describe("getMainlineCommitsQueryVariables", () => { projectIdentifier: "projectIdentifier", limit: 5, skipOrderNumber: 0, + revision: "", }, filterState: { statuses: [], @@ -561,6 +587,7 @@ describe("getMainlineCommitsQueryVariables", () => { projectIdentifier: "projectIdentifier", limit: 5, skipOrderNumber: 0, + revision: "", }, filterState: { statuses: [], @@ -584,6 +611,7 @@ describe("getMainlineCommitsQueryVariables", () => { projectIdentifier: "projectIdentifier", limit: 5, skipOrderNumber: 0, + revision: "", }, filterState: { statuses: [TaskStatus.Succeeded], @@ -607,6 +635,7 @@ describe("getMainlineCommitsQueryVariables", () => { projectIdentifier: "projectIdentifier", limit: 5, skipOrderNumber: 0, + revision: "", }, filterState: { statuses: [TaskStatus.Succeeded], @@ -630,6 +659,7 @@ describe("getMainlineCommitsQueryVariables", () => { projectIdentifier: "projectIdentifier", limit: 5, skipOrderNumber: 0, + revision: "", }, filterState: { statuses: [TaskStatus.Succeeded, TaskStatus.Failed], @@ -652,6 +682,7 @@ describe("getMainlineCommitsQueryVariables", () => { projectIdentifier: "projectIdentifier", limit: 5, skipOrderNumber: 0, + revision: "", }, filterState: { statuses: [TaskStatus.Failed], @@ -674,6 +705,7 @@ describe("getMainlineCommitsQueryVariables", () => { projectIdentifier: "projectIdentifier", limit: 5, skipOrderNumber: 0, + revision: "", }, filterState: { statuses: [], @@ -696,6 +728,7 @@ describe("getMainlineCommitsQueryVariables", () => { projectIdentifier: "projectIdentifier", limit: 5, skipOrderNumber: 0, + revision: "", }, filterState: { statuses: [], @@ -716,6 +749,7 @@ describe("getMainlineCommitsQueryVariables", () => { projectIdentifier: "projectIdentifier", limit: 5, skipOrderNumber: 0, + revision: "", }, filterState: { statuses: [TaskStatus.Failed], diff --git a/src/pages/commits/utils.ts b/src/pages/commits/utils.ts index a55375856d..ca761c12ab 100644 --- a/src/pages/commits/utils.ts +++ b/src/pages/commits/utils.ts @@ -31,6 +31,7 @@ interface MainlineCommitOptions { projectIdentifier: string; limit: number; skipOrderNumber: number; + revision: string; } interface CommitsPageReducerState { filterState: FilterState; diff --git a/src/types/commits.ts b/src/types/commits.ts index 34d901dc7c..fd76f49427 100644 --- a/src/types/commits.ts +++ b/src/types/commits.ts @@ -17,6 +17,7 @@ export enum ChartToggleQueryParams { export enum MainlineCommitQueryParams { Requester = "requester", SkipOrderNumber = "skipOrderNumber", + Revision = "revision", } export enum ChartTypes {