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 5232561f9a..9ccf171170 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/Buttons/LoadingButton.stories.tsx b/src/components/Buttons/LoadingButton.stories.tsx new file mode 100644 index 0000000000..f2252e1fde --- /dev/null +++ b/src/components/Buttons/LoadingButton.stories.tsx @@ -0,0 +1,21 @@ +import { Variant } from "@leafygreen-ui/button"; +import { CustomStoryObj, CustomMeta } from "test_utils/types"; +import { LoadingButton } from "."; + +export default { + component: LoadingButton, +} satisfies CustomMeta; + +export const Default: CustomStoryObj = { + render: (args) => Button text, + args: { + loading: false, + variant: Variant.Default, + }, + argTypes: { + variant: { + options: Object.values(Variant), + control: { type: "select" }, + }, + }, +}; diff --git a/src/components/Buttons/LoadingButton.tsx b/src/components/Buttons/LoadingButton.tsx index ed203e1a55..e4443df704 100644 --- a/src/components/Buttons/LoadingButton.tsx +++ b/src/components/Buttons/LoadingButton.tsx @@ -1,19 +1,20 @@ import { forwardRef } from "react"; import { ExtendableBox } from "@leafygreen-ui/box"; import LeafyGreenButton, { ButtonProps } from "@leafygreen-ui/button"; -import Icon from "components/Icon"; +import { Spinner } from "@leafygreen-ui/loading-indicator"; -type Props = ButtonProps & { +type Props = Omit & { loading?: boolean; }; export const LoadingButton: ExtendableBox< Props & { ref?: React.Ref }, "button" -> = forwardRef(({ leftGlyph, loading = false, ...rest }: Props, ref) => ( +> = forwardRef(({ loading = false, ...rest }: Props, ref) => ( : leftGlyph} + isLoading={loading} + loadingIndicator={} {...rest} /> )); 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/DisplayModal.tsx b/src/components/DisplayModal.tsx index b82138fde3..bbd9960cd3 100644 --- a/src/components/DisplayModal.tsx +++ b/src/components/DisplayModal.tsx @@ -1,30 +1,20 @@ import styled from "@emotion/styled"; -import Modal, { ModalSize } from "@leafygreen-ui/modal"; +import Modal, { ModalProps } from "@leafygreen-ui/modal"; import { Body, BodyProps, H3 } from "@leafygreen-ui/typography"; import { size as tokenSize, zIndex } from "constants/tokens"; -export interface DisplayModalProps { - "data-cy"?: string; - open?: boolean; - setOpen?: ( - open: boolean - ) => void | React.Dispatch>; - size?: ModalSize; +type DisplayModalProps = Omit & { title?: React.ReactNode | string; - children: React.ReactNode; subtitle?: string; -} +}; export const DisplayModal: React.FC = ({ children, - "data-cy": dataCy, - open, - setOpen, - size, subtitle, title, + ...rest }) => ( - + {title &&

{title}

} {subtitle && ( {subtitle} 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/components/Settings/EventLog/EventLog.tsx b/src/components/Settings/EventLog/EventLog.tsx index 4ab57ae962..bac0e2cc5b 100644 --- a/src/components/Settings/EventLog/EventLog.tsx +++ b/src/components/Settings/EventLog/EventLog.tsx @@ -1,8 +1,7 @@ import styled from "@emotion/styled"; -import Button from "@leafygreen-ui/button"; import Card from "@leafygreen-ui/card"; -import { Spinner } from "@leafygreen-ui/loading-indicator"; import { Subtitle } from "@leafygreen-ui/typography"; +import { LoadingButton } from "components/Buttons"; import { size } from "constants/tokens"; import { EventDiffTable } from "./EventDiffTable"; import { Header } from "./Header"; @@ -42,15 +41,14 @@ export const EventLog: React.FC = ({ ); })} {!allEventsFetched && !!events.length && ( - + )} {allEventsFetched && {allEventsFetchedCopy}} diff --git a/src/components/Table/BaseTable.tsx b/src/components/Table/BaseTable.tsx index e18f0805c6..dd284caa4e 100644 --- a/src/components/Table/BaseTable.tsx +++ b/src/components/Table/BaseTable.tsx @@ -1,3 +1,4 @@ +import { css } from "@leafygreen-ui/emotion"; import { Cell, ExpandedContent, @@ -43,7 +44,16 @@ export const BaseTable = ({ {table.getRowModel().rows.map((row) => ( - + div { + max-height: unset; + } + `} + > {row.getVisibleCells().map((cell) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} 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/configurePatch/configurePatchCore/index.tsx b/src/pages/configurePatch/configurePatchCore/index.tsx index fe6cee86dd..57c80bfc78 100644 --- a/src/pages/configurePatch/configurePatchCore/index.tsx +++ b/src/pages/configurePatch/configurePatchCore/index.tsx @@ -2,10 +2,10 @@ import { useMemo } from "react"; import { useMutation } from "@apollo/client"; import styled from "@emotion/styled"; import Button from "@leafygreen-ui/button"; -import { Spinner } from "@leafygreen-ui/loading-indicator"; import { Tab } from "@leafygreen-ui/tabs"; import TextInput from "@leafygreen-ui/text-input"; import { useNavigate } from "react-router-dom"; +import { LoadingButton } from "components/Buttons"; import { CodeChanges } from "components/CodeChanges"; import { MetadataCard, @@ -153,28 +153,29 @@ const ConfigurePatchCore: React.FC = ({ patch }) => { value={description} onChange={(e) => setDescription(e.target.value)} /> - {activated && ( - - window.history.state.idx > 0 - ? navigate(-1) - : navigate(getVersionRoute(id)) - } + + {activated && ( + + )} + - Cancel - - )} - } - > - Schedule - + Schedule + + @@ -315,8 +316,10 @@ const StyledInput = styled(TextInput)` width: 100%; `; -const StyledButton = styled(Button)` +const ButtonWrapper = styled.div` margin-top: ${size.m}; + display: flex; + gap: ${size.s}; `; const FlexRow = styled.div` 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 ( + disabled={!canEditDistro} {...rest} state={state} tab={tab} diff --git a/src/pages/host/Metadata.tsx b/src/pages/host/Metadata.tsx index 2ddd9ed8fc..f1476ae039 100644 --- a/src/pages/host/Metadata.tsx +++ b/src/pages/host/Metadata.tsx @@ -8,12 +8,10 @@ import { MetadataTitle, } from "components/MetadataCard"; import { StyledLink } from "components/styles"; -import { getTaskRoute } from "constants/routes"; +import { getDistroSettingsRoute, getTaskRoute } from "constants/routes"; import { HostQuery } from "gql/generated/types"; -import { environmentVariables } from "utils"; const { gray } = palette; -const { getUiUrl } = environmentVariables; export const Metadata: React.FC<{ loading: boolean; @@ -35,7 +33,7 @@ export const Metadata: React.FC<{ const { id: runningTaskId, name: runningTaskName } = runningTask ?? {}; const taskLink = getTaskRoute(runningTaskId); - const distroLink = `${getUiUrl()}/distros##${distroId}`; + const distroLink = getDistroSettingsRoute(distroId); return ( 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; diff --git a/src/pages/task/actionButtons/previousCommits/index.tsx b/src/pages/task/actionButtons/previousCommits/index.tsx index 66f5bbc50d..bca43f6812 100644 --- a/src/pages/task/actionButtons/previousCommits/index.tsx +++ b/src/pages/task/actionButtons/previousCommits/index.tsx @@ -1,11 +1,10 @@ import { useReducer, useEffect } from "react"; import { useLazyQuery, useQuery } from "@apollo/client"; import styled from "@emotion/styled"; -import Button from "@leafygreen-ui/button"; -import { Spinner } from "@leafygreen-ui/loading-indicator"; import { Option, Select } from "@leafygreen-ui/select"; import Tooltip from "@leafygreen-ui/tooltip"; import { useTaskAnalytics } from "analytics"; +import { LoadingButton } from "components/Buttons"; import { ConditionalWrapper } from "components/ConditionalWrapper"; import { finishedTaskStatuses } from "constants/task"; import { size } from "constants/tokens"; @@ -201,22 +200,21 @@ export const PreviousCommits: React.FC = ({ taskId }) => { )} > - + ); 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 && ( diff --git a/src/pages/version/ParametersModal/ParametersModal.stories.tsx b/src/pages/version/ParametersModal/ParametersModal.stories.tsx new file mode 100644 index 0000000000..753cad8639 --- /dev/null +++ b/src/pages/version/ParametersModal/ParametersModal.stories.tsx @@ -0,0 +1,20 @@ +import { CustomStoryObj, CustomMeta } from "test_utils/types"; +import { ParametersModal } from "."; + +export default { + component: ParametersModal, +} satisfies CustomMeta; + +export const Default: CustomStoryObj = { + render: (args) => ( + + ), +}; diff --git a/src/pages/version/ParametersModal/ParametersModal.test.tsx b/src/pages/version/ParametersModal/ParametersModal.test.tsx new file mode 100644 index 0000000000..d366b199c1 --- /dev/null +++ b/src/pages/version/ParametersModal/ParametersModal.test.tsx @@ -0,0 +1,30 @@ +import { render, screen, userEvent } from "test_utils"; +import { ParametersModal } from "."; + +const parameters = [ + { key: "Key 1", value: "Value 1" }, + { key: "Key 2", value: "Value 2" }, +]; + +describe("parameters modal", () => { + it("modal is closed by default", () => { + render(); + expect(screen.queryByDataCy("parameters-modal")).toBeNull(); + }); + + it("link does not render if there are no parameters", () => { + render(); + expect(screen.queryByDataCy("parameters-link")).toBeNull(); + }); + + it("can click the link to open the modal and view parameters", async () => { + const user = userEvent.setup(); + render(); + await user.click(screen.getByDataCy("parameters-link")); + await screen.findByDataCy("parameters-modal"); + expect(screen.getByText(parameters[0].key)).toBeInTheDocument(); + expect(screen.getByText(parameters[0].value)).toBeInTheDocument(); + expect(screen.getByText(parameters[1].key)).toBeInTheDocument(); + expect(screen.getByText(parameters[1].value)).toBeInTheDocument(); + }); +}); diff --git a/src/pages/version/ParametersModal.tsx b/src/pages/version/ParametersModal/index.tsx similarity index 56% rename from src/pages/version/ParametersModal.tsx rename to src/pages/version/ParametersModal/index.tsx index 869d920a89..b61223e348 100644 --- a/src/pages/version/ParametersModal.tsx +++ b/src/pages/version/ParametersModal/index.tsx @@ -1,10 +1,10 @@ -import { useState } from "react"; +import { useRef, useState } from "react"; import styled from "@emotion/styled"; -import Badge from "@leafygreen-ui/badge"; +import { useLeafyGreenTable, LGColumnDef } from "@leafygreen-ui/table/new"; import { DisplayModal } from "components/DisplayModal"; import { MetadataItem } from "components/MetadataCard"; import { StyledLink } from "components/styles"; -import { size } from "constants/tokens"; +import { BaseTable } from "components/Table/BaseTable"; import { Parameter } from "gql/generated/types"; interface ParametersProps { @@ -13,6 +13,15 @@ interface ParametersProps { export const ParametersModal: React.FC = ({ parameters }) => { const [showModal, setShowModal] = useState(false); + + const tableContainerRef = useRef(null); + + const table = useLeafyGreenTable({ + containerRef: tableContainerRef, + data: parameters, + columns, + }); + return ( <> {parameters !== undefined && parameters.length > 0 && ( @@ -26,23 +35,32 @@ export const ParametersModal: React.FC = ({ parameters }) => { )} - {parameters?.map((param) => ( - - {param.key}: {param.value} - - ))} + + + ); }; -const StyledBadge = styled(Badge)` - :not(:last-of-type) { - margin-right: ${size.s}; - } +const OverflowContainer = styled.div` + max-height: 600px; + overflow-y: scroll; `; + +const columns: LGColumnDef[] = [ + { + accessorKey: "key", + header: "Key", + }, + { + accessorKey: "value", + header: "Value", + }, +];