Fix display of platform labels (#1866)

* Add policies UI components

* Add policy filter to hosts page
This commit is contained in:
gillespi314 2021-08-31 20:02:23 -05:00 committed by GitHub
parent abd4584742
commit 84615afbb3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 272 additions and 297 deletions

View file

@ -0,0 +1 @@
Fix display of platform labels on manage hosts page

View file

@ -1,112 +0,0 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import { filter } from "lodash";
import Button from "components/buttons/Button";
import InputField from "components/forms/fields/InputField";
import labelInterface from "interfaces/label";
import PanelGroup from "components/side_panels/HostSidePanel/PanelGroup";
import SecondarySidePanelContainer from "components/side_panels/SecondarySidePanelContainer";
import statusLabelsInterface from "interfaces/status_labels";
import PlusIcon from "../../../../assets/images/icon-plus-16x16@2x.png";
const baseClass = "host-side-panel";
class HostSidePanel extends Component {
static propTypes = {
labels: PropTypes.arrayOf(labelInterface),
onAddLabelClick: PropTypes.func,
onLabelClick: PropTypes.func,
selectedFilter: PropTypes.string,
statusLabels: statusLabelsInterface,
canAddNewLabel: PropTypes.bool,
};
constructor(props) {
super(props);
this.state = { labelFilter: "" };
}
onFilterLabels = (labelFilter) => {
const lowerLabelFilter = labelFilter.toLowerCase();
this.setState({ labelFilter: lowerLabelFilter });
return false;
};
render() {
const {
labels,
onAddLabelClick,
onLabelClick,
selectedFilter,
canAddNewLabel,
} = this.props;
const { labelFilter } = this.state;
const { onFilterLabels } = this;
const allHostLabels = filter(labels, { type: "all" });
const hostPlatformLabels = filter(labels, (label) => {
return label.type === "platform" && label.count > 0;
});
const customLabels = filter(labels, (label) => {
const lowerDisplayText = label.display_text.toLowerCase();
return label.type === "custom" && lowerDisplayText.match(labelFilter);
});
return (
<SecondarySidePanelContainer className={`${baseClass}`}>
<PanelGroup
groupItems={allHostLabels}
onLabelClick={onLabelClick}
selectedFilter={selectedFilter}
type="all-hosts"
/>
<h3>Operating systems</h3>
<PanelGroup
groupItems={hostPlatformLabels}
onLabelClick={onLabelClick}
selectedFilter={selectedFilter}
type="platform"
/>
<div className="title">
<div>
<h3>Labels</h3>
</div>
<div>
{canAddNewLabel && (
<Button
variant="text-icon"
onClick={onAddLabelClick}
className={`${baseClass}__add-label-btn`}
>
Add label <img src={PlusIcon} alt="Add label icon" />
</Button>
)}
</div>
</div>
<div
className={`${baseClass}__panel-group-item ${baseClass}__panel-group-item--filter`}
>
<InputField
name="tags-filter"
onChange={onFilterLabels}
placeholder="Filter labels by name..."
value={labelFilter}
inputWrapperClass={`${baseClass}__filter-labels`}
/>
</div>
<PanelGroup
groupItems={customLabels}
onLabelClick={onLabelClick}
selectedFilter={selectedFilter}
type="label"
/>
</SecondarySidePanelContainer>
);
}
}
export default HostSidePanel;

View file

@ -0,0 +1,125 @@
import React, { useState, useCallback } from "react";
import { filter } from "lodash";
import Button from "components/buttons/Button";
// @ts-ignore
import InputField from "components/forms/fields/InputField";
// @ts-ignore
import PanelGroup from "components/side_panels/HostSidePanel/PanelGroup";
// @ts-ignore
import SecondarySidePanelContainer from "components/side_panels/SecondarySidePanelContainer";
import { ILabel } from "interfaces/label";
import { PLATFORM_LABEL_DISPLAY_ORDER } from "utilities/constants";
import PlusIcon from "../../../../assets/images/icon-plus-16x16@2x.png";
const baseClass = "host-side-panel";
interface IHostSidePanelProps {
labels: ILabel[];
onAddLabelClick: (evt: React.MouseEvent<HTMLButtonElement>) => void;
onLabelClick: (selectedLabel: ILabel) => boolean;
selectedFilter: string;
canAddNewLabel: boolean;
}
const HostSidePanel = (props: IHostSidePanelProps): JSX.Element => {
const {
labels,
onAddLabelClick,
onLabelClick,
selectedFilter,
canAddNewLabel,
} = props;
const [labelFilter, setLabelFilter] = useState<string>("");
const onFilterLabels = useCallback(
(filterString: string): void => {
setLabelFilter(filterString.toLowerCase());
},
[setLabelFilter]
);
const allHostLabels = filter(labels, { type: "all" });
const hostPlatformLabels = (() => {
const unorderedList: ILabel[] = labels.filter(
(label) => label.type === "platform"
);
const orderedList: ILabel[] = [];
PLATFORM_LABEL_DISPLAY_ORDER.forEach((name) => {
const label = unorderedList.find((el) => el.name === name);
label && orderedList.push(label);
});
return orderedList.filter(
(label) =>
["macOS", "MS Windows", "All Linux"].includes(label.name) ||
label.count !== 0
);
})();
const customLabels = filter(labels, (label) => {
const lowerDisplayText = label.display_text.toLowerCase();
return label.type === "custom" && lowerDisplayText.match(labelFilter);
});
return (
<SecondarySidePanelContainer className={`${baseClass}`}>
<PanelGroup
groupItems={allHostLabels}
onLabelClick={onLabelClick}
selectedFilter={selectedFilter}
type="all-hosts"
/>
<h3>Operating systems</h3>
<PanelGroup
groupItems={hostPlatformLabels}
onLabelClick={onLabelClick}
selectedFilter={selectedFilter}
type="platform"
/>
<div className="title">
<div>
<h3>Labels</h3>
</div>
<div>
{canAddNewLabel && (
<Button
variant="text-icon"
onClick={onAddLabelClick}
className={`${baseClass}__add-label-btn`}
>
<span>
Add label <img src={PlusIcon} alt="Add label icon" />
</span>
</Button>
)}
</div>
</div>
<div
className={`${baseClass}__panel-group-item ${baseClass}__panel-group-item--filter`}
>
<InputField
name="tags-filter"
onChange={onFilterLabels}
placeholder="Filter labels by name..."
value={labelFilter}
inputWrapperClass={`${baseClass}__filter-labels`}
/>
</div>
<PanelGroup
groupItems={customLabels}
onLabelClick={onLabelClick}
selectedFilter={selectedFilter}
type="label"
/>
</SecondarySidePanelContainer>
);
};
export default HostSidePanel;

View file

@ -21,7 +21,8 @@ class PanelGroup extends Component {
renderGroupItem = (item) => {
const { onLabelClick, selectedFilter, statusLabels, type } = this.props;
const selected = item.slug === selectedFilter || type === selectedFilter;
const selected =
(item && item.slug === selectedFilter) || type === selectedFilter;
return (
<PanelGroupItem

View file

@ -7,7 +7,6 @@ describe("PanelGroup - component", () => {
const validPanelGroupItems = [
{ type: "all", display_text: "All Hosts", hosts_count: 20 },
{ type: "platform", display_text: "MAC OS", hosts_count: 10 },
{ type: "status", display_text: "ONLINE", hosts_count: 10 },
];
const component = mount(<PanelGroup groupItems={validPanelGroupItems} />);
@ -15,6 +14,6 @@ describe("PanelGroup - component", () => {
it("renders a PanelGroupItem for each group item", () => {
const panelGroupItems = component.find("PanelGroupItem");
expect(panelGroupItems.length).toEqual(3);
expect(panelGroupItems.length).toEqual(2);
});
});

View file

@ -1,93 +0,0 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import classnames from "classnames";
import statusLabelsInterface from "interfaces/status_labels";
import darwinIcon from "../../../../../assets/images/icon-darwin-fleet-black-16x16@2x.png";
import linuxIcon from "../../../../../assets/images/icon-linux-fleet-black-16x16@2x.png";
import ubuntuIcon from "../../../../../assets/images/icon-ubuntu-fleet-black-16x16@2x.png";
import centosIcon from "../../../../../assets/images/icon-centos-fleet-black-16x16@2x.png";
import windowsIcon from "../../../../../assets/images/icon-windows-fleet-black-16x16@2x.png";
const baseClass = "panel-group-item";
const displayIcon = (name) => {
switch (name) {
case "macOS":
return <img src={darwinIcon} alt="Apple icon" />;
case "Linux":
return <img src={linuxIcon} alt="Linux icon" />;
case "Ubuntu Linux":
return <img src={ubuntuIcon} alt="Ubuntu icon" />;
case "CentOS Linux":
return <img src={centosIcon} alt="Centos icon" />;
case "Windows":
return <img src={windowsIcon} alt="Windows icon" />;
default:
break;
}
};
class PanelGroupItem extends Component {
static propTypes = {
item: PropTypes.shape({
count: PropTypes.number.isRequired,
title_description: PropTypes.string,
display_text: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
name: PropTypes.string,
label_type: PropTypes.string,
}).isRequired,
onLabelClick: PropTypes.func,
isSelected: PropTypes.bool,
statusLabels: statusLabelsInterface,
type: PropTypes.string,
};
displayCount = () => {
const { item, statusLabels, type } = this.props;
if (type !== "status") {
return item.count;
}
if (statusLabels.loading_counts) {
return "";
}
return statusLabels[`${item.id}_count`];
};
render() {
const { displayCount } = this;
const { item, onLabelClick, isSelected } = this.props;
const { display_text: displayText, type, label_type, name } = item;
const wrapperClassName = classnames(
baseClass,
"button",
"button--contextual-nav-item",
`${baseClass}__${type.toLowerCase()}`,
`${baseClass}__${type.toLowerCase()}--${displayText
.toLowerCase()
.replace(" ", "-")}`,
{
[`${baseClass}--selected`]: isSelected,
}
);
return (
<button className={wrapperClassName} onClick={onLabelClick}>
<div className={`${baseClass}__flexy`}>
<span className={`${baseClass}__name`}>
{label_type === "builtin" && displayIcon(name)}&nbsp;
{displayText}
</span>
<span className={`${baseClass}__count`}>{displayCount()}</span>
</div>
</button>
);
}
}
export default PanelGroupItem;

View file

@ -1,66 +0,0 @@
import React from "react";
import { mount } from "enzyme";
import PanelGroupItem from "./PanelGroupItem";
describe("PanelGroupItem - component", () => {
const id = 0;
const validPanelGroupItem = {
count: 20,
display_text: "All Hosts",
type: "all",
id,
};
const validStatusGroupItem = {
count: 111,
display_text: "Online Hosts",
id: "online",
type: "status",
};
const statusLabels = {
online_count: 20,
loading_counts: false,
};
const loadingStatusLabels = {
online_count: 20,
loading_counts: true,
};
const labelComponent = mount(
<PanelGroupItem item={validPanelGroupItem} statusLabels={statusLabels} />
);
const statusLabelComponent = mount(
<PanelGroupItem
item={validStatusGroupItem}
statusLabels={statusLabels}
type="status"
/>
);
const loadingStatusLabelComponent = mount(
<PanelGroupItem
item={validStatusGroupItem}
statusLabels={loadingStatusLabels}
type="status"
/>
);
it("renders the item text", () => {
expect(labelComponent.text()).toContain(validPanelGroupItem.display_text);
});
it("renders the item count", () => {
expect(labelComponent.text()).toContain(validPanelGroupItem.count);
expect(statusLabelComponent.text()).not.toContain(
validStatusGroupItem.count
);
expect(statusLabelComponent.text()).toContain(statusLabels.online_count);
expect(loadingStatusLabelComponent.text()).not.toContain(
statusLabels.online_count
);
expect(loadingStatusLabelComponent.text()).not.toContain(
validPanelGroupItem.count
);
});
});

View file

@ -0,0 +1,74 @@
import React from "react";
import classnames from "classnames";
import { ILabel } from "interfaces/label";
import { PLATFORM_LABEL_DISPLAY_NAMES } from "utilities/constants";
import darwinIcon from "../../../../../assets/images/icon-darwin-fleet-black-16x16@2x.png";
import linuxIcon from "../../../../../assets/images/icon-linux-fleet-black-16x16@2x.png";
import ubuntuIcon from "../../../../../assets/images/icon-ubuntu-fleet-black-16x16@2x.png";
import centosIcon from "../../../../../assets/images/icon-centos-fleet-black-16x16@2x.png";
import windowsIcon from "../../../../../assets/images/icon-windows-fleet-black-16x16@2x.png";
const baseClass = "panel-group-item";
const displayName = (name: string) => {
return PLATFORM_LABEL_DISPLAY_NAMES[name] || name;
};
const displayIcon = (name: string) => {
switch (name) {
case "macOS":
return <img src={darwinIcon} alt="Apple icon" />;
case "Linux":
return <img src={linuxIcon} alt="Linux icon" />;
case "Ubuntu Linux":
return <img src={ubuntuIcon} alt="Ubuntu icon" />;
case "CentOS Linux":
return <img src={centosIcon} alt="Centos icon" />;
case "Windows":
return <img src={windowsIcon} alt="Windows icon" />;
default:
return null;
}
};
interface IPanelGroupItemProps {
item: ILabel;
onLabelClick: () => void;
isSelected: boolean;
}
const PanelGroupItem = (props: IPanelGroupItemProps): JSX.Element => {
const { item, onLabelClick, isSelected } = props;
const {
count,
display_text: displayText,
label_type: labelType,
name,
} = item;
const wrapperClassName = classnames(
baseClass,
"button",
"button--contextual-nav-item",
`${baseClass}__${displayText.toLowerCase().replace(" ", "-")}`,
{
[`${baseClass}--selected`]: isSelected,
}
);
return (
<button className={wrapperClassName} onClick={onLabelClick}>
<div className={`${baseClass}__flexy`}>
<span className={`${baseClass}__name`}>
{labelType === "builtin" && displayIcon(displayName(name))}
&nbsp;
{displayName(name)}
</span>
<span className={`${baseClass}__count`}>{count}</span>
</div>
</button>
);
};
export default PanelGroupItem;

View file

@ -24,6 +24,11 @@
color: $core-vibrant-blue !important;
align-self: right;
span {
display: flex;
align-items: center;
}
img {
margin-left: $pad-xsmall;
}

View file

@ -4,6 +4,7 @@ import moment from "moment";
import yaml from "js-yaml";
import stringUtils from "utilities/strings";
import { ITeam } from "interfaces/team";
import { PLATFORM_LABEL_DISPLAY_TYPES } from "utilities/constants";
const ORG_INFO_ATTRS = ["org_name", "org_logo_url"];
const ADMIN_ATTRS = ["email", "name", "password", "password_confirmation"];
@ -176,20 +177,11 @@ export const frontendFormattedConfig = (config: any) => {
};
const formatLabelResponse = (response: any): { [index: string]: any } => {
const labelTypeForDisplayText: { [index: string]: any } = {
"All Hosts": "all",
"MS Windows": "platform",
"CentOS Linux": "platform",
macOS: "platform",
"Ubuntu Linux": "platform",
"Red Hat Linux": "platform",
};
const labels = response.labels.map((label: any) => {
return {
...label,
slug: labelSlug(label),
type: labelTypeForDisplayText[label.display_text] || "custom",
type: PLATFORM_LABEL_DISPLAY_TYPES[label.display_text] || "custom",
};
});

View file

@ -19,11 +19,15 @@ export interface ILabel {
updated_at: string;
id: number | string;
name: string;
// description: string;
query: string;
label_type: string;
label_type: "regular" | "builtin";
label_membership_type: string;
hosts_count: number;
display_text: string;
count: number; // seems to be a repeat of hosts_count issue #1618
host_ids: number[] | null;
type: "custom" | "platform" | "status";
// slug: string; // e.g., "labels/13" | "online"
// target_type: string; // e.g., "labels"
}

View file

@ -1,5 +1,12 @@
import PropTypes from "prop-types";
export interface IStatusLabels {
loading_counts: boolean;
new_count: number;
online_count: number;
offline_count: number;
mia_count: number;
}
export default PropTypes.shape({
loading_counts: PropTypes.bool,
new_count: PropTypes.number,

View file

@ -33,7 +33,10 @@ import policiesClient from "services/entities/policies";
import permissionUtils from "utilities/permissions";
import sortUtils from "utilities/sort";
import { PolicyResponse } from "utilities/constants";
import {
PLATFORM_LABEL_DISPLAY_NAMES,
PolicyResponse,
} from "utilities/constants";
import { getNextLocationPath } from "./helpers";
import {
defaultHiddenColumns,
@ -126,8 +129,11 @@ export class ManageHostsPage extends PureComponent {
isOnGlobalTeam: PropTypes.bool,
isBasicTier: PropTypes.bool,
currentUser: userInterface,
policyId: PropTypes.number,
policyResponse: PolicyResponse,
policyId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
policyResponse: PropTypes.oneOf([
PolicyResponse.PASSING,
PolicyResponse.FAILING,
]),
};
static defaultProps = {
@ -1036,15 +1042,17 @@ export class ManageHostsPage extends PureComponent {
renderHeaderLabelBlock = ({
description,
display_text: displayText,
type,
label_type: labelType,
}) => {
const { onEditLabelClick, toggleDeleteLabelModal } = this;
displayText = PLATFORM_LABEL_DISPLAY_NAMES[displayText] || displayText;
return (
<div className={`${baseClass}__label-block`}>
<div className="title">
<span>{displayText}</span>
{type !== "platform" && (
{labelType !== "builtin" && (
<>
<Button onClick={onEditLabelClick} variant={"text-icon"}>
<img src={PencilIcon} alt="Edit label" />
@ -1091,6 +1099,7 @@ export class ManageHostsPage extends PureComponent {
</div>
);
}
return null;
};
renderForm = () => {

View file

@ -11,13 +11,6 @@ export const FREQUENCY_DROPDOWN_OPTIONS = [
{ value: 604800, label: "Every week" },
];
export const PLATFORM_OPTIONS = [
{ label: "All", value: "" },
{ label: "Windows", value: "windows" },
{ label: "Linux", value: "linux" },
{ label: "macOS", value: "darwin" },
];
export const LOGGING_TYPE_OPTIONS = [
{ label: "Snapshot", value: "snapshot" },
{ label: "Differential", value: "differential" },
@ -54,3 +47,39 @@ export const MIN_OSQUERY_VERSION_OPTIONS = [
{ label: "1.8.2 +", value: "1.8.2" },
{ label: "1.8.1 +", value: "1.8.1" },
];
export const PLATFORM_LABEL_DISPLAY_NAMES: Record<string, string> = {
"All Hosts": "All Hosts",
"All Linux": "Linux",
"CentOS Linux": "CentOS Linux",
macOS: "macOS",
"MS Windows": "Windows",
"Red Hat Linux": "Red Hat Linux",
"Ubuntu Linux": "Ubuntu Linux",
};
export const PLATFORM_LABEL_DISPLAY_ORDER = [
"macOS",
"All Linux",
"CentOS Linux",
"Red Hat Linux",
"Ubuntu Linux",
"MS Windows",
];
export const PLATFORM_LABEL_DISPLAY_TYPES: Record<string, string> = {
"All Hosts": "all",
"All Linux": "platform",
"CentOS Linux": "platform",
macOS: "platform",
"MS Windows": "platform",
"Red Hat Linux": "platform",
"Ubuntu Linux": "platform",
};
export const PLATFORM_OPTIONS = [
{ label: "All", value: "" },
{ label: "Windows", value: "windows" },
{ label: "Linux", value: "linux" },
{ label: "macOS", value: "darwin" },
];