Skip to content
This repository has been archived by the owner on Jul 2, 2024. It is now read-only.

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
DEVPROD-4193: Redirect to project identifier on Project Health page
Browse files Browse the repository at this point in the history
minnakt committed Mar 6, 2024
1 parent e30fd1b commit 1a89e9e
Showing 7 changed files with 211 additions and 9 deletions.
17 changes: 12 additions & 5 deletions src/components/Header/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -16,9 +16,12 @@ import { useAuthStateContext } from "context/Auth";
import { UserQuery, SpruceConfigQuery } from "gql/generated/types";
import { USER, SPRUCE_CONFIG } from "gql/queries";
import { useLegacyUIURL } from "hooks";
import { validators } from "utils";
import { AuxiliaryDropdown } from "./AuxiliaryDropdown";
import { UserDropdown } from "./UserDropdown";

const { validateObjectId } = validators;

const { blue, gray, white } = palette;

export const Navbar: React.FC = () => {
@@ -33,16 +36,20 @@ export const Navbar: React.FC = () => {
const { projectIdentifier: projectFromUrl } = useParams<{
projectIdentifier: string;
}>();
const currProject = Cookies.get(CURRENT_PROJECT);

// Update current project cookie if the project in the URL does not equal the cookie value.
// Update current project cookie if the project in the URL is not an objectId and is not equal
// to the current project.
// This will inform future navigations to the /commits page.
useEffect(() => {
if (projectFromUrl && projectFromUrl !== Cookies.get(CURRENT_PROJECT)) {
if (
projectFromUrl &&
!validateObjectId(projectFromUrl) &&
projectFromUrl !== currProject
) {
Cookies.set(CURRENT_PROJECT, projectFromUrl);
}
}, [projectFromUrl]);

const currProject = projectFromUrl ?? Cookies.get(CURRENT_PROJECT);
}, [currProject, projectFromUrl]);

const { data: configData } = useQuery<SpruceConfigQuery>(SPRUCE_CONFIG, {
skip: currProject !== undefined,
9 changes: 9 additions & 0 deletions src/gql/generated/types.ts
Original file line number Diff line number Diff line change
@@ -7389,6 +7389,15 @@ export type ProjectSettingsQuery = {
};
};

export type ProjectQueryVariables = Exact<{
idOrIdentifier: Scalars["String"]["input"];
}>;

export type ProjectQuery = {
__typename?: "Query";
project: { __typename?: "Project"; id: string; identifier: string };
};

export type ProjectsQueryVariables = Exact<{ [key: string]: never }>;

export type ProjectsQuery = {
2 changes: 2 additions & 0 deletions src/gql/queries/index.ts
Original file line number Diff line number Diff line change
@@ -46,6 +46,7 @@ import PROJECT_EVENT_LOGS from "./project-event-logs.graphql";
import PROJECT_HEALTH_VIEW from "./project-health-view.graphql";
import PROJECT_PATCHES from "./project-patches.graphql";
import PROJECT_SETTINGS from "./project-settings.graphql";
import PROJECT from "./project.graphql";
import PROJECTS from "./projects.graphql";
import MY_PUBLIC_KEYS from "./public-keys.graphql";
import REPO_EVENT_LOGS from "./repo-event-logs.graphql";
@@ -124,6 +125,7 @@ export {
PATCH,
POD_EVENTS,
POD,
PROJECT,
PROJECT_BANNER,
PROJECT_EVENT_LOGS,
PROJECT_HEALTH_VIEW,
6 changes: 6 additions & 0 deletions src/gql/queries/project.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
query Project($idOrIdentifier: String!) {
project(projectIdentifier: $idOrIdentifier) {
id
identifier
}
}
36 changes: 36 additions & 0 deletions src/hooks/useProjectRedirect/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useQuery } from "@apollo/client";
import { useParams, useLocation, useNavigate } from "react-router-dom";
import { ProjectQuery, ProjectQueryVariables } from "gql/generated/types";
import { PROJECT } from "gql/queries";
import { validators } from "utils";

const { validateObjectId } = validators;

/**
* useProjectRedirect will replace the project id with the project identifier in the URL.
* @returns isRedirecting - boolean to indicate if a redirect is in progress
*/
export const useProjectRedirect = () => {
const { projectIdentifier: project } = useParams<{
projectIdentifier: string;
}>();
const navigate = useNavigate();
const location = useLocation();

const needsRedirect = validateObjectId(project);

const { loading } = useQuery<ProjectQuery, ProjectQueryVariables>(PROJECT, {
skip: !needsRedirect,
variables: {
idOrIdentifier: project,
},
onCompleted: (projectData) => {
const { identifier } = projectData.project;
const currentUrl = location.pathname.concat(location.search);
const redirectPathname = currentUrl.replace(project, identifier);
navigate(redirectPathname);
},
});

return { isRedirecting: needsRedirect && loading };
};
137 changes: 137 additions & 0 deletions src/hooks/useProjectRedirect/useProjectRedirect.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { MockedProvider } from "@apollo/client/testing";
import { GraphQLError } from "graphql";
import { MemoryRouter, Routes, Route, useLocation } from "react-router-dom";
import { ProjectQuery, ProjectQueryVariables } from "gql/generated/types";
import { PROJECT } from "gql/queries";
import { renderHook, waitFor } from "test_utils";
import { ApolloMock } from "types/gql";
import { useProjectRedirect } from ".";

const useJointHook = () => {
const { isRedirecting } = useProjectRedirect();
const { pathname, search } = useLocation();
return { isRedirecting, pathname, search };
};

describe("useProjectRedirect", () => {
const ProviderWrapper: React.FC<{
children: React.ReactNode;
location: string;
}> = ({ children, location }) => (
<MockedProvider mocks={[repoMock, projectMock]}>
<MemoryRouter initialEntries={[location]}>
<Routes>
<Route element={children} path="/commits/:projectIdentifier" />
</Routes>
</MemoryRouter>
</MockedProvider>
);

it("should not redirect if URL has project identifier", async () => {
const { result } = renderHook(() => useJointHook(), {
wrapper: ({ children }) =>
ProviderWrapper({ children, location: "/commits/my-project" }),
});
expect(result.current).toMatchObject({
isRedirecting: false,
pathname: "/commits/my-project",
search: "",
});
});

it("should redirect if URL has project ID", async () => {
const { result } = renderHook(() => useJointHook(), {
wrapper: ({ children }) =>
ProviderWrapper({ children, location: `/commits/${projectId}` }),
});
expect(result.current).toMatchObject({
isRedirecting: true,
pathname: "/commits/5f74d99ab2373627c047c5e5",
search: "",
});
await waitFor(() => {
expect(result.current).toMatchObject({
isRedirecting: false,
pathname: "/commits/my-project",
search: "",
});
});
});

it("should preserve query params when redirecting", async () => {
const { result } = renderHook(() => useJointHook(), {
wrapper: ({ children }) =>
ProviderWrapper({
children,
location: `/commits/${projectId}?taskName=thirdparty`,
}),
});
expect(result.current).toMatchObject({
isRedirecting: true,
pathname: "/commits/5f74d99ab2373627c047c5e5",
search: "?taskName=thirdparty",
});
await waitFor(() => {
expect(result.current).toMatchObject({
isRedirecting: false,
pathname: "/commits/my-project",
search: "?taskName=thirdparty",
});
});
});

it("should attempt redirect if URL has repo ID but stop attempting after query", async () => {
const { result } = renderHook(() => useJointHook(), {
wrapper: ({ children }) =>
ProviderWrapper({ children, location: `/commits/${repoId}` }),
});
expect(result.current).toMatchObject({
isRedirecting: true,
pathname: "/commits/5e6bb9e23066155a993e0f1a",
search: "",
});
await waitFor(() => {
expect(result.current).toMatchObject({
isRedirecting: false,
pathname: "/commits/5e6bb9e23066155a993e0f1a",
search: "",
});
});
});
});

const projectId = "5f74d99ab2373627c047c5e5";
const projectMock: ApolloMock<ProjectQuery, ProjectQueryVariables> = {
request: {
query: PROJECT,
variables: {
idOrIdentifier: projectId,
},
},
result: {
data: {
project: {
__typename: "Project",
id: projectId,
identifier: "my-project",
},
},
},
};

const repoId = "5e6bb9e23066155a993e0f1a";
const repoMock: ApolloMock<ProjectQuery, ProjectQueryVariables> = {
request: {
query: PROJECT,
variables: {
idOrIdentifier: repoId,
},
},
result: {
errors: [
new GraphQLError(
`Error finding project by id ${repoId}: 404 (Not Found): project '${repoId}' not found`,
),
],
},
};
13 changes: 9 additions & 4 deletions src/pages/commits/index.tsx
Original file line number Diff line number Diff line change
@@ -36,6 +36,7 @@ import {
useUpsertQueryParams,
useUserSettings,
} from "hooks";
import { useProjectRedirect } from "hooks/useProjectRedirect";
import { useQueryParam } from "hooks/useQueryParam";
import { ProjectFilterOptions, MainlineCommitQueryParams } from "types/commits";
import { array, queryString, validators } from "utils";
@@ -69,16 +70,17 @@ const Commits = () => {
const { projectIdentifier } = useParams<{
projectIdentifier: string;
}>();

usePageTitle(`Project Health | ${projectIdentifier}`);
const { isRedirecting } = useProjectRedirect();

const recentlySelectedProject = Cookies.get(CURRENT_PROJECT);
// Push default project to URL if there isn't a project in
// the URL already and an mci-project-cookie does not exist.
const { data: spruceData } = useQuery<
SpruceConfigQuery,
SpruceConfigQueryVariables
>(SPRUCE_CONFIG, {
skip: !!projectIdentifier || !!recentlySelectedProject,
skip: !!projectIdentifier || !!recentlySelectedProject || isRedirecting,
});

useEffect(() => {
@@ -139,7 +141,7 @@ const Commits = () => {
MainlineCommitsQuery,
MainlineCommitsQueryVariables
>(MAINLINE_COMMITS, {
skip: !projectIdentifier || isResizing,
skip: !projectIdentifier || isRedirecting || isResizing,
errorPolicy: "all",
fetchPolicy: "cache-and-network",
variables,
@@ -233,7 +235,10 @@ const Commits = () => {
versions={versions}
revision={revision}
isLoading={
(loading && !versions) || !projectIdentifier || isResizing
(loading && !versions) ||
!projectIdentifier ||
isRedirecting ||
isResizing
}
hasTaskFilter={hasTasks}
hasFilters={hasFilters}

0 comments on commit 1a89e9e

Please sign in to comment.