mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 01:18:42 +00:00
Add team policies to new policy flow UI (#3116)
This commit is contained in:
parent
036093874d
commit
a51225f3a5
9 changed files with 109 additions and 45 deletions
|
|
@ -11,34 +11,44 @@ type Props = {
|
|||
};
|
||||
|
||||
type InitialStateType = {
|
||||
selectedOsqueryTable: IOsqueryTable;
|
||||
lastEditedQueryName: string;
|
||||
lastEditedQueryDescription: string;
|
||||
lastEditedQueryBody: string;
|
||||
setLastEditedQueryName: (value: string) => void;
|
||||
setLastEditedQueryDescription: (value: string) => void;
|
||||
setLastEditedQueryBody: (value: string) => void;
|
||||
policyTeamId: number;
|
||||
setPolicyTeamId: (id: number) => void;
|
||||
selectedOsqueryTable: IOsqueryTable;
|
||||
setSelectedOsqueryTable: (tableName: string) => void;
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
selectedOsqueryTable: find(osqueryTables, { name: "users" }),
|
||||
lastEditedQueryName: DEFAULT_POLICY.name,
|
||||
lastEditedQueryDescription: DEFAULT_POLICY.description,
|
||||
lastEditedQueryBody: DEFAULT_POLICY.query,
|
||||
setLastEditedQueryName: () => null,
|
||||
setLastEditedQueryDescription: () => null,
|
||||
setLastEditedQueryBody: () => null,
|
||||
policyTeamId: 0,
|
||||
setPolicyTeamId: () => null,
|
||||
selectedOsqueryTable: find(osqueryTables, { name: "users" }),
|
||||
setSelectedOsqueryTable: () => null,
|
||||
};
|
||||
|
||||
const actions = {
|
||||
SET_SELECTED_OSQUERY_TABLE: "SET_SELECTED_OSQUERY_TABLE",
|
||||
SET_LAST_EDITED_QUERY_INFO: "SET_LAST_EDITED_QUERY_INFO",
|
||||
SET_POLICY_TEAM_ID: "SET_POLICY_TEAM_ID",
|
||||
SET_SELECTED_OSQUERY_TABLE: "SET_SELECTED_OSQUERY_TABLE",
|
||||
};
|
||||
|
||||
const reducer = (state: any, action: any) => {
|
||||
switch (action.type) {
|
||||
case actions.SET_POLICY_TEAM_ID:
|
||||
return {
|
||||
...state,
|
||||
policyTeamId: action.id,
|
||||
};
|
||||
case actions.SET_SELECTED_OSQUERY_TABLE:
|
||||
return {
|
||||
...state,
|
||||
|
|
@ -71,7 +81,6 @@ const PolicyProvider = ({ children }: Props) => {
|
|||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
|
||||
const value = {
|
||||
selectedOsqueryTable: state.selectedOsqueryTable,
|
||||
lastEditedQueryName: state.lastEditedQueryName,
|
||||
lastEditedQueryDescription: state.lastEditedQueryDescription,
|
||||
lastEditedQueryBody: state.lastEditedQueryBody,
|
||||
|
|
@ -93,6 +102,11 @@ const PolicyProvider = ({ children }: Props) => {
|
|||
lastEditedQueryBody,
|
||||
});
|
||||
},
|
||||
policyTeamId: state.policyTeamId,
|
||||
setPolicyTeamId: (id: number) => {
|
||||
dispatch({ type: actions.SET_POLICY_TEAM_ID, id });
|
||||
},
|
||||
selectedOsqueryTable: state.selectedOsqueryTable,
|
||||
setSelectedOsqueryTable: (tableName: string) => {
|
||||
dispatch({ type: actions.SET_SELECTED_OSQUERY_TABLE, tableName });
|
||||
},
|
||||
|
|
|
|||
|
|
@ -16,4 +16,5 @@ export interface IPolicyFormData {
|
|||
description?: string | number | boolean | any[] | undefined;
|
||||
name?: string | number | boolean | any[] | undefined;
|
||||
query?: string | number | boolean | any[] | undefined;
|
||||
team_id?: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { ITeam } from "interfaces/team";
|
|||
import { IUser } from "interfaces/user";
|
||||
|
||||
import { AppContext } from "context/app";
|
||||
import { PolicyContext } from "context/policy";
|
||||
|
||||
import fleetQueriesAPI from "services/entities/queries";
|
||||
import globalPoliciesAPI from "services/entities/global_policies";
|
||||
|
|
@ -67,6 +68,8 @@ const ManagePolicyPage = (managePoliciesPageProps: {
|
|||
isPremiumTier,
|
||||
} = useContext(AppContext);
|
||||
|
||||
const { setPolicyTeamId } = useContext(PolicyContext);
|
||||
|
||||
const { isTeamMaintainer, isTeamAdmin } = permissionsUtils;
|
||||
const canAddOrRemovePolicy = (user: IUser | null, teamId: number | null) =>
|
||||
isGlobalAdmin ||
|
||||
|
|
@ -74,12 +77,16 @@ const ManagePolicyPage = (managePoliciesPageProps: {
|
|||
isTeamMaintainer(user, teamId) ||
|
||||
isTeamAdmin(user, teamId);
|
||||
|
||||
const { data: teams } = useQuery(["teams"], () => teamsAPI.loadAll({}), {
|
||||
enabled: !!isPremiumTier,
|
||||
select: (data) => data.teams,
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
const { data: teams } = useQuery<{ teams: ITeam[] }, Error, ITeam[]>(
|
||||
["teams"],
|
||||
() => teamsAPI.loadAll({}),
|
||||
{
|
||||
enabled: !!isPremiumTier,
|
||||
select: (data) => data.teams,
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
}
|
||||
);
|
||||
|
||||
const { data: fleetQueries } = useQuery(
|
||||
["fleetQueries"],
|
||||
|
|
@ -161,6 +168,7 @@ const ManagePolicyPage = (managePoliciesPageProps: {
|
|||
router.replace(path);
|
||||
setShowInheritedPolicies(false);
|
||||
setSelectedPolicyIds([]);
|
||||
setPolicyTeamId(id);
|
||||
};
|
||||
|
||||
const toggleRemovePoliciesModal = () =>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { AppContext } from "context/app";
|
|||
import { PolicyContext } from "context/policy";
|
||||
import { QUERIES_PAGE_STEPS, DEFAULT_POLICY } from "utilities/constants";
|
||||
import globalPoliciesAPI from "services/entities/global_policies"; // @ts-ignore
|
||||
import teamPoliciesAPI from "services/entities/team_policies"; // @ts-ignore
|
||||
import hostAPI from "services/entities/hosts"; // @ts-ignore
|
||||
import { IPolicyFormData, IPolicy } from "interfaces/policy";
|
||||
import { ITarget } from "interfaces/target";
|
||||
|
|
@ -108,7 +109,9 @@ const PolicyPage = ({
|
|||
const {
|
||||
mutateAsync: createPolicy,
|
||||
} = useMutation((formData: IPolicyFormData) =>
|
||||
globalPoliciesAPI.create(formData)
|
||||
formData.team_id
|
||||
? teamPoliciesAPI.create(formData)
|
||||
: globalPoliciesAPI.create(formData)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -13,11 +13,11 @@ export interface INewPolicyModalProps {
|
|||
baseClass: string;
|
||||
queryValue: string;
|
||||
onCreatePolicy: (formData: IPolicyFormData) => void;
|
||||
setIsSaveModalOpen: (isOpen: boolean) => void;
|
||||
setIsNewPolicyModalOpen: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
const validatePolicyName = (name: string) => {
|
||||
const errors: { [key: string]: any } = {};
|
||||
const errors: { [key: string]: string } = {};
|
||||
|
||||
if (!name) {
|
||||
errors.name = "Policy name must be present";
|
||||
|
|
@ -31,11 +31,11 @@ const NewPolicyModal = ({
|
|||
baseClass,
|
||||
queryValue,
|
||||
onCreatePolicy,
|
||||
setIsSaveModalOpen,
|
||||
}: INewPolicyModalProps) => {
|
||||
setIsNewPolicyModalOpen,
|
||||
}: INewPolicyModalProps): JSX.Element => {
|
||||
const [name, setName] = useState<string>("");
|
||||
const [description, setDescription] = useState<string>("");
|
||||
const [errors, setErrors] = useState<{ [key: string]: any }>({});
|
||||
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
||||
|
||||
useDeepEffect(() => {
|
||||
if (name) {
|
||||
|
|
@ -43,7 +43,7 @@ const NewPolicyModal = ({
|
|||
}
|
||||
}, [name]);
|
||||
|
||||
const handleUpdate = (evt: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const handleSavePolicy = (evt: React.MouseEvent<HTMLButtonElement>) => {
|
||||
evt.preventDefault();
|
||||
|
||||
const { valid, errors: newErrors } = validatePolicyName(name);
|
||||
|
|
@ -59,12 +59,12 @@ const NewPolicyModal = ({
|
|||
query: queryValue,
|
||||
});
|
||||
|
||||
setIsSaveModalOpen(false);
|
||||
setIsNewPolicyModalOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal title={"Save policy"} onExit={() => setIsSaveModalOpen(false)}>
|
||||
<Modal title={"Save policy"} onExit={() => setIsNewPolicyModalOpen(false)}>
|
||||
<form className={`${baseClass}__save-modal-form`} autoComplete="off">
|
||||
<InputField
|
||||
name="name"
|
||||
|
|
@ -89,7 +89,7 @@ const NewPolicyModal = ({
|
|||
>
|
||||
<Button
|
||||
className={`${baseClass}__btn`}
|
||||
onClick={() => setIsSaveModalOpen(false)}
|
||||
onClick={() => setIsNewPolicyModalOpen(false)}
|
||||
variant="text-link"
|
||||
>
|
||||
Cancel
|
||||
|
|
@ -98,7 +98,7 @@ const NewPolicyModal = ({
|
|||
className={`${baseClass}__btn`}
|
||||
type="button"
|
||||
variant="brand"
|
||||
onClick={handleUpdate}
|
||||
onClick={handleSavePolicy}
|
||||
>
|
||||
Save policy
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import Avatar from "components/Avatar";
|
|||
import FleetAce from "components/FleetAce"; // @ts-ignore
|
||||
import validateQuery from "components/forms/validators/validate_query";
|
||||
import Button from "components/buttons/Button";
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
import Spinner from "components/Spinner"; // @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import NewPolicyModal from "../NewPolicyModal";
|
||||
|
|
@ -42,7 +41,7 @@ interface IPolicyFormProps {
|
|||
}
|
||||
|
||||
const validateQuerySQL = (query: string) => {
|
||||
const errors: { [key: string]: any } = {};
|
||||
const errors: { [key: string]: string } = {};
|
||||
const { error: queryError, valid: queryValid } = validateQuery(query);
|
||||
|
||||
if (!queryValid) {
|
||||
|
|
@ -66,8 +65,10 @@ const PolicyForm = ({
|
|||
renderLiveQueryWarning,
|
||||
}: IPolicyFormProps): JSX.Element => {
|
||||
const isEditMode = !!policyIdForEdit;
|
||||
const [errors, setErrors] = useState<{ [key: string]: any }>({});
|
||||
const [isSaveModalOpen, setIsSaveModalOpen] = useState<boolean>(false);
|
||||
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
||||
const [isNewPolicyModalOpen, setIsNewPolicyModalOpen] = useState<boolean>(
|
||||
false
|
||||
);
|
||||
const [showQueryEditor, setShowQueryEditor] = useState<boolean>(false);
|
||||
const [compatiblePlatforms, setCompatiblePlatforms] = useState<string[]>([]);
|
||||
const [isEditingName, setIsEditingName] = useState<boolean>(false);
|
||||
|
|
@ -135,7 +136,7 @@ const PolicyForm = ({
|
|||
});
|
||||
};
|
||||
|
||||
const promptSaveQuery = (forceNew = false) => (
|
||||
const handleSavePolicy = (forceNew = false) => (
|
||||
evt: React.MouseEvent<HTMLButtonElement>
|
||||
) => {
|
||||
evt.preventDefault();
|
||||
|
|
@ -160,7 +161,7 @@ const PolicyForm = ({
|
|||
|
||||
if (valid) {
|
||||
if (!isEditMode || forceNew) {
|
||||
setIsSaveModalOpen(true);
|
||||
setIsNewPolicyModalOpen(true);
|
||||
} else {
|
||||
onUpdate({
|
||||
name: lastEditedQueryName,
|
||||
|
|
@ -174,6 +175,8 @@ const PolicyForm = ({
|
|||
setIsEditingName(false);
|
||||
setIsEditingDescription(false);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderAuthor = (): JSX.Element | null => {
|
||||
|
|
@ -221,6 +224,8 @@ const PolicyForm = ({
|
|||
} else if (compatiblePlatforms[0] === "None") {
|
||||
return "No platforms (check your query for invalid tables or tables that are supported on different platforms)";
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const displayFormattedPlatforms = compatiblePlatforms.map((string) => {
|
||||
|
|
@ -422,7 +427,7 @@ const PolicyForm = ({
|
|||
onChange={(sqlString: string) => {
|
||||
setLastEditedQueryBody(sqlString);
|
||||
}}
|
||||
handleSubmit={promptSaveQuery}
|
||||
handleSubmit={handleSavePolicy}
|
||||
/>
|
||||
{renderPlatformCompatibility()}
|
||||
{renderLiveQueryWarning()}
|
||||
|
|
@ -444,7 +449,7 @@ const PolicyForm = ({
|
|||
<Button
|
||||
className={`${baseClass}__save`}
|
||||
variant="brand"
|
||||
onClick={promptSaveQuery()}
|
||||
onClick={handleSavePolicy()}
|
||||
disabled={
|
||||
isAnyTeamMaintainerOrTeamAdmin &&
|
||||
!hasTeamMaintainerPermissions
|
||||
|
|
@ -480,12 +485,12 @@ const PolicyForm = ({
|
|||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
{isSaveModalOpen && (
|
||||
{isNewPolicyModalOpen && (
|
||||
<NewPolicyModal
|
||||
baseClass={baseClass}
|
||||
queryValue={lastEditedQueryBody}
|
||||
onCreatePolicy={onCreatePolicy}
|
||||
setIsSaveModalOpen={setIsSaveModalOpen}
|
||||
setIsNewPolicyModalOpen={setIsNewPolicyModalOpen}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ import React, { useContext, useEffect } from "react";
|
|||
import { Link } from "react-router";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { InjectedRouter } from "react-router/lib/Router";
|
||||
import { UseMutateAsyncFunction } from "react-query";
|
||||
|
||||
import globalPoliciesAPI from "services/entities/global_policies";
|
||||
import teamPoliciesAPI from "services/entities/team_policies";
|
||||
import { AppContext } from "context/app";
|
||||
import { PolicyContext } from "context/policy"; // @ts-ignore
|
||||
import { renderFlash } from "redux/nodes/notifications/actions";
|
||||
|
|
@ -24,7 +24,7 @@ interface IQueryEditorProps {
|
|||
storedPolicyError: any;
|
||||
showOpenSchemaActionText: boolean;
|
||||
isStoredPolicyLoading: boolean;
|
||||
createPolicy: UseMutateAsyncFunction<any, unknown, IPolicyFormData, unknown>;
|
||||
createPolicy: (formData: IPolicyFormData) => Promise<any>;
|
||||
onOsqueryTableSelect: (tableName: string) => void;
|
||||
goToSelectTargets: () => void;
|
||||
onOpenSchemaSidebar: () => void;
|
||||
|
|
@ -44,7 +44,7 @@ const QueryEditor = ({
|
|||
goToSelectTargets,
|
||||
onOpenSchemaSidebar,
|
||||
renderLiveQueryWarning,
|
||||
}: IQueryEditorProps) => {
|
||||
}: IQueryEditorProps): JSX.Element | null => {
|
||||
const dispatch = useDispatch();
|
||||
const { currentUser } = useContext(AppContext);
|
||||
|
||||
|
|
@ -54,6 +54,7 @@ const QueryEditor = ({
|
|||
lastEditedQueryName,
|
||||
lastEditedQueryDescription,
|
||||
lastEditedQueryBody,
|
||||
policyTeamId,
|
||||
} = useContext(PolicyContext);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -67,9 +68,22 @@ const QueryEditor = ({
|
|||
}
|
||||
}, []);
|
||||
|
||||
const onSavePolicyFormSubmit = debounce(async (formData: IPolicyFormData) => {
|
||||
const onCreatePolicy = debounce(async (formData: IPolicyFormData) => {
|
||||
// TODO: The approach taken with selectedTeamId context works in most cases. Howeve, the context
|
||||
// will reset to global if page is refreshed. This will cause bugs where a global policy gets
|
||||
// created when the user intended a team policy. For non-gloabl users, request will fail but the
|
||||
// erorr is opaque and would require them to navigate back to the manage policies page to select
|
||||
// a team and start over, in which case it might be better to intercept the unauthorized errors
|
||||
// and redirect to the manage policies page (unless we have added a means to select a team on
|
||||
// the edit/create policy form itself).
|
||||
if (policyTeamId) {
|
||||
formData.team_id = policyTeamId;
|
||||
}
|
||||
|
||||
try {
|
||||
const { policy }: { policy: IPolicy } = await createPolicy(formData);
|
||||
const policy: IPolicy = await createPolicy(formData).then(
|
||||
(data) => data.policy
|
||||
);
|
||||
router.push(PATHS.EDIT_POLICY(policy));
|
||||
dispatch(renderFlash("success", "Policy created!"));
|
||||
} catch (createError) {
|
||||
|
|
@ -94,8 +108,20 @@ const QueryEditor = ({
|
|||
lastEditedQueryBody,
|
||||
});
|
||||
|
||||
const updateAPIRequest = () => {
|
||||
// storedPolicy.team_id is used for existing policies because selectedTeamId is subject to change
|
||||
const team_id = storedPolicy?.team_id;
|
||||
|
||||
return team_id
|
||||
? teamPoliciesAPI.update(policyIdForEdit, {
|
||||
...updatedPolicy,
|
||||
team_id,
|
||||
})
|
||||
: globalPoliciesAPI.update(policyIdForEdit, updatedPolicy);
|
||||
};
|
||||
|
||||
try {
|
||||
await globalPoliciesAPI.update(policyIdForEdit, updatedPolicy);
|
||||
await updateAPIRequest();
|
||||
dispatch(renderFlash("success", "Policy updated!"));
|
||||
} catch (updateError) {
|
||||
console.error(updateError);
|
||||
|
|
@ -121,7 +147,7 @@ const QueryEditor = ({
|
|||
<span>Back to policies</span>
|
||||
</Link>
|
||||
<PolicyForm
|
||||
onCreatePolicy={onSavePolicyFormSubmit}
|
||||
onCreatePolicy={onCreatePolicy}
|
||||
goToSelectTargets={goToSelectTargets}
|
||||
onOsqueryTableSelect={onOsqueryTableSelect}
|
||||
onUpdate={onUpdatePolicy}
|
||||
|
|
|
|||
|
|
@ -4,13 +4,10 @@ import endpoints from "fleet/endpoints";
|
|||
import { IPolicyFormData } from "interfaces/policy";
|
||||
|
||||
export default {
|
||||
create: (data: number | IPolicyFormData) => {
|
||||
// TODO: How does the frontend need to support legacy policies?
|
||||
create: (data: IPolicyFormData) => {
|
||||
const { GLOBAL_POLICIES } = endpoints;
|
||||
|
||||
if (typeof data === "number") {
|
||||
return sendRequest("POST", GLOBAL_POLICIES, { query_id: data });
|
||||
}
|
||||
|
||||
return sendRequest("POST", GLOBAL_POLICIES, data);
|
||||
},
|
||||
destroy: (ids: number[]) => {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,24 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import sendRequest from "services";
|
||||
import endpoints from "fleet/endpoints";
|
||||
import { IPolicyFormData } from "interfaces/policy";
|
||||
// const endpoints = { TEAMS: "/v1/fleet/team" };
|
||||
|
||||
export default {
|
||||
create: (team_id: number, query_id: number) => {
|
||||
// TODO: How does the frontend need to support legacy policies?
|
||||
create: (data: IPolicyFormData) => {
|
||||
const { name, description, query, team_id } = data;
|
||||
const { TEAMS } = endpoints;
|
||||
const path = `${TEAMS}/${team_id}/policies`;
|
||||
|
||||
return sendRequest("POST", path, { query_id });
|
||||
return sendRequest("POST", path, { name, description, query });
|
||||
},
|
||||
update: (id: number, data: IPolicyFormData) => {
|
||||
const { name, description, query, team_id } = data;
|
||||
const { TEAMS } = endpoints;
|
||||
const path = `${TEAMS}/${team_id}/policies/${id}`;
|
||||
|
||||
return sendRequest("PATCH", path, { name, description, query });
|
||||
},
|
||||
destroy: (team_id: number, ids: number[]) => {
|
||||
const { TEAMS } = endpoints;
|
||||
|
|
|
|||
Loading…
Reference in a new issue