From 7550fd69faeaf41633690fc92871a6734c49b3fd Mon Sep 17 00:00:00 2001 From: Jacob Shandling <61553566+jacobshandling@users.noreply.github.com> Date: Thu, 25 Jan 2024 18:19:49 +0000 Subject: [PATCH] =?UTF-8?q?UI=20=E2=80=93=20Team-level=20host=20expiry=20s?= =?UTF-8?q?etting=20(#16276)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## ➡️ #15965 Without global setting: ![Screenshot-2024-01-24-at-12035PM(1)](https://github.com/fleetdm/fleet/assets/61553566/a98007a4-21b2-4f55-84e3-f58cf529af43) With global setting: ![Screenshot-2024-01-24-at-12925PM(1)](https://github.com/fleetdm/fleet/assets/61553566/e6d20038-d2c2-4f75-a82e-3d4c0c8cb1fd) - [x] Changes file added for user-visible changes in `changes/` - [x] Added/updated tests - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Jacob Shandling Co-authored-by: Sharon Katz <121527325+sharon-fdm@users.noreply.github.com> --- changes/15965-team-host-expiry-frontend | 1 + frontend/docs/patterns.md | 27 ++- frontend/interfaces/team.ts | 4 + frontend/interfaces/team_subnav.ts | 11 + .../AgentOptionsPage/AgentOptionsPage.tsx | 14 +- .../AgentOptionsPage/_styles.scss | 1 - .../MembersPage/MembersPage.tsx | 15 +- .../MembersPage/_styles.scss | 1 - .../TeamDetailsWrapper/TeamDetailsWrapper.tsx | 4 + .../TeamSettings/TeamSettings.tsx | 223 ++++++++++++++++++ .../TeamSettings/_styles.scss | 7 + .../TeamHostExpiryToggle.tests.tsx | 57 +++++ .../TeamHostExpiryToggle.tsx | 85 +++++++ .../TeamHostExpiryToggle/_styles.scss | 13 + .../components/TeamHostExpiryToggle/index.ts | 1 + .../TeamDetailsWrapper/TeamSettings/index.ts | 1 + .../TeamDetailsWrapper/_styles.scss | 4 + .../admin/components/SideNav/_styles.scss | 2 +- frontend/router/index.tsx | 2 + frontend/router/paths.ts | 6 + frontend/services/entities/teams.ts | 15 +- frontend/styles/var/_global.scss | 3 + frontend/styles/var/forms.scss | 1 - 23 files changed, 459 insertions(+), 39 deletions(-) create mode 100644 changes/15965-team-host-expiry-frontend create mode 100644 frontend/interfaces/team_subnav.ts create mode 100644 frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamSettings/TeamSettings.tsx create mode 100644 frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamSettings/_styles.scss create mode 100644 frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamSettings/components/TeamHostExpiryToggle/TeamHostExpiryToggle.tests.tsx create mode 100644 frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamSettings/components/TeamHostExpiryToggle/TeamHostExpiryToggle.tsx create mode 100644 frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamSettings/components/TeamHostExpiryToggle/_styles.scss create mode 100644 frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamSettings/components/TeamHostExpiryToggle/index.ts create mode 100644 frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamSettings/index.ts delete mode 100644 frontend/styles/var/forms.scss diff --git a/changes/15965-team-host-expiry-frontend b/changes/15965-team-host-expiry-frontend new file mode 100644 index 000000000..c764fd1b8 --- /dev/null +++ b/changes/15965-team-host-expiry-frontend @@ -0,0 +1 @@ +* Implement the UI for the new team-level host expiry setting feature. diff --git a/frontend/docs/patterns.md b/frontend/docs/patterns.md index bcaa3cf3c..0ba21794d 100644 --- a/frontend/docs/patterns.md +++ b/frontend/docs/patterns.md @@ -325,16 +325,6 @@ Below are a few need-to-knows about what's available in Fleet's CSS: 1) When creating a form, **not** in a modal, use the class `${baseClass}__button-wrap` for the action buttons (cancel, save, delete, etc.) and proceed to style as needed. -## Other - -### Local states - -Our first line of defense for state management is local states (i.e. `useState`). We -use local states to keep pages/components separate from one another and easy to -maintain. If states need to be passed to direct children, then prop-drilling should -suffice as long as we do not go more than two levels deep. Otherwise, if states need -to be used across multiple unrelated components or 3+ levels from a parent, -then the [app's context](#react-context) should be used. ## Icons and Images @@ -364,3 +354,20 @@ The icon should now be available to use with the `Icon` component from the given The recommend line limit per page/component is 500 lines. This is only a recommendation. Larger files are to be split into multiple files if possible. + +## Other + +### Local states + +Our first line of defense for state management is local states (i.e. `useState`). We +use local states to keep pages/components separate from one another and easy to +maintain. If states need to be passed to direct children, then prop-drilling should +suffice as long as we do not go more than two levels deep. Otherwise, if states need +to be used across multiple unrelated components or 3+ levels from a parent, +then the [app's context](#react-context) should be used. + +### Reading and updating configs + +If you are dealing with a page that *updates* any kind of config, you'll want to access that config +with a fresh API call to be sure you have the updated values. Otherwise, that is, you are dealing +with a page that is only *reading* config values, get them from context. diff --git a/frontend/interfaces/team.ts b/frontend/interfaces/team.ts index e44c93f5f..e3628af0a 100644 --- a/frontend/interfaces/team.ts +++ b/frontend/interfaces/team.ts @@ -63,6 +63,10 @@ export interface ITeam extends ITeamSummary { grace_period_days: number | null; }; }; + host_expiry_settings?: { + host_expiry_enabled: boolean; + host_expiry_window: number; // days + }; } /** diff --git a/frontend/interfaces/team_subnav.ts b/frontend/interfaces/team_subnav.ts new file mode 100644 index 000000000..09516bd30 --- /dev/null +++ b/frontend/interfaces/team_subnav.ts @@ -0,0 +1,11 @@ +import { InjectedRouter } from "react-router"; + +export interface ITeamSubnavProps { + location: { + pathname: string; + search: string; + hash?: string; + query: { team_id?: string }; + }; + router: InjectedRouter; +} diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/AgentOptionsPage/AgentOptionsPage.tsx b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/AgentOptionsPage/AgentOptionsPage.tsx index 890d742ec..fa59ded99 100644 --- a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/AgentOptionsPage/AgentOptionsPage.tsx +++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/AgentOptionsPage/AgentOptionsPage.tsx @@ -1,7 +1,6 @@ import React, { useContext, useState, useEffect } from "react"; import { useQuery } from "react-query"; import { useErrorHandler } from "react-error-boundary"; -import { InjectedRouter } from "react-router"; import yaml from "js-yaml"; import { constructErrorString, agentOptionsToYaml } from "utilities/yaml"; import { EMPTY_AGENT_OPTIONS } from "utilities/constants"; @@ -21,23 +20,14 @@ import Spinner from "components/Spinner"; import CustomLink from "components/CustomLink"; // @ts-ignore import YamlAce from "components/YamlAce"; +import { ITeamSubnavProps } from "interfaces/team_subnav"; const baseClass = "agent-options"; -interface IAgentOptionsPageProps { - location: { - pathname: string; - search: string; - hash?: string; - query: { team_id?: string }; - }; - router: InjectedRouter; -} - const AgentOptionsPage = ({ location, router, -}: IAgentOptionsPageProps): JSX.Element => { +}: ITeamSubnavProps): JSX.Element => { const { renderFlash } = useContext(NotificationContext); const { isRouteOk, teamIdForApi } = useTeamIdParam({ diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/AgentOptionsPage/_styles.scss b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/AgentOptionsPage/_styles.scss index 7ebf71e69..f967a7043 100644 --- a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/AgentOptionsPage/_styles.scss +++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/AgentOptionsPage/_styles.scss @@ -2,7 +2,6 @@ font-size: $x-small; &__page-description { - padding-top: 40px; padding-bottom: $pad-medium; margin: 0; } diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/MembersPage.tsx b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/MembersPage.tsx index 9fac9bc67..5f052dc12 100644 --- a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/MembersPage.tsx +++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/MembersPage.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useContext, useMemo, useState } from "react"; import { useQuery } from "react-query"; -import { InjectedRouter, Link } from "react-router"; +import { Link } from "react-router"; import { AppContext } from "context/app"; import { NotificationContext } from "context/notification"; @@ -9,6 +9,7 @@ import { IEmptyTableProps } from "interfaces/empty_table"; import { IApiError } from "interfaces/errors"; import { INewMembersBody, ITeam } from "interfaces/team"; import { IUpdateUserFormData, IUser, IUserFormErrors } from "interfaces/user"; +import { ITeamSubnavProps } from "interfaces/team_subnav"; import PATHS from "router/paths"; import usersAPI from "services/entities/users"; import inviteAPI from "services/entities/invites"; @@ -39,17 +40,7 @@ import { const baseClass = "members"; const noMembersClass = "no-members"; -interface IMembersPageProps { - location: { - pathname: string; - search: string; - hash?: string; - query: { team_id?: string }; - }; - router: InjectedRouter; -} - -const MembersPage = ({ location, router }: IMembersPageProps): JSX.Element => { +const MembersPage = ({ location, router }: ITeamSubnavProps): JSX.Element => { const { renderFlash } = useContext(NotificationContext); const { config, currentUser, isGlobalAdmin, isPremiumTier } = useContext( AppContext diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/_styles.scss b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/_styles.scss index 0e66522d5..1b51626b2 100644 --- a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/_styles.scss +++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/_styles.scss @@ -1,7 +1,6 @@ .members { &__page-description { font-size: $x-small; - padding-top: 40px; padding-bottom: $pad-medium; margin: 0; } diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx index 4c928f559..bbf035615 100644 --- a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx +++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx @@ -51,6 +51,10 @@ const teamDetailsSubNav: ITeamDetailsSubNavItem[] = [ name: "Agent options", getPathname: PATHS.TEAM_DETAILS_OPTIONS, }, + { + name: "Settings", + getPathname: PATHS.TEAM_DETAILS_SETTINGS, + }, ]; interface ITeamDetailsPageProps { diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamSettings/TeamSettings.tsx b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamSettings/TeamSettings.tsx new file mode 100644 index 000000000..a2a5372d6 --- /dev/null +++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamSettings/TeamSettings.tsx @@ -0,0 +1,223 @@ +import React, { useCallback, useContext, useEffect, useState } from "react"; + +import { useQuery } from "react-query"; + +import { NotificationContext } from "context/notification"; + +import useTeamIdParam from "hooks/useTeamIdParam"; + +import { IApiError } from "interfaces/errors"; +import { IConfig } from "interfaces/config"; +import { ITeamConfig } from "interfaces/team"; +import { ITeamSubnavProps } from "interfaces/team_subnav"; + +import configAPI from "services/entities/config"; +import teamsAPI, { ILoadTeamResponse } from "services/entities/teams"; + +import Button from "components/buttons/Button"; +import DataError from "components/DataError"; +// @ts-ignore +import InputField from "components/forms/fields/InputField"; +import Spinner from "components/Spinner"; + +import TeamHostExpiryToggle from "./components/TeamHostExpiryToggle"; + +const baseClass = "team-settings"; + +const HOST_EXPIRY_ERROR_TEXT = "Host expiry window must be a positive number."; + +const TeamSettings = ({ location, router }: ITeamSubnavProps) => { + const [ + formDataTeamHostExpiryEnabled, + setFormDataTeamHostExpiryEnabled, + ] = useState(false); // default false until API response + const [ + formDataTeamHostExpiryWindow, + setFormDataTeamHostExpiryWindow, + ] = useState(""); + const [updatingTeamSettings, setUpdatingTeamSettings] = useState(false); + const [formErrors, setFormErrors] = useState>( + {} + ); + + const { renderFlash } = useContext(NotificationContext); + + const { isRouteOk, teamIdForApi } = useTeamIdParam({ + location, + router, + includeAllTeams: false, + includeNoTeam: false, + permittedAccessByTeamRole: { + admin: true, + maintainer: false, + observer: false, + observer_plus: false, + }, + }); + + const { + data: appConfig, + isLoading: isLoadingAppConfig, + error: errorLoadGlobalConfig, + } = useQuery( + ["globalConfig"], + () => configAPI.loadAll(), + { refetchOnWindowFocus: false } + ); + const { + host_expiry_settings: { + host_expiry_enabled: globalHostExpiryEnabled, + host_expiry_window: globalHostExpiryWindow, + }, + } = appConfig ?? { host_expiry_settings: {} }; + + const { + isLoading: isLoadingTeamConfig, + refetch: refetchTeamConfig, + error: errorLoadTeamConfig, + } = useQuery( + ["teamConfig", teamIdForApi], + () => teamsAPI.load(teamIdForApi), + { + enabled: isRouteOk && !!teamIdForApi, + select: (data) => data.team, + onSuccess: (teamConfig) => { + // default this setting to current team setting + // can be updated by user actions + setFormDataTeamHostExpiryEnabled( + teamConfig?.host_expiry_settings?.host_expiry_enabled ?? false + ); + setFormDataTeamHostExpiryWindow( + teamConfig.host_expiry_settings?.host_expiry_window ?? "" + ); + }, + refetchOnWindowFocus: false, + } + ); + + const validate = useCallback(() => { + const errors: Record = {}; + const numHostExpiryWindow = Number(formDataTeamHostExpiryWindow); + if ( + // with no global setting, team window can't be empty if enabled + (!globalHostExpiryEnabled && + formDataTeamHostExpiryEnabled && + !numHostExpiryWindow) || + // if nonempty, must be a positive number + isNaN(numHostExpiryWindow) || + // if overriding a global setting, can be empty to disable local setting + numHostExpiryWindow < 0 + ) { + errors.host_expiry_window = HOST_EXPIRY_ERROR_TEXT; + } + + setFormErrors(errors); + }, [ + formDataTeamHostExpiryEnabled, + formDataTeamHostExpiryWindow, + globalHostExpiryEnabled, + ]); + + useEffect(() => { + validate(); + }, [formDataTeamHostExpiryEnabled, formDataTeamHostExpiryWindow, validate]); + + const updateTeamHostExpiry = useCallback( + (evt: React.MouseEvent) => { + evt.preventDefault(); + setUpdatingTeamSettings(true); + const castedHostExpiryWindow = Number(formDataTeamHostExpiryWindow); + let enableHostExpiry; + if (globalHostExpiryEnabled) { + if (!castedHostExpiryWindow) { + enableHostExpiry = false; + } else { + enableHostExpiry = formDataTeamHostExpiryEnabled; + } + } else { + enableHostExpiry = formDataTeamHostExpiryEnabled; + } + teamsAPI + .update( + { + host_expiry_settings: { + host_expiry_enabled: enableHostExpiry, + host_expiry_window: castedHostExpiryWindow, + }, + }, + teamIdForApi + ) + .then(() => { + renderFlash("success", "Successfully updated settings."); + refetchTeamConfig(); + }) + .catch((errorResponse: { data: IApiError }) => { + renderFlash( + "error", + `Could not update team settings. ${errorResponse.data.errors[0].reason}` + ); + }) + .finally(() => { + setUpdatingTeamSettings(false); + }); + }, + [ + formDataTeamHostExpiryEnabled, + formDataTeamHostExpiryWindow, + globalHostExpiryEnabled, + refetchTeamConfig, + renderFlash, + teamIdForApi, + ] + ); + + const renderForm = () => { + if (errorLoadGlobalConfig || errorLoadTeamConfig) { + return ; + } + if (isLoadingTeamConfig || isLoadingAppConfig) { + return ; + } + return ( +
+ {globalHostExpiryEnabled !== undefined && ( + + )} + {formDataTeamHostExpiryEnabled && ( + + )} + + + ); + }; + + return ( +
+
Host expiry settings
+ {renderForm()} +
+ ); +}; +export default TeamSettings; diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamSettings/_styles.scss b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamSettings/_styles.scss new file mode 100644 index 000000000..75c3ce19a --- /dev/null +++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamSettings/_styles.scss @@ -0,0 +1,7 @@ +.team-settings { + max-width: $settings-form-max-width; + .section-header { + border-bottom: 1px solid #e3e3e3; + padding-bottom: 8px; + } +} diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamSettings/components/TeamHostExpiryToggle/TeamHostExpiryToggle.tests.tsx b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamSettings/components/TeamHostExpiryToggle/TeamHostExpiryToggle.tests.tsx new file mode 100644 index 000000000..7c5bbac93 --- /dev/null +++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamSettings/components/TeamHostExpiryToggle/TeamHostExpiryToggle.tests.tsx @@ -0,0 +1,57 @@ +import React from "react"; + +import { render, screen } from "@testing-library/react"; + +import TeamHostExpiryToggle from "./TeamHostExpiryToggle"; + +describe("TeamHostExpiryToggle component", () => { + // global setting disabled + it("Renders correctly with no global window set", () => { + render( + + ); + + expect(screen.getByText(/Enable host expiry/)).toBeInTheDocument(); + expect(screen.queryByText(/Host expiry is globally enabled/)).toBeNull(); + }); + + // global setting enabled + it("Renders as expected when global enabled, local disabled", () => { + render( + + ); + + expect(screen.getByText(/Enable host expiry/)).toBeInTheDocument(); + expect( + screen.getByText(/Host expiry is globally enabled/) + ).toBeInTheDocument(); + expect(screen.getByText(/Add custom expiry window/)).toBeInTheDocument(); + }); + + it("Renders as expected when global enabled, local enabled", () => { + render( + + ); + + expect(screen.getByText(/Enable host expiry/)).toBeInTheDocument(); + expect( + screen.getByText(/Host expiry is globally enabled/) + ).toBeInTheDocument(); + expect(screen.queryByText(/Add custom expiry window/)).toBeNull(); + }); +}); diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamSettings/components/TeamHostExpiryToggle/TeamHostExpiryToggle.tsx b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamSettings/components/TeamHostExpiryToggle/TeamHostExpiryToggle.tsx new file mode 100644 index 000000000..a574729e2 --- /dev/null +++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamSettings/components/TeamHostExpiryToggle/TeamHostExpiryToggle.tsx @@ -0,0 +1,85 @@ +import Checkbox from "components/forms/fields/Checkbox"; +import Icon from "components/Icon"; +import React from "react"; +import { Link } from "react-router"; + +const baseClass = "team-host-expiry-toggle"; + +interface ITeamHostExpiryToggle { + globalHostExpiryEnabled: boolean; + globalHostExpiryWindow?: number; + teamExpiryEnabled: boolean; + setTeamExpiryEnabled: (value: boolean) => void; +} + +const TeamHostExpiryToggle = ({ + globalHostExpiryEnabled, + globalHostExpiryWindow, + teamExpiryEnabled, + setTeamExpiryEnabled, +}: ITeamHostExpiryToggle) => { + const renderHelpText = () => + // this will never be rendered while globalHostExpiryWindow is undefined + globalHostExpiryEnabled ? ( +
+ Host expiry is globally enabled in organization settings. By default, + hosts expire after {globalHostExpiryWindow} days.{" "} + {!teamExpiryEnabled && ( + { + e.preventDefault(); + setTeamExpiryEnabled(true); + }} + className={`${baseClass}__add-custom-window`} + > + <> + Add custom expiry window + + + + )} +
+ ) : ( + <> + ); + return ( +
+ + When enabled, allows automatic cleanup of +
+ hosts that have not communicated with Fleet in +
+ the number of days specified in the{" "} + + Host expiry +
+ window +
{" "} + setting.{" "} + + (Default: Off) + + + ) + } + > + Enable host expiry +
+
+ ); +}; + +export default TeamHostExpiryToggle; diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamSettings/components/TeamHostExpiryToggle/_styles.scss b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamSettings/components/TeamHostExpiryToggle/_styles.scss new file mode 100644 index 000000000..43791035f --- /dev/null +++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamSettings/components/TeamHostExpiryToggle/_styles.scss @@ -0,0 +1,13 @@ +.team-host-expiry-toggle { + &__disabled-team-host-expiry-toggle > .fleet-checkbox { + @include disabled; + } + + &__add-custom-window { + display: inline-flex; + align-items: center; + cursor: pointer; + font-weight: inherit; + font-size: inherit; + } +} diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamSettings/components/TeamHostExpiryToggle/index.ts b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamSettings/components/TeamHostExpiryToggle/index.ts new file mode 100644 index 000000000..d657327ae --- /dev/null +++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamSettings/components/TeamHostExpiryToggle/index.ts @@ -0,0 +1 @@ +export { default } from "./TeamHostExpiryToggle"; diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamSettings/index.ts b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamSettings/index.ts new file mode 100644 index 000000000..e5df1768f --- /dev/null +++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamSettings/index.ts @@ -0,0 +1 @@ +export { default } from "./TeamSettings"; diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/_styles.scss b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/_styles.scss index d23d10383..e170b5fee 100644 --- a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/_styles.scss +++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/_styles.scss @@ -27,4 +27,8 @@ margin-left: $pad-medium; padding: 4px 12px; } + + .react-tabs__tab-list { + margin-bottom: 50px; + } } diff --git a/frontend/pages/admin/components/SideNav/_styles.scss b/frontend/pages/admin/components/SideNav/_styles.scss index cad6fa413..df0a4a4f4 100644 --- a/frontend/pages/admin/components/SideNav/_styles.scss +++ b/frontend/pages/admin/components/SideNav/_styles.scss @@ -23,7 +23,7 @@ // Global style max width of all content to 754px > * { width: 100%; - max-width: 754px; + max-width: $settings-form-max-width; } } } diff --git a/frontend/router/index.tsx b/frontend/router/index.tsx index d6eac93ac..608d944b2 100644 --- a/frontend/router/index.tsx +++ b/frontend/router/index.tsx @@ -63,6 +63,7 @@ import SoftwareTitles from "pages/SoftwarePage/SoftwareTitles"; import SoftwareOS from "pages/SoftwarePage/SoftwareOS"; import SoftwareTitleDetailsPage from "pages/SoftwarePage/SoftwareTitleDetailsPage"; import SoftwareVersionDetailsPage from "pages/SoftwarePage/SoftwareVersionDetailsPage"; +import TeamSettings from "pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamSettings"; import SoftwareOSDetailsPage from "pages/SoftwarePage/SoftwareOSDetailsPage"; import PATHS from "router/paths"; @@ -158,6 +159,7 @@ const routes = ( + diff --git a/frontend/router/paths.ts b/frontend/router/paths.ts index 13f89749b..21c74ab3a 100644 --- a/frontend/router/paths.ts +++ b/frontend/router/paths.ts @@ -143,6 +143,12 @@ export default { } return `${URL_PREFIX}/settings/teams`; }, + TEAM_DETAILS_SETTINGS: (teamId?: number) => { + if (teamId !== undefined && teamId > 0) { + return `${URL_PREFIX}/settings/teams/settings?team_id=${teamId}`; + } + return `${URL_PREFIX}/settings/teams`; + }, MANAGE_PACKS: `${URL_PREFIX}/packs/manage`, NEW_PACK: `${URL_PREFIX}/packs/new`, MANAGE_QUERIES: `${URL_PREFIX}/queries/manage`, diff --git a/frontend/services/entities/teams.ts b/frontend/services/entities/teams.ts index 7c49b841d..1916d94b4 100644 --- a/frontend/services/entities/teams.ts +++ b/frontend/services/entities/teams.ts @@ -50,6 +50,10 @@ export interface IUpdateTeamFormData { grace_period_days: number; }; }; + host_expiry_settings: { + host_expiry_enabled: boolean; + host_expiry_window: number; // days + }; } export default { @@ -91,7 +95,13 @@ export default { return sendRequest("GET", path); }, update: ( - { name, webhook_settings, integrations, mdm }: Partial, + { + name, + webhook_settings, + integrations, + mdm, + host_expiry_settings, + }: Partial, teamId?: number ): Promise => { if (typeof teamId === "undefined") { @@ -123,6 +133,9 @@ export default { if (mdm) { requestBody.mdm = mdm; } + if (host_expiry_settings) { + requestBody.host_expiry_settings = host_expiry_settings; + } return sendRequest("PATCH", path, requestBody); }, diff --git a/frontend/styles/var/_global.scss b/frontend/styles/var/_global.scss index 65567cb51..1032bacc1 100644 --- a/frontend/styles/var/_global.scss +++ b/frontend/styles/var/_global.scss @@ -6,3 +6,6 @@ $border-radius-xxlarge: 16px; // box shadow $box-shadow: 0px 3px 0px rgba(226, 228, 234, 0.4); + +// dimensions +$settings-form-max-width: 754px; diff --git a/frontend/styles/var/forms.scss b/frontend/styles/var/forms.scss deleted file mode 100644 index 3543ed280..000000000 --- a/frontend/styles/var/forms.scss +++ /dev/null @@ -1 +0,0 @@ -$form-field-label-height: 24px;