Add new "Fleet Desktop" section to global settings page (#6161)

This commit is contained in:
gillespi314 2022-06-11 12:23:02 -05:00 committed by GitHub
parent 18de43f35b
commit c146ea4aa4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 195 additions and 3 deletions

View File

@ -0,0 +1 @@
* Enable UI for Fleet Premium licensees to set custom transparancy url for Fleet Desktop

View File

@ -696,6 +696,15 @@ describe(
cy.loginWithCySession("anna@organization.com", GOOD_PASSWORD);
cy.visit("/settings/users");
});
it("hides access to Fleet Desktop settings", () => {
cy.visit("settings/organization");
cy.getAttached(".app-settings__form-nav-list").within(() => {
cy.findByText(/organization info/i).should("exist");
cy.findByText(/fleet desktop/i).should("not.exist");
});
cy.visit("settings/organization/fleet-desktop");
cy.findAllByText(/access denied/i).should("exist");
});
it("hides access team settings", () => {
cy.findByText(/teams/i).should("not.exist");
});

View File

@ -682,6 +682,26 @@ describe("Premium tier - Global Admin user", () => {
cy.findByLabelText(/password/i).should("exist");
});
});
it("allows access to Fleet Desktop settings", () => {
cy.visit("settings/organization");
cy.getAttached(".app-settings__form-nav-list").within(() => {
cy.findByText(/organization info/i).should("exist");
cy.findByText(/fleet desktop/i)
.should("exist")
.click();
});
cy.getAttached("[id=transparency_url")
.should("have.value", "https://fleetdm.com/transparency")
.clear()
.type("example.com/transparency");
cy.findByRole("button", { name: /save/i }).click();
cy.findByText(/successfully updated/i).should("exist");
cy.visit("settings/organization/fleet-desktop");
cy.getAttached("[id=transparency_url").should(
"have.value",
"example.com/transparency"
);
});
});
describe("User profile page", () => {
it("renders elements according to role-based access controls", () => {

View File

@ -72,6 +72,10 @@ export default PropTypes.shape({
}),
});
export interface IFleetDesktopSettings {
transparency_url: string;
}
export interface IConfigFormData {
smtpAuthenticationMethod: string;
smtpAuthenticationType: string;
@ -105,6 +109,7 @@ export interface IConfigFormData {
hostStatusWebhookHostPercentage?: number;
hostStatusWebhookDaysCount?: number;
enableUsageStatistics: boolean;
transparency_url: string;
}
export interface IConfig {
@ -162,6 +167,7 @@ export interface IConfig {
expiration: string;
note: string;
};
fleet_desktop: IFleetDesktopSettings;
vulnerabilities: {
databases_path: string;
periodicity: number;

View File

@ -1,4 +1,5 @@
import React, { useCallback, useContext, useState, useEffect } from "react";
import { useErrorHandler } from "react-error-boundary";
import { useQuery } from "react-query";
import { Params } from "react-router/lib/Router";
import { Link } from "react-router";
@ -19,6 +20,7 @@ import AgentOptions from "./cards/Agents";
import HostStatusWebhook from "./cards/HostStatusWebhook";
import Statistics from "./cards/Statistics";
import Advanced from "./cards/Advanced";
import FleetDesktop from "./cards/FleetDesktop";
interface IAppSettingsPageProps {
params: Params;
@ -29,8 +31,10 @@ export const baseClass = "app-settings";
const AppSettingsPage = ({
params: { section: sectionTitle },
}: IAppSettingsPageProps): JSX.Element => {
const { isFreeTier, isPremiumTier, setConfig } = useContext(AppContext);
const { renderFlash } = useContext(NotificationContext);
const { setConfig } = useContext(AppContext);
const handlePageError = useErrorHandler();
const [activeSection, setActiveSection] = useState<string>("info");
@ -50,7 +54,7 @@ const AppSettingsPage = ({
};
const onFormSubmit = useCallback(
(formData: IConfig) => {
(formData: Partial<IConfig>) => {
if (!appConfig) {
return false;
}
@ -87,10 +91,13 @@ const AppSettingsPage = ({
);
useEffect(() => {
if (isFreeTier && sectionTitle === "fleet-desktop") {
handlePageError({ status: 403 });
}
if (sectionTitle) {
setActiveSection(sectionTitle);
}
}, [sectionTitle]);
}, [isFreeTier, sectionTitle]);
const renderSection = () => {
if (!isLoading && appConfig) {
@ -123,6 +130,13 @@ const AppSettingsPage = ({
{activeSection === "advanced" && (
<Advanced appConfig={appConfig} handleSubmit={onFormSubmit} />
)}
{isPremiumTier && activeSection === "fleet-desktop" && (
<FleetDesktop
appConfig={appConfig}
isPremiumTier={isPremiumTier}
handleSubmit={onFormSubmit}
/>
)}
</>
);
}
@ -208,6 +222,18 @@ const AppSettingsPage = ({
Usage statistics
</Link>
</li>
{isPremiumTier && (
<li>
<Link
className={`${baseClass}__nav-link ${isNavItemActive(
"fleet-desktop"
)}`}
to={PATHS.ADMIN_SETTINGS_FLEET_DESKTOP}
>
Fleet Desktop
</Link>
</li>
)}
<li>
<Link
className={`${baseClass}__nav-link ${isNavItemActive(

View File

@ -293,6 +293,18 @@
margin-bottom: $pad-large;
}
&__transparency {
color: $core-vibrant-blue;
font-size: $x-small;
font-weight: $bold;
text-decoration: none;
img {
width: 12px;
height: 12px;
}
}
.component__tooltip-wrapper {
margin-bottom: $pad-xsmall;
}

View File

@ -0,0 +1,112 @@
import React, { useState } from "react";
import { IConfig, IConfigFormData } from "interfaces/config";
import Button from "components/buttons/Button";
// @ts-ignore
import InputField from "components/forms/fields/InputField";
import validUrl from "components/forms/validators/valid_url";
import OpenNewTabIcon from "../../../../../../assets/images/open-new-tab-12x12@2x.png";
import {
DEFAULT_TRANSPARENCY_URL,
IAppConfigFormProps,
IFormField,
IAppConfigFormErrors,
} from "../constants";
const baseClass = "app-config-form";
const FleetDesktop = ({
appConfig,
handleSubmit,
isPremiumTier,
}: IAppConfigFormProps): JSX.Element => {
const [formData, setFormData] = useState<
Pick<IConfigFormData, "transparency_url">
>({
transparency_url:
appConfig.fleet_desktop?.transparency_url || DEFAULT_TRANSPARENCY_URL,
});
const [formErrors, setFormErrors] = useState<IAppConfigFormErrors>({});
const handleInputChange = ({ value }: IFormField) => {
setFormData({ transparency_url: value.toString() });
setFormErrors({});
};
const validateForm = () => {
const { transparency_url } = formData;
const errors: IAppConfigFormErrors = {};
if (!transparency_url) {
errors.transparency_url = "Transparency URL name must be present";
} else if (!validUrl(transparency_url)) {
errors.transparency_url = `${transparency_url} is not a valid URL`;
}
setFormErrors(errors);
};
const onFormSubmit = (evt: React.MouseEvent<HTMLFormElement>) => {
evt.preventDefault();
const formDataForAPI: Pick<IConfig, "fleet_desktop"> = {
fleet_desktop: {
transparency_url: formData.transparency_url,
},
};
handleSubmit(formDataForAPI);
};
if (!isPremiumTier) {
return <></>;
}
return (
<form className={baseClass} onSubmit={onFormSubmit} autoComplete="off">
<div className={`${baseClass}__section`}>
<h2>Fleet Desktop</h2>
<div className={`${baseClass}__inputs`}>
<InputField
label="Custom transparency URL"
onChange={handleInputChange}
name="transparency_url"
value={formData.transparency_url}
parseTarget
onBlur={validateForm}
error={formErrors.transparency_url}
/>
<p className={`${baseClass}__component-label`}>
When an end user clicks Transparency in the Fleet Desktop menu, by
default they are taken to{" "}
<a
className={`${baseClass}__transparency`}
href="https://fleetdm.com/transparency"
target="_blank"
rel="noopener noreferrer"
>
{" "}
https://fleetdm.com/transparency{" "}
<img className="icon" src={OpenNewTabIcon} alt="open new tab" />
</a>{" "}
. You can override the URL to take them to a resource of your
choice.
</p>
</div>
</div>
<Button
type="submit"
variant="brand"
disabled={Object.keys(formErrors).length > 0}
>
Save
</Button>
</form>
);
};
export default FleetDesktop;

View File

@ -0,0 +1 @@
export { default } from "./FleetDesktop";

View File

@ -1,7 +1,10 @@
import { IConfig } from "interfaces/config";
export const DEFAULT_TRANSPARENCY_URL = "https://fleetdm.com/transparency";
export interface IAppConfigFormProps {
appConfig: IConfig;
isPremiumTier?: boolean;
handleSubmit: any;
}
@ -26,6 +29,7 @@ export interface IAppConfigFormErrors {
destination_url?: string | null;
host_expiry_window?: string | null;
agent_options?: string | null;
transparency_url?: string | null;
}
export const authMethodOptions = [

View File

@ -18,6 +18,7 @@ export default {
ADMIN_SETTINGS_HOST_STATUS_WEBHOOK: `${URL_PREFIX}/settings/organization/host-status-webhook`,
ADMIN_SETTINGS_STATISTICS: `${URL_PREFIX}/settings/organization/statistics`,
ADMIN_SETTINGS_ADVANCED: `${URL_PREFIX}/settings/organization/advanced`,
ADMIN_SETTINGS_FLEET_DESKTOP: `${URL_PREFIX}/settings/organization/fleet-desktop`,
ALL_PACKS: `${URL_PREFIX}/packs/all`,
EDIT_PACK: (packId: number): string => {
return `${URL_PREFIX}/packs/${packId}/edit`;