mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 00:49:03 +00:00
cleanup of manage host page utilities (#7055)
* colocate LabelForm with ManageHostPage * add url utility to create query string from params object
This commit is contained in:
parent
4ae4eabf31
commit
aa60f3a54f
11 changed files with 174 additions and 115 deletions
|
|
@ -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";
|
||||
|
|
|
|||
34
frontend/pages/hosts/ManageHostsPage/constants.ts
Normal file
34
frontend/pages/hosts/ManageHostsPage/constants.ts
Normal file
|
|
@ -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.",
|
||||
},
|
||||
];
|
||||
|
|
@ -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<string, string>;
|
||||
|
||||
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("/");
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<IOperatingSystemsResponse> => {
|
||||
}: IGetVersionParams): Promise<IOperatingSystemsResponse> => {
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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<IGetSoftwareProps>;
|
|||
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<typeof params, QueryParams>(
|
||||
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<ISoftwareResponse> => {
|
||||
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<ISoftwareCountResponse> => {
|
||||
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 (
|
||||
|
|
|
|||
41
frontend/utilities/url/index.ts
Normal file
41
frontend/utilities/url/index.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { isEmpty, reduce, omitBy, Dictionary } from "lodash";
|
||||
|
||||
type QueryValues = string | number | boolean | undefined | null;
|
||||
export type QueryParams = Record<string, QueryValues>;
|
||||
type FilteredQueryValues = string | number | boolean;
|
||||
type FilteredQueryParams = Record<string, FilteredQueryValues>;
|
||||
|
||||
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<FilteredQueryValues>;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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<FilteredQueryParams, string[]>(
|
||||
filteredParams,
|
||||
reduceQueryParams,
|
||||
[]
|
||||
).join("&");
|
||||
}
|
||||
return queryString;
|
||||
};
|
||||
42
frontend/utilities/url/url.tests.ts
Normal file
42
frontend/utilities/url/url.tests.ts
Normal file
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue