From 4a3b61abb5ac0c9dcf7023343be950573778605a Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Fri, 22 May 2026 14:11:42 +0200 Subject: [PATCH] posture check drawer --- .../PostureCheckDrawer.scss | 125 +++++++ .../PostureCheckDrawer/PostureCheckDrawer.tsx | 311 ++++++++++++++++++ .../PostureChecksPage/PostureChecksPage.tsx | 90 +++-- .../PostureChecksPage/PostureChecksTable.tsx | 101 +----- .../PostureChecksPage/postureCheckMenu.ts | 93 ++++++ web/src/pages/PostureChecksPage/style.scss | 6 + web/src/shared/query.ts | 3 +- 7 files changed, 614 insertions(+), 115 deletions(-) create mode 100644 web/src/pages/PostureChecksPage/PostureCheckDrawer/PostureCheckDrawer.scss create mode 100644 web/src/pages/PostureChecksPage/PostureCheckDrawer/PostureCheckDrawer.tsx create mode 100644 web/src/pages/PostureChecksPage/postureCheckMenu.ts diff --git a/web/src/pages/PostureChecksPage/PostureCheckDrawer/PostureCheckDrawer.scss b/web/src/pages/PostureChecksPage/PostureCheckDrawer/PostureCheckDrawer.scss new file mode 100644 index 000000000..682825188 --- /dev/null +++ b/web/src/pages/PostureChecksPage/PostureCheckDrawer/PostureCheckDrawer.scss @@ -0,0 +1,125 @@ +#modals-root > .modal-root .drawer-modal.posture-check-drawer { + width: 500px; + + > .modal-content { + padding: 0; + overflow: hidden; + } +} + +.posture-check-drawer-body { + flex: 1; + min-height: 0; + overflow-y: auto; + padding: 0 var(--spacing-xl); +} + +.drawer-block { + padding: var(--spacing-xl) 0; +} + +.drawer-description { + font: var(--t-body-sm-400); + color: var(--fg-faded); + white-space: pre-wrap; +} + +.drawer-os-section, +.drawer-locations { + .os-name { + font: var(--t-body-sm-500); + color: var(--fg-neutral); + } +} + +.drawer-os-section { + display: flex; + flex-direction: column; + gap: var(--spacing-md); + + .os-header { + display: flex; + align-items: center; + gap: var(--spacing-sm); + } + + .os-rows { + display: flex; + flex-direction: column; + gap: var(--spacing-md); + padding-left: 28px; + } + + .os-row { + display: flex; + align-items: center; + + &:has(.os-row-value-list) { + align-items: flex-start; + } + + .os-row-label { + flex: 0 0 119px; + font: var(--t-body-xs-500); + color: var(--fg-disabled); + } + + .os-row-value { + font: var(--t-body-sm-400); + color: var(--fg-default); + flex: 1; + } + + .os-row-value-list { + display: flex; + flex-direction: column; + gap: var(--spacing-sm); + flex: 1; + + .os-row-value { + flex: unset; + } + } + } +} + +.drawer-locations { + display: flex; + align-items: flex-start; + gap: var(--spacing-md); + + &:has(.locations-empty) { + align-items: center; + } + + .locations-label { + display: flex; + align-items: center; + gap: var(--spacing-sm); + width: 119px; + } + + .locations-value { + flex: 1; + min-width: 0; + } + + .locations-chips { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-md); + } + + .locations-empty { + font: var(--t-body-sm-400); + color: var(--fg-disabled); + } +} + +.posture-check-drawer-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--spacing-xl); + border-top: 1px solid var(--border-disabled); +} diff --git a/web/src/pages/PostureChecksPage/PostureCheckDrawer/PostureCheckDrawer.tsx b/web/src/pages/PostureChecksPage/PostureCheckDrawer/PostureCheckDrawer.tsx new file mode 100644 index 000000000..e595de566 --- /dev/null +++ b/web/src/pages/PostureChecksPage/PostureCheckDrawer/PostureCheckDrawer.tsx @@ -0,0 +1,311 @@ +import './PostureCheckDrawer.scss'; +import { useMutation, useSuspenseQuery } from '@tanstack/react-query'; +import { useNavigate } from '@tanstack/react-router'; +import { Fragment, Suspense } from 'react'; +import { m } from '../../../paraglide/messages'; +import api from '../../../shared/api/api'; +import type { ApiDevicePosture } from '../../../shared/api/types'; +import { Button } from '../../../shared/defguard-ui/components/Button/Button'; +import { ButtonMenu } from '../../../shared/defguard-ui/components/ButtonMenu/MenuButton'; +import { Chip } from '../../../shared/defguard-ui/components/Chip/Chip'; +import { Divider } from '../../../shared/defguard-ui/components/Divider/Divider'; +import { DrawerModal } from '../../../shared/defguard-ui/components/DrawerModal/DrawerModal'; +import { Icon } from '../../../shared/defguard-ui/components/Icon'; +import { IconKind } from '../../../shared/defguard-ui/components/Icon/icon-types'; +import { Snackbar } from '../../../shared/defguard-ui/providers/snackbar/snackbar'; +import { + getDevicePostureQueryOptions, + getLocationsQueryOptions, +} from '../../../shared/query'; +import { buildPostureCheckMenuItems } from '../postureCheckMenu'; +import type { PostureCheckRow } from '../postureChecks'; + +type OsDetailRow = { + label: string; + value: string | string[]; +}; + +type OsSection = { + icon: (typeof IconKind)[keyof typeof IconKind]; + name: string; + rows: OsDetailRow[]; +}; + +const getWindowsSection = ( + rule: Extract, +): OsSection => { + const otherItems: string[] = []; + if (rule.ad_domain_joined_required) otherItems.push('Connected to Active Directory'); + if (rule.antivirus_required) otherItems.push('Antivirus installed'); + if (rule.disk_encryption_required) otherItems.push('Disk encryption enabled'); + + const rows: OsDetailRow[] = []; + if (rule.min_os_version !== null) { + rows.push({ label: 'Version', value: `Windows ${rule.min_os_version} and higher` }); + } + if (rule.windows_security_update_max_age !== null) { + rows.push({ + label: 'Update', + value: `Minimum ${rule.windows_security_update_max_age} month update`, + }); + } + if (otherItems.length > 0) { + rows.push({ label: 'Other', value: otherItems }); + } + + return { icon: IconKind.Windows, name: 'Windows', rows }; +}; + +const getMacosSection = ( + rule: Extract, +): OsSection => { + const otherItems: string[] = []; + if (rule.disk_encryption_required) otherItems.push('Disk encryption enabled'); + if (rule.device_integrity_required) otherItems.push('Device integrity'); + + const rows: OsDetailRow[] = []; + if (rule.min_os_version !== null) { + rows.push({ label: 'Version', value: `macOS ${rule.min_os_version} and higher` }); + } + if (otherItems.length > 0) { + rows.push({ label: 'Other', value: otherItems }); + } + + return { icon: IconKind.Apple, name: 'macOS', rows }; +}; + +const getLinuxSection = ( + rule: Extract, +): OsSection => { + const otherItems: string[] = []; + if (rule.disk_encryption_required) otherItems.push('Disk encryption enabled'); + + const rows: OsDetailRow[] = []; + if (rule.min_kernel_version !== null) { + rows.push({ + label: 'Version', + value: `Kernel ${rule.min_kernel_version} and higher`, + }); + } + if (otherItems.length > 0) { + rows.push({ label: 'Other', value: otherItems }); + } + + return { icon: IconKind.Linux, name: 'Linux', rows }; +}; + +const getIosSection = ( + rule: Extract, +): OsSection => { + const rows: OsDetailRow[] = []; + if (rule.min_os_version !== null) { + rows.push({ label: 'Version', value: `iOS ${rule.min_os_version}+` }); + } + + return { icon: IconKind.AppStore, name: 'iOS', rows }; +}; + +const getAndroidSection = ( + rule: Extract, +): OsSection => { + const otherItems: string[] = []; + if (rule.device_integrity_required) otherItems.push('Device integrity'); + + const rows: OsDetailRow[] = []; + if (rule.min_os_version !== null) { + rows.push({ label: 'Version', value: `Android ${rule.min_os_version}+` }); + } + if (otherItems.length > 0) { + rows.push({ label: 'Other', value: otherItems }); + } + + return { icon: IconKind.Android, name: 'Android', rows }; +}; + +const getDefguardSection = (posture: ApiDevicePosture): OsSection | null => { + if (!posture.min_client_version && !posture.allow_prerelease_client) return null; + + const rows: OsDetailRow[] = []; + if (posture.min_client_version) { + rows.push({ + label: 'Version', + value: `Defguard ${posture.min_client_version} and higher`, + }); + } + if (posture.allow_prerelease_client) { + rows.push({ label: 'Other', value: 'Pre-release allowed' }); + } + + return { icon: IconKind.Defguard, name: 'Defguard', rows }; +}; + +const buildOsSections = (posture: ApiDevicePosture): OsSection[] => { + const sections: OsSection[] = []; + + for (const rule of posture.os_rules ?? []) { + switch (rule.os_type) { + case 'windows': + sections.push(getWindowsSection(rule)); + break; + case 'macos': + sections.push(getMacosSection(rule)); + break; + case 'linux': + sections.push(getLinuxSection(rule)); + break; + case 'ios': + sections.push(getIosSection(rule)); + break; + case 'android': + sections.push(getAndroidSection(rule)); + break; + } + } + + const defguardSection = getDefguardSection(posture); + if (defguardSection) sections.push(defguardSection); + + return sections; +}; + +type ContentProps = { + row: PostureCheckRow; + onClose: () => void; +}; + +const PostureCheckDrawerContent = ({ row, onClose }: ContentProps) => { + const navigate = useNavigate(); + const { data: postureCheck } = useSuspenseQuery(getDevicePostureQueryOptions(row.id)); + const { data: locations } = useSuspenseQuery(getLocationsQueryOptions); + + const { mutate: assignLocations } = useMutation({ + mutationFn: (locationIds: number[]) => + api.devicePosture.setLocationsForDevicePosture(row.id, locationIds), + meta: { + invalidate: [['device-posture'], ['network']], + }, + onSuccess: () => { + Snackbar.default(m.modal_assign_posture_check_locations_success()); + }, + onError: () => { + Snackbar.error(m.modal_assign_posture_check_locations_error()); + }, + }); + + const locationOptions = locations.map((loc) => ({ + id: loc.id, + label: loc.name, + searchFields: [loc.name, ...loc.address], + })); + + const assignedLocationNames = locationOptions + .filter((loc) => row.locations.includes(loc.id)) + .map((loc) => loc.label); + + const osSections = buildOsSections(postureCheck); + + const menuItems = buildPostureCheckMenuItems({ + row, + locationOptions, + navigate, + assignLocations, + onAfterEdit: onClose, + onAfterDelete: onClose, + }); + + return ( + <> +
+ {postureCheck?.description && ( + <> +

{postureCheck.description}

+ + + )} + + {osSections.map((section, idx) => ( + + {idx > 0 && } +
+
+ + {section.name} +
+
+ {section.rows.map((detail) => ( +
+ {detail.label} + {Array.isArray(detail.value) ? ( +
+ {detail.value.map((item) => ( + + {item} + + ))} +
+ ) : ( + {detail.value} + )} +
+ ))} +
+
+
+ ))} + + {(osSections.length > 0 || postureCheck?.description) && } + +
+
+ + Locations +
+
+ {assignedLocationNames.length > 0 ? ( +
+ {assignedLocationNames.map((name) => ( + + ))} +
+ ) : ( + Not used in any location + )} +
+
+
+ +
+ +
+ + ); +}; + +type Props = { + selectedRow: PostureCheckRow | null; + onClose: () => void; +}; + +export const PostureCheckDrawer = ({ selectedRow, onClose }: Props) => { + return ( + + {selectedRow && ( + + + + )} + + ); +}; diff --git a/web/src/pages/PostureChecksPage/PostureChecksPage.tsx b/web/src/pages/PostureChecksPage/PostureChecksPage.tsx index 994b2dce7..4ac237ff3 100644 --- a/web/src/pages/PostureChecksPage/PostureChecksPage.tsx +++ b/web/src/pages/PostureChecksPage/PostureChecksPage.tsx @@ -1,7 +1,12 @@ -import { useInfiniteQuery, useQuery, useSuspenseQuery } from '@tanstack/react-query'; +import { + useInfiniteQuery, + useQuery, + useQueryClient, + useSuspenseQuery, +} from '@tanstack/react-query'; import { useNavigate } from '@tanstack/react-router'; import type { ColumnFiltersState } from '@tanstack/react-table'; -import { Suspense, useMemo, useState } from 'react'; +import { Suspense, useCallback, useMemo, useState } from 'react'; import { m } from '../../paraglide/messages'; import api from '../../shared/api/api'; import { Page } from '../../shared/components/Page/Page'; @@ -17,12 +22,14 @@ import { } from '../../shared/query'; import { canUseEnterpriseFeature, licenseActionCheck } from '../../shared/utils/license'; import { shouldFetchPostureChecksEnterpriseData } from './license'; +import { PostureCheckDrawer } from './PostureCheckDrawer/PostureCheckDrawer'; import { PostureChecksTable } from './PostureChecksTable'; import { getPostureCheckColumnFilterOptions, getPostureCheckTableFilterMessages, mapApiDevicePostureToRow, mapPostureCheckFilterValueToRequestValue, + type PostureCheckRow, } from './postureChecks'; import { getPostureCheckVersionValues } from './types'; @@ -57,6 +64,11 @@ const PostureChecksContent = () => { enabled: shouldFetchPostureChecksEnterpriseData(canUseEnterprise), }); const [columnFilters, setColumnFilters] = useState([]); + const [selectedRow, setSelectedRow] = useState(null); + const queryClient = useQueryClient(); + const handleDrawerClose = useCallback(() => { + setSelectedRow(null); + }, []); const versionValues = useMemo( () => (versionMetadata ? getPostureCheckVersionValues(versionMetadata) : null), [versionMetadata], @@ -101,10 +113,28 @@ const PostureChecksContent = () => { }); const flatQueryData = useMemo(() => data?.pages.flat() ?? null, [data?.pages]); - const postureChecks = useMemo( - () => flatQueryData?.flatMap((page) => page.data).map(mapApiDevicePostureToRow) ?? [], + const flatPostures = useMemo( + () => flatQueryData?.flatMap((page) => page.data) ?? [], [flatQueryData], ); + const postureChecks = useMemo( + () => flatPostures.map(mapApiDevicePostureToRow), + [flatPostures], + ); + const posturesById = useMemo( + () => new Map(flatPostures.map((posture) => [posture.id, posture])), + [flatPostures], + ); + const handleRowClick = useCallback( + (row: PostureCheckRow) => { + const posture = posturesById.get(row.id); + if (posture) { + queryClient.setQueryData(['device-posture', row.id], posture); + } + setSelectedRow(row); + }, + [posturesById, queryClient], + ); const lastItem = flatQueryData ? flatQueryData[flatQueryData.length - 1] : null; const pagination = lastItem ? lastItem.pagination : null; @@ -128,30 +158,34 @@ const PostureChecksContent = () => { } return ( - - {postureChecks.length > 0 || columnFilters.length > 0 ? ( - { - fetchNextPage(); - }} - postureChecks={postureChecks} - /> - ) : ( - - )} - + <> + + {postureChecks.length > 0 || columnFilters.length > 0 ? ( + { + fetchNextPage(); + }} + onRowClick={handleRowClick} + postureChecks={postureChecks} + /> + ) : ( + + )} + + + ); }; diff --git a/web/src/pages/PostureChecksPage/PostureChecksTable.tsx b/web/src/pages/PostureChecksPage/PostureChecksTable.tsx index be9f72bf9..d9388668a 100644 --- a/web/src/pages/PostureChecksPage/PostureChecksTable.tsx +++ b/web/src/pages/PostureChecksPage/PostureChecksTable.tsx @@ -1,5 +1,5 @@ import { useMutation, useSuspenseQuery } from '@tanstack/react-query'; -import { Link, useNavigate } from '@tanstack/react-router'; +import { useNavigate } from '@tanstack/react-router'; import { type ColumnFiltersState, createColumnHelper, @@ -11,12 +11,9 @@ import { import { useMemo } from 'react'; import { m } from '../../paraglide/messages'; import api from '../../shared/api/api'; -import { useSelectionModal } from '../../shared/components/modals/SelectionModal/useSelectionModal'; import { Button } from '../../shared/defguard-ui/components/Button/Button'; import type { ButtonProps } from '../../shared/defguard-ui/components/Button/types'; import { EmptyStateFlexible } from '../../shared/defguard-ui/components/EmptyStateFlexible/EmptyStateFlexible'; -import { IconKind } from '../../shared/defguard-ui/components/Icon'; -import type { MenuItemsGroup } from '../../shared/defguard-ui/components/Menu/types'; import { tableEditColumnSize } from '../../shared/defguard-ui/components/table/consts'; import { TableBody } from '../../shared/defguard-ui/components/table/TableBody/TableBody'; import { TableCell } from '../../shared/defguard-ui/components/table/TableCell/TableCell'; @@ -24,14 +21,9 @@ import { TableEditCell } from '../../shared/defguard-ui/components/table/TableEd import { TableTop } from '../../shared/defguard-ui/components/table/TableTop/TableTop'; import type { TableFilterMessages } from '../../shared/defguard-ui/components/table/types'; import { Snackbar } from '../../shared/defguard-ui/providers/snackbar/snackbar'; -import { openModal } from '../../shared/hooks/modalControls/modalsSubjects'; -import { ModalName } from '../../shared/hooks/modalControls/modalTypes'; import { getLocationsQueryOptions } from '../../shared/query'; -import { - getDeletePostureCheckModalData, - type PostureCheckColumnFilterOptions, - type PostureCheckRow, -} from './postureChecks'; +import { buildPostureCheckMenuItems } from './postureCheckMenu'; +import type { PostureCheckColumnFilterOptions, PostureCheckRow } from './postureChecks'; import './style.scss'; type Props = { @@ -43,6 +35,7 @@ type Props = { loadingNextPage: boolean; onColumnFiltersChange: OnChangeFn; onNextPage: () => void; + onRowClick: (row: PostureCheckRow) => void; postureChecks: PostureCheckRow[]; }; @@ -57,6 +50,7 @@ export const PostureChecksTable = ({ loadingNextPage, onColumnFiltersChange, onNextPage, + onRowClick, postureChecks, }: Props) => { const navigate = useNavigate(); @@ -96,15 +90,12 @@ export const PostureChecksTable = ({ minSize: 306, cell: (info) => ( - onRowClick(info.row.original)} > {info.getValue()} - + ), }), @@ -205,79 +196,19 @@ export const PostureChecksTable = ({ enableResizing: false, cell: (info) => { const row = info.row.original; - const menuItems: MenuItemsGroup[] = [ - { - items: [ - { - text: m.controls_edit(), - icon: 'edit', - onClick: () => { - void navigate({ - to: '/acl/posture-checks/$postureCheckId/edit', - params: { - postureCheckId: String(row.id), - }, - }); - }, - }, - { - text: 'Duplicate', - icon: IconKind.Duplicate, - onClick: () => { - Snackbar.default(`Duplicate is not available yet for "${row.name}".`); - }, - }, - { - text: m.posture_checks_row_menu_assign_locations(), - icon: 'add-location', - onClick: () => { - useSelectionModal.setState({ - isOpen: true, - title: m.modal_assign_posture_check_locations_title(), - options: locationOptions, - selected: new Set(row.locations), - onSubmit: (selected) => { - assignLocations({ - postureCheckId: row.id, - locations: selected as number[], - }); - }, - }); - }, - }, - ], - }, - { - items: [ - { - text: m.controls_delete(), - icon: 'delete', - variant: 'danger', - onClick: () => { - const assignedLocationNames = locationOptions - .filter((location) => row.locations.includes(location.id)) - .map((location) => location.label); - - openModal(ModalName.ConfirmAction, { - ...getDeletePostureCheckModalData(row, assignedLocationNames), - onSuccess: () => { - Snackbar.default(m.modal_delete_posture_check_success()); - }, - onError: () => { - Snackbar.error(m.modal_delete_posture_check_error()); - }, - }); - }, - }, - ], - }, - ]; + const menuItems = buildPostureCheckMenuItems({ + row, + locationOptions, + navigate, + assignLocations: (locations) => + assignLocations({ postureCheckId: row.id, locations }), + }); return ; }, }), ], - [assignLocations, columnFilterOptions, locationOptions, navigate], + [assignLocations, columnFilterOptions, locationOptions, navigate, onRowClick], ); const table = useReactTable({ diff --git a/web/src/pages/PostureChecksPage/postureCheckMenu.ts b/web/src/pages/PostureChecksPage/postureCheckMenu.ts new file mode 100644 index 000000000..451a3a16c --- /dev/null +++ b/web/src/pages/PostureChecksPage/postureCheckMenu.ts @@ -0,0 +1,93 @@ +import type { useNavigate } from '@tanstack/react-router'; +import { m } from '../../paraglide/messages'; +import { useSelectionModal } from '../../shared/components/modals/SelectionModal/useSelectionModal'; +import type { SelectionOption } from '../../shared/components/SelectionSection/type'; +import { IconKind } from '../../shared/defguard-ui/components/Icon'; +import type { MenuItemsGroup } from '../../shared/defguard-ui/components/Menu/types'; +import { Snackbar } from '../../shared/defguard-ui/providers/snackbar/snackbar'; +import { openModal } from '../../shared/hooks/modalControls/modalsSubjects'; +import { ModalName } from '../../shared/hooks/modalControls/modalTypes'; +import { getDeletePostureCheckModalData, type PostureCheckRow } from './postureChecks'; + +type LocationOption = SelectionOption; + +type BuildPostureCheckMenuArgs = { + row: PostureCheckRow; + locationOptions: LocationOption[]; + navigate: ReturnType; + assignLocations: (locationIds: number[]) => void; + onAfterEdit?: () => void; + onAfterDelete?: () => void; +}; + +export const buildPostureCheckMenuItems = ({ + row, + locationOptions, + navigate, + assignLocations, + onAfterEdit, + onAfterDelete, +}: BuildPostureCheckMenuArgs): MenuItemsGroup[] => [ + { + items: [ + { + text: m.controls_edit(), + icon: 'edit', + onClick: () => { + void navigate({ + to: '/acl/posture-checks/$postureCheckId/edit', + params: { postureCheckId: String(row.id) }, + }); + onAfterEdit?.(); + }, + }, + { + text: 'Duplicate', + icon: IconKind.Duplicate, + onClick: () => { + Snackbar.default(`Duplicate is not available yet for "${row.name}".`); + }, + }, + { + text: m.posture_checks_row_menu_assign_locations(), + icon: 'add-location', + onClick: () => { + useSelectionModal.setState({ + isOpen: true, + title: m.modal_assign_posture_check_locations_title(), + options: locationOptions, + selected: new Set(row.locations), + onSubmit: (selected) => { + assignLocations(selected as number[]); + }, + }); + }, + }, + ], + }, + { + items: [ + { + text: m.controls_delete(), + icon: 'delete', + variant: 'danger', + onClick: () => { + const assignedLocationNames = locationOptions + .filter((location) => row.locations.includes(location.id)) + .map((location) => location.label); + + openModal(ModalName.ConfirmAction, { + ...getDeletePostureCheckModalData(row, assignedLocationNames), + onSuccess: () => { + Snackbar.default(m.modal_delete_posture_check_success()); + onAfterDelete?.(); + }, + onError: () => { + Snackbar.error(m.modal_delete_posture_check_error()); + }, + }); + }, + }, + ], + }, +]; diff --git a/web/src/pages/PostureChecksPage/style.scss b/web/src/pages/PostureChecksPage/style.scss index d78d91936..8fa53ffa2 100644 --- a/web/src/pages/PostureChecksPage/style.scss +++ b/web/src/pages/PostureChecksPage/style.scss @@ -6,4 +6,10 @@ text-overflow: ellipsis; font: var(--t-body-sm-400); color: var(--fg-action); + background: none; + border: none; + padding: 0; + cursor: pointer; + text-align: left; + text-decoration: underline; } diff --git a/web/src/shared/query.ts b/web/src/shared/query.ts index cbe371d62..b8655a38e 100644 --- a/web/src/shared/query.ts +++ b/web/src/shared/query.ts @@ -219,9 +219,8 @@ export const getDevicePostureVersionMetadataQueryOptions = queryOptions({ export const getDevicePostureQueryOptions = (id: number) => queryOptions({ - queryFn: () => api.devicePosture.getDevicePosture(id), + queryFn: () => api.devicePosture.getDevicePosture(id).then((resp) => resp.data), queryKey: ['device-posture', id], - select: (resp) => resp.data, }); export const getOpenIdProvidersQueryOptions = queryOptions({