Fleet UI: Fix bug with label sidebar not collapsing (#7402)

This commit is contained in:
RachelElysia 2022-09-01 14:42:25 -07:00 committed by GitHub
parent 4930616bbb
commit 3e310ba150
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 351 additions and 260 deletions

View File

@ -0,0 +1 @@
* Bug fix to collapse label sidebar

View File

@ -0,0 +1,32 @@
import { useCallback, useState } from "react";
interface IUseToggleSidePanelHook {
isSidePanelOpen: boolean;
toggleSidePanel: () => void;
setSidePanelOpen: (isOpen: boolean) => void;
}
const useToggleSidePanel = (
initialIsOpened: boolean
): IUseToggleSidePanelHook => {
const [isSidePanelOpen, setIsOpen] = useState<boolean>(initialIsOpened);
const toggleSidePanel = useCallback(() => {
setIsOpen(!isSidePanelOpen);
}, [setIsOpen, isSidePanelOpen]);
const setSidePanelOpen = useCallback(
(isOpen: boolean) => {
setIsOpen(isOpen);
},
[setIsOpen]
);
return {
isSidePanelOpen,
toggleSidePanel,
setSidePanelOpen,
};
};
export default useToggleSidePanel;

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import React, { useState, useContext, useEffect } from "react";
import { IAceEditor } from "react-ace/lib/types";
import { noop, size } from "lodash";
import { useDebouncedCallback } from "use-debounce";
@ -12,16 +12,19 @@ import InputField from "components/forms/fields/InputField";
import FleetAce from "components/FleetAce";
// @ts-ignore
import validateQuery from "components/forms/validators/validate_query";
import InfoIcon from "../../../../assets/images/icon-info-purple-14x14@2x.png";
interface ILabelFormProps {
baseError: string;
selectedLabel?: ILabel;
isEdit?: boolean;
isUpdatingLabel?: boolean;
onCancel: () => void;
handleSubmit: (formData: ILabelFormData) => void;
onOsqueryTableSelect?: (tableName: string) => void;
onOpenSchemaSidebar: () => void;
onOsqueryTableSelect: (tableName: string) => void;
showOpenSchemaActionText: boolean;
backendValidators: { [key: string]: string };
isUpdatingLabel?: boolean;
}
const baseClass = "label-form";
@ -57,11 +60,13 @@ const LabelForm = ({
baseError,
selectedLabel,
isEdit,
isUpdatingLabel,
onCancel,
handleSubmit,
onOpenSchemaSidebar,
onOsqueryTableSelect,
showOpenSchemaActionText,
backendValidators,
isUpdatingLabel,
}: ILabelFormProps): JSX.Element => {
const [name, setName] = useState(selectedLabel?.name || "");
const [nameError, setNameError] = useState("");
@ -156,6 +161,21 @@ const LabelForm = ({
});
};
const renderLabelComponent = (): JSX.Element | null => {
if (!showOpenSchemaActionText) {
return null;
}
return (
<Button variant="text-icon" onClick={onOpenSchemaSidebar}>
<>
<img alt="" src={InfoIcon} />
Show schema
</>
</Button>
);
};
const isBuiltin =
selectedLabel &&
(selectedLabel.label_type === "builtin" || selectedLabel.type === "status");
@ -190,6 +210,7 @@ const LabelForm = ({
onChange={onQueryChange}
value={query}
label="SQL"
labelActionComponent={renderLabelComponent()}
onLoad={onLoad}
readOnly={isEdit}
wrapperClassName={`${baseClass}__text-editor-wrapper`}

View File

@ -0,0 +1,218 @@
import React, { useState, useContext, useEffect } from "react";
import { useQuery } from "react-query";
import { InjectedRouter, Params } from "react-router/lib/Router";
import PATHS from "router/paths";
import MainContent from "components/MainContent";
import SidePanelContent from "components/SidePanelContent";
import QuerySidePanel from "components/side_panels/QuerySidePanel";
import Spinner from "components/Spinner";
import { QueryContext } from "context/query";
import { NotificationContext } from "context/notification";
import { IApiError } from "interfaces/errors";
import { ILabel, ILabelFormData } from "interfaces/label";
import labelsAPI, { ILabelsResponse } from "services/entities/labels";
import deepDifference from "utilities/deep_difference";
import useToggleSidePanel from "hooks/useToggleSidePanel";
import LabelForm from "./LabelForm";
const baseClass = "label-page";
interface ILabelPageProps {
router: InjectedRouter;
params: Params;
location: {
pathname: string;
};
}
const DEFAULT_CREATE_LABEL_ERRORS = {
name: "",
};
const LabelPage = ({
router,
params,
location,
}: ILabelPageProps): JSX.Element | null => {
const isEditLabel = !location.pathname.includes("new");
const [selectedLabel, setSelectedLabel] = useState<ILabel>();
const { isSidePanelOpen, setSidePanelOpen } = useToggleSidePanel(true);
const [showOpenSchemaActionText, setShowOpenSchemaActionText] = useState(
false
);
const [labelValidator, setLabelValidator] = useState<{
[key: string]: string;
}>(DEFAULT_CREATE_LABEL_ERRORS);
const [isUpdatingLabel, setIsUpdatingLabel] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const { selectedOsqueryTable, setSelectedOsqueryTable } = useContext(
QueryContext
);
const { renderFlash } = useContext(NotificationContext);
const { data: labels, error: labelsError } = useQuery<
ILabelsResponse,
Error,
ILabel[]
>(["labels"], () => labelsAPI.loadAll(), {
select: (data: ILabelsResponse) => data.labels,
onSuccess: (responseLabels: ILabel[]) => {
if (params.label_id) {
const selectLabel = responseLabels.find(
(label) => label.id === parseInt(params.label_id, 10)
);
setSelectedLabel(selectLabel);
setIsLoading(false);
}
},
});
const onCloseSchemaSidebar = () => {
setSidePanelOpen(false);
setShowOpenSchemaActionText(true);
};
const onOpenSchemaSidebar = () => {
setSidePanelOpen(true);
setShowOpenSchemaActionText(false);
};
const onOsqueryTableSelect = (tableName: string) => {
setSelectedOsqueryTable(tableName);
};
const onEditLabel = (formData: ILabelFormData) => {
if (!selectedLabel) {
console.error("Label isn't available. This should not happen.");
return;
}
setIsUpdatingLabel(true);
const updateAttrs = deepDifference(formData, selectedLabel);
labelsAPI
.update(selectedLabel, updateAttrs)
.then(() => {
router.push(PATHS.MANAGE_HOSTS_LABEL(selectedLabel.id));
renderFlash(
"success",
"Label updated. Try refreshing this page in just a moment to see the updated host count for your label."
);
})
.catch((updateError: { data: IApiError }) => {
if (updateError.data.errors[0].reason.includes("Duplicate")) {
setLabelValidator({
name: "A label with this name already exists",
});
} else if (
updateError.data.errors[0].reason.includes(
"Data too long for column 'name'"
)
) {
setLabelValidator({
name: "Label name is too long",
});
} else if (
updateError.data.errors[0].reason.includes(
"Data too long for column 'description'"
)
) {
setLabelValidator({
description: "Label description is too long",
});
} else {
renderFlash("error", "Could not create label. Please try again.");
}
})
.finally(() => {
setIsUpdatingLabel(false);
});
};
const onAddLabel = (formData: ILabelFormData) => {
setIsUpdatingLabel(true);
labelsAPI
.create(formData)
.then((label: ILabel) => {
router.push(PATHS.MANAGE_HOSTS_LABEL(label.id));
renderFlash(
"success",
"Label created. Try refreshing this page in just a moment to see the updated host count for your label."
);
})
.catch((updateError: any) => {
if (updateError.data.errors[0].reason.includes("Duplicate")) {
setLabelValidator({
name: "A label with this name already exists",
});
} else if (
updateError.data.errors[0].reason.includes(
"Data too long for column 'name'"
)
) {
setLabelValidator({
name: "Label name is too long",
});
} else if (
updateError.data.errors[0].reason.includes(
"Data too long for column 'description'"
)
) {
setLabelValidator({
description: "Label description is too long",
});
} else {
renderFlash("error", "Could not create label. Please try again.");
}
})
.finally(() => {
setIsUpdatingLabel(false);
});
};
const onCancelLabel = () => {
router.goBack();
};
return (
<>
<MainContent className={baseClass}>
<div className={`${baseClass}__wrapper`}>
{isLoading ? (
<Spinner />
) : (
<LabelForm
selectedLabel={selectedLabel}
onCancel={onCancelLabel}
isEdit={isEditLabel}
isUpdatingLabel={isUpdatingLabel}
handleSubmit={isEditLabel ? onEditLabel : onAddLabel}
onOpenSchemaSidebar={onOpenSchemaSidebar}
onOsqueryTableSelect={onOsqueryTableSelect}
baseError={labelsError?.message || ""}
backendValidators={labelValidator}
showOpenSchemaActionText={showOpenSchemaActionText}
/>
)}
</div>
</MainContent>
{isSidePanelOpen && !isEditLabel && (
<SidePanelContent>
<QuerySidePanel
key="query-side-panel"
onOsqueryTableSelect={onOsqueryTableSelect}
selectedOsqueryTable={selectedOsqueryTable}
onClose={onCloseSchemaSidebar}
/>
</SidePanelContent>
)}
</>
);
};
export default LabelPage;

View File

@ -0,0 +1,14 @@
.label-page {
&__sandboxMode {
margin-top: 70px;
}
h1 {
margin: 0 0 $pad-xlarge;
}
h2 {
font-size: $x-small;
font-weight: $bold;
}
}

View File

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

View File

@ -26,25 +26,21 @@ import {
import PATHS from "router/paths";
import { AppContext } from "context/app";
import { QueryContext } from "context/query";
import { TableContext } from "context/table";
import { NotificationContext } from "context/notification";
import {
IEnrollSecret,
IEnrollSecretsResponse,
} from "interfaces/enroll_secret";
import { IApiError } from "interfaces/errors";
import { IHost } from "interfaces/host";
import { ILabel, ILabelFormData } from "interfaces/label";
import { ILabel } from "interfaces/label";
import { IMDMSolution } from "interfaces/macadmins";
import { IOperatingSystemVersion } from "interfaces/operating_system";
import { IPolicy } from "interfaces/policy";
import { ISoftware } from "interfaces/software";
import { ITeam } from "interfaces/team";
import deepDifference from "utilities/deep_difference";
import sortUtils from "utilities/sort";
import {
DEFAULT_CREATE_LABEL_ERRORS,
HOSTS_SEARCH_BOX_PLACEHOLDER,
HOSTS_SEARCH_BOX_TOOLTIP,
PLATFORM_LABEL_DISPLAY_NAMES,
@ -54,14 +50,12 @@ import {
import Button from "components/buttons/Button";
// @ts-ignore
import Dropdown from "components/forms/fields/Dropdown";
import QuerySidePanel from "components/side_panels/QuerySidePanel";
import TableContainer from "components/TableContainer";
import TableDataError from "components/DataError";
import { IActionButtonProps } from "components/TableContainer/DataTable/ActionButton";
import TeamsDropdown from "components/TeamsDropdown";
import Spinner from "components/Spinner";
import MainContent from "components/MainContent";
import SidePanelContent from "components/SidePanelContent";
import { getValidatedTeamId } from "utilities/helpers";
import {
@ -70,8 +64,6 @@ import {
generateAvailableTableHeaders,
} from "./HostTableConfig";
import {
NEW_LABEL_HASH,
EDIT_LABEL_HASH,
ALL_HOSTS_LABEL,
LABEL_SLUG_PREFIX,
DEFAULT_SORT_HEADER,
@ -80,8 +72,6 @@ import {
HOST_SELECT_STATUSES,
} from "./constants";
import { isAcceptableStatus, getNextLocationPath } from "./helpers";
import LabelForm from "./components/LabelForm";
import DeleteSecretModal from "../../../components/DeleteSecretModal";
import SecretEditorModal from "../../../components/SecretEditorModal";
import AddHostsModal from "../../../components/AddHostsModal";
@ -166,10 +156,6 @@ const ManageHostsPage = ({
});
}
}
const { selectedOsqueryTable, setSelectedOsqueryTable } = useContext(
QueryContext
);
const { setResetSelectedRows } = useContext(TableContext);
const hostHiddenColumns = localStorage.getItem("hostHiddenColumns");
@ -242,18 +228,13 @@ const ManageHostsPage = ({
currentQueryOptions,
setCurrentQueryOptions,
] = useState<ILoadHostsOptions>();
const [labelValidator, setLabelValidator] = useState<{
[key: string]: string;
}>(DEFAULT_CREATE_LABEL_ERRORS);
const [resetPageIndex, setResetPageIndex] = useState(false);
const [isUpdatingLabel, setIsUpdatingLabel] = useState(false);
const [isUpdatingSecret, setIsUpdatingSecret] = useState(false);
const [isUpdatingHosts, setIsUpdatingHosts] = useState(false);
const [resetPageIndex, setResetPageIndex] = useState<boolean>(false);
const [isUpdatingLabel, setIsUpdatingLabel] = useState<boolean>(false);
const [isUpdatingSecret, setIsUpdatingSecret] = useState<boolean>(false);
const [isUpdatingHosts, setIsUpdatingHosts] = useState<boolean>(false);
// ======== end states
const isAddLabel = location.hash === NEW_LABEL_HASH;
const isEditLabel = location.hash === EDIT_LABEL_HASH;
const routeTemplate = route?.path ?? "";
const policyId = queryParams?.policy_id;
const policyResponse: PolicyResponse = queryParams?.policy_response;
@ -747,17 +728,12 @@ const ManageHostsPage = ({
};
const onAddLabelClick = () => {
setLabelValidator(DEFAULT_CREATE_LABEL_ERRORS);
router.push(`${PATHS.MANAGE_HOSTS}${NEW_LABEL_HASH}`);
router.push(`${PATHS.NEW_LABEL}`);
};
const onEditLabelClick = (evt: React.MouseEvent<HTMLButtonElement>) => {
evt.preventDefault();
setLabelValidator(DEFAULT_CREATE_LABEL_ERRORS);
router.push(
`${PATHS.MANAGE_HOSTS}/${getLabelSelected()}${EDIT_LABEL_HASH}`
);
router.push(`${PATHS.EDIT_LABEL(parseInt(labelID, 10))}`);
};
const onSaveColumns = (newHiddenColumns: string[]) => {
@ -766,10 +742,6 @@ const ManageHostsPage = ({
setShowEditColumnsModal(false);
};
const onCancelLabel = () => {
router.goBack();
};
// NOTE: used to reset page number to 0 when modifying filters
useEffect(() => {
setResetPageIndex(false);
@ -985,102 +957,6 @@ const ManageHostsPage = ({
}
};
const onEditLabel = (formData: ILabelFormData) => {
if (!selectedLabel) {
console.error("Label isn't available. This should not happen.");
return;
}
const updateAttrs = deepDifference(formData, selectedLabel);
setIsUpdatingLabel(true);
labelsAPI
.update(selectedLabel, updateAttrs)
.then(() => {
refetchLabels();
renderFlash(
"success",
"Label updated. Try refreshing this page in just a moment to see the updated host count for your label."
);
setLabelValidator({});
})
.catch((updateError: { data: IApiError }) => {
if (updateError.data.errors[0].reason.includes("Duplicate")) {
setLabelValidator({
name: "A label with this name already exists",
});
} else if (
updateError.data.errors[0].reason.includes(
"Data too long for column 'name'"
)
) {
setLabelValidator({
name: "Label name is too long",
});
} else if (
updateError.data.errors[0].reason.includes(
"Data too long for column 'description'"
)
) {
setLabelValidator({
description: "Label description is too long",
});
} else {
renderFlash("error", "Could not create label. Please try again.");
}
})
.finally(() => {
setIsUpdatingLabel(false);
});
};
const onOsqueryTableSelect = (tableName: string) => {
setSelectedOsqueryTable(tableName);
};
const onSaveAddLabel = (formData: ILabelFormData) => {
setIsUpdatingLabel(true);
labelsAPI
.create(formData)
.then(() => {
router.push(PATHS.MANAGE_HOSTS);
renderFlash(
"success",
"Label created. Try refreshing this page in just a moment to see the updated host count for your label."
);
setLabelValidator({});
refetchLabels();
})
.catch((updateError: any) => {
if (updateError.data.errors[0].reason.includes("Duplicate")) {
setLabelValidator({
name: "A label with this name already exists",
});
} else if (
updateError.data.errors[0].reason.includes(
"Data too long for column 'name'"
)
) {
setLabelValidator({
name: "Label name is too long",
});
} else if (
updateError.data.errors[0].reason.includes(
"Data too long for column 'description'"
)
) {
setLabelValidator({
description: "Label description is too long",
});
} else {
renderFlash("error", "Could not create label. Please try again.");
}
})
.finally(() => {
setIsUpdatingLabel(false);
});
};
const onClearLabelFilter = () => {
const allHostsLabel = labels?.find((label) => label.name === "All Hosts");
if (allHostsLabel !== undefined) {
@ -1724,38 +1600,6 @@ const ManageHostsPage = ({
return null;
};
const renderForm = () => {
if (isAddLabel) {
return (
<LabelForm
onCancel={onCancelLabel}
onOsqueryTableSelect={onOsqueryTableSelect}
handleSubmit={onSaveAddLabel}
baseError={labelsError?.message || ""}
backendValidators={labelValidator}
isUpdatingLabel={isUpdatingLabel}
/>
);
}
if (isEditLabel) {
return (
<LabelForm
selectedLabel={selectedLabel}
onCancel={onCancelLabel}
onOsqueryTableSelect={onOsqueryTableSelect}
handleSubmit={onEditLabel}
baseError={labelsError?.message || ""}
backendValidators={labelValidator}
isUpdatingLabel={isUpdatingLabel}
isEdit
/>
);
}
return false;
};
const renderCustomControls = () => {
// we filter out the status labels as we dont want to display them in the label
// filter select dropdown.
@ -1932,63 +1776,47 @@ const ManageHostsPage = ({
return (
<>
<MainContent>
<>
{renderForm()}
{!isAddLabel && !isEditLabel && (
<div className={`${baseClass}`}>
<div className="header-wrap">
{renderHeader()}
<div className={`${baseClass} button-wrap`}>
{!isSandboxMode &&
canEnrollHosts &&
!hasHostErrors &&
!hasHostCountErrors && (
<Button
onClick={() => setShowEnrollSecretModal(true)}
className={`${baseClass}__enroll-hosts button`}
variant="inverse"
>
<span>Manage enroll secret</span>
</Button>
)}
{canEnrollHosts &&
!hasHostErrors &&
!hasHostCountErrors &&
!(
getStatusSelected() === ALL_HOSTS_LABEL &&
selectedLabel?.count === 0
) &&
!(
getStatusSelected() === ALL_HOSTS_LABEL &&
filteredHostCount === 0
) && (
<Button
variant="brand"
onClick={toggleAddHostsModal}
className={`${baseClass}__add-hosts`}
>
<span>Add hosts</span>
</Button>
)}
</div>
</div>
{renderActiveFilterBlock()}
{renderNoEnrollSecretBanner()}
{renderTable()}
<div className={`${baseClass}`}>
<div className="header-wrap">
{renderHeader()}
<div className={`${baseClass} button-wrap`}>
{!isSandboxMode &&
canEnrollHosts &&
!hasHostErrors &&
!hasHostCountErrors && (
<Button
onClick={() => setShowEnrollSecretModal(true)}
className={`${baseClass}__enroll-hosts button`}
variant="inverse"
>
<span>Manage enroll secret</span>
</Button>
)}
{canEnrollHosts &&
!hasHostErrors &&
!hasHostCountErrors &&
!(
getStatusSelected() === ALL_HOSTS_LABEL &&
selectedLabel?.count === 0
) &&
!(
getStatusSelected() === ALL_HOSTS_LABEL &&
filteredHostCount === 0
) && (
<Button
onClick={toggleAddHostsModal}
className={`${baseClass}__add-hosts button button--brand`}
>
<span>Add hosts</span>
</Button>
)}
</div>
)}
</>
</div>
{renderActiveFilterBlock()}
{renderNoEnrollSecretBanner()}
{renderTable()}
</div>
</MainContent>
{isAddLabel && (
<SidePanelContent>
<QuerySidePanel
key="query-side-panel"
onOsqueryTableSelect={onOsqueryTableSelect}
selectedOsqueryTable={selectedOsqueryTable}
/>
</SidePanelContent>
)}
{canEnrollHosts && showDeleteSecretModal && renderDeleteSecretModal()}
{canEnrollHosts && showSecretEditorModal && renderSecretEditorModal()}
{canEnrollHosts && showEnrollSecretModal && renderEnrollSecretModal()}

View File

@ -1,5 +1,3 @@
export const NEW_LABEL_HASH = "#new_label";
export const EDIT_LABEL_HASH = "#edit_label";
export const ALL_HOSTS_LABEL = "all-hosts";
export const LABEL_SLUG_PREFIX = "labels/";

View File

@ -24,6 +24,7 @@ import EmailTokenRedirect from "components/EmailTokenRedirect";
import ForgotPasswordPage from "pages/ForgotPasswordPage";
import HostDetailsPage from "pages/hosts/details/HostDetailsPage";
import Homepage from "pages/Homepage";
import LabelPage from "pages/LabelPage";
import LoginPage, { LoginPreviewPage } from "pages/LoginPage";
import LogoutPage from "pages/LogoutPage";
import ManageHostsPage from "pages/hosts/ManageHostsPage";
@ -120,6 +121,11 @@ const routes = (
<Route path="options" component={AgentOptionsPage} />
</Route>
</Route>
<Route path="labels">
<IndexRedirect to={"new"} />
<Route path=":label_id" component={LabelPage} />
<Route path="new" component={LabelPage} />
</Route>
<Route path="hosts">
<IndexRedirect to={"manage"} />
<Route path="manage" component={ManageHostsPage} />

View File

@ -25,6 +25,9 @@ export default {
PACK: (packId: number): string => {
return `${URL_PREFIX}/packs/${packId}`;
},
EDIT_LABEL: (labelId: number): string => {
return `${URL_PREFIX}/labels/${labelId}`;
},
EDIT_QUERY: (query: IQuery): string => {
return `${URL_PREFIX}/queries/${query.id}`;
},
@ -39,6 +42,9 @@ export default {
LOGIN: `${URL_PREFIX}/login`,
LOGOUT: `${URL_PREFIX}/logout`,
MANAGE_HOSTS: `${URL_PREFIX}/hosts/manage`,
MANAGE_HOSTS_LABEL: (labelId: number | string): string => {
return `${URL_PREFIX}/hosts/manage/labels/${labelId}`;
},
HOST_DETAILS: (host: IHost): string => {
return `${URL_PREFIX}/hosts/${host.id}`;
},
@ -63,6 +69,7 @@ export default {
return `${URL_PREFIX}/schedule/manage/teams/${teamId}`;
},
MANAGE_POLICIES: `${URL_PREFIX}/policies/manage`,
NEW_LABEL: `${URL_PREFIX}/labels/new`,
NEW_POLICY: `${URL_PREFIX}/policies/new`,
NEW_QUERY: `${URL_PREFIX}/queries/new`,
RESET_PASSWORD: `${URL_PREFIX}/login/reset`,

View File

@ -566,7 +566,3 @@ export const DEFAULT_CREATE_USER_ERRORS = {
password: "",
sso_enabled: null,
};
export const DEFAULT_CREATE_LABEL_ERRORS = {
name: "",
};

View File

@ -59,37 +59,6 @@ const labelSlug = (label: ILabel): string => {
return `labels/${id}`;
};
const labelStubs = [
{
id: "new",
count: 0,
description: "Hosts that have been enrolled to Fleet in the last 24 hours.",
display_text: "New",
slug: "new",
statusLabelKey: "new_count",
title_description: "(added in last 24hrs)",
type: "status",
},
{
id: "online",
count: 0,
description: "Hosts that have recently checked-in to Fleet.",
display_text: "Online",
slug: "online",
statusLabelKey: "online_count",
type: "status",
},
{
id: "offline",
count: 0,
description: "Hosts that have not checked-in to Fleet recently.",
display_text: "Offline",
slug: "offline",
statusLabelKey: "offline_count",
type: "status",
},
];
const isLabel = (target: ISelectTargetsEntity) => {
return "label_type" in target;
};
@ -250,7 +219,7 @@ const formatLabelResponse = (response: any): ILabel[] => {
};
});
return labels.concat(labelStubs);
return labels;
};
export const formatSelectedTargetsForApi = (