From 9b09e0609384d0a039165966bf933c92df5c8a60 Mon Sep 17 00:00:00 2001 From: Mohamed Khelif Date: Wed, 4 Oct 2023 17:23:28 -0400 Subject: [PATCH 1/6] EVG-21018 Catch logout errors (#2082) --- src/context/auth.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) 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) { From f05d428ffd06dc61de1194f55edeaa7701e523dd Mon Sep 17 00:00:00 2001 From: Sophie Stadler Date: Thu, 5 Oct 2023 15:01:09 -0400 Subject: [PATCH 2/6] EVG-21009: Make distro settings page readonly if user lacks edit permissions (#2086) --- .../integration/distroSettings/permissions.ts | 83 +++++++++++++++++++ src/gql/generated/types.ts | 6 +- .../user-distro-settings-permissions.graphql | 1 + .../DeleteDistro/DeleteDistro.test.tsx | 2 + .../NewDistro/NewDistroButton.test.tsx | 2 + src/pages/distroSettings/tabs/BaseTab.tsx | 18 +++- 6 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 cypress/integration/distroSettings/permissions.ts 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/src/gql/generated/types.ts b/src/gql/generated/types.ts index 0ecf455a77..9ead10a0cb 100644 --- a/src/gql/generated/types.ts +++ b/src/gql/generated/types.ts @@ -8471,7 +8471,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/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/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/tabs/BaseTab.tsx b/src/pages/distroSettings/tabs/BaseTab.tsx index 0fe356f323..603c81553d 100644 --- a/src/pages/distroSettings/tabs/BaseTab.tsx +++ b/src/pages/distroSettings/tabs/BaseTab.tsx @@ -1,6 +1,12 @@ +import { useQuery } from "@apollo/client"; import { useParams } from "react-router-dom"; import { Form } from "components/Settings/Form"; import { GetFormSchema, ValidateProps } from "components/SpruceForm"; +import { + UserDistroSettingsPermissionsQuery, + UserDistroSettingsPermissionsQueryVariables, +} from "gql/generated/types"; +import { USER_DISTRO_SETTINGS_PERMISSIONS } from "gql/queries"; import { usePopulateForm, useDistroSettingsContext } from "../Context"; import { FormStateMap, WritableDistroSettingsType } from "./types"; @@ -15,12 +21,22 @@ export const BaseTab = ({ 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 ( + disabled={!canEditDistro} {...rest} state={state} tab={tab} From da300b22b5a647a462b9c8d3ca481f28112341e8 Mon Sep 17 00:00:00 2001 From: Mohamed Khelif Date: Fri, 6 Oct 2023 11:26:18 -0400 Subject: [PATCH 3/6] v3.0.152 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b20b644b4a..d32353b8c0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "spruce", - "version": "3.0.151", + "version": "3.0.152", "private": true, "scripts": { "bootstrap-logkeeper": "./scripts/bootstrap-logkeeper.sh", From 31a3c7330e107f2545b50be7a5b7360dbcae5f5f Mon Sep 17 00:00:00 2001 From: Sophie Stadler Date: Mon, 9 Oct 2023 12:40:43 -0400 Subject: [PATCH 4/6] EVG-19955: Release distro settings (#2091) --- cypress/integration/nav_bar.ts | 16 +++++++++------- src/components/Content/index.tsx | 2 +- src/components/Header/AuxiliaryDropdown.tsx | 14 ++++++-------- src/constants/externalResources.ts | 3 --- src/constants/routes.ts | 4 ++-- src/gql/generated/types.ts | 2 ++ src/hooks/index.ts | 1 + src/hooks/useFirstDistro.ts | 18 ++++++++++++++++++ src/hooks/useLegacyUIURL.ts | 4 +++- .../DistroSelect/DistroSelect.test.tsx | 2 +- src/pages/distroSettings/NavigationModal.tsx | 3 ++- .../NewDistro/CopyModal.test.tsx | 2 +- .../NewDistro/CreateModal.test.tsx | 2 +- src/pages/distroSettings/index.tsx | 9 --------- src/pages/host/Metadata.tsx | 6 ++---- src/pages/task/metadata/index.tsx | 8 ++++---- 16 files changed, 53 insertions(+), 43 deletions(-) create mode 100644 src/hooks/useFirstDistro.ts 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/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/gql/generated/types.ts b/src/gql/generated/types.ts index 9ead10a0cb..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"]; 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/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/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 ( diff --git a/src/pages/task/metadata/index.tsx b/src/pages/task/metadata/index.tsx index 4feafaf3dd..4e0975d94a 100644 --- a/src/pages/task/metadata/index.tsx +++ b/src/pages/task/metadata/index.tsx @@ -16,11 +16,11 @@ import { import { StyledLink, StyledRouterLink } from "components/styles"; import { SEEN_HONEYCOMB_GUIDE_CUE } from "constants/cookies"; import { - getDistroPageUrl, getHoneycombTraceUrl, getHoneycombSystemMetricsUrl, } from "constants/externalResources"; import { + getDistroSettingsRoute, getTaskQueueRoute, getTaskRoute, getHostRoute, @@ -238,15 +238,15 @@ export const Metadata: React.FC = ({ error, loading, task, taskId }) => { {!isContainerTask && distroId && ( Distro:{" "} - taskAnalytics.sendEvent({ name: "Click Distro Link" }) } + to={getDistroSettingsRoute(distroId)} > {distroId} - + )} {ami && ( From 5fef04a2b6cc64c3f664e42836a7c3e187b0c058 Mon Sep 17 00:00:00 2001 From: Sophie Stadler Date: Mon, 9 Oct 2023 13:05:12 -0400 Subject: [PATCH 5/6] v3.0.153 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d32353b8c0..cc411d73e6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "spruce", - "version": "3.0.152", + "version": "3.0.153", "private": true, "scripts": { "bootstrap-logkeeper": "./scripts/bootstrap-logkeeper.sh", From f4e27db5db3a89233c9e0b18617646baa4d84d86 Mon Sep 17 00:00:00 2001 From: Sophie Stadler Date: Mon, 9 Oct 2023 16:53:43 -0400 Subject: [PATCH 6/6] EVG-19976: Linkify JIRA tickets in user subscriptions table (#2089) --- .../notificationTab/UserSubscriptions.tsx | 147 ++++++++++-------- 1 file changed, 82 insertions(+), 65 deletions(-) diff --git a/src/pages/preferences/preferencesTabs/notificationTab/UserSubscriptions.tsx b/src/pages/preferences/preferencesTabs/notificationTab/UserSubscriptions.tsx index a5db552319..6281c337ec 100644 --- a/src/pages/preferences/preferencesTabs/notificationTab/UserSubscriptions.tsx +++ b/src/pages/preferences/preferencesTabs/notificationTab/UserSubscriptions.tsx @@ -1,4 +1,4 @@ -import { useRef, useState } from "react"; +import { useMemo, useRef, useState } from "react"; import { useMutation } from "@apollo/client"; import styled from "@emotion/styled"; import Button from "@leafygreen-ui/button"; @@ -30,7 +30,12 @@ import { Selector, } from "gql/generated/types"; import { DELETE_SUBSCRIPTIONS } from "gql/mutations"; -import { notificationMethodToCopy } from "types/subscription"; +import { useSpruceConfig } from "hooks"; +import { + NotificationMethods, + notificationMethodToCopy, +} from "types/subscription"; +import { jiraLinkify } from "utils/string/jiraLinkify"; import { ClearSubscriptions } from "./ClearSubscriptions"; import { getResourceRoute, useSubscriptionData } from "./utils"; @@ -38,6 +43,8 @@ const { gray } = palette; export const UserSubscriptions: React.FC<{}> = () => { const dispatchToast = useToastContext(); + const spruceConfig = useSpruceConfig(); + const jiraHost = spruceConfig?.jira?.host; const [deleteSubscriptions] = useMutation< DeleteSubscriptionsMutation, @@ -64,6 +71,79 @@ export const UserSubscriptions: React.FC<{}> = () => { const [rowSelection, setRowSelection] = useState({}); const tableContainerRef = useRef(null); + + const columns = useMemo( + () => [ + { + accessorKey: "resourceType", + cell: ({ getValue }) => { + const resourceType = getValue(); + return resourceTypeToCopy[resourceType] ?? resourceType; + }, + ...getColumnTreeSelectFilterProps({ + "data-cy": "status-filter-popover", + tData: resourceTypeTreeData, + title: "Type", + }), + }, + { + header: "ID", + accessorKey: "selectors", + cell: ({ + getValue, + row: { + original: { resourceType }, + }, + }) => { + const selectors = getValue(); + const resourceSelector = selectors.find( + (s: Selector) => s.type !== "object" && s.type !== "requester" + ); + const { data: selectorId } = resourceSelector ?? {}; + const route = getResourceRoute(resourceType, resourceSelector); + + return route ? ( + {selectorId} + ) : ( + selectorId + ); + }, + }, + { + accessorKey: "trigger", + ...getColumnTreeSelectFilterProps({ + "data-cy": "trigger-filter-popover", + tData: triggerTreeData, + title: "Event", + }), + cell: ({ getValue }) => { + const trigger = getValue(); + return triggerToCopy[trigger] ?? trigger; + }, + }, + { + header: "Notify by", + accessorKey: "subscriber.type", + cell: ({ getValue }) => { + const subscriberType = getValue(); + return notificationMethodToCopy[subscriberType] ?? subscriberType; + }, + }, + { + header: "Target", + accessorKey: "subscriber", + cell: ({ getValue }) => { + const subscriber = getValue(); + const text = getSubscriberText(subscriber); + return subscriber.type === NotificationMethods.JIRA_COMMENT + ? jiraLinkify(text, jiraHost) + : text; + }, + }, + ], + [jiraHost] + ); + const table = useLeafyGreenTable({ columns, containerRef: tableContainerRef, @@ -142,69 +222,6 @@ export const UserSubscriptions: React.FC<{}> = () => { ); }; -const columns = [ - { - accessorKey: "resourceType", - cell: ({ getValue }) => { - const resourceType = getValue(); - return resourceTypeToCopy?.[resourceType] ?? resourceType; - }, - ...getColumnTreeSelectFilterProps({ - "data-cy": "status-filter-popover", - tData: resourceTypeTreeData, - title: "Type", - }), - }, - { - header: "ID", - accessorKey: "selectors", - cell: ({ - getValue, - row: { - original: { resourceType }, - }, - }) => { - const selectors = getValue(); - const resourceSelector = selectors.find( - (s: Selector) => s.type !== "object" && s.type !== "requester" - ); - const { data: selectorId } = resourceSelector ?? {}; - const route = getResourceRoute(resourceType, resourceSelector); - - return route ? ( - {selectorId} - ) : ( - selectorId - ); - }, - }, - { - accessorKey: "trigger", - ...getColumnTreeSelectFilterProps({ - "data-cy": "trigger-filter-popover", - tData: triggerTreeData, - title: "Event", - }), - cell: ({ getValue }) => { - const trigger = getValue(); - return triggerToCopy?.[trigger] ?? trigger; - }, - }, - { - header: "Notify by", - accessorKey: "subscriber.type", - cell: ({ getValue }) => { - const subscriberType = getValue(); - return notificationMethodToCopy[subscriberType] ?? subscriberType; - }, - }, - { - header: "Target", - accessorKey: "subscriber", - cell: ({ getValue }) => getSubscriberText(getValue()), - }, -]; - const InteractiveWrapper = styled.div` display: flex; justify-content: space-between;