From 9ab0eb2acd87e9a8882dbbfdfa0bec0a2b92694c Mon Sep 17 00:00:00 2001 From: jacobshandling <61553566+jacobshandling@users.noreply.github.com> Date: Thu, 1 May 2025 11:43:38 -0700 Subject: [PATCH] UI: Update conditional access on a per-policy basis (#28658) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## For #28049 , #28610 - **Implement front end ability to enable or disable conditional access on a per-policy basis** - **Update policy status UI to include new "action required" state, representing a failed policy on a host with conditional access enabled** - Additional improvements Screenshot 2025-04-29 at 1 32 33 PM Screenshot 2025-04-29 at 12 15 39 PM Screenshot 2025-04-29 at 12 15 49 PM updated policies table headers - [x] Changes file added for user-visible changes in `changes/` - [ ] Added/updated automated tests - [x] A detailed QA plan exists on the associated ticket (if it isn't there, work with the product group's QA engineer to add it) - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Jacob Shandling --- changes/28049-conditional-access-fe-updates | 2 + frontend/__mocks__/policyMock.ts | 4 + .../PaginatedList/PaginatedList.tsx | 148 ++++++++++-------- .../components/PaginatedList/_styles.scss | 8 +- .../SoftwareOptionsSelector.tsx | 4 +- .../StatusIndicatorWithIcon.tsx | 11 +- .../TableContainer/DataTable/DataTable.tsx | 5 + .../forms/fields/Checkbox/Checkbox.tsx | 41 ++++- frontend/interfaces/policy.ts | 2 + .../WindowsMdmPage/WindowsMdmPage.tsx | 2 +- .../cards/Advanced/Advanced.tsx | 16 +- .../TeamSettings/TeamSettings.tsx | 2 +- .../TeamHostExpiryToggle.tsx | 2 +- .../details/cards/Policies/HostPolicies.tsx | 3 +- .../HostPoliciesTableConfig.tsx | 80 ++++++---- .../ManagePoliciesPage/ManagePoliciesPage.tsx | 55 +++++-- .../CalendarEventsModal.tsx | 4 +- .../ConditionalAccessModal.tsx | 102 ++++++++---- .../ConditionalAccessModal/_styles.scss | 9 ++ .../InstallSoftwareModal.tsx | 2 +- .../OtherWorkflowsModal.tsx | 4 +- .../PoliciesPaginatedList.tests.tsx | 41 ++++- .../PoliciesPaginatedList.tsx | 126 ++++++++------- .../PoliciesTable/PoliciesTableConfig.tsx | 4 +- .../PolicyRunScriptModal.tsx | 2 +- .../ManagePoliciesPage/helpers.tests.tsx | 4 + .../ManageQueriesPage/ManageQueriesPage.tsx | 4 - frontend/services/entities/team_policies.ts | 3 + frontend/utilities/sort/sort_functions.ts | 17 ++ 29 files changed, 462 insertions(+), 245 deletions(-) create mode 100644 changes/28049-conditional-access-fe-updates create mode 100644 frontend/pages/policies/ManagePoliciesPage/components/ConditionalAccessModal/_styles.scss diff --git a/changes/28049-conditional-access-fe-updates b/changes/28049-conditional-access-fe-updates new file mode 100644 index 0000000000..49af6e91bf --- /dev/null +++ b/changes/28049-conditional-access-fe-updates @@ -0,0 +1,2 @@ +- Add ability to set conditional access per policy, and update host policy UI to incorporate + conditional access data diff --git a/frontend/__mocks__/policyMock.ts b/frontend/__mocks__/policyMock.ts index ba60c55c74..ad569e31d9 100644 --- a/frontend/__mocks__/policyMock.ts +++ b/frontend/__mocks__/policyMock.ts @@ -23,6 +23,7 @@ const DEFAULT_POLICY_MOCK: IPolicyStats = { has_run: true, next_update_ms: 3600000, calendar_events_enabled: true, + conditional_access_enabled: false, install_software: { name: "testSw0", software_title_id: 1, @@ -59,6 +60,7 @@ export const createMockPoliciesResponse = ( has_run: true, next_update_ms: 3600000, calendar_events_enabled: false, + conditional_access_enabled: false, }, { id: 29090, @@ -81,6 +83,7 @@ export const createMockPoliciesResponse = ( has_run: true, next_update_ms: 3600000, calendar_events_enabled: false, + conditional_access_enabled: false, }, { id: 136, @@ -105,6 +108,7 @@ export const createMockPoliciesResponse = ( has_run: true, next_update_ms: 3600000, calendar_events_enabled: false, + conditional_access_enabled: false, }, ], }; diff --git a/frontend/components/PaginatedList/PaginatedList.tsx b/frontend/components/PaginatedList/PaginatedList.tsx index 1e4e81f919..f138826328 100644 --- a/frontend/components/PaginatedList/PaginatedList.tsx +++ b/frontend/components/PaginatedList/PaginatedList.tsx @@ -40,7 +40,11 @@ interface IPaginatedListProps { If string, a key in an item whose truthiness will be checked. if function, a function that given an item, returns a boolean. *required in conjunction with `useCheckBoxes` */ - isSelected?: string | ((item: TItem) => boolean); + isSelected?: string | ((item: TItem) => boolean); // TODO - rename as `isItemSelected + /** How to determine whether an item is disabled */ + isItemDisabled?: (item: TItem) => boolean; + /** How to determine the tooltip to show on hover over the item's checkbox */ + getItemTooltipContent?: (item: TItem) => React.ReactNode; /** Custom function to render the label for an item. */ renderItemLabel?: (item: TItem) => ReactElement | null; /** Custom function to render extra markup (besides the label) in an item row. */ @@ -67,6 +71,8 @@ interface IPaginatedListProps { useCheckBoxes?: boolean; /** Allow the parent to trigger the loading overlay */ ancestralUpdating?: boolean; + /** Help text to display below the list and above the pagination controls */ + helpText?: React.ReactNode; } function PaginatedListInner>( @@ -83,10 +89,13 @@ function PaginatedListInner>( setDirtyOnClickRow = true, onUpdate, isSelected, + isItemDisabled, + getItemTooltipContent, disabled = false, heading, useCheckBoxes = true, ancestralUpdating = false, + helpText, }: IPaginatedListProps, ref: Ref> ) { @@ -206,71 +215,80 @@ function PaginatedListInner>( )} -
-
    - {heading && ( -
  • - {heading} +
      + {heading && ( +
    • + {heading} +
    • + )} + {items.map((_item) => { + // If an item has been marked as changed, use the changed version + // of the item rather than the one from the page fetch. This allows + // us to render an item correctly even after we've navigated away + // from its page and then back again. + const item = dirtyItems[_item[idKey]] ?? _item; + + const itemDisabled = isItemDisabled && isItemDisabled(item); + + const rowClasses = classnames(`${baseClass}__row`, { + [`${baseClass}__row--disabled`]: itemDisabled, + }); + return ( + // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions +
    • { + if (itemDisabled) return; + const clickedItem = onClickRow(item); + if (setDirtyOnClickRow) + setDirtyItems({ + ...dirtyItems, + [item[idKey]]: clickedItem, + }); + }} + > + {useCheckBoxes && isSelected && ( + + {renderItemLabel ? ( + renderItemLabel(item) + ) : ( + {item[labelKey]}} /> + )} + + )} + {renderItemRow && + // If a custom row renderer was supplied, call it with the item value + // as well as the callback the parent can use to indicate changes to an item. + renderItemRow(item, (changedItem) => { + setDirtyItems({ + ...dirtyItems, + [changedItem[idKey]]: changedItem, + }); + })}
    • - )} - {items.map((_item) => { - // If an item has been marked as changed, use the changed version - // of the item rather than the one from the page fetch. This allows - // us to render an item correctly even after we've navigated away - // from its page and then back again. - const item = dirtyItems[_item[idKey]] ?? _item; - return ( - // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -
    • { - const clickedItem = onClickRow(item); - if (setDirtyOnClickRow) - setDirtyItems({ - ...dirtyItems, - [item[idKey]]: clickedItem, - }); - }} - > - {useCheckBoxes && isSelected && ( - - {renderItemLabel ? ( - renderItemLabel(item) - ) : ( - {item[labelKey]}} /> - )} - - )} - {renderItemRow && - // If a custom row renderer was supplied, call it with the item value - // as well as the callback the parent can use to indicate changes to an item. - renderItemRow(item, (changedItem) => { - setDirtyItems({ - ...dirtyItems, - [changedItem[idKey]]: changedItem, - }); - })} -
    • - ); - })} -
    - setCurrentPage(currentPage + 1)} - onPrevPage={() => setCurrentPage(currentPage - 1)} - hidePagination={currentPage === 0 && disableNext} - /> -
+ ); + })} + + {helpText &&

{helpText}

} + setCurrentPage(currentPage + 1)} + onPrevPage={() => setCurrentPage(currentPage - 1)} + hidePagination={currentPage === 0 && disableNext} + /> ); } diff --git a/frontend/components/PaginatedList/_styles.scss b/frontend/components/PaginatedList/_styles.scss index 68b90c200b..5989f0d9f5 100644 --- a/frontend/components/PaginatedList/_styles.scss +++ b/frontend/components/PaginatedList/_styles.scss @@ -1,4 +1,5 @@ .paginated-list { + gap: $pad-medium; &__header { padding: $pad-medium $pad-large; font-size: $x-small; @@ -68,8 +69,7 @@ min-width: 0; /* This is crucial for proper shrinking */ } - - &:hover { + &:not(.paginated-list__row--disabled):hover { background: $ui-off-white; cursor: pointer; @@ -102,4 +102,8 @@ @include disabled; } } + + .pagination { + margin-top: 0; + } } diff --git a/frontend/components/SoftwareOptionsSelector/SoftwareOptionsSelector.tsx b/frontend/components/SoftwareOptionsSelector/SoftwareOptionsSelector.tsx index 77f4817619..715fa63680 100644 --- a/frontend/components/SoftwareOptionsSelector/SoftwareOptionsSelector.tsx +++ b/frontend/components/SoftwareOptionsSelector/SoftwareOptionsSelector.tsx @@ -80,7 +80,7 @@ const SoftwareOptionsSelector = ({ value={formData.selfService} onChange={(newVal: boolean) => onToggleSelfService(newVal)} className={`${baseClass}__self-service-checkbox`} - tooltipContent={!isSelfServiceDisabled && SELF_SERVICE_TOOLTIP} + labelTooltipContent={!isSelfServiceDisabled && SELF_SERVICE_TOOLTIP} disabled={isSelfServiceDisabled} > Self-service @@ -90,7 +90,7 @@ const SoftwareOptionsSelector = ({ value={formData.automaticInstall} onChange={(newVal: boolean) => onToggleAutomaticInstall(newVal)} className={`${baseClass}__automatic-install-checkbox`} - tooltipContent={ + labelTooltipContent={ showAutomaticInstallTooltip && getAutomaticInstallTooltip() } disabled={isAutomaticInstallDisabled} diff --git a/frontend/components/StatusIndicatorWithIcon/StatusIndicatorWithIcon.tsx b/frontend/components/StatusIndicatorWithIcon/StatusIndicatorWithIcon.tsx index 3ef8a5c8f7..8c73af4a42 100644 --- a/frontend/components/StatusIndicatorWithIcon/StatusIndicatorWithIcon.tsx +++ b/frontend/components/StatusIndicatorWithIcon/StatusIndicatorWithIcon.tsx @@ -12,7 +12,9 @@ export type IndicatorStatus = | "successPartial" | "pending" | "pendingPartial" - | "error"; + | "error" + | "failure" + | "actionRequired"; interface IStatusIndicatorWithIconProps { status: IndicatorStatus; @@ -33,6 +35,8 @@ const statusIconNameMapping: Record = { pending: "pending", pendingPartial: "pending-outline", error: "error", + failure: "error-outline", + actionRequired: "error", }; const StatusIndicatorWithIcon = ({ @@ -50,7 +54,10 @@ const StatusIndicatorWithIcon = ({ }); const valueContent = ( - + {value} ); diff --git a/frontend/components/TableContainer/DataTable/DataTable.tsx b/frontend/components/TableContainer/DataTable/DataTable.tsx index 04cefe9050..0469d312c0 100644 --- a/frontend/components/TableContainer/DataTable/DataTable.tsx +++ b/frontend/components/TableContainer/DataTable/DataTable.tsx @@ -227,6 +227,11 @@ const DataTable = ({ ) => { return sort.hasLength(a.values[id], b.values[id]); }, + hostPolicyStatus: ( + a: { values: Record }, + b: { values: Record }, + id: string + ) => sort.hostPolicyStatus(a.values[id], b.values[id]), }), [] ), diff --git a/frontend/components/forms/fields/Checkbox/Checkbox.tsx b/frontend/components/forms/fields/Checkbox/Checkbox.tsx index 0c1de8fac1..585de6b752 100644 --- a/frontend/components/forms/fields/Checkbox/Checkbox.tsx +++ b/frontend/components/forms/fields/Checkbox/Checkbox.tsx @@ -23,7 +23,10 @@ export interface ICheckboxProps { wrapperClassName?: string; indeterminate?: boolean; parseTarget?: boolean; - tooltipContent?: React.ReactNode; + /** to display over the checkbox label */ + labelTooltipContent?: React.ReactNode; + /** to display over the checkbox icon */ + iconTooltipContent?: React.ReactNode; isLeftLabel?: boolean; helpText?: React.ReactNode; /** Use in table action only @@ -44,7 +47,8 @@ const Checkbox = (props: ICheckboxProps) => { wrapperClassName, indeterminate = false, parseTarget, - tooltipContent, + labelTooltipContent, + iconTooltipContent, isLeftLabel, helpText, enableEnterToCheck = false, @@ -111,6 +115,30 @@ const Checkbox = (props: ICheckboxProps) => { return "checkbox-unchecked"; }; + const renderIcon = () => { + const icon = ( + + ); + if (iconTooltipContent) { + return ( + + {icon} + + ); + } + return icon; + }; + return (