From aa60f3a54f37fd35a870a44697b8edc8e4cdd131 Mon Sep 17 00:00:00 2001 From: Gabriel Hernandez Date: Fri, 5 Aug 2022 14:27:10 +0100 Subject: [PATCH] cleanup of manage host page utilities (#7055) * colocate LabelForm with ManageHostPage * add url utility to create query string from params object --- .../hosts/ManageHostsPage/ManageHostsPage.tsx | 7 +- .../pages/hosts/ManageHostsPage/constants.ts | 34 ++++++++ .../pages/hosts/ManageHostsPage/helpers.ts | 84 +++++-------------- .../hosts/components}/LabelForm/LabelForm.tsx | 0 .../hosts/components}/LabelForm/_styles.scss | 0 .../hosts/components}/LabelForm/index.ts | 0 frontend/services/entities/macadmins.ts | 9 +- .../services/entities/operating_systems.ts | 18 ++-- frontend/services/entities/software.ts | 54 ++++++------ frontend/utilities/url/index.ts | 41 +++++++++ frontend/utilities/url/url.tests.ts | 42 ++++++++++ 11 files changed, 174 insertions(+), 115 deletions(-) create mode 100644 frontend/pages/hosts/ManageHostsPage/constants.ts rename frontend/{components/forms => pages/hosts/components}/LabelForm/LabelForm.tsx (100%) rename frontend/{components/forms => pages/hosts/components}/LabelForm/_styles.scss (100%) rename frontend/{components/forms => pages/hosts/components}/LabelForm/index.ts (100%) create mode 100644 frontend/utilities/url/index.ts create mode 100644 frontend/utilities/url/url.tests.ts diff --git a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx index 0c01677bcf..f355452f91 100644 --- a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx +++ b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx @@ -49,7 +49,6 @@ import Button from "components/buttons/Button"; // @ts-ignore import Dropdown from "components/forms/fields/Dropdown"; import HostSidePanel from "components/side_panels/HostSidePanel"; -import LabelForm from "components/forms/LabelForm"; import QuerySidePanel from "components/side_panels/QuerySidePanel"; import TableContainer from "components/TableContainer"; import TableDataError from "components/DataError"; @@ -73,10 +72,10 @@ import { DEFAULT_SORT_HEADER, DEFAULT_SORT_DIRECTION, HOST_SELECT_STATUSES, - isAcceptableStatus, - getNextLocationPath, -} from "./helpers"; +} 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"; diff --git a/frontend/pages/hosts/ManageHostsPage/constants.ts b/frontend/pages/hosts/ManageHostsPage/constants.ts new file mode 100644 index 0000000000..a2a83ae5ab --- /dev/null +++ b/frontend/pages/hosts/ManageHostsPage/constants.ts @@ -0,0 +1,34 @@ +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/"; + +export const DEFAULT_SORT_HEADER = "hostname"; +export const DEFAULT_SORT_DIRECTION = "asc"; + +export const HOST_SELECT_STATUSES = [ + { + disabled: false, + label: "All hosts", + value: ALL_HOSTS_LABEL, + helpText: "All hosts that have been enrolled to Fleet.", + }, + { + disabled: false, + label: "Online hosts", + value: "online", + helpText: "Hosts that have recently checked in to Fleet.", + }, + { + disabled: false, + label: "Offline hosts", + value: "offline", + helpText: "Hosts that have not checked in to Fleet recently.", + }, + { + disabled: false, + label: "New hosts", + value: "new", + helpText: "Hosts that have been enrolled to Fleet in the last 24 hours.", + }, +]; diff --git a/frontend/pages/hosts/ManageHostsPage/helpers.ts b/frontend/pages/hosts/ManageHostsPage/helpers.ts index 6ae4ccd02d..4e613c0a8e 100644 --- a/frontend/pages/hosts/ManageHostsPage/helpers.ts +++ b/frontend/pages/hosts/ManageHostsPage/helpers.ts @@ -1,4 +1,5 @@ -import { isString, isPlainObject, isEmpty, reduce, trim, union } from "lodash"; +import { isEmpty, reduce, trim, union } from "lodash"; +import { buildQueryStringFromParams } from "utilities/url"; interface ILocationParams { pathPrefix?: string; @@ -7,40 +8,7 @@ interface ILocationParams { queryParams?: { [key: string]: string | number }; } -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/"; - -export const DEFAULT_SORT_HEADER = "hostname"; -export const DEFAULT_SORT_DIRECTION = "asc"; - -export const HOST_SELECT_STATUSES = [ - { - disabled: false, - label: "All hosts", - value: ALL_HOSTS_LABEL, - helpText: "All hosts that have been enrolled to Fleet.", - }, - { - disabled: false, - label: "Online hosts", - value: "online", - helpText: "Hosts that have recently checked in to Fleet.", - }, - { - disabled: false, - label: "Offline hosts", - value: "offline", - helpText: "Hosts that have not checked in to Fleet recently.", - }, - { - disabled: false, - label: "New hosts", - value: "new", - helpText: "Hosts that have been enrolled to Fleet in the last 24 hours.", - }, -]; +type RouteParams = Record; export const isAcceptableStatus = (filter: string): boolean => { return filter === "new" || filter === "online" || filter === "offline"; @@ -61,43 +29,31 @@ export const isValidPemCertificate = (cert: string): boolean => { return regexPemHeader.test(cert) && regexPemFooter.test(cert); }; +const createRouteString = (routeTemplate: string, routeParams: RouteParams) => { + let routeString = ""; + if (!isEmpty(routeParams)) { + routeString = reduce( + routeParams, + (string, value, key) => { + return string.replace(`:${key}`, encodeURIComponent(value)); + }, + routeTemplate + ); + } + return routeString; +}; + export const getNextLocationPath = ({ pathPrefix = "", routeTemplate = "", routeParams = {}, queryParams = {}, }: ILocationParams): string => { - const pathPrefixFinal = isString(pathPrefix) ? pathPrefix : ""; - const routeTemplateFinal = (isString(routeTemplate) && routeTemplate) || ""; - const routeParamsFinal = isPlainObject(routeParams) ? routeParams : {}; - const queryParamsFinal = isPlainObject(queryParams) ? queryParams : {}; - - let routeString = ""; - - if (!isEmpty(routeParamsFinal)) { - routeString = reduce( - routeParamsFinal, - (string, value, key) => { - return string.replace(`:${key}`, encodeURIComponent(value)); - }, - routeTemplateFinal - ); - } - - let queryString = ""; - if (!isEmpty(queryParamsFinal)) { - queryString = reduce( - queryParamsFinal, - (arr: string[], value, key) => { - key && arr.push(`${key}=${encodeURIComponent(value)}`); - return arr; - }, - [] - ).join("&"); - } + const routeString = createRouteString(routeTemplate, routeParams); + const queryString = buildQueryStringFromParams(queryParams); const nextLocation = union( - trim(pathPrefixFinal, "/").split("/"), + trim(pathPrefix, "/").split("/"), routeString.split("/") ).join("/"); diff --git a/frontend/components/forms/LabelForm/LabelForm.tsx b/frontend/pages/hosts/components/LabelForm/LabelForm.tsx similarity index 100% rename from frontend/components/forms/LabelForm/LabelForm.tsx rename to frontend/pages/hosts/components/LabelForm/LabelForm.tsx diff --git a/frontend/components/forms/LabelForm/_styles.scss b/frontend/pages/hosts/components/LabelForm/_styles.scss similarity index 100% rename from frontend/components/forms/LabelForm/_styles.scss rename to frontend/pages/hosts/components/LabelForm/_styles.scss diff --git a/frontend/components/forms/LabelForm/index.ts b/frontend/pages/hosts/components/LabelForm/index.ts similarity index 100% rename from frontend/components/forms/LabelForm/index.ts rename to frontend/pages/hosts/components/LabelForm/index.ts diff --git a/frontend/services/entities/macadmins.ts b/frontend/services/entities/macadmins.ts index 008bc1904a..31bdb5c7c5 100644 --- a/frontend/services/entities/macadmins.ts +++ b/frontend/services/entities/macadmins.ts @@ -1,16 +1,13 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import sendRequest from "services"; import endpoints from "utilities/endpoints"; +import { buildQueryStringFromParams } from "utilities/url"; export default { loadAll: (teamId?: number) => { const { MACADMINS } = endpoints; - let path = MACADMINS; - - if (teamId) { - path += `?team_id=${teamId}`; - } - + const queryString = buildQueryStringFromParams({ team_id: teamId }); + const path = `${MACADMINS}?${queryString}`; return sendRequest("GET", path); }, }; diff --git a/frontend/services/entities/operating_systems.ts b/frontend/services/entities/operating_systems.ts index 723810d8d9..fd3aba1579 100644 --- a/frontend/services/entities/operating_systems.ts +++ b/frontend/services/entities/operating_systems.ts @@ -3,13 +3,14 @@ import sendRequest from "services"; import endpoints from "utilities/endpoints"; import { IOperatingSystemVersion } from "interfaces/operating_system"; import { IOsqueryPlatform } from "interfaces/platform"; +import { buildQueryStringFromParams } from "utilities/url"; export interface IOperatingSystemsResponse { counts_updated_at: string; os_versions: IOperatingSystemVersion[]; } -interface IGetOperatingSystemProps { +interface IGetVersionParams { platform: IOsqueryPlatform; teamId?: number; } @@ -18,18 +19,11 @@ export default { getVersions: async ({ platform, teamId, - }: IGetOperatingSystemProps): Promise => { + }: IGetVersionParams): Promise => { const { OS_VERSIONS } = endpoints; - let path = OS_VERSIONS; - - const queryParams = [`platform=${platform}`]; - - if (teamId) { - queryParams.push(`team_id=${teamId}`); - } - - const queryString = `?${queryParams.join("&")}`; - path += queryString; + const queryParams = { platform, team_id: teamId }; + const queryString = buildQueryStringFromParams(queryParams); + const path = `${OS_VERSIONS}?${queryString}`; try { return sendRequest("GET", path); diff --git a/frontend/services/entities/software.ts b/frontend/services/entities/software.ts index 4426d8b109..a7a60a2147 100644 --- a/frontend/services/entities/software.ts +++ b/frontend/services/entities/software.ts @@ -1,8 +1,9 @@ -import { snakeCase } from "lodash"; +import { snakeCase, reduce } from "lodash"; import sendRequest from "services"; import endpoints from "utilities/endpoints"; import { ISoftware } from "interfaces/software"; +import { buildQueryStringFromParams, QueryParams } from "utilities/url"; interface IGetSoftwareProps { page?: number; @@ -32,19 +33,15 @@ type ISoftwareParams = Partial; const ORDER_KEY = "name"; const ORDER_DIRECTION = "asc"; -const buildQueryStringFromParams = (params: ISoftwareParams) => { - const filteredParams = Object.entries(params).filter( - ([key, value]) => !!value +const convertParamsToSnakeCase = (params: ISoftwareParams) => { + return reduce( + params, + (result, val, key) => { + result[snakeCase(key)] = val; + return result; + }, + {} ); - if (!filteredParams.length) { - return ""; - } - return `?${filteredParams - .map( - ([key, value]) => - `${encodeURIComponent(snakeCase(key))}=${encodeURIComponent(value)}` - ) - .join("&")}`; }; export default { @@ -58,21 +55,19 @@ export default { teamId, }: ISoftwareParams): Promise => { const { SOFTWARE } = endpoints; - const pagination = perPage ? `page=${page}&per_page=${perPage}` : ""; - const sort = `order_key=${orderKey}&order_direction=${orderDir}`; - let path = `${SOFTWARE}?${pagination}&${sort}`; + const queryParams = { + page, + perPage, + orderKey, + orderDirection: orderDir, + teamId, + query, + vulnerable, + }; - if (teamId) { - path += `&team_id=${teamId}`; - } - - if (query) { - path += `&query=${encodeURIComponent(query)}`; - } - - if (vulnerable) { - path += `&vulnerable=${vulnerable}`; - } + const snakeCaseParams = convertParamsToSnakeCase(queryParams); + const queryString = buildQueryStringFromParams(snakeCaseParams); + const path = `${SOFTWARE}?${queryString}`; try { return sendRequest("GET", path); @@ -84,9 +79,10 @@ export default { count: async (params: ISoftwareParams): Promise => { const { SOFTWARE } = endpoints; const path = `${SOFTWARE}/count`; - const queryString = buildQueryStringFromParams(params); + const snakeCaseParams = convertParamsToSnakeCase(params); + const queryString = buildQueryStringFromParams(snakeCaseParams); - return sendRequest("GET", path.concat(queryString)); + return sendRequest("GET", path.concat(`?${queryString}`)); }, getSoftwareById: async ( diff --git a/frontend/utilities/url/index.ts b/frontend/utilities/url/index.ts new file mode 100644 index 0000000000..c6edfac76f --- /dev/null +++ b/frontend/utilities/url/index.ts @@ -0,0 +1,41 @@ +import { isEmpty, reduce, omitBy, Dictionary } from "lodash"; + +type QueryValues = string | number | boolean | undefined | null; +export type QueryParams = Record; +type FilteredQueryValues = string | number | boolean; +type FilteredQueryParams = Record; + +const reduceQueryParams = ( + params: string[], + value: FilteredQueryValues, + key: string +) => { + key && params.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`); + return params; +}; + +const filterEmptyParams = (queryParams: QueryParams) => { + return omitBy( + queryParams, + (value) => value === undefined || value === "" || value === null + ) as Dictionary; +}; + +/** + * creates a query string from a query params object. If a value is undefined, null, + * or an empty string on the queryParams object, that key-value pair will be + * excluded from the query string. + */ +export const buildQueryStringFromParams = (queryParams: QueryParams) => { + const filteredParams = filterEmptyParams(queryParams); + + let queryString = ""; + if (!isEmpty(queryParams)) { + queryString = reduce( + filteredParams, + reduceQueryParams, + [] + ).join("&"); + } + return queryString; +}; diff --git a/frontend/utilities/url/url.tests.ts b/frontend/utilities/url/url.tests.ts new file mode 100644 index 0000000000..bf2e8f1f1d --- /dev/null +++ b/frontend/utilities/url/url.tests.ts @@ -0,0 +1,42 @@ +import { buildQueryStringFromParams } from "."; + +describe("url utilites", () => { + it("creates a query string from a params object", () => { + const params = { + query: "test", + page: 1, + order: "asc", + isNew: true, + }; + expect(buildQueryStringFromParams(params)).toBe( + "query=test&page=1&order=asc&isNew=true" + ); + }); + + it("filters out undefined values", () => { + const params = { + query: undefined, + page: 1, + order: "asc", + }; + expect(buildQueryStringFromParams(params)).toBe("page=1&order=asc"); + }); + + it("filters out empty string values", () => { + const params = { + query: "", + page: 1, + order: "asc", + }; + expect(buildQueryStringFromParams(params)).toBe("page=1&order=asc"); + }); + + it("filters out null values", () => { + const params = { + query: null, + page: 1, + order: "asc", + }; + expect(buildQueryStringFromParams(params)).toBe("page=1&order=asc"); + }); +});