diff --git a/cypress/integration/distroSettings/permissions.ts b/cypress/integration/distroSettings/permissions.ts
new file mode 100644
index 0000000000..370c9841d8
--- /dev/null
+++ b/cypress/integration/distroSettings/permissions.ts
@@ -0,0 +1,83 @@
+describe("with various permission levels", () => {
+ it("hides the new distro button when a user cannot create distros", () => {
+ const userData = {
+ data: {
+ user: {
+ userId: "admin",
+ permissions: {
+ canCreateDistro: false,
+ distroPermissions: {
+ admin: true,
+ edit: true,
+ },
+ },
+ },
+ },
+ };
+ cy.overwriteGQL("UserDistroSettingsPermissions", userData);
+ cy.visit("/distro/rhel71-power8-large/settings/general");
+ cy.dataCy("new-distro-button").should("not.exist");
+ cy.dataCy("delete-distro-button").should(
+ "not.have.attr",
+ "aria-disabled",
+ "true"
+ );
+ cy.get("textarea").should("not.be.disabled");
+ });
+
+ it("disables the delete button when user lacks admin permissions", () => {
+ const userData = {
+ data: {
+ user: {
+ userId: "admin",
+ permissions: {
+ canCreateDistro: false,
+ distroPermissions: {
+ admin: false,
+ edit: true,
+ },
+ },
+ },
+ },
+ };
+ cy.overwriteGQL("UserDistroSettingsPermissions", userData);
+ cy.visit("/distro/rhel71-power8-large/settings/general");
+ cy.dataCy("new-distro-button").should("not.exist");
+ cy.dataCy("delete-distro-button").should(
+ "have.attr",
+ "aria-disabled",
+ "true"
+ );
+ cy.get("textarea").should("not.be.disabled");
+ });
+
+ it("disables fields when user lacks edit permissions", () => {
+ const userData = {
+ data: {
+ user: {
+ userId: "admin",
+ permissions: {
+ canCreateDistro: false,
+ distroPermissions: {
+ admin: false,
+ edit: false,
+ },
+ },
+ },
+ },
+ };
+ cy.overwriteGQL("UserDistroSettingsPermissions", userData);
+ cy.visit("/distro/rhel71-power8-large/settings/general");
+ cy.dataCy("new-distro-button").should("not.exist");
+ cy.dataCy("delete-distro-button").should(
+ "have.attr",
+ "aria-disabled",
+ "true"
+ );
+ cy.dataCy("distro-settings-page").within(() => {
+ cy.get("input").should("be.disabled");
+ cy.get("textarea").should("be.disabled");
+ cy.get("button").should("have.attr", "aria-disabled", "true");
+ });
+ });
+});
diff --git a/cypress/integration/nav_bar.ts b/cypress/integration/nav_bar.ts
index b910aa2128..7206c95e0b 100644
--- a/cypress/integration/nav_bar.ts
+++ b/cypress/integration/nav_bar.ts
@@ -43,13 +43,6 @@ describe("Nav Bar", () => {
cy.visit(SPRUCE_URLS.cli);
cy.dataCy("legacy-ui-link").should("not.exist");
});
- it("Nav Dropdown should provide links to legacy pages", () => {
- cy.visit(SPRUCE_URLS.version);
- cy.dataCy("legacy_route").should("not.exist");
- cy.dataCy("auxiliary-dropdown-link").click();
- cy.dataCy("legacy_route").should("exist");
- cy.dataCy("legacy_route").should("have.attr", "href", LEGACY_URLS.distros);
- });
it("Nav Dropdown should link to patches page of most recent project if cookie exists", () => {
cy.setCookie(projectCookie, "spruce");
cy.visit(SPRUCE_URLS.userPatches);
@@ -57,6 +50,15 @@ describe("Nav Bar", () => {
cy.dataCy("auxiliary-dropdown-project-patches").click();
cy.location("pathname").should("eq", "/project/spruce/patches");
});
+ it("Nav Dropdown should link to the first distro returned by the distros resolver", () => {
+ cy.visit(SPRUCE_URLS.version);
+ cy.dataCy("auxiliary-dropdown-link").click();
+ cy.dataCy("auxiliary-dropdown-distro-settings").should(
+ "have.attr",
+ "href",
+ "/distro/localhost/settings/general"
+ );
+ });
it("Nav Dropdown should link to patches page of default project in SpruceConfig if cookie does not exist", () => {
cy.clearCookie(projectCookie);
cy.visit(SPRUCE_URLS.userPatches);
diff --git a/package.json b/package.json
index b20b644b4a..cc411d73e6 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "spruce",
- "version": "3.0.151",
+ "version": "3.0.153",
"private": true,
"scripts": {
"bootstrap-logkeeper": "./scripts/bootstrap-logkeeper.sh",
diff --git a/src/components/Content/index.tsx b/src/components/Content/index.tsx
index 41e03b3520..f672760462 100644
--- a/src/components/Content/index.tsx
+++ b/src/components/Content/index.tsx
@@ -42,7 +42,7 @@ export const Content: React.FC = () => (
}>
- }>
+ }>
} />
diff --git a/src/components/Header/AuxiliaryDropdown.tsx b/src/components/Header/AuxiliaryDropdown.tsx
index 78bb47d1d7..dc71cf8b53 100644
--- a/src/components/Header/AuxiliaryDropdown.tsx
+++ b/src/components/Header/AuxiliaryDropdown.tsx
@@ -1,17 +1,15 @@
import { useNavbarAnalytics } from "analytics";
-import { legacyRoutes } from "constants/externalResources";
import {
routes,
+ getDistroSettingsRoute,
getProjectPatchesRoute,
getProjectSettingsRoute,
getTaskQueueRoute,
getCommitQueueRoute,
} from "constants/routes";
-import { environmentVariables } from "utils";
+import { useFirstDistro } from "hooks";
import { NavDropdown } from "./NavDropdown";
-const { getUiUrl } = environmentVariables;
-
interface AuxiliaryDropdownProps {
projectIdentifier: string;
}
@@ -19,8 +17,8 @@ interface AuxiliaryDropdownProps {
export const AuxiliaryDropdown: React.FC = ({
projectIdentifier,
}) => {
- const uiURL = getUiUrl();
const { sendEvent } = useNavbarAnalytics();
+ const distro = useFirstDistro();
const menuItems = [
{
@@ -39,9 +37,9 @@ export const AuxiliaryDropdown: React.FC = ({
onClick: () => sendEvent({ name: "Click Task Queue Link" }),
},
{
- "data-cy": "legacy_route",
- href: `${uiURL}${legacyRoutes.distros}`,
- text: "Distros",
+ "data-cy": "auxiliary-dropdown-distro-settings",
+ to: getDistroSettingsRoute(distro),
+ text: "Distro Settings",
onClick: () => sendEvent({ name: "Click Distros Link" }),
},
diff --git a/src/constants/externalResources.ts b/src/constants/externalResources.ts
index a98ef74bd3..768bcca167 100644
--- a/src/constants/externalResources.ts
+++ b/src/constants/externalResources.ts
@@ -101,9 +101,6 @@ export const getParsleyTestLogURL = (buildId: string, testId: string) =>
export const getParsleyBuildLogURL = (buildId: string) =>
`${getParsleyUrl()}/resmoke/${buildId}/all`;
-export const getDistroPageUrl = (distroId: string) =>
- `${getUiUrl()}/distros##${distroId}`;
-
export const getHoneycombTraceUrl = (traceId: string, startTs: Date) =>
`${getHoneycombBaseURL()}/datasets/evergreen-agent/trace?trace_id=${traceId}&trace_start_ts=${getUnixTime(
new Date(startTs)
diff --git a/src/constants/routes.ts b/src/constants/routes.ts
index 2a57559109..257538b0b7 100644
--- a/src/constants/routes.ts
+++ b/src/constants/routes.ts
@@ -82,7 +82,7 @@ export const routes = {
commits: paths.commits,
configurePatch: `${paths.patch}/:id/configure`,
container: `${paths.container}/:id`,
- distro: `${paths.distro}/:distroId/${PageNames.Settings}`,
+ distroSettings: `${paths.distro}/:distroId/${PageNames.Settings}`,
host: `${paths.host}/:id`,
hosts: paths.hosts,
jobLogs: `${paths.jobLogs}/:buildId`,
@@ -240,7 +240,7 @@ export const getDistroSettingsRoute = (
) =>
tab
? `${paths.distro}/${distroId}/${PageNames.Settings}/${tab}`
- : `${paths.distro}/${distroId}/${PageNames.Settings}`;
+ : `${paths.distro}/${distroId}/${PageNames.Settings}/${DistroSettingsTabRoutes.General}`;
export const getCommitQueueRoute = (projectIdentifier: string) =>
`${paths.commitQueue}/${encodeURIComponent(projectIdentifier)}`;
diff --git a/src/context/auth.tsx b/src/context/auth.tsx
index 13f4fb4116..8d704d3637 100644
--- a/src/context/auth.tsx
+++ b/src/context/auth.tsx
@@ -60,10 +60,14 @@ const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
credentials: "include",
method: "GET",
redirect: "manual",
- }).then(() => {
- dispatch({ type: "deauthenticated" });
- window.location.href = `${getLoginDomain()}/login`;
- });
+ })
+ .then(() => {
+ dispatch({ type: "deauthenticated" });
+ window.location.href = `${getLoginDomain()}/login`;
+ })
+ .catch((error) => {
+ leaveBreadcrumb("Logout failed", { error }, "user");
+ });
},
dispatchAuthenticated: () => {
if (!state.isAuthenticated) {
diff --git a/src/gql/generated/types.ts b/src/gql/generated/types.ts
index 0ecf455a77..538313c0c0 100644
--- a/src/gql/generated/types.ts
+++ b/src/gql/generated/types.ts
@@ -557,6 +557,7 @@ export type File = {
__typename?: "File";
link: Scalars["String"]["output"];
name: Scalars["String"]["output"];
+ urlParsley?: Maybe;
visibility: Scalars["String"]["output"];
};
@@ -2935,6 +2936,7 @@ export type Version = {
finishTime?: Maybe;
gitTags?: Maybe>;
id: Scalars["String"]["output"];
+ ignored: Scalars["Boolean"]["output"];
isPatch: Scalars["Boolean"]["output"];
manifest?: Maybe;
message: Scalars["String"]["output"];
@@ -8471,7 +8473,11 @@ export type UserDistroSettingsPermissionsQuery = {
permissions: {
__typename?: "Permissions";
canCreateDistro: boolean;
- distroPermissions: { __typename?: "DistroPermissions"; admin: boolean };
+ distroPermissions: {
+ __typename?: "DistroPermissions";
+ admin: boolean;
+ edit: boolean;
+ };
};
};
};
diff --git a/src/gql/queries/user-distro-settings-permissions.graphql b/src/gql/queries/user-distro-settings-permissions.graphql
index 41be96777d..bd16143c6d 100644
--- a/src/gql/queries/user-distro-settings-permissions.graphql
+++ b/src/gql/queries/user-distro-settings-permissions.graphql
@@ -4,6 +4,7 @@ query UserDistroSettingsPermissions($distroId: String!) {
canCreateDistro
distroPermissions(options: { distroId: $distroId }) {
admin
+ edit
}
}
userId
diff --git a/src/hooks/index.ts b/src/hooks/index.ts
index d26fcedbd3..d6a42af7bc 100644
--- a/src/hooks/index.ts
+++ b/src/hooks/index.ts
@@ -22,5 +22,6 @@ export { useSpruceConfig } from "./useSpruceConfig";
export { useUserSettings } from "./useUserSettings";
export { useUserTimeZone } from "./useUserTimeZone";
export { useDateFormat } from "./useDateFormat";
+export { useFirstDistro } from "./useFirstDistro";
export { useBreadcrumbRoot } from "./useBreadcrumbRoot";
export { useResize } from "./useResize";
diff --git a/src/hooks/useFirstDistro.ts b/src/hooks/useFirstDistro.ts
new file mode 100644
index 0000000000..7c09714de3
--- /dev/null
+++ b/src/hooks/useFirstDistro.ts
@@ -0,0 +1,18 @@
+import { useQuery } from "@apollo/client";
+import { DistrosQuery, DistrosQueryVariables } from "gql/generated/types";
+import { DISTROS } from "gql/queries";
+
+/**
+ * `useFirstDistro` returns the alphabetically first distro from Evergreen's list of distros.
+ * This can be used to generate a general link to distro settings.
+ * @returns the distro ID
+ */
+export const useFirstDistro = () => {
+ const { data } = useQuery(DISTROS, {
+ variables: {
+ onlySpawnable: false,
+ },
+ });
+
+ return data?.distros?.[0]?.name ?? "ubuntu2204-large";
+};
diff --git a/src/hooks/useLegacyUIURL.ts b/src/hooks/useLegacyUIURL.ts
index fd20f996c4..f15c3d88b0 100644
--- a/src/hooks/useLegacyUIURL.ts
+++ b/src/hooks/useLegacyUIURL.ts
@@ -25,6 +25,7 @@ export const useLegacyUIURL = (): string | null => {
[routes.spawnHost]: `${uiURL}/spawn#?resourcetype=hosts`,
[routes.spawnVolume]: `${uiURL}/spawn#?resourcetype=volumes`,
[`${routes.commits}/:id`]: `${uiURL}/waterfall/${id}`,
+ [`${routes.distroSettings}/*`]: `${uiURL}/distros##${id}`,
[routes.hosts]: `${uiURL}/hosts`,
[routes.host]: `${uiURL}/host/${id}`,
};
@@ -34,7 +35,8 @@ export const useLegacyUIURL = (): string | null => {
if (matchedPath !== null) {
setId(
get(matchedPath, "params.id", "") ||
- get(matchedPath, "params.identifier", "")
+ get(matchedPath, "params.identifier", "") ||
+ get(matchedPath, "params.distroId", "")
);
setLegacyUIUrl(legacyUIMap[legacyUIKeys[i]]);
break;
diff --git a/src/pages/distroSettings/DeleteDistro/DeleteDistro.test.tsx b/src/pages/distroSettings/DeleteDistro/DeleteDistro.test.tsx
index 0d1299a913..83e6e26de9 100644
--- a/src/pages/distroSettings/DeleteDistro/DeleteDistro.test.tsx
+++ b/src/pages/distroSettings/DeleteDistro/DeleteDistro.test.tsx
@@ -131,6 +131,7 @@ const isAdminMock: ApolloMock<
distroPermissions: {
__typename: "DistroPermissions",
admin: true,
+ edit: true,
},
},
},
@@ -159,6 +160,7 @@ const notAdminMock: ApolloMock<
distroPermissions: {
__typename: "DistroPermissions",
admin: false,
+ edit: false,
},
},
},
diff --git a/src/pages/distroSettings/DistroSelect/DistroSelect.test.tsx b/src/pages/distroSettings/DistroSelect/DistroSelect.test.tsx
index 38c956106c..a5b21e6a9d 100644
--- a/src/pages/distroSettings/DistroSelect/DistroSelect.test.tsx
+++ b/src/pages/distroSettings/DistroSelect/DistroSelect.test.tsx
@@ -40,7 +40,7 @@ describe("distro select", () => {
expect(screen.getByDataCy("distro-select-options")).toBeInTheDocument();
await user.click(screen.getByText("abc"));
expect(screen.queryByDataCy("distro-select-options")).toBeNull();
- expect(router.state.location.pathname).toBe("/distro/abc/settings");
+ expect(router.state.location.pathname).toBe("/distro/abc/settings/general");
});
it("typing in the text input will narrow down search results", async () => {
diff --git a/src/pages/distroSettings/NavigationModal.tsx b/src/pages/distroSettings/NavigationModal.tsx
index 970d50ee23..af7b4b0500 100644
--- a/src/pages/distroSettings/NavigationModal.tsx
+++ b/src/pages/distroSettings/NavigationModal.tsx
@@ -10,7 +10,8 @@ export const NavigationModal: React.FC = () => {
const shouldConfirmNavigation = ({ nextLocation }): boolean => {
const isDistroSettingsRoute =
- nextLocation && !!matchPath(`${routes.distro}/*`, nextLocation.pathname);
+ nextLocation &&
+ !!matchPath(`${routes.distroSettings}/*`, nextLocation.pathname);
if (!isDistroSettingsRoute) {
return hasUnsaved;
}
diff --git a/src/pages/distroSettings/NewDistro/CopyModal.test.tsx b/src/pages/distroSettings/NewDistro/CopyModal.test.tsx
index b1cb040770..7494740b67 100644
--- a/src/pages/distroSettings/NewDistro/CopyModal.test.tsx
+++ b/src/pages/distroSettings/NewDistro/CopyModal.test.tsx
@@ -71,7 +71,7 @@ describe("copy distro modal", () => {
await waitFor(() => expect(dispatchToast.warning).toHaveBeenCalledTimes(0));
await waitFor(() => expect(dispatchToast.error).toHaveBeenCalledTimes(0));
expect(router.state.location.pathname).toBe(
- `/distro/${newDistroId}/settings`
+ `/distro/${newDistroId}/settings/general`
);
});
diff --git a/src/pages/distroSettings/NewDistro/CreateModal.test.tsx b/src/pages/distroSettings/NewDistro/CreateModal.test.tsx
index 13cb8632e1..e34d0c80ea 100644
--- a/src/pages/distroSettings/NewDistro/CreateModal.test.tsx
+++ b/src/pages/distroSettings/NewDistro/CreateModal.test.tsx
@@ -61,7 +61,7 @@ describe("create distro modal", () => {
await waitFor(() => expect(dispatchToast.warning).toHaveBeenCalledTimes(0));
await waitFor(() => expect(dispatchToast.error).toHaveBeenCalledTimes(0));
expect(router.state.location.pathname).toBe(
- `/distro/${newDistroId}/settings`
+ `/distro/${newDistroId}/settings/general`
);
});
diff --git a/src/pages/distroSettings/NewDistro/NewDistroButton.test.tsx b/src/pages/distroSettings/NewDistro/NewDistroButton.test.tsx
index 1ca1c5ca9f..4e42215787 100644
--- a/src/pages/distroSettings/NewDistro/NewDistroButton.test.tsx
+++ b/src/pages/distroSettings/NewDistro/NewDistroButton.test.tsx
@@ -41,6 +41,7 @@ describe("new distro button", () => {
distroPermissions: {
__typename: "DistroPermissions",
admin: false,
+ edit: false,
},
},
},
@@ -131,6 +132,7 @@ const hasPermissionsMock: ApolloMock<
distroPermissions: {
__typename: "DistroPermissions",
admin: true,
+ edit: true,
},
},
},
diff --git a/src/pages/distroSettings/index.tsx b/src/pages/distroSettings/index.tsx
index 34874a3eb8..e4a96fa975 100644
--- a/src/pages/distroSettings/index.tsx
+++ b/src/pages/distroSettings/index.tsx
@@ -17,7 +17,6 @@ import { useToastContext } from "context/toast";
import { DistroQuery, DistroQueryVariables } from "gql/generated/types";
import { DISTRO } from "gql/queries";
import { usePageTitle } from "hooks";
-import { isProduction } from "utils/environmentVariables";
import { DistroSettingsProvider } from "./Context";
import { DistroSelect } from "./DistroSelect";
import { getTabTitle } from "./getTabTitle";
@@ -44,14 +43,6 @@ const DistroSettings: React.FC = () => {
}
);
- if (isProduction()) {
- return (
-
- Coming Soon 🌱⚙️
-
- );
- }
-
if (!Object.values(DistroSettingsTabRoutes).includes(currentTab)) {
return (
({
initialFormState,
...rest
}: BaseTabProps) => {
- const { tab } = useParams<{ tab: T }>();
+ const { distroId, tab } = useParams<{ distroId: string; tab: T }>();
const state = useDistroSettingsContext();
usePopulateForm(initialFormState, tab);
+ const { data } = useQuery<
+ UserDistroSettingsPermissionsQuery,
+ UserDistroSettingsPermissionsQueryVariables
+ >(USER_DISTRO_SETTINGS_PERMISSIONS, {
+ variables: { distroId },
+ });
+ const canEditDistro =
+ data?.user?.permissions?.distroPermissions?.edit ?? false;
+
return (