mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 00:45:19 +00:00
UI – Team-level host expiry setting (#16276)
## ➡️ #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 <jacob@fleetdm.com> Co-authored-by: Sharon Katz <121527325+sharon-fdm@users.noreply.github.com>
This commit is contained in:
parent
aa60187aa1
commit
7550fd69fa
1
changes/15965-team-host-expiry-frontend
Normal file
1
changes/15965-team-host-expiry-frontend
Normal file
@ -0,0 +1 @@
|
||||
* Implement the UI for the new team-level host expiry setting feature.
|
@ -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.
|
||||
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
11
frontend/interfaces/team_subnav.ts
Normal file
11
frontend/interfaces/team_subnav.ts
Normal file
@ -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;
|
||||
}
|
@ -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({
|
||||
|
@ -2,7 +2,6 @@
|
||||
font-size: $x-small;
|
||||
|
||||
&__page-description {
|
||||
padding-top: 40px;
|
||||
padding-bottom: $pad-medium;
|
||||
margin: 0;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -1,7 +1,6 @@
|
||||
.members {
|
||||
&__page-description {
|
||||
font-size: $x-small;
|
||||
padding-top: 40px;
|
||||
padding-bottom: $pad-medium;
|
||||
margin: 0;
|
||||
}
|
||||
|
@ -51,6 +51,10 @@ const teamDetailsSubNav: ITeamDetailsSubNavItem[] = [
|
||||
name: "Agent options",
|
||||
getPathname: PATHS.TEAM_DETAILS_OPTIONS,
|
||||
},
|
||||
{
|
||||
name: "Settings",
|
||||
getPathname: PATHS.TEAM_DETAILS_SETTINGS,
|
||||
},
|
||||
];
|
||||
|
||||
interface ITeamDetailsPageProps {
|
||||
|
@ -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<number | string>("");
|
||||
const [updatingTeamSettings, setUpdatingTeamSettings] = useState(false);
|
||||
const [formErrors, setFormErrors] = useState<Record<string, string | null>>(
|
||||
{}
|
||||
);
|
||||
|
||||
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<IConfig, Error, IConfig>(
|
||||
["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<ILoadTeamResponse, Error, ITeamConfig>(
|
||||
["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<string, string> = {};
|
||||
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<HTMLFormElement>) => {
|
||||
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 <DataError />;
|
||||
}
|
||||
if (isLoadingTeamConfig || isLoadingAppConfig) {
|
||||
return <Spinner />;
|
||||
}
|
||||
return (
|
||||
<form onSubmit={updateTeamHostExpiry}>
|
||||
{globalHostExpiryEnabled !== undefined && (
|
||||
<TeamHostExpiryToggle
|
||||
globalHostExpiryEnabled={globalHostExpiryEnabled}
|
||||
globalHostExpiryWindow={globalHostExpiryWindow}
|
||||
teamExpiryEnabled={formDataTeamHostExpiryEnabled}
|
||||
setTeamExpiryEnabled={setFormDataTeamHostExpiryEnabled}
|
||||
/>
|
||||
)}
|
||||
{formDataTeamHostExpiryEnabled && (
|
||||
<InputField
|
||||
label="Host expiry window"
|
||||
// type="text" allows `validate` to differentiate between
|
||||
// non-numerical input and an empty input
|
||||
type="text"
|
||||
onChange={setFormDataTeamHostExpiryWindow}
|
||||
name="host-expiry-window"
|
||||
value={formDataTeamHostExpiryWindow}
|
||||
error={formErrors.host_expiry_window}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
variant="brand"
|
||||
className="button-wrap"
|
||||
isLoading={updatingTeamSettings}
|
||||
disabled={Object.keys(formErrors).length > 0}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className={`${baseClass}`}>
|
||||
<div className="section-header">Host expiry settings</div>
|
||||
{renderForm()}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
export default TeamSettings;
|
@ -0,0 +1,7 @@
|
||||
.team-settings {
|
||||
max-width: $settings-form-max-width;
|
||||
.section-header {
|
||||
border-bottom: 1px solid #e3e3e3;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
}
|
@ -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(
|
||||
<TeamHostExpiryToggle
|
||||
globalHostExpiryEnabled={false}
|
||||
globalHostExpiryWindow={undefined}
|
||||
teamExpiryEnabled={false}
|
||||
setTeamExpiryEnabled={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
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(
|
||||
<TeamHostExpiryToggle
|
||||
globalHostExpiryEnabled
|
||||
globalHostExpiryWindow={2}
|
||||
teamExpiryEnabled={false}
|
||||
setTeamExpiryEnabled={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
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(
|
||||
<TeamHostExpiryToggle
|
||||
globalHostExpiryEnabled
|
||||
globalHostExpiryWindow={2}
|
||||
teamExpiryEnabled
|
||||
setTeamExpiryEnabled={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText(/Enable host expiry/)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/Host expiry is globally enabled/)
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByText(/Add custom expiry window/)).toBeNull();
|
||||
});
|
||||
});
|
@ -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 ? (
|
||||
<div className="help-text">
|
||||
Host expiry is globally enabled in organization settings. By default,
|
||||
hosts expire after {globalHostExpiryWindow} days.{" "}
|
||||
{!teamExpiryEnabled && (
|
||||
<Link
|
||||
to={""}
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
setTeamExpiryEnabled(true);
|
||||
}}
|
||||
className={`${baseClass}__add-custom-window`}
|
||||
>
|
||||
<>
|
||||
Add custom expiry window
|
||||
<Icon name="chevron-right" color="core-fleet-blue" size="small" />
|
||||
</>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
return (
|
||||
<div className={`${baseClass}`}>
|
||||
<Checkbox
|
||||
name="enableHostExpiry"
|
||||
onChange={setTeamExpiryEnabled}
|
||||
value={teamExpiryEnabled || globalHostExpiryEnabled}
|
||||
wrapperClassName={
|
||||
globalHostExpiryEnabled
|
||||
? `${baseClass}__disabled-team-host-expiry-toggle`
|
||||
: ""
|
||||
}
|
||||
helpText={renderHelpText()}
|
||||
tooltipContent={
|
||||
!globalHostExpiryEnabled && (
|
||||
<>
|
||||
When enabled, allows automatic cleanup of
|
||||
<br />
|
||||
hosts that have not communicated with Fleet in
|
||||
<br />
|
||||
the number of days specified in the{" "}
|
||||
<strong>
|
||||
Host expiry
|
||||
<br />
|
||||
window
|
||||
</strong>{" "}
|
||||
setting.{" "}
|
||||
<em>
|
||||
(Default: <strong>Off</strong>)
|
||||
</em>
|
||||
</>
|
||||
)
|
||||
}
|
||||
>
|
||||
Enable host expiry
|
||||
</Checkbox>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamHostExpiryToggle;
|
@ -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;
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { default } from "./TeamHostExpiryToggle";
|
@ -0,0 +1 @@
|
||||
export { default } from "./TeamSettings";
|
@ -27,4 +27,8 @@
|
||||
margin-left: $pad-medium;
|
||||
padding: 4px 12px;
|
||||
}
|
||||
|
||||
.react-tabs__tab-list {
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,7 @@
|
||||
// Global style max width of all content to 754px
|
||||
> * {
|
||||
width: 100%;
|
||||
max-width: 754px;
|
||||
max-width: $settings-form-max-width;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 = (
|
||||
<Route path="teams" component={TeamDetailsWrapper}>
|
||||
<Route path="members" component={MembersPage} />
|
||||
<Route path="options" component={AgentOptionsPage} />
|
||||
<Route path="settings" component={TeamSettings} />
|
||||
</Route>
|
||||
<Redirect from="teams/:team_id" to="teams" />
|
||||
<Redirect from="teams/:team_id/members" to="teams" />
|
||||
|
@ -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`,
|
||||
|
@ -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<IUpdateTeamFormData>,
|
||||
{
|
||||
name,
|
||||
webhook_settings,
|
||||
integrations,
|
||||
mdm,
|
||||
host_expiry_settings,
|
||||
}: Partial<IUpdateTeamFormData>,
|
||||
teamId?: number
|
||||
): Promise<ITeamConfig> => {
|
||||
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);
|
||||
},
|
||||
|
@ -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;
|
||||
|
@ -1 +0,0 @@
|
||||
$form-field-label-height: 24px;
|
Loading…
Reference in New Issue
Block a user