App Settings Page: Host status webhook UI (#1899)

* Skeleton UI

* Rebase to main

* Work towards config API request modification

* Nest and unnest when formatting for server and frontend

* Changelog

* Add validation to UI, fix ? vertical spacing

* Rebase e2e

* 1 of 2 passing unit tests for config

* Update REST-API.md to include webhook_settings

* Destructure / flatten config webhook in unit test

* Merge advance options e2e conflict

* x and y example not x and x

* Fix observer e2e

* Add new data to read only example request

Co-authored-by: Noah Talerman <noahtal@umich.edu>
Co-authored-by: Martavis Parker <martavis@auraticdevelopment.com>
This commit is contained in:
RachelElysia
2021-09-10 10:49:11 -07:00
committed by GitHub
co-authored by Noah Talerman Martavis Parker
parent 13e5e6a6db
commit d32479622c
15 changed files with 292 additions and 10 deletions
@@ -28,6 +28,19 @@ const authTypeOptions = [
{ label: "Username and Password", value: "authtype_username_password" },
{ label: "None", value: "authtype_none" },
];
const percentageOfHosts = [
{ label: "1%", value: 1 },
{ label: "5%", value: 5 },
{ label: "10%", value: 10 },
{ label: "25%", value: 25 },
];
const numberOfDays = [
{ label: "1 day", value: 1 },
{ label: "3 days", value: 3 },
{ label: "7 days", value: 7 },
{ label: "14 days", value: 14 },
];
const baseClass = "app-config-form";
const formFields = [
"authentication_method",
@@ -58,6 +71,10 @@ const formFields = [
"host_expiry_window",
"live_query_disabled",
"agent_options",
"enable_host_status_webhook",
"destination_url",
"host_percentage",
"days_count",
"enable_analytics",
];
class AppConfigForm extends Component {
@@ -90,6 +107,10 @@ class AppConfigForm extends Component {
host_expiry_window: formFieldInterface.isRequired,
live_query_disabled: formFieldInterface.isRequired,
agent_options: formFieldInterface.isRequired,
enable_host_status_webhook: formFieldInterface.isRequired,
destination_url: formFieldInterface,
host_percentage: formFieldInterface,
days_count: formFieldInterface,
enable_analytics: formFieldInterface.isRequired,
}).isRequired,
enrollSecret: PropTypes.arrayOf(enrollSecretInterface).isRequired,
@@ -101,10 +122,28 @@ class AppConfigForm extends Component {
super(props);
this.state = {
showHostStatusWebhookPreviewModal: false,
showUsageStatsPreviewModal: false,
};
}
onToggleAdvancedOptions = (evt) => {
evt.preventDefault();
const { showAdvancedOptions } = this.state;
this.setState({ showAdvancedOptions: !showAdvancedOptions });
return false;
};
toggleHostStatusWebhookPreviewModal = () => {
const { showHostStatusWebhookPreviewModal } = this.state;
this.setState({
showHostStatusWebhookPreviewModal: !showHostStatusWebhookPreviewModal,
});
};
toggleUsageStatsPreviewModal = () => {
const { showUsageStatsPreviewModal } = this.state;
this.setState({
@@ -181,6 +220,45 @@ class AppConfigForm extends Component {
);
};
renderHostStatusWebhookPreviewModal = () => {
const { toggleHostStatusWebhookPreviewModal } = this;
const { showHostStatusWebhookPreviewModal } = this.state;
if (!showHostStatusWebhookPreviewModal) {
return null;
}
const json = {
message:
"More than X% of your hosts have not checked into Fleet for more than Y days. Youve been sent this message because the Host status webhook is enabled in your Fleet instance.",
data: {
unseen_hosts: 1,
total_hosts: 2,
days_unseen: 3,
},
};
return (
<Modal
title="Host status webhook"
onExit={toggleHostStatusWebhookPreviewModal}
className={`${baseClass}__host-status-webhook-preview-modal`}
>
<p>
An example request sent to your configured <b>Destination URL</b>.
</p>
<div className={`${baseClass}__host-status-webhook-preview`}>
<pre dangerouslySetInnerHTML={{ __html: syntaxHighlight(json) }} />
</div>
<div className="flex-end">
<Button type="button" onClick={toggleHostStatusWebhookPreviewModal}>
Done
</Button>
</div>
</Modal>
);
};
renderUsageStatsPreviewModal = () => {
const { toggleUsageStatsPreviewModal } = this;
const { showUsageStatsPreviewModal } = this.state;
@@ -217,7 +295,9 @@ class AppConfigForm extends Component {
const {
renderAdvancedOptions,
renderSmtpSection,
toggleHostStatusWebhookPreviewModal,
toggleUsageStatsPreviewModal,
renderHostStatusWebhookPreviewModal,
renderUsageStatsPreviewModal,
} = this;
@@ -398,7 +478,7 @@ class AppConfigForm extends Component {
<div className={`${baseClass}__details`}>
<IconToolTip
text={
"The hostname / IP address and corresponding port of your organization&apos;s SMTP server."
"The hostname / IP address and corresponding port of your organization's SMTP server."
}
/>
</div>
@@ -479,6 +559,89 @@ class AppConfigForm extends Component {
</div>
</div>
<div className={`${baseClass}__section`}>
<h2>
<a id="host-status-webhook">Host status webhook</a>
</h2>
<div className={`${baseClass}__host-status-webhook`}>
<p className={`${baseClass}__section-description`}>
Send an alert if a portion of your hosts go offline.
</p>
<Checkbox {...fields.enable_host_status_webhook}>
Enable host status webhook
</Checkbox>
<p className={`${baseClass}__section-description`}>
A request will be sent to your configured <b>Destination URL</b>{" "}
if the configured <b>Percentage of hosts</b> have not checked
into Fleet for the configured <b>Number of days</b>.
</p>
</div>
<div
className={`${baseClass}__inputs ${baseClass}__inputs--webhook`}
>
<Button
type="button"
variant="inverse"
onClick={toggleHostStatusWebhookPreviewModal}
>
Preview request
</Button>
</div>
<div className={`${baseClass}__inputs`}>
<InputField
{...fields.destination_url}
placeholder="https://server.com/example"
label="Destination URL"
/>
</div>
<div className={`${baseClass}__details`}>
<IconToolTip
isHtml
text={
"\
<center><p>Provide a URL to deliver <br/>the webhook request to.</p></center>\
"
}
/>
</div>
<div
className={`${baseClass}__inputs ${baseClass}__host-percentage`}
>
<Dropdown
{...fields.host_percentage}
label="Percentage of hosts"
options={percentageOfHosts}
/>
</div>
<div className={`${baseClass}__details`}>
<IconToolTip
isHtml
text={
"\
<center><p>Select the minimum percentage of hosts that<br/>must fail to check into Fleet in order to trigger<br/>the webhook request.</p></center>\
"
}
/>
</div>
<div className={`${baseClass}__inputs ${baseClass}__days-count`}>
<Dropdown
{...fields.days_count}
label="Number of days"
options={numberOfDays}
/>
</div>
<div className={`${baseClass}__details`}>
<IconToolTip
isHtml
text={
"\
<center><p>Select the minimum number of days that the<br/>configured <b>Percentage of hosts</b> must fail to<br/>check into Fleet in order to trigger the<br/>webhook request.</p></center>\
"
}
/>
</div>
</div>
<div className={`${baseClass}__section`}>
<h2>
<a id="usage-stats">Usage statistics</a>
@@ -527,6 +690,7 @@ class AppConfigForm extends Component {
</Button>
</form>
{renderUsageStatsPreviewModal()}
{renderHostStatusWebhookPreviewModal()}
</>
);
}
@@ -123,7 +123,7 @@
&__inputs {
width: 60%;
float: left;
padding: 0 $pad-xxlarge 0 0;
padding-right: $pad-small;
box-sizing: border-box;
.input-field {
@@ -152,11 +152,15 @@
&--usage {
margin-top: 24px;
}
&--webhook {
margin-bottom: 24px;
}
}
&__details {
float: right;
width: 40%;
height: 87px;
.icon-tooltip {
margin: $pad-xlarge 0;
@@ -216,7 +220,8 @@
); // because the outline extended beyond the sticky page description
}
&__usage-stats-preview-modal {
&__usage-stats-preview-modal,
&__host-status-webhook-preview-modal {
.flex-end {
margin-top: 20px;
display: flex;
@@ -22,6 +22,10 @@ export default (formData) => {
host_expiry_enabled: hostExpiryEnabled,
host_expiry_window: hostExpiryWindow = 0,
agent_options: agentOptions,
enable_host_status_webhook: enableHostStatusWebhook,
destination_url: destinationUrl,
host_percentage: hostPercentage,
days_count: daysCount,
} = formData;
if (enableSSO) {
@@ -68,6 +72,20 @@ export default (formData) => {
}
}
if (enableHostStatusWebhook) {
if (!destinationUrl) {
errors.destination_url = "Destination URL must be present";
}
if (!hostPercentage) {
errors.host_percentage = "Host percentage must be present";
}
if (!daysCount) {
errors.days_count = "Days count must be present";
}
}
if (hostExpiryEnabled) {
if (isNaN(hostExpiryWindow) || Number(hostExpiryWindow) <= 0) {
errors.host_expiry_window =