diff --git a/src/pages/tenant/reports/graph-office-reports/index.js b/src/pages/tenant/reports/graph-office-reports/index.js index b97505d12ed4..39bb153c06b6 100644 --- a/src/pages/tenant/reports/graph-office-reports/index.js +++ b/src/pages/tenant/reports/graph-office-reports/index.js @@ -64,7 +64,7 @@ const Page = () => { waiting: !!currentTenant, }) - const reportOptions = (reportListApi.data ?? []).map((r) => ({ + const reportOptions = (Array.isArray(reportListApi.data) ? reportListApi.data : []).map((r) => ({ label: prettifyReportName(r.name), value: r.name, type: r.type ?? null, @@ -156,7 +156,7 @@ const Page = () => { ) : ( { const pageTitle = 'Standard & Drift Alignment' + const [granular, setGranular] = useState(false) - const filterList = [ + const summaryFilterList = [ { filterName: 'Drift Templates', value: [{ id: 'standardType', value: 'drift' }], @@ -21,7 +25,25 @@ const Page = () => { }, ] - const actions = [ + const granularFilterList = [ + { + filterName: 'Non-Compliant', + value: [{ id: 'complianceStatus', value: 'Non-Compliant' }], + type: 'column', + }, + { + filterName: 'Compliant', + value: [{ id: 'complianceStatus', value: 'Compliant' }], + type: 'column', + }, + { + filterName: 'License Missing', + value: [{ id: 'complianceStatus', value: 'License Missing' }], + type: 'column', + }, + ] + + const summaryActions = [ { label: 'View Tenant Report', link: '/tenant/manage/applied-standards/?tenantFilter=[tenantFilter]&templateId=[standardId]', @@ -29,6 +51,13 @@ const Page = () => { color: 'info', target: '_self', }, + { + label: 'Edit Template', + link: '/tenant/standards/templates/template?id=[standardId]&type=[standardType]', + icon: , + color: 'success', + target: '_self', + }, { label: 'Manage Drift', link: '/tenant/manage/drift?templateId=[standardId]&tenantFilter=[tenantFilter]', @@ -53,22 +82,399 @@ const Page = () => { }, ] + const granularActions = [ + { + label: 'View Tenant Report', + link: '/tenant/manage/applied-standards/?tenantFilter=[tenantFilter]&templateId=[templateId]', + icon: , + color: 'info', + target: '_self', + }, + { + label: 'Edit Template', + link: '/tenant/standards/templates/template?id=[templateId]&type=[templateType]', + icon: , + color: 'success', + target: '_self', + }, + { + label: 'Manage Drift', + link: '/tenant/manage/drift?templateId=[templateId]&tenantFilter=[tenantFilter]', + icon: , + color: 'info', + target: '_self', + condition: (row) => row.templateType === 'drift', + }, + ] + + const parseValue = (value) => { + if (value === null || value === undefined || value === '') return null + if (typeof value === 'string') { + try { + return JSON.parse(value) + } catch { + return value + } + } + return value + } + + const normalizeObj = (val) => { + if (Array.isArray(val)) return val.map(normalizeObj) + if (val !== null && typeof val === 'object') { + return Object.fromEntries( + Object.keys(val) + .sort() + .map((k) => [k, normalizeObj(val[k])]) + ) + } + return val + } + + const compareValues = (expected, current) => { + if (!expected || !current) return null + try { + const expectedObj = normalizeObj( + typeof expected === 'object' ? expected : JSON.parse(expected) + ) + const currentObj = normalizeObj(typeof current === 'object' ? current : JSON.parse(current)) + if (JSON.stringify(expectedObj) === JSON.stringify(currentObj)) return null + const differences = {} + const allKeys = new Set([...Object.keys(expectedObj), ...Object.keys(currentObj)]) + allKeys.forEach((key) => { + const e = normalizeObj(expectedObj[key]) + const c = normalizeObj(currentObj[key]) + if (JSON.stringify(e) !== JSON.stringify(c)) { + differences[key] = { expected: expectedObj[key], current: currentObj[key] } + } + }) + return Object.keys(differences).length > 0 ? differences : null + } catch { + return null + } + } + + const granularOffCanvas = { + size: 'md', + title: 'Standard Details', + contentPadding: 0, + children: (row) => { + const expectedParsed = parseValue(row.expectedValue) + const currentParsed = parseValue(row.currentValue) + const diffs = compareValues(expectedParsed, currentParsed) + const baseName = row.standardId?.split('.').slice(0, -1).join('.') + const prettyName = + standardsData.find((s) => s.name === row.standardId)?.label ?? + standardsData.find((s) => s.name === baseName)?.label ?? + row.standardName + + const complianceColors = { + compliant: 'success', + 'non-compliant': 'error', + 'license missing': 'warning', + 'reporting disabled': 'default', + } + const statusColor = + complianceColors[String(row.complianceStatus ?? '').toLowerCase()] ?? 'default' + + const properties = [ + { label: 'Standard', value: prettyName }, + { label: 'Status', value: row.complianceStatus, color: statusColor }, + { label: 'Template', value: row.templateName }, + { + label: 'Type', + value: row.standardType === 'drift' ? 'Drift Standard' : 'Classic Standard', + }, + { + label: 'Last Applied', + value: row.latestDataCollection + ? new Date(row.latestDataCollection).toLocaleString() + : 'N/A', + }, + ] + + return ( + + {/* Property list */} + } + sx={{ borderBottom: '1px solid', borderColor: 'divider' }} + > + {properties.map(({ label, value, color }) => ( + + + {label} + + {color ? ( + + ) : ( + + {value ?? 'N/A'} + + )} + + ))} + + + {/* Diff / value content */} + {(expectedParsed || currentParsed) && ( + + {diffs ? ( + <> + + Property Differences + + {Object.entries(diffs).map(([key, { expected, current }]) => ( + + + {key} + + + + + Expected + + + + {JSON.stringify(expected, null, 2)} + + + + + + Current + + + + {JSON.stringify(current, null, 2)} + + + + + + ))} + + ) : ( + <> + {expectedParsed !== null && ( + + + Expected + + + + {typeof expectedParsed === 'object' + ? JSON.stringify(expectedParsed, null, 2) + : String(expectedParsed)} + + + + )} + {currentParsed !== null && ( + + + Current + + + + {typeof currentParsed === 'object' + ? JSON.stringify(currentParsed, null, 2) + : String(currentParsed)} + + + + )} + + )} + + )} + + ) + }, + } + + const modeToggle = ( + + + + ) : ( + + ) + } + label={granular ? 'Per Standard' : 'Summary'} + onClick={() => setGranular((v) => !v)} + color="primary" + variant="filled" + size="small" + clickable + /> + + + ) + return ( ) } diff --git a/src/utils/get-cipp-formatting.js b/src/utils/get-cipp-formatting.js index 93f364ca77b2..b900905202ca 100644 --- a/src/utils/get-cipp-formatting.js +++ b/src/utils/get-cipp-formatting.js @@ -33,6 +33,7 @@ import DOMPurify from 'dompurify' import { getSignInErrorCodeTranslation } from './get-cipp-signin-errorcode-translation' import { CollapsibleChipList } from '../components/CippComponents/CollapsibleChipList' import countryList from '../data/countryList.json' +import standardsData from '../data/standards.json' // Helper function to convert country codes to country names const getCountryNameFromCode = (countryCode) => { @@ -436,6 +437,29 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr )) } } + if (cellName === 'complianceStatus') { + if (isText) return data + const complianceColors = { + compliant: 'success', + 'non-compliant': 'error', + 'license missing': 'warning', + 'reporting disabled': 'default', + } + const color = complianceColors[String(data).toLowerCase()] ?? 'default' + return + } + + if (cellName === 'standardName') { + // Already resolved for templates; do a standards.json lookup for classic standards + if (!data?.startsWith('standards.')) return isText ? data : {data} + const baseName = data.split('.').slice(0, -1).join('.') + const label = + standardsData.find((s) => s.name === data)?.label ?? + standardsData.find((s) => s.name === baseName)?.label ?? + data + return label + } + if (cellName === 'standardType') { return isText ? ( data