From 973571fdc41fd61224b08321ef2cccecfaadd9cf Mon Sep 17 00:00:00 2001 From: Esco Date: Thu, 27 Mar 2025 14:34:42 +0100 Subject: [PATCH 01/41] feat: Phishing Simulation Configuration standard remove policy name --- src/data/standards.json | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/data/standards.json b/src/data/standards.json index 952616f3853a..172a7f87e613 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -2018,6 +2018,44 @@ "powershellEquivalent": "Set-AtpPolicyForO365", "recommendedBy": ["CIS"] }, + { + "name": "standards.PhishingSimulations", + "cat": "Defender Standards", + "tag": [], + "helpText": "This creates a phishing simulation policy that enables phishing simulations for the entire tenant.", + "addedComponent": [ + { + "type": "autoComplete", + "multiple": true, + "creatable": true, + "required": true, + "label": "Phishing Simulation Domains", + "name": "standards.PhishingSimulations.Domains" + }, + { + "type": "autoComplete", + "multiple": true, + "creatable": true, + "required": true, + "label": "Phishing Simulation Sender IP Ranges", + "name": "standards.PhishingSimulations.SenderIpRanges" + }, + { + "type": "autoComplete", + "multiple": true, + "creatable": true, + "required": false, + "label": "Phishing Simulation Urls", + "name": "standards.PhishingSimulations.PhishingSimUrls" + } + ], + "label": "Phishing Simulation Configuration", + "impact": "Medium Impact", + "impactColour": "info", + "addedDate": "2025-03-27", + "powershellEquivalent": "New-TenantAllowBlockListItems, New-PhishSimOverridePolicy and New-ExoPhishSimOverrideRule", + "recommendedBy": [] + }, { "name": "standards.MalwareFilterPolicy", "cat": "Defender Standards", From 9a0c926adebfd8766f773733d0daca6717e08498 Mon Sep 17 00:00:00 2001 From: Esco Date: Fri, 28 Mar 2025 14:54:06 +0100 Subject: [PATCH 02/41] feat: Spoof Intelligence standard shameful typo --- src/data/standards.json | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/data/standards.json b/src/data/standards.json index 71d668bf8bca..d47ac7edca66 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -2099,6 +2099,28 @@ "powershellEquivalent": "Set-MalwareFilterPolicy or New-MalwareFilterPolicy", "recommendedBy": ["CIS"] }, + { + "name": "standards.PhishSimSpoofIntelligence", + "cat": "Defender Standards", + "tag": [], + "helpText": "This adds allowed domains to the Spoof Intelligence Allow/Block List.", + "addedComponent": [ + { + "type": "autoComplete", + "multiple": true, + "creatable": true, + "required": false, + "label": "Allowed Domains", + "name": "standards.PhishSimSpoofIntelligence.AllowedDomains" + } + ], + "label": "Add allowed domains to Spoof Intelligence", + "impact": "Medium Impact", + "impactColour": "info", + "addedDate": "2025-03-28", + "powershellEquivalent": "New-TenantAllowBlockListSpoofItems", + "recommendedBy": [] + }, { "name": "standards.SpamFilterPolicy", "cat": "Defender Standards", From 9921e1b7df3725e933915fdcd4aeff34292bd2a4 Mon Sep 17 00:00:00 2001 From: Esco Date: Mon, 31 Mar 2025 14:50:11 +0200 Subject: [PATCH 03/41] feat: DefaultPlatformRestrictions standard Update standards.json --- src/data/standards.json | 74 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/src/data/standards.json b/src/data/standards.json index 71d668bf8bca..bda027c24276 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -2545,6 +2545,80 @@ "powershellEquivalent": "Graph API", "recommendedBy": [] }, + { + "name": "standards.DefaultPlatformRestrictions", + "cat": "Intune Standards", + "tag": [], + "helpText": "Sets the default platform restrictions for enrolling devices into Intune. Note: Do not block personally owned if platform is blocked.", + "addedComponent": [ + { + "type": "switch", + "name": "standards.DefaultPlatformRestrictions.platformAndroidForWorkBlocked", + "label": "Block platform Android Enterprise (work profile)", + "default": false + }, + { + "type": "switch", + "name": "standards.DefaultPlatformRestrictions.personalAndroidForWorkBlocked", + "label": "Block personally owned Android Enterprise (work profile)", + "default": false + }, + { + "type": "switch", + "name": "standards.DefaultPlatformRestrictions.platformAndroidBlocked", + "label": "Block platform Android", + "default": false + }, + { + "type": "switch", + "name": "standards.DefaultPlatformRestrictions.personalAndroidBlocked", + "label": "Block personally owned Android", + "default": false + }, + { + "type": "switch", + "name": "standards.DefaultPlatformRestrictions.platformiOSBlocked", + "label": "Block platform iOS", + "default": false + }, + { + "type": "switch", + "name": "standards.DefaultPlatformRestrictions.personaliOSBlocked", + "label": "Block personally owned iOS", + "default": false + }, + { + "type": "switch", + "name": "standards.DefaultPlatformRestrictions.platformMacOSBlocked", + "label": "Block platform macOS", + "default": false + }, + { + "type": "switch", + "name": "standards.DefaultPlatformRestrictions.personalMacOSBlocked", + "label": "Block personally owned macOS", + "default": false + }, + { + "type": "switch", + "name": "standards.DefaultPlatformRestrictions.platformWindowsBlocked", + "label": "Block platform Windows", + "default": false + }, + { + "type": "switch", + "name": "standards.DefaultPlatformRestrictions.personalWindowsBlocked", + "label": "Block personally owned Windows", + "default": false + } + ], + "label": "Device enrollment restrictions", + "impact": "Low Impact", + "impactColour": "info", + "addedDate": "2025-04-01", + "powershellEquivalent": "Graph API", + "recommendedBy": [] + }, { "name": "standards.intuneDeviceReg", "cat": "Intune Standards", From fcd89d4830a71233b1f68aa25ecdb83681c97642 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 31 Mar 2025 11:43:53 -0400 Subject: [PATCH 04/41] fix multiple link clicks --- src/components/CippComponents/CippApiDialog.jsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/CippComponents/CippApiDialog.jsx b/src/components/CippComponents/CippApiDialog.jsx index 865e28e38d6c..779cca813dda 100644 --- a/src/components/CippComponents/CippApiDialog.jsx +++ b/src/components/CippComponents/CippApiDialog.jsx @@ -284,6 +284,10 @@ export const CippApiDialog = (props) => { const [linkClicked, setLinkClicked] = useState(false); + useEffect(() => { + setLinkClicked(false); + }, [api.link]); + useEffect(() => { if (api.link && !linkClicked && row && Object.keys(row).length > 0) { const timeoutId = setTimeout(() => { From ae64d69f25e30f779c45bf2ea6ea7f06f9825ee2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Mon, 31 Mar 2025 20:59:23 +0200 Subject: [PATCH 05/41] Add enable/disable mailbox rule actions with confirmation prompts --- .../administration/mailbox-rules/index.js | 33 +++++++++++++++++- .../administration/users/user/exchange.jsx | 34 +++++++++++++++++-- 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/src/pages/email/administration/mailbox-rules/index.js b/src/pages/email/administration/mailbox-rules/index.js index e25ba44d8ae6..8dc0fcceb020 100644 --- a/src/pages/email/administration/mailbox-rules/index.js +++ b/src/pages/email/administration/mailbox-rules/index.js @@ -4,14 +4,45 @@ import { TrashIcon } from "@heroicons/react/24/outline"; import { getCippTranslation } from "../../../../utils/get-cipp-translation"; import { getCippFormatting } from "../../../../utils/get-cipp-formatting"; import { CippPropertyListCard } from "../../../../components/CippCards/CippPropertyListCard"; +import { Block, PlayArrow, DeleteForever } from "@mui/icons-material"; const Page = () => { const pageTitle = "Mailbox Rules"; const actions = [ + { + label: "Enable Mailbox Rule", + type: "POST", + icon: , + url: "/api/ExecSetMailboxRule", + data: { + ruleId: "Identity", + userPrincipalName: "UserPrincipalName", + ruleName: "Name", + Enable: true, + }, + condition: (row) => !row.Enabled, + confirmText: "Are you sure you want to enable this mailbox rule?", + multiPost: false, + }, + { + label: "Disable Mailbox Rule", + type: "POST", + icon: , + url: "/api/ExecSetMailboxRule", + data: { + ruleId: "Identity", + userPrincipalName: "UserPrincipalName", + ruleName: "Name", + Disable: true, + }, + condition: (row) => row.Enabled, + confirmText: "Are you sure you want to disable this mailbox rule?", + multiPost: false, + }, { label: "Remove Mailbox Rule", type: "POST", - icon: , + icon: , url: "/api/ExecRemoveMailboxRule", data: { ruleId: "Identity", userPrincipalName: "UserPrincipalName", ruleName: "Name" }, confirmText: "Are you sure you want to remove this mailbox rule?", diff --git a/src/pages/identity/administration/users/user/exchange.jsx b/src/pages/identity/administration/users/user/exchange.jsx index 136636349783..9d3c263f3b95 100644 --- a/src/pages/identity/administration/users/user/exchange.jsx +++ b/src/pages/identity/administration/users/user/exchange.jsx @@ -18,7 +18,7 @@ import CippExchangeSettingsForm from "../../../../../components/CippFormPages/Ci import { useForm } from "react-hook-form"; import { Alert, Button, Collapse, CircularProgress, Typography } from "@mui/material"; import { CippApiResults } from "../../../../../components/CippComponents/CippApiResults"; -import { TrashIcon } from "@heroicons/react/24/outline"; +import { Block, PlayArrow, DeleteForever } from "@mui/icons-material"; import { CippPropertyListCard } from "../../../../../components/CippCards/CippPropertyListCard"; import { getCippTranslation } from "../../../../../utils/get-cipp-translation"; import { getCippFormatting } from "../../../../../utils/get-cipp-formatting"; @@ -188,10 +188,40 @@ const Page = () => { ]; const mailboxRuleActions = [ + { + label: "Enable Mailbox Rule", + type: "POST", + icon: , + url: "/api/ExecSetMailboxRule", + data: { + ruleId: "Identity", + userPrincipalName: graphUserRequest.data?.[0]?.userPrincipalName, + ruleName: "Name", + Enable: true, + }, + condition: (row) => !row.Enabled, + confirmText: "Are you sure you want to enable this mailbox rule?", + multiPost: false, + }, + { + label: "Disable Mailbox Rule", + type: "POST", + icon: , + url: "/api/ExecSetMailboxRule", + data: { + ruleId: "Identity", + userPrincipalName: graphUserRequest.data?.[0]?.userPrincipalName, + ruleName: "Name", + Disable: true, + }, + condition: (row) => row.Enabled, + confirmText: "Are you sure you want to disable this mailbox rule?", + multiPost: false, + }, { label: "Remove Mailbox Rule", type: "POST", - icon: , + icon: , url: "/api/ExecRemoveMailboxRule", data: { ruleId: "Identity", From b3db8ed546c0e3722bd5593f8599378fbd0ef2b9 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 1 Apr 2025 21:15:14 -0400 Subject: [PATCH 06/41] object flattening for better CSV export --- src/components/csvExportButton.js | 48 ++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/src/components/csvExportButton.js b/src/components/csvExportButton.js index c09474b0472c..266dbd443508 100644 --- a/src/components/csvExportButton.js +++ b/src/components/csvExportButton.js @@ -8,32 +8,60 @@ const csvConfig = mkConfig({ useKeysAsHeaders: true, }); +const flattenObject = (obj, parentKey = "") => { + const flattened = {}; + Object.keys(obj).forEach((key) => { + const fullKey = parentKey ? `${parentKey}.${key}` : key; + if (typeof obj[key] === "object" && obj[key] !== null && !Array.isArray(obj[key])) { + Object.assign(flattened, flattenObject(obj[key], fullKey)); + } else if (Array.isArray(obj[key])) { + obj[key].forEach((item, index) => { + if (typeof item === "object" && item !== null) { + Object.assign(flattened, flattenObject(item, `${fullKey}[${index}]`)); + } else { + flattened[`${fullKey}[${index}]`] = item; + } + }); + } else { + flattened[fullKey] = obj[key]; + } + }); + return flattened; +}; + export const CSVExportButton = (props) => { const { rows, columns, reportName, columnVisibility, ...other } = props; const handleExportRows = (rows) => { - const rowData = rows.map((row) => row.original); + const rowData = rows.map((row) => flattenObject(row.original)); const columnKeys = columns.filter((c) => columnVisibility[c.id]).map((c) => c.id); - rowData.forEach((row) => { - Object.keys(row).forEach((key) => { - if (!columnKeys.includes(key)) { - delete row[key]; + + const filterRowData = (row, allowedKeys) => { + const filteredRow = {}; + allowedKeys.forEach((key) => { + if (key in row) { + filteredRow[key] = row[key]; } }); - }); + return filteredRow; + }; + + const filteredData = rowData.map((row) => filterRowData(row, columnKeys)); - //for every existing row, get the valid formatting using getCippFormatting. - const formattedData = rowData.map((row) => { + const formattedData = filteredData.map((row) => { const formattedRow = {}; - Object.keys(row).forEach((key) => { - formattedRow[key] = getCippFormatting(row[key], key, "text", false); + columnKeys.forEach((key) => { + const value = row[key]; + formattedRow[key] = getCippFormatting(value, key, "text", false); }); return formattedRow; }); + const csv = generateCsv(csvConfig)(formattedData); csvConfig["filename"] = `${reportName}`; download(csvConfig)(csv); }; + return ( From 555827057b94c146f562465ae94b236ba8c09846 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 2 Apr 2025 00:32:48 -0400 Subject: [PATCH 07/41] handle formatting on nested objects --- src/components/csvExportButton.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/components/csvExportButton.js b/src/components/csvExportButton.js index 266dbd443508..5e7424dad1c8 100644 --- a/src/components/csvExportButton.js +++ b/src/components/csvExportButton.js @@ -15,13 +15,18 @@ const flattenObject = (obj, parentKey = "") => { if (typeof obj[key] === "object" && obj[key] !== null && !Array.isArray(obj[key])) { Object.assign(flattened, flattenObject(obj[key], fullKey)); } else if (Array.isArray(obj[key])) { - obj[key].forEach((item, index) => { - if (typeof item === "object" && item !== null) { - Object.assign(flattened, flattenObject(item, `${fullKey}[${index}]`)); - } else { - flattened[`${fullKey}[${index}]`] = item; - } - }); + // Handle arrays of objects by applying the formatter on each property + flattened[fullKey] = obj[key] + .map((item) => + typeof item === "object" + ? JSON.stringify( + Object.fromEntries( + Object.entries(flattenObject(item)).map(([k, v]) => [k, getCippFormatting(v, k, "text", false)]) + ) + ) + : getCippFormatting(item, fullKey, "text", false) + ) + .join(", "); } else { flattened[fullKey] = obj[key]; } @@ -52,6 +57,7 @@ export const CSVExportButton = (props) => { const formattedRow = {}; columnKeys.forEach((key) => { const value = row[key]; + // Pass flattened data to the formatter for CSV export formattedRow[key] = getCippFormatting(value, key, "text", false); }); return formattedRow; From f0d1ce9cf63b0b6676af9e23ec46849e9989b221 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 2 Apr 2025 12:54:39 -0400 Subject: [PATCH 08/41] sort integration companies --- src/components/CippIntegrations/CippIntegrationTenantMapping.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/CippIntegrations/CippIntegrationTenantMapping.jsx b/src/components/CippIntegrations/CippIntegrationTenantMapping.jsx index 53ed74c6e822..d495bc370e6c 100644 --- a/src/components/CippIntegrations/CippIntegrationTenantMapping.jsx +++ b/src/components/CippIntegrations/CippIntegrationTenantMapping.jsx @@ -197,6 +197,7 @@ const CippIntegrationSettings = ({ children }) => { creatable={false} multiple={false} isFetching={mappings.isFetching} + sortOptions={true} /> From 82a3180ede70abf3b2b2aced463377b658cfc86f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Wed, 2 Apr 2025 22:37:23 +0200 Subject: [PATCH 09/41] feat: add litigation hold action to CippExchangeActions component --- .../CippComponents/CippExchangeActions.jsx | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/components/CippComponents/CippExchangeActions.jsx b/src/components/CippComponents/CippExchangeActions.jsx index 667b5b6fd686..46b583fdf766 100644 --- a/src/components/CippComponents/CippExchangeActions.jsx +++ b/src/components/CippComponents/CippExchangeActions.jsx @@ -15,8 +15,8 @@ import { Key, PostAdd, Add, + Gavel, } from "@mui/icons-material"; -import { useSettings } from "/src/hooks/use-settings.js"; export const CippExchangeActions = () => { // const tenant = useSettings().currentTenant; @@ -167,6 +167,27 @@ export const CippExchangeActions = () => { condition: (row) => row.MessageCopyForSentAsEnabled === true && row.recipientTypeDetails === "SharedMailbox", }, + { + label: "Set Litigation Hold", + type: "POST", + url: "/api/ExecSetLitigationHold", + data: { UPN: "UPN", Id: "Id" }, + confirmText: "Are you sure you want to set litigation hold?", + icon: , + fields: [ + { + type: "switch", + name: "disable", + label: "Disable Litigation Hold", + }, + { + type: "number", + name: "days", + label: "Hold Duration (Days)", + placeholder: "e.g. 30. 0 for indefinite", + }, + ], + }, { label: "Set mailbox locale", type: "POST", From 9b34ba6ed3576d60e4699e008dd0cd9a9017f651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Thu, 3 Apr 2025 21:17:19 +0200 Subject: [PATCH 10/41] fix: update litigation hold action to use 'Identity' and add condition for licensed users --- src/components/CippComponents/CippExchangeActions.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/CippComponents/CippExchangeActions.jsx b/src/components/CippComponents/CippExchangeActions.jsx index 46b583fdf766..0f20fe2b9cc4 100644 --- a/src/components/CippComponents/CippExchangeActions.jsx +++ b/src/components/CippComponents/CippExchangeActions.jsx @@ -171,9 +171,10 @@ export const CippExchangeActions = () => { label: "Set Litigation Hold", type: "POST", url: "/api/ExecSetLitigationHold", - data: { UPN: "UPN", Id: "Id" }, + data: { UPN: "UPN", Identity: "Id" }, confirmText: "Are you sure you want to set litigation hold?", icon: , + condition: (row) => row.LicensedForLitigationHold === true, fields: [ { type: "switch", @@ -184,7 +185,7 @@ export const CippExchangeActions = () => { type: "number", name: "days", label: "Hold Duration (Days)", - placeholder: "e.g. 30. 0 for indefinite", + placeholder: "Blank or 0 for indefinite", }, ], }, From 6dc321c792693bae4c8b436bb99a0cbb9986959e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Thu, 3 Apr 2025 21:23:32 +0200 Subject: [PATCH 11/41] Better text maybe? --- src/components/CippComponents/CippExchangeActions.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CippComponents/CippExchangeActions.jsx b/src/components/CippComponents/CippExchangeActions.jsx index 0f20fe2b9cc4..0cce26aec516 100644 --- a/src/components/CippComponents/CippExchangeActions.jsx +++ b/src/components/CippComponents/CippExchangeActions.jsx @@ -172,7 +172,7 @@ export const CippExchangeActions = () => { type: "POST", url: "/api/ExecSetLitigationHold", data: { UPN: "UPN", Identity: "Id" }, - confirmText: "Are you sure you want to set litigation hold?", + confirmText: "What do you want to set the Litigation Hold to?", icon: , condition: (row) => row.LicensedForLitigationHold === true, fields: [ From eb7613a202c751203953f00668dc23eaa199d877 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 4 Apr 2025 10:42:29 -0400 Subject: [PATCH 12/41] fix replace text on JSX elements in CippApiDialog --- .../CippComponents/CippApiDialog.jsx | 43 +++++++++++++------ .../CippComponents/CippGdapActions.jsx | 2 +- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/src/components/CippComponents/CippApiDialog.jsx b/src/components/CippComponents/CippApiDialog.jsx index 779cca813dda..4cba7dd8d108 100644 --- a/src/components/CippComponents/CippApiDialog.jsx +++ b/src/components/CippComponents/CippApiDialog.jsx @@ -3,7 +3,7 @@ import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Grid } from import { Stack } from "@mui/system"; import { CippApiResults } from "./CippApiResults"; import { ApiGetCall, ApiPostCall } from "../../api/ApiCall"; -import { useEffect, useState } from "react"; +import React, { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { useSettings } from "../../hooks/use-settings"; import CippFormComponent from "./CippFormComponent"; @@ -323,18 +323,37 @@ export const CippApiDialog = (props) => { }; var confirmText; - if (typeof api?.confirmText === "string" && !Array.isArray(row)) { - confirmText = api.confirmText.replace(/\[([^\]]+)\]/g, (_, key) => { - return getNestedValue(row, key) || `[${key}]`; - }); - } else if (Array.isArray(row) && row.length > 1) { - confirmText = api.confirmText.replace(/\[([^\]]+)\]/g, "the selected rows"); - } else if (Array.isArray(row) && row.length === 1) { - confirmText = api.confirmText.replace(/\[([^\]]+)\]/g, (_, key) => { - return getNestedValue(row[0], key) || `[${key}]`; - }); + if (typeof api?.confirmText === "string") { + if (!Array.isArray(row)) { + confirmText = api.confirmText.replace(/\[([^\]]+)\]/g, (_, key) => { + return getNestedValue(row, key) || `[${key}]`; + }); + } else if (row.length > 1) { + confirmText = api.confirmText.replace(/\[([^\]]+)\]/g, "the selected rows"); + } else if (row.length === 1) { + confirmText = api.confirmText.replace(/\[([^\]]+)\]/g, (_, key) => { + return getNestedValue(row[0], key) || `[${key}]`; + }); + } } else { - confirmText = api.confirmText; + // Handle JSX/Component confirmText + const replaceTextInElement = (element) => { + if (!element) return element; + if (typeof element === 'string') { + if (Array.isArray(row) && row.length > 1) { + return element.replace(/\[([^\]]+)\]/g, "the selected rows"); + } else if (Array.isArray(row) && row.length === 1) { + return element.replace(/\[([^\]]+)\]/g, (_, key) => getNestedValue(row[0], key) || `[${key}]`); + } + return element.replace(/\[([^\]]+)\]/g, (_, key) => getNestedValue(row, key) || `[${key}]`); + } + if (React.isValidElement(element)) { + const newChildren = React.Children.map(element.props.children, replaceTextInElement); + return React.cloneElement(element, {}, newChildren); + } + return element; + }; + confirmText = replaceTextInElement(api?.confirmText); } return ( diff --git a/src/components/CippComponents/CippGdapActions.jsx b/src/components/CippComponents/CippGdapActions.jsx index d2ef3a41e531..b2200cd64b7b 100644 --- a/src/components/CippComponents/CippGdapActions.jsx +++ b/src/components/CippComponents/CippGdapActions.jsx @@ -82,7 +82,7 @@ export const CippGdapActions = () => [ confirmText: ( <> - Are you sure you want to reset the role mappings for this relationship? + Are you sure you want to reset the role mappings for [customer.displayName]? Resetting GDAP role mappings will perform the following actions: From 816201b2f339bdc57558e12867deef678cba5d89 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 4 Apr 2025 13:33:03 -0400 Subject: [PATCH 13/41] offcanvas support for CippInfoBar --- src/components/CippCards/CippInfoBar.jsx | 150 +++++++++++------- .../tenant/gdap-management/offboarding.js | 27 +++- 2 files changed, 123 insertions(+), 54 deletions(-) diff --git a/src/components/CippCards/CippInfoBar.jsx b/src/components/CippCards/CippInfoBar.jsx index c7ce557fdfbe..bcdf4930475f 100644 --- a/src/components/CippCards/CippInfoBar.jsx +++ b/src/components/CippCards/CippInfoBar.jsx @@ -1,58 +1,102 @@ +import React, { useState } from "react"; import { Box, Card, Stack, SvgIcon, Typography, Skeleton } from "@mui/material"; import Grid from "@mui/material/Grid"; +import { CippOffCanvas } from "../CippComponents/CippOffCanvas"; +import { CippPropertyListCard } from "./CippPropertyListCard"; -export const CippInfoBar = ({ data, isFetching }) => ( - - - {data.map((item) => ( - ({ - xs: `1px solid ${theme.palette.divider}`, - md: "none", - }), - borderRight: (theme) => ({ - md: `1px solid ${theme.palette.divider}`, - }), - "&:nth-of-type(3)": { - borderBottom: (theme) => ({ - xs: `1px solid ${theme.palette.divider}`, - sm: "none", - }), - }, - "&:nth-of-type(4)": { - borderBottom: "none", - borderRight: "none", - }, - }} - > - - {item?.icon && ( - - {item.icon} - - )} - { - if (!item?.icon) { - return { pl: 2 }; - } +export const CippInfoBar = ({ data, isFetching }) => { + const [visibleIndex, setVisibleIndex] = useState(null); + + return ( + + + {data.map((item, index) => ( + <> + setVisibleIndex(index) : undefined} + sx={{ + cursor: item.offcanvas ? "pointer" : "default", + borderBottom: (theme) => ({ + xs: `1px solid ${theme.palette.divider}`, + md: "none", + }), + borderRight: (theme) => ({ + md: `1px solid ${theme.palette.divider}`, + }), + "&:nth-of-type(3)": { + borderBottom: (theme) => ({ + xs: `1px solid ${theme.palette.divider}`, + sm: "none", + }), + }, + "&:nth-of-type(4)": { + borderBottom: "none", + borderRight: "none", + }, }} > - - {item.name} - - - {isFetching ? : item.data} - - - - - ))} - - -); + + {item?.icon && ( + + {item.icon} + + )} + { + if (!item?.icon) { + return { pl: 2 }; + } + }} + > + + {item.name} + + + {isFetching ? : item.data} + + + + + {item.offcanvas && ( + <> + {console.log("item.offcanvas", item.offcanvas)} + setVisibleIndex(null)} + > + + + + {item?.offcanvas?.propertyItems?.length > 0 && ( + + )} + + + + + + )} + + ))} + + + ); +}; diff --git a/src/pages/tenant/gdap-management/offboarding.js b/src/pages/tenant/gdap-management/offboarding.js index 57a6d31f381b..bba83bba45aa 100644 --- a/src/pages/tenant/gdap-management/offboarding.js +++ b/src/pages/tenant/gdap-management/offboarding.js @@ -137,6 +137,15 @@ const Page = () => { (relationship) => relationship?.customer?.tenantId === tenantId.value )?.length ?? 0, icon: , + offcanvas: { + title: "GDAP Relationships", + propertyItems: gdapRelationships.data?.Results + ?.filter((relationship) => relationship?.customer?.tenantId === tenantId.value) + ?.map((relationship) => ({ + label: `Relationship: ${relationship?.displayName}`, + value: `Id: ${relationship?.id}`, + })), + }, }, { name: "CSP Contract", @@ -152,11 +161,27 @@ const Page = () => { name: "MSP Applications", data: mspApps.data?.Results?.length ?? 0, icon: , + offcanvas: { + title: "MSP Applications", + propertyItems: mspApps.data?.Results?.map((app) => ({ + label: app?.displayName, + value: app?.appId, + })), + }, }, { name: "Vendor Applications", - data: 0, + data: vendorApps.data?.pages?.reduce((sum, page) => sum + (page?.Results?.length ?? 0), 0) ?? 0, icon: , + offcanvas: { + title: "Vendor Applications", + propertyItems: vendorApps.data?.pages + ?.reduce((sum, page) => sum.concat(page?.Results ?? []), []) + .map((app) => ({ + label: app?.displayName, + value: app?.appId, + })), + } }, ]} /> From 7e9fc57bbfce23088b9602e4d9740e48461ee56c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Fri, 4 Apr 2025 19:52:05 +0200 Subject: [PATCH 14/41] feat: add rename ap device action with validation --- .../endpoint/autopilot/list-devices/index.js | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/pages/endpoint/autopilot/list-devices/index.js b/src/pages/endpoint/autopilot/list-devices/index.js index 038e5b9d8d2a..2721b68924f9 100644 --- a/src/pages/endpoint/autopilot/list-devices/index.js +++ b/src/pages/endpoint/autopilot/list-devices/index.js @@ -2,7 +2,7 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { CippApiDialog } from "/src/components/CippComponents/CippApiDialog.jsx"; import { Button } from "@mui/material"; -import { PersonAdd, Delete, Sync, Add } from "@mui/icons-material"; +import { PersonAdd, Delete, Sync, Add, Edit } from "@mui/icons-material"; import { useDialog } from "../../../../hooks/use-dialog"; import Link from "next/link"; import { useState } from "react"; @@ -51,6 +51,44 @@ const Page = () => { confirmText: "Are you sure you want to delete this device?", color: "danger", }, + { + label: "Rename Device", + icon: , + type: "POST", + url: "/api/ExecRenameAPDevice", + data: { + deviceId: "id", + serialNumber: "serialNumber", + }, + confirmText: "Enter the new display name for the device.", + fields: [ + { + type: "textField", + name: "displayName", + label: "New Display Name", + required: true, + validate: (value) => { + if (!value) { + return "Display name is required."; + } + if (value.length > 15) { + return "Display name must be 15 characters or less."; + } + if (/\s/.test(value)) { + return "Display name cannot contain spaces."; + } + if (!/^[a-zA-Z0-9-]+$/.test(value)) { + return "Display name can only contain letters, numbers, and hyphens."; + } + if (/^[0-9]+$/.test(value)) { + return "Display name cannot contain only numbers."; + } + return true; // Indicates validation passed + }, + }, + ], + color: "secondary", + }, ]; const offCanvas = { From 1be89a08d029f3e957e954f61b521e5dd874a2c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Fri, 4 Apr 2025 19:59:16 +0200 Subject: [PATCH 15/41] Reorder --- .../endpoint/autopilot/list-devices/index.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/pages/endpoint/autopilot/list-devices/index.js b/src/pages/endpoint/autopilot/list-devices/index.js index 2721b68924f9..18958cd120ca 100644 --- a/src/pages/endpoint/autopilot/list-devices/index.js +++ b/src/pages/endpoint/autopilot/list-devices/index.js @@ -42,15 +42,6 @@ const Page = () => { ], color: "info", }, - { - label: "Delete Device", - icon: , - type: "POST", - url: "/api/RemoveAPDevice", - data: { ID: "id" }, - confirmText: "Are you sure you want to delete this device?", - color: "danger", - }, { label: "Rename Device", icon: , @@ -89,6 +80,15 @@ const Page = () => { ], color: "secondary", }, + { + label: "Delete Device", + icon: , + type: "POST", + url: "/api/RemoveAPDevice", + data: { ID: "id" }, + confirmText: "Are you sure you want to delete this device?", + color: "danger", + }, ]; const offCanvas = { From 2de712b1cb469a2a15627ea4c2376d97b38e68b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Fri, 4 Apr 2025 21:03:11 +0200 Subject: [PATCH 16/41] feat: add edit group tag action for autopilot devices with validation --- .../endpoint/autopilot/list-devices/index.js | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/pages/endpoint/autopilot/list-devices/index.js b/src/pages/endpoint/autopilot/list-devices/index.js index 18958cd120ca..4f641664f439 100644 --- a/src/pages/endpoint/autopilot/list-devices/index.js +++ b/src/pages/endpoint/autopilot/list-devices/index.js @@ -2,7 +2,7 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { CippApiDialog } from "/src/components/CippComponents/CippApiDialog.jsx"; import { Button } from "@mui/material"; -import { PersonAdd, Delete, Sync, Add, Edit } from "@mui/icons-material"; +import { PersonAdd, Delete, Sync, Add, Edit, Sell } from "@mui/icons-material"; import { useDialog } from "../../../../hooks/use-dialog"; import Link from "next/link"; import { useState } from "react"; @@ -80,6 +80,31 @@ const Page = () => { ], color: "secondary", }, + { + label: "Edit Group Tag", + icon: , + type: "POST", + url: "/api/ExecSetAPDeviceGroupTag", + data: { + deviceId: "id", + serialNumber: "serialNumber", + }, + confirmText: "Enter the new group tag for the device.", + fields: [ + { + type: "textField", + name: "groupTag", + label: "Group Tag", + validate: (value) => { + if (value && value.length > 128) { + return "Group tag cannot exceed 128 characters."; + } + return true; // Validation passed + }, + }, + ], + color: "secondary", + }, { label: "Delete Device", icon: , From 35f7da57a28c562bdd6ce7463a7b42c0987442f0 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 8 Apr 2025 16:26:21 +0200 Subject: [PATCH 17/41] multipost for quartinune --- src/pages/email/administration/quarantine/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/email/administration/quarantine/index.js b/src/pages/email/administration/quarantine/index.js index 9ed7c94d2160..fc0323c4f77e 100644 --- a/src/pages/email/administration/quarantine/index.js +++ b/src/pages/email/administration/quarantine/index.js @@ -104,6 +104,7 @@ const Page = () => { label: "Release", type: "POST", url: "/api/ExecQuarantineManagement", + multiPost: true, data: { Identity: "Identity", Type: "!Release", From 8a9667a9fbbed1fab43d47ab748584aae9b090fc Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 8 Apr 2025 16:41:54 +0200 Subject: [PATCH 18/41] message trace id --- src/pages/email/tools/message-trace/index.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/pages/email/tools/message-trace/index.js b/src/pages/email/tools/message-trace/index.js index 1cde2ba78cba..9a856cefb8af 100644 --- a/src/pages/email/tools/message-trace/index.js +++ b/src/pages/email/tools/message-trace/index.js @@ -83,6 +83,12 @@ const Page = () => { }, icon: , }, + { + label: "View in Explorer", + noConfirm: true, + link: `https://security.microsoft.com/realtimereportsv3?tid=${tenantFilter}&dltarget=Explorer&dlstorage=Url&viewid=allemail&query-NetworkMessageId=[MessageTraceId]`, + icon: , + }, ]; const onSubmit = () => { From 6d6fe86d2f78ea66f6602cf97b530acfc6070077 Mon Sep 17 00:00:00 2001 From: Esco Date: Mon, 7 Apr 2025 13:49:13 +0200 Subject: [PATCH 19/41] feat: Sharepoint Mass Deletion Alert standard Update standards.json autocomplete switch to auto complete include license information a remove \n --- src/data/standards.json | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/data/standards.json b/src/data/standards.json index 50ea2a224e55..494319c2156e 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -1687,6 +1687,41 @@ "powershellEquivalent": "New-ProtectionAlert and Set-ProtectionAlert", "recommendedBy": [] }, + { + "name": "standards.SharePointMassDeletionAlert", + "cat": "Defender Standards", + "tag": [], + "helpText": "Sets a e-mail address to alert when a User deletes more than 20 SharePoint files within 60 minutes. NB: Requires a Office 365 E5 subscription, Office 365 E3 with Threat Intelligence or Office 365 EquivioAnalytics add-on.", + "docsDescription": "Sets a e-mail address to alert when a User deletes more than 20 SharePoint files within 60 minutes. This is useful for monitoring and ensuring that the correct SharePoint files are deleted. NB: Requires a Office 365 E5 subscription, Office 365 E3 with Threat Intelligence or Office 365 EquivioAnalytics add-on.", + "addedComponent": [ + { + "type": "number", + "name": "standards.SharePointMassDeletionAlert.Threshold", + "label": "Max files to delete within the time frame", + "defaultValue": 20 + }, + { + "type": "number", + "name": "standards.SharePointMassDeletionAlert.TimeWindow", + "label": "Time frame in minutes", + "defaultValue": 60 + }, + { + "type": "autoComplete", + "multiple": true, + "creatable": true, + "required": true, + "name": "standards.SharePointMassDeletionAlert.NotifyUser", + "label": "E-mail to receive the alert" + } + ], + "label": "SharePoint Mass Deletion Alert", + "impact": "Low Impact", + "impactColour": "info", + "addedDate": "2025-04-07", + "powershellEquivalent": "New-ProtectionAlert and Set-ProtectionAlert", + "recommendedBy": [] + }, { "name": "standards.SafeLinksPolicy", "cat": "Defender Standards", From a0eb6abcee11464b431ba6f6ff3a23ab4f589123 Mon Sep 17 00:00:00 2001 From: Esco Date: Wed, 9 Apr 2025 10:16:19 +0200 Subject: [PATCH 20/41] dev feat: cspell --- .vscode/extensions.json | 6 ++++++ cspell.json | 25 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 .vscode/extensions.json create mode 100644 cspell.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000000..6eaf6dd1ea6f --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "editorconfig.editorconfig", + "streetsidesoftware.code-spell-checker", + ] +} diff --git a/cspell.json b/cspell.json new file mode 100644 index 000000000000..93017de9bc12 --- /dev/null +++ b/cspell.json @@ -0,0 +1,25 @@ +{ + "version": "0.2", + "ignorePaths": [], + "dictionaryDefinitions": [], + "dictionaries": [], + "words": [ + "CIPP", + "CIPP-API", + "Entra", + "Intune", + "GDAP", + "Yubikey", + "Sherweb", + "Autotask", + "Datto", + "Syncro", + "ImmyBot", + "Choco", + ], + "ignoreWords": [ + "CIPPAPI", + "locationcipp", + ], + "import": [] +} From d34a722737849876c7e159a424769b9d45737b1d Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 9 Apr 2025 12:56:32 -0400 Subject: [PATCH 21/41] fix BEC polling --- src/pages/identity/administration/users/user/bec.jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/identity/administration/users/user/bec.jsx b/src/pages/identity/administration/users/user/bec.jsx index ed7c586489ec..9e8c1a59eb9e 100644 --- a/src/pages/identity/administration/users/user/bec.jsx +++ b/src/pages/identity/administration/users/user/bec.jsx @@ -58,7 +58,11 @@ const Page = () => { // Fetch BEC Check result using GUID const becPollingCall = ApiGetCall({ - url: `/api/execBECCheck?GUID=${becInitialCall.data?.GUID}`, + url: `/api/execBECCheck`, + data: { + GUID: becInitialCall.data?.GUID, + tenantFilter: userSettingsDefaults.currentTenant, + }, queryKey: `execBECCheck-polling-${becInitialCall.data?.GUID}`, waiting: false, }); From 08a325b68f79a583d631ed4c9e76d2259856b32d Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 10 Apr 2025 22:14:26 -0400 Subject: [PATCH 22/41] fix add group template --- src/pages/identity/administration/groups/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/identity/administration/groups/index.js b/src/pages/identity/administration/groups/index.js index ce0fd6b290f8..427a2ae002a5 100644 --- a/src/pages/identity/administration/groups/index.js +++ b/src/pages/identity/administration/groups/index.js @@ -86,7 +86,7 @@ const Page = () => { url: "/api/AddGroupTemplate", icon: , data: { - Displayname: "displayname", + Displayname: "displayName", Description: "description", GroupType: "calculatedGroupType", MembershipRules: "membershipRule", From 181bf3f6d884052dc83075a8876da2f075fd34f7 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 10 Apr 2025 22:49:39 -0400 Subject: [PATCH 23/41] fix bpa sync --- .../CippComponents/BPASyncDialog.jsx | 92 +++++++++++++++++++ .../CippComponents/CippApiResults.jsx | 2 +- .../tenant/standards/bpa-report/index.js | 39 +++++--- 3 files changed, 119 insertions(+), 14 deletions(-) create mode 100644 src/components/CippComponents/BPASyncDialog.jsx diff --git a/src/components/CippComponents/BPASyncDialog.jsx b/src/components/CippComponents/BPASyncDialog.jsx new file mode 100644 index 000000000000..cba62df8e67c --- /dev/null +++ b/src/components/CippComponents/BPASyncDialog.jsx @@ -0,0 +1,92 @@ +import React, { useState } from "react"; +import { + Dialog, + DialogContent, + DialogTitle, + Button, + DialogActions, + Alert, + CircularProgress, +} from "@mui/material"; +import { CheckCircle, Error, Sync } from "@mui/icons-material"; +import { useForm, FormProvider } from "react-hook-form"; +import { CippFormTenantSelector } from "./CippFormTenantSelector"; +import { ApiPostCall } from "/src/api/ApiCall"; +import { CippApiResults } from "./CippApiResults"; + +export const BPASyncDialog = ({ createDialog }) => { + const methods = useForm({ + defaultValues: { + tenantFilter: { + value: "AllTenants", + label: "*All Tenants", + }, + }, + }); + + // Use methods for form handling and control + const { handleSubmit, control } = methods; + + const [tenantId, setTenantId] = useState(""); + const [isSyncing, setIsSyncing] = useState(false); + + // Use ApiGetCall instead of useApiCall + const bpaSyncResults = ApiPostCall({ + urlfromdata: true, + }); + + const handleForm = (values) => { + setTenantId(values.tenantFilter || ""); + setIsSyncing(true); + + bpaSyncResults.mutate({ + url: "/api/ExecBPA", + queryKey: `bpa-sync-${tenantId}`, + data: tenantId ? { TenantFilter: tenantId } : {}, + }); + }; + + // Reset syncing state when dialog is closed + const handleClose = () => { + setIsSyncing(false); + createDialog.handleClose(); + }; + + return ( + + +
+ Force BPA Sync + +
+

+ This will force a Best Practice Analyzer (BPA) sync. Select a tenant (or all + tenants) below. +

+ +
+ +
+ + + + +
+
+
+ ); +}; diff --git a/src/components/CippComponents/CippApiResults.jsx b/src/components/CippComponents/CippApiResults.jsx index b7bcc0a675be..21917507c396 100644 --- a/src/components/CippComponents/CippApiResults.jsx +++ b/src/components/CippComponents/CippApiResults.jsx @@ -1,4 +1,4 @@ -import { Close, Download, RouterOutlined } from "@mui/icons-material"; +import { Close, Download } from "@mui/icons-material"; import { Alert, CircularProgress, diff --git a/src/pages/tenant/standards/bpa-report/index.js b/src/pages/tenant/standards/bpa-report/index.js index 5307dfa9a934..1c98aad23b69 100644 --- a/src/pages/tenant/standards/bpa-report/index.js +++ b/src/pages/tenant/standards/bpa-report/index.js @@ -3,17 +3,22 @@ import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx" import { Layout as DashboardLayout } from "/src/layouts/index.js"; // had to add an extra path here because I added an extra folder structure. We should switch to absolute pathing so we dont have to deal with relative. import Link from "next/link"; import { EyeIcon } from "@heroicons/react/24/outline"; -import { CopyAll, Delete, Edit, AddBox, GitHub } from "@mui/icons-material"; +import { CopyAll, Delete, Edit, AddBox, GitHub, Sync } from "@mui/icons-material"; import { ApiGetCall } from "/src/api/ApiCall"; +import { Stack } from "@mui/system"; +import { BPASyncDialog } from "/src/components/CippComponents/BPASyncDialog"; +import { useDialog } from "/src/hooks/use-dialog"; const Page = () => { const pageTitle = "Best Practice Reports"; + const bpaDialog = useDialog(); const integrations = ApiGetCall({ url: "/api/ListExtensionsConfig", queryKey: "Integrations", refetchOnMount: false, refetchOnReconnect: false, }); + const actions = [ { label: "View Report", @@ -95,18 +100,26 @@ const Page = () => { ]; return ( - }> - Add Template - - } - actions={actions} - simpleColumns={["Name", "Style"]} - queryKey="ListBPATemplates" - /> + <> + + + + + } + actions={actions} + simpleColumns={["Name", "Style"]} + queryKey="ListBPATemplates" + /> + + ); }; From e5332e813bfee8dbd95be6b01e6583457b6c7cbd Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 11 Apr 2025 14:18:59 -0400 Subject: [PATCH 24/41] fix edit alert with tenant exclusions --- .../alert-configuration/alert.jsx | 40 ++++++++++++++----- src/utils/get-cipp-formatting.js | 12 +++++- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/src/pages/tenant/administration/alert-configuration/alert.jsx b/src/pages/tenant/administration/alert-configuration/alert.jsx index ba6943f77d9c..53475d4b31c7 100644 --- a/src/pages/tenant/administration/alert-configuration/alert.jsx +++ b/src/pages/tenant/administration/alert-configuration/alert.jsx @@ -81,22 +81,43 @@ const AlertWizard = () => { const alert = existingAlert?.data?.find((alert) => alert.RowKey === router.query.id); if (alert?.LogType === "Scripted") { setAlertType("script"); - formControl.setValue("tenantFilter", { - value: alert.RawAlert.Tenant, - label: alert.RawAlert.Tenant, - }); + + console.log(alert); + + // Create formatted excluded tenants array if it exists + const excludedTenantsFormatted = Array.isArray(alert.excludedTenants) + ? alert.excludedTenants.map((tenant) => ({ value: tenant, label: tenant })) + : []; + + // Format the command object const usedCommand = alertList?.find( (cmd) => cmd.name === alert.RawAlert.Command.replace("Get-CIPPAlert", "") ); - formControl.setValue("command", { value: usedCommand, label: usedCommand.label }); - formControl.setValue( - "recurrence", - recurrenceOptions?.find((opt) => opt.value === alert.RawAlert.Recurrence) + + // Format recurrence option + const recurrenceOption = recurrenceOptions?.find( + (opt) => opt.value === alert.RawAlert.Recurrence ); + + // Format post execution values const postExecutionValue = postExecutionOptions.filter((opt) => alert.RawAlert.PostExecution.split(",").includes(opt.value) ); - formControl.setValue("postExecution", postExecutionValue); + + // Reset the form with all values at once + formControl.reset( + { + tenantFilter: { + value: alert.RawAlert.Tenant, + label: alert.RawAlert.Tenant, + }, + excludedTenants: excludedTenantsFormatted, + command: { value: usedCommand, label: usedCommand.label }, + recurrence: recurrenceOption, + postExecution: postExecutionValue, + }, + { keepDirty: false } + ); } if (alert?.PartitionKey === "Webhookv2") { setAlertType("audit"); @@ -113,6 +134,7 @@ const AlertWizard = () => { formControl.reset({ RowKey: router.query.clone ? undefined : router.query.id ? router.query.id : undefined, tenantFilter: alert.RawAlert.Tenants, + excludedTenants: alert.RawAlert.excludedTenants, Actions: alert.RawAlert.Actions, conditions: alert.RawAlert.Conditions, logbook: foundLogbook, diff --git a/src/utils/get-cipp-formatting.js b/src/utils/get-cipp-formatting.js index 8eb3e4e1dc5a..019407339e10 100644 --- a/src/utils/get-cipp-formatting.js +++ b/src/utils/get-cipp-formatting.js @@ -264,12 +264,20 @@ export const getCippFormatting = (data, cellName, type, canReceive) => { } if (cellName === "excludedTenants") { + // Handle null or undefined data + if (data === null || data === undefined) { + return isText ? "No data" : ; + } //check if data is an array. if (Array.isArray(data)) { return isText - ? data.join(", ") + ? data.map(item => (typeof item === 'object' && item?.label) ? item.label : item).join(", ") : data.map((item) => ( - + item && )); } } From edff8a6f2672905e53e9b420af431a3be756aab5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= <31723128+kris6673@users.noreply.github.com> Date: Fri, 11 Apr 2025 23:02:22 +0200 Subject: [PATCH 25/41] Update Close_Stale_Issues_and_PRs.yml --- .github/workflows/Close_Stale_Issues_and_PRs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Close_Stale_Issues_and_PRs.yml b/.github/workflows/Close_Stale_Issues_and_PRs.yml index 41dc1be235d7..96b5dc6d5e14 100644 --- a/.github/workflows/Close_Stale_Issues_and_PRs.yml +++ b/.github/workflows/Close_Stale_Issues_and_PRs.yml @@ -13,6 +13,6 @@ jobs: stale-issue-message: 'This issue is stale because it has been open 10 days with no activity. We will close this issue soon. If you want this feature implemented you can contribute it. See: https://docs.cipp.app/dev-documentation/contributing-to-the-code . Please notify the team if you are working on this yourself.' close-issue-message: 'This issue was closed because it has been stalled for 14 days with no activity.' stale-issue-label: 'no-activity' - exempt-issue-labels: 'planned' + exempt-issue-labels: 'planned,bug' days-before-stale: 9 - days-before-close: 14 + days-before-close: 5 From 6c11192dcfd9233cd5809826f5a1a11f9fb40fda Mon Sep 17 00:00:00 2001 From: Esco Date: Sat, 12 Apr 2025 00:11:34 +0200 Subject: [PATCH 26/41] chore: more words & cleanup standards.json --- cspell.json | 6 ++++++ src/data/standards.json | 28 ++++++++++++++-------------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/cspell.json b/cspell.json index 93017de9bc12..921542d92e48 100644 --- a/cspell.json +++ b/cspell.json @@ -9,6 +9,9 @@ "Entra", "Intune", "GDAP", + "OBEE", + "AITM", + "Passwordless", "Yubikey", "Sherweb", "Autotask", @@ -20,6 +23,9 @@ "ignoreWords": [ "CIPPAPI", "locationcipp", + "TNEF", + "winmail", + "PSTN", ], "import": [] } diff --git a/src/data/standards.json b/src/data/standards.json index 50ea2a224e55..31889ee2842c 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -217,7 +217,7 @@ "name": "standards.DisableBasicAuthSMTP", "cat": "Global Standards", "tag": [], - "helpText": "Disables SMTP AUTH for the organization and all users. This is the default for new tenants. ", + "helpText": "Disables SMTP AUTH for the organization and all users. This is the default for new tenants.", "docsDescription": "Disables SMTP basic authentication for the tenant and all users with it explicitly enabled.", "addedComponent": [], "label": "Disable SMTP Basic Authentication", @@ -546,7 +546,7 @@ "name": "standards.DisableTenantCreation", "cat": "Entra (AAD) Standards", "tag": ["CIS"], - "helpText": "Restricts creation of M365 tenants to the Global Administrator or Tenant Creator roles. ", + "helpText": "Restricts creation of M365 tenants to the Global Administrator or Tenant Creator roles.", "docsDescription": "Users by default are allowed to create M365 tenants. This disables that so only admins can create new M365 tenants.", "addedComponent": [], "label": "Disable M365 Tenant creation by users", @@ -785,7 +785,7 @@ { "type": "number", "name": "standards.StaleEntraDevices.deviceAgeThreshold", - "label": "Days before stale(Dont set below 30)" + "label": "Days before stale(Do not set below 30)" } ], "disabledFeatures": { @@ -1691,7 +1691,7 @@ "name": "standards.SafeLinksPolicy", "cat": "Defender Standards", "tag": ["CIS", "mdo_safelinksforemail", "mdo_safelinksforOfficeApps"], - "helpText": "This creates a safelink policy that automatically scans, tracks, and and enables safe links for Email, Office, and Teams for both external and internal senders", + "helpText": "This creates a Safe Links policy that automatically scans, tracks, and and enables safe links for Email, Office, and Teams for both external and internal senders", "addedComponent": [ { "type": "switch", @@ -1717,7 +1717,7 @@ "label": "Do not rewrite the following URLs in email" } ], - "label": "Default SafeLinks Policy", + "label": "Default Safe Links Policy", "impact": "Low Impact", "impactColour": "info", "addedDate": "2024-03-25", @@ -1737,7 +1737,7 @@ "mdo_antiphishingpolicies", "mdo_phishthresholdlevel" ], - "helpText": "This creates a Anti-Phishing policy that automatically enables Mailbox Intelligence and spoofing, optional switches for Mailtips.", + "helpText": "This creates a Anti-Phishing policy that automatically enables Mailbox Intelligence and spoofing, optional switches for Mail tips.", "addedComponent": [ { "type": "number", @@ -1930,7 +1930,7 @@ "impact": "Low Impact", "impactColour": "info", "addedDate": "2024-03-25", - "powershellEquivalent": "Set-AntiphishPolicy or New-AntiphishPolicy", + "powershellEquivalent": "Set-AntiPhishPolicy or New-AntiPhishPolicy", "recommendedBy": ["CIS"] }, { @@ -2704,7 +2704,7 @@ "cat": "Intune Standards", "tag": [], "helpText": "Requires MFA for all users to register devices with Intune. This is useful when not using Conditional Access.", - "label": "Require Multifactor Authentication to register or join devices with Microsoft Entra", + "label": "Require Multi-factor Authentication to register or join devices with Microsoft Entra", "impact": "Medium Impact", "impactColour": "warning", "addedDate": "2023-10-23", @@ -2812,7 +2812,7 @@ "impactColour": "info", "addedDate": "2024-07-09", "powershellEquivalent": "Set-SPOTenant -EnableAzureADB2BIntegration $true", - "recommendedBy": ["CIS 3.0"] + "recommendedBy": ["CIS"] }, { "name": "standards.SPDisallowInfectedFiles", @@ -2870,7 +2870,7 @@ "impactColour": "warning", "addedDate": "2024-07-09", "powershellEquivalent": "Set-SPOTenant -ExternalUserExpireInDays 30 -ExternalUserExpirationRequired $True", - "recommendedBy": ["CIS 3.0"] + "recommendedBy": ["CIS"] }, { "name": "standards.SPEmailAttestation", @@ -3012,7 +3012,7 @@ "helpText": "Disables the ability for external users to share files they don't own. Sharing links can only be made for People with existing access", "docsDescription": "Disables the ability for external users to share files they don't own. Sharing links can only be made for People with existing access. This is a tenant wide setting and overrules any settings set on the site level", "addedComponent": [], - "label": "Disable Resharing by External Users", + "label": "Disable Re-sharing by External Users", "impact": "High Impact", "impactColour": "danger", "addedDate": "2022-06-15", @@ -3188,7 +3188,7 @@ "impactColour": "info", "addedDate": "2024-11-12", "powershellEquivalent": "Set-CsTeamsMeetingPolicy -AllowAnonymousUsersToJoinMeeting $false -AllowAnonymousUsersToStartMeeting $false -AutoAdmittedUsers EveryoneInCompanyExcludingGuests -AllowPSTNUsersToBypassLobby $false -MeetingChatEnabledType EnabledExceptAnonymous -DesignatedPresenterRoleMode $DesignatedPresenterRoleMode -AllowExternalParticipantGiveRequestControl $false", - "recommendedBy": ["CIS 3.0"] + "recommendedBy": ["CIS"] }, { "name": "standards.TeamsEmailIntegration", @@ -3208,7 +3208,7 @@ "impactColour": "info", "addedDate": "2024-07-30", "powershellEquivalent": "Set-CsTeamsClientConfiguration -AllowEmailIntoChannel $false", - "recommendedBy": ["CIS 3.0"] + "recommendedBy": ["CIS"] }, { "name": "standards.TeamsExternalFileSharing", @@ -3247,7 +3247,7 @@ "impactColour": "info", "addedDate": "2024-07-28", "powershellEquivalent": "Set-CsTeamsClientConfiguration -AllowGoogleDrive $false -AllowShareFile $false -AllowBox $false -AllowDropBox $false -AllowEgnyte $false", - "recommendedBy": ["CIS 3.0"] + "recommendedBy": ["CIS"] }, { "name": "standards.TeamsEnrollUser", From 8534996ec2adeada89c76df36c980669d6673625 Mon Sep 17 00:00:00 2001 From: Esco Date: Sat, 12 Apr 2025 00:55:32 +0200 Subject: [PATCH 27/41] chore: add missing powershellEquivalent value --- src/data/standards.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/standards.json b/src/data/standards.json index 06d5cf3efadc..92ef2e2806f5 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -772,7 +772,7 @@ "impact": "Medium Impact", "impactColour": "warning", "addedDate": "2024-11-12", - "powershellEquivalent": "", + "powershellEquivalent": "Graph API", "recommendedBy": [] }, { @@ -2606,7 +2606,7 @@ "impact": "Low Impact", "impactColour": "info", "addedDate": "2024-11-12", - "powershellEquivalent": "", + "powershellEquivalent": "Graph API", "recommendedBy": [] }, { From 18db036a40970901abe9bd53d2167f87ca60d556 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sat, 12 Apr 2025 12:50:33 +0200 Subject: [PATCH 28/41] Sherweb code changes --- src/data/Extensions.json | 102 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/src/data/Extensions.json b/src/data/Extensions.json index 4be4aec14662..895bb0971b14 100644 --- a/src/data/Extensions.json +++ b/src/data/Extensions.json @@ -95,6 +95,108 @@ "compareValue": true, "action": "disable" } + }, + { + "type": "switch", + "name": "Sherweb.AutoMigrations", + "label": "Enable automated migration to Sherweb", + "condition": { + "field": "Sherweb.Enabled", + "compareType": "is", + "compareValue": true, + "action": "disable" + } + }, + { + "type": "autoComplete", + "name": "Sherweb.migrationMethods", + "label": "Select how you'd like automated migrations to be handled", + "options": [ + { + "label": "Notify only - This will notify you when a subscription is in its cancellation window for non Sherweb subscriptions", + "value": "notifyOnly" + }, + { + "label": "Buy and notify - This will automatically buy the subscription and notify you when a subscription is in its cancellation window for non Sherweb subscriptions", + "value": "buyAndNotify" + }, + { + "label": "Buy and cancel - This will automatically buy the subscription and cancel the old subscription when a subscription is in its cancellation window for non Sherweb subscriptions", + "value": "buyAndCancel" + } + ], + "multiple": false, + "condition": { + "field": "Sherweb.AutoMigrations", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "autoComplete", + "name": "Sherweb.migrateFrom", + "label": "Select the vendor to automatically migrate from", + "options": [ + { + "label": "Pax8", + "value": "Pax8" + } + ], + "multiple": false, + "condition": { + "field": "Sherweb.migrationMethods", + "compareType": "is", + "compareValue": "buyAndCancel" + } + }, + { + "type": "autoComplete", + "name": "Sherweb.migrateToLicense", + "label": "Select the type of license to automatically migrate to", + "options": [ + { + "label": "Yearly", + "value": "Y1Y" + }, + { + "label": "Annual paid monthly", + "value": "M1Y" + }, + { + "label": "Monthly", + "value": "M2M" + } + ], + "multiple": false, + "condition": { + "field": "Sherweb.migrationMethods", + "compareType": "contains", + "compareValue": "buy" + } + }, + { + "type": "password", + "name": "Sherweb.paxclientId", + "label": "Pax8 Client Id", + "placeholder": "Enter your Pax Client Id", + "required": true, + "condition": { + "field": "Sherweb.migrateFrom", + "compareType": "is", + "compareValue": "Pax8" + } + }, + { + "type": "password", + "name": "Sherweb.paxclientSecret", + "label": "Pax8 Client Secret", + "placeholder": "Enter your Pax Client Secret", + "required": true, + "condition": { + "field": "Sherweb.migrateFrom", + "compareType": "is", + "compareValue": "Pax8" + } } ] }, From da805ecaf591e3260a4f8dfbd2cbe41233cbfa12 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 12 Apr 2025 16:47:19 -0400 Subject: [PATCH 29/41] fix scheduler form and add run now --- .../CippFormPages/CippSchedulerForm.jsx | 21 ++++++++++++++----- src/pages/cipp/scheduler/index.js | 12 +++++++++-- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/components/CippFormPages/CippSchedulerForm.jsx b/src/components/CippFormPages/CippSchedulerForm.jsx index 325fd788757e..d49b2f069254 100644 --- a/src/components/CippFormPages/CippSchedulerForm.jsx +++ b/src/components/CippFormPages/CippSchedulerForm.jsx @@ -51,7 +51,7 @@ const CippSchedulerForm = (props) => { }; const recurrenceOptions = [ - { value: "0", label: "Only once" }, + { value: "0", label: "Once" }, { value: "1d", label: "Every 1 day" }, { value: "7d", label: "Every 7 days" }, { value: "30d", label: "Every 30 days" }, @@ -69,8 +69,8 @@ const CippSchedulerForm = (props) => { }); const tenantList = ApiGetCall({ - url: "/api/ListTenants", - queryKey: "ListTenants", + url: "/api/ListTenants?AllTenantSelector=true", + queryKey: "ListTenants-AllTenants", }); useEffect(() => { if (scheduledTaskList.isSuccess && router.query.id) { @@ -86,16 +86,27 @@ const CippSchedulerForm = (props) => { ); if (commands.isSuccess) { const command = commands.data.find((command) => command.Function === task.Command); + var recurrence = recurrenceOptions.find( + (option) => option.value === task.Recurrence || option.label === task.Recurrence + ); + + // if scheduledtime type is a date, convert to unixtime + if (typeof task.ScheduledTime === "date") { + task.ScheduledTime = Math.floor(task.ScheduledTime.getTime() / 1000); + } else if (typeof task.ScheduledTime === "string") { + task.ScheduledTime = Math.floor(new Date(task.ScheduledTime).getTime() / 1000); + } + const ResetParams = { tenantFilter: { value: tenantFilter?.defaultDomainName, - label: tenantFilter?.defaultDomainName, + label: `${tenantFilter?.displayName} (${tenantFilter?.defaultDomainName})`, }, RowKey: router.query.Clone ? null : task.RowKey, Name: router.query.Clone ? `${task.Name} (Clone)` : task?.Name, command: { label: task.Command, value: task.Command, addedFields: command }, ScheduledTime: task.ScheduledTime, - Recurrence: task.Recurrence, + Recurrence: recurrence, parameters: task.Parameters, postExecution: postExecution, advancedParameters: task.RawJsonParameters ? true : false, diff --git a/src/pages/cipp/scheduler/index.js b/src/pages/cipp/scheduler/index.js index 81cf3213ed02..02d7d3c93ae0 100644 --- a/src/pages/cipp/scheduler/index.js +++ b/src/pages/cipp/scheduler/index.js @@ -2,13 +2,21 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippTablePage from "/src/components/CippComponents/CippTablePage"; import { Button, Typography } from "@mui/material"; import Link from "next/link"; -import { CalendarDaysIcon, EyeIcon, TrashIcon } from "@heroicons/react/24/outline"; +import { CalendarDaysIcon, TrashIcon } from "@heroicons/react/24/outline"; import { useState } from "react"; -import { CopyAll, Edit } from "@mui/icons-material"; +import { CopyAll, Edit, PlayArrow } from "@mui/icons-material"; import { CippCodeBlock } from "../../../components/CippComponents/CippCodeBlock"; const Page = () => { const actions = [ + { + label: "Run Now", + type: "POST", + url: "/api/AddScheduledItem", + data: { RowKey: "RowKey", RunNow: true }, + icon: , + confirmText: "Are you sure you want to run [Name]?", + }, { label: "Edit Job", link: "/cipp/scheduler/job?id=[RowKey]", From 3064fbaa169f64c4f19e9fe643150b9c09190c21 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 12 Apr 2025 18:21:18 -0400 Subject: [PATCH 30/41] new scheduled task details --- .../CippComponents/ScheduledTaskDetails.jsx | 202 ++++++++++++++++++ src/pages/cipp/scheduler/index.js | 10 +- 2 files changed, 205 insertions(+), 7 deletions(-) create mode 100644 src/components/CippComponents/ScheduledTaskDetails.jsx diff --git a/src/components/CippComponents/ScheduledTaskDetails.jsx b/src/components/CippComponents/ScheduledTaskDetails.jsx new file mode 100644 index 000000000000..b9f78247c361 --- /dev/null +++ b/src/components/CippComponents/ScheduledTaskDetails.jsx @@ -0,0 +1,202 @@ +import React, { useEffect, useState } from "react"; +import { + Box, + Typography, + Dialog, + DialogContent, + DialogTitle, + IconButton, + Accordion, + AccordionSummary, + AccordionDetails, + Stack, + Chip, + Button, +} from "@mui/material"; +import { CippCodeBlock } from "./CippCodeBlock"; +import { ApiGetCall } from "../../api/ApiCall"; +import { getCippTranslation } from "../../utils/get-cipp-translation"; +import { CippPropertyListCard } from "../CippCards/CippPropertyListCard"; +import { Close, ExpandMore, Sync } from "@mui/icons-material"; +import { ClipboardDocumentListIcon } from "@heroicons/react/24/outline"; +import { getCippFormatting } from "../../utils/get-cipp-formatting"; +import { CippDataTable } from "../CippTable/CippDataTable"; +import { CippTimeAgo } from "/src/components/CippComponents/CippTimeAgo"; + +const ScheduledTaskDetails = ({ data }) => { + const [taskDetails, setTaskDetails] = useState(null); + const [dialogOpen, setDialogOpen] = useState(false); + const [selectedResult, setSelectedResult] = useState(null); + const [expanded, setExpanded] = useState(false); + + const handleChange = (panel) => (event, newExpanded) => { + setExpanded(newExpanded ? panel : false); + }; + + const taskDetailResults = ApiGetCall({ + url: `/api/ListScheduledItemDetails`, + data: { + RowKey: data.RowKey, + }, + queryKey: `ListScheduledItemDetails-${data.RowKey}`, + }); + + const taskProperties = [ + "Name", + "Command", + "Tenant", + "Recurrence", + "ScheduledTime", + "ExecutedTime", + "PostExecution", + ]; + + useEffect(() => { + if (taskDetailResults.isSuccess && taskDetailResults?.data) { + setTaskDetails(taskDetailResults.data); + } + }, [data.RowKey, taskDetailResults.isSuccess, taskDetailResults.data]); + + const handleViewResult = (result) => { + setSelectedResult(result); + setDialogOpen(true); + }; + + return ( + <> + setDialogOpen(false)}> + + Task Result Details + setDialogOpen(false)} + sx={{ position: "absolute", right: 8, top: 8 }} + > + + + + + setDialogOpen(false)} + code={selectedResult?.Results} + /> + + + + + Task Details + taskDetailResults.refetch()} startIcon={}> + Refresh + + } + layout="dual" + showDivider={false} + propertyItems={taskProperties + .filter((prop) => taskDetails?.Task?.[prop] != null && taskDetails?.Task?.[prop] !== "") + .map((prop) => { + return { + label: getCippTranslation(prop), + value: getCippFormatting(taskDetails?.Task?.[prop], prop), + }; + })} + isFetching={taskDetailResults.isFetching} + /> + + {taskDetails?.Task?.Parameters && ( + + }> + Task Parameters + + + { + return { + label: key, + value: getCippFormatting(value, key), + }; + })} + isFetching={taskDetailResults.isFetching} + /> + + + )} + + {taskDetails?.Details?.length > 0 && ( + <> + + Execution Results + + + {taskDetails.Details.map((result, index) => ( + + } + sx={{ + "& .MuiAccordionSummary-content": { + display: "flex", + justifyContent: "space-between", + alignItems: "center", + width: "100%", + }, + }} + > + {result.TenantName || result.Tenant} + } + sx={{ mx: 1 }} + /> + + + {result.Results === "null" ? ( + No data available + ) : Array.isArray(result.Results) ? ( + + ) : typeof result.Results === "object" ? ( + ({ + label: key, + value: typeof value === "object" ? JSON.stringify(value) : value, + }))} + /> + ) : ( + +
+                          {result.Results}
+                        
+
+ )} +
+
+ ))} +
+ + )} +
+ + ); +}; + +export default ScheduledTaskDetails; diff --git a/src/pages/cipp/scheduler/index.js b/src/pages/cipp/scheduler/index.js index 02d7d3c93ae0..867826ca579a 100644 --- a/src/pages/cipp/scheduler/index.js +++ b/src/pages/cipp/scheduler/index.js @@ -5,7 +5,7 @@ import Link from "next/link"; import { CalendarDaysIcon, TrashIcon } from "@heroicons/react/24/outline"; import { useState } from "react"; import { CopyAll, Edit, PlayArrow } from "@mui/icons-material"; -import { CippCodeBlock } from "../../../components/CippComponents/CippCodeBlock"; +import ScheduledTaskDetails from "../../../components/CippComponents/ScheduledTaskDetails"; const Page = () => { const actions = [ @@ -66,12 +66,8 @@ const Page = () => { ]; const offCanvas = { - children: (extendedData) => ( - <> - Job Results - - - ), + children: (extendedData) => , + size: "xl", actions: actions, }; const [showHiddenJobs, setShowHiddenJobs] = useState(false); From a7ca3d3c4b2f0483ba0fc7dee6d4fe6ff4b433c7 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 12 Apr 2025 19:00:39 -0400 Subject: [PATCH 31/41] add more filters and tweaks to scheduled task detail --- .../CippComponents/ScheduledTaskDetails.jsx | 245 +++++++++++------- src/pages/cipp/scheduler/index.js | 2 +- 2 files changed, 153 insertions(+), 94 deletions(-) diff --git a/src/components/CippComponents/ScheduledTaskDetails.jsx b/src/components/CippComponents/ScheduledTaskDetails.jsx index b9f78247c361..618ae131ef67 100644 --- a/src/components/CippComponents/ScheduledTaskDetails.jsx +++ b/src/components/CippComponents/ScheduledTaskDetails.jsx @@ -2,23 +2,20 @@ import React, { useEffect, useState } from "react"; import { Box, Typography, - Dialog, - DialogContent, - DialogTitle, IconButton, Accordion, AccordionSummary, AccordionDetails, - Stack, Chip, - Button, + TextField, + InputAdornment, + Tooltip, + Stack, } from "@mui/material"; -import { CippCodeBlock } from "./CippCodeBlock"; import { ApiGetCall } from "../../api/ApiCall"; import { getCippTranslation } from "../../utils/get-cipp-translation"; import { CippPropertyListCard } from "../CippCards/CippPropertyListCard"; -import { Close, ExpandMore, Sync } from "@mui/icons-material"; -import { ClipboardDocumentListIcon } from "@heroicons/react/24/outline"; +import { ExpandMore, Sync, Search, Close } from "@mui/icons-material"; import { getCippFormatting } from "../../utils/get-cipp-formatting"; import { CippDataTable } from "../CippTable/CippDataTable"; import { CippTimeAgo } from "/src/components/CippComponents/CippTimeAgo"; @@ -28,6 +25,7 @@ const ScheduledTaskDetails = ({ data }) => { const [dialogOpen, setDialogOpen] = useState(false); const [selectedResult, setSelectedResult] = useState(null); const [expanded, setExpanded] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); const handleChange = (panel) => (event, newExpanded) => { setExpanded(newExpanded ? panel : false); @@ -54,44 +52,50 @@ const ScheduledTaskDetails = ({ data }) => { useEffect(() => { if (taskDetailResults.isSuccess && taskDetailResults?.data) { setTaskDetails(taskDetailResults.data); + + // Auto-expand the only result if there's just one + if (taskDetailResults.data.Details?.length === 1) { + setExpanded(`execution-results-0`); + } } }, [data.RowKey, taskDetailResults.isSuccess, taskDetailResults.data]); - const handleViewResult = (result) => { - setSelectedResult(result); - setDialogOpen(true); - }; + const filteredDetails = taskDetails?.Details?.filter((result) => { + if (!searchQuery) return true; + + const searchLower = searchQuery.toLowerCase(); + const tenantMatches = (result.TenantName || result.Tenant || "") + .toLowerCase() + .includes(searchLower); + + let resultsMatches = false; + if (typeof result.Results === "object" && result.Results !== null) { + const resultsStr = JSON.stringify(result.Results).toLowerCase(); + resultsMatches = resultsStr.includes(searchLower); + } + + return tenantMatches || resultsMatches; + }); return ( <> - setDialogOpen(false)}> - - Task Result Details - setDialogOpen(false)} - sx={{ position: "absolute", right: 8, top: 8 }} - > - - - - - setDialogOpen(false)} - code={selectedResult?.Results} - /> - - - - Task Details + + Task Details + taskDetailResults.refetch()}> + + + taskDetailResults.refetch()} startIcon={}> - Refresh - - } layout="dual" showDivider={false} propertyItems={taskProperties @@ -133,64 +137,119 @@ const ScheduledTaskDetails = ({ data }) => { {taskDetails?.Details?.length > 0 && ( <> - - Execution Results - + + + Execution Results{" "} + {filteredDetails && ( + + ({filteredDetails.length} of {taskDetails.Details.length}) + + )} + + setSearchQuery(e.target.value)} + InputProps={{ + startAdornment: ( + + + + ), + endAdornment: searchQuery && ( + + + setSearchQuery("")} + aria-label="Clear search" + > + + + + + ), + }} + /> + - {taskDetails.Details.map((result, index) => ( - - } - sx={{ - "& .MuiAccordionSummary-content": { - display: "flex", - justifyContent: "space-between", - alignItems: "center", - width: "100%", - }, - }} + {filteredDetails && + filteredDetails.map((result, index) => ( + - {result.TenantName || result.Tenant} - } - sx={{ mx: 1 }} - /> - - - {result.Results === "null" ? ( - No data available - ) : Array.isArray(result.Results) ? ( - } + sx={{ + "& .MuiAccordionSummary-content": { + display: "flex", + justifyContent: "space-between", + alignItems: "center", + width: "100%", + }, + }} + > + {result.TenantName || result.Tenant} + } + sx={{ mx: 1 }} /> - ) : typeof result.Results === "object" ? ( - ({ - label: key, - value: typeof value === "object" ? JSON.stringify(value) : value, - }))} - /> - ) : ( - -
-                          {result.Results}
-                        
-
- )} -
-
- ))} + + + {result.Results === "null" ? ( + No data available + ) : Array.isArray(result.Results) ? ( + + ) : typeof result.Results === "object" ? ( + ({ + label: key, + value: typeof value === "object" ? JSON.stringify(value) : value, + }))} + /> + ) : ( + +
+                            {result.Results}
+                          
+
+ )} +
+ + ))} + {filteredDetails && filteredDetails.length === 0 && ( + + + No results match your search criteria + + + )}
)} diff --git a/src/pages/cipp/scheduler/index.js b/src/pages/cipp/scheduler/index.js index 867826ca579a..bf162c3147da 100644 --- a/src/pages/cipp/scheduler/index.js +++ b/src/pages/cipp/scheduler/index.js @@ -1,6 +1,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippTablePage from "/src/components/CippComponents/CippTablePage"; -import { Button, Typography } from "@mui/material"; +import { Button } from "@mui/material"; import Link from "next/link"; import { CalendarDaysIcon, TrashIcon } from "@heroicons/react/24/outline"; import { useState } from "react"; From 96935f7859abef58ee9a38dc2f030e38de926b72 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 12 Apr 2025 19:22:18 -0400 Subject: [PATCH 32/41] standards ui tweaks --- .../CippStandards/CippStandardAccordion.jsx | 454 ++++++++++++------ src/pages/tenant/standards/template.jsx | 7 +- 2 files changed, 323 insertions(+), 138 deletions(-) diff --git a/src/components/CippStandards/CippStandardAccordion.jsx b/src/components/CippStandards/CippStandardAccordion.jsx index fdb538a4dec0..a46d27e6db13 100644 --- a/src/components/CippStandards/CippStandardAccordion.jsx +++ b/src/components/CippStandards/CippStandardAccordion.jsx @@ -12,8 +12,20 @@ import { Grid, Tooltip, Chip, + TextField, + InputAdornment, + ButtonGroup, + Button, } from "@mui/material"; -import { ExpandMore as ExpandMoreIcon, Delete, Add, Public } from "@mui/icons-material"; +import { + ExpandMore as ExpandMoreIcon, + Delete, + Add, + Public, + Search, + Close, + FilterAlt, +} from "@mui/icons-material"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import { useWatch } from "react-hook-form"; import _ from "lodash"; @@ -78,6 +90,8 @@ const CippStandardAccordion = ({ formControl, }) => { const [configuredState, setConfiguredState] = useState({}); + const [filter, setFilter] = useState("all"); + const [searchQuery, setSearchQuery] = useState(""); const watchedValues = useWatch({ control: formControl.control, @@ -100,7 +114,6 @@ const CippStandardAccordion = ({ const isConfigured = actionFilled && addedComponentsFilled; - // Only update state if there's a change to reduce unnecessary re-renders. if (newConfiguredState[standardName] !== isConfigured) { newConfiguredState[standardName] = isConfigured; } @@ -110,148 +123,319 @@ const CippStandardAccordion = ({ if (!_.isEqual(newConfiguredState, configuredState)) { setConfiguredState(newConfiguredState); } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [watchedValues, standards, selectedStandards]); - return Object.keys(selectedStandards)?.map((standardName) => { - const standard = standards.find((s) => s.name === standardName.split("[")[0]); - if (!standard) return null; + const groupedStandards = useMemo(() => { + const result = {}; + + Object.keys(selectedStandards).forEach((standardName) => { + const standard = standards.find((s) => s.name === standardName.split("[")[0]); + if (!standard) return; + + const category = standard.cat || "Other Standards"; + + if (!result[category]) { + result[category] = []; + } + + result[category].push({ + standardName, + standard, + }); + }); - const isExpanded = expanded === standardName; - const hasAddedComponents = standard.addedComponent && standard.addedComponent.length > 0; - const isConfigured = configuredState[standardName]; - const disabledFeatures = standard.disabledFeatures || {}; + Object.keys(result).forEach((category) => { + result[category].sort((a, b) => a.standard.label.localeCompare(b.standard.label)); + }); + + return result; + }, [selectedStandards, standards]); - let selectedActions = _.get(watchedValues, `${standardName}.action`); - //if selectedActions is not an array, convert it to an array - if (selectedActions && !Array.isArray(selectedActions)) { - selectedActions = [selectedActions]; + const filteredGroupedStandards = useMemo(() => { + if (!searchQuery && filter === "all") { + return groupedStandards; } - const selectedTemplateName = standard.multiple - ? _.get(watchedValues, `${standardName}.${standard.addedComponent?.[0]?.name}`) - : ""; - const accordionTitle = selectedTemplateName - ? `${standard.label} - ${selectedTemplateName.label}` - : standard.label; - - return ( - - - - - {standard.cat === "Global Standards" ? ( - - ) : standard.cat === "Entra (AAD) Standards" ? ( - - ) : standard.cat === "Exchange Standards" ? ( - - ) : standard.cat === "Defender Standards" ? ( - - ) : standard.cat === "Intune Standards" ? ( - - ) : ( - - )} - - - {accordionTitle} - {selectedActions && selectedActions?.length > 0 && ( - - {selectedActions?.map((action, index) => ( - <> - - - ))} - - - )} - { - //add a chip that shows the impact - } - - {standard.helpText} - - - - - {standard.multiple && ( - - handleAddMultipleStandard(standardName)}> - - - - )} - - {isConfigured ? "Configured" : "Unconfigured"} - handleRemoveStandard(standardName)}> - - - - handleAccordionToggle(standardName)}> - { + const filteredStandards = groupedStandards[category].filter(({ standardName, standard }) => { + const matchesSearch = + !searchQuery || + standard.label.toLowerCase().includes(searchQuery.toLowerCase()) || + (standard.helpText && + standard.helpText.toLowerCase().includes(searchQuery.toLowerCase())); + + const isConfigured = configuredState[standardName]; + const matchesFilter = + filter === "all" || + (filter === "configured" && isConfigured) || + (filter === "unconfigured" && !isConfigured); + + return matchesSearch && matchesFilter; + }); + + if (filteredStandards.length > 0) { + result[category] = filteredStandards; + } + }); + + return result; + }, [groupedStandards, searchQuery, filter, configuredState]); + + const standardCounts = useMemo(() => { + let allCount = 0; + let configuredCount = 0; + let unconfiguredCount = 0; + + Object.keys(groupedStandards).forEach((category) => { + groupedStandards[category].forEach(({ standardName }) => { + allCount++; + if (configuredState[standardName]) { + configuredCount++; + } else { + unconfiguredCount++; + } + }); + }); + + return { allCount, configuredCount, unconfiguredCount }; + }, [groupedStandards, configuredState]); + + const hasFilteredStandards = Object.keys(filteredGroupedStandards).length > 0; + + return ( + <> + {Object.keys(selectedStandards).length > 0 && ( + <> + + + setSearchQuery(e.target.value)} + slotProps={{ + input: { + startAdornment: ( + + + + ), + endAdornment: searchQuery && ( + + + setSearchQuery("")} + aria-label="Clear search" + > + + + + + ), + }, + }} /> - + + + + + + + - - - - - - - - - - - {hasAddedComponents && ( - - - {standard.addedComponent?.map((component, idx) => ( - + + No standards match the selected filter criteria or search query. + + + )} + + )} + + {Object.keys(filteredGroupedStandards).map((category) => ( + + + {category} + + + {filteredGroupedStandards[category].map(({ standardName, standard }) => { + const isExpanded = expanded === standardName; + const hasAddedComponents = + standard.addedComponent && standard.addedComponent.length > 0; + const isConfigured = configuredState[standardName]; + const disabledFeatures = standard.disabledFeatures || {}; + + let selectedActions = _.get(watchedValues, `${standardName}.action`); + if (selectedActions && !Array.isArray(selectedActions)) { + selectedActions = [selectedActions]; + } + + const selectedTemplateName = standard.multiple + ? _.get(watchedValues, `${standardName}.${standard.addedComponent?.[0]?.name}`) + : ""; + const accordionTitle = selectedTemplateName + ? `${standard.label} - ${selectedTemplateName.label}` + : standard.label; + + return ( + + + + + {standard.cat === "Global Standards" ? ( + + ) : standard.cat === "Entra (AAD) Standards" ? ( + + ) : standard.cat === "Exchange Standards" ? ( + + ) : standard.cat === "Defender Standards" ? ( + + ) : standard.cat === "Intune Standards" ? ( + + ) : ( + + )} + + + {accordionTitle} + {selectedActions && selectedActions?.length > 0 && ( + + {selectedActions?.map((action, index) => ( + + + + ))} + + + )} + + {standard.helpText} + + + + + {standard.multiple && ( + + handleAddMultipleStandard(standardName)}> + + + + )} + + + {isConfigured ? "Configured" : "Unconfigured"} + + handleRemoveStandard(standardName)}> + + + + handleAccordionToggle(standardName)}> + - ))} - - - )} - - - - - ); - }); + + + + + + + + + + + + + {hasAddedComponents && ( + + + {standard.addedComponent?.map((component, idx) => ( + + ))} + + + )} + + + + + ); + })} + + ))} + + ); }; export default CippStandardAccordion; diff --git a/src/pages/tenant/standards/template.jsx b/src/pages/tenant/standards/template.jsx index fb9f23ae4b3f..04c8b9b9c906 100644 --- a/src/pages/tenant/standards/template.jsx +++ b/src/pages/tenant/standards/template.jsx @@ -1,4 +1,5 @@ -import { Box, Button, Container, Stack, Typography, SvgIcon, Grid, Skeleton } from "@mui/material"; +import { Box, Button, Container, Stack, Typography, SvgIcon, Skeleton } from "@mui/material"; +import { Grid } from "@mui/system"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { useForm } from "react-hook-form"; import { useRouter } from "next/router"; @@ -200,7 +201,7 @@ const Page = () => { {/* Left Column for Accordions */} - + { updatedAt={updatedAt} /> - + {/* Show accordions based on selectedStandards (which is populated by API when editing) */} {existingTemplate.isLoading ? ( From c668b0d8d3652621eb35692426e455648bb8ed28 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 12 Apr 2025 19:28:53 -0400 Subject: [PATCH 33/41] also filter by category --- .../CippStandards/CippStandardAccordion.jsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/CippStandards/CippStandardAccordion.jsx b/src/components/CippStandards/CippStandardAccordion.jsx index a46d27e6db13..4564e4dc49c9 100644 --- a/src/components/CippStandards/CippStandardAccordion.jsx +++ b/src/components/CippStandards/CippStandardAccordion.jsx @@ -157,14 +157,21 @@ const CippStandardAccordion = ({ } const result = {}; + const searchLower = searchQuery.toLowerCase(); Object.keys(groupedStandards).forEach((category) => { + const categoryMatchesSearch = !searchQuery || category.toLowerCase().includes(searchLower); + const filteredStandards = groupedStandards[category].filter(({ standardName, standard }) => { const matchesSearch = !searchQuery || - standard.label.toLowerCase().includes(searchQuery.toLowerCase()) || - (standard.helpText && - standard.helpText.toLowerCase().includes(searchQuery.toLowerCase())); + categoryMatchesSearch || + standard.label.toLowerCase().includes(searchLower) || + (standard.helpText && standard.helpText.toLowerCase().includes(searchLower)) || + (standard.cat && standard.cat.toLowerCase().includes(searchLower)) || + (standard.tag && + Array.isArray(standard.tag) && + standard.tag.some((tag) => tag.toLowerCase().includes(searchLower))); const isConfigured = configuredState[standardName]; const matchesFilter = From c9dc223c24252b88dbe968015387e5908a159cc5 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 12 Apr 2025 20:32:26 -0400 Subject: [PATCH 34/41] add status check for if reporting is enabled on the template --- src/pages/tenant/standards/compare/index.js | 136 +++++++++++++------- 1 file changed, 88 insertions(+), 48 deletions(-) diff --git a/src/pages/tenant/standards/compare/index.js b/src/pages/tenant/standards/compare/index.js index 7b96932616fe..f78f32f4cee6 100644 --- a/src/pages/tenant/standards/compare/index.js +++ b/src/pages/tenant/standards/compare/index.js @@ -172,6 +172,13 @@ const Page = () => { const standardInfo = standards.find((s) => s.name === standardId); const standardSettings = standardConfig.standards?.[standardKey] || {}; console.log(standardInfo); + + // Check if reporting is enabled for this standard by checking the action property + // The standard should be reportable if there's an action with value === 'Report' + const actions = standardConfig.action || []; + const reportingEnabled = + actions.filter((action) => action.value === "Report").length > 0; + // Find the tenant's value for this standard const currentTenantStandard = currentTenantData.find( (s) => s.standardId === standardId @@ -179,6 +186,7 @@ const Page = () => { // Determine compliance status let isCompliant = false; + let reportingDisabled = !reportingEnabled; // Check if the standard is directly in the tenant object (like "standards.AuditLog": true) const standardIdWithoutPrefix = standardId.replace("standards.", ""); @@ -203,6 +211,13 @@ const Page = () => { } } + // Determine compliance status text based on reporting flag + const complianceStatus = reportingDisabled + ? "Reporting Disabled" + : isCompliant + ? "Compliant" + : "Non-Compliant"; + // Use the direct standard value from the tenant object if it exists allStandards.push({ standardId, @@ -212,7 +227,8 @@ const Page = () => { ? directStandardValue : currentTenantStandard?.value, standardValue: standardSettings, - complianceStatus: isCompliant ? "Compliant" : "Non-Compliant", + complianceStatus, + reportingDisabled, complianceDetails: standardInfo?.docsDescription || standardInfo?.helpText || "", standardDescription: standardInfo?.helpText || "", standardImpact: standardInfo?.impact || "Medium Impact", @@ -260,7 +276,13 @@ const Page = () => { comparisonData?.filter((standard) => standard.complianceStatus === "Compliant").length || 0; const nonCompliantCount = comparisonData?.filter((standard) => standard.complianceStatus === "Non-Compliant").length || 0; - const compliancePercentage = allCount > 0 ? Math.round((compliantCount / allCount) * 100) : 0; + const reportingDisabledCount = + comparisonData?.filter((standard) => standard.complianceStatus === "Reporting Disabled") + .length || 0; + const compliancePercentage = + allCount > 0 + ? Math.round((compliantCount / (allCount - reportingDisabledCount || 1)) * 100) + : 0; return ( @@ -589,11 +611,15 @@ const Page = () => { bgcolor: standard.complianceStatus === "Compliant" ? "success.main" + : standard.complianceStatus === "Reporting Disabled" + ? "grey.500" : "error.main", }} > {standard.complianceStatus === "Compliant" ? ( + ) : standard.complianceStatus === "Reporting Disabled" ? ( + ) : ( )} @@ -751,6 +777,8 @@ const Page = () => { backgroundColor: standard.complianceStatus === "Compliant" ? "success.main" + : standard.complianceStatus === "Reporting Disabled" + ? "grey.500" : "error.main", borderRadius: "50%", width: 8, @@ -777,49 +805,55 @@ const Page = () => { borderColor: "divider", }} > - {Object.entries(standard.currentTenantValue).map(([key, value]) => { - const standardValueForKey = - standard.standardValue && typeof standard.standardValue === "object" - ? standard.standardValue[key] - : undefined; - - const isDifferent = - standardValueForKey !== undefined && - JSON.stringify(value) !== JSON.stringify(standardValueForKey); - - return ( - - - {key}: - - - {standard.complianceStatus === "Compliant" && value === true - ? "Compliant" - : typeof value === "object" && value !== null - ? value.label || JSON.stringify(value) - : value === true - ? "Enabled" - : value === false - ? "Disabled" - : String(value)} - - - ); - })} + {standard.complianceStatus === "Reporting Disabled" ? ( + + Reporting is disabled for this standard in the template configuration. + + ) : ( + Object.entries(standard.currentTenantValue).map(([key, value]) => { + const standardValueForKey = + standard.standardValue && typeof standard.standardValue === "object" + ? standard.standardValue[key] + : undefined; + + const isDifferent = + standardValueForKey !== undefined && + JSON.stringify(value) !== JSON.stringify(standardValueForKey); + + return ( + + + {key}: + + + {standard.complianceStatus === "Compliant" && value === true + ? "Compliant" + : typeof value === "object" && value !== null + ? value.label || JSON.stringify(value) + : value === true + ? "Enabled" + : value === false + ? "Disabled" + : String(value)} + + + ); + }) + )} ) : ( { color: standard.complianceStatus === "Compliant" ? "success.main" + : standard.complianceStatus === "Reporting Disabled" + ? "text.secondary" : "error.main", fontWeight: - standard.complianceStatus !== "Compliant" ? "medium" : "inherit", + standard.complianceStatus === "Non-Compliant" ? "medium" : "inherit", }} > - {standard.complianceStatus === "Compliant" && - standard.currentTenantValue === true ? ( + {standard.complianceStatus === "Reporting Disabled" ? ( + + Reporting is disabled for this standard in the template configuration. + + ) : standard.complianceStatus === "Compliant" && + standard.currentTenantValue === true ? ( This setting is configured correctly From 49d2beea4450e2ee2cb62dbc9fe1010cf4e681e5 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 12 Apr 2025 20:51:43 -0400 Subject: [PATCH 35/41] update extensions --- src/data/Extensions.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/data/Extensions.json b/src/data/Extensions.json index 895bb0971b14..3c61cbbba574 100644 --- a/src/data/Extensions.json +++ b/src/data/Extensions.json @@ -176,9 +176,9 @@ }, { "type": "password", - "name": "Sherweb.paxclientId", - "label": "Pax8 Client Id", - "placeholder": "Enter your Pax Client Id", + "name": "Pax8.clientId", + "label": "Pax8 Client ID", + "placeholder": "Enter your Pax8 Client ID", "required": true, "condition": { "field": "Sherweb.migrateFrom", @@ -188,7 +188,7 @@ }, { "type": "password", - "name": "Sherweb.paxclientSecret", + "name": "Pax8.APIKey", "label": "Pax8 Client Secret", "placeholder": "Enter your Pax Client Secret", "required": true, From 015cf88341fa72bb8e3d581ca20cc90b1d15f4e2 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 12 Apr 2025 22:41:48 -0400 Subject: [PATCH 36/41] fix resubmit support --- src/components/CippComponents/CippApiDialog.jsx | 7 +++++-- src/components/CippTable/CippDataTable.js | 1 + src/pages/cipp/scheduler/index.js | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/CippComponents/CippApiDialog.jsx b/src/components/CippComponents/CippApiDialog.jsx index 4cba7dd8d108..d083a3c9d2bf 100644 --- a/src/components/CippComponents/CippApiDialog.jsx +++ b/src/components/CippComponents/CippApiDialog.jsx @@ -339,11 +339,14 @@ export const CippApiDialog = (props) => { // Handle JSX/Component confirmText const replaceTextInElement = (element) => { if (!element) return element; - if (typeof element === 'string') { + if (typeof element === "string") { if (Array.isArray(row) && row.length > 1) { return element.replace(/\[([^\]]+)\]/g, "the selected rows"); } else if (Array.isArray(row) && row.length === 1) { - return element.replace(/\[([^\]]+)\]/g, (_, key) => getNestedValue(row[0], key) || `[${key}]`); + return element.replace( + /\[([^\]]+)\]/g, + (_, key) => getNestedValue(row[0], key) || `[${key}]` + ); } return element.replace(/\[([^\]]+)\]/g, (_, key) => getNestedValue(row, key) || `[${key}]`); } diff --git a/src/components/CippTable/CippDataTable.js b/src/components/CippTable/CippDataTable.js index 9237ab580ad1..25beed16fb5d 100644 --- a/src/components/CippTable/CippDataTable.js +++ b/src/components/CippTable/CippDataTable.js @@ -429,6 +429,7 @@ export const CippDataTable = (props) => { api={actionData.action} row={actionData.data} relatedQueryKeys={queryKey ? queryKey : title} + {...actionData.action} /> ); }, [actionData.ready, createDialog, actionData.action, actionData.data, queryKey, title])} diff --git a/src/pages/cipp/scheduler/index.js b/src/pages/cipp/scheduler/index.js index bf162c3147da..89aa1634241a 100644 --- a/src/pages/cipp/scheduler/index.js +++ b/src/pages/cipp/scheduler/index.js @@ -16,6 +16,7 @@ const Page = () => { data: { RowKey: "RowKey", RunNow: true }, icon: , confirmText: "Are you sure you want to run [Name]?", + allowResubmit: true, }, { label: "Edit Job", From 84f4e75614e3dfa30b123f7939ec51cd1c72c90c Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sun, 13 Apr 2025 01:18:13 -0400 Subject: [PATCH 37/41] scheduled task info page --- .../CippComponents/ScheduledTaskDetails.jsx | 315 +++++++++--------- src/pages/cipp/scheduler/index.js | 7 +- src/pages/cipp/scheduler/task.js | 22 ++ 3 files changed, 189 insertions(+), 155 deletions(-) create mode 100644 src/pages/cipp/scheduler/task.js diff --git a/src/components/CippComponents/ScheduledTaskDetails.jsx b/src/components/CippComponents/ScheduledTaskDetails.jsx index 618ae131ef67..88aabea0f512 100644 --- a/src/components/CippComponents/ScheduledTaskDetails.jsx +++ b/src/components/CippComponents/ScheduledTaskDetails.jsx @@ -11,6 +11,7 @@ import { InputAdornment, Tooltip, Stack, + Skeleton, } from "@mui/material"; import { ApiGetCall } from "../../api/ApiCall"; import { getCippTranslation } from "../../utils/get-cipp-translation"; @@ -22,8 +23,6 @@ import { CippTimeAgo } from "/src/components/CippComponents/CippTimeAgo"; const ScheduledTaskDetails = ({ data }) => { const [taskDetails, setTaskDetails] = useState(null); - const [dialogOpen, setDialogOpen] = useState(false); - const [selectedResult, setSelectedResult] = useState(null); const [expanded, setExpanded] = useState(false); const [searchQuery, setSearchQuery] = useState(""); @@ -40,7 +39,7 @@ const ScheduledTaskDetails = ({ data }) => { }); const taskProperties = [ - "Name", + "TaskState", "Command", "Tenant", "Recurrence", @@ -79,24 +78,19 @@ const ScheduledTaskDetails = ({ data }) => { return ( <> - - - Task Details - taskDetailResults.refetch()}> - - - + + {taskDetails?.Task?.Name} + taskDetailResults.refetch()}> + + +
+ } layout="dual" + title="Details" + variant="outlined" showDivider={false} propertyItems={taskProperties .filter((prop) => taskDetails?.Task?.[prop] != null && taskDetails?.Task?.[prop] !== "") @@ -109,151 +103,164 @@ const ScheduledTaskDetails = ({ data }) => { isFetching={taskDetailResults.isFetching} /> - {taskDetails?.Task?.Parameters && ( - - }> - Task Parameters - - - { - return { - label: key, - value: getCippFormatting(value, key), - }; - })} - isFetching={taskDetailResults.isFetching} - /> - - + {taskDetailResults.isFetching ? ( + + ) : ( + <> + {taskDetails?.Task?.Parameters && ( + + }> + Task Parameters + + + { + return { + label: key, + value: getCippFormatting(value, key), + }; + } + )} + isFetching={taskDetailResults.isFetching} + /> + + + )} + )} - {taskDetails?.Details?.length > 0 && ( + {taskDetailResults.isFetching ? ( + + ) : ( <> - - - Execution Results{" "} - {filteredDetails && ( - - ({filteredDetails.length} of {taskDetails.Details.length}) + {taskDetails?.Details?.length > 0 && ( + <> + + + Execution Results{" "} + {filteredDetails && ( + + ({filteredDetails.length} of {taskDetails.Details.length}) + + )} - )} - - setSearchQuery(e.target.value)} - InputProps={{ - startAdornment: ( - - - - ), - endAdornment: searchQuery && ( - - - setSearchQuery("")} - aria-label="Clear search" - > - - - - - ), - }} - /> - - - {filteredDetails && - filteredDetails.map((result, index) => ( - - } + sx={{ width: 250 }} + placeholder="Search results..." + value={searchQuery} + onChange={(e) => setSearchQuery(e.target.value)} + InputProps={{ + startAdornment: ( + + + + ), + endAdornment: searchQuery && ( + + + setSearchQuery("")} + aria-label="Clear search" + > + + + + + ), + }} + /> + + + {filteredDetails && + filteredDetails.map((result, index) => ( + + } + sx={{ + "& .MuiAccordionSummary-content": { + display: "flex", + justifyContent: "space-between", + alignItems: "center", + width: "100%", + }, + }} + > + {result.TenantName || result.Tenant} + } + sx={{ mx: 1 }} + /> + + + {result.Results === "null" ? ( + No data available + ) : Array.isArray(result.Results) ? ( + + ) : typeof result.Results === "object" ? ( + ({ + label: key, + value: typeof value === "object" ? JSON.stringify(value) : value, + }))} + /> + ) : ( + +
+                                {result.Results}
+                              
+
+ )} +
+
+ ))} + {filteredDetails && filteredDetails.length === 0 && ( + - {result.TenantName || result.Tenant} - } - sx={{ mx: 1 }} - /> - - - {result.Results === "null" ? ( - No data available - ) : Array.isArray(result.Results) ? ( - - ) : typeof result.Results === "object" ? ( - ({ - label: key, - value: typeof value === "object" ? JSON.stringify(value) : value, - }))} - /> - ) : ( - -
-                            {result.Results}
-                          
-
- )} -
- - ))} - {filteredDetails && filteredDetails.length === 0 && ( - - - No results match your search criteria - - - )} -
+ + No results match your search criteria + + + )} + + + )} )} - + ); }; diff --git a/src/pages/cipp/scheduler/index.js b/src/pages/cipp/scheduler/index.js index 89aa1634241a..6bd64ff2d6fd 100644 --- a/src/pages/cipp/scheduler/index.js +++ b/src/pages/cipp/scheduler/index.js @@ -2,13 +2,18 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippTablePage from "/src/components/CippComponents/CippTablePage"; import { Button } from "@mui/material"; import Link from "next/link"; -import { CalendarDaysIcon, TrashIcon } from "@heroicons/react/24/outline"; +import { CalendarDaysIcon, EyeIcon, TrashIcon } from "@heroicons/react/24/outline"; import { useState } from "react"; import { CopyAll, Edit, PlayArrow } from "@mui/icons-material"; import ScheduledTaskDetails from "../../../components/CippComponents/ScheduledTaskDetails"; const Page = () => { const actions = [ + { + label: "View Task Details", + link: "/cipp/scheduler/task?id=[RowKey]", + icon: , + }, { label: "Run Now", type: "POST", diff --git a/src/pages/cipp/scheduler/task.js b/src/pages/cipp/scheduler/task.js new file mode 100644 index 000000000000..a41c47ee55bf --- /dev/null +++ b/src/pages/cipp/scheduler/task.js @@ -0,0 +1,22 @@ +import { useRouter } from "next/router"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import ScheduledTaskDetails from "../../../components/CippComponents/ScheduledTaskDetails"; +import CippPageCard from "../../../components/CippCards/CippPageCard"; +import { CardContent, CardHeader } from "@mui/material"; + +const Page = () => { + const router = useRouter(); + const { id } = router.query; + + return ( + + + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; From 69fe571cba6e98b6cb537df6b820881604c196c8 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 13 Apr 2025 13:04:34 +0200 Subject: [PATCH 38/41] Add sort --- src/layouts/top-nav.js | 125 +++++++++++++++++++++++++++++++++++------ 1 file changed, 107 insertions(+), 18 deletions(-) diff --git a/src/layouts/top-nav.js b/src/layouts/top-nav.js index 0cebc1ac3acd..f41183ba0f11 100644 --- a/src/layouts/top-nav.js +++ b/src/layouts/top-nav.js @@ -5,6 +5,10 @@ import Bars3Icon from "@heroicons/react/24/outline/Bars3Icon"; import MoonIcon from "@heroicons/react/24/outline/MoonIcon"; import SunIcon from "@heroicons/react/24/outline/SunIcon"; import BookmarkIcon from "@mui/icons-material/Bookmark"; +import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward"; +import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward"; +import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; +import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; import { Box, Divider, @@ -27,6 +31,8 @@ import { NotificationsPopover } from "./notifications-popover"; import { useDialog } from "../hooks/use-dialog"; import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; import { CippCentralSearch } from "../components/CippComponents/CippCentralSearch"; +import { applySort } from "../utils/apply-sort"; + const TOP_NAV_HEIGHT = 64; export const TopNav = (props) => { @@ -42,6 +48,7 @@ export const TopNav = (props) => { }, [settings]); const [anchorEl, setAnchorEl] = useState(null); + const [sortOrder, setSortOrder] = useState("asc"); const handleBookmarkClick = (event) => { setAnchorEl(event.currentTarget); @@ -51,8 +58,42 @@ export const TopNav = (props) => { setAnchorEl(null); }; - const open = Boolean(anchorEl); - const id = open ? "bookmark-popover" : undefined; + const handleSortToggle = () => { + const newSortOrder = sortOrder === "asc" ? "desc" : "asc"; + setSortOrder(newSortOrder); + + // Save the new sort order and re-order bookmarks + const sortedBookmarks = applySort(settings.bookmarks || [], "label", newSortOrder); + settings.handleUpdate({ + bookmarks: sortedBookmarks, + sortOrder: newSortOrder, + }); + }; + + // Move a bookmark up in the list + const moveBookmarkUp = (index) => { + if (index <= 0) return; + + const updatedBookmarks = [...(settings.bookmarks || [])]; + const temp = updatedBookmarks[index]; + updatedBookmarks[index] = updatedBookmarks[index - 1]; + updatedBookmarks[index - 1] = temp; + + settings.handleUpdate({ bookmarks: updatedBookmarks }); + }; + + // Move a bookmark down in the list + const moveBookmarkDown = (index) => { + const bookmarks = settings.bookmarks || []; + if (index >= bookmarks.length - 1) return; + + const updatedBookmarks = [...bookmarks]; + const temp = updatedBookmarks[index]; + updatedBookmarks[index] = updatedBookmarks[index + 1]; + updatedBookmarks[index + 1] = temp; + + settings.handleUpdate({ bookmarks: updatedBookmarks }); + }; useEffect(() => { const handleKeyDown = (event) => { @@ -67,10 +108,22 @@ export const TopNav = (props) => { }; }, []); + useEffect(() => { + if (settings.sortOrder) { + setSortOrder(settings.sortOrder); + } + }, [settings.sortOrder]); + const openSearch = () => { searchDialog.handleOpen(); }; + // Use the sorted bookmarks if sorting is applied, otherwise use the bookmarks in their current order + const displayBookmarks = settings.bookmarks || []; + + const open = Boolean(anchorEl); + const id = open ? "bookmark-popover" : undefined; + return ( { horizontal: "center", }} > - - {(settings.bookmarks || []).length === 0 ? ( + + + + + {sortOrder === "asc" ? : } + + + Sort Alphabetically + + {displayBookmarks.length === 0 ? ( No bookmarks added yet - } + primary={No bookmarks added yet} /> ) : ( - settings.bookmarks.map((bookmark, idx) => ( + displayBookmarks.map((bookmark, idx) => ( handleBookmarkClose()} + sx={{ + color: "inherit", + display: "flex", + justifyContent: "space-between", + }} > - {bookmark.label} - } - /> + handleBookmarkClose()} + sx={{ + textDecoration: "none", + color: "inherit", + flexGrow: 1, + marginRight: 2, + }} + > + {bookmark.label} + + + { + e.preventDefault(); + moveBookmarkUp(idx); + }} + disabled={idx === 0} + > + + + { + e.preventDefault(); + moveBookmarkDown(idx); + }} + disabled={idx === displayBookmarks.length - 1} + > + + + )) )} From 9bf1fee3708a60336cf87ae5751c3696ce83a3a9 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 13 Apr 2025 13:30:23 +0200 Subject: [PATCH 39/41] task removal --- .../conditional/deploy-vacation/index.js | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/pages/tenant/conditional/deploy-vacation/index.js b/src/pages/tenant/conditional/deploy-vacation/index.js index ca4092689195..e1c24e2a761c 100644 --- a/src/pages/tenant/conditional/deploy-vacation/index.js +++ b/src/pages/tenant/conditional/deploy-vacation/index.js @@ -3,8 +3,22 @@ import CippTablePage from "/src/components/CippComponents/CippTablePage"; import { Button } from "@mui/material"; import { EventAvailable } from "@mui/icons-material"; import Link from "next/link"; +import { Delete } from "@mui/icons-material"; const Page = () => { + const actions = [ + { + label: "Cancel Vacation Mode", + type: "POST", + url: "/api/RemoveScheduledItem", + data: { ID: "RowKey" }, + confirmText: + "Are you sure you want to cancel this vacation mode entry? This might mean the user will remain in vacation mode permanently.", + icon: , + multiPost: false, + }, + ]; + return ( { apiUrl="/api/ListScheduledItems?Type=Set-CIPPCAExclusion" queryKey="VacationMode" tenantInTitle={false} + actions={actions} simpleColumns={[ "Name", "TaskState", @@ -26,6 +41,18 @@ const Page = () => { "Parameters.UserName", "Parameters.PolicyId", ]} + offCanvas={{ + extendedInfoFields: [ + "Name", + "TaskState", + "ScheduledTime", + "Parameters.UserName", + "Parameters.PolicyId", + "Tenant", + "ExecutedTime", + ], + actions: actions, + }} /> ); }; From d8e150ae8e15d028d1ca486e8e5f58bae6d56230 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sun, 13 Apr 2025 13:10:51 -0400 Subject: [PATCH 40/41] standards organization --- .../CippStandards/CippStandardAccordion.jsx | 15 +- src/pages/tenant/standards/compare/index.js | 685 ++++++++++-------- 2 files changed, 382 insertions(+), 318 deletions(-) diff --git a/src/components/CippStandards/CippStandardAccordion.jsx b/src/components/CippStandards/CippStandardAccordion.jsx index 4564e4dc49c9..8037de3b2e2b 100644 --- a/src/components/CippStandards/CippStandardAccordion.jsx +++ b/src/components/CippStandards/CippStandardAccordion.jsx @@ -36,6 +36,7 @@ import Defender from "../../icons/iconly/bulk/defender"; import Intune from "../../icons/iconly/bulk/intune"; import GDAPRoles from "/src/data/GDAPRoles"; import timezoneList from "/src/data/timezoneList"; +import standards from "/src/data/standards.json"; const getAvailableActions = (disabledFeatures) => { const allActions = [ @@ -81,7 +82,7 @@ const CippAddedComponent = React.memo(({ standardName, component, formControl }) CippAddedComponent.displayName = "CippAddedComponent"; const CippStandardAccordion = ({ - standards, + standards: providedStandards, selectedStandards, expanded, handleAccordionToggle, @@ -101,7 +102,7 @@ const CippStandardAccordion = ({ const newConfiguredState = { ...configuredState }; Object.keys(selectedStandards).forEach((standardName) => { - const standard = standards.find((s) => s.name === standardName.split("[")[0]); + const standard = providedStandards.find((s) => s.name === standardName.split("[")[0]); if (standard) { const actionFilled = !!_.get(watchedValues, `${standardName}.action`, false); @@ -123,16 +124,18 @@ const CippStandardAccordion = ({ if (!_.isEqual(newConfiguredState, configuredState)) { setConfiguredState(newConfiguredState); } - }, [watchedValues, standards, selectedStandards]); + }, [watchedValues, providedStandards, selectedStandards]); const groupedStandards = useMemo(() => { const result = {}; Object.keys(selectedStandards).forEach((standardName) => { - const standard = standards.find((s) => s.name === standardName.split("[")[0]); + const baseStandardName = standardName.split("[")[0]; + const standard = providedStandards.find((s) => s.name === baseStandardName); if (!standard) return; - const category = standard.cat || "Other Standards"; + const standardInfo = standards.find((s) => s.name === baseStandardName); + const category = standardInfo?.cat || "Other Standards"; if (!result[category]) { result[category] = []; @@ -149,7 +152,7 @@ const CippStandardAccordion = ({ }); return result; - }, [selectedStandards, standards]); + }, [selectedStandards, providedStandards]); const filteredGroupedStandards = useMemo(() => { if (!searchQuery && filter === "all") { diff --git a/src/pages/tenant/standards/compare/index.js b/src/pages/tenant/standards/compare/index.js index f78f32f4cee6..59f092bd81ab 100644 --- a/src/pages/tenant/standards/compare/index.js +++ b/src/pages/tenant/standards/compare/index.js @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import React, { useState, useEffect, useMemo } from "react"; import { Button, Card, @@ -257,19 +257,69 @@ const Page = () => { ]); const comparisonModeOptions = [{ label: "Compare Tenant to Standard", value: "standard" }]; - const filteredData = comparisonData?.filter((standard) => { - const matchesFilter = - filter === "all" || - (filter === "compliant" && standard.complianceStatus === "Compliant") || - (filter === "nonCompliant" && standard.complianceStatus === "Non-Compliant"); + // Group standards by category + const groupedStandards = useMemo(() => { + if (!comparisonData) return {}; - const matchesSearch = - !searchQuery || - standard.standardName.toLowerCase().includes(searchQuery.toLowerCase()) || - standard.standardDescription.toLowerCase().includes(searchQuery.toLowerCase()); + const result = {}; - return matchesFilter && matchesSearch; - }); + comparisonData.forEach((standard) => { + // Find the standard info in the standards.json data + const standardInfo = standards.find((s) => s.name === standard.standardId); + + // Use the category from standards.json, or default to "Other Standards" + const category = standardInfo?.cat || "Other Standards"; + + if (!result[category]) { + result[category] = []; + } + + result[category].push(standard); + }); + + // Sort standards within each category + Object.keys(result).forEach((category) => { + result[category].sort((a, b) => a.standardName.localeCompare(b.standardName)); + }); + + return result; + }, [comparisonData]); + + const filteredGroupedStandards = useMemo(() => { + if (!groupedStandards) return {}; + + if (!searchQuery && filter === "all") { + return groupedStandards; + } + + const result = {}; + const searchLower = searchQuery.toLowerCase(); + + Object.keys(groupedStandards).forEach((category) => { + const categoryMatchesSearch = !searchQuery || category.toLowerCase().includes(searchLower); + + const filteredStandards = groupedStandards[category].filter((standard) => { + const matchesFilter = + filter === "all" || + (filter === "compliant" && standard.complianceStatus === "Compliant") || + (filter === "nonCompliant" && standard.complianceStatus === "Non-Compliant"); + + const matchesSearch = + !searchQuery || + categoryMatchesSearch || + standard.standardName.toLowerCase().includes(searchLower) || + standard.standardDescription.toLowerCase().includes(searchLower); + + return matchesFilter && matchesSearch; + }); + + if (filteredStandards.length > 0) { + result[category] = filteredStandards; + } + }); + + return result; + }, [groupedStandards, searchQuery, filter]); const allCount = comparisonData?.length || 0; const compliantCount = @@ -571,7 +621,7 @@ const Page = () => { )} - {filteredData && filteredData.length === 0 && ( + {filteredGroupedStandards && Object.keys(filteredGroupedStandards).length === 0 && ( No standards match the selected filter criteria or search query. @@ -581,344 +631,355 @@ const Page = () => { )} - {filteredData && - filteredData.length > 0 && - filteredData.map((standard, index) => ( - - - - + + {Object.keys(filteredGroupedStandards).map((category) => ( + + + {category} + + + {filteredGroupedStandards[category].map((standard, index) => ( + + + - - - {standard.complianceStatus === "Compliant" ? ( - - ) : standard.complianceStatus === "Reporting Disabled" ? ( - - ) : ( - - )} - - - {standard?.standardName} - - - - - - - - - - {!standard.standardValue ? ( - - This data has not yet been collected. Collect the data by pressing the - report button on the top of the page. - - ) : ( - - + + - {standard.standardValue && - typeof standard.standardValue === "object" && - Object.keys(standard.standardValue).length > 0 ? ( - Object.entries(standard.standardValue).map(([key, value]) => ( - - - {key}: - - - {typeof value === "object" && value !== null - ? value.label || JSON.stringify(value) - : value === true - ? "Enabled" - : value === false - ? "Disabled" - : String(value)} - - - )) + {standard.complianceStatus === "Compliant" ? ( + + ) : standard.complianceStatus === "Reporting Disabled" ? ( + ) : ( - - {standard.standardValue === true ? ( - - This setting is configured correctly - - ) : standard.standardValue === false ? ( - - This setting is not configured correctly - - ) : standard.standardValue !== undefined ? ( - typeof standard.standardValue === "object" ? ( - "No settings configured" - ) : ( - String(standard.standardValue) - ) - ) : ( - - This setting is not configured, or data has not been collected. - If you are getting this after data collection, the tenant might - not be licensed for this feature - - )} - + )} + + {standard?.standardName} + + + + + + + + + + {!standard.standardValue ? ( + + This data has not yet been collected. Collect the data by pressing the + report button on the top of the page. + + ) : ( + + + + {standard.standardValue && + typeof standard.standardValue === "object" && + Object.keys(standard.standardValue).length > 0 ? ( + Object.entries(standard.standardValue).map(([key, value]) => ( + + + {key}: + + + {typeof value === "object" && value !== null + ? value.label || JSON.stringify(value) + : value === true + ? "Enabled" + : value === false + ? "Disabled" + : String(value)} + + + )) + ) : ( + + {standard.standardValue === true ? ( + + This setting is configured correctly + + ) : standard.standardValue === false ? ( + + This setting is not configured correctly + + ) : standard.standardValue !== undefined ? ( + typeof standard.standardValue === "object" ? ( + "No settings configured" + ) : ( + String(standard.standardValue) + ) + ) : ( + + This setting is not configured, or data has not been + collected. If you are getting this after data collection, the + tenant might not be licensed for this feature + + )} + + )} + + + )} + + + - )} - - - - - - + + - - - + + - + + + + + + + {currentTenant} + + + + + - + + + {standard.complianceStatus} + - - {currentTenant} - - - - - + + + + {/* Existing tenant comparison content */} + {typeof standard.currentTenantValue === "object" && + standard.currentTenantValue !== null ? ( + {standard.complianceStatus === "Reporting Disabled" ? ( + + Reporting is disabled for this standard in the template configuration. + + ) : ( + Object.entries(standard.currentTenantValue).map(([key, value]) => { + const standardValueForKey = + standard.standardValue && typeof standard.standardValue === "object" + ? standard.standardValue[key] + : undefined; + + const isDifferent = + standardValueForKey !== undefined && + JSON.stringify(value) !== JSON.stringify(standardValueForKey); + + return ( + + + {key}: + + + {standard.complianceStatus === "Compliant" && value === true + ? "Compliant" + : typeof value === "object" && value !== null + ? value.label || JSON.stringify(value) + : value === true + ? "Enabled" + : value === false + ? "Disabled" + : String(value)} + + + ); + }) + )} + + ) : ( + - - {standard.complianceStatus} + > + {standard.complianceStatus === "Reporting Disabled" ? ( + + Reporting is disabled for this standard in the template configuration. + + ) : standard.complianceStatus === "Compliant" && + standard.currentTenantValue === true ? ( + + This setting is configured correctly + + ) : standard.currentTenantValue === false ? ( + + This setting is not configured correctly + + ) : standard.currentTenantValue !== undefined ? ( + String(standard.currentTenantValue) + ) : ( + + This setting is not configured, or data has not been collected. If you + are getting this after data collection, the tenant might not be + licensed for this feature + + )} - - - - - - {typeof standard.currentTenantValue === "object" && - standard.currentTenantValue !== null ? ( - - {standard.complianceStatus === "Reporting Disabled" ? ( - - Reporting is disabled for this standard in the template configuration. - - ) : ( - Object.entries(standard.currentTenantValue).map(([key, value]) => { - const standardValueForKey = - standard.standardValue && typeof standard.standardValue === "object" - ? standard.standardValue[key] - : undefined; - - const isDifferent = - standardValueForKey !== undefined && - JSON.stringify(value) !== JSON.stringify(standardValueForKey); - - return ( - - - {key}: - - - {standard.complianceStatus === "Compliant" && value === true - ? "Compliant" - : typeof value === "object" && value !== null - ? value.label || JSON.stringify(value) - : value === true - ? "Enabled" - : value === false - ? "Disabled" - : String(value)} - - - ); - }) - )} - - ) : ( - - {standard.complianceStatus === "Reporting Disabled" ? ( - - Reporting is disabled for this standard in the template configuration. - - ) : standard.complianceStatus === "Compliant" && - standard.currentTenantValue === true ? ( - - This setting is configured correctly - - ) : standard.currentTenantValue === false ? ( - - This setting is not configured correctly - - ) : standard.currentTenantValue !== undefined ? ( - String(standard.currentTenantValue) - ) : ( - - This setting is not configured, or data has not been collected. If you - are getting this after data collection, the tenant might not be licensed - for this feature - - )} - - )} - - - - - {standard.complianceDetails && ( - - - - - - - {standard.complianceDetails} - + )} + - )} - - ))} + + {standard.complianceDetails && ( + + + + + + + {standard.complianceDetails} + + + + )} + + ))} + + ))} Date: Sun, 13 Apr 2025 19:50:31 +0200 Subject: [PATCH 41/41] version up --- public/version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/version.json b/public/version.json index e13bd0cafa30..a687242f92c7 100644 --- a/public/version.json +++ b/public/version.json @@ -1,3 +1,3 @@ { - "version": "7.4.2" + "version": "7.5.0" }