diff --git a/changes/1792-all-linux-label b/changes/1792-all-linux-label new file mode 100644 index 0000000000..49f3c485c2 --- /dev/null +++ b/changes/1792-all-linux-label @@ -0,0 +1 @@ +Fix display of platform labels on manage hosts page diff --git a/frontend/components/side_panels/HostSidePanel/HostSidePanel.jsx b/frontend/components/side_panels/HostSidePanel/HostSidePanel.jsx deleted file mode 100644 index 6dc3b3a8d1..0000000000 --- a/frontend/components/side_panels/HostSidePanel/HostSidePanel.jsx +++ /dev/null @@ -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 ( - - - -

Operating systems

- -
-
-

Labels

-
-
- {canAddNewLabel && ( - - )} -
-
-
- -
- -
- ); - } -} - -export default HostSidePanel; diff --git a/frontend/components/side_panels/HostSidePanel/HostSidePanel.tsx b/frontend/components/side_panels/HostSidePanel/HostSidePanel.tsx new file mode 100644 index 0000000000..e8e0b52e59 --- /dev/null +++ b/frontend/components/side_panels/HostSidePanel/HostSidePanel.tsx @@ -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) => 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(""); + + 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 ( + + + +

Operating systems

+ +
+
+

Labels

+
+
+ {canAddNewLabel && ( + + )} +
+
+
+ +
+ +
+ ); +}; + +export default HostSidePanel; diff --git a/frontend/components/side_panels/HostSidePanel/PanelGroup/PanelGroup.jsx b/frontend/components/side_panels/HostSidePanel/PanelGroup/PanelGroup.jsx index 6743d33abb..f65241c4dc 100644 --- a/frontend/components/side_panels/HostSidePanel/PanelGroup/PanelGroup.jsx +++ b/frontend/components/side_panels/HostSidePanel/PanelGroup/PanelGroup.jsx @@ -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 ( { 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(); @@ -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); }); }); diff --git a/frontend/components/side_panels/HostSidePanel/PanelGroupItem/PanelGroupItem.jsx b/frontend/components/side_panels/HostSidePanel/PanelGroupItem/PanelGroupItem.jsx deleted file mode 100644 index 70619030d1..0000000000 --- a/frontend/components/side_panels/HostSidePanel/PanelGroupItem/PanelGroupItem.jsx +++ /dev/null @@ -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 Apple icon; - case "Linux": - return Linux icon; - case "Ubuntu Linux": - return Ubuntu icon; - case "CentOS Linux": - return Centos icon; - case "Windows": - return 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 ( - - ); - } -} - -export default PanelGroupItem; diff --git a/frontend/components/side_panels/HostSidePanel/PanelGroupItem/PanelGroupItem.tests.jsx b/frontend/components/side_panels/HostSidePanel/PanelGroupItem/PanelGroupItem.tests.jsx deleted file mode 100644 index a79912a011..0000000000 --- a/frontend/components/side_panels/HostSidePanel/PanelGroupItem/PanelGroupItem.tests.jsx +++ /dev/null @@ -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( - - ); - - const statusLabelComponent = mount( - - ); - - const loadingStatusLabelComponent = mount( - - ); - - 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 - ); - }); -}); diff --git a/frontend/components/side_panels/HostSidePanel/PanelGroupItem/PanelGroupItem.tsx b/frontend/components/side_panels/HostSidePanel/PanelGroupItem/PanelGroupItem.tsx new file mode 100644 index 0000000000..4e257a6d70 --- /dev/null +++ b/frontend/components/side_panels/HostSidePanel/PanelGroupItem/PanelGroupItem.tsx @@ -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 Apple icon; + case "Linux": + return Linux icon; + case "Ubuntu Linux": + return Ubuntu icon; + case "CentOS Linux": + return Centos icon; + case "Windows": + return 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 ( + + ); +}; + +export default PanelGroupItem; diff --git a/frontend/components/side_panels/HostSidePanel/PanelGroupItem/index.js b/frontend/components/side_panels/HostSidePanel/PanelGroupItem/index.ts similarity index 100% rename from frontend/components/side_panels/HostSidePanel/PanelGroupItem/index.js rename to frontend/components/side_panels/HostSidePanel/PanelGroupItem/index.ts diff --git a/frontend/components/side_panels/HostSidePanel/_styles.scss b/frontend/components/side_panels/HostSidePanel/_styles.scss index 57ef421461..182a3275fb 100644 --- a/frontend/components/side_panels/HostSidePanel/_styles.scss +++ b/frontend/components/side_panels/HostSidePanel/_styles.scss @@ -24,6 +24,11 @@ color: $core-vibrant-blue !important; align-self: right; + span { + display: flex; + align-items: center; + } + img { margin-left: $pad-xsmall; } diff --git a/frontend/components/side_panels/HostSidePanel/index.js b/frontend/components/side_panels/HostSidePanel/index.ts similarity index 100% rename from frontend/components/side_panels/HostSidePanel/index.js rename to frontend/components/side_panels/HostSidePanel/index.ts diff --git a/frontend/fleet/helpers.ts b/frontend/fleet/helpers.ts index 7864d77c9c..6bb09b4839 100644 --- a/frontend/fleet/helpers.ts +++ b/frontend/fleet/helpers.ts @@ -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", }; }); diff --git a/frontend/interfaces/label.ts b/frontend/interfaces/label.ts index 06da9a7639..6895daba45 100644 --- a/frontend/interfaces/label.ts +++ b/frontend/interfaces/label.ts @@ -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" } diff --git a/frontend/interfaces/status_labels.js b/frontend/interfaces/status_labels.ts similarity index 60% rename from frontend/interfaces/status_labels.js rename to frontend/interfaces/status_labels.ts index 5f29ccd9aa..293e900eca 100644 --- a/frontend/interfaces/status_labels.js +++ b/frontend/interfaces/status_labels.ts @@ -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, diff --git a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.jsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.jsx index 85efae6100..0d74bff8e4 100644 --- a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.jsx +++ b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.jsx @@ -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 (
{displayText} - {type !== "platform" && ( + {labelType !== "builtin" && ( <>
); } + return null; }; renderForm = () => { diff --git a/frontend/utilities/constants.ts b/frontend/utilities/constants.ts index f8eb0fcf98..7181900f97 100644 --- a/frontend/utilities/constants.ts +++ b/frontend/utilities/constants.ts @@ -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 = { + "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 = { + "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" }, +];