mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Add new feature: Policies (#1772)
This commit is contained in:
parent
247267daa5
commit
452392f5d2
42 changed files with 1642 additions and 54 deletions
Binary file not shown.
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 444 B |
BIN
assets/images/icon-main-policies-16x16@2x.png
Normal file
BIN
assets/images/icon-main-policies-16x16@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 581 B |
1
changes/1790-policies
Normal file
1
changes/1790-policies
Normal file
|
|
@ -0,0 +1 @@
|
|||
Add new policies feature
|
||||
107
cypress/integration/all/app/policiesflow.spec.ts
Normal file
107
cypress/integration/all/app/policiesflow.spec.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
// describe(
|
||||
// "Policies flow",
|
||||
// {
|
||||
// defaultCommandTimeout: 20000,
|
||||
// },
|
||||
// () => {
|
||||
// beforeEach(() => {
|
||||
// cy.setup();
|
||||
// cy.login();
|
||||
// cy.seedQueries();
|
||||
// });
|
||||
// it("Can create, check, and delete a policy successfully", () => {
|
||||
// cy.intercept({
|
||||
// method: "GET",
|
||||
// url: "/api/v1/fleet/global/policies",
|
||||
// }).as("getPolicies");
|
||||
// cy.intercept({
|
||||
// method: "GET",
|
||||
// url: "/api/v1/fleet/config",
|
||||
// }).as("getConfig");
|
||||
|
||||
// cy.visit("/policies/manage");
|
||||
|
||||
// // wait for state of policy table to settle otherwise re-renders cause elements to detach and tests will fail
|
||||
// cy.wait("@getPolicies");
|
||||
// cy.wait("@getConfig");
|
||||
|
||||
// cy.wait(1000); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||
|
||||
// // Add a policy
|
||||
// cy.get(".no-policies__inner")
|
||||
// .findByText(/add a policy/i)
|
||||
// .should("exist")
|
||||
// .click();
|
||||
// cy.get(".add-policy-modal").within(() => {
|
||||
// cy.findByText(/select query/i)
|
||||
// .should("exist")
|
||||
// .click();
|
||||
// cy.findByText(
|
||||
// /Detect Linux hosts with high severity vulnerable versions of OpenSSL/i
|
||||
// ).click();
|
||||
// cy.findByRole("button", { name: /cancel/i }).should("exist");
|
||||
// cy.findByRole("button", { name: /add/i }).should("exist").click();
|
||||
// });
|
||||
|
||||
// // Confirm that policy was added successfully
|
||||
// cy.findByText(/successfully added policy/i).should("exist");
|
||||
// cy.findByText(/select query/i).should("not.exist");
|
||||
// cy.get(".policies-list-wrapper").within(() => {
|
||||
// cy.findByText(/1 query/i).should("exist");
|
||||
// cy.findByText(/passing/i).should("exist");
|
||||
// cy.findByText(
|
||||
// /Detect Linux hosts with high severity vulnerable versions of OpenSSL/i
|
||||
// ).should("exist");
|
||||
|
||||
// // Click on link in table and confirm that policies filter block diplays as expected on manage hosts page
|
||||
// cy.get("tbody").within(() => {
|
||||
// cy.get("tr")
|
||||
// .first()
|
||||
// .within(() => {
|
||||
// cy.get("td").last().children().first().should("exist").click();
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
// cy.get(".manage-hosts__policies-filter-block").within(() => {
|
||||
// cy.findByText(
|
||||
// /Detect Linux hosts with high severity vulnerable versions of OpenSSL/i
|
||||
// ).should("exist");
|
||||
// cy.findByText(/passing/i).should("not.exist");
|
||||
// cy.findByText(/failing/i)
|
||||
// .should("exist")
|
||||
// .click();
|
||||
// cy.findByText(/passing/i).should("exist");
|
||||
// cy.get('img[alt="Remove policy filter"]').click();
|
||||
// cy.findByText(
|
||||
// /Detect Linux hosts with high severity vulnerable versions of OpenSSL/i
|
||||
// ).should("not.exist");
|
||||
// });
|
||||
|
||||
// // Click on policies tab to return to manage policies page
|
||||
// cy.get(".site-nav-container").within(() => {
|
||||
// cy.findByText(/policies/i)
|
||||
// .should("exist")
|
||||
// .click();
|
||||
// });
|
||||
|
||||
// // Delete policy
|
||||
// cy.get("tbody").within(() => {
|
||||
// cy.get("tr")
|
||||
// .first()
|
||||
// .within(() => {
|
||||
// cy.get(".fleet-checkbox__input").check({ force: true });
|
||||
// });
|
||||
// });
|
||||
// cy.findByRole("button", { name: /remove/i }).click();
|
||||
// cy.get(".remove-policies-modal").within(() => {
|
||||
// cy.findByRole("button", { name: /cancel/i }).should("exist");
|
||||
// cy.findByRole("button", { name: /remove/i })
|
||||
// .should("exist")
|
||||
// .click();
|
||||
// });
|
||||
// cy.findByText(
|
||||
// /Detect Linux hosts with high severity vulnerable versions of OpenSSL/i
|
||||
// ).should("not.exist");
|
||||
// });
|
||||
// }
|
||||
// );
|
||||
|
|
@ -71,6 +71,14 @@ Cypress.Commands.add("seedQueries", () => {
|
|||
description: "List authorized_keys for each user on the system.",
|
||||
observer_can_run: false,
|
||||
},
|
||||
{
|
||||
name:
|
||||
"Detect Linux hosts with high severity vulnerable versions of OpenSSL",
|
||||
query:
|
||||
"SELECT name AS name, version AS version, 'deb_packages' AS source FROM deb_packages WHERE name LIKE 'openssl%' UNION SELECT name AS name, version AS version, 'apt_sources' AS source FROM apt_sources WHERE name LIKE 'openssl%' UNION SELECT name AS name, version AS version, 'rpm_packages' AS source FROM rpm_packages WHERE name LIKE 'openssl%';",
|
||||
description: "Retrieves the OpenSSL version.",
|
||||
observer_can_run: false,
|
||||
},
|
||||
];
|
||||
|
||||
queries.forEach((queryForm) => {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ interface ITableContainerProps {
|
|||
actionButtonIcon?: string;
|
||||
actionButtonVariant?: string;
|
||||
onQueryChange: (queryData: ITableQueryData) => void;
|
||||
inputPlaceHolder: string;
|
||||
inputPlaceHolder?: string;
|
||||
disableActionButton?: boolean;
|
||||
resultsTitle: string;
|
||||
additionalQueries?: string;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
import React from "react";
|
||||
|
||||
const PoliciesPageWrapper = (props: {
|
||||
children: React.ReactNode;
|
||||
}): React.ReactNode | null => {
|
||||
const { children } = props;
|
||||
|
||||
return children || null;
|
||||
};
|
||||
|
||||
export default PoliciesPageWrapper;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./PoliciesPageWrapper";
|
||||
|
|
@ -12,6 +12,7 @@ import navItems from "./navItems";
|
|||
import HostsIcon from "../../../../assets/images/icon-main-hosts@2x-16x16@2x.png";
|
||||
import QueriesIcon from "../../../../assets/images/icon-main-queries@2x-16x16@2x.png";
|
||||
import PacksIcon from "../../../../assets/images/icon-main-packs@2x-16x16@2x.png";
|
||||
import PoliciesIcon from "../../../../assets/images/icon-main-policies-16x16@2x.png";
|
||||
import AdminIcon from "../../../../assets/images/icon-main-settings@2x-16x16@2x.png";
|
||||
|
||||
class SiteTopNav extends Component {
|
||||
|
|
@ -62,6 +63,14 @@ class SiteTopNav extends Component {
|
|||
icon = (
|
||||
<img src={PacksIcon} alt={`${iconName} icon`} className={iconClasses} />
|
||||
);
|
||||
else if (iconName === "policies")
|
||||
icon = (
|
||||
<img
|
||||
src={PoliciesIcon}
|
||||
alt={`${iconName} icon`}
|
||||
className={iconClasses}
|
||||
/>
|
||||
);
|
||||
else if (iconName === "settings")
|
||||
icon = (
|
||||
<img src={AdminIcon} alt={`${iconName} icon`} className={iconClasses} />
|
||||
|
|
|
|||
|
|
@ -55,6 +55,15 @@ export default (currentUser) => {
|
|||
pathname: PATHS.MANAGE_SCHEDULE,
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: "policies",
|
||||
name: "Policies",
|
||||
iconName: "policies",
|
||||
location: {
|
||||
regex: new RegExp(`^${URL_PREFIX}/(policies)/`),
|
||||
pathname: PATHS.MANAGE_POLICIES,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (permissionUtils.isGlobalAdmin(currentUser)) {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export default {
|
|||
PACKS: "/v1/fleet/packs",
|
||||
PERFORM_REQUIRED_PASSWORD_RESET: "/v1/fleet/perform_required_password_reset",
|
||||
QUERIES: "/v1/fleet/queries",
|
||||
POLICIES: "/v1/fleet/global/policies",
|
||||
RESET_PASSWORD: "/v1/fleet/reset_password",
|
||||
RUN_QUERY: "/v1/fleet/queries/run",
|
||||
SCHEDULED_QUERIES: "/v1/fleet/schedule",
|
||||
|
|
|
|||
|
|
@ -508,7 +508,7 @@ const inGigaBytes = (bytes: number): string => {
|
|||
return (bytes / BYTES_PER_GIGABYTE).toFixed(1);
|
||||
};
|
||||
|
||||
const inMilliseconds = (nanoseconds: number): number => {
|
||||
export const inMilliseconds = (nanoseconds: number): number => {
|
||||
return nanoseconds / NANOSECONDS_PER_MILLISECOND;
|
||||
};
|
||||
|
||||
|
|
@ -639,6 +639,7 @@ export default {
|
|||
humanHostDetailUpdated,
|
||||
hostTeamName,
|
||||
humanQueryLastRun,
|
||||
inMilliseconds,
|
||||
licenseExpirationWarning,
|
||||
secondsToHms,
|
||||
secondsToDhms,
|
||||
|
|
|
|||
7
frontend/interfaces/policy.ts
Normal file
7
frontend/interfaces/policy.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export interface IPolicy {
|
||||
id: number;
|
||||
query_id: number;
|
||||
query_name: string;
|
||||
passing_host_count: number;
|
||||
failing_host_count: number;
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ import React, { PureComponent } from "react";
|
|||
import PropTypes from "prop-types";
|
||||
import { connect } from "react-redux";
|
||||
import { push, goBack } from "react-router-redux";
|
||||
import { find, isEmpty, omit } from "lodash";
|
||||
import { find, isEmpty, memoize, omit } from "lodash";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
import Dropdown from "components/forms/fields/Dropdown";
|
||||
|
|
@ -28,8 +28,12 @@ import PATHS from "router/paths";
|
|||
import deepDifference from "utilities/deep_difference";
|
||||
|
||||
import hostClient from "services/entities/hosts";
|
||||
import policiesClient from "services/entities/policies";
|
||||
|
||||
import permissionUtils from "utilities/permissions";
|
||||
import sortUtils from "utilities/sort";
|
||||
|
||||
import { PolicyResponse } from "utilities/constants";
|
||||
import { getNextLocationPath } from "./helpers";
|
||||
import {
|
||||
defaultHiddenColumns,
|
||||
|
|
@ -40,11 +44,13 @@ import EnrollSecretModal from "./components/EnrollSecretModal";
|
|||
import AddHostModal from "./components/AddHostModal";
|
||||
import NoHosts from "./components/NoHosts";
|
||||
import EmptyHosts from "./components/EmptyHosts";
|
||||
import PoliciesFilter from "./components/PoliciesFilter";
|
||||
import EditColumnsModal from "./components/EditColumnsModal/EditColumnsModal";
|
||||
import TransferHostModal from "./components/TransferHostModal";
|
||||
import EditColumnsIcon from "../../../../assets/images/icon-edit-columns-16x16@2x.png";
|
||||
import PencilIcon from "../../../../assets/images/icon-pencil-14x14@2x.png";
|
||||
import TrashIcon from "../../../../assets/images/icon-trash-14x14@2x.png";
|
||||
import CloseIcon from "../../../../assets/images/icon-close-fleet-black-16x16@2x.png";
|
||||
|
||||
const baseClass = "manage-hosts";
|
||||
|
||||
|
|
@ -116,11 +122,12 @@ export class ManageHostsPage extends PureComponent {
|
|||
canEnrollHosts: PropTypes.bool,
|
||||
canAddNewLabels: PropTypes.bool,
|
||||
teams: PropTypes.arrayOf(teamInterface),
|
||||
loadingTeams: PropTypes.bool,
|
||||
isGlobalAdmin: PropTypes.bool,
|
||||
isOnGlobalTeam: PropTypes.bool,
|
||||
isBasicTier: PropTypes.bool,
|
||||
currentUser: userInterface,
|
||||
policyId: PropTypes.number,
|
||||
policyResponse: PolicyResponse,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
|
@ -171,21 +178,42 @@ export class ManageHostsPage extends PureComponent {
|
|||
sortBy: initialSortBy,
|
||||
isConfigLoaded: !isEmpty(this.props.config),
|
||||
isTeamsLoaded: !isEmpty(this.props.teams),
|
||||
isTeamsLoading: false,
|
||||
policyName: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { dispatch } = this.props;
|
||||
const { dispatch, policyId } = this.props;
|
||||
dispatch(getLabels());
|
||||
|
||||
if (policyId) {
|
||||
policiesClient
|
||||
.load(policyId)
|
||||
.then((response) => {
|
||||
const { query_name: policyName } = response.policy;
|
||||
this.setState({ policyName });
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
// dispatch(
|
||||
// renderFlash(
|
||||
// "error",
|
||||
// "Sorry, we could not retrieve the policy name."
|
||||
// )
|
||||
// );
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps() {
|
||||
const { config, dispatch, isBasicTier, loadingTeams } = this.props;
|
||||
const { isConfigLoaded, isTeamsLoaded } = this.state;
|
||||
const { config, dispatch, isBasicTier } = this.props;
|
||||
const { isConfigLoaded, isTeamsLoaded, isTeamsLoading } = this.state;
|
||||
if (!isConfigLoaded && !isEmpty(config)) {
|
||||
this.setState({ isConfigLoaded: true });
|
||||
}
|
||||
if (isConfigLoaded && isBasicTier && !isTeamsLoaded && !loadingTeams) {
|
||||
if (isConfigLoaded && isBasicTier && !isTeamsLoaded && !isTeamsLoading) {
|
||||
this.setState({ isTeamsLoading: true });
|
||||
dispatch(teamActions.loadAll({}))
|
||||
.then(() => {
|
||||
this.setState({
|
||||
|
|
@ -201,6 +229,11 @@ export class ManageHostsPage extends PureComponent {
|
|||
this.setState({
|
||||
isTeamsLoaded: false,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
this.setState({
|
||||
isTeamsLoading: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -278,6 +311,8 @@ export class ManageHostsPage extends PureComponent {
|
|||
const { getValidatedTeamId, retrieveHosts } = this;
|
||||
const {
|
||||
dispatch,
|
||||
policyId,
|
||||
policyResponse,
|
||||
routeTemplate,
|
||||
routeParams,
|
||||
selectedFilters,
|
||||
|
|
@ -307,7 +342,9 @@ export class ManageHostsPage extends PureComponent {
|
|||
selectedLabels: selectedFilters,
|
||||
globalFilter: searchQuery,
|
||||
sortBy,
|
||||
teamId,
|
||||
teamId: selectedTeam,
|
||||
policyId,
|
||||
policyResponse,
|
||||
});
|
||||
|
||||
// Rebuild queryParams to dispatch new browser location to react-router
|
||||
|
|
@ -328,6 +365,12 @@ export class ManageHostsPage extends PureComponent {
|
|||
if (teamId) {
|
||||
queryParams.team_id = teamId;
|
||||
}
|
||||
if (policyId) {
|
||||
queryParams.policy_id = policyId;
|
||||
}
|
||||
if (policyResponse) {
|
||||
queryParams.policy_response = policyResponse;
|
||||
}
|
||||
|
||||
dispatch(
|
||||
push(
|
||||
|
|
@ -458,6 +501,8 @@ export class ManageHostsPage extends PureComponent {
|
|||
selectedFilters,
|
||||
selectedLabel,
|
||||
selectedTeam: selectedTeamFilter,
|
||||
policyId,
|
||||
policyResponse,
|
||||
} = this.props;
|
||||
const {
|
||||
selectedHostIds,
|
||||
|
|
@ -498,6 +543,8 @@ export class ManageHostsPage extends PureComponent {
|
|||
globalFilter: searchQuery,
|
||||
sortBy,
|
||||
teamId: selectedTeamFilter,
|
||||
policyId,
|
||||
policyResponse,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
|
|
@ -521,6 +568,18 @@ export class ManageHostsPage extends PureComponent {
|
|||
return selectedFilters.find((f) => !f.includes(LABEL_SLUG_PREFIX));
|
||||
};
|
||||
|
||||
getSortedTeamOptions = memoize((teams) =>
|
||||
teams
|
||||
.map((team) => {
|
||||
return {
|
||||
disabled: false,
|
||||
label: team.name,
|
||||
value: team.id,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => sortUtils.caseInsensitiveAsc(b.label, a.label))
|
||||
);
|
||||
|
||||
getValidatedTeamId = (teamId) => {
|
||||
const { currentUser, isOnGlobalTeam, teams } = this.props;
|
||||
|
||||
|
|
@ -545,6 +604,7 @@ export class ManageHostsPage extends PureComponent {
|
|||
|
||||
generateTeamFilterDropdownOptions = (teams) => {
|
||||
const { currentUser, isOnGlobalTeam } = this.props;
|
||||
const { getSortedTeamOptions } = this;
|
||||
|
||||
let currentUserTeams = [];
|
||||
if (isOnGlobalTeam) {
|
||||
|
|
@ -561,26 +621,7 @@ export class ManageHostsPage extends PureComponent {
|
|||
},
|
||||
];
|
||||
|
||||
const sortedCurrentUserTeamOptions = currentUserTeams
|
||||
.map((team) => {
|
||||
return {
|
||||
disabled: false,
|
||||
label: team.name,
|
||||
value: team.id,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const labelA = a.label.toUpperCase();
|
||||
const labelB = b.label.toUpperCase();
|
||||
if (labelA < labelB) {
|
||||
return -1;
|
||||
}
|
||||
if (labelA > labelB) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0; // values are equal
|
||||
});
|
||||
const sortedCurrentUserTeamOptions = getSortedTeamOptions(currentUserTeams);
|
||||
|
||||
return allTeamsOption.concat(sortedCurrentUserTeamOptions);
|
||||
};
|
||||
|
|
@ -593,7 +634,7 @@ export class ManageHostsPage extends PureComponent {
|
|||
|
||||
options = {
|
||||
...options,
|
||||
team_id: getValidatedTeamId(options.teamId),
|
||||
teamId: getValidatedTeamId(options.teamId),
|
||||
};
|
||||
|
||||
try {
|
||||
|
|
@ -657,11 +698,79 @@ export class ManageHostsPage extends PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
handleChangePoliciesFilter = (policyResponse) => {
|
||||
const {
|
||||
dispatch,
|
||||
policyId,
|
||||
routeTemplate,
|
||||
routeParams,
|
||||
queryParams,
|
||||
selectedFilters,
|
||||
selectedTeam,
|
||||
} = this.props;
|
||||
const { searchQuery, sortBy } = this.state;
|
||||
const { retrieveHosts } = this;
|
||||
|
||||
retrieveHosts({
|
||||
globalFilter: searchQuery,
|
||||
policyId,
|
||||
policyResponse,
|
||||
selectedLabels: selectedFilters,
|
||||
sortBy,
|
||||
teamId: selectedTeam,
|
||||
});
|
||||
|
||||
dispatch(
|
||||
push(
|
||||
getNextLocationPath({
|
||||
pathPrefix: PATHS.MANAGE_HOSTS,
|
||||
routeTemplate,
|
||||
routeParams,
|
||||
queryParams: Object.assign({}, queryParams, {
|
||||
policy_id: policyId,
|
||||
policy_response: policyResponse,
|
||||
}),
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
handleClearPoliciesFilter = () => {
|
||||
const {
|
||||
dispatch,
|
||||
routeTemplate,
|
||||
routeParams,
|
||||
queryParams,
|
||||
selectedFilters,
|
||||
selectedTeam,
|
||||
} = this.props;
|
||||
const { searchQuery, sortBy } = this.state;
|
||||
const { retrieveHosts } = this;
|
||||
|
||||
retrieveHosts({
|
||||
globalFilter: searchQuery,
|
||||
selectedLabels: selectedFilters,
|
||||
sortBy,
|
||||
teamId: selectedTeam,
|
||||
});
|
||||
dispatch(
|
||||
push(
|
||||
getNextLocationPath({
|
||||
pathPrefix: PATHS.MANAGE_HOSTS,
|
||||
routeTemplate,
|
||||
routeParams,
|
||||
queryParams: omit(queryParams, ["policy_id", "policy_response"]),
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
// The handleChange method below is for the filter-by-team dropdown rather than the dropdown used in modals
|
||||
// TODO confirm that sort order, pagination work as expected
|
||||
handleChangeSelectedTeamFilter = (selectedTeam) => {
|
||||
const {
|
||||
dispatch,
|
||||
policyId,
|
||||
policyResponse,
|
||||
selectedFilters,
|
||||
routeTemplate,
|
||||
routeParams,
|
||||
|
|
@ -678,6 +787,8 @@ export class ManageHostsPage extends PureComponent {
|
|||
selectedLabels: selectedFilters,
|
||||
globalFilter: searchQuery,
|
||||
sortBy,
|
||||
policyId,
|
||||
policyResponse,
|
||||
};
|
||||
retrieveHosts(hostsOptions);
|
||||
|
||||
|
|
@ -693,8 +804,7 @@ export class ManageHostsPage extends PureComponent {
|
|||
};
|
||||
|
||||
handleLabelChange = ({ slug }) => {
|
||||
const { dispatch, selectedFilters, selectedTeam } = this.props;
|
||||
const { getValidatedTeamId } = this;
|
||||
const { dispatch, queryParams, selectedFilters } = this.props;
|
||||
const { MANAGE_HOSTS } = PATHS;
|
||||
const isAllHosts = slug === ALL_HOSTS_LABEL;
|
||||
const newFilters = [...selectedFilters];
|
||||
|
|
@ -720,15 +830,22 @@ export class ManageHostsPage extends PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
let nextLocation = isAllHosts
|
||||
? MANAGE_HOSTS
|
||||
: `${MANAGE_HOSTS}/${newFilters.join("/")}`;
|
||||
|
||||
const teamIdParam = getValidatedTeamId(selectedTeam);
|
||||
if (teamIdParam) {
|
||||
nextLocation += `?team_id=${teamIdParam}`;
|
||||
// Non-status labels are not compatible with policies so omit policy params from next location
|
||||
let newQueryParams = queryParams;
|
||||
if (newFilters.find((f) => f.includes(LABEL_SLUG_PREFIX))) {
|
||||
newQueryParams = omit(newQueryParams, ["policy_id", "policy_response"]);
|
||||
}
|
||||
dispatch(push(nextLocation));
|
||||
|
||||
dispatch(
|
||||
push(
|
||||
getNextLocationPath({
|
||||
pathPrefix: isAllHosts
|
||||
? MANAGE_HOSTS
|
||||
: `${MANAGE_HOSTS}/${newFilters.join("/")}`,
|
||||
queryParams: newQueryParams,
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
handleStatusDropdownChange = (statusName) => {
|
||||
|
|
@ -743,7 +860,6 @@ export class ManageHostsPage extends PureComponent {
|
|||
handleLabelChange(selected);
|
||||
};
|
||||
|
||||
// TODO revisit UX for server errors for invalid team_id (e.g., team_id=0, team_id=null, team_id=foo, etc.)
|
||||
renderTeamsFilterDropdown = () => {
|
||||
const { isBasicTier, selectedTeam, teams } = this.props;
|
||||
const { isConfigLoaded, isTeamsLoaded } = this.state;
|
||||
|
|
@ -780,6 +896,26 @@ export class ManageHostsPage extends PureComponent {
|
|||
);
|
||||
};
|
||||
|
||||
renderPoliciesFilterBlock = () => {
|
||||
const { policyId, policyResponse } = this.props;
|
||||
const { policyName } = this.state;
|
||||
const { handleClearPoliciesFilter, handleChangePoliciesFilter } = this;
|
||||
|
||||
return (
|
||||
<div className={`${baseClass}__policies-filter-block`}>
|
||||
<PoliciesFilter
|
||||
policyId={policyId}
|
||||
policyResponse={policyResponse}
|
||||
onChange={handleChangePoliciesFilter}
|
||||
/>
|
||||
<p>{policyName}</p>
|
||||
<Button onClick={handleClearPoliciesFilter} variant={"text-icon"}>
|
||||
<img src={CloseIcon} alt="Remove policy filter" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderEditColumnsModal = () => {
|
||||
const { config, currentUser } = this.props;
|
||||
const { showEditColumnsModal, hiddenColumns } = this.state;
|
||||
|
|
@ -927,20 +1063,34 @@ export class ManageHostsPage extends PureComponent {
|
|||
};
|
||||
|
||||
renderHeader = () => {
|
||||
const { renderHeaderLabelBlock, renderTeamsFilterDropdown } = this;
|
||||
const { selectedLabel } = this.props;
|
||||
const type = selectedLabel?.type;
|
||||
const { renderTeamsFilterDropdown } = this;
|
||||
|
||||
return (
|
||||
<div className={`${baseClass}__header`}>
|
||||
<div className={`${baseClass}__text`}>
|
||||
{renderTeamsFilterDropdown()}
|
||||
{type !== "all" &&
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderLabelOrPolicyBlock = () => {
|
||||
const { renderHeaderLabelBlock, renderPoliciesFilterBlock } = this;
|
||||
const { policyId, selectedLabel } = this.props;
|
||||
const type = selectedLabel?.type;
|
||||
|
||||
if (policyId || selectedLabel) {
|
||||
return (
|
||||
<div className={`${baseClass}__labels-policies-wrap`}>
|
||||
{policyId && renderPoliciesFilterBlock()}
|
||||
{!policyId &&
|
||||
type !== "all" &&
|
||||
type !== "status" &&
|
||||
selectedLabel &&
|
||||
renderHeaderLabelBlock(selectedLabel)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
renderForm = () => {
|
||||
|
|
@ -1111,6 +1261,7 @@ export class ManageHostsPage extends PureComponent {
|
|||
const {
|
||||
renderForm,
|
||||
renderHeader,
|
||||
renderLabelOrPolicyBlock,
|
||||
renderSidePanel,
|
||||
renderAddHostModal,
|
||||
renderEnrollSecretModal,
|
||||
|
|
@ -1158,6 +1309,7 @@ export class ManageHostsPage extends PureComponent {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
{renderLabelOrPolicyBlock()}
|
||||
{isConfigLoaded && (!isBasicTier || isTeamsLoaded) && renderTable()}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -1178,6 +1330,9 @@ const mapStateToProps = (state, ownProps) => {
|
|||
const queryParams = location.query;
|
||||
const routeTemplate = route && route.path ? route.path : "";
|
||||
|
||||
const policyId = queryParams?.policy_id;
|
||||
const policyResponse = queryParams?.policy_response;
|
||||
|
||||
const { active_label: activeLabel, label_id: labelID } = params;
|
||||
const selectedFilters = [];
|
||||
|
||||
|
|
@ -1208,7 +1363,6 @@ const mapStateToProps = (state, ownProps) => {
|
|||
const config = state.app.config;
|
||||
|
||||
const teams = memoizedGetEntity(state.entities.teams.data);
|
||||
const loadingTeams = state.entities.teams.loading;
|
||||
|
||||
// If there is no team_id, set selectedTeam to 0 so dropdown defaults to "All teams"
|
||||
const selectedTeam = location.query?.team_id || 0;
|
||||
|
|
@ -1252,8 +1406,9 @@ const mapStateToProps = (state, ownProps) => {
|
|||
isOnGlobalTeam,
|
||||
isBasicTier,
|
||||
teams,
|
||||
loadingTeams,
|
||||
selectedTeam,
|
||||
policyId,
|
||||
policyResponse,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@
|
|||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: $pad-medium;
|
||||
|
||||
.button-wrap {
|
||||
min-width: 266px;
|
||||
}
|
||||
}
|
||||
|
||||
.ace-fleet {
|
||||
|
|
@ -13,6 +17,7 @@
|
|||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 125px;
|
||||
|
||||
.form-field {
|
||||
margin-bottom: 0;
|
||||
|
|
@ -53,8 +58,6 @@
|
|||
}
|
||||
|
||||
&__label-block {
|
||||
margin-top: $pad-medium;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -96,6 +99,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.table-container {
|
||||
padding-top: $pad-small;
|
||||
}
|
||||
|
||||
.data-table-container {
|
||||
.data-table {
|
||||
&__wrapper {
|
||||
|
|
@ -153,9 +160,7 @@
|
|||
left: -12px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
&__header {
|
||||
.Select-control {
|
||||
background-color: #fff;
|
||||
border: 0 !important;
|
||||
|
|
@ -217,6 +222,21 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__labels-policies-wrap {
|
||||
margin-bottom: $pad-medium;
|
||||
}
|
||||
|
||||
&__policies-filter-block {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
p {
|
||||
font-size: $x-small;
|
||||
font-weight: $bold;
|
||||
padding-left: $pad-medium;
|
||||
}
|
||||
}
|
||||
|
||||
&__enroll-hosts {
|
||||
padding: $pad-small;
|
||||
margin-right: $pad-small;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
import React from "react";
|
||||
|
||||
// @ts-ignore
|
||||
import Dropdown from "components/forms/fields/Dropdown";
|
||||
import { PolicyResponse } from "utilities/constants";
|
||||
|
||||
interface IPoliciesFilterProps {
|
||||
policyResponse: PolicyResponse;
|
||||
onChange: (selectedFilter: PolicyResponse) => void;
|
||||
}
|
||||
|
||||
const baseClass = "policies-filter";
|
||||
|
||||
const POLICY_RESPONSE_OPTIONS = [
|
||||
{
|
||||
disabled: false,
|
||||
label: "Passing",
|
||||
value: PolicyResponse.PASSING,
|
||||
helpText: "Hosts that passed the last time they checked into Fleet.",
|
||||
},
|
||||
{
|
||||
disabled: false,
|
||||
label: "Failing",
|
||||
value: PolicyResponse.FAILING,
|
||||
helpText: "Hosts that failed the last time they checked into Fleet.",
|
||||
},
|
||||
];
|
||||
|
||||
const PoliciesFilter = (props: IPoliciesFilterProps): JSX.Element => {
|
||||
const { onChange, policyResponse } = props;
|
||||
const value = policyResponse;
|
||||
|
||||
return (
|
||||
<div className={`${baseClass}__policies-block`}>
|
||||
<Dropdown
|
||||
value={value}
|
||||
className={`${baseClass}__status_dropdown`}
|
||||
options={POLICY_RESPONSE_OPTIONS}
|
||||
searchable={false}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PoliciesFilter;
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
.policies-filter {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
&__status_dropdown {
|
||||
width: 159px;
|
||||
|
||||
.Select-menu-outer {
|
||||
width: 364px;
|
||||
max-height: 310px;
|
||||
|
||||
.Select-menu {
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./PoliciesFilter";
|
||||
|
|
@ -0,0 +1,262 @@
|
|||
import React, { useState, useCallback, useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
// @ts-ignore
|
||||
import { IConfig } from "interfaces/config";
|
||||
import { IQuery } from "interfaces/query";
|
||||
import { IPolicy } from "interfaces/policy";
|
||||
|
||||
import configAPI from "services/entities/config";
|
||||
import policiesAPI from "services/entities/policies";
|
||||
// @ts-ignore
|
||||
import queryActions from "redux/nodes/entities/queries/actions";
|
||||
// @ts-ignore
|
||||
import { renderFlash } from "redux/nodes/notifications/actions";
|
||||
|
||||
import { inMilliseconds, secondsToHms } from "fleet/helpers";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
import InfoBanner from "components/InfoBanner/InfoBanner";
|
||||
import PolicyError from "./components/PolicyError";
|
||||
import PoliciesListWrapper from "./components/PoliciesListWrapper";
|
||||
import AddPolicyModal from "./components/AddPolicyModal";
|
||||
import RemovePoliciesModal from "./components/RemovePoliciesModal";
|
||||
|
||||
const baseClass = "manage-policies-page";
|
||||
|
||||
const DOCS_LINK =
|
||||
"https://github.com/fleetdm/fleet/blob/fleet-v4.3.0/docs/2-Deploying/2-Configuration.md#osquery_detail_update_interval";
|
||||
interface IRootState {
|
||||
app: {
|
||||
config: IConfig;
|
||||
};
|
||||
entities: {
|
||||
queries: {
|
||||
isLoading: boolean;
|
||||
data: IQuery[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const renderTable = (
|
||||
policiesList: IPolicy[],
|
||||
isLoading: boolean,
|
||||
isLoadingError: boolean,
|
||||
onRemovePoliciesClick: (selectedTableIds: number[]) => void,
|
||||
toggleAddPolicyModal: () => void
|
||||
): JSX.Element => {
|
||||
if (isLoadingError) {
|
||||
return <PolicyError />;
|
||||
}
|
||||
|
||||
return (
|
||||
<PoliciesListWrapper
|
||||
policiesList={policiesList}
|
||||
isLoading={isLoading}
|
||||
onRemovePoliciesClick={onRemovePoliciesClick}
|
||||
toggleAddPolicyModal={toggleAddPolicyModal}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ManagePolicyPage = (): JSX.Element => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const queries = useSelector((state: IRootState) => state.entities.queries);
|
||||
const queriesList = Object.values(queries.data);
|
||||
|
||||
const [showAddPolicyModal, setShowAddPolicyModal] = useState(false);
|
||||
const [showRemovePoliciesModal, setShowRemovePoliciesModal] = useState(false);
|
||||
const [selectedIds, setSelectedIds] = useState<number[] | never[]>([]);
|
||||
|
||||
const [updateInterval, setUpdateInterval] = useState<string>(
|
||||
"update interval"
|
||||
);
|
||||
const [policies, setPolicies] = useState<IPolicy[] | never[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isLoadingError, setIsLoadingError] = useState(false);
|
||||
|
||||
const getPolicies = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await policiesAPI.loadAll();
|
||||
setPolicies(response.policies);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
dispatch(
|
||||
renderFlash("error", "Sorry, we could not retrieve your policies.")
|
||||
);
|
||||
setIsLoadingError(true);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [dispatch]);
|
||||
|
||||
const getInterval = useCallback(async () => {
|
||||
try {
|
||||
const response = await configAPI.loadAll();
|
||||
const interval = secondsToHms(
|
||||
inMilliseconds(response.update_interval.osquery_detail) / 1000
|
||||
);
|
||||
setUpdateInterval(interval);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
dispatch(
|
||||
renderFlash(
|
||||
"error",
|
||||
"Sorry, we could not retrieve your update interval."
|
||||
)
|
||||
);
|
||||
}
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
getPolicies();
|
||||
getInterval();
|
||||
}, [getInterval, getPolicies]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(queryActions.loadAll());
|
||||
}, [dispatch]);
|
||||
|
||||
const toggleAddPolicyModal = useCallback(() => {
|
||||
setShowAddPolicyModal(!showAddPolicyModal);
|
||||
}, [showAddPolicyModal, setShowAddPolicyModal]);
|
||||
|
||||
const toggleRemovePoliciesModal = useCallback(() => {
|
||||
setShowRemovePoliciesModal(!showRemovePoliciesModal);
|
||||
}, [showRemovePoliciesModal, setShowRemovePoliciesModal]);
|
||||
|
||||
// TODO typing for mouse event?
|
||||
const onRemovePoliciesClick = useCallback(
|
||||
(selectedTableIds: number[]): void => {
|
||||
toggleRemovePoliciesModal();
|
||||
setSelectedIds(selectedTableIds);
|
||||
},
|
||||
[toggleRemovePoliciesModal]
|
||||
);
|
||||
|
||||
const onRemovePoliciesSubmit = useCallback(() => {
|
||||
const ids = selectedIds;
|
||||
policiesAPI
|
||||
.destroy(ids)
|
||||
.then(() => {
|
||||
dispatch(
|
||||
renderFlash(
|
||||
"success",
|
||||
`Successfully removed ${
|
||||
ids && ids.length === 1 ? "policy" : "policies"
|
||||
}.`
|
||||
)
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(
|
||||
renderFlash(
|
||||
"error",
|
||||
`Unable to remove ${
|
||||
ids && ids.length === 1 ? "policy" : "policies"
|
||||
}. Please try again.`
|
||||
)
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
toggleRemovePoliciesModal();
|
||||
getPolicies();
|
||||
});
|
||||
}, [dispatch, getPolicies, selectedIds, toggleRemovePoliciesModal]);
|
||||
|
||||
const onAddPolicySubmit = useCallback(
|
||||
(query_id: number | undefined) => {
|
||||
if (!query_id) {
|
||||
dispatch(
|
||||
renderFlash("error", "Could not add policy. Please try again.")
|
||||
);
|
||||
console.log("Missing query id; cannot add policy");
|
||||
return false;
|
||||
}
|
||||
policiesAPI
|
||||
.create(query_id)
|
||||
.then(() => {
|
||||
dispatch(renderFlash("success", `Successfully added policy.`));
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(
|
||||
renderFlash("error", "Could not add policy. Please try again.")
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
toggleAddPolicyModal();
|
||||
getPolicies();
|
||||
});
|
||||
return false;
|
||||
},
|
||||
[dispatch, getPolicies, toggleAddPolicyModal]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass}__wrapper body-wrap`}>
|
||||
<div className={`${baseClass}__header-wrap`}>
|
||||
<div className={`${baseClass}__header`}>
|
||||
<div className={`${baseClass}__text`}>
|
||||
<h1 className={`${baseClass}__title`}>
|
||||
<span>Policies</span>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
{policies && policies.length !== 0 && !isLoadingError && (
|
||||
<div className={`${baseClass}__action-button-container`}>
|
||||
<Button
|
||||
variant="brand"
|
||||
className={`${baseClass}__add-policy-button`}
|
||||
onClick={toggleAddPolicyModal}
|
||||
>
|
||||
Add a policy
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={`${baseClass}__description`}>
|
||||
<p>Policy queries report which hosts are compliant.</p>
|
||||
</div>
|
||||
{policies && policies.length !== 0 && !isLoadingError && (
|
||||
<InfoBanner className={`${baseClass}__sandbox-info`}>
|
||||
<p>
|
||||
Your policies are checked every <b>{updateInterval.trim()}</b>.
|
||||
Check out the Fleet documentation on{" "}
|
||||
<a href={DOCS_LINK} target="_blank" rel="noreferrer">
|
||||
<b>how to edit this frequency</b>
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</InfoBanner>
|
||||
)}
|
||||
<div>
|
||||
{renderTable(
|
||||
policies,
|
||||
isLoading,
|
||||
isLoadingError,
|
||||
onRemovePoliciesClick,
|
||||
toggleAddPolicyModal
|
||||
)}
|
||||
</div>
|
||||
{showAddPolicyModal && (
|
||||
<AddPolicyModal
|
||||
onCancel={toggleAddPolicyModal}
|
||||
onSubmit={onAddPolicySubmit}
|
||||
allQueries={queriesList}
|
||||
/>
|
||||
)}
|
||||
{showRemovePoliciesModal && (
|
||||
<RemovePoliciesModal
|
||||
onCancel={toggleRemovePoliciesModal}
|
||||
onSubmit={onRemovePoliciesSubmit}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManagePolicyPage;
|
||||
197
frontend/pages/policies/ManagePoliciesPage/_styles.scss
Normal file
197
frontend/pages/policies/ManagePoliciesPage/_styles.scss
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
.manage-policies-page {
|
||||
&__header-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: $pad-small;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.form-field {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__text {
|
||||
margin-right: $pad-large;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: $large;
|
||||
|
||||
.fleeticon {
|
||||
color: $core-fleet-blue;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.fleeticon-success-check {
|
||||
color: $ui-success;
|
||||
}
|
||||
|
||||
.fleeticon-offline {
|
||||
color: $ui-error;
|
||||
}
|
||||
|
||||
.fleeticon-mia {
|
||||
color: $core-fleet-black;
|
||||
}
|
||||
}
|
||||
|
||||
&__description {
|
||||
margin: 0;
|
||||
padding-top: $pad-xsmall;
|
||||
|
||||
h2 {
|
||||
text-transform: uppercase;
|
||||
color: $core-fleet-black;
|
||||
font-weight: $regular;
|
||||
font-size: $small;
|
||||
}
|
||||
|
||||
p {
|
||||
color: $core-dark-blue-grey;
|
||||
margin: 0;
|
||||
font-size: $x-small;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
&__action-button-container {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.button {
|
||||
font-size: $x-small;
|
||||
|
||||
img {
|
||||
transform: scale(0.5);
|
||||
}
|
||||
}
|
||||
|
||||
&__advanced-button {
|
||||
margin-right: $pad-medium;
|
||||
}
|
||||
|
||||
&__sandbox-info {
|
||||
margin-top: $pad-large;
|
||||
margin-bottom: $pad-xxlarge;
|
||||
|
||||
p {
|
||||
font-size: $x-small;
|
||||
margin: 0;
|
||||
margin-bottom: $pad-medium;
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $core-vibrant-blue;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__modal-buttons {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
.button:first-child {
|
||||
margin-right: $pad-medium;
|
||||
}
|
||||
}
|
||||
|
||||
&__team-dropdown {
|
||||
border: 0 !important;
|
||||
position: relative;
|
||||
|
||||
:hover {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
&.is-focused {
|
||||
.Select-control {
|
||||
border: 0 !important;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.Select-menu-outer {
|
||||
position: absolute;
|
||||
left: -12px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.Select.is-open {
|
||||
.Select-value-label {
|
||||
color: $core-vibrant-blue !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__header {
|
||||
.Select-control {
|
||||
background-color: #fff;
|
||||
border: 0 !important;
|
||||
border-radius: none;
|
||||
position: none;
|
||||
width: max-content; // move select arrow
|
||||
height: 20px;
|
||||
|
||||
&:hover {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:hover .Select-value-label {
|
||||
color: $core-vibrant-blue !important;
|
||||
}
|
||||
|
||||
.Select-arrow-zone {
|
||||
padding-left: $pad-small;
|
||||
|
||||
.Select-arrow {
|
||||
top: 1px !important;
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
}
|
||||
.Select-multi-value-wrapper {
|
||||
width: max-content; // move select arrow
|
||||
height: 20px;
|
||||
margin-bottom: $pad-xsmall;
|
||||
|
||||
.Select-input {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.Select-value {
|
||||
position: relative; // move select arrow
|
||||
display: inline-block; // move select arrow
|
||||
line-height: 28px;
|
||||
padding: 0;
|
||||
border: 0 !important;
|
||||
background-color: #fff !important;
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
|
||||
&.is-focused {
|
||||
border: 0 !important;
|
||||
}
|
||||
:hover {
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
.Select-value-label {
|
||||
font-size: $large !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
/* This component is used for creating policies */
|
||||
|
||||
import React, { useState, useCallback } from "react";
|
||||
|
||||
// @ts-ignore
|
||||
import Modal from "components/modals/Modal";
|
||||
import Button from "components/buttons/Button";
|
||||
import InfoBanner from "components/InfoBanner/InfoBanner";
|
||||
// @ts-ignore
|
||||
import Dropdown from "components/forms/fields/Dropdown";
|
||||
import { IQuery } from "interfaces/query";
|
||||
|
||||
const baseClass = "add-policy-modal";
|
||||
|
||||
interface IAddPolicyModalProps {
|
||||
allQueries: IQuery[];
|
||||
onCancel: () => void;
|
||||
onSubmit: (query_id: number | undefined) => void;
|
||||
}
|
||||
|
||||
const AddPolicyModal = ({
|
||||
onCancel,
|
||||
onSubmit,
|
||||
allQueries,
|
||||
}: IAddPolicyModalProps): JSX.Element => {
|
||||
const [selectedQuery, setSelectedQuery] = useState<number>();
|
||||
|
||||
const createQueryDropdownOptions = () => {
|
||||
return allQueries.map(({ id, name }) => ({
|
||||
value: id,
|
||||
label: name,
|
||||
}));
|
||||
};
|
||||
|
||||
const onChangeSelectQuery = useCallback(
|
||||
(queryId: number) => {
|
||||
setSelectedQuery(queryId);
|
||||
},
|
||||
[setSelectedQuery]
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal title={"Add a policy"} onExit={onCancel} className={baseClass}>
|
||||
<form className={`${baseClass}__form`}>
|
||||
<Dropdown
|
||||
searchable
|
||||
options={createQueryDropdownOptions()}
|
||||
onChange={onChangeSelectQuery}
|
||||
placeholder={"Select query"}
|
||||
value={selectedQuery}
|
||||
wrapperClassName={`${baseClass}__select-query-dropdown-wrapper`}
|
||||
/>
|
||||
<InfoBanner className={`${baseClass}__sandbox-info`}>
|
||||
<p>
|
||||
Host that return results for the selected query are <b>Passing</b>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Hosts that do not return results for the selected query are{" "}
|
||||
<b>Failing</b>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
To test which hosts return results, it is recommened to first run
|
||||
your query as a live query by heading to <b>Queries</b> and then
|
||||
selecting a query.
|
||||
</p>
|
||||
</InfoBanner>
|
||||
<div className={`${baseClass}__btn-wrap`}>
|
||||
<Button
|
||||
className={`${baseClass}__btn`}
|
||||
type="button"
|
||||
variant="brand"
|
||||
onClick={() => onSubmit(selectedQuery)}
|
||||
disabled={!selectedQuery}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
<Button
|
||||
className={`${baseClass}__btn`}
|
||||
onClick={onCancel}
|
||||
variant="inverse"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddPolicyModal;
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
.add-policy-modal {
|
||||
&__sandbox-info {
|
||||
margin-top: $pad-medium;
|
||||
margin-bottom: $pad-large;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
margin-bottom: $pad-medium;
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: $core-vibrant-blue;
|
||||
font-weight: $regular;
|
||||
font-size: $x-small;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&__info-header {
|
||||
font-weight: $bold;
|
||||
}
|
||||
|
||||
&__btn-wrap {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
&__advanced-options-button {
|
||||
margin: $pad-medium 0;
|
||||
color: $core-vibrant-blue;
|
||||
font-weight: $bold;
|
||||
font-size: $x-small;
|
||||
}
|
||||
|
||||
.downcarat {
|
||||
&::after {
|
||||
content: url("../assets/images/icon-chevron-blue-16x16@2x.png");
|
||||
transform: scale(0.5);
|
||||
width: 16px;
|
||||
border-radius: 0px;
|
||||
padding: 0px;
|
||||
padding-left: 2px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.upcarat {
|
||||
&::after {
|
||||
content: url("../assets/images/icon-chevron-blue-16x16@2x.png");
|
||||
transform: scale(0.5) rotate(180deg);
|
||||
width: 16px;
|
||||
border-radius: 0px;
|
||||
padding: 0px;
|
||||
padding-left: 2px;
|
||||
margin-bottom: 4px;
|
||||
margin-left: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.Select-value-label {
|
||||
font-size: $small;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./AddPolicyModal";
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
import React from "react";
|
||||
import { noop } from "lodash";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
import { IPolicy } from "interfaces/policy";
|
||||
import TableContainer from "components/TableContainer";
|
||||
import { generateTableHeaders, generateDataSet } from "./PoliciesTableConfig";
|
||||
|
||||
const baseClass = "policies-list-wrapper";
|
||||
const noPoliciesClass = "no-policies";
|
||||
|
||||
interface IPoliciesListWrapperProps {
|
||||
policiesList: IPolicy[];
|
||||
isLoading: boolean;
|
||||
onRemovePoliciesClick: (selectedTableIds: number[]) => void;
|
||||
toggleAddPolicyModal: () => void;
|
||||
}
|
||||
|
||||
const PoliciesListWrapper = (props: IPoliciesListWrapperProps): JSX.Element => {
|
||||
const {
|
||||
policiesList,
|
||||
isLoading,
|
||||
onRemovePoliciesClick,
|
||||
toggleAddPolicyModal,
|
||||
} = props;
|
||||
|
||||
const NoPolicies = () => {
|
||||
return (
|
||||
<div className={`${noPoliciesClass}`}>
|
||||
<div className={`${noPoliciesClass}__inner`}>
|
||||
<div className={`${noPoliciesClass}__inner-text`}>
|
||||
<h2>You don't have any policies.</h2>
|
||||
<p>
|
||||
Policies allow you to monitor which devices meet a certain
|
||||
standard.
|
||||
</p>
|
||||
<div className={`${noPoliciesClass}__-cta-buttons`}>
|
||||
<Button
|
||||
variant="brand"
|
||||
className={`${noPoliciesClass}__add-policy-button`}
|
||||
onClick={toggleAddPolicyModal}
|
||||
>
|
||||
Add a policy
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const tableHeaders = generateTableHeaders();
|
||||
|
||||
return (
|
||||
<div className={`${baseClass}`}>
|
||||
<TableContainer
|
||||
resultsTitle={"queries"}
|
||||
columns={tableHeaders}
|
||||
data={generateDataSet(policiesList)}
|
||||
isLoading={isLoading}
|
||||
defaultSortHeader={"query_name"}
|
||||
defaultSortDirection={"asc"}
|
||||
manualSortBy
|
||||
showMarkAllPages={false}
|
||||
isAllPagesSelected={false}
|
||||
disablePagination
|
||||
onPrimarySelectActionClick={onRemovePoliciesClick}
|
||||
primarySelectActionButtonVariant="text-icon"
|
||||
primarySelectActionButtonIcon="close"
|
||||
primarySelectActionButtonText={"Remove"}
|
||||
emptyComponent={NoPolicies}
|
||||
onQueryChange={noop}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PoliciesListWrapper;
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
/* eslint-disable react/prop-types */
|
||||
// disable this rule as it was throwing an error in Header and Cell component
|
||||
// definitions for the selection row for some reason when we dont really need it.
|
||||
import React from "react";
|
||||
import { memoize } from "lodash";
|
||||
|
||||
// @ts-ignore
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
import LinkCell from "components/TableContainer/DataTable/LinkCell/LinkCell";
|
||||
import TextCell from "components/TableContainer/DataTable/TextCell";
|
||||
import { IPolicy } from "interfaces/policy";
|
||||
import PATHS from "router/paths";
|
||||
import sortUtils from "utilities/sort";
|
||||
import { PolicyResponse } from "utilities/constants";
|
||||
|
||||
// TODO functions for paths math e.g., path={PATHS.MANAGE_HOSTS + getParams(cellProps.row.original)}
|
||||
|
||||
const TAGGED_TEMPLATES = {
|
||||
hostsByStatusRoute: (id: number, status: PolicyResponse) => {
|
||||
return `?policy_id=${id}&policy_response=${status}`;
|
||||
},
|
||||
};
|
||||
interface IHeaderProps {
|
||||
column: {
|
||||
title: string;
|
||||
isSortedDesc: boolean;
|
||||
};
|
||||
getToggleAllRowsSelectedProps: () => any; // TODO: do better with types
|
||||
toggleAllRowsSelected: () => void;
|
||||
}
|
||||
|
||||
interface ICellProps {
|
||||
cell: {
|
||||
value: any;
|
||||
};
|
||||
row: {
|
||||
original: IPolicy;
|
||||
getToggleRowSelectedProps: () => any; // TODO: do better with types
|
||||
toggleRowSelected: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
interface IDataColumn {
|
||||
Header: ((props: IHeaderProps) => JSX.Element) | string;
|
||||
Cell: (props: ICellProps) => JSX.Element;
|
||||
id?: string;
|
||||
title?: string;
|
||||
accessor?: string;
|
||||
disableHidden?: boolean;
|
||||
disableSortBy?: boolean;
|
||||
sortType?: string;
|
||||
}
|
||||
// interface IPoliciesTableData {
|
||||
// name: string;
|
||||
// passing: number;
|
||||
// failing: number;
|
||||
// id: number;
|
||||
// query_id: number;
|
||||
// query_name: string;
|
||||
// }
|
||||
|
||||
// NOTE: cellProps come from react-table
|
||||
// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties
|
||||
const generateTableHeaders = (): IDataColumn[] => {
|
||||
return [
|
||||
{
|
||||
id: "selection",
|
||||
Header: (cellProps: IHeaderProps): JSX.Element => {
|
||||
const props = cellProps.getToggleAllRowsSelectedProps();
|
||||
const checkboxProps = {
|
||||
value: props.checked,
|
||||
indeterminate: props.indeterminate,
|
||||
onChange: () => cellProps.toggleAllRowsSelected(),
|
||||
};
|
||||
return <Checkbox {...checkboxProps} />;
|
||||
},
|
||||
Cell: (cellProps: ICellProps): JSX.Element => {
|
||||
const props = cellProps.row.getToggleRowSelectedProps();
|
||||
const checkboxProps = {
|
||||
value: props.checked,
|
||||
onChange: () => cellProps.row.toggleRowSelected(),
|
||||
};
|
||||
return <Checkbox {...checkboxProps} />;
|
||||
},
|
||||
disableHidden: true,
|
||||
},
|
||||
{
|
||||
title: "Query",
|
||||
Header: "Query",
|
||||
disableSortBy: true,
|
||||
// sortType: "caseInsensitive",
|
||||
accessor: "query_name",
|
||||
Cell: (cellProps: ICellProps): JSX.Element => (
|
||||
<TextCell value={cellProps.cell.value} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Passing",
|
||||
Header: "Passing",
|
||||
disableSortBy: true,
|
||||
accessor: "passing_host_count",
|
||||
Cell: (cellProps: ICellProps): JSX.Element => (
|
||||
<LinkCell
|
||||
value={`${cellProps.cell.value} hosts`}
|
||||
path={
|
||||
PATHS.MANAGE_HOSTS +
|
||||
TAGGED_TEMPLATES.hostsByStatusRoute(
|
||||
cellProps.row.original.id,
|
||||
PolicyResponse.PASSING
|
||||
)
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Failing",
|
||||
Header: "Failing",
|
||||
disableSortBy: true,
|
||||
accessor: "failing_host_count",
|
||||
Cell: (cellProps: ICellProps): JSX.Element => (
|
||||
<LinkCell
|
||||
value={`${cellProps.cell.value} hosts`}
|
||||
path={
|
||||
PATHS.MANAGE_HOSTS +
|
||||
TAGGED_TEMPLATES.hostsByStatusRoute(
|
||||
cellProps.row.original.id,
|
||||
PolicyResponse.FAILING
|
||||
)
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const generateDataSet = memoize((all_policies: IPolicy[] = []): IPolicy[] => {
|
||||
all_policies = all_policies.sort((a, b) =>
|
||||
sortUtils.caseInsensitiveAsc(b.query_name, a.query_name)
|
||||
);
|
||||
return all_policies;
|
||||
});
|
||||
|
||||
export { generateTableHeaders, generateDataSet };
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
.policy-list-wrapper {
|
||||
border-collapse: collapse;
|
||||
|
||||
&__wrapper {
|
||||
border: 1px solid $ui-fleet-blue-15;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-top: $pad-medium;
|
||||
}
|
||||
|
||||
thead {
|
||||
background-color: $ui-off-white;
|
||||
border-bottom: 1px solid $ui-fleet-blue-15;
|
||||
|
||||
th {
|
||||
font-size: $x-small;
|
||||
font-weight: $bold;
|
||||
text-align: left;
|
||||
padding: $pad-medium $pad-large;
|
||||
|
||||
&:nth-child(1) {
|
||||
border-top-left-radius: 6px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
width: calc(49% - 20px);
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 20%;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
&:nth-last-child(2) {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
width: 150px;
|
||||
border-top-right-radius: 6px;
|
||||
border-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__th-pack-name {
|
||||
padding-left: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&__select-all {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&__empty-table {
|
||||
text-align: center;
|
||||
font-size: $x-small;
|
||||
color: $core-fleet-black;
|
||||
}
|
||||
|
||||
&__policy-count {
|
||||
color: $core-fleet-black;
|
||||
font-size: $x-small;
|
||||
font-weight: $bold;
|
||||
margin: 0 12px 0 0;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.no-policies {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-top: $pad-xxxlarge;
|
||||
|
||||
h1 {
|
||||
font-size: $large;
|
||||
font-weight: $regular;
|
||||
line-height: normal;
|
||||
letter-spacing: normal;
|
||||
color: $core-fleet-black;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: $small;
|
||||
font-weight: $bold;
|
||||
margin: 0 0 $pad-large;
|
||||
line-height: 20px;
|
||||
color: $core-fleet-black;
|
||||
}
|
||||
|
||||
&__inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
h1 {
|
||||
font-size: $small;
|
||||
font-weight: $bold;
|
||||
margin-bottom: $pad-medium;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 176px;
|
||||
margin-right: $pad-xlarge;
|
||||
}
|
||||
|
||||
p {
|
||||
color: $core-fleet-black;
|
||||
font-weight: $regular;
|
||||
font-size: $x-small;
|
||||
margin: 0;
|
||||
margin-bottom: $pad-large;
|
||||
}
|
||||
}
|
||||
|
||||
&__inner-text {
|
||||
padding-left: $pad-xlarge;
|
||||
width: 485px;
|
||||
}
|
||||
|
||||
&__add-policy-button {
|
||||
margin-right: $pad-small;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./PoliciesListWrapper";
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/**
|
||||
* Component when there is an error retrieving policy set up in fleet
|
||||
*/
|
||||
import React from "react";
|
||||
|
||||
import OpenNewTabIcon from "../../../../../../assets/images/open-new-tab-12x12@2x.png";
|
||||
import ErrorIcon from "../../../../../../assets/images/icon-error-16x16@2x.png";
|
||||
|
||||
const baseClass = "policy-error";
|
||||
|
||||
const PolicyError = (): JSX.Element => {
|
||||
return (
|
||||
<div className={`${baseClass}`}>
|
||||
<div className={`${baseClass}__inner`}>
|
||||
<div className="info">
|
||||
<span className="info__header">
|
||||
<img src={ErrorIcon} alt="error icon" id="error-icon" />
|
||||
Something's gone wrong.
|
||||
</span>
|
||||
<span className="info__data">Refresh the page or log in again.</span>
|
||||
<span className="info__data">
|
||||
If this keeps happening, please
|
||||
<a
|
||||
href="https://github.com/fleetdm/fleet/issues"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
file an issue.
|
||||
<img src={OpenNewTabIcon} alt="open new tab" id="new-tab-icon" />
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PolicyError;
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
.policy-error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-top: $pad-xxxlarge;
|
||||
|
||||
#error-icon {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
#new-tab-icon {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
a {
|
||||
font-size: $x-small;
|
||||
color: $core-vibrant-blue;
|
||||
font-weight: $bold;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&__inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.info {
|
||||
&__header {
|
||||
display: block;
|
||||
color: $core-fleet-black;
|
||||
font-weight: $bold;
|
||||
font-size: $x-small;
|
||||
text-align: left;
|
||||
}
|
||||
&__data {
|
||||
display: block;
|
||||
color: $core-fleet-black;
|
||||
font-weight: normal;
|
||||
font-size: $x-small;
|
||||
text-align: left;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./PolicyError";
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
import React from "react";
|
||||
|
||||
import Modal from "components/modals/Modal";
|
||||
import Button from "components/buttons/Button";
|
||||
|
||||
const baseClass = "remove-policies-modal";
|
||||
|
||||
interface IRemovePoliciesModalProps {
|
||||
onCancel: () => void;
|
||||
onSubmit: () => void;
|
||||
}
|
||||
|
||||
const RemovePoliciesModal = (props: IRemovePoliciesModalProps): JSX.Element => {
|
||||
const { onCancel, onSubmit } = props;
|
||||
|
||||
return (
|
||||
<Modal title={"Remove policies"} onExit={onCancel} className={baseClass}>
|
||||
<div className={baseClass}>
|
||||
Are you sure you want to remove the selected policies?
|
||||
<div className={`${baseClass}__btn-wrap`}>
|
||||
<Button
|
||||
className={`${baseClass}__btn`}
|
||||
type="button"
|
||||
variant="alert"
|
||||
onClick={onSubmit}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
<Button
|
||||
className={`${baseClass}__btn`}
|
||||
onClick={onCancel}
|
||||
variant="inverse-alert"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default RemovePoliciesModal;
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
.remove-policies-modal {
|
||||
font-size: $x-small;
|
||||
|
||||
&__btn-wrap {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
margin-top: $pad-xxlarge;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./RemovePoliciesModal";
|
||||
1
frontend/pages/policies/ManagePoliciesPage/index.ts
Normal file
1
frontend/pages/policies/ManagePoliciesPage/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./ManagePoliciesPage";
|
||||
|
|
@ -31,9 +31,11 @@ import LogoutPage from "pages/LogoutPage";
|
|||
import ManageHostsPage from "pages/hosts/ManageHostsPage";
|
||||
import ManageQueriesPage from "pages/queries/ManageQueriesPage";
|
||||
import ManagePacksPage from "pages/packs/ManagePacksPage";
|
||||
import ManagePoliciesPage from "pages/policies/ManagePoliciesPage";
|
||||
import ManageSchedulePage from "pages/schedule/ManageSchedulePage";
|
||||
import PackPageWrapper from "components/packs/PackPageWrapper";
|
||||
import PackComposerPage from "pages/packs/PackComposerPage";
|
||||
import PoliciesPageWrapper from "components/policies/PoliciesPageWrapper";
|
||||
import QueryPage from "pages/queries/QueryPage";
|
||||
import QueryPageWrapper from "components/queries/QueryPageWrapper";
|
||||
import RegistrationPage from "pages/RegistrationPage";
|
||||
|
|
@ -110,6 +112,9 @@ const routes = (
|
|||
<Route path="edit" component={EditPackPage} />
|
||||
</Route>
|
||||
</Route>
|
||||
<Route path="policies" component={PoliciesPageWrapper}>
|
||||
<Route path="manage" component={ManagePoliciesPage} />
|
||||
</Route>
|
||||
<Route path="schedule" component={SchedulePageWrapper}>
|
||||
<Route path="manage" component={ManageSchedulePage} />
|
||||
<Route
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ export default {
|
|||
MANAGE_TEAM_SCHEDULE: (teamId: number): string => {
|
||||
return `${URL_PREFIX}/schedule/manage/teams/${teamId}`;
|
||||
},
|
||||
MANAGE_POLICIES: `${URL_PREFIX}/policies/manage`,
|
||||
NEW_QUERY: `${URL_PREFIX}/queries/new`,
|
||||
RESET_PASSWORD: `${URL_PREFIX}/login/reset`,
|
||||
SETUP: `${URL_PREFIX}/setup`,
|
||||
|
|
|
|||
14
frontend/services/entities/config.ts
Normal file
14
frontend/services/entities/config.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import sendRequest from "services";
|
||||
import endpoints from "fleet/endpoints";
|
||||
// import { IConfig } from "interfaces/host";
|
||||
|
||||
// TODO add other methods from "fleet/entities/config"
|
||||
|
||||
export default {
|
||||
loadAll: () => {
|
||||
const { CONFIG } = endpoints;
|
||||
const path = `${CONFIG}`;
|
||||
|
||||
return sendRequest("GET", path);
|
||||
},
|
||||
};
|
||||
|
|
@ -14,6 +14,8 @@ interface IHostLoadOptions {
|
|||
globalFilter: string;
|
||||
sortBy: ISortOption[];
|
||||
teamId: number;
|
||||
policyId: number;
|
||||
policyResponse: string;
|
||||
}
|
||||
|
||||
export default {
|
||||
|
|
@ -43,6 +45,8 @@ export default {
|
|||
const globalFilter = options?.globalFilter || "";
|
||||
const sortBy = options?.sortBy || [];
|
||||
const teamId = options?.teamId || null;
|
||||
const policyId = options?.policyId || null;
|
||||
const policyResponse = options?.policyResponse || null;
|
||||
|
||||
// TODO: add this query param logic to client class
|
||||
const pagination = `page=${page}&per_page=${perPage}`;
|
||||
|
|
@ -92,6 +96,11 @@ export default {
|
|||
path += `&team_id=${teamId}`;
|
||||
}
|
||||
|
||||
if (!label && policyId) {
|
||||
path += `&policy_id=${policyId}`;
|
||||
path += `&policy_response=${policyResponse || "passing"}`; // TODO confirm whether there should be a default if there is an id but no response sepcified
|
||||
}
|
||||
|
||||
return sendRequest("GET", path);
|
||||
},
|
||||
transferToTeam: (teamId: number | null, hostIds: number[]) => {
|
||||
|
|
@ -102,6 +111,8 @@ export default {
|
|||
hosts: hostIds,
|
||||
});
|
||||
},
|
||||
|
||||
// TODO confirm interplay with policies
|
||||
transferToTeamByFilter: (
|
||||
teamId: number | null,
|
||||
query: string,
|
||||
|
|
|
|||
28
frontend/services/entities/policies.ts
Normal file
28
frontend/services/entities/policies.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import sendRequest from "services";
|
||||
import endpoints from "fleet/endpoints";
|
||||
// import { IPolicyFormData, IPolicy } from "interfaces/policy";
|
||||
|
||||
export default {
|
||||
create: (query_id: number) => {
|
||||
const { POLICIES } = endpoints;
|
||||
|
||||
return sendRequest("POST", POLICIES, { query_id });
|
||||
},
|
||||
destroy: (ids: number[]) => {
|
||||
const { POLICIES } = endpoints;
|
||||
const path = `${POLICIES}/delete`;
|
||||
|
||||
return sendRequest("POST", path, { ids });
|
||||
},
|
||||
load: (id: number) => {
|
||||
const { POLICIES } = endpoints;
|
||||
const path = `${POLICIES}/${id}`;
|
||||
|
||||
return sendRequest("GET", path);
|
||||
},
|
||||
loadAll: () => {
|
||||
const { POLICIES } = endpoints;
|
||||
|
||||
return sendRequest("GET", POLICIES);
|
||||
},
|
||||
};
|
||||
|
|
@ -1,3 +1,8 @@
|
|||
export enum PolicyResponse {
|
||||
PASSING = "passing",
|
||||
FAILING = "failing",
|
||||
}
|
||||
|
||||
export const FREQUENCY_DROPDOWN_OPTIONS = [
|
||||
{ value: 3600, label: "Every hour" },
|
||||
{ value: 21600, label: "Every 6 hours" },
|
||||
|
|
|
|||
1
frontend/utilities/sort/index.ts
Normal file
1
frontend/utilities/sort/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./sort_functions";
|
||||
14
frontend/utilities/sort/sort_functions.ts
Normal file
14
frontend/utilities/sort/sort_functions.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
const caseInsensitiveAsc = (a: string, b: string): number => {
|
||||
a = a.toLowerCase();
|
||||
b = b.toLowerCase();
|
||||
|
||||
if (b > a) {
|
||||
return 1;
|
||||
}
|
||||
if (b < a) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
export default { caseInsensitiveAsc };
|
||||
Loading…
Reference in a new issue