diff --git a/.github/workflows/Node_Project_Check.yml b/.github/workflows/Node_Project_Check.yml
index 1116a307ceb7..3347edfbe84d 100644
--- a/.github/workflows/Node_Project_Check.yml
+++ b/.github/workflows/Node_Project_Check.yml
@@ -20,7 +20,7 @@ jobs:
steps:
- uses: actions/checkout@v6
- name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v6.3.0
+ uses: actions/setup-node@v6.4.0
with:
node-version: ${{ matrix.node-version }}
- name: Install and Build Test
diff --git a/.github/workflows/cipp_dev_build.yml b/.github/workflows/cipp_dev_build.yml
index f0131a4f3692..394b8bc794f1 100644
--- a/.github/workflows/cipp_dev_build.yml
+++ b/.github/workflows/cipp_dev_build.yml
@@ -26,7 +26,7 @@ jobs:
echo "node_version=$node_sanitized_version" >> $GITHUB_OUTPUT
- name: Set up Node.js
- uses: actions/setup-node@v6.3.0
+ uses: actions/setup-node@v6.4.0
with:
node-version: ${{ steps.get_node_version.outputs.node_version }}
diff --git a/.github/workflows/cipp_frontend_build.yml b/.github/workflows/cipp_frontend_build.yml
index 8dedfa497669..5c8d7230e9d3 100644
--- a/.github/workflows/cipp_frontend_build.yml
+++ b/.github/workflows/cipp_frontend_build.yml
@@ -26,7 +26,7 @@ jobs:
echo "node_version=$node_sanitized_version" >> $GITHUB_OUTPUT
- name: Set up Node.js
- uses: actions/setup-node@v6.3.0
+ uses: actions/setup-node@v6.4.0
with:
node-version: ${{ steps.get_node_version.outputs.node_version }}
diff --git a/.github/workflows/pr_check.yml b/.github/workflows/pr_check.yml
index ea36ac1e82f7..84555d0e0ebb 100644
--- a/.github/workflows/pr_check.yml
+++ b/.github/workflows/pr_check.yml
@@ -22,7 +22,7 @@ jobs:
# Only process fork PRs with specific branch conditions
# Must be a fork AND (source is main/master OR target is main/master)
if: |
- github.event.pull_request.head.repo.fork == true &&
+ github.event.pull_request.head.repo.fork == true &&
((github.event.pull_request.head.ref == 'main' || github.event.pull_request.head.ref == 'master') ||
(github.event.pull_request.base.ref == 'main' || github.event.pull_request.base.ref == 'master'))
uses: actions/github-script@v9
@@ -31,7 +31,31 @@ jobs:
script: |
let message = '';
+ // Check if the fork has open PRs (indicates pull bot or similar is active)
+ const forkOwner = context.payload.pull_request.head.repo.owner.login;
+ const forkRepo = context.payload.pull_request.head.repo.name;
+ const forkPullsUrl = context.payload.pull_request.head.repo.html_url + '/pulls';
+
+ let openPRs = [];
+ try {
+ const { data: prs } = await github.rest.pulls.list({
+ owner: forkOwner,
+ repo: forkRepo,
+ state: 'open',
+ per_page: 5
+ });
+ openPRs = prs;
+ } catch (e) {
+ // Can't read fork PRs — skip
+ }
+
message += '🔄 If you are attempting to update your CIPP repo please follow the instructions at: https://docs.cipp.app/setup/self-hosting-guide/updating. Are you a sponsor? Contact the helpdesk for direct assistance with updating to the latest version.';
+
+ if (openPRs.length > 0) {
+ message += ` It looks like you may already have a pending update PR on your fork — check your [open pull requests](${forkPullsUrl}) to accept it.`;
+ } else {
+ message += ` You can enable [Pull Bot](https://github.com/apps/pull) or [Repo Sync](https://github.com/apps/repo-sync) to automatically keep your fork up to date.`;
+ }
message += '\n\n';
// Check if PR is targeting main/master
@@ -40,7 +64,7 @@ jobs:
}
// Check if PR is from a fork's main/master branch
- if (context.payload.pull_request.head.repo.fork &&
+ if (context.payload.pull_request.head.repo.fork &&
(context.payload.pull_request.head.ref === 'main' || context.payload.pull_request.head.ref === 'master')) {
message += '⚠️ This PR cannot be merged because it originates from your fork\'s main/master branch. If you are attempting to contribute code please PR from your dev branch or another non-main/master branch.\n\n';
}
diff --git a/next.config.js b/next.config.js
index 2399b9b84ed7..97685f34f91e 100644
--- a/next.config.js
+++ b/next.config.js
@@ -1,6 +1,27 @@
+const disableOptimizePackageImports = process.env.NEXT_DISABLE_OPTIMIZE_PACKAGE_IMPORTS === '1'
+
/** @type {import('next').NextConfig} */
const config = {
reactStrictMode: false,
+ experimental: {
+ optimizePackageImports: disableOptimizePackageImports
+ ? []
+ : [
+ '@mui/material',
+ '@mui/icons-material',
+ '@mui/lab',
+ '@mui/system',
+ '@mui/x-date-pickers',
+ 'material-react-table',
+ 'mui-tiptap',
+ 'recharts',
+ '@react-pdf/renderer',
+ ],
+ webpackMemoryOptimizations: true,
+ preloadEntriesOnStart: false,
+ turbopackFileSystemCacheForDev: false,
+ turbopackMemoryLimit: 4096,
+ },
images: {
unoptimized: true,
},
@@ -12,12 +33,6 @@ const config = {
},
},
},
- experimental: {
- webpackMemoryOptimizations: true,
- preloadEntriesOnStart: false,
- turbopackFileSystemCacheForDev: false,
- turbopackMemoryLimit: 4096,
- },
async redirects() {
return []
},
diff --git a/package.json b/package.json
index b6cd7f8010a3..9a98ddfc4b3a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "cipp",
- "version": "10.4.2",
+ "version": "10.4.5",
"author": "CIPP Contributors",
"homepage": "https://cipp.app/",
"bugs": {
@@ -16,7 +16,7 @@
},
"scripts": {
"dev": "next -H 127.0.0.1",
- "build": "next build && rm -rf package.json yarn.lock",
+ "build": "next build --webpack && rm -rf package.json yarn.lock",
"start": "next start",
"export": "next export",
"lint": "npx eslint .",
@@ -43,8 +43,8 @@
"@reduxjs/toolkit": "^2.11.2",
"@tanstack/query-sync-storage-persister": "^5.90.25",
"@tanstack/react-query": "^5.96.2",
- "@tanstack/react-query-devtools": "^5.51.11",
- "@tanstack/react-query-persist-client": "^5.76.0",
+ "@tanstack/react-query-devtools": "^5.96.2",
+ "@tanstack/react-query-persist-client": "^5.96.2",
"@tanstack/react-table": "^8.19.2",
"@tiptap/core": "^3.4.1",
"@tiptap/extension-heading": "^3.4.1",
@@ -53,7 +53,7 @@
"@tiptap/pm": "^3.22.3",
"@tiptap/react": "^3.20.5",
"@tiptap/starter-kit": "^3.20.5",
- "@uiw/react-json-view": "^2.0.0-alpha.41",
+ "@uiw/react-json-view": "^2.0.0-alpha.42",
"@vvo/tzdb": "^6.198.0",
"apexcharts": "5.10.4",
"axios": "1.15.0",
@@ -73,7 +73,7 @@
"lodash.isequal": "4.5.0",
"material-react-table": "^3.0.1",
"monaco-editor": "^0.55.1",
- "mui-tiptap": "^1.29.1",
+ "mui-tiptap": "^1.30.0",
"next": "^16.2.2",
"nprogress": "0.2.0",
"numeral": "2.0.6",
@@ -102,7 +102,7 @@
"react-time-ago": "^7.3.3",
"react-virtuoso": "^4.18.5",
"react-window": "^2.2.7",
- "recharts": "^3.7.0",
+ "recharts": "^3.8.1",
"redux": "5.0.1",
"redux-devtools-extension": "2.13.9",
"redux-persist": "^6.0.0",
diff --git a/public/languageList.json b/public/languageList.json
index 769bf0b60f6b..fe6f901b3089 100644
--- a/public/languageList.json
+++ b/public/languageList.json
@@ -160,6 +160,13 @@
"languageTag": "Danish (da-DK)",
"LCID": "1030"
},
+ {
+ "language": "Dutch",
+ "Geographic area": "Belgium",
+ "tag": "nl-BE",
+ "languageTag": "Dutch (nl-BE)",
+ "LCID": "2067"
+ },
{
"language": "Dutch",
"Geographic area": "Netherlands",
diff --git a/public/manifest.json b/public/manifest.json
index 2cc60cd8b5a7..42f5d73ea6af 100644
--- a/public/manifest.json
+++ b/public/manifest.json
@@ -1,15 +1,26 @@
{
- "short_name": "Carpatin",
- "name": "Carpatin",
+ "short_name": "CIPP",
+ "name": "CIPP - CyberDrian Improved Partner Portal",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
+ },
+ {
+ "src": "android-chrome-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "android-chrome-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png"
}
],
- "start_url": ".",
+ "start_url": "/",
+ "scope": "/",
"display": "standalone",
- "theme_color": "#000000",
+ "theme_color": "#ffffff",
"background_color": "#ffffff"
-}
\ No newline at end of file
+}
diff --git a/public/sw.js b/public/sw.js
new file mode 100644
index 000000000000..a5b7af04ecf4
--- /dev/null
+++ b/public/sw.js
@@ -0,0 +1,8 @@
+// Minimal service worker to satisfy Chrome's installability criteria.
+// This does NOT cache anything or provide offline support — it simply
+// passes all requests through to the network so Chrome treats the site
+// as an installable web app.
+
+self.addEventListener('install', () => self.skipWaiting())
+self.addEventListener('activate', (event) => event.waitUntil(self.clients.claim()))
+self.addEventListener('fetch', () => {})
diff --git a/public/version.json b/public/version.json
index fdddd5f6239a..a09d0fcf2ccd 100644
--- a/public/version.json
+++ b/public/version.json
@@ -1,3 +1,3 @@
{
- "version": "10.4.2"
+ "version": "10.4.5"
}
diff --git a/src/components/CippCards/CippDomainCards.jsx b/src/components/CippCards/CippDomainCards.jsx
index 9268fcd08f96..fede72bd1347 100644
--- a/src/components/CippCards/CippDomainCards.jsx
+++ b/src/components/CippCards/CippDomainCards.jsx
@@ -470,6 +470,13 @@ export const CippDomainCards = ({ domain: propDomain = "", fullwidth = false })
waiting: !!domain,
});
+ const { data: autoDiscoverData, isFetching: autoDiscoverLoading } = ApiGetCall({
+ url: "/api/ListDomainHealth",
+ queryKey: `autodiscover-${domain}`,
+ data: { Domain: domain, Action: "ReadAutoDiscover" },
+ waiting: !!domain,
+ });
+
const { data: httpsData, isFetching: httpsLoading } = ApiGetCall({
url: "/api/ListDomainHealth",
queryKey: `https-${domain}-${subdomains}`,
@@ -684,6 +691,26 @@ export const CippDomainCards = ({ domain: propDomain = "", fullwidth = false })
}
/>
+
+
+
+ AutoDiscover ({autoDiscoverData?.RecordType || "None"}):
+
+
+
+
+ }
+ />
+
{enableHttps && (
Disconnect all current sessions
Remove all MFA methods for the user
Disable all inbox rules for the user
+ Disable OneDrive sharing
- );
+ )
}
diff --git a/src/components/CippComponents/CippApiResults.jsx b/src/components/CippComponents/CippApiResults.jsx
index 1c4ca364c362..12a0ec4be593 100644
--- a/src/components/CippComponents/CippApiResults.jsx
+++ b/src/components/CippComponents/CippApiResults.jsx
@@ -188,21 +188,23 @@ export const CippApiResults = (props) => {
} else {
setFetchingVisible(false);
}
- if (!errorsOnly) {
- if (allResults.length > 0) {
- setFinalResults(
- allResults.map((res, index) => ({
- id: index,
- text: res.text,
- copyField: res.copyField,
- severity: res.severity,
- visible: true,
- ...res,
- })),
- );
- } else {
- setFinalResults([]);
- }
+ const resultsToShow = errorsOnly
+ ? allResults.filter((r) => r.severity === "error")
+ : allResults;
+
+ if (resultsToShow.length > 0) {
+ setFinalResults(
+ resultsToShow.map((res, index) => ({
+ id: index,
+ text: res.text,
+ copyField: res.copyField,
+ severity: res.severity,
+ visible: true,
+ ...res,
+ })),
+ );
+ } else {
+ setFinalResults([]);
}
}, [
apiObject.isError,
diff --git a/src/components/CippComponents/CippAppTemplateDrawer.jsx b/src/components/CippComponents/CippAppTemplateDrawer.jsx
index e9db8701f3d6..4727f66988f2 100644
--- a/src/components/CippComponents/CippAppTemplateDrawer.jsx
+++ b/src/components/CippComponents/CippAppTemplateDrawer.jsx
@@ -892,6 +892,21 @@ export const CippAppTemplateDrawer = ({
/>
+
+
+
+
+
{/* Add App Button */}
{applicationType?.value && (
diff --git a/src/components/CippComponents/CippApplicationDeployDrawer.jsx b/src/components/CippComponents/CippApplicationDeployDrawer.jsx
index 99c2cd52d249..a68ade232835 100644
--- a/src/components/CippComponents/CippApplicationDeployDrawer.jsx
+++ b/src/components/CippComponents/CippApplicationDeployDrawer.jsx
@@ -1,119 +1,119 @@
-import React, { useEffect, useCallback, useState } from "react";
-import { Divider, Button, Alert, CircularProgress } from "@mui/material";
-import { Grid } from "@mui/system";
-import { useForm, useWatch } from "react-hook-form";
-import { Add } from "@mui/icons-material";
-import { CippOffCanvas } from "./CippOffCanvas";
-import CippFormComponent from "./CippFormComponent";
-import { CippFormTenantSelector } from "./CippFormTenantSelector";
-import { CippFormCondition } from "./CippFormCondition";
-import { CippApiResults } from "./CippApiResults";
-import languageList from "../../data/languageList.json";
-import { ApiPostCall } from "../../api/ApiCall";
+import React, { useEffect, useCallback, useState } from 'react'
+import { Divider, Button, Alert, CircularProgress } from '@mui/material'
+import { Grid } from '@mui/system'
+import { useForm, useWatch } from 'react-hook-form'
+import { Add } from '@mui/icons-material'
+import { CippOffCanvas } from './CippOffCanvas'
+import CippFormComponent from './CippFormComponent'
+import { CippFormTenantSelector } from './CippFormTenantSelector'
+import { CippFormCondition } from './CippFormCondition'
+import { CippApiResults } from './CippApiResults'
+import languageList from '../../data/languageList.json'
+import { ApiPostCall } from '../../api/ApiCall'
export const CippApplicationDeployDrawer = ({
- buttonText = "Add Application",
+ buttonText = 'Add Application',
requiredPermissions = [],
PermissionButton = Button,
}) => {
- const [drawerVisible, setDrawerVisible] = useState(false);
+ const [drawerVisible, setDrawerVisible] = useState(false)
const formControl = useForm({
- mode: "onChange",
- });
+ mode: 'onChange',
+ })
const selectedTenants = useWatch({
control: formControl.control,
- name: "selectedTenants",
- });
+ name: 'selectedTenants',
+ })
const applicationType = useWatch({
control: formControl.control,
- name: "appType",
- });
+ name: 'appType',
+ })
const searchQuerySelection = useWatch({
control: formControl.control,
- name: "packageSearch",
- });
+ name: 'packageSearch',
+ })
const updateSearchSelection = useCallback(
(searchQuerySelection) => {
if (searchQuerySelection) {
- formControl.setValue("packagename", searchQuerySelection.value.packagename);
- formControl.setValue("applicationName", searchQuerySelection.value.applicationName);
- formControl.setValue("description", searchQuerySelection.value.description);
+ formControl.setValue('packagename', searchQuerySelection.value.packagename)
+ formControl.setValue('applicationName', searchQuerySelection.value.applicationName)
+ formControl.setValue('description', searchQuerySelection.value.description)
searchQuerySelection.value.customRepo
- ? formControl.setValue("customRepo", searchQuerySelection.value.customRepo)
- : null;
+ ? formControl.setValue('customRepo', searchQuerySelection.value.customRepo)
+ : null
}
},
- [formControl.setValue],
- );
+ [formControl.setValue]
+ )
useEffect(() => {
- updateSearchSelection(searchQuerySelection);
- }, [updateSearchSelection, searchQuerySelection]);
+ updateSearchSelection(searchQuerySelection)
+ }, [updateSearchSelection, searchQuerySelection])
const postUrl = {
- mspApp: "/api/AddMSPApp",
- StoreApp: "/api/AddStoreApp",
- winGetApp: "/api/AddwinGetApp",
- chocolateyApp: "/api/AddChocoApp",
- officeApp: "/api/AddOfficeApp",
- win32ScriptApp: "/api/AddWin32ScriptApp",
- };
+ mspApp: '/api/AddMSPApp',
+ StoreApp: '/api/AddStoreApp',
+ winGetApp: '/api/AddwinGetApp',
+ chocolateyApp: '/api/AddChocoApp',
+ officeApp: '/api/AddOfficeApp',
+ win32ScriptApp: '/api/AddWin32ScriptApp',
+ }
const ChocosearchResults = ApiPostCall({
urlFromData: true,
- });
+ })
const winGetSearchResults = ApiPostCall({
urlFromData: true,
- });
+ })
const deployApplication = ApiPostCall({
urlFromData: true,
- relatedQueryKeys: ["Queued Applications"],
- });
+ relatedQueryKeys: ['Queued Applications'],
+ })
const searchApp = (searchText, type) => {
- if (type === "choco") {
+ if (type === 'choco') {
ChocosearchResults.mutate({
url: `/api/ListAppsRepository`,
data: { search: searchText },
queryKey: `SearchApp-${searchText}-${type}`,
- });
+ })
}
- if (type === "StoreApp") {
+ if (type === 'StoreApp') {
winGetSearchResults.mutate({
url: `/api/ListPotentialApps`,
- data: { searchString: searchText, type: "WinGet" },
+ data: { searchString: searchText, type: 'WinGet' },
queryKey: `SearchApp-${searchText}-${type}`,
- });
+ })
}
- };
+ }
const handleSubmit = () => {
- const formData = formControl.getValues();
- const formattedData = { ...formData };
- formattedData.tenantFilter = "allTenants"; //added to prevent issues with location check. temp fix
+ const formData = formControl.getValues()
+ const formattedData = { ...formData }
+ formattedData.tenantFilter = 'allTenants' //added to prevent issues with location check. temp fix
formattedData.selectedTenants = selectedTenants.map((tenant) => ({
defaultDomainName: tenant.value,
customerId: tenant.addedFields.customerId,
- }));
+ }))
deployApplication.mutate({
url: postUrl[applicationType?.value],
data: formattedData,
- relatedQueryKeys: ["Queued Applications"],
- });
- };
+ relatedQueryKeys: ['Queued Applications'],
+ })
+ }
const handleCloseDrawer = () => {
- setDrawerVisible(false);
- formControl.reset();
- };
+ setDrawerVisible(false)
+ formControl.reset()
+ }
return (
<>
@@ -130,7 +130,7 @@ export const CippApplicationDeployDrawer = ({
onClose={handleCloseDrawer}
size="xl"
footer={
-
+
+ }
+ api={{
+ url: "/api/ListCIPPUsers",
+ dataKey: "Users",
+ }}
+ queryKey="cippUsersList"
+ simpleColumns={["UPN", "Roles"]}
+ offCanvas={offCanvas}
+ />
+
+
+
+
+
+ );
+};
+
+export default CippUserManagement;
diff --git a/src/components/CippStandards/CippStandardAccordion.jsx b/src/components/CippStandards/CippStandardAccordion.jsx
index 1a2811d462c3..b42c73d6c5ba 100644
--- a/src/components/CippStandards/CippStandardAccordion.jsx
+++ b/src/components/CippStandards/CippStandardAccordion.jsx
@@ -2,6 +2,7 @@ import React, { useEffect, useState, useMemo } from "react";
import {
Card,
Stack,
+ Alert,
Avatar,
Box,
Typography,
@@ -53,8 +54,9 @@ const getAvailableActions = (disabledFeatures) => {
return allActions.filter((action) => !disabledFeatures?.[action.value.toLowerCase()]);
};
-const CippAddedComponent = React.memo(({ standardName, component, formControl }) => {
+const CippAddedComponent = React.memo(({ standardName, component, formControl, currentValue }) => {
const updatedComponent = { ...component };
+ const fieldName = `${standardName}.${updatedComponent.name}`;
if (component.type === "AdminRolesMultiSelect") {
updatedComponent.type = "autoComplete";
@@ -73,15 +75,30 @@ const CippAddedComponent = React.memo(({ standardName, component, formControl })
updatedComponent.type = component.type;
}
+ const warningThreshold = Number(updatedComponent.warningThreshold);
+ const numericValue = Number(currentValue);
+ const showThresholdWarning =
+ Number.isFinite(warningThreshold) &&
+ !Number.isNaN(numericValue) &&
+ `${currentValue}`.trim() !== "" &&
+ numericValue > warningThreshold;
+
+ const warningMessage =
+ updatedComponent.warningMessage ||
+ `Values above ${warningThreshold} can match unrelated policies. Use with caution.`;
+
return (
-
+
+
+ {showThresholdWarning && {warningMessage}}
+
);
});
@@ -432,7 +449,10 @@ const CippStandardAccordion = ({
(standard.cat && standard.cat.toLowerCase().includes(searchLower)) ||
(standard.tag &&
Array.isArray(standard.tag) &&
- standard.tag.some((tag) => tag.toLowerCase().includes(searchLower)));
+ standard.tag.some((tag) => tag.toLowerCase().includes(searchLower))) ||
+ (standard.appliesToTest &&
+ Array.isArray(standard.appliesToTest) &&
+ standard.appliesToTest.some((testId) => testId.toLowerCase().includes(searchLower)));
const isConfigured = _.get(configuredState, standardName);
const matchesFilter =
@@ -937,6 +957,10 @@ const CippStandardAccordion = ({
standardName={standardName}
component={component}
formControl={formControl}
+ currentValue={_.get(
+ watchedValues,
+ `${standardName}.${component.name}`,
+ )}
/>
) : (
@@ -945,6 +969,10 @@ const CippStandardAccordion = ({
standardName={standardName}
component={component}
formControl={formControl}
+ currentValue={_.get(
+ watchedValues,
+ `${standardName}.${component.name}`,
+ )}
/>
),
)}
@@ -995,6 +1023,10 @@ const CippStandardAccordion = ({
standardName={standardName}
component={component}
formControl={formControl}
+ currentValue={_.get(
+ watchedValues,
+ `${standardName}.${component.name}`,
+ )}
/>
) : (
@@ -1003,6 +1035,10 @@ const CippStandardAccordion = ({
standardName={standardName}
component={component}
formControl={formControl}
+ currentValue={_.get(
+ watchedValues,
+ `${standardName}.${component.name}`,
+ )}
/>
),
)}
diff --git a/src/components/CippStandards/CippStandardDialog.jsx b/src/components/CippStandards/CippStandardDialog.jsx
index 6ebe6362930c..4761ffcab94f 100644
--- a/src/components/CippStandards/CippStandardDialog.jsx
+++ b/src/components/CippStandards/CippStandardDialog.jsx
@@ -788,7 +788,11 @@ const CippStandardDialog = ({
standard.label.toLowerCase().includes(localSearchQuery.toLowerCase()) ||
standard.helpText.toLowerCase().includes(localSearchQuery.toLowerCase()) ||
(standard.tag &&
- standard.tag.some((tag) => tag.toLowerCase().includes(localSearchQuery.toLowerCase())));
+ standard.tag.some((tag) => tag.toLowerCase().includes(localSearchQuery.toLowerCase()))) ||
+ (standard.appliesToTest &&
+ standard.appliesToTest.some((testId) =>
+ testId.toLowerCase().includes(localSearchQuery.toLowerCase())
+ ));
// Category filter
const matchesCategory =
diff --git a/src/components/CippTable/CIPPTableToptoolbar.js b/src/components/CippTable/CIPPTableToptoolbar.js
index 58d8a180813b..232c752bb2c9 100644
--- a/src/components/CippTable/CIPPTableToptoolbar.js
+++ b/src/components/CippTable/CIPPTableToptoolbar.js
@@ -1239,7 +1239,7 @@ export const CIPPTableToptoolbar = React.memo(
)}
{/* Cold start indicator */}
- {getRequestData?.data?.pages?.[0].Metadata?.ColdStart === true && (
+ {getRequestData?.data?.pages?.[0]?.Metadata?.ColdStart === true && (
diff --git a/src/components/CippTable/CippDataTable.js b/src/components/CippTable/CippDataTable.js
index 0fc4f87432b7..253327713912 100644
--- a/src/components/CippTable/CippDataTable.js
+++ b/src/components/CippTable/CippDataTable.js
@@ -579,6 +579,9 @@ export const CippDataTable = (props) => {
}, [columns.length, usedData, queryKey, settings?.currentTenant, filterTypeMap])
const createDialog = useDialog()
+ const hasActions = !!actions
+ const hasOffCanvas = !!offCanvas
+ const hasOnChange = !!onChange
// Compute modeInfo via useMemo so it stays stable but updates when relevant inputs change.
const modeInfo = useMemo(
@@ -593,7 +596,7 @@ export const CippDataTable = (props) => {
maxHeightOffset,
settings
),
- [simple, !!actions, !!offCanvas, !!onChange, maxHeightOffset, settings?.tablePageSize?.value]
+ [simple, hasActions, hasOffCanvas, hasOnChange, maxHeightOffset, settings?.tablePageSize?.value]
)
// Include updateTrigger in data memo to force re-render when license backfill completes
@@ -651,7 +654,15 @@ export const CippDataTable = (props) => {
const muiTableBodyRowProps = useMemo(() => {
if (offCanvasOnRowClick && offCanvas) {
return ({ row }) => ({
- onClick: () => {
+ onClick: (event) => {
+ if (
+ event.target?.closest?.(
+ 'button, a, input, textarea, select, [role="button"], [role="menuitem"], [data-no-row-click="true"]'
+ )
+ ) {
+ return
+ }
+
setOffCanvasData(row.original)
const filteredRowsArray = table?.getFilteredRowModel?.()?.rows
if (filteredRowsArray) {
diff --git a/src/components/CippTable/CippDataTableButton.jsx b/src/components/CippTable/CippDataTableButton.jsx
index 79eec0f04bc5..86c3c887e1e9 100644
--- a/src/components/CippTable/CippDataTableButton.jsx
+++ b/src/components/CippTable/CippDataTableButton.jsx
@@ -5,7 +5,9 @@ import { getCippTranslation } from "../../utils/get-cipp-translation";
const CippDataTableButton = ({ data, title, tableTitle = "Data" }) => {
const [openDialogs, setOpenDialogs] = useState([]);
- const handleOpenDialog = () => {
+ const handleOpenDialog = (event) => {
+ event?.stopPropagation();
+
let dataArray;
if (Array.isArray(data)) {
@@ -21,7 +23,8 @@ const CippDataTableButton = ({ data, title, tableTitle = "Data" }) => {
setOpenDialogs([...openDialogs, dataArray]);
};
- const handleCloseDialog = (index) => {
+ const handleCloseDialog = (index, event) => {
+ event?.stopPropagation?.();
setOpenDialogs(openDialogs.filter((_, i) => i !== index));
};
const dataIsNotANullArray =
@@ -48,7 +51,9 @@ const CippDataTableButton = ({ data, title, tableTitle = "Data" }) => {
+
+
{!setupCompleted && (
@@ -338,7 +349,7 @@ export const Layout = (props) => {
)}
- {(currentTenant === "AllTenants" || !currentTenant) && !allTenantsSupport ? (
+ {(currentTenant === 'AllTenants' || !currentTenant) && !allTenantsSupport ? (
@@ -348,7 +359,7 @@ export const Layout = (props) => {
title="Not supported"
imageUrl="/assets/illustrations/undraw_website_ij0l.svg"
text={
- "The page does not support all Tenants, please select a different tenant using the tenant selector."
+ 'The page does not support all Tenants, please select a different tenant using the tenant selector.'
}
/>
@@ -368,5 +379,5 @@ export const Layout = (props) => {
>
- );
-};
+ )
+}
diff --git a/src/pages/_app.js b/src/pages/_app.js
index 96a49eb32d99..aa387f0417fa 100644
--- a/src/pages/_app.js
+++ b/src/pages/_app.js
@@ -53,7 +53,6 @@ import {
ClearAll as ClearAllIcon,
} from '@mui/icons-material'
import { SvgIcon } from '@mui/material'
-import discordIcon from '../../public/discord-mark-blue.svg'
import React, { useEffect, useState, useRef } from 'react'
import { usePathname } from 'next/navigation'
import { useRouter } from 'next/router'
@@ -69,6 +68,7 @@ TimeAgo.addDefaultLocale(en)
const queryClient = new QueryClient()
const clientSideEmotionCache = createEmotionCache()
+
const App = (props) => {
const { Component, emotionCache = clientSideEmotionCache, pageProps } = props
const getLayout = Component.getLayout ?? ((page) => page)
@@ -80,6 +80,11 @@ const App = (props) => {
useEffect(() => {
if (typeof window === 'undefined') return
+ // Register minimal service worker for Chrome installability
+ if ('serviceWorker' in navigator) {
+ navigator.serviceWorker.register('/sw.js').catch(() => {})
+ }
+
const language = navigator.language || navigator.userLanguage || 'en-US'
const baseLang = language.split('-')[0]
@@ -226,13 +231,7 @@ const App = (props) => {
},
{
id: 'discord',
- icon: (
-
- ),
+ icon:
,
name: 'Join the Discord!',
href: 'https://discord.gg/cyberdrain',
onClick: () => window.open('https://discord.gg/cyberdrain', '_blank'),
diff --git a/src/pages/_document.js b/src/pages/_document.js
index c764cde02995..4cceb2676ef2 100644
--- a/src/pages/_document.js
+++ b/src/pages/_document.js
@@ -8,6 +8,12 @@ class CustomDocument extends Document {
return (
+
+
+
+
+
+
{
+ return (
+
+
+
+
+ Manage users who can access CIPP. Add users by their email address (UPN) and assign
+ them built-in or custom roles. Users not in this list will still be able to log in if
+ "Allow All Tenant Users" is enabled, but they will only receive default
+ (authenticated) permissions. Role resolution also considers Entra group mappings
+ configured on the CIPP Roles page.
+
+
+
+
+
+ );
+};
+
+Page.getLayout = (page) => (
+
+ {page}
+
+);
+
+export default Page;
diff --git a/src/pages/cipp/advanced/super-admin/container.js b/src/pages/cipp/advanced/super-admin/container.js
new file mode 100644
index 000000000000..d56595ac546c
--- /dev/null
+++ b/src/pages/cipp/advanced/super-admin/container.js
@@ -0,0 +1,26 @@
+import { Container } from "@mui/material";
+import { Grid } from "@mui/system";
+import { TabbedLayout } from "../../../../layouts/TabbedLayout";
+import { Layout as DashboardLayout } from "../../../../layouts/index.js";
+import tabOptions from "./tabOptions";
+import { CippContainerManagement } from "../../../../components/CippSettings/CippContainerManagement";
+
+const Page = () => {
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+Page.getLayout = (page) => (
+
+ {page}
+
+);
+
+export default Page;
diff --git a/src/pages/cipp/advanced/super-admin/sso.js b/src/pages/cipp/advanced/super-admin/sso.js
new file mode 100644
index 000000000000..fc5b112f3f1c
--- /dev/null
+++ b/src/pages/cipp/advanced/super-admin/sso.js
@@ -0,0 +1,26 @@
+import { Container } from "@mui/material";
+import { Grid } from "@mui/system";
+import { TabbedLayout } from "../../../../layouts/TabbedLayout";
+import { Layout as DashboardLayout } from "../../../../layouts/index.js";
+import tabOptions from "./tabOptions";
+import { CippSSOSettings } from "../../../../components/CippSettings/CippSSOSettings";
+
+const Page = () => {
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+Page.getLayout = (page) => (
+
+ {page}
+
+);
+
+export default Page;
diff --git a/src/pages/cipp/advanced/super-admin/tabOptions.json b/src/pages/cipp/advanced/super-admin/tabOptions.json
index 672df76996c6..fbccb6b73c55 100644
--- a/src/pages/cipp/advanced/super-admin/tabOptions.json
+++ b/src/pages/cipp/advanced/super-admin/tabOptions.json
@@ -22,5 +22,17 @@
{
"label": "SAM App Permissions",
"path": "/cipp/advanced/super-admin/sam-app-permissions"
+ },
+ {
+ "label": "CIPP Users",
+ "path": "/cipp/advanced/super-admin/cipp-users"
+ },
+ {
+ "label": "SSO",
+ "path": "/cipp/advanced/super-admin/sso"
+ },
+ {
+ "label": "Container Management",
+ "path": "/cipp/advanced/super-admin/container"
}
]
diff --git a/src/pages/email/administration/hve-accounts/index.js b/src/pages/email/administration/hve-accounts/index.js
new file mode 100644
index 000000000000..265884d44650
--- /dev/null
+++ b/src/pages/email/administration/hve-accounts/index.js
@@ -0,0 +1,204 @@
+import { Layout as DashboardLayout } from '../../../../layouts/index.js'
+import { CippTablePage } from '../../../../components/CippComponents/CippTablePage.jsx'
+import { CippHVEUserDrawer } from '../../../../components/CippComponents/CippHVEUserDrawer.jsx'
+import { useCippReportDB } from '../../../../components/CippComponents/CippReportDBControls'
+import { Stack } from '@mui/system'
+import { TrashIcon } from '@heroicons/react/24/outline'
+import {
+ Edit,
+ AlternateEmail,
+ Receipt,
+ RemoveCircleOutline,
+ Reply,
+} from '@mui/icons-material'
+
+const Page = () => {
+ const pageTitle = 'HVE Accounts'
+
+ const reportDB = useCippReportDB({
+ apiUrl: '/api/ListHVEAccounts',
+ queryKey: 'ListHVEAccounts',
+ cacheName: 'HVEAccounts',
+ syncTitle: 'Sync HVE Accounts',
+ allowToggle: true,
+ defaultCached: true,
+ })
+
+ const actions = [
+ {
+ label: 'Edit Display Name',
+ type: 'POST',
+ url: '/api/ExecHVEUser',
+ icon: ,
+ data: { Identity: 'primarySmtpAddress', Action: 'Edit' },
+ fields: [
+ {
+ type: 'textField',
+ name: 'DisplayName',
+ label: 'Display Name',
+ },
+ ],
+ confirmText: 'Update display name for [primarySmtpAddress]',
+ hideBulk: true,
+ },
+ {
+ label: 'Set Reply-To Address',
+ type: 'POST',
+ url: '/api/ExecHVEUser',
+ icon: ,
+ data: { Identity: 'primarySmtpAddress', Action: 'Edit' },
+ fields: [
+ {
+ type: 'textField',
+ name: 'ReplyTo',
+ label: 'Reply-To Address',
+ placeholder: 'e.g. replies@contoso.com (leave empty to clear)',
+ },
+ ],
+ confirmText: 'Update reply-to address for [primarySmtpAddress]',
+ hideBulk: true,
+ },
+ {
+ label: 'Change Primary SMTP Address',
+ type: 'POST',
+ url: '/api/ExecHVEUser',
+ icon: ,
+ data: { Identity: 'primarySmtpAddress', Action: 'Edit' },
+ fields: [
+ {
+ type: 'textField',
+ name: 'username',
+ label: 'Username (local part)',
+ placeholder: 'e.g. hveaccount01',
+ },
+ {
+ type: 'autoComplete',
+ name: 'domain',
+ label: 'Domain',
+ api: {
+ url: '/api/ListGraphRequest',
+ dataKey: 'Results',
+ queryKey: 'listDomains-hve',
+ labelField: (option) => option.id,
+ valueField: 'id',
+ addedField: {
+ isDefault: 'isDefault',
+ isVerified: 'isVerified',
+ },
+ data: {
+ Endpoint: 'domains',
+ manualPagination: true,
+ $count: true,
+ $top: 99,
+ },
+ dataFilter: (domains) =>
+ domains
+ .filter((d) => d?.addedFields?.isVerified === true)
+ .sort((a, b) => {
+ if (a.addedFields?.isDefault === true) return -1
+ if (b.addedFields?.isDefault === true) return 1
+ return 0
+ }),
+ },
+ },
+ ],
+ confirmText: 'Change primary SMTP address for [primarySmtpAddress]',
+ hideBulk: true,
+ },
+ {
+ label: 'Assign Billing Policy',
+ type: 'POST',
+ url: '/api/ExecHVEUser',
+ icon: ,
+ data: { Identity: 'primarySmtpAddress', Action: 'AssignBillingPolicy' },
+ fields: [
+ {
+ type: 'autoComplete',
+ name: 'BillingPolicyId',
+ label: 'Billing Policy',
+ multiple: false,
+ api: {
+ url: '/api/ListHVEAccounts',
+ queryKey: 'ListHVEBillingPolicies',
+ labelField: (option) =>
+ `${option.Name || option.BillingPolicyName || option.BillingPolicyId} (${option.BillingPolicyId || option.Guid || option.Identity})`,
+ valueField: (option) => option.BillingPolicyId || option.Guid || option.Identity,
+ data: {
+ ListBillingPolicies: true,
+ },
+ },
+ },
+ ],
+ confirmText: 'Assign billing policy to [primarySmtpAddress]. Current policy: [BillingPolicyName]',
+ hideBulk: true,
+ },
+ {
+ label: 'Remove Billing Policy',
+ type: 'POST',
+ url: '/api/ExecHVEUser',
+ icon: ,
+ data: { Identity: 'primarySmtpAddress', Action: 'RemoveBillingPolicy' },
+ confirmText:
+ 'Remove billing policy [BillingPolicyName] from [primarySmtpAddress]?',
+ condition: (row) => row.BillingPolicyName && row.BillingPolicyName !== 'None',
+ hideBulk: true,
+ },
+ {
+ label: 'Delete HVE Account',
+ type: 'POST',
+ icon: ,
+ url: '/api/ExecHVEUser',
+ data: { Identity: 'primarySmtpAddress', Action: 'Remove' },
+ confirmText: 'Are you sure you want to delete HVE account [primarySmtpAddress]?',
+ multiPost: false,
+ },
+ ]
+
+ const offCanvas = {
+ extendedInfoFields: [
+ 'displayName',
+ 'primarySmtpAddress',
+ 'Alias',
+ 'AdditionalEmailAddresses',
+ 'BillingPolicyName',
+ 'BillingPolicyId',
+ 'WhenCreated',
+ 'ExternalDirectoryObjectId',
+ ],
+ actions: actions,
+ }
+
+ const simpleColumns = [
+ ...reportDB.cacheColumns.filter((c) => c === 'Tenant'),
+ 'displayName',
+ 'primarySmtpAddress',
+ 'Alias',
+ 'WhenCreated',
+ 'AdditionalEmailAddresses',
+ ...reportDB.cacheColumns.filter((c) => c !== 'Tenant'),
+ ]
+
+ return (
+ <>
+
+
+ {reportDB.controls}
+
+ }
+ />
+ {reportDB.syncDialog}
+ >
+ )
+}
+
+Page.getLayout = (page) => {page}
+
+export default Page
diff --git a/src/pages/email/administration/mailboxes/index.js b/src/pages/email/administration/mailboxes/index.js
index f0187d414983..01bc84afae65 100644
--- a/src/pages/email/administration/mailboxes/index.js
+++ b/src/pages/email/administration/mailboxes/index.js
@@ -1,7 +1,6 @@
import { Layout as DashboardLayout } from '../../../../layouts/index.js'
import { CippTablePage } from '../../../../components/CippComponents/CippTablePage.jsx'
import CippExchangeActions from '../../../../components/CippComponents/CippExchangeActions'
-import { CippHVEUserDrawer } from '../../../../components/CippComponents/CippHVEUserDrawer.jsx'
import { CippSharedMailboxDrawer } from '../../../../components/CippComponents/CippSharedMailboxDrawer.jsx'
import { useCippReportDB } from '../../../../components/CippComponents/CippReportDBControls'
import { Stack } from '@mui/system'
@@ -71,7 +70,6 @@ const Page = () => {
cardButton={
-
{reportDB.controls}
}
diff --git a/src/pages/email/administration/quarantine/index.js b/src/pages/email/administration/quarantine/index.js
index 0aa088af9a58..7443eb5e714f 100644
--- a/src/pages/email/administration/quarantine/index.js
+++ b/src/pages/email/administration/quarantine/index.js
@@ -1,6 +1,6 @@
-import { Layout as DashboardLayout } from "../../../../layouts/index.js";
-import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx";
-import { useEffect, useState } from "react";
+import { Layout as DashboardLayout } from '../../../../layouts/index.js'
+import { CippTablePage } from '../../../../components/CippComponents/CippTablePage.jsx'
+import { useEffect, useState } from 'react'
import {
Dialog,
DialogTitle,
@@ -9,168 +9,172 @@ import {
Skeleton,
Typography,
CircularProgress,
-} from "@mui/material";
-import { Block, Close, Done, DoneAll } from "@mui/icons-material";
-import { CippMessageViewer } from "../../../../components/CippComponents/CippMessageViewer.jsx";
-import { ApiGetCall, ApiPostCall } from "../../../../api/ApiCall";
-import { useSettings } from "../../../../hooks/use-settings";
-import { EyeIcon, DocumentTextIcon } from "@heroicons/react/24/outline";
-import { CippDataTable } from "../../../../components/CippTable/CippDataTable";
+} from '@mui/material'
+import { Block, Close, Done, DoneAll } from '@mui/icons-material'
+import { CippMessageViewer } from '../../../../components/CippComponents/CippMessageViewer.jsx'
+import { ApiGetCall, ApiPostCall } from '../../../../api/ApiCall'
+import { useSettings } from '../../../../hooks/use-settings'
+import { EyeIcon, DocumentTextIcon } from '@heroicons/react/24/outline'
+import { CippDataTable } from '../../../../components/CippTable/CippDataTable'
const simpleColumns = [
- "ReceivedTime",
- "ReleaseStatus",
- "Subject",
- "SenderAddress",
- "RecipientAddress",
- "Type",
- "PolicyName",
- "Tenant",
-];
-const detailColumns = ["Received", "Status", "SenderAddress", "RecipientAddress"];
-const pageTitle = "Quarantine Management";
+ 'ReceivedTime',
+ 'ReleaseStatus',
+ 'Subject',
+ 'SenderAddress',
+ 'RecipientAddress',
+ 'Type',
+ 'PolicyName',
+ 'Tenant',
+]
+const detailColumns = ['Received', 'Status', 'SenderAddress', 'RecipientAddress']
+const pageTitle = 'Quarantine Management'
const Page = () => {
- const tenantFilter = useSettings().currentTenant;
- const [dialogOpen, setDialogOpen] = useState(false);
- const [dialogContent, setDialogContent] = useState(null);
- const [messageId, setMessageId] = useState(null);
- const [traceDialogOpen, setTraceDialogOpen] = useState(false);
- const [traceDetails, setTraceDetails] = useState([]);
- const [traceMessageId, setTraceMessageId] = useState(null);
- const [messageSubject, setMessageSubject] = useState(null);
- const [messageContentsWaiting, setMessageContentsWaiting] = useState(false);
+ const tenantFilter = useSettings().currentTenant
+ const [dialogOpen, setDialogOpen] = useState(false)
+ const [dialogContent, setDialogContent] = useState(null)
+ const [messageId, setMessageId] = useState(null)
+ const [traceDialogOpen, setTraceDialogOpen] = useState(false)
+ const [traceDetails, setTraceDetails] = useState([])
+ const [traceMessageId, setTraceMessageId] = useState(null)
+ const [messageSubject, setMessageSubject] = useState(null)
+ const [messageContentsWaiting, setMessageContentsWaiting] = useState(false)
const getMessageContents = ApiGetCall({
- url: "/api/ListMailQuarantineMessage",
+ url: '/api/ListMailQuarantineMessage',
data: {
tenantFilter: tenantFilter,
Identity: messageId,
},
waiting: messageContentsWaiting,
queryKey: `ListMailQuarantineMessage-${messageId}`,
- });
+ })
const getMessageTraceDetails = ApiPostCall({
urlFromData: true,
queryKey: `MessageTraceDetail-${traceMessageId}`,
onResult: (result) => {
- setTraceDetails(result);
+ setTraceDetails(result)
},
- });
+ })
const viewMessage = (row) => {
- const id = row.Identity;
- setMessageId(id);
+ const id = row.Identity
+ setMessageId(id)
if (!messageContentsWaiting) {
- setMessageContentsWaiting(true);
+ setMessageContentsWaiting(true)
}
- getMessageContents.refetch();
- setDialogOpen(true);
- };
+ getMessageContents.refetch()
+ setDialogOpen(true)
+ }
const viewMessageTrace = (row) => {
- setTraceMessageId(row.MessageId);
+ setTraceMessageId(row.MessageId)
getMessageTraceDetails.mutate({
- url: "/api/ListMessageTrace",
+ url: '/api/ListMessageTrace',
data: {
tenantFilter: tenantFilter,
messageId: row.MessageId,
},
- });
- setMessageSubject(row.Subject);
- setTraceDialogOpen(true);
- };
+ })
+ setMessageSubject(row.Subject)
+ setTraceDialogOpen(true)
+ }
useEffect(() => {
if (getMessageContents.isSuccess) {
- setDialogContent();
+ setDialogContent()
} else {
- setDialogContent();
+ setDialogContent()
}
- }, [getMessageContents.isSuccess, getMessageContents.data]);
+ }, [getMessageContents.isSuccess, getMessageContents.data])
const actions = [
{
- label: "View Message",
+ label: 'View Message',
noConfirm: true,
customFunction: viewMessage,
icon: ,
+ hideBulk: true,
},
{
- label: "View Message Trace",
+ label: 'View Message Trace',
noConfirm: true,
customFunction: viewMessageTrace,
icon: ,
+ hideBulk: true,
},
{
- label: "Release",
- type: "POST",
- url: "/api/ExecQuarantineManagement",
+ label: 'Release',
+ type: 'POST',
+ url: '/api/ExecQuarantineManagement',
multiPost: true,
data: {
- Identity: "Identity",
- Type: "!Release",
+ Identity: 'Identity',
+ Type: '!Release',
},
- confirmText: "Are you sure you want to release this message?",
+ confirmText: 'Are you sure you want to release this message?',
icon: ,
- condition: (row) => row.ReleaseStatus !== "RELEASED",
+ condition: (row) => row.ReleaseStatus !== 'RELEASED',
},
{
- label: "Deny",
- type: "POST",
- url: "/api/ExecQuarantineManagement",
+ label: 'Deny',
+ type: 'POST',
+ url: '/api/ExecQuarantineManagement',
+ multiPost: true,
data: {
- Identity: "Identity",
- Type: "!Deny",
+ Identity: 'Identity',
+ Type: '!Deny',
},
- confirmText: "Are you sure you want to deny this message?",
+ confirmText: 'Are you sure you want to deny this message?',
icon: ,
- condition: (row) => row.ReleaseStatus === "REQUESTED",
+ condition: (row) => row.ReleaseStatus === 'REQUESTED',
},
{
- label: "Release & Allow Sender",
- type: "POST",
- url: "/api/ExecQuarantineManagement",
+ label: 'Release & Allow Sender',
+ type: 'POST',
+ url: '/api/ExecQuarantineManagement',
+ multiPost: true,
data: {
- Identity: "Identity",
- Type: "!Release",
+ Identity: 'Identity',
+ Type: '!Release',
AllowSender: true,
- SenderAddress: "SenderAddress",
- PolicyName: "PolicyName",
+ SenderAddress: 'SenderAddress',
+ PolicyName: 'PolicyName',
},
confirmText:
- "Are you sure you want to release this email and add the sender to the whitelist?",
+ 'Are you sure you want to release this email and add the sender to the whitelist?',
icon: ,
- condition: (row) => row.ReleaseStatus !== "RELEASED",
+ condition: (row) => row.ReleaseStatus !== 'RELEASED',
},
- ];
+ ]
const offCanvas = {
- extendedInfoFields: ["MessageId", "RecipientAddress", "Type"],
+ extendedInfoFields: ['MessageId', 'RecipientAddress', 'Type'],
actions: actions,
- };
+ }
const filterList = [
{
- filterName: "Not Released",
- value: [{ id: "ReleaseStatus", value: "NOTRELEASED" }],
- type: "column",
- filterType: "equal",
+ filterName: 'Not Released',
+ value: [{ id: 'ReleaseStatus', value: 'NOTRELEASED' }],
+ type: 'column',
+ filterType: 'equal',
},
{
- filterName: "Released",
- value: [{ id: "ReleaseStatus", value: "RELEASED" }],
- type: "column",
- filterType: "equal",
+ filterName: 'Released',
+ value: [{ id: 'ReleaseStatus', value: 'RELEASED' }],
+ type: 'column',
+ filterType: 'equal',
},
{
- filterName: "Requested",
- value: [{ id: "ReleaseStatus", value: "REQUESTED" }],
- type: "column",
- filterType: "equal",
+ filterName: 'Requested',
+ value: [{ id: 'ReleaseStatus', value: 'REQUESTED' }],
+ type: 'column',
+ filterType: 'equal',
},
- ];
+ ]
return (
<>
@@ -189,7 +193,7 @@ const Page = () => {
setDialogOpen(false)}
- sx={{ position: "absolute", right: 8, top: 8 }}
+ sx={{ position: 'absolute', right: 8, top: 8 }}
>
@@ -207,7 +211,7 @@ const Page = () => {
setTraceDialogOpen(false)}
- sx={{ position: "absolute", right: 8, top: 8 }}
+ sx={{ position: 'absolute', right: 8, top: 8 }}
>
@@ -227,7 +231,7 @@ const Page = () => {
data={traceDetails ?? []}
refreshFunction={() =>
getMessageTraceDetails.mutate({
- url: "/api/ListMessageTrace",
+ url: '/api/ListMessageTrace',
data: {
tenantFilter: tenantFilter,
messageId: traceMessageId,
@@ -240,9 +244,9 @@ const Page = () => {
>
- );
-};
+ )
+}
-Page.getLayout = (page) => {page};
+Page.getLayout = (page) => {page}
-export default Page;
+export default Page
diff --git a/src/pages/email/administration/tenant-allow-block-list-templates/index.js b/src/pages/email/administration/tenant-allow-block-list-templates/index.js
index 85de23ce2ec4..4e945b486c4d 100644
--- a/src/pages/email/administration/tenant-allow-block-list-templates/index.js
+++ b/src/pages/email/administration/tenant-allow-block-list-templates/index.js
@@ -1,13 +1,26 @@
+import { useState } from 'react'
import { Layout as DashboardLayout } from '../../../../layouts/index.js'
import { CippTablePage } from '../../../../components/CippComponents/CippTablePage.jsx'
-import { Delete } from '@mui/icons-material'
+import { Delete, Edit } from '@mui/icons-material'
import CippJsonView from '../../../../components/CippFormPages/CippJSONView'
import { CippTenantAllowBlockListTemplateDrawer } from '../../../../components/CippComponents/CippTenantAllowBlockListTemplateDrawer.jsx'
const Page = () => {
const pageTitle = 'Tenant Allow/Block List Templates'
+ const [editDrawerVisible, setEditDrawerVisible] = useState(false)
+ const [editData, setEditData] = useState(null)
const actions = [
+ {
+ label: 'Edit Template',
+ noConfirm: true,
+ customFunction: (row) => {
+ setEditData(row)
+ setEditDrawerVisible(true)
+ },
+ icon: ,
+ color: 'primary',
+ },
{
label: 'Delete Template',
type: 'POST',
@@ -36,18 +49,26 @@ const Page = () => {
]
return (
-
- }
- />
+ <>
+ }
+ />
+ {
+ setEditDrawerVisible(visible)
+ if (!visible) setEditData(null)
+ }}
+ />
+ >
)
}
diff --git a/src/pages/email/tools/message-trace/index.js b/src/pages/email/tools/message-trace/index.js
index 56ccf9bcd20a..d5876859b182 100644
--- a/src/pages/email/tools/message-trace/index.js
+++ b/src/pages/email/tools/message-trace/index.js
@@ -347,6 +347,5 @@ const Page = () => {
);
};
-Page.getLayout = (page) => {page};
-
+Page.getLayout = (page) => {page};
export default Page;
diff --git a/src/pages/endpoint/MEM/assignment-filters/index.js b/src/pages/endpoint/MEM/assignment-filters/index.js
index 462647494c98..bedf0ef1ada4 100644
--- a/src/pages/endpoint/MEM/assignment-filters/index.js
+++ b/src/pages/endpoint/MEM/assignment-filters/index.js
@@ -5,11 +5,19 @@ import Link from "next/link";
import { TrashIcon } from "@heroicons/react/24/outline";
import { Edit, Add, Book } from "@mui/icons-material";
import { Stack } from "@mui/system";
-import { useSettings } from "../../../../hooks/use-settings";
+import { useCippReportDB } from "../../../../components/CippComponents/CippReportDBControls";
const Page = () => {
const pageTitle = "Assignment Filters";
- const { currentTenant } = useSettings();
+
+ const reportDB = useCippReportDB({
+ apiUrl: "/api/ListAssignmentFilters",
+ queryKey: "assignment-filters",
+ cacheName: "IntuneAssignmentFilters",
+ syncTitle: "Sync Assignment Filters Report",
+ allowToggle: true,
+ defaultCached: false,
+ });
const actions = [
{
@@ -62,28 +70,35 @@ const Page = () => {
actions: actions,
};
+ const simpleColumns = [
+ ...reportDB.cacheColumns,
+ "displayName",
+ "description",
+ "platform",
+ "assignmentFilterManagementType",
+ "rule",
+ ];
+
return (
-
- }>
- Add Assignment Filter
-
-
- }
- apiUrl="/api/ListAssignmentFilters"
- queryKey={`assignment-filters-${currentTenant}`}
- actions={actions}
- offCanvas={offCanvas}
- simpleColumns={[
- "displayName",
- "description",
- "platform",
- "assignmentFilterManagementType",
- "rule",
- ]}
- />
+ <>
+
+ }>
+ Add Assignment Filter
+
+ {reportDB.controls}
+
+ }
+ apiUrl={reportDB.resolvedApiUrl}
+ queryKey={reportDB.resolvedQueryKey}
+ actions={actions}
+ offCanvas={offCanvas}
+ simpleColumns={simpleColumns}
+ />
+ {reportDB.syncDialog}
+ >
);
};
diff --git a/src/pages/endpoint/MEM/compare-policies/index.js b/src/pages/endpoint/MEM/compare-policies/index.js
index da74c739462b..80b660e666a1 100644
--- a/src/pages/endpoint/MEM/compare-policies/index.js
+++ b/src/pages/endpoint/MEM/compare-policies/index.js
@@ -4,7 +4,7 @@ import { ApiPostCall } from "../../../../api/ApiCall";
import { CippFormComponent } from "../../../../components/CippComponents/CippFormComponent";
import { CippFormCondition } from "../../../../components/CippComponents/CippFormCondition";
import { CippFormTenantSelector } from "../../../../components/CippComponents/CippFormTenantSelector";
-import { CippCodeBlock } from "../../../../components/CippComponents/CippCodeBlock";
+import CippJsonView from "../../../../components/CippFormPages/CippJSONView";
import {
Box,
Button,
@@ -19,16 +19,12 @@ import {
TableHead,
TableRow,
Paper,
- Accordion,
- AccordionSummary,
- AccordionDetails,
Alert,
Stack,
Chip,
Skeleton,
} from "@mui/material";
import {
- ExpandMore as ExpandMoreIcon,
CompareArrows as CompareArrowsIcon,
CheckCircle as CheckCircleIcon,
Error as ErrorIcon,
@@ -359,14 +355,10 @@ const Page = () => {
return errData?.Results || compareApi.error?.message || "An error occurred";
}, [compareApi.isError, compareApi.error]);
- const sourceAJson = useMemo(
- () => (results?.sourceAData ? JSON.stringify(results.sourceAData, null, 2) : ""),
- [results?.sourceAData],
- );
- const sourceBJson = useMemo(
- () => (results?.sourceBData ? JSON.stringify(results.sourceBData, null, 2) : ""),
- [results?.sourceBData],
- );
+ const comparisonRows = useMemo(() => {
+ if (!Array.isArray(results?.Results)) return [];
+ return results.Results.filter(Boolean);
+ }, [results?.Results]);
return (
@@ -418,14 +410,14 @@ const Page = () => {
>
{results.identical
? "Policies are identical - no differences found."
- : `${results.Results?.length || 0} difference${results.Results?.length === 1 ? "" : "s"} found between policies.`}
+ : `${comparisonRows.length} difference${comparisonRows.length === 1 ? "" : "s"} found between policies.`}
A: {results.sourceALabel} — B:{" "}
{results.sourceBLabel}
- {!results.identical && results.Results?.length > 0 && (
+ {!results.identical && comparisonRows.length > 0 && (
@@ -437,7 +429,7 @@ const Page = () => {
- {results.Results.map((row, index) => (
+ {comparisonRows.map((row, index) => (
({
@@ -457,23 +449,17 @@ const Page = () => {
)}
-
- }>
- Source A Raw JSON — {results.sourceALabel}
-
-
-
-
-
-
-
- }>
- Source B Raw JSON — {results.sourceBLabel}
-
-
-
-
-
+
+
+
)}
diff --git a/src/pages/endpoint/MEM/devices/index.js b/src/pages/endpoint/MEM/devices/index.js
index e8e34e9338d5..087052560120 100644
--- a/src/pages/endpoint/MEM/devices/index.js
+++ b/src/pages/endpoint/MEM/devices/index.js
@@ -4,7 +4,8 @@ import { CippApiDialog } from "../../../../components/CippComponents/CippApiDial
import { useSettings } from "../../../../hooks/use-settings";
import { useDialog } from "../../../../hooks/use-dialog.js";
import { EyeIcon } from "@heroicons/react/24/outline";
-import { Box, Button } from "@mui/material";
+import { Button } from "@mui/material";
+import { Stack } from "@mui/system";
import {
Sync,
RestartAlt,
@@ -412,11 +413,11 @@ const Page = () => {
offCanvas={offCanvas}
simpleColumns={simpleColumns}
cardButton={
-
+
}>
Sync DEP
-
+
}
/>
{
const pageTitle = 'App Protection & Configuration Policies'
const cardButtonPermissions = ['Endpoint.MEM.ReadWrite']
const tenant = useSettings().currentTenant
+ const reportDB = useCippReportDB({
+ apiUrl: '/api/ListAppProtectionPolicies',
+ queryKey: 'ListAppProtectionPolicies',
+ cacheName: 'IntuneAppProtectionPolicies',
+ syncTitle: 'Sync App Protection Policies Report',
+ allowToggle: true,
+ defaultCached: false,
+ })
+
const actions = useCippIntunePolicyActions(tenant, 'URLName', {
templateData: {
ID: 'id',
@@ -31,6 +42,7 @@ const Page = () => {
}
const simpleColumns = [
+ ...reportDB.cacheColumns,
'displayName',
'PolicyTypeName',
'PolicyAssignment',
@@ -39,20 +51,27 @@ const Page = () => {
]
return (
-
- }
- />
+ <>
+
+
+ {reportDB.controls}
+
+ }
+ />
+ {reportDB.syncDialog}
+ >
)
}
diff --git a/src/pages/endpoint/MEM/list-compliance-policies/index.js b/src/pages/endpoint/MEM/list-compliance-policies/index.js
index b3394023c492..32574567a0ff 100644
--- a/src/pages/endpoint/MEM/list-compliance-policies/index.js
+++ b/src/pages/endpoint/MEM/list-compliance-policies/index.js
@@ -4,12 +4,23 @@ import { PermissionButton } from "../../../../utils/permissions.js";
import { CippPolicyDeployDrawer } from "../../../../components/CippComponents/CippPolicyDeployDrawer.jsx";
import { useSettings } from "../../../../hooks/use-settings.js";
import { useCippIntunePolicyActions } from "../../../../components/CippComponents/CippIntunePolicyActions.jsx";
+import { useCippReportDB } from "../../../../components/CippComponents/CippReportDBControls";
+import { Stack } from "@mui/system";
const Page = () => {
const pageTitle = "Intune Compliance Policies";
const cardButtonPermissions = ["Endpoint.MEM.ReadWrite"];
const tenant = useSettings().currentTenant;
+ const reportDB = useCippReportDB({
+ apiUrl: "/api/ListCompliancePolicies",
+ queryKey: "ListCompliancePolicies",
+ cacheName: "IntuneCompliancePolicies",
+ syncTitle: "Sync Compliance Policies Report",
+ allowToggle: true,
+ defaultCached: false,
+ });
+
const actions = useCippIntunePolicyActions(tenant, "deviceCompliancePolicies", {
templateData: {
ID: "id",
@@ -29,6 +40,7 @@ const Page = () => {
};
const simpleColumns = [
+ ...reportDB.cacheColumns,
"displayName",
"PolicyTypeName",
"PolicyAssignment",
@@ -38,20 +50,27 @@ const Page = () => {
];
return (
-
- }
- />
+ <>
+
+
+ {reportDB.controls}
+
+ }
+ />
+ {reportDB.syncDialog}
+ >
);
};
diff --git a/src/pages/endpoint/MEM/list-policies/index.js b/src/pages/endpoint/MEM/list-policies/index.js
index 8cc3cd2cb653..22559f99097c 100644
--- a/src/pages/endpoint/MEM/list-policies/index.js
+++ b/src/pages/endpoint/MEM/list-policies/index.js
@@ -5,6 +5,7 @@ import { CippPolicyDeployDrawer } from '../../../../components/CippComponents/Ci
import { useSettings } from '../../../../hooks/use-settings.js'
import { useCippIntunePolicyActions } from '../../../../components/CippComponents/CippIntunePolicyActions.jsx'
import { useCippReportDB } from '../../../../components/CippComponents/CippReportDBControls'
+import { CippIntunePolicyDetails } from '../../../../components/CippComponents/CippIntunePolicyDetails.jsx'
import { Stack } from '@mui/system'
const Page = () => {
@@ -37,6 +38,8 @@ const Page = () => {
'PolicyTypeName',
],
actions: actions,
+ children: (row) => ,
+ size: 'lg',
}
const simpleColumns = [
@@ -49,6 +52,7 @@ const Page = () => {
'lastModifiedDateTime',
]
+
return (
<>
{
const [codeContentChanged, setCodeContentChanged] = useState(false);
const [warnOpen, setWarnOpen] = useState(false);
const [currentScript, setCurrentScript] = useState(null);
+ const [scriptTenant, setScriptTenant] = useState(null);
+
+ const tenantFilter = useSettings().currentTenant;
+ const reportDB = useCippReportDB({
+ apiUrl: "/api/ListIntuneScript",
+ queryKey: "ListIntuneScript",
+ cacheName: "IntuneScripts",
+ syncTitle: "Sync Intune Scripts Report",
+ allowToggle: true,
+ defaultCached: false,
+ });
const dispatch = useDispatch();
@@ -48,17 +60,16 @@ const Page = () => {
: "powershell";
}, [currentScript?.scriptType]);
- const tenantFilter = useSettings().currentTenant;
const {
isLoading: scriptIsLoading,
isRefetching: scriptIsFetching,
refetch: scriptRefetch,
data,
} = useQuery({
- queryKey: ["script", { scriptId }],
+ queryKey: ["script", { scriptId, scriptTenant }],
queryFn: async () => {
const response = await fetch(
- `/api/EditIntuneScript?TenantFilter=${tenantFilter}&ScriptId=${scriptId}`
+ `/api/EditIntuneScript?TenantFilter=${scriptTenant || tenantFilter}&ScriptId=${scriptId}`
);
return response.json();
},
@@ -79,6 +90,7 @@ const Page = () => {
const handleScriptEdit = async (row, action) => {
setScriptId(row.id);
+ setScriptTenant(row?.Tenant || tenantFilter);
setCodeOpen(!codeOpen);
};
@@ -94,6 +106,7 @@ const Page = () => {
setCodeOpen(!codeOpen);
setCodeContentChanged(false);
setScriptId(null);
+ setScriptTenant(null);
setCodeContent("");
}
};
@@ -114,7 +127,7 @@ const Page = () => {
scriptType,
} = currentScript;
const patchData = {
- TenantFilter: tenantFilter,
+ TenantFilter: scriptTenant || tenantFilter,
ScriptId: id,
ScriptType: scriptType,
IntuneScript: JSON.stringify({
@@ -197,7 +210,7 @@ const Page = () => {
],
confirmText: 'Are you sure you want to assign "[displayName]" to all users?',
customDataformatter: (row, action, formData) => ({
- tenantFilter: tenantFilter,
+ tenantFilter: tenantFilter === "AllTenants" && row?.Tenant ? row.Tenant : tenantFilter,
ID: row?.id,
Type: getScriptEndpoint(row?.scriptType),
AssignTo: "allLicensedUsers",
@@ -223,7 +236,7 @@ const Page = () => {
],
confirmText: 'Are you sure you want to assign "[displayName]" to all devices?',
customDataformatter: (row, action, formData) => ({
- tenantFilter: tenantFilter,
+ tenantFilter: tenantFilter === "AllTenants" && row?.Tenant ? row.Tenant : tenantFilter,
ID: row?.id,
Type: getScriptEndpoint(row?.scriptType),
AssignTo: "AllDevices",
@@ -249,7 +262,7 @@ const Page = () => {
],
confirmText: 'Are you sure you want to assign "[displayName]" to all users and devices?',
customDataformatter: (row, action, formData) => ({
- tenantFilter: tenantFilter,
+ tenantFilter: tenantFilter === "AllTenants" && row?.Tenant ? row.Tenant : tenantFilter,
ID: row?.id,
Type: getScriptEndpoint(row?.scriptType),
AssignTo: "AllDevicesAndUsers",
@@ -305,7 +318,7 @@ const Page = () => {
customDataformatter: (row, action, formData) => {
const selectedGroups = Array.isArray(formData?.groupTargets) ? formData.groupTargets : [];
return {
- tenantFilter: tenantFilter,
+ tenantFilter: tenantFilter === "AllTenants" && row?.Tenant ? row.Tenant : tenantFilter,
ID: row?.id,
Type: getScriptEndpoint(row?.scriptType),
GroupIds: selectedGroups.map((group) => group.value).filter(Boolean),
@@ -354,6 +367,7 @@ const Page = () => {
};
const simpleColumns = [
+ ...reportDB.cacheColumns,
"scriptType",
"displayName",
"ScriptAssignment",
@@ -367,10 +381,12 @@ const Page = () => {
<>
+ {reportDB.syncDialog}
>
);
};
-Page.getLayout = (page) => {page};
+Page.getLayout = (page) => {page};
export default Page;
diff --git a/src/pages/endpoint/MEM/list-templates/index.js b/src/pages/endpoint/MEM/list-templates/index.js
index fcfc8aa81e5a..c1c81670d8b7 100644
--- a/src/pages/endpoint/MEM/list-templates/index.js
+++ b/src/pages/endpoint/MEM/list-templates/index.js
@@ -1,161 +1,251 @@
-import { Layout as DashboardLayout } from "../../../../layouts/index.js";
-import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx";
-import { PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
-import { Edit, GitHub, LocalOffer, LocalOfferOutlined, CopyAll } from "@mui/icons-material";
-import CippJsonView from "../../../../components/CippFormPages/CippJSONView";
-import { ApiGetCall } from "../../../../api/ApiCall";
-import { CippPolicyImportDrawer } from "../../../../components/CippComponents/CippPolicyImportDrawer.jsx";
-import { PermissionButton } from "../../../../utils/permissions.js";
+import { Layout as DashboardLayout } from '../../../../layouts/index.js'
+import { CippTablePage } from '../../../../components/CippComponents/CippTablePage.jsx'
+import { PencilIcon, TrashIcon } from '@heroicons/react/24/outline'
+import { Edit, GitHub, LocalOffer, LocalOfferOutlined, CopyAll } from '@mui/icons-material'
+import CippJsonView from '../../../../components/CippFormPages/CippJSONView'
+import { ApiGetCall } from '../../../../api/ApiCall'
+import { CippPolicyImportDrawer } from '../../../../components/CippComponents/CippPolicyImportDrawer.jsx'
+import { PermissionButton } from '../../../../utils/permissions.js'
+import {
+ Box,
+ Chip,
+ Link,
+ Stack,
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableRow,
+ Tooltip,
+ Typography,
+} from '@mui/material'
+import NextLink from 'next/link'
const Page = () => {
- const pageTitle = "Available Endpoint Manager Templates";
- const cardButtonPermissions = ["Endpoint.MEM.ReadWrite"];
+ const pageTitle = 'Available Endpoint Manager Templates'
+ const cardButtonPermissions = ['Endpoint.MEM.ReadWrite']
const integrations = ApiGetCall({
- url: "/api/ListExtensionsConfig",
- queryKey: "Integrations",
+ url: '/api/ListExtensionsConfig',
+ queryKey: 'Integrations',
refetchOnMount: false,
refetchOnReconnect: false,
- });
+ })
const actions = [
{
- label: "Edit Template",
+ label: 'Edit Template',
link: `/endpoint/MEM/list-templates/edit?id=[GUID]`,
icon: ,
- color: "info",
+ color: 'info',
condition: (row) => row.isSynced === false,
},
{
- label: "Edit Template Name and Description",
- type: "POST",
- url: "/api/ExecEditTemplate",
+ label: 'Edit Template Name and Description',
+ type: 'POST',
+ url: '/api/ExecEditTemplate',
fields: [
{
- type: "textField",
- name: "displayName",
- label: "Display Name",
+ type: 'textField',
+ name: 'displayName',
+ label: 'Display Name',
},
{
- type: "textField",
- name: "description",
- label: "Description",
+ type: 'textField',
+ name: 'description',
+ label: 'Description',
},
],
- data: { GUID: "GUID", Type: "!IntuneTemplate" },
+ data: { GUID: 'GUID', Type: '!IntuneTemplate' },
defaultvalues: (row) => ({
displayName: row.displayName,
description: row.description,
}),
confirmText:
- "Enter the new name and description for the template. Warning: This will disconnect the template from a template library if applied.",
+ 'Enter the new name and description for the template. Warning: This will disconnect the template from a template library if applied.',
multiPost: false,
icon: ,
- color: "info",
+ color: 'info',
},
{
- label: "Clone Template",
- type: "POST",
- url: "/api/ExecCloneTemplate",
- data: { GUID: "GUID", Type: "!IntuneTemplate" },
+ label: 'Clone Template',
+ type: 'POST',
+ url: '/api/ExecCloneTemplate',
+ data: { GUID: 'GUID', Type: '!IntuneTemplate' },
confirmText:
- "Are you sure you want to clone [displayName]? Cloned template are no longer synced with a template library.",
+ 'Are you sure you want to clone [displayName]? Cloned template are no longer synced with a template library.',
multiPost: false,
icon: ,
- color: "info",
+ color: 'info',
},
{
- label: "Add to package",
- type: "POST",
- url: "/api/ExecSetPackageTag",
- data: { GUID: "GUID" },
+ label: 'Add to package',
+ type: 'POST',
+ url: '/api/ExecSetPackageTag',
+ data: { GUID: 'GUID' },
fields: [
{
- type: "textField",
- name: "Package",
- label: "Package Name",
+ type: 'textField',
+ name: 'Package',
+ label: 'Package Name',
required: true,
validators: {
- required: { value: true, message: "Package name is required" },
+ required: { value: true, message: 'Package name is required' },
},
},
],
- confirmText: "Enter the package name to assign to the selected template(s).",
+ confirmText: 'Enter the package name to assign to the selected template(s).',
multiPost: true,
icon: ,
- color: "info",
+ color: 'info',
},
{
- label: "Remove from package",
- type: "POST",
- url: "/api/ExecSetPackageTag",
- data: { GUID: "GUID", Remove: true },
- confirmText: "Are you sure you want to remove the selected template(s) from their package?",
+ label: 'Remove from package',
+ type: 'POST',
+ url: '/api/ExecSetPackageTag',
+ data: { GUID: 'GUID', Remove: true },
+ confirmText: 'Are you sure you want to remove the selected template(s) from their package?',
multiPost: true,
icon: ,
- color: "warning",
+ color: 'warning',
},
{
- label: "Save to GitHub",
- type: "POST",
- url: "/api/ExecCommunityRepo",
+ label: 'Save to GitHub',
+ type: 'POST',
+ url: '/api/ExecCommunityRepo',
icon: ,
data: {
- Action: "UploadTemplate",
- GUID: "GUID",
+ Action: 'UploadTemplate',
+ GUID: 'GUID',
},
fields: [
{
- label: "Repository",
- name: "FullName",
- type: "select",
+ label: 'Repository',
+ name: 'FullName',
+ type: 'select',
api: {
- url: "/api/ListCommunityRepos",
+ url: '/api/ListCommunityRepos',
data: {
WriteAccess: true,
},
- queryKey: "CommunityRepos-Write",
- dataKey: "Results",
- valueField: "FullName",
- labelField: "FullName",
+ queryKey: 'CommunityRepos-Write',
+ dataKey: 'Results',
+ valueField: 'FullName',
+ labelField: 'FullName',
},
multiple: false,
creatable: false,
required: true,
validators: {
- required: { value: true, message: "This field is required" },
+ required: { value: true, message: 'This field is required' },
},
},
{
- label: "Commit Message",
- placeholder: "Enter a commit message for adding this file to GitHub",
- name: "Message",
- type: "textField",
+ label: 'Commit Message',
+ placeholder: 'Enter a commit message for adding this file to GitHub',
+ name: 'Message',
+ type: 'textField',
multiline: true,
required: true,
rows: 4,
},
],
- confirmText: "Are you sure you want to save this template to the selected repository?",
+ confirmText: 'Are you sure you want to save this template to the selected repository?',
condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled,
},
{
- label: "Delete Template",
- type: "POST",
- url: "/api/RemoveIntuneTemplate",
- data: { ID: "GUID" },
- confirmText: "Do you want to delete the template?",
+ label: 'Delete Template',
+ type: 'POST',
+ url: '/api/RemoveIntuneTemplate',
+ data: { ID: 'GUID' },
+ confirmText: 'Do you want to delete the template?',
multiPost: false,
icon: ,
- color: "danger",
+ color: 'danger',
},
- ];
+ ]
const offCanvas = {
- children: (row) => ,
- size: "lg",
- };
+ children: (row) => (
+
+ {Array.isArray(row.usage) && row.usage.length > 0 && (
+
+
+ Used in Standards Templates
+
+
+
+
+ Template Name
+ Included In
+
+
+
+ {row.usage.map((u, i) => (
+
+
+
+ {u.templateName ?? u.templateId}
+
+
+
+ {u.matchType === 'package' ? (
+
+ }
+ />
+
+ ) : (
+
+
+
+ )}
+
+
+ ))}
+
+
+
+ )}
+
+
+ ),
+ size: 'lg',
+ }
- const simpleColumns = ["displayName", "isSynced", "package", "description", "Type"];
+ const simpleColumns = ['displayName', 'isSynced', 'package', 'description', 'Type', 'usage']
+
+ const filterList = [
+ {
+ filterName: 'Synced Templates',
+ value: [{ id: 'isSynced', value: 'Yes' }],
+ type: 'column',
+ },
+ {
+ filterName: 'Custom Templates',
+ value: [{ id: 'isSynced', value: 'No' }],
+ type: 'column',
+ },
+ ]
return (
<>
@@ -166,6 +256,7 @@ const Page = () => {
actions={actions}
offCanvas={offCanvas}
simpleColumns={simpleColumns}
+ filters={filterList}
queryKey="ListIntuneTemplates-table"
cardButton={
{
}
/>
>
- );
-};
+ )
+}
-Page.getLayout = (page) => {page};
-export default Page;
+Page.getLayout = (page) => {page}
+export default Page
diff --git a/src/pages/endpoint/MEM/reusable-settings/index.js b/src/pages/endpoint/MEM/reusable-settings/index.js
index c301082ea1f0..75219f0d4136 100644
--- a/src/pages/endpoint/MEM/reusable-settings/index.js
+++ b/src/pages/endpoint/MEM/reusable-settings/index.js
@@ -4,15 +4,29 @@ import { CippTablePage } from "../../../../components/CippComponents/CippTablePa
import { Layout as DashboardLayout } from "../../../../layouts/index.js";
import { useSettings } from "../../../../hooks/use-settings";
import CippJsonView from "../../../../components/CippFormPages/CippJSONView";
+import { Stack } from "@mui/system";
+import { useCippReportDB } from "../../../../components/CippComponents/CippReportDBControls";
const Page = () => {
const { currentTenant } = useSettings();
const pageTitle = "Reusable Settings";
+ const reportDB = useCippReportDB({
+ apiUrl: "/api/ListIntuneReusableSettings",
+ queryKey: "ListIntuneReusableSettings",
+ cacheName: "IntuneReusableSettings",
+ syncTitle: "Sync Reusable Settings Report",
+ allowToggle: true,
+ defaultCached: false,
+ });
+ const isAllTenants = reportDB.isAllTenants;
+
const actions = [
{
label: "Edit Reusable Setting",
- link: `/endpoint/MEM/reusable-settings/edit?id=[id]&tenant=${currentTenant}&tenantFilter=${currentTenant}`,
+ link: isAllTenants
+ ? "/endpoint/MEM/reusable-settings/edit?id=[id]&tenant=[Tenant]&tenantFilter=[Tenant]"
+ : `/endpoint/MEM/reusable-settings/edit?id=[id]&tenant=${currentTenant}&tenantFilter=${currentTenant}`,
},
{
label: "Delete Reusable Setting",
@@ -47,18 +61,32 @@ const Page = () => {
size: "lg",
};
+ const simpleColumns = [
+ ...reportDB.cacheColumns,
+ "displayName",
+ "description",
+ "id",
+ "version",
+ ];
+
return (
-
- }
- apiUrl="/api/ListIntuneReusableSettings"
- queryKey={`ListIntuneReusableSettings-${currentTenant}`}
- actions={actions}
- offCanvas={offCanvas}
- simpleColumns={["displayName", "description", "id", "version"]}
- />
+ <>
+
+
+ {reportDB.controls}
+
+ }
+ apiUrl={reportDB.resolvedApiUrl}
+ queryKey={reportDB.resolvedQueryKey}
+ actions={actions}
+ offCanvas={offCanvas}
+ simpleColumns={simpleColumns}
+ />
+ {reportDB.syncDialog}
+ >
);
};
diff --git a/src/pages/endpoint/applications/list/index.js b/src/pages/endpoint/applications/list/index.js
index 41671638cfce..fec79a4cf7f9 100644
--- a/src/pages/endpoint/applications/list/index.js
+++ b/src/pages/endpoint/applications/list/index.js
@@ -4,9 +4,11 @@ import { CippApiDialog } from "../../../../components/CippComponents/CippApiDial
import { GlobeAltIcon, TrashIcon, UserIcon, UserGroupIcon } from "@heroicons/react/24/outline";
import { LaptopMac, Sync, BookmarkAdd } from "@mui/icons-material";
import { CippApplicationDeployDrawer } from "../../../../components/CippComponents/CippApplicationDeployDrawer";
-import { Button, Box } from "@mui/material";
+import { Button } from "@mui/material";
+import { Stack } from "@mui/system";
import { useSettings } from "../../../../hooks/use-settings.js";
import { useDialog } from "../../../../hooks/use-dialog.js";
+import { useCippReportDB } from "../../../../components/CippComponents/CippReportDBControls";
const assignmentIntentOptions = [
{ label: "Required", value: "Required" },
@@ -44,9 +46,18 @@ const mapOdataToAppType = (odataType) => {
const Page = () => {
const pageTitle = "Applications";
- const syncDialog = useDialog();
+ const vppSyncDialog = useDialog();
const tenant = useSettings().currentTenant;
+ const reportDB = useCippReportDB({
+ apiUrl: "/api/ListApps",
+ queryKey: "ListApps",
+ cacheName: "IntuneApplications",
+ syncTitle: "Sync Intune Applications Report",
+ allowToggle: true,
+ defaultCached: false,
+ });
+
const getAssignmentFilterFields = () => [
{
type: "autoComplete",
@@ -291,6 +302,7 @@ const Page = () => {
};
const simpleColumns = [
+ ...reportDB.cacheColumns,
"displayName",
"AppAssignment",
"AppExclude",
@@ -303,22 +315,24 @@ const Page = () => {
<>
+
- }>
+ }>
Sync VPP
-
+ {reportDB.controls}
+
}
/>
{
confirmText: `Are you sure you want to sync Apple Volume Purchase Program (VPP) tokens? This will sync all VPP tokens for ${tenant}.`,
}}
/>
+ {reportDB.syncDialog}
>
);
};
-Page.getLayout = (page) => {page};
+Page.getLayout = (page) => {page};
export default Page;
diff --git a/src/pages/endpoint/applications/templates/index.js b/src/pages/endpoint/applications/templates/index.js
index 6c4a0eb53285..b4a2a2bd1e28 100644
--- a/src/pages/endpoint/applications/templates/index.js
+++ b/src/pages/endpoint/applications/templates/index.js
@@ -1,122 +1,127 @@
-import { useState } from "react";
-import { Layout as DashboardLayout } from "../../../../layouts/index.js";
-import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx";
-import { TrashIcon } from "@heroicons/react/24/outline";
-import { Edit, RocketLaunch } from "@mui/icons-material";
-import { CippAppTemplateDrawer } from "../../../../components/CippComponents/CippAppTemplateDrawer";
-import CippJsonView from "../../../../components/CippFormPages/CippJSONView";
-import { Box } from "@mui/material";
-import { ApiGetCall } from "../../../../api/ApiCall";
-import { GitHub } from "@mui/icons-material";
+import { useState } from 'react'
+import { Layout as DashboardLayout } from '../../../../layouts/index.js'
+import { CippTablePage } from '../../../../components/CippComponents/CippTablePage.jsx'
+import { TrashIcon } from '@heroicons/react/24/outline'
+import { Edit, RocketLaunch } from '@mui/icons-material'
+import { CippAppTemplateDrawer } from '../../../../components/CippComponents/CippAppTemplateDrawer'
+import CippJsonView from '../../../../components/CippFormPages/CippJSONView'
+import { Box } from '@mui/material'
+import { ApiGetCall } from '../../../../api/ApiCall'
+import { GitHub } from '@mui/icons-material'
const Page = () => {
- const pageTitle = "Application Templates";
- const [editDrawerOpen, setEditDrawerOpen] = useState(false);
- const [editTemplate, setEditTemplate] = useState(null);
+ const pageTitle = 'Application Templates'
+ const [editDrawerOpen, setEditDrawerOpen] = useState(false)
+ const [editTemplate, setEditTemplate] = useState(null)
const integrations = ApiGetCall({
- url: "/api/ListExtensionsConfig",
- queryKey: "Integrations",
+ url: '/api/ListExtensionsConfig',
+ queryKey: 'Integrations',
refetchOnMount: false,
refetchOnReconnect: false,
- });
+ })
const actions = [
{
- label: "Edit Template",
+ label: 'Edit Template',
icon: ,
- color: "info",
+ color: 'info',
noConfirm: true,
customFunction: (row) => {
- setEditTemplate({ ...row });
- setEditDrawerOpen(true);
+ setEditTemplate({ ...row })
+ setEditDrawerOpen(true)
},
},
{
- label: "Save to GitHub",
- type: "POST",
- url: "/api/ExecCommunityRepo",
+ label: 'Save to GitHub',
+ type: 'POST',
+ url: '/api/ExecCommunityRepo',
icon: ,
data: {
- Action: "UploadTemplate",
- GUID: "GUID",
+ Action: 'UploadTemplate',
+ GUID: 'GUID',
},
fields: [
{
- label: "Repository",
- name: "FullName",
- type: "select",
+ label: 'Repository',
+ name: 'FullName',
+ type: 'select',
api: {
- url: "/api/ListCommunityRepos",
+ url: '/api/ListCommunityRepos',
data: {
WriteAccess: true,
},
- queryKey: "CommunityRepos-Write",
- dataKey: "Results",
- valueField: "FullName",
- labelField: "FullName",
+ queryKey: 'CommunityRepos-Write',
+ dataKey: 'Results',
+ valueField: 'FullName',
+ labelField: 'FullName',
},
multiple: false,
creatable: false,
required: true,
validators: {
- required: { value: true, message: "This field is required" },
+ required: { value: true, message: 'This field is required' },
},
},
{
- label: "Commit Message",
- placeholder: "Enter a commit message for adding this file to GitHub",
- name: "Message",
- type: "textField",
+ label: 'Commit Message',
+ placeholder: 'Enter a commit message for adding this file to GitHub',
+ name: 'Message',
+ type: 'textField',
multiline: true,
required: true,
rows: 4,
},
],
- confirmText: "Are you sure you want to save this template to the selected repository?",
+ confirmText: 'Are you sure you want to save this template to the selected repository?',
condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled,
},
{
- label: "Deploy Template",
- type: "POST",
- url: "/api/ExecDeployAppTemplate",
+ label: 'Deploy Template',
+ type: 'POST',
+ url: '/api/ExecDeployAppTemplate',
icon: ,
- color: "info",
+ color: 'info',
fields: [
{
- type: "autoComplete",
- name: "selectedTenants",
- label: "Select Tenants",
+ type: 'autoComplete',
+ name: 'selectedTenants',
+ label: 'Select Tenants',
multiple: true,
creatable: false,
api: {
- url: "/api/ListTenants?AllTenantSelector=true",
- queryKey: "ListTenants-AppTemplateDeploy",
+ url: '/api/ListTenants?AllTenantSelector=true',
+ queryKey: 'ListTenants-AppTemplateDeploy',
labelField: (tenant) => `${tenant.displayName} (${tenant.defaultDomainName})`,
- valueField: "defaultDomainName",
+ valueField: 'defaultDomainName',
addedField: {
- customerId: "customerId",
- defaultDomainName: "defaultDomainName",
+ customerId: 'customerId',
+ defaultDomainName: 'defaultDomainName',
},
},
- validators: { required: "Please select at least one tenant" },
+ validators: { required: 'Please select at least one tenant' },
},
{
- type: "radio",
- name: "AssignTo",
- label: "Override Assignment (optional)",
+ type: 'radio',
+ name: 'AssignTo',
+ label: 'Override Assignment (optional)',
options: [
- { label: "Keep template assignment", value: "" },
- { label: "Do not assign", value: "On" },
- { label: "Assign to all users", value: "allLicensedUsers" },
- { label: "Assign to all devices", value: "AllDevices" },
- { label: "Assign to all users and devices", value: "AllDevicesAndUsers" },
- { label: "Assign to Custom Group", value: "customGroup" },
+ { label: 'Keep template assignment', value: '' },
+ { label: 'Do not assign', value: 'On' },
+ { label: 'Assign to all users', value: 'allLicensedUsers' },
+ { label: 'Assign to all devices', value: 'AllDevices' },
+ { label: 'Assign to all users and devices', value: 'AllDevicesAndUsers' },
+ { label: 'Assign to Custom Group', value: 'customGroup' },
],
},
{
- type: "textField",
- name: "customGroup",
- label: "Custom Group Names (comma separated, wildcards allowed)",
+ type: 'textField',
+ name: 'customGroup',
+ label: 'Custom Group Names (comma separated, wildcards allowed)',
+ },
+ {
+ type: 'textField',
+ name: 'excludeGroup',
+ label: 'Exclude Group Names (comma separated, wildcards allowed)',
},
],
customDataformatter: (row, action, formData) => ({
@@ -125,26 +130,27 @@ const Page = () => {
defaultDomainName: t.value,
customerId: t.addedFields?.customerId,
})),
- AssignTo: formData?.AssignTo || "",
- customGroup: formData?.customGroup || "",
+ AssignTo: formData?.AssignTo || '',
+ customGroup: formData?.customGroup || '',
+ excludeGroup: formData?.excludeGroup || '',
}),
confirmText: 'Deploy "[displayName]" ([appCount] apps) to the selected tenants?',
},
{
- label: "Delete Template",
- type: "POST",
- url: "/api/RemoveAppTemplate",
- data: { ID: "GUID" },
+ label: 'Delete Template',
+ type: 'POST',
+ url: '/api/RemoveAppTemplate',
+ data: { ID: 'GUID' },
confirmText: 'Delete the template "[displayName]"?',
icon: ,
- color: "danger",
+ color: 'danger',
},
- ];
+ ]
const offCanvas = {
children: (row) => ,
- size: "lg",
- };
+ size: 'lg',
+ }
return (
<>
@@ -154,10 +160,10 @@ const Page = () => {
apiUrl="/api/ListAppTemplates"
actions={actions}
offCanvas={offCanvas}
- simpleColumns={["displayName", "description", "appCount", "appTypes", "appNames"]}
+ simpleColumns={['displayName', 'description', 'appCount', 'appTypes', 'appNames']}
queryKey="ListAppTemplates"
cardButton={
-
+
}
@@ -166,13 +172,13 @@ const Page = () => {
editData={editTemplate}
open={editDrawerOpen}
onClose={() => {
- setEditDrawerOpen(false);
- setEditTemplate(null);
+ setEditDrawerOpen(false)
+ setEditTemplate(null)
}}
/>
>
- );
-};
+ )
+}
-Page.getLayout = (page) => {page};
-export default Page;
+Page.getLayout = (page) => {page}
+export default Page
diff --git a/src/pages/identity/administration/groups/index.js b/src/pages/identity/administration/groups/index.js
index 3320ac353204..c64f443f5728 100644
--- a/src/pages/identity/administration/groups/index.js
+++ b/src/pages/identity/administration/groups/index.js
@@ -1,8 +1,8 @@
-import { Button } from "@mui/material";
-import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx";
-import { Layout as DashboardLayout } from "../../../../layouts/index.js";
-import Link from "next/link";
-import { TrashIcon, EyeIcon } from "@heroicons/react/24/outline";
+import { Button } from '@mui/material'
+import { CippTablePage } from '../../../../components/CippComponents/CippTablePage.jsx'
+import { Layout as DashboardLayout } from '../../../../layouts/index.js'
+import Link from 'next/link'
+import { TrashIcon, EyeIcon } from '@heroicons/react/24/outline'
import {
Visibility,
GroupAdd,
@@ -12,108 +12,120 @@ import {
GroupSharp,
CloudSync,
RocketLaunch,
-} from "@mui/icons-material";
-import { Stack } from "@mui/system";
-import { useState } from "react";
-import { useSettings } from "../../../../hooks/use-settings";
+} from '@mui/icons-material'
+import { Stack } from '@mui/system'
+import { useState } from 'react'
+import { useSettings } from '../../../../hooks/use-settings'
+import { useCippReportDB } from '../../../../components/CippComponents/CippReportDBControls'
const Page = () => {
- const pageTitle = "Groups";
- const [showMembers, setShowMembers] = useState(false);
- const { currentTenant } = useSettings();
+ const pageTitle = 'Groups'
+ const [showMembers, setShowMembers] = useState(false)
+ const { currentTenant } = useSettings()
+
+ const reportDB = useCippReportDB({
+ apiUrl: '/api/ListGroups',
+ queryKey: 'ListGroups',
+ cacheName: 'Groups',
+ syncTitle: 'Sync Groups Report',
+ allowToggle: true,
+ defaultCached: false,
+ allowAllTenantSync: true,
+ cacheColumns: ['CacheTimestamp'],
+ })
const handleMembersToggle = () => {
- setShowMembers(!showMembers);
- };
+ setShowMembers(!showMembers)
+ }
const actions = [
{
- label: "View Group",
+ label: 'View Group',
link: `/identity/administration/groups/group?groupId=[id]&tenantFilter=${currentTenant}`,
- color: "info",
+ color: 'info',
icon: ,
multiPost: false,
},
{
//tested
- label: "Edit Group",
- link: "/identity/administration/groups/edit?groupId=[id]&groupType=[groupType]",
+ label: 'Edit Group',
+ link: '/identity/administration/groups/edit?groupId=[id]&groupType=[groupType]',
multiPost: false,
icon: ,
- color: "success",
+ color: 'success',
},
{
- label: "Set Global Address List Visibility",
- type: "POST",
- url: "/api/ExecGroupsHideFromGAL",
+ label: 'Set Global Address List Visibility',
+ type: 'POST',
+ url: '/api/ExecGroupsHideFromGAL',
icon: ,
data: {
- ID: "mail",
- GroupType: "groupType",
+ ID: 'mail',
+ GroupType: 'groupType',
},
fields: [
{
- type: "radio",
- name: "HidefromGAL",
- label: "Global Address List Visibility",
+ type: 'radio',
+ name: 'HidefromGAL',
+ label: 'Global Address List Visibility',
options: [
- { label: "Hidden", value: true },
- { label: "Shown", value: false },
+ { label: 'Hidden', value: true },
+ { label: 'Shown', value: false },
],
- validators: { required: "Please select a visibility option" },
+ validators: { required: 'Please select a visibility option' },
},
],
confirmText:
- "Are you sure you want to hide this group from the global address list? Remember this will not work if the group is AD Synched.",
+ 'Are you sure you want to hide this group from the global address list? Remember this will not work if the group is AD Synched.',
multiPost: false,
},
{
- label: "Only allow messages from people inside the organisation",
- type: "POST",
- url: "/api/ExecGroupsDeliveryManagement",
+ label: 'Only allow messages from people inside the organisation',
+ type: 'POST',
+ url: '/api/ExecGroupsDeliveryManagement',
icon: ,
data: {
- ID: "mail",
- GroupType: "groupType",
+ ID: 'mail',
+ GroupType: 'groupType',
OnlyAllowInternal: true,
},
confirmText:
- "Are you sure you want to only allow messages from people inside the organisation? Remember this will not work if the group is AD Synched.",
+ 'Are you sure you want to only allow messages from people inside the organisation? Remember this will not work if the group is AD Synched.',
multiPost: false,
},
{
- label: "Allow messages from people inside and outside the organisation",
- type: "POST",
+ label: 'Allow messages from people inside and outside the organisation',
+ type: 'POST',
icon: ,
- url: "/api/ExecGroupsDeliveryManagement",
+ url: '/api/ExecGroupsDeliveryManagement',
data: {
- ID: "mail",
- GroupType: "groupType",
+ ID: 'mail',
+ GroupType: 'groupType',
OnlyAllowInternal: false,
},
confirmText:
- "Are you sure you want to allow messages from people inside and outside the organisation? Remember this will not work if the group is AD Synched.",
+ 'Are you sure you want to allow messages from people inside and outside the organisation? Remember this will not work if the group is AD Synched.',
multiPost: false,
},
{
- label: "Set Source of Authority",
- type: "POST",
- url: "/api/ExecSetCloudManaged",
+ label: 'Set Source of Authority',
+ type: 'POST',
+ url: '/api/ExecSetCloudManaged',
icon: ,
data: {
- ID: "id",
- displayName: "displayName",
- type: "!Group",
+ ID: 'id',
+ displayName: 'displayName',
+ type: '!Group',
},
fields: [
{
- type: "radio",
- name: "isCloudManaged",
- label: "Source of Authority",
+ type: 'radio',
+ name: 'isCloudManaged',
+ label: 'Source of Authority',
options: [
- { label: "Cloud Managed", value: true },
- { label: "On-Premises Managed", value: false },
+ { label: 'Cloud Managed', value: true },
+ { label: 'On-Premises Managed', value: false },
],
- validators: { required: "Please select a source of authority" },
+ validators: { required: 'Please select a source of authority' },
},
],
confirmText:
@@ -121,31 +133,31 @@ const Page = () => {
multiPost: false,
},
{
- label: "Create template based on group",
- type: "POST",
- url: "/api/AddGroupTemplate",
+ label: 'Create template based on group',
+ type: 'POST',
+ url: '/api/AddGroupTemplate',
icon: ,
data: {
- displayName: "displayName",
- description: "description",
- groupType: "calculatedGroupType",
- membershipRules: "membershipRule",
- allowExternal: "allowExternal",
- username: "mailNickname",
+ displayName: 'displayName',
+ description: 'description',
+ groupType: 'calculatedGroupType',
+ membershipRules: 'membershipRule',
+ allowExternal: 'allowExternal',
+ username: 'mailNickname',
},
- confirmText: "Are you sure you want to create a template based on this group?",
+ confirmText: 'Are you sure you want to create a template based on this group?',
multiPost: false,
},
{
- label: "Create Team from Group",
- type: "POST",
- url: "/api/AddGroupTeam",
+ label: 'Create Team from Group',
+ type: 'POST',
+ url: '/api/AddGroupTeam',
icon: ,
data: {
- GroupId: "id",
+ GroupId: 'id',
},
confirmText:
- "Are you sure you want to create a Team from this group? Note: The group must be at least 15 minutes old for this to work.",
+ 'Are you sure you want to create a Team from this group? Note: The group must be at least 15 minutes old for this to work.',
multiPost: false,
defaultvalues: {
TeamSettings: {
@@ -166,7 +178,7 @@ const Page = () => {
},
funSettings: {
allowGiphy: true,
- giphyContentRating: "strict",
+ giphyContentRating: 'strict',
allowStickersAndMemes: false,
allowCustomMemes: false,
},
@@ -174,181 +186,191 @@ const Page = () => {
},
fields: [
{
- type: "heading",
- name: "memberSettingsHeading",
- label: "Member Settings",
+ type: 'heading',
+ name: 'memberSettingsHeading',
+ label: 'Member Settings',
},
{
- type: "switch",
- name: "TeamSettings.memberSettings.allowCreatePrivateChannels",
- label: "Allow members to create private channels",
+ type: 'switch',
+ name: 'TeamSettings.memberSettings.allowCreatePrivateChannels',
+ label: 'Allow members to create private channels',
},
{
- type: "switch",
- name: "TeamSettings.memberSettings.allowCreateUpdateChannels",
- label: "Allow members to create and update channels",
+ type: 'switch',
+ name: 'TeamSettings.memberSettings.allowCreateUpdateChannels',
+ label: 'Allow members to create and update channels',
},
{
- type: "switch",
- name: "TeamSettings.memberSettings.allowDeleteChannels",
- label: "Allow members to delete channels",
+ type: 'switch',
+ name: 'TeamSettings.memberSettings.allowDeleteChannels',
+ label: 'Allow members to delete channels',
},
{
- type: "switch",
- name: "TeamSettings.memberSettings.allowAddRemoveApps",
- label: "Allow members to add and remove apps",
+ type: 'switch',
+ name: 'TeamSettings.memberSettings.allowAddRemoveApps',
+ label: 'Allow members to add and remove apps',
},
{
- type: "switch",
- name: "TeamSettings.memberSettings.allowCreateUpdateRemoveTabs",
- label: "Allow members to create, update and remove tabs",
+ type: 'switch',
+ name: 'TeamSettings.memberSettings.allowCreateUpdateRemoveTabs',
+ label: 'Allow members to create, update and remove tabs',
},
{
- type: "switch",
- name: "TeamSettings.memberSettings.allowCreateUpdateRemoveConnectors",
- label: "Allow members to create, update and remove connectors",
+ type: 'switch',
+ name: 'TeamSettings.memberSettings.allowCreateUpdateRemoveConnectors',
+ label: 'Allow members to create, update and remove connectors',
},
{
- type: "heading",
- name: "messagingSettingsHeading",
- label: "Messaging Settings",
+ type: 'heading',
+ name: 'messagingSettingsHeading',
+ label: 'Messaging Settings',
},
{
- type: "switch",
- name: "TeamSettings.messagingSettings.allowUserEditMessages",
- label: "Allow users to edit their messages",
+ type: 'switch',
+ name: 'TeamSettings.messagingSettings.allowUserEditMessages',
+ label: 'Allow users to edit their messages',
},
{
- type: "switch",
- name: "TeamSettings.messagingSettings.allowUserDeleteMessages",
- label: "Allow users to delete their messages",
+ type: 'switch',
+ name: 'TeamSettings.messagingSettings.allowUserDeleteMessages',
+ label: 'Allow users to delete their messages',
},
{
- type: "switch",
- name: "TeamSettings.messagingSettings.allowOwnerDeleteMessages",
- label: "Allow owners to delete messages",
+ type: 'switch',
+ name: 'TeamSettings.messagingSettings.allowOwnerDeleteMessages',
+ label: 'Allow owners to delete messages',
},
{
- type: "switch",
- name: "TeamSettings.messagingSettings.allowTeamMentions",
- label: "Allow @team mentions",
+ type: 'switch',
+ name: 'TeamSettings.messagingSettings.allowTeamMentions',
+ label: 'Allow @team mentions',
},
{
- type: "switch",
- name: "TeamSettings.messagingSettings.allowChannelMentions",
- label: "Allow @channel mentions",
+ type: 'switch',
+ name: 'TeamSettings.messagingSettings.allowChannelMentions',
+ label: 'Allow @channel mentions',
},
{
- type: "heading",
- name: "funSettingsHeading",
- label: "Fun Settings",
+ type: 'heading',
+ name: 'funSettingsHeading',
+ label: 'Fun Settings',
},
{
- type: "switch",
- name: "TeamSettings.funSettings.allowGiphy",
- label: "Allow Giphy",
+ type: 'switch',
+ name: 'TeamSettings.funSettings.allowGiphy',
+ label: 'Allow Giphy',
},
{
- type: "select",
- name: "TeamSettings.funSettings.giphyContentRating",
- label: "Giphy content rating",
+ type: 'select',
+ name: 'TeamSettings.funSettings.giphyContentRating',
+ label: 'Giphy content rating',
options: [
- { value: "strict", label: "Strict" },
- { value: "moderate", label: "Moderate" },
+ { value: 'strict', label: 'Strict' },
+ { value: 'moderate', label: 'Moderate' },
],
},
{
- type: "switch",
- name: "TeamSettings.funSettings.allowStickersAndMemes",
- label: "Allow stickers and memes",
+ type: 'switch',
+ name: 'TeamSettings.funSettings.allowStickersAndMemes',
+ label: 'Allow stickers and memes',
},
{
- type: "switch",
- name: "TeamSettings.funSettings.allowCustomMemes",
- label: "Allow custom memes",
+ type: 'switch',
+ name: 'TeamSettings.funSettings.allowCustomMemes',
+ label: 'Allow custom memes',
},
],
- condition: (row) => row?.calculatedGroupType === "m365",
+ condition: (row) => row?.calculatedGroupType === 'm365',
},
{
- label: "Delete Group",
- type: "POST",
- url: "/api/ExecGroupsDelete",
+ label: 'Delete Group',
+ type: 'POST',
+ url: '/api/ExecGroupsDelete',
icon: ,
data: {
- ID: "id",
- GroupType: "groupType",
- DisplayName: "displayName",
+ ID: 'id',
+ GroupType: 'groupType',
+ DisplayName: 'displayName',
},
- confirmText: "Are you sure you want to delete this group.",
+ confirmText: 'Are you sure you want to delete this group.',
multiPost: false,
},
- ];
+ ]
const offCanvas = {
extendedInfoFields: [
- "displayName",
- "userPrincipalName",
- "id",
- "mail",
- "description",
- "mailEnabled",
- "securityEnabled",
- "visibility",
- "assignedLicenses",
- "licenseProcessingState.state",
- "onPremisesSamAccountName",
- "membershipRule",
- "onPremisesSyncEnabled",
+ 'displayName',
+ 'userPrincipalName',
+ 'id',
+ 'mail',
+ 'description',
+ 'mailEnabled',
+ 'securityEnabled',
+ 'visibility',
+ 'assignedLicenses',
+ 'licenseProcessingState.state',
+ 'onPremisesSamAccountName',
+ 'membershipRule',
+ 'onPremisesSyncEnabled',
],
actions: actions,
- };
+ }
return (
-
-
- {showMembers ? "Hide Members" : "Show Members"}
-
- }>
- Add Group
-
- }
- >
- Deploy Group Template
-
-
- }
- apiUrl="/api/ListGroups"
- apiData={{ expandMembers: showMembers }}
- queryKey={
- showMembers
- ? `groups-with-members-${currentTenant}`
- : `groups-without-members-${currentTenant}`
- }
- actions={actions}
- offCanvas={offCanvas}
- simpleColumns={[
- "displayName",
- "description",
- "mail",
- "mailEnabled",
- "mailNickname",
- "groupType",
- "assignedLicenses",
- "licenseProcessingState.state",
- "visibility",
- "onPremisesSamAccountName",
- "membershipRule",
- "onPremisesSyncEnabled",
- ]}
- />
- );
-};
+ <>
+
+ {!reportDB.useReportDB && (
+
+ {showMembers ? 'Hide Members' : 'Show Members'}
+
+ )}
+ }>
+ Add Group
+
+ }
+ >
+ Deploy Group Template
+
+ {reportDB.controls}
+
+ }
+ apiUrl={reportDB.resolvedApiUrl}
+ apiData={reportDB.useReportDB ? undefined : { expandMembers: showMembers }}
+ queryKey={
+ reportDB.useReportDB
+ ? reportDB.resolvedQueryKey
+ : showMembers
+ ? `groups-with-members-${currentTenant}`
+ : `groups-without-members-${currentTenant}`
+ }
+ actions={actions}
+ offCanvas={offCanvas}
+ simpleColumns={[
+ ...reportDB.cacheColumns,
+ ...(reportDB.isAllTenants && reportDB.useReportDB ? ['Tenant'] : []),
+ 'displayName',
+ 'description',
+ 'mail',
+ 'mailEnabled',
+ 'mailNickname',
+ 'groupType',
+ 'assignedLicenses',
+ 'licenseProcessingState.state',
+ 'visibility',
+ 'onPremisesSamAccountName',
+ 'membershipRule',
+ 'onPremisesSyncEnabled',
+ ]}
+ />
+ {reportDB.syncDialog}
+ >
+ )
+}
-Page.getLayout = (page) => {page};
+Page.getLayout = (page) => {page}
-export default Page;
+export default Page
diff --git a/src/pages/identity/administration/jit-admin-templates/add.jsx b/src/pages/identity/administration/jit-admin-templates/add.jsx
index 986e7ea378d1..0a57a8b33882 100644
--- a/src/pages/identity/administration/jit-admin-templates/add.jsx
+++ b/src/pages/identity/administration/jit-admin-templates/add.jsx
@@ -9,6 +9,7 @@ import { CippFormDomainSelector } from "../../../../components/CippComponents/Ci
import { CippFormUserSelector } from "../../../../components/CippComponents/CippFormUserSelector";
import { CippFormGroupSelector } from "../../../../components/CippComponents/CippFormGroupSelector";
import gdaproles from "../../../../data/GDAPRoles.json";
+import countryList from "../../../../data/countryList.json";
import { useSettings } from "../../../../hooks/use-settings";
import { useEffect } from "react";
@@ -352,6 +353,19 @@ const Page = () => {
/>
)}
+
+ ({
+ label: Name,
+ value: Code,
+ }))}
+ formControl={formControl}
+ />
+
{
/>
)}
+
+ ({
+ label: Name,
+ value: Code,
+ }))}
+ formControl={formControl}
+ />
+
{
const watcher = useWatch({ control: formControl.control });
const useTAP = useWatch({ control: formControl.control, name: "UseTAP" });
+ const startDate = useWatch({ control: formControl.control, name: "startDate" });
+ const endDate = useWatch({ control: formControl.control, name: "endDate" });
+ const tapLifetimeInMinutes = useWatch({
+ control: formControl.control,
+ name: "tapLifetimeInMinutes",
+ });
const tapPolicy = ApiGetCall({
url: selectedTenant
@@ -46,6 +53,22 @@ const Page = () => {
const useRoles = useWatch({ control: formControl.control, name: "useRoles" });
const useGroups = useWatch({ control: formControl.control, name: "useGroups" });
+ useEffect(() => {
+ if (!useTAP || !startDate || !endDate) {
+ formControl.setValue("tapLifetimeInMinutes", null);
+ return;
+ }
+
+ const requestedMinutes = Math.max(1, Math.round((endDate - startDate) / 60));
+ const tapPolicyConfig = tapPolicy.data?.Results?.[0];
+ const policyMax = tapPolicyConfig?.maximumLifetimeInMinutes ?? 1440;
+ const policyMin = Math.min(tapPolicyConfig?.minimumLifetimeInMinutes ?? 1, policyMax);
+ formControl.setValue(
+ "tapLifetimeInMinutes",
+ Math.min(Math.max(requestedMinutes, policyMin), policyMax)
+ );
+ }, [useTAP, startDate, endDate, tapPolicy.data, formControl]);
+
// Clear fields when switches are toggled off
useEffect(() => {
if (!useRoles) {
@@ -205,6 +228,9 @@ const Page = () => {
if (template.defaultExistingUser) {
formControl.setValue("existingUser", template.defaultExistingUser, { shouldDirty: true });
}
+ if (template.defaultUsageLocation) {
+ formControl.setValue("usageLocation", template.defaultUsageLocation, { shouldDirty: true });
+ }
// Dates
if (template.defaultDuration) {
@@ -343,6 +369,19 @@ const Page = () => {
validators={{ required: "Domain is required" }}
/>
+
+ ({
+ label: Name,
+ value: Code,
+ }))}
+ formControl={formControl}
+ />
+
@@ -484,6 +523,11 @@ const Page = () => {
/>
+
{
TAP is not enabled in this tenant. TAP generation will fail.
)}
+ {useTAP && tapLifetimeInMinutes && (
+
+ TAP will be valid for {tapLifetimeInMinutes} minutes.
+
+ )}
{
const filterList = [
{
filterName: "Users at Risk",
- value: [{ id: "riskState", value: "atRisk" }],
+ value: [{ id: "riskState", value: "at Risk" }],
type: "column",
},
{
diff --git a/src/pages/identity/administration/users/patch-wizard.jsx b/src/pages/identity/administration/users/patch-wizard.jsx
index 300aebfaafa0..1168f12f593e 100644
--- a/src/pages/identity/administration/users/patch-wizard.jsx
+++ b/src/pages/identity/administration/users/patch-wizard.jsx
@@ -15,11 +15,13 @@ import {
Switch,
FormControlLabel,
Autocomplete,
+ Alert,
} from '@mui/material'
import { CippWizardStepButtons } from '../../../../components/CippWizard/CippWizardStepButtons'
import { ApiPostCall, ApiGetCall } from '../../../../api/ApiCall'
import { CippApiResults } from '../../../../components/CippComponents/CippApiResults'
import { CippDataTable } from '../../../../components/CippTable/CippDataTable'
+import { CippFormUserSelector } from '../../../../components/CippComponents/CippFormUserSelector'
import { Delete } from '@mui/icons-material'
// User properties that can be patched
@@ -54,6 +56,11 @@ const PATCHABLE_PROPERTIES = [
label: 'Job Title',
type: 'string',
},
+ {
+ property: 'manager',
+ label: 'Manager',
+ type: 'userSelector',
+ },
{
property: 'officeLocation',
label: 'Office Location',
@@ -79,6 +86,11 @@ const PATCHABLE_PROPERTIES = [
label: 'Show in Address List',
type: 'boolean',
},
+ {
+ property: 'sponsor',
+ label: 'Sponsor',
+ type: 'userSelector',
+ },
{
property: 'state',
label: 'State/Province',
@@ -182,6 +194,21 @@ const PropertySelectionStep = (props) => {
// Get unique tenant domains from users
const tenantDomains =
[...new Set(users?.map((user) => user.Tenant || user.tenantFilter).filter(Boolean))] || []
+ const firstTenantDomain = tenantDomains[0]
+ const hasManagerSelected = selectedProperties.includes('manager')
+ const hasSponsorSelected = selectedProperties.includes('sponsor')
+ const hasRelationshipSelected = hasManagerSelected || hasSponsorSelected
+
+ useEffect(() => {
+ if (!hasRelationshipSelected || !firstTenantDomain) {
+ return
+ }
+
+ const currentTenantFilter = formControl.getValues('tenantFilter')
+ if (currentTenantFilter?.value !== firstTenantDomain) {
+ formControl.setValue('tenantFilter', { value: firstTenantDomain })
+ }
+ }, [firstTenantDomain, formControl, hasRelationshipSelected])
// Fetch custom data mappings for all tenants
const customDataMappings = ApiGetCall({
@@ -248,6 +275,21 @@ const PropertySelectionStep = (props) => {
)
}
+ if (property?.type === 'userSelector') {
+ return (
+
+ )
+ }
+
// Default to text input for string types with consistent styling
return (
{
Properties to update
+ {hasRelationshipSelected && tenantDomains.length > 1 && (
+
+ The user picker is scoped to {firstTenantDomain}. Cross-tenant manager or sponsor
+ assignment is not supported, so the selected user must exist in each target tenant.
+
+ )}
{selectedProperties.map(renderPropertyInput)}
@@ -455,7 +503,14 @@ const ConfirmationStep = (props) => {
}
selectedProperties.forEach((propName) => {
- if (propertyValues[propName] !== undefined && propertyValues[propName] !== '') {
+ const propertyValue = propertyValues[propName]
+
+ if (propertyValue !== undefined && propertyValue !== '' && propertyValue !== null) {
+ if (propName === 'manager' || propName === 'sponsor') {
+ if (propertyValue?.value) userData[propName] = propertyValue.value
+ return
+ }
+
// Handle dot notation properties (e.g., "extension_abc123.customField")
if (propName.includes('.')) {
const parts = propName.split('.')
@@ -470,10 +525,10 @@ const ConfirmationStep = (props) => {
}
// Set the final property value
- current[parts[parts.length - 1]] = propertyValues[propName]
+ current[parts[parts.length - 1]] = propertyValue
} else {
// Handle regular properties
- userData[propName] = propertyValues[propName]
+ userData[propName] = propertyValue
}
}
})
@@ -522,8 +577,13 @@ const ConfirmationStep = (props) => {
{selectedProperties.map((propName) => {
const property = allProperties.find((p) => p.property === propName)
const value = propertyValues[propName]
- const displayValue =
- property?.type === 'boolean' ? (value ? 'Yes' : 'No') : value || 'Not set'
+ let displayValue = value || 'Not set'
+
+ if (propName === 'manager' || propName === 'sponsor') {
+ displayValue = value?.label || value?.value || 'Not set'
+ } else if (property?.type === 'boolean') {
+ displayValue = value ? 'Yes' : 'No'
+ }
return (
diff --git a/src/pages/identity/reports/risk-detections/index.js b/src/pages/identity/reports/risk-detections/index.js
index ef62600025d3..cd68e4eb13ed 100644
--- a/src/pages/identity/reports/risk-detections/index.js
+++ b/src/pages/identity/reports/risk-detections/index.js
@@ -52,7 +52,7 @@ const Page = () => {
const filterList = [
{
filterName: "Users at Risk",
- value: [{ id: "riskState", value: "atRisk" }],
+ value: [{ id: "riskState", value: "at Risk" }],
type: "column",
},
{
diff --git a/src/pages/onboardingv2.js b/src/pages/onboardingv2.js
index 3046798627ac..c32d85a24b8c 100644
--- a/src/pages/onboardingv2.js
+++ b/src/pages/onboardingv2.js
@@ -1,160 +1,8 @@
-import { Layout as DashboardLayout } from "../layouts/index.js";
-import { CippWizardConfirmation } from "../components/CippWizard/CippWizardConfirmation.jsx";
-import { CippDeploymentStep } from "../components/CippWizard/CIPPDeploymentStep.jsx";
-import CippWizardPage from "../components/CippWizard/CippWizardPage.jsx";
-import { CippWizardOptionsList } from "../components/CippWizard/CippWizardOptionsList.jsx";
-import { CippSAMDeploy } from "../components/CippWizard/CippSAMDeploy.jsx";
-import { CippTenantModeDeploy } from "../components/CippWizard/CippTenantModeDeploy.jsx";
-import { CippBaselinesStep } from "../components/CippWizard/CippBaselinesStep.jsx";
-import { CippNotificationsStep } from "../components/CippWizard/CippNotificationsStep.jsx";
-import { CippAlertsStep } from "../components/CippWizard/CippAlertsStep.jsx";
-import { CippAddTenantTypeSelection } from "../components/CippWizard/CippAddTenantTypeSelection.jsx";
-import { CippDirectTenantDeploy } from "../components/CippWizard/CippDirectTenantDeploy.jsx";
-import { CippGDAPTenantSetup } from "../components/CippWizard/CippGDAPTenantSetup.jsx";
-import { CippGDAPTenantOnboarding } from "../components/CippWizard/CippGDAPTenantOnboarding.jsx";
-import { BuildingOfficeIcon, CloudIcon, CpuChipIcon } from "@heroicons/react/24/outline";
-import { useRouter } from "next/router";
+import { Layout as DashboardLayout } from '../layouts/index.js'
+import OnboardingWizardPage from '../components/CippWizard/OnboardingWizardPage.jsx'
-const Page = () => {
- const router = useRouter();
- const selectedOptionQuery = router.query?.selectedOption;
- const deepLinkedOption = Array.isArray(selectedOptionQuery)
- ? selectedOptionQuery[0]
- : selectedOptionQuery;
- const setupOptions = [
- {
- description:
- "Choose this option if this is your first setup, or if you'd like to redo the previous setup.",
- icon: ,
- label: "First Setup",
- value: "FirstSetup",
- },
- {
- description: "Choose this option if you would like to add a tenant to your environment.",
- icon: ,
- label: "Add a tenant",
- value: "AddTenant",
- },
- {
- description:
- "Choose this option if you want to setup which application registration is used to connect to your tenants.",
- icon: ,
- label: "Create a new application registration for me and connect to my tenants",
- value: "CreateApp",
- },
- {
- description: "I would like to refresh my token or replace the account I've used.",
- icon: ,
- label: "Refresh Tokens for existing application registration",
- value: "UpdateTokens",
- },
- {
- description:
- "I have an existing application and would like to manually enter my token, or update them. This is only recommended for advanced users.",
- icon: ,
- label: "Manually enter credentials",
- value: "Manual",
- },
- ];
+const Page = () =>
- const hasDeepLinkedOption =
- typeof deepLinkedOption === "string" &&
- setupOptions.some((option) => option.value === deepLinkedOption);
+Page.getLayout = (page) => {page}
- const steps = [
- {
- description: "Onboarding",
- component: CippWizardOptionsList,
- hideStepWhen: () => hasDeepLinkedOption,
- componentProps: {
- title: "Select your setup method",
- subtext: `This wizard will guide you through setting up CIPPs access to your client tenants. If this is your first time setting up CIPP you will want to choose the option "Create application for me and connect to my tenants",`,
- valuesKey: "SyncTool",
- options: setupOptions,
- },
- },
- {
- description: "Application",
- component: CippSAMDeploy,
- showStepWhen: (values) =>
- values?.selectedOption === "CreateApp" || values?.selectedOption === "FirstSetup",
- },
- {
- description: "Tenants",
- component: CippTenantModeDeploy,
- showStepWhen: (values) =>
- values?.selectedOption === "CreateApp" || values?.selectedOption === "FirstSetup",
- },
- {
- description: "Tenant Type",
- component: CippAddTenantTypeSelection,
- showStepWhen: (values) => values?.selectedOption === "AddTenant",
- },
- {
- description: "Direct Tenant",
- component: CippDirectTenantDeploy,
- showStepWhen: (values) =>
- values?.selectedOption === "AddTenant" && values?.tenantType === "Direct",
- },
- {
- description: "GDAP Setup",
- component: CippGDAPTenantSetup,
- showStepWhen: (values) =>
- values?.selectedOption === "AddTenant" && values?.tenantType === "GDAP",
- },
- {
- description: "GDAP Onboarding",
- component: CippGDAPTenantOnboarding,
- showStepWhen: (values) =>
- values?.selectedOption === "AddTenant" &&
- values?.tenantType === "GDAP" &&
- values?.GDAPInviteAccepted === true,
- },
- {
- description: "Baselines",
- component: CippBaselinesStep,
- showStepWhen: (values) => values?.selectedOption === "FirstSetup",
- },
- {
- description: "Notifications",
- component: CippNotificationsStep,
- showStepWhen: (values) => values?.selectedOption === "FirstSetup",
- },
- {
- description: "Next Steps",
- component: CippAlertsStep,
- showStepWhen: (values) => values?.selectedOption === "FirstSetup",
- },
- {
- description: "Refresh Tokens",
- component: CippDeploymentStep,
- showStepWhen: (values) => values?.selectedOption === "UpdateTokens",
- },
- {
- description: "Manually enter credentials",
- component: CippDeploymentStep,
- showStepWhen: (values) => values?.selectedOption === "Manual",
- },
- {
- description: "Confirmation",
- component: CippWizardConfirmation,
- //confirm and finish button, perform tasks, launch checks etc.
- },
- ];
-
- return (
- <>
-
- >
- );
-};
-
-Page.getLayout = (page) => {page};
-
-export default Page;
+export default Page
diff --git a/src/pages/security/compliance/dlp-templates/index.js b/src/pages/security/compliance/dlp-templates/index.js
new file mode 100644
index 000000000000..99e949f700d1
--- /dev/null
+++ b/src/pages/security/compliance/dlp-templates/index.js
@@ -0,0 +1,108 @@
+import { Layout as DashboardLayout } from "../../../../layouts/index.js";
+import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx";
+import { TrashIcon } from "@heroicons/react/24/outline";
+import { GitHub } from "@mui/icons-material";
+import { ApiGetCall } from "../../../../api/ApiCall";
+import { CippPolicyImportDrawer } from "../../../../components/CippComponents/CippPolicyImportDrawer.jsx";
+import { CippDeployCompliancePolicyDrawer } from "../../../../components/CippComponents/CippDeployCompliancePolicyDrawer.jsx";
+import { PermissionButton } from "../../../../utils/permissions.js";
+
+const Page = () => {
+ const pageTitle = "DLP Compliance Policy Templates";
+ const cardButtonPermissions = ["Security.DlpCompliancePolicy.ReadWrite"];
+
+ const integrations = ApiGetCall({
+ url: "/api/ListExtensionsConfig",
+ queryKey: "Integrations",
+ refetchOnMount: false,
+ refetchOnReconnect: false,
+ });
+
+ const actions = [
+ {
+ label: "Save to GitHub",
+ type: "POST",
+ url: "/api/ExecCommunityRepo",
+ icon: ,
+ data: {
+ Action: "UploadTemplate",
+ GUID: "GUID",
+ },
+ fields: [
+ {
+ label: "Repository",
+ name: "FullName",
+ type: "select",
+ api: {
+ url: "/api/ListCommunityRepos",
+ data: { WriteAccess: true },
+ queryKey: "CommunityRepos-Write",
+ dataKey: "Results",
+ valueField: "FullName",
+ labelField: "FullName",
+ },
+ multiple: false,
+ creatable: false,
+ required: true,
+ validators: { required: { value: true, message: "This field is required" } },
+ },
+ {
+ label: "Commit Message",
+ placeholder: "Enter a commit message for adding this file to GitHub",
+ name: "Message",
+ type: "textField",
+ multiline: true,
+ required: true,
+ rows: 4,
+ },
+ ],
+ confirmText: "Are you sure you want to save this template to the selected repository?",
+ condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled,
+ },
+ {
+ label: "Delete Template",
+ type: "POST",
+ url: "/api/RemoveDlpCompliancePolicyTemplate",
+ data: { ID: "GUID" },
+ confirmText: "Do you want to delete the template?",
+ icon: ,
+ color: "danger",
+ },
+ ];
+
+ const offCanvas = {
+ extendedInfoFields: ["name", "comments", "Mode", "Workload", "Enabled", "GUID"],
+ actions: actions,
+ };
+
+ const simpleColumns = ["name", "comments", "Mode", "Workload", "Enabled", "GUID"];
+
+ return (
+
+
+
+ >
+ }
+ />
+ );
+};
+
+Page.getLayout = (page) => {page};
+export default Page;
diff --git a/src/pages/security/compliance/dlp/index.js b/src/pages/security/compliance/dlp/index.js
new file mode 100644
index 000000000000..750cfe6ade53
--- /dev/null
+++ b/src/pages/security/compliance/dlp/index.js
@@ -0,0 +1,112 @@
+import { Layout as DashboardLayout } from "../../../../layouts/index.js";
+import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx";
+import { Book, Block, Check } from "@mui/icons-material";
+import { TrashIcon } from "@heroicons/react/24/outline";
+import { CippDeployCompliancePolicyDrawer } from "../../../../components/CippComponents/CippDeployCompliancePolicyDrawer.jsx";
+import { PermissionButton } from "../../../../utils/permissions.js";
+
+const Page = () => {
+ const pageTitle = "DLP Compliance Policies";
+ const apiUrl = "/api/ListDlpCompliancePolicy";
+ const cardButtonPermissions = ["Security.DlpCompliancePolicy.ReadWrite"];
+
+ const actions = [
+ {
+ label: "Create template based on policy",
+ type: "POST",
+ icon: ,
+ url: "/api/AddDlpCompliancePolicyTemplate",
+ dataFunction: (data) => {
+ return { ...data };
+ },
+ confirmText: "Are you sure you want to create a template based on this DLP policy?",
+ },
+ {
+ label: "Enable Policy",
+ type: "POST",
+ icon: ,
+ url: "/api/EditDlpCompliancePolicy",
+ data: {
+ State: "!enable",
+ Identity: "Name",
+ },
+ confirmText: "Are you sure you want to enable this DLP policy?",
+ condition: (row) => row.Enabled === false,
+ },
+ {
+ label: "Disable Policy",
+ type: "POST",
+ icon: ,
+ url: "/api/EditDlpCompliancePolicy",
+ data: {
+ State: "!disable",
+ Identity: "Name",
+ },
+ confirmText: "Are you sure you want to disable this DLP policy?",
+ condition: (row) => row.Enabled === true,
+ },
+ {
+ label: "Delete Policy",
+ type: "POST",
+ icon: ,
+ url: "/api/RemoveDlpCompliancePolicy",
+ data: {
+ Identity: "Name",
+ },
+ confirmText: "Are you sure you want to delete this DLP policy?",
+ color: "danger",
+ },
+ ];
+
+ const offCanvas = {
+ extendedInfoFields: [
+ "Name",
+ "Comment",
+ "Mode",
+ "Enabled",
+ "Workload",
+ "ExchangeLocation",
+ "SharePointLocation",
+ "OneDriveLocation",
+ "TeamsLocation",
+ "EndpointDlpLocation",
+ "RuleCount",
+ "CreatedBy",
+ "WhenCreatedUTC",
+ "WhenChangedUTC",
+ ],
+ actions: actions,
+ };
+
+ const simpleColumns = [
+ "Name",
+ "Mode",
+ "Enabled",
+ "Workload",
+ "RuleCount",
+ "CreatedBy",
+ "WhenCreatedUTC",
+ "WhenChangedUTC",
+ ];
+
+ return (
+
+ }
+ />
+ );
+};
+
+Page.getLayout = (page) => {page};
+export default Page;
diff --git a/src/pages/security/compliance/labels-templates/index.js b/src/pages/security/compliance/labels-templates/index.js
new file mode 100644
index 000000000000..78477f7735b5
--- /dev/null
+++ b/src/pages/security/compliance/labels-templates/index.js
@@ -0,0 +1,115 @@
+import { Layout as DashboardLayout } from "../../../../layouts/index.js";
+import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx";
+import { TrashIcon } from "@heroicons/react/24/outline";
+import { GitHub } from "@mui/icons-material";
+import { ApiGetCall } from "../../../../api/ApiCall";
+import { CippPolicyImportDrawer } from "../../../../components/CippComponents/CippPolicyImportDrawer.jsx";
+import { CippDeployCompliancePolicyDrawer } from "../../../../components/CippComponents/CippDeployCompliancePolicyDrawer.jsx";
+import { PermissionButton } from "../../../../utils/permissions.js";
+
+const Page = () => {
+ const pageTitle = "Sensitivity Label Templates";
+ const cardButtonPermissions = ["Security.SensitivityLabel.ReadWrite"];
+
+ const integrations = ApiGetCall({
+ url: "/api/ListExtensionsConfig",
+ queryKey: "Integrations",
+ refetchOnMount: false,
+ refetchOnReconnect: false,
+ });
+
+ const actions = [
+ {
+ label: "Save to GitHub",
+ type: "POST",
+ url: "/api/ExecCommunityRepo",
+ icon: ,
+ data: {
+ Action: "UploadTemplate",
+ GUID: "GUID",
+ },
+ fields: [
+ {
+ label: "Repository",
+ name: "FullName",
+ type: "select",
+ api: {
+ url: "/api/ListCommunityRepos",
+ data: { WriteAccess: true },
+ queryKey: "CommunityRepos-Write",
+ dataKey: "Results",
+ valueField: "FullName",
+ labelField: "FullName",
+ },
+ multiple: false,
+ creatable: false,
+ required: true,
+ validators: { required: { value: true, message: "This field is required" } },
+ },
+ {
+ label: "Commit Message",
+ placeholder: "Enter a commit message for adding this file to GitHub",
+ name: "Message",
+ type: "textField",
+ multiline: true,
+ required: true,
+ rows: 4,
+ },
+ ],
+ confirmText: "Are you sure you want to save this template to the selected repository?",
+ condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled,
+ },
+ {
+ label: "Delete Template",
+ type: "POST",
+ url: "/api/RemoveSensitivityLabelTemplate",
+ data: { ID: "GUID" },
+ confirmText: "Do you want to delete the template?",
+ icon: ,
+ color: "danger",
+ },
+ ];
+
+ const offCanvas = {
+ extendedInfoFields: [
+ "name",
+ "DisplayName",
+ "comments",
+ "ContentType",
+ "EncryptionEnabled",
+ "GUID",
+ ],
+ actions: actions,
+ };
+
+ const simpleColumns = ["name", "DisplayName", "comments", "ContentType", "EncryptionEnabled", "GUID"];
+
+ return (
+
+
+
+ >
+ }
+ />
+ );
+};
+
+Page.getLayout = (page) => {page};
+export default Page;
diff --git a/src/pages/security/compliance/labels/index.js b/src/pages/security/compliance/labels/index.js
new file mode 100644
index 000000000000..e35ff5942b0f
--- /dev/null
+++ b/src/pages/security/compliance/labels/index.js
@@ -0,0 +1,91 @@
+import { Layout as DashboardLayout } from "../../../../layouts/index.js";
+import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx";
+import { Book } from "@mui/icons-material";
+import { TrashIcon } from "@heroicons/react/24/outline";
+import { CippDeployCompliancePolicyDrawer } from "../../../../components/CippComponents/CippDeployCompliancePolicyDrawer.jsx";
+import { PermissionButton } from "../../../../utils/permissions.js";
+
+const Page = () => {
+ const pageTitle = "Sensitivity Labels";
+ const apiUrl = "/api/ListSensitivityLabel";
+ const cardButtonPermissions = ["Security.SensitivityLabel.ReadWrite"];
+
+ const actions = [
+ {
+ label: "Create template based on label",
+ type: "POST",
+ icon: ,
+ url: "/api/AddSensitivityLabelTemplate",
+ dataFunction: (data) => {
+ return { ...data };
+ },
+ confirmText: "Are you sure you want to create a template based on this sensitivity label?",
+ },
+ {
+ label: "Delete Label",
+ type: "POST",
+ icon: ,
+ url: "/api/RemoveSensitivityLabel",
+ data: {
+ Identity: "Guid",
+ },
+ confirmText:
+ "Are you sure you want to delete this sensitivity label? Labels currently published to users will be removed from policies.",
+ color: "danger",
+ },
+ ];
+
+ const offCanvas = {
+ extendedInfoFields: [
+ "DisplayName",
+ "Name",
+ "Comment",
+ "Tooltip",
+ "ParentId",
+ "ContentType",
+ "EncryptionEnabled",
+ "EncryptionProtectionType",
+ "ContentMarkingHeaderEnabled",
+ "ContentMarkingFooterEnabled",
+ "ContentMarkingWatermarkEnabled",
+ "SiteAndGroupProtectionEnabled",
+ "Priority",
+ "Disabled",
+ "PublishedInPolicies",
+ ],
+ actions: actions,
+ };
+
+ const simpleColumns = [
+ "DisplayName",
+ "Name",
+ "ContentType",
+ "EncryptionEnabled",
+ "ContentMarkingHeaderEnabled",
+ "ContentMarkingWatermarkEnabled",
+ "SiteAndGroupProtectionEnabled",
+ "Priority",
+ "Disabled",
+ ];
+
+ return (
+
+ }
+ />
+ );
+};
+
+Page.getLayout = (page) => {page};
+export default Page;
diff --git a/src/pages/security/compliance/retention-templates/index.js b/src/pages/security/compliance/retention-templates/index.js
new file mode 100644
index 000000000000..291c989c35ec
--- /dev/null
+++ b/src/pages/security/compliance/retention-templates/index.js
@@ -0,0 +1,108 @@
+import { Layout as DashboardLayout } from "../../../../layouts/index.js";
+import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx";
+import { TrashIcon } from "@heroicons/react/24/outline";
+import { GitHub } from "@mui/icons-material";
+import { ApiGetCall } from "../../../../api/ApiCall";
+import { CippPolicyImportDrawer } from "../../../../components/CippComponents/CippPolicyImportDrawer.jsx";
+import { CippDeployCompliancePolicyDrawer } from "../../../../components/CippComponents/CippDeployCompliancePolicyDrawer.jsx";
+import { PermissionButton } from "../../../../utils/permissions.js";
+
+const Page = () => {
+ const pageTitle = "Retention Policy Templates";
+ const cardButtonPermissions = ["Security.RetentionCompliancePolicy.ReadWrite"];
+
+ const integrations = ApiGetCall({
+ url: "/api/ListExtensionsConfig",
+ queryKey: "Integrations",
+ refetchOnMount: false,
+ refetchOnReconnect: false,
+ });
+
+ const actions = [
+ {
+ label: "Save to GitHub",
+ type: "POST",
+ url: "/api/ExecCommunityRepo",
+ icon: ,
+ data: {
+ Action: "UploadTemplate",
+ GUID: "GUID",
+ },
+ fields: [
+ {
+ label: "Repository",
+ name: "FullName",
+ type: "select",
+ api: {
+ url: "/api/ListCommunityRepos",
+ data: { WriteAccess: true },
+ queryKey: "CommunityRepos-Write",
+ dataKey: "Results",
+ valueField: "FullName",
+ labelField: "FullName",
+ },
+ multiple: false,
+ creatable: false,
+ required: true,
+ validators: { required: { value: true, message: "This field is required" } },
+ },
+ {
+ label: "Commit Message",
+ placeholder: "Enter a commit message for adding this file to GitHub",
+ name: "Message",
+ type: "textField",
+ multiline: true,
+ required: true,
+ rows: 4,
+ },
+ ],
+ confirmText: "Are you sure you want to save this template to the selected repository?",
+ condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled,
+ },
+ {
+ label: "Delete Template",
+ type: "POST",
+ url: "/api/RemoveRetentionCompliancePolicyTemplate",
+ data: { ID: "GUID" },
+ confirmText: "Do you want to delete the template?",
+ icon: ,
+ color: "danger",
+ },
+ ];
+
+ const offCanvas = {
+ extendedInfoFields: ["name", "comments", "Enabled", "RestrictiveRetention", "GUID"],
+ actions: actions,
+ };
+
+ const simpleColumns = ["name", "comments", "Enabled", "RestrictiveRetention", "GUID"];
+
+ return (
+
+
+
+ >
+ }
+ />
+ );
+};
+
+Page.getLayout = (page) => {page};
+export default Page;
diff --git a/src/pages/security/compliance/retention/index.js b/src/pages/security/compliance/retention/index.js
new file mode 100644
index 000000000000..962301013f29
--- /dev/null
+++ b/src/pages/security/compliance/retention/index.js
@@ -0,0 +1,111 @@
+import { Layout as DashboardLayout } from "../../../../layouts/index.js";
+import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx";
+import { Book, Block, Check } from "@mui/icons-material";
+import { TrashIcon } from "@heroicons/react/24/outline";
+import { CippDeployCompliancePolicyDrawer } from "../../../../components/CippComponents/CippDeployCompliancePolicyDrawer.jsx";
+import { PermissionButton } from "../../../../utils/permissions.js";
+
+const Page = () => {
+ const pageTitle = "Purview Retention Policies";
+ const apiUrl = "/api/ListRetentionCompliancePolicy";
+ const cardButtonPermissions = ["Security.RetentionCompliancePolicy.ReadWrite"];
+
+ const actions = [
+ {
+ label: "Create template based on policy",
+ type: "POST",
+ icon: ,
+ url: "/api/AddRetentionCompliancePolicyTemplate",
+ data: { Identity: "Name" },
+ confirmText: "Are you sure you want to create a template based on this retention policy?",
+ },
+ {
+ label: "Enable Policy",
+ type: "POST",
+ icon: ,
+ url: "/api/EditRetentionCompliancePolicy",
+ data: {
+ State: "!enable",
+ Identity: "Name",
+ },
+ confirmText: "Are you sure you want to enable this retention policy?",
+ condition: (row) => row.Enabled === false,
+ },
+ {
+ label: "Disable Policy",
+ type: "POST",
+ icon: ,
+ url: "/api/EditRetentionCompliancePolicy",
+ data: {
+ State: "!disable",
+ Identity: "Name",
+ },
+ confirmText: "Are you sure you want to disable this retention policy?",
+ condition: (row) => row.Enabled === true,
+ },
+ {
+ label: "Delete Policy",
+ type: "POST",
+ icon: ,
+ url: "/api/RemoveRetentionCompliancePolicy",
+ data: {
+ Identity: "Name",
+ },
+ confirmText: "Are you sure you want to delete this retention policy?",
+ color: "danger",
+ },
+ ];
+
+ const offCanvas = {
+ extendedInfoFields: [
+ "Name",
+ "Comment",
+ "Enabled",
+ "Workload",
+ "RestrictiveRetention",
+ "ExchangeLocation",
+ "SharePointLocation",
+ "OneDriveLocation",
+ "ModernGroupLocation",
+ "TeamsChannelLocation",
+ "TeamsChatLocation",
+ "RuleCount",
+ "CreatedBy",
+ "WhenCreatedUTC",
+ "WhenChangedUTC",
+ ],
+ actions: actions,
+ };
+
+ const simpleColumns = [
+ "Name",
+ "Enabled",
+ "Workload",
+ "RuleCount",
+ "RestrictiveRetention",
+ "CreatedBy",
+ "WhenCreatedUTC",
+ "WhenChangedUTC",
+ ];
+
+ return (
+
+ }
+ />
+ );
+};
+
+Page.getLayout = (page) => {page};
+export default Page;
diff --git a/src/pages/security/compliance/sit-templates/index.js b/src/pages/security/compliance/sit-templates/index.js
new file mode 100644
index 000000000000..3b6bf4b87121
--- /dev/null
+++ b/src/pages/security/compliance/sit-templates/index.js
@@ -0,0 +1,108 @@
+import { Layout as DashboardLayout } from "../../../../layouts/index.js";
+import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx";
+import { TrashIcon } from "@heroicons/react/24/outline";
+import { GitHub } from "@mui/icons-material";
+import { ApiGetCall } from "../../../../api/ApiCall";
+import { CippPolicyImportDrawer } from "../../../../components/CippComponents/CippPolicyImportDrawer.jsx";
+import { CippDeployCompliancePolicyDrawer } from "../../../../components/CippComponents/CippDeployCompliancePolicyDrawer.jsx";
+import { PermissionButton } from "../../../../utils/permissions.js";
+
+const Page = () => {
+ const pageTitle = "Sensitive Information Type Templates";
+ const cardButtonPermissions = ["Security.SensitiveInfoType.ReadWrite"];
+
+ const integrations = ApiGetCall({
+ url: "/api/ListExtensionsConfig",
+ queryKey: "Integrations",
+ refetchOnMount: false,
+ refetchOnReconnect: false,
+ });
+
+ const actions = [
+ {
+ label: "Save to GitHub",
+ type: "POST",
+ url: "/api/ExecCommunityRepo",
+ icon: ,
+ data: {
+ Action: "UploadTemplate",
+ GUID: "GUID",
+ },
+ fields: [
+ {
+ label: "Repository",
+ name: "FullName",
+ type: "select",
+ api: {
+ url: "/api/ListCommunityRepos",
+ data: { WriteAccess: true },
+ queryKey: "CommunityRepos-Write",
+ dataKey: "Results",
+ valueField: "FullName",
+ labelField: "FullName",
+ },
+ multiple: false,
+ creatable: false,
+ required: true,
+ validators: { required: { value: true, message: "This field is required" } },
+ },
+ {
+ label: "Commit Message",
+ placeholder: "Enter a commit message for adding this file to GitHub",
+ name: "Message",
+ type: "textField",
+ multiline: true,
+ required: true,
+ rows: 4,
+ },
+ ],
+ confirmText: "Are you sure you want to save this template to the selected repository?",
+ condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled,
+ },
+ {
+ label: "Delete Template",
+ type: "POST",
+ url: "/api/RemoveSensitiveInfoTypeTemplate",
+ data: { ID: "GUID" },
+ confirmText: "Do you want to delete the template?",
+ icon: ,
+ color: "danger",
+ },
+ ];
+
+ const offCanvas = {
+ extendedInfoFields: ["name", "comments", "Pattern", "Confidence", "Locale", "GUID"],
+ actions: actions,
+ };
+
+ const simpleColumns = ["name", "comments", "Pattern", "Confidence", "Locale", "GUID"];
+
+ return (
+
+
+
+ >
+ }
+ />
+ );
+};
+
+Page.getLayout = (page) => {page};
+export default Page;
diff --git a/src/pages/security/compliance/sit/index.js b/src/pages/security/compliance/sit/index.js
new file mode 100644
index 000000000000..3101f0502218
--- /dev/null
+++ b/src/pages/security/compliance/sit/index.js
@@ -0,0 +1,70 @@
+import { Layout as DashboardLayout } from "../../../../layouts/index.js";
+import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx";
+import { TrashIcon } from "@heroicons/react/24/outline";
+import { CippDeployCompliancePolicyDrawer } from "../../../../components/CippComponents/CippDeployCompliancePolicyDrawer.jsx";
+import { PermissionButton } from "../../../../utils/permissions.js";
+
+const Page = () => {
+ const pageTitle = "Sensitive Information Types";
+ const apiUrl = "/api/ListSensitiveInfoType";
+ const cardButtonPermissions = ["Security.SensitiveInfoType.ReadWrite"];
+
+ const actions = [
+ {
+ label: "Delete SIT",
+ type: "POST",
+ icon: ,
+ url: "/api/RemoveSensitiveInfoType",
+ data: {
+ Identity: "Name",
+ },
+ confirmText:
+ "Are you sure you want to delete this Sensitive Information Type? Built-in Microsoft types cannot be deleted.",
+ color: "danger",
+ },
+ ];
+
+ const offCanvas = {
+ extendedInfoFields: [
+ "Name",
+ "Description",
+ "Publisher",
+ "Recommended",
+ "RulePackId",
+ "RulePackVersion",
+ "State",
+ "Type",
+ ],
+ actions: actions,
+ };
+
+ const simpleColumns = [
+ "Name",
+ "Publisher",
+ "Description",
+ "Recommended",
+ "RulePackVersion",
+ "State",
+ ];
+
+ return (
+
+ }
+ />
+ );
+};
+
+Page.getLayout = (page) => {page};
+export default Page;
diff --git a/src/pages/security/reports/mde-onboarding/index.js b/src/pages/security/reports/mde-onboarding/index.js
index 839d85f6906a..955ec17fa66a 100644
--- a/src/pages/security/reports/mde-onboarding/index.js
+++ b/src/pages/security/reports/mde-onboarding/index.js
@@ -14,11 +14,14 @@ import {
SvgIcon,
} from "@mui/material";
import { Sync, OpenInNew } from "@mui/icons-material";
+import { Grid } from "@mui/system";
import { ApiGetCall } from "../../../../api/ApiCall";
import { CippHead } from "../../../../components/CippComponents/CippHead";
import { useDialog } from "../../../../hooks/use-dialog";
import { CippApiDialog } from "../../../../components/CippComponents/CippApiDialog";
import { CippQueueTracker } from "../../../../components/CippTable/CippQueueTracker";
+import { CippPropertyListCard } from "../../../../components/CippCards/CippPropertyListCard";
+import { getCippFormatting } from "../../../../utils/get-cipp-formatting";
import { useState } from "react";
import { useCippReportDB } from "../../../../components/CippComponents/CippReportDBControls";
@@ -63,73 +66,232 @@ const SingleTenantView = ({ tenant }) => {
const item = Array.isArray(data) ? data[0] : data;
const status = item?.partnerState || "Unknown";
+ const platformItems = [
+ {
+ label: "Windows",
+ value: getCippFormatting(item?.windowsEnabled, "windowsEnabled"),
+ },
+ {
+ label: "iOS",
+ value: getCippFormatting(item?.iosEnabled, "iosEnabled"),
+ },
+ {
+ label: "Android",
+ value: getCippFormatting(item?.androidEnabled, "androidEnabled"),
+ },
+ {
+ label: "macOS",
+ value: getCippFormatting(item?.macEnabled, "macEnabled"),
+ },
+ ];
+
+ const mamItems = [
+ {
+ label: "iOS MAM",
+ value: getCippFormatting(
+ item?.iosMobileApplicationManagementEnabled,
+ "iosMobileApplicationManagementEnabled"
+ ),
+ },
+ {
+ label: "Android MAM",
+ value: getCippFormatting(
+ item?.androidMobileApplicationManagementEnabled,
+ "androidMobileApplicationManagementEnabled"
+ ),
+ },
+ {
+ label: "Windows MAM",
+ value: getCippFormatting(
+ item?.windowsMobileApplicationManagementEnabled,
+ "windowsMobileApplicationManagementEnabled"
+ ),
+ },
+ {
+ label: "MDE Attach",
+ value: getCippFormatting(
+ item?.microsoftDefenderForEndpointAttachEnabled,
+ "microsoftDefenderForEndpointAttachEnabled"
+ ),
+ },
+ ];
+
+ const dataCollectionItems = [
+ {
+ label: "Block iOS on missing partner data",
+ value: getCippFormatting(
+ item?.iosDeviceBlockedOnMissingPartnerData,
+ "iosDeviceBlockedOnMissingPartnerData"
+ ),
+ },
+ {
+ label: "Block Android on missing partner data",
+ value: getCippFormatting(
+ item?.androidDeviceBlockedOnMissingPartnerData,
+ "androidDeviceBlockedOnMissingPartnerData"
+ ),
+ },
+ {
+ label: "Block Windows on missing partner data",
+ value: getCippFormatting(
+ item?.windowsDeviceBlockedOnMissingPartnerData,
+ "windowsDeviceBlockedOnMissingPartnerData"
+ ),
+ },
+ {
+ label: "Block macOS on missing partner data",
+ value: getCippFormatting(
+ item?.macDeviceBlockedOnMissingPartnerData,
+ "macDeviceBlockedOnMissingPartnerData"
+ ),
+ },
+ {
+ label: "Block unsupported OS versions",
+ value: getCippFormatting(
+ item?.partnerUnsupportedOsVersionBlocked,
+ "partnerUnsupportedOsVersionBlocked"
+ ),
+ },
+ {
+ label: "Unresponsiveness threshold (days)",
+ value:
+ item?.partnerUnresponsivenessThresholdInDays ??
+ getCippFormatting(null, "partnerUnresponsivenessThresholdInDays"),
+ },
+ {
+ label: "Collect iOS app metadata",
+ value: getCippFormatting(
+ item?.allowPartnerToCollectIOSApplicationMetadata,
+ "allowPartnerToCollectIOSApplicationMetadata"
+ ),
+ },
+ {
+ label: "Collect iOS personal app metadata",
+ value: getCippFormatting(
+ item?.allowPartnerToCollectIOSPersonalApplicationMetadata,
+ "allowPartnerToCollectIOSPersonalApplicationMetadata"
+ ),
+ },
+ {
+ label: "Collect iOS certificate metadata",
+ value: getCippFormatting(
+ item?.allowPartnerToCollectIosCertificateMetadata,
+ "allowPartnerToCollectIosCertificateMetadata"
+ ),
+ },
+ {
+ label: "Collect iOS personal certificate metadata",
+ value: getCippFormatting(
+ item?.allowPartnerToCollectIosPersonalCertificateMetadata,
+ "allowPartnerToCollectIosPersonalCertificateMetadata"
+ ),
+ },
+ ];
+
return (
<>
-
-
-
-
-
-
-
- }
- size="small"
- onClick={syncDialog.handleOpen}
- >
- Sync
-
-
- }
- />
-
- {isFetching ? (
-
- ) : (
-
-
- Status:
-
+
+
+
+
-
- {item?.CacheTimestamp && (
-
- Last synced: {new Date(item.CacheTimestamp).toLocaleString()}
-
- )}
- {item?.error && (
-
- {item.error}
-
- )}
- {tenantId && status !== "enabled" && status !== "available" && (
}
- href={`https://security.microsoft.com/securitysettings/endpoints/onboarding?tid=${tenantId}`}
- target="_blank"
- rel="noopener noreferrer"
- sx={{ alignSelf: "flex-start" }}
+ startIcon={
+
+
+
+ }
+ size="small"
+ onClick={syncDialog.handleOpen}
>
- Start Onboarding
+ Sync
- )}
-
- )}
-
-
+
+ }
+ />
+
+ {isFetching ? (
+
+ ) : (
+
+
+ Status:
+
+
+ {item?.lastHeartbeatDateTime && (
+
+ Last heartbeat:{" "}
+ {new Date(item.lastHeartbeatDateTime).toLocaleString()}
+
+ )}
+ {item?.CacheTimestamp && (
+
+ Last synced: {new Date(item.CacheTimestamp).toLocaleString()}
+
+ )}
+ {item?.error && (
+
+ {item.error}
+
+ )}
+ {tenantId && status !== "enabled" && status !== "available" && (
+ }
+ href={`https://security.microsoft.com/securitysettings/endpoints/onboarding?tid=${tenantId}`}
+ target="_blank"
+ rel="noopener noreferrer"
+ sx={{ alignSelf: "flex-start" }}
+ >
+ Start Onboarding
+
+ )}
+
+ )}
+
+
+
+ {!isFetching && item && (
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
{
apiUrl={reportDB.resolvedApiUrl}
apiData={reportDB.resolvedApiData}
queryKey={reportDB.resolvedQueryKey}
- simpleColumns={["Tenant", "partnerState", "CacheTimestamp"]}
+ simpleColumns={[
+ "Tenant",
+ "partnerState",
+ "lastHeartbeatDateTime",
+ "microsoftDefenderForEndpointAttachEnabled",
+ "windowsEnabled",
+ "iosEnabled",
+ "androidEnabled",
+ "macEnabled",
+ "iosMobileApplicationManagementEnabled",
+ "androidMobileApplicationManagementEnabled",
+ "windowsMobileApplicationManagementEnabled",
+ "partnerUnresponsivenessThresholdInDays",
+ "CacheTimestamp",
+ ]}
cardButton={reportDB.controls}
/>
{reportDB.syncDialog}
diff --git a/src/pages/teams-share/onedrive/index.js b/src/pages/teams-share/onedrive/index.js
index b5ef1f7a2792..7c06e88f1441 100644
--- a/src/pages/teams-share/onedrive/index.js
+++ b/src/pages/teams-share/onedrive/index.js
@@ -8,10 +8,12 @@ const Page = () => {
const reportDB = useCippReportDB({
apiUrl: '/api/ListSites?type=OneDriveUsageAccount',
queryKey: 'ListSites-OneDriveUsageAccount',
- cacheName: 'OneDriveUsage',
- syncTitle: 'Sync OneDrive Usage',
+ cacheName: 'Sites',
+ syncTitle: 'Sync OneDrive Report',
+ syncData: { Types: 'OneDriveUsageAccount' },
allowToggle: true,
defaultCached: false,
+ allowAllTenantSync: true,
})
const actions = [
@@ -115,6 +117,6 @@ const Page = () => {
)
}
-Page.getLayout = (page) => {page}
+Page.getLayout = (page) => {page}
export default Page
diff --git a/src/pages/teams-share/sharepoint/index.js b/src/pages/teams-share/sharepoint/index.js
index ac08cec280cd..cf1353f52b7f 100644
--- a/src/pages/teams-share/sharepoint/index.js
+++ b/src/pages/teams-share/sharepoint/index.js
@@ -22,10 +22,12 @@ const Page = () => {
const reportDB = useCippReportDB({
apiUrl: '/api/ListSites?type=SharePointSiteUsage',
queryKey: 'ListSites-SharePointSiteUsage',
- cacheName: 'SharePointSiteUsage',
- syncTitle: 'Sync SharePoint Site Usage',
+ cacheName: 'Sites',
+ syncTitle: 'Sync SharePoint Sites Report',
+ syncData: { Types: 'SharePointSiteUsage' },
allowToggle: true,
- defaultCached: false,
+ defaultCached: true,
+ allowAllTenantSync: true,
})
const actions = [
@@ -270,6 +272,6 @@ const Page = () => {
)
}
-Page.getLayout = (page) => {page}
+Page.getLayout = (page) => {page}
export default Page
diff --git a/src/pages/teams-share/teams/business-voice/index.js b/src/pages/teams-share/teams/business-voice/index.js
index a3aa56764153..3c9354706147 100644
--- a/src/pages/teams-share/teams/business-voice/index.js
+++ b/src/pages/teams-share/teams/business-voice/index.js
@@ -1,10 +1,20 @@
import { Layout as DashboardLayout } from "../../../../layouts/index.js";
import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx";
+import { useCippReportDB } from "../../../../components/CippComponents/CippReportDBControls";
import { PersonAdd, PersonRemove, LocationOn } from "@mui/icons-material";
const Page = () => {
const pageTitle = "Teams Business Voice";
+ const reportDB = useCippReportDB({
+ apiUrl: "/api/ListTeamsVoice",
+ queryKey: "ListTeamsVoice",
+ cacheName: "TeamsVoice",
+ syncTitle: "Sync Teams Business Voice Report",
+ allowToggle: true,
+ defaultCached: false,
+ });
+
const actions = [
// the modal dropdowns that were added below may not exist yet, and will need to be tested.
{
@@ -81,34 +91,40 @@ const Page = () => {
};
return (
-
+ <>
+
+ {reportDB.syncDialog}
+ >
);
};
-Page.getLayout = (page) => {page};
+Page.getLayout = (page) => {page};
export default Page;
diff --git a/src/pages/teams-share/teams/list-team/index.js b/src/pages/teams-share/teams/list-team/index.js
index bf48cccb08a3..99b51994bafe 100644
--- a/src/pages/teams-share/teams/list-team/index.js
+++ b/src/pages/teams-share/teams/list-team/index.js
@@ -1,13 +1,24 @@
import { Layout as DashboardLayout } from "../../../../layouts/index.js";
import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx";
import { Button } from "@mui/material";
+import { Stack } from "@mui/system";
import { Delete, GroupAdd } from "@mui/icons-material";
import Link from "next/link";
import { Edit } from "@mui/icons-material";
+import { useCippReportDB } from "../../../../components/CippComponents/CippReportDBControls";
const Page = () => {
const pageTitle = "Teams";
+ const reportDB = useCippReportDB({
+ apiUrl: "/api/ListTeams?type=list",
+ queryKey: "ListTeams-list",
+ cacheName: "Teams",
+ syncTitle: "Sync Teams Report",
+ allowToggle: true,
+ defaultCached: false,
+ });
+
const actions = [
{
label: "Edit Group",
@@ -32,22 +43,34 @@ const Page = () => {
];
return (
-
- }>
- Add Team
-
- >
- }
- />
+ <>
+
+ }>
+ Add Team
+
+ {reportDB.controls}
+
+ }
+ />
+ {reportDB.syncDialog}
+ >
);
};
-Page.getLayout = (page) => {page};
+Page.getLayout = (page) => {page};
export default Page;
diff --git a/src/pages/teams-share/teams/teams-activity/index.js b/src/pages/teams-share/teams/teams-activity/index.js
index 2f2797a57cbb..f5fb2bb53754 100644
--- a/src/pages/teams-share/teams/teams-activity/index.js
+++ b/src/pages/teams-share/teams/teams-activity/index.js
@@ -1,18 +1,40 @@
import { Layout as DashboardLayout } from "../../../../layouts/index.js";
import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx";
+import { useCippReportDB } from "../../../../components/CippComponents/CippReportDBControls";
const Page = () => {
const pageTitle = "Teams Activity List";
+ const reportDB = useCippReportDB({
+ apiUrl: "/api/ListTeamsActivity?type=TeamsUserActivityUser",
+ queryKey: "ListTeamsActivity-TeamsUserActivityUser",
+ cacheName: "TeamsActivity",
+ syncTitle: "Sync Teams Activity Report",
+ allowToggle: true,
+ defaultCached: false,
+ });
+
return (
-
+ <>
+
+ {reportDB.syncDialog}
+ >
);
};
-Page.getLayout = (page) => {page};
+Page.getLayout = (page) => {page};
export default Page;
diff --git a/src/pages/tenant/administration/alert-configuration/alert.jsx b/src/pages/tenant/administration/alert-configuration/alert.jsx
index d9af06f2b549..7829d9df639e 100644
--- a/src/pages/tenant/administration/alert-configuration/alert.jsx
+++ b/src/pages/tenant/administration/alert-configuration/alert.jsx
@@ -29,12 +29,14 @@ import { ApiGetCall, ApiPostCall } from '../../../../api/ApiCall'
import { PlusIcon } from '@heroicons/react/24/outline'
import { CippFormCondition } from '../../../../components/CippComponents/CippFormCondition'
import { CippHead } from '../../../../components/CippComponents/CippHead'
+import { useSettings } from '../../../../hooks/use-settings'
const AlertWizard = () => {
const apiRequest = ApiPostCall({
relatedQueryKeys: ['ListAlertsQueue', 'ListCurrentAlerts'],
})
const router = useRouter()
+ const tenantFilter = useSettings().currentTenant
const [editAlert, setAlertEdit] = useState(false)
useEffect(() => {
if (router.query.id) {
@@ -46,6 +48,8 @@ const AlertWizard = () => {
url: '/api/ListAlertsQueue',
relatedQueryKeys: 'ListAlertsQueue',
queryKey: 'ListCurrentAlerts',
+ data: { tenantFilter },
+ waiting: !!tenantFilter,
})
const [recurrenceOptions, setRecurrenceOptions] = useState([
{ value: '30m', label: 'Every 30 minutes' },
diff --git a/src/pages/tenant/conditional/list-template/index.js b/src/pages/tenant/conditional/list-template/index.js
index 41221d021582..5f7daf3b2737 100644
--- a/src/pages/tenant/conditional/list-template/index.js
+++ b/src/pages/tenant/conditional/list-template/index.js
@@ -2,7 +2,7 @@ import { Layout as DashboardLayout } from "../../../../layouts/index.js";
import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx";
import { Button, Box } from "@mui/material";
import CippJsonView from "../../../../components/CippFormPages/CippJSONView";
-import { Delete, GitHub, Edit, RocketLaunch } from "@mui/icons-material";
+import { Delete, GitHub, Edit, RocketLaunch, LocalOffer, LocalOfferOutlined } from "@mui/icons-material";
import { ApiGetCall } from "../../../../api/ApiCall";
import { CippPolicyImportDrawer } from "../../../../components/CippComponents/CippPolicyImportDrawer.jsx";
import { CippCADeployDrawer } from "../../../../components/CippComponents/CippCADeployDrawer.jsx";
@@ -42,6 +42,37 @@ const Page = () => {
icon: ,
color: "info",
},
+ {
+ label: "Add to package",
+ type: "POST",
+ url: "/api/ExecSetPackageTag",
+ data: { GUID: "GUID" },
+ fields: [
+ {
+ type: "textField",
+ name: "Package",
+ label: "Package Name",
+ required: true,
+ validators: {
+ required: { value: true, message: "Package name is required" },
+ },
+ },
+ ],
+ confirmText: "Enter the package name to assign to the selected template(s).",
+ multiPost: true,
+ icon: ,
+ color: "info",
+ },
+ {
+ label: "Remove from package",
+ type: "POST",
+ url: "/api/ExecSetPackageTag",
+ data: { GUID: "GUID", Remove: true },
+ confirmText: "Are you sure you want to remove the selected template(s) from their package?",
+ multiPost: true,
+ icon: ,
+ color: "warning",
+ },
{
label: "Save to GitHub",
type: "POST",
@@ -110,7 +141,7 @@ const Page = () => {
queryKey="ListCATemplates-table"
actions={actions}
offCanvas={offCanvas}
- simpleColumns={["displayName", "GUID"]}
+ simpleColumns={["displayName", "package", "GUID"]}
cardButton={
diff --git a/src/pages/tenant/manage/applied-standards.js b/src/pages/tenant/manage/applied-standards.js
index 58cc78f19c88..376507ec4017 100644
--- a/src/pages/tenant/manage/applied-standards.js
+++ b/src/pages/tenant/manage/applied-standards.js
@@ -1430,6 +1430,12 @@ const Page = () => {
templateDetails.refetch()
},
currentTenant,
+ templateTenants: Array.isArray(selectedTemplate?.tenantFilter)
+ ? selectedTemplate.tenantFilter
+ : [],
+ excludedTenants: Array.isArray(selectedTemplate?.excludedTenants)
+ ? selectedTemplate.excludedTenants
+ : [],
}),
]
diff --git a/src/pages/tenant/manage/configuration-backup.js b/src/pages/tenant/manage/configuration-backup.js
index 15154dc5e258..2d839fa6466d 100644
--- a/src/pages/tenant/manage/configuration-backup.js
+++ b/src/pages/tenant/manage/configuration-backup.js
@@ -89,8 +89,8 @@ const Page = () => {
queryKey: `BackupTasks-${currentTenant}`,
});
- // Use the actual backup files as the backup data
- const filteredBackupData = Array.isArray(backupList.data) ? backupList.data : [];
+ // Use the actual backup files as the backup data — filter out any null entries
+ const filteredBackupData = Array.isArray(backupList.data) ? backupList.data.filter(Boolean) : [];
// Generate backup tags from actual API response items - use raw items directly
const generateBackupTags = (backup) => {
// Use the Items array directly from the API response without any translation
@@ -173,11 +173,12 @@ const Page = () => {
};
// Filter backup data by selected tenant if in AllTenants view
+ const selectedTenantValue = backupTenantFilter?.value ?? backupTenantFilter;
const tenantFilteredBackupData =
settings.currentTenant === "AllTenants" &&
- backupTenantFilter &&
- backupTenantFilter !== "AllTenants"
- ? filteredBackupData.filter((backup) => backup.TenantFilter === backupTenantFilter)
+ selectedTenantValue &&
+ selectedTenantValue !== "AllTenants"
+ ? filteredBackupData.filter((backup) => backup.TenantFilter === selectedTenantValue)
: filteredBackupData;
const backupDisplayItems = tenantFilteredBackupData.map((backup, index) => ({
@@ -188,7 +189,7 @@ const Page = () => {
tags: generateBackupTags(backup),
}));
- // Process existing backup configuration, find tenantFilter. by comparing settings.currentTenant with Tenant.value
+ // Process existing backup configuration
const currentConfig = Array.isArray(existingBackupConfig.data)
? existingBackupConfig.data.find(
(tenant) =>
@@ -383,8 +384,9 @@ const Page = () => {
No Backup Configuration
No backup schedule is currently configured for{" "}
- {settings.currentTenant === "AllTenants" ? "any tenant" : settings.currentTenant}.
- Click "Add Backup Schedule" to create an automated backup configuration.
+ {settings.currentTenant === "AllTenants" ? "AllTenants" : settings.currentTenant}.
+ Click "Add Backup Schedule" to create an automated backup configuration that will apply to all tenants.
+ A tenant specific backup can exist alongside a global backup, and will run according to its own schedule.
)}
diff --git a/src/pages/tenant/manage/drift.js b/src/pages/tenant/manage/drift.js
index 8c383590ed04..df2a3869dc38 100644
--- a/src/pages/tenant/manage/drift.js
+++ b/src/pages/tenant/manage/drift.js
@@ -95,6 +95,12 @@ const ManageDriftPage = () => {
queryKey: 'ListIntuneTemplates',
})
+ // API call to get all CA templates for displayName lookup
+ const caTemplatesApi = ApiGetCall({
+ url: '/api/ListCATemplates',
+ queryKey: 'ListCATemplates',
+ })
+
// API call for standards comparison (when templateId is available)
const comparisonApi = ApiGetCall({
url: '/api/ListStandardsCompare',
@@ -232,6 +238,14 @@ const ManageDriftPage = () => {
displayName = template.TemplateList.label
}
}
+ // If not found in standardSettings, look up in all CA templates (for tag templates)
+ if (!displayName && Array.isArray(caTemplatesApi.data)) {
+ const template = caTemplatesApi.data.find((t) => t.GUID === guid)
+ if (template?.displayName) {
+ displayName = template.displayName
+ }
+ }
+
// If template not found, return null to filter it out later
if (!displayName) {
return null
@@ -1362,6 +1376,7 @@ const ManageDriftPage = () => {
)
// Actions for the ActionsMenu
+ const currentDriftTemplate = standardsApi.data?.find((t) => t.GUID === templateId)
const actions = createDriftManagementActions({
templateId,
onRefresh: () => {
@@ -1375,6 +1390,12 @@ const ManageDriftPage = () => {
setTriggerReport(true)
},
currentTenant: tenantFilter,
+ templateTenants: Array.isArray(currentDriftTemplate?.tenantFilter)
+ ? currentDriftTemplate.tenantFilter
+ : [],
+ excludedTenants: Array.isArray(currentDriftTemplate?.excludedTenants)
+ ? currentDriftTemplate.excludedTenants
+ : [],
})
// Effect to trigger the ExecutiveReportButton when needed
diff --git a/src/pages/tenant/manage/driftManagementActions.js b/src/pages/tenant/manage/driftManagementActions.js
index 5d7cd8e80488..7b3f59c7bca0 100644
--- a/src/pages/tenant/manage/driftManagementActions.js
+++ b/src/pages/tenant/manage/driftManagementActions.js
@@ -1,5 +1,6 @@
-import React from "react";
-import { Edit, Sync, PlayArrow, PictureAsPdf } from "@mui/icons-material";
+import React from 'react'
+import { Edit, Sync, PlayArrow, PictureAsPdf } from '@mui/icons-material'
+import { CippFormTemplateTenantSelector } from '../../../components/CippComponents/CippFormTemplateTenantSelector.jsx'
/**
* Creates the standard drift management actions array
@@ -11,29 +12,31 @@ import { Edit, Sync, PlayArrow, PictureAsPdf } from "@mui/icons-material";
*/
export const createDriftManagementActions = ({
templateId,
- templateType = "classic",
+ templateType = 'classic',
showEditTemplate = false,
onRefresh,
onGenerateReport,
currentTenant,
+ templateTenants = [],
+ excludedTenants = [],
}) => {
const actions = [
{
- label: "Refresh Data",
+ label: 'Refresh Data',
icon: ,
noConfirm: true,
customFunction: onRefresh,
},
- ];
+ ]
// Add Generate Report action if handler is provided
if (onGenerateReport) {
actions.push({
- label: "Generate Report",
+ label: 'Generate Report',
icon: ,
noConfirm: true,
customFunction: onGenerateReport,
- });
+ })
}
// Add template-specific actions if templateId is available
@@ -41,57 +44,55 @@ export const createDriftManagementActions = ({
// Conditionally add Edit Template action
if (showEditTemplate) {
actions.push({
- label: "Edit Template",
+ label: 'Edit Template',
icon: ,
- color: "info",
+ color: 'info',
noConfirm: true,
customFunction: () => {
// Use Next.js router for internal navigation
- import("next/router")
+ import('next/router')
.then(({ default: router }) => {
router.push(
`/tenant/standards/templates/template?id=${templateId}&type=${templateType}`
- );
+ )
})
.catch(() => {
// Fallback to window.location if router is not available
- window.location.href = `/tenant/standards/templates/template?id=${templateId}&type=${templateType}`;
- });
+ window.location.href = `/tenant/standards/templates/template?id=${templateId}&type=${templateType}`
+ })
},
- });
+ })
}
- actions.push(
- {
- label: `Run Standard Now (${currentTenant || "Currently Selected Tenant"})`,
- type: "GET",
- url: "/api/ExecStandardsRun",
- icon: ,
- data: {
- TemplateId: templateId,
- },
- confirmText: "Are you sure you want to force a run of this standard?",
- multiPost: false,
+ actions.push({
+ label: 'Run Standard Now',
+ type: 'GET',
+ url: '/api/ExecStandardsRun',
+ icon: ,
+ data: {
+ TemplateId: templateId,
},
- {
- label: "Run Standard Now (All Tenants in Template)",
- type: "GET",
- url: "/api/ExecStandardsRun",
- icon: ,
- data: {
- TemplateId: templateId,
- tenantFilter: "allTenants",
- },
- confirmText: "Are you sure you want to force a run of this standard?",
- multiPost: false,
- }
- );
+ customDataformatter: (_row, _action, formData) => ({
+ TemplateId: templateId,
+ tenantFilter: formData.tenantFilter?.value ?? formData.tenantFilter,
+ }),
+ children: ({ formHook }) => (
+
+ ),
+ confirmText: 'Are you sure you want to force a run of this standard?',
+ allowResubmit: true,
+ multiPost: false,
+ })
}
- return actions;
-};
+ return actions
+}
/**
* Default export for backward compatibility
*/
-export default createDriftManagementActions;
+export default createDriftManagementActions
diff --git a/src/pages/tenant/manage/policies-deployed.js b/src/pages/tenant/manage/policies-deployed.js
index d38f68d03698..616f5a64491f 100644
--- a/src/pages/tenant/manage/policies-deployed.js
+++ b/src/pages/tenant/manage/policies-deployed.js
@@ -1,6 +1,6 @@
-import { Layout as DashboardLayout } from "../../../layouts/index.js";
-import { useRouter } from "next/router";
-import { Policy, Security, AdminPanelSettings, Devices, ExpandMore } from "@mui/icons-material";
+import { Layout as DashboardLayout } from '../../../layouts/index.js'
+import { useRouter } from 'next/router'
+import { Policy, Security, AdminPanelSettings, Devices, ExpandMore } from '@mui/icons-material'
import {
Box,
Stack,
@@ -9,34 +9,34 @@ import {
AccordionSummary,
AccordionDetails,
Chip,
-} from "@mui/material";
-import { HeaderedTabbedLayout } from "../../../layouts/HeaderedTabbedLayout";
-import tabOptions from "./tabOptions.json";
-import { CippDataTable } from "../../../components/CippTable/CippDataTable";
-import { CippHead } from "../../../components/CippComponents/CippHead";
-import { ApiGetCall } from "../../../api/ApiCall";
-import standardsData from "../../../data/standards.json";
-import { createDriftManagementActions } from "./driftManagementActions";
-import { useSettings } from "../../../hooks/use-settings";
-import { CippAutoComplete } from "../../../components/CippComponents/CippAutocomplete";
-import { useEffect } from "react";
+} from '@mui/material'
+import { HeaderedTabbedLayout } from '../../../layouts/HeaderedTabbedLayout'
+import tabOptions from './tabOptions.json'
+import { CippDataTable } from '../../../components/CippTable/CippDataTable'
+import { CippHead } from '../../../components/CippComponents/CippHead'
+import { ApiGetCall } from '../../../api/ApiCall'
+import standardsData from '../../../data/standards.json'
+import { createDriftManagementActions } from './driftManagementActions'
+import { useSettings } from '../../../hooks/use-settings'
+import { CippAutoComplete } from '../../../components/CippComponents/CippAutocomplete'
+import { useEffect } from 'react'
const PoliciesDeployedPage = () => {
- const userSettingsDefaults = useSettings();
- const router = useRouter();
- const { templateId } = router.query;
- const tenantFilter = router.query.tenantFilter || userSettingsDefaults.tenantFilter;
- const currentTenant = userSettingsDefaults.currentTenant;
+ const userSettingsDefaults = useSettings()
+ const router = useRouter()
+ const { templateId } = router.query
+ const tenantFilter = router.query.tenantFilter || userSettingsDefaults.tenantFilter
+ const currentTenant = userSettingsDefaults.currentTenant
// API call to get standards template data
const standardsApi = ApiGetCall({
- url: "/api/listStandardTemplates",
- queryKey: "ListStandardsTemplates-Drift",
- });
+ url: '/api/listStandardTemplates',
+ queryKey: 'ListStandardsTemplates-Drift',
+ })
// API call to get standards comparison data
const comparisonApi = ApiGetCall({
- url: "/api/ListStandardsCompare",
+ url: '/api/ListStandardsCompare',
data: {
TemplateId: templateId,
TenantFilter: tenantFilter,
@@ -44,66 +44,70 @@ const PoliciesDeployedPage = () => {
},
queryKey: `StandardsCompare-${templateId}-${tenantFilter}`,
enabled: !!templateId && !!tenantFilter,
- });
+ })
// API call to get drift data for deviation statuses
const driftApi = ApiGetCall({
- url: "/api/listTenantDrift",
+ url: '/api/listTenantDrift',
data: {
tenantFilter: tenantFilter,
standardsId: templateId,
},
queryKey: `TenantDrift-${templateId}-${tenantFilter}`,
enabled: !!templateId && !!tenantFilter,
- });
+ })
// API call to get all Intune templates for displayName lookup
const intuneTemplatesApi = ApiGetCall({
- url: "/api/ListIntuneTemplates",
- queryKey: "ListIntuneTemplates",
- });
+ url: '/api/ListIntuneTemplates',
+ queryKey: 'ListIntuneTemplates',
+ })
+
+ // API call to get all CA templates for displayName lookup
+ const caTemplatesApi = ApiGetCall({
+ url: '/api/ListCATemplates',
+ queryKey: 'ListCATemplates',
+ })
// Find the current template from standards data
- const currentTemplate = (standardsApi.data || []).find(
- (template) => template.GUID === templateId
- );
- const templateStandards = currentTemplate?.standards || {};
- const comparisonData = comparisonApi.data?.[0] || {};
+ const currentTemplate = (standardsApi.data || []).find((template) => template.GUID === templateId)
+ const templateStandards = currentTemplate?.standards || {}
+ const comparisonData = comparisonApi.data?.[0] || {}
// Helper function to get status from comparison data with deviation status
const getStatus = (standardKey, templateValue = null, templateType = null) => {
- const comparisonKey = `standards.${standardKey}`;
- const comparisonItem = comparisonData[comparisonKey];
- const value = comparisonItem?.Value;
+ const comparisonKey = `standards.${standardKey}`
+ const comparisonItem = comparisonData[comparisonKey]
+ const value = comparisonItem?.Value
// If value is true, it's deployed and compliant
if (value === true) {
- return "Deployed";
+ return 'Deployed'
}
// Check if ExpectedValue and CurrentValue match (like drift.js does)
if (comparisonItem?.ExpectedValue && comparisonItem?.CurrentValue) {
try {
- const expectedStr = JSON.stringify(comparisonItem.ExpectedValue);
- const currentStr = JSON.stringify(comparisonItem.CurrentValue);
+ const expectedStr = JSON.stringify(comparisonItem.ExpectedValue)
+ const currentStr = JSON.stringify(comparisonItem.CurrentValue)
if (expectedStr === currentStr) {
- return "Deployed";
+ return 'Deployed'
}
} catch (e) {
- console.error("Error comparing values:", e);
+ console.error('Error comparing values:', e)
}
}
// If value is explicitly false, it means not deployed (not a deviation)
if (value === false) {
- return "Not Deployed";
+ return 'Not Deployed'
}
// If value is null/undefined, check drift data for deviation status
- const driftData = Array.isArray(driftApi.data) ? driftApi.data : [];
+ const driftData = Array.isArray(driftApi.data) ? driftApi.data : []
// For templates, we need to match against the full template path
- let searchKeys = [standardKey, `standards.${standardKey}`];
+ let searchKeys = [standardKey, `standards.${standardKey}`]
// Add template-specific search keys
if (templateValue && templateType) {
@@ -111,7 +115,7 @@ const PoliciesDeployedPage = () => {
`standards.${templateType}.${templateValue}`,
`${templateType}.${templateValue}`,
templateValue
- );
+ )
}
const deviation = driftData.find((item) =>
@@ -122,26 +126,26 @@ const PoliciesDeployedPage = () => {
item.standardName?.includes(key) ||
item.policyName?.includes(key)
)
- );
+ )
if (deviation && deviation.Status) {
- return `Deviation - ${deviation.Status}`;
+ return `Deviation - ${deviation.Status}`
}
// Only return "Deviation - New" if we have comparison data but value is null
if (comparisonItem) {
- return "Deviation - New";
+ return 'Deviation - New'
}
- return "Not Configured";
- };
+ return 'Not Configured'
+ }
// Helper function to get display name from drift data
const getDisplayNameFromDrift = (standardKey, templateValue = null, templateType = null) => {
- const driftData = Array.isArray(driftApi.data) ? driftApi.data : [];
+ const driftData = Array.isArray(driftApi.data) ? driftApi.data : []
// For templates, we need to match against the full template path
- let searchKeys = [standardKey, `standards.${standardKey}`];
+ let searchKeys = [standardKey, `standards.${standardKey}`]
// Add template-specific search keys
if (templateValue && templateType) {
@@ -149,7 +153,7 @@ const PoliciesDeployedPage = () => {
`standards.${templateType}.${templateValue}`,
`${templateType}.${templateValue}`,
templateValue
- );
+ )
}
const deviation = driftData.find((item) =>
@@ -160,277 +164,285 @@ const PoliciesDeployedPage = () => {
item.standardName?.includes(key) ||
item.policyName?.includes(key)
)
- );
+ )
// If found in drift data, return the display name
if (deviation?.standardDisplayName) {
- return deviation.standardDisplayName;
+ return deviation.standardDisplayName
}
// If not found in drift data and this is an Intune template, look it up in the Intune templates API
- if (templateType === "IntuneTemplate" && templateValue && intuneTemplatesApi.data) {
- const template = intuneTemplatesApi.data.find((t) => t.GUID === templateValue);
+ if (templateType === 'IntuneTemplate' && templateValue && intuneTemplatesApi.data) {
+ const template = intuneTemplatesApi.data.find((t) => t.GUID === templateValue)
if (template?.Displayname) {
- return template.Displayname;
+ return template.Displayname
+ }
+ }
+
+ // If not found in drift data and this is a CA template, look it up in the CA templates API
+ if (templateType === 'ConditionalAccessTemplate' && templateValue && caTemplatesApi.data) {
+ const template = caTemplatesApi.data.find((t) => t.GUID === templateValue)
+ if (template?.displayName) {
+ return template.displayName
}
}
- return null;
- };
+ return null
+ }
// Helper function to get last refresh date
const getLastRefresh = (standardKey) => {
- const comparisonKey = `standards.${standardKey}`;
- const lastRefresh = comparisonData[comparisonKey]?.LastRefresh;
- return lastRefresh ? new Date(lastRefresh).toLocaleDateString() : "N/A";
- };
+ const comparisonKey = `standards.${standardKey}`
+ const lastRefresh = comparisonData[comparisonKey]?.LastRefresh
+ return lastRefresh ? new Date(lastRefresh).toLocaleDateString() : 'N/A'
+ }
// Helper function to get standard name from standards.json
const getStandardName = (standardKey) => {
- const standardName = `standards.${standardKey}`;
- const standard = standardsData.find((s) => s.name === standardName);
- return standard?.label || standardKey.replace(/([A-Z])/g, " $1").trim();
- };
+ const standardName = `standards.${standardKey}`
+ const standard = standardsData.find((s) => s.name === standardName)
+ return standard?.label || standardKey.replace(/([A-Z])/g, ' $1').trim()
+ }
// Helper function to get template label from standards API data
const getTemplateLabel = (templateValue, templateType) => {
- if (!templateValue || !currentTemplate) return "Unknown Template";
+ if (!templateValue || !currentTemplate) return 'Unknown Template'
// Search through all templates in the current template data
- const allTemplates = currentTemplate.standards || {};
+ const allTemplates = currentTemplate.standards || {}
// Look for the template in the specific type array
if (allTemplates[templateType] && Array.isArray(allTemplates[templateType])) {
const template = allTemplates[templateType].find(
(t) => t.TemplateList?.value === templateValue
- );
+ )
if (template?.TemplateList?.label) {
- return template.TemplateList.label;
+ return template.TemplateList.label
}
}
// If not found in the specific type, search through all template types
for (const [key, templates] of Object.entries(allTemplates)) {
if (Array.isArray(templates)) {
- const template = templates.find((t) => t.TemplateList?.value === templateValue);
+ const template = templates.find((t) => t.TemplateList?.value === templateValue)
if (template?.TemplateList?.label) {
- return template.TemplateList.label;
+ return template.TemplateList.label
}
}
}
- return "Unknown Template";
- };
+ return 'Unknown Template'
+ }
// Process Security Standards (everything NOT IntuneTemplates or ConditionalAccessTemplates)
const deployedStandards = Object.entries(templateStandards)
- .filter(([key]) => key !== "IntuneTemplate" && key !== "ConditionalAccessTemplate")
+ .filter(([key]) => key !== 'IntuneTemplate' && key !== 'ConditionalAccessTemplate')
.map(([key, value], index) => ({
id: index + 1,
name: getStandardName(key),
- category: "Security Standard",
+ category: 'Security Standard',
status: getStatus(key),
lastModified: getLastRefresh(key),
standardKey: key,
- }));
+ }))
// Process Intune Templates
- const intunePolices = [];
- (templateStandards.IntuneTemplate || []).forEach((template, index) => {
- console.log("Processing IntuneTemplate in policies-deployed:", template);
+ const intunePolices = []
+ ;(templateStandards.IntuneTemplate || []).forEach((template, index) => {
+ console.log('Processing IntuneTemplate in policies-deployed:', template)
// Check if this template has TemplateList-Tags (try both property formats)
- const templateListTags = template["TemplateList-Tags"] || template.TemplateListTags;
+ const templateListTags = template['TemplateList-Tags'] || template.TemplateListTags
// Check if this template has TemplateList-Tags and expand them
if (templateListTags?.value && templateListTags?.addedFields?.templates) {
console.log(
- "Found TemplateList-Tags for IntuneTemplate in policies-deployed:",
+ 'Found TemplateList-Tags for IntuneTemplate in policies-deployed:',
templateListTags
- );
- console.log("Templates to expand:", templateListTags.addedFields.templates);
+ )
+ console.log('Templates to expand:', templateListTags.addedFields.templates)
// Expand TemplateList-Tags into multiple template items
templateListTags.addedFields.templates.forEach((expandedTemplate, expandedIndex) => {
- console.log("Expanding IntuneTemplate in policies-deployed:", expandedTemplate);
- const standardKey = `IntuneTemplate.${expandedTemplate.GUID}`;
+ console.log('Expanding IntuneTemplate in policies-deployed:', expandedTemplate)
+ const standardKey = `IntuneTemplate.${expandedTemplate.GUID}`
const driftDisplayName = getDisplayNameFromDrift(
standardKey,
expandedTemplate.GUID,
- "IntuneTemplate"
- );
- const packageTagName = templateListTags.value;
+ 'IntuneTemplate'
+ )
+ const packageTagName = templateListTags.value
const templateName =
- expandedTemplate.displayName || expandedTemplate.name || "Unknown Template";
+ expandedTemplate.displayName || expandedTemplate.name || 'Unknown Template'
intunePolices.push({
id: intunePolices.length + 1,
name: `${driftDisplayName || templateName} (via ${packageTagName})`,
- category: "Intune Template",
- platform: "Multi-Platform",
- status: getStatus(standardKey, expandedTemplate.GUID, "IntuneTemplate"),
+ category: 'Intune Template',
+ platform: 'Multi-Platform',
+ status: getStatus(standardKey, expandedTemplate.GUID, 'IntuneTemplate'),
lastModified: getLastRefresh(standardKey),
- assignedGroups: template.AssignTo || "N/A",
+ assignedGroups: template.AssignTo || 'N/A',
templateValue: expandedTemplate.GUID,
- });
- });
+ })
+ })
} else {
// Regular TemplateList processing
- const templateGuid = template.TemplateList?.value;
- const standardKey = `IntuneTemplate.${templateGuid}`;
- const driftDisplayName = getDisplayNameFromDrift(standardKey, templateGuid, "IntuneTemplate");
+ const templateGuid = template.TemplateList?.value
+ const standardKey = `IntuneTemplate.${templateGuid}`
+ const driftDisplayName = getDisplayNameFromDrift(standardKey, templateGuid, 'IntuneTemplate')
// Try multiple fallbacks for the name
- let templateName = driftDisplayName;
+ let templateName = driftDisplayName
if (!templateName) {
- const templateLabel = getTemplateLabel(templateGuid, "IntuneTemplate");
- if (templateLabel !== "Unknown Template") {
- templateName = `Intune - ${templateLabel}`;
+ const templateLabel = getTemplateLabel(templateGuid, 'IntuneTemplate')
+ if (templateLabel !== 'Unknown Template') {
+ templateName = `Intune - ${templateLabel}`
}
}
// If still no name, try looking up directly in intuneTemplatesApi by GUID
if (!templateName && templateGuid && intuneTemplatesApi.data) {
- const intuneTemplate = intuneTemplatesApi.data.find((t) => t.GUID === templateGuid);
+ const intuneTemplate = intuneTemplatesApi.data.find((t) => t.GUID === templateGuid)
if (intuneTemplate?.Displayname) {
- templateName = intuneTemplate.Displayname;
+ templateName = intuneTemplate.Displayname
}
}
// Final fallback
if (!templateName) {
- templateName = `Intune - ${templateGuid || "Unknown Template"}`;
+ templateName = `Intune - ${templateGuid || 'Unknown Template'}`
}
intunePolices.push({
id: intunePolices.length + 1,
name: templateName,
- category: "Intune Template",
- platform: "Multi-Platform",
- status: getStatus(standardKey, templateGuid, "IntuneTemplate"),
+ category: 'Intune Template',
+ platform: 'Multi-Platform',
+ status: getStatus(standardKey, templateGuid, 'IntuneTemplate'),
lastModified: getLastRefresh(standardKey),
- assignedGroups: template.AssignTo || "N/A",
+ assignedGroups: template.AssignTo || 'N/A',
templateValue: templateGuid,
- });
+ })
}
- });
+ })
// Add any templates from comparison data that weren't in template standards (e.g., from tags)
// Check for IntuneTemplate entries in comparison data
Object.keys(comparisonData).forEach((key) => {
- if (key.startsWith("standards.IntuneTemplate.")) {
- const guid = key.replace("standards.IntuneTemplate.", "");
+ if (key.startsWith('standards.IntuneTemplate.')) {
+ const guid = key.replace('standards.IntuneTemplate.', '')
// Check if this GUID is already in our list
- const alreadyExists = intunePolices.some((p) => p.templateValue === guid);
+ const alreadyExists = intunePolices.some((p) => p.templateValue === guid)
if (!alreadyExists && comparisonData[key]?.Value === true) {
- const standardKey = `IntuneTemplate.${guid}`;
- const driftDisplayName = getDisplayNameFromDrift(standardKey, guid, "IntuneTemplate");
+ const standardKey = `IntuneTemplate.${guid}`
+ const driftDisplayName = getDisplayNameFromDrift(standardKey, guid, 'IntuneTemplate')
intunePolices.push({
id: intunePolices.length + 1,
name: driftDisplayName || `Intune - ${guid}`,
- category: "Intune Template",
- platform: "Multi-Platform",
- status: getStatus(standardKey, guid, "IntuneTemplate"),
+ category: 'Intune Template',
+ platform: 'Multi-Platform',
+ status: getStatus(standardKey, guid, 'IntuneTemplate'),
lastModified: getLastRefresh(standardKey),
- assignedGroups: "N/A",
+ assignedGroups: 'N/A',
templateValue: guid,
- });
+ })
}
}
- });
+ })
// Process Conditional Access Templates
- const conditionalAccessPolicies = [];
- (templateStandards.ConditionalAccessTemplate || []).forEach((template, index) => {
- console.log("Processing ConditionalAccessTemplate in policies-deployed:", template);
+ const conditionalAccessPolicies = []
+ ;(templateStandards.ConditionalAccessTemplate || []).forEach((template, index) => {
+ console.log('Processing ConditionalAccessTemplate in policies-deployed:', template)
// Check if this template has TemplateList-Tags (try both property formats)
- const templateListTags = template["TemplateList-Tags"] || template.TemplateListTags;
+ const templateListTags = template['TemplateList-Tags'] || template.TemplateListTags
// Check if this template has TemplateList-Tags and expand them
if (templateListTags?.value && templateListTags?.addedFields?.templates) {
console.log(
- "Found TemplateList-Tags for ConditionalAccessTemplate in policies-deployed:",
+ 'Found TemplateList-Tags for ConditionalAccessTemplate in policies-deployed:',
templateListTags
- );
- console.log("Templates to expand:", templateListTags.addedFields.templates);
+ )
+ console.log('Templates to expand:', templateListTags.addedFields.templates)
// Expand TemplateList-Tags into multiple template items
templateListTags.addedFields.templates.forEach((expandedTemplate, expandedIndex) => {
- console.log("Expanding ConditionalAccessTemplate in policies-deployed:", expandedTemplate);
- const standardKey = `ConditionalAccessTemplate.${expandedTemplate.GUID}`;
+ console.log('Expanding ConditionalAccessTemplate in policies-deployed:', expandedTemplate)
+ const standardKey = `ConditionalAccessTemplate.${expandedTemplate.GUID}`
const driftDisplayName = getDisplayNameFromDrift(
standardKey,
expandedTemplate.GUID,
- "ConditionalAccessTemplate"
- );
- const packageTagName = templateListTags.value;
+ 'ConditionalAccessTemplate'
+ )
+ const packageTagName = templateListTags.value
const templateName =
- expandedTemplate.displayName || expandedTemplate.name || "Unknown Template";
+ expandedTemplate.displayName || expandedTemplate.name || 'Unknown Template'
conditionalAccessPolicies.push({
id: conditionalAccessPolicies.length + 1,
name: `${driftDisplayName || templateName} (via ${packageTagName})`,
- state: template.state || "Unknown",
- conditions: "Conditional Access Policy",
- controls: "Access Control",
+ state: template.state || 'Unknown',
+ conditions: 'Conditional Access Policy',
+ controls: 'Access Control',
lastModified: getLastRefresh(standardKey),
- status: getStatus(standardKey, expandedTemplate.GUID, "ConditionalAccessTemplate"),
+ status: getStatus(standardKey, expandedTemplate.GUID, 'ConditionalAccessTemplate'),
templateValue: expandedTemplate.GUID,
- });
- });
+ })
+ })
} else {
// Regular TemplateList processing
- const standardKey = `ConditionalAccessTemplate.${template.TemplateList?.value}`;
+ const standardKey = `ConditionalAccessTemplate.${template.TemplateList?.value}`
const driftDisplayName = getDisplayNameFromDrift(
standardKey,
template.TemplateList?.value,
- "ConditionalAccessTemplate"
- );
+ 'ConditionalAccessTemplate'
+ )
const templateLabel = getTemplateLabel(
template.TemplateList?.value,
- "ConditionalAccessTemplate"
- );
+ 'ConditionalAccessTemplate'
+ )
conditionalAccessPolicies.push({
id: conditionalAccessPolicies.length + 1,
name: driftDisplayName || `Conditional Access - ${templateLabel}`,
- state: template.state || "Unknown",
- conditions: "Conditional Access Policy",
- controls: "Access Control",
+ state: template.state || 'Unknown',
+ conditions: 'Conditional Access Policy',
+ controls: 'Access Control',
lastModified: getLastRefresh(standardKey),
- status: getStatus(standardKey, template.TemplateList?.value, "ConditionalAccessTemplate"),
+ status: getStatus(standardKey, template.TemplateList?.value, 'ConditionalAccessTemplate'),
templateValue: template.TemplateList?.value,
- });
+ })
}
- });
+ })
// Add any CA templates from comparison data that weren't in template standards
Object.keys(comparisonData).forEach((key) => {
- if (key.startsWith("standards.ConditionalAccessTemplate.")) {
- const guid = key.replace("standards.ConditionalAccessTemplate.", "");
+ if (key.startsWith('standards.ConditionalAccessTemplate.')) {
+ const guid = key.replace('standards.ConditionalAccessTemplate.', '')
// Check if this GUID is already in our list
- const alreadyExists = conditionalAccessPolicies.some((p) => p.templateValue === guid);
+ const alreadyExists = conditionalAccessPolicies.some((p) => p.templateValue === guid)
if (!alreadyExists && comparisonData[key]?.Value === true) {
- const standardKey = `ConditionalAccessTemplate.${guid}`;
+ const standardKey = `ConditionalAccessTemplate.${guid}`
const driftDisplayName = getDisplayNameFromDrift(
standardKey,
guid,
- "ConditionalAccessTemplate"
- );
+ 'ConditionalAccessTemplate'
+ )
conditionalAccessPolicies.push({
id: conditionalAccessPolicies.length + 1,
name: driftDisplayName || `Conditional Access - ${guid}`,
- state: "Unknown",
- conditions: "Conditional Access Policy",
- controls: "Access Control",
+ state: 'Unknown',
+ conditions: 'Conditional Access Policy',
+ controls: 'Access Control',
lastModified: getLastRefresh(standardKey),
- status: getStatus(standardKey, guid, "ConditionalAccessTemplate"),
+ status: getStatus(standardKey, guid, 'ConditionalAccessTemplate'),
templateValue: guid,
- });
+ })
}
}
- });
+ })
// Simple filter for all templates (no type filtering)
const templateOptions = standardsApi.data
@@ -442,35 +454,41 @@ const PoliciesDeployedPage = () => {
`Template ${template.GUID}`,
value: template.GUID,
}))
- : [];
+ : []
// Find currently selected template
const selectedTemplateOption =
templateId && templateOptions.length
? templateOptions.find((option) => option.value === templateId) || null
- : null;
+ : null
// Effect to refetch APIs when templateId changes (needed for shallow routing)
useEffect(() => {
if (templateId) {
- comparisonApi.refetch();
- driftApi.refetch();
+ comparisonApi.refetch()
+ driftApi.refetch()
}
- }, [templateId]);
+ }, [templateId])
const actions = createDriftManagementActions({
templateId,
- templateType: currentTemplate?.type || "classic",
+ templateType: currentTemplate?.type || 'classic',
showEditTemplate: true,
onRefresh: () => {
- standardsApi.refetch();
- comparisonApi.refetch();
- driftApi.refetch();
+ standardsApi.refetch()
+ comparisonApi.refetch()
+ driftApi.refetch()
},
currentTenant,
- });
- const title = "View Deployed Policies";
- const subtitle = [];
+ templateTenants: Array.isArray(currentTemplate?.tenantFilter)
+ ? currentTemplate.tenantFilter
+ : [],
+ excludedTenants: Array.isArray(currentTemplate?.excludedTenants)
+ ? currentTemplate.excludedTenants
+ : [],
+ })
+ const title = 'View Deployed Policies'
+ const subtitle = []
return (
{
{/* Filters Section */}
-
+
{
defaultValue={selectedTemplateOption}
value={selectedTemplateOption}
onChange={(selectedTemplate) => {
- const query = { ...router.query };
+ const query = { ...router.query }
if (selectedTemplate && selectedTemplate.value) {
- query.templateId = selectedTemplate.value;
+ query.templateId = selectedTemplate.value
} else {
- delete query.templateId;
+ delete query.templateId
}
router.replace(
{
@@ -507,7 +525,7 @@ const PoliciesDeployedPage = () => {
},
undefined,
{ shallow: true }
- );
+ )
}}
sx={{ width: 300 }}
placeholder="Select template..."
@@ -528,7 +546,7 @@ const PoliciesDeployedPage = () => {
{
title="Intune Templates"
data={intunePolices}
simpleColumns={[
- "name",
- "category",
- "platform",
- "status",
- "lastModified",
- "assignedGroups",
+ 'name',
+ 'category',
+ 'platform',
+ 'status',
+ 'lastModified',
+ 'assignedGroups',
]}
noCard={true}
isFetching={
@@ -580,12 +598,12 @@ const PoliciesDeployedPage = () => {
title="Conditional Access Templates"
data={conditionalAccessPolicies}
simpleColumns={[
- "name",
- "state",
- "status",
- "conditions",
- "controls",
- "lastModified",
+ 'name',
+ 'state',
+ 'status',
+ 'conditions',
+ 'controls',
+ 'lastModified',
]}
noCard={true}
isFetching={
@@ -597,9 +615,9 @@ const PoliciesDeployedPage = () => {
- );
-};
+ )
+}
-PoliciesDeployedPage.getLayout = (page) => {page};
+PoliciesDeployedPage.getLayout = (page) => {page}
-export default PoliciesDeployedPage;
+export default PoliciesDeployedPage
diff --git a/src/pages/tenant/reports/list-licenses/index.js b/src/pages/tenant/reports/list-licenses/index.js
index 35d61ea16158..4d877df75f8f 100644
--- a/src/pages/tenant/reports/list-licenses/index.js
+++ b/src/pages/tenant/reports/list-licenses/index.js
@@ -1,24 +1,92 @@
-import { Layout as DashboardLayout } from "../../../../layouts/index.js";
-import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx";
+import { Layout as DashboardLayout } from '../../../../layouts/index.js'
+import { CippTablePage } from '../../../../components/CippComponents/CippTablePage.jsx'
+import { AssignmentInd } from '@mui/icons-material'
+import CippFormComponent from '../../../../components/CippComponents/CippFormComponent'
const Page = () => {
- const pageTitle = "Licences Report";
- const apiUrl = "/api/ListLicenses";
+ const pageTitle = 'Licences Report'
+ const apiUrl = '/api/ListLicenses'
const simpleColumns = [
- "Tenant",
- "License",
- "CountUsed",
- "CountAvailable",
- "TotalLicenses",
- "AssignedUsers",
- "AssignedGroups",
- "TermInfo", // TODO TermInfo is not showing as a clickable json object in the table, like CApolicies does in the mfa report. IDK how to fix it. -Bobby
- ];
+ 'Tenant',
+ 'License',
+ 'CountUsed',
+ 'CountAvailable',
+ 'TotalLicenses',
+ 'AssignedUsers',
+ 'AssignedGroups',
+ 'TermInfo', // TODO TermInfo is not showing as a clickable json object in the table, like CApolicies does in the mfa report. IDK how to fix it. -Bobby
+ ]
- return ;
-};
+ const actions = [
+ {
+ label: 'Assign License to User',
+ type: 'POST',
+ url: '/api/ExecBulkLicense',
+ icon: ,
+ confirmText: 'Are you sure you want to assign [License] to the selected user?',
+ multiPost: false,
+ children: ({ formHook, row }) => (
+ `${option.displayName} (${option.userPrincipalName})`,
+ valueField: 'id',
+ queryKey: `Users-${row?.Tenant}`,
+ data: {
+ Endpoint: 'users',
+ $select: 'id,displayName,userPrincipalName',
+ $count: true,
+ $orderby: 'displayName',
+ $top: 999,
+ },
+ }}
+ />
+ ),
+ customDataformatter: (row, action, formData) => ({
+ tenantFilter: row.Tenant,
+ LicenseOperation: 'Add',
+ Licenses: [{ label: row.License, value: row.skuId }],
+ userIds: [formData.userIds?.value],
+ }),
+ },
+ ]
-Page.getLayout = (page) => {page};
+ const offCanvas = {
+ extendedInfoFields: [
+ 'Tenant',
+ 'License',
+ 'CountUsed',
+ 'CountAvailable',
+ 'TotalLicenses',
+ 'AssignedUsers',
+ 'AssignedGroups',
+ 'TermInfo',
+ ],
+ actions: actions,
+ }
-export default Page;
+ return (
+
+ )
+}
+
+Page.getLayout = (page) => {page}
+
+export default Page
diff --git a/src/pages/tenant/standards/alignment/index.js b/src/pages/tenant/standards/alignment/index.js
index 45238be2960a..60c05489bc24 100644
--- a/src/pages/tenant/standards/alignment/index.js
+++ b/src/pages/tenant/standards/alignment/index.js
@@ -1,16 +1,248 @@
import { CippTablePage } from '../../../../components/CippComponents/CippTablePage.jsx'
import { Layout as DashboardLayout } from '../../../../layouts/index.js'
import { TabbedLayout } from '../../../../layouts/TabbedLayout'
+import { ApiGetCallWithPagination } from '../../../../api/ApiCall'
+import { useSettings } from '../../../../hooks/use-settings'
import { Delete, Edit } from '@mui/icons-material'
-import { EyeIcon, ListBulletIcon, ChartBarIcon } from '@heroicons/react/24/outline'
+import { EyeIcon, ListBulletIcon, ChartBarIcon, Squares2X2Icon } from '@heroicons/react/24/outline'
import tabOptions from '../tabOptions.json'
-import { useState } from 'react'
-import { Box, Chip, Divider, Stack, Tooltip, Typography } from '@mui/material'
+import { useEffect, useMemo, useState } from 'react'
+import ReactMarkdown from 'react-markdown'
+import remarkGfm from 'remark-gfm'
+import {
+ Box,
+ Chip,
+ Divider,
+ Stack,
+ ToggleButton,
+ ToggleButtonGroup,
+ Tooltip,
+ Typography,
+} from '@mui/material'
import standardsData from '../../../../data/standards.json'
+const complianceColors = {
+ compliant: 'success',
+ 'non-compliant': 'error',
+ 'accepted deviation': 'info',
+ 'customer specific': 'info',
+ 'license missing': 'warning',
+ 'reporting disabled': 'default',
+}
+
+const compliancePriority = {
+ compliant: 10,
+ 'reporting disabled': 20,
+ 'customer specific': 30,
+ 'accepted deviation': 40,
+ 'license missing': 50,
+ 'non-compliant': 60,
+}
+
+const getComplianceStatus = (status) => String(status ?? 'Unknown').trim() || 'Unknown'
+
+const getComplianceColor = (status) =>
+ complianceColors[getComplianceStatus(status).toLowerCase()] ?? 'default'
+
+const getCompliancePriority = (status) =>
+ compliancePriority[getComplianceStatus(status).toLowerCase()] ?? 0
+
+const isAlignedComplianceStatus = (status) =>
+ ['compliant', 'accepted deviation', 'customer specific'].includes(
+ getComplianceStatus(status).toLowerCase()
+ )
+
+const getPageRows = (page) => {
+ if (Array.isArray(page)) return page
+ if (Array.isArray(page?.Results)) return page.Results
+ if (Array.isArray(page?.Data)) return page.Data
+ if (Array.isArray(page?.data)) return page.data
+ if (Array.isArray(page?.value)) return page.value
+ return []
+}
+
+const getStandardInfo = (standardId) => {
+ const baseName = standardId?.split('.').slice(0, -1).join('.')
+ return (
+ standardsData.find((s) => s.name === standardId) ??
+ standardsData.find((s) => s.name === baseName)
+ )
+}
+
const Page = () => {
const pageTitle = 'Standard & Drift Alignment'
- const [granular, setGranular] = useState(false)
+ const tenant = useSettings().currentTenant
+ const [viewMode, setViewMode] = useState('summary')
+ const [byStandardTenantFilter, setByStandardTenantFilter] = useState('all')
+ const isSummary = viewMode === 'summary'
+ const isGranular = viewMode === 'granular'
+ const isByStandard = viewMode === 'byStandard'
+
+ const {
+ data: byStandardApiData,
+ fetchNextPage: fetchNextByStandardPage,
+ hasNextPage: byStandardHasNextPage,
+ isFetching: byStandardIsFetching,
+ isSuccess: byStandardIsSuccess,
+ } = ApiGetCallWithPagination({
+ url: '/api/ListTenantAlignment',
+ data: { tenantFilter: tenant, granular: 'true' },
+ queryKey: `listTenantAlignment-byStandard-source-${tenant}`,
+ waiting: isByStandard,
+ })
+
+ useEffect(() => {
+ if (isByStandard && byStandardIsSuccess && byStandardHasNextPage && !byStandardIsFetching) {
+ fetchNextByStandardPage()
+ }
+ }, [
+ byStandardApiData?.pages?.length,
+ byStandardHasNextPage,
+ byStandardIsFetching,
+ byStandardIsSuccess,
+ fetchNextByStandardPage,
+ isByStandard,
+ ])
+
+ const byStandardSourceData = useMemo(
+ () => byStandardApiData?.pages?.flatMap((page) => getPageRows(page)) ?? [],
+ [byStandardApiData]
+ )
+
+ const byStandardData = useMemo(() => {
+ const groupedStandards = new Map()
+
+ byStandardSourceData.forEach((row) => {
+ const standardKey = row.standardId || row.standardName
+ if (!standardKey) return
+
+ const standardInfo = getStandardInfo(row.standardId)
+ const hasExactMatch = standardsData.find((s) => s.name === row.standardId)
+ const standardName = hasExactMatch
+ ? (standardInfo?.label ?? row.standardName ?? standardKey)
+ : (row.standardName ?? standardInfo?.label ?? standardKey)
+
+ if (!groupedStandards.has(standardKey)) {
+ groupedStandards.set(standardKey, {
+ standardId: standardKey,
+ standardName,
+ category: standardInfo?.cat ?? 'Uncategorized',
+ standardTypes: new Set(),
+ tenants: new Map(),
+ })
+ }
+
+ const standard = groupedStandards.get(standardKey)
+ const standardType = row.standardType ?? row.templateType
+ if (standardType) standard.standardTypes.add(standardType)
+
+ const tenantKey = row.tenantFilter ?? row.tenantName ?? row.Tenant ?? 'Unknown'
+ const status = getComplianceStatus(row.complianceStatus)
+ const tenant = standard.tenants.get(tenantKey) ?? {
+ tenantFilter: tenantKey,
+ complianceStatus: status,
+ rows: [],
+ }
+
+ tenant.rows.push(row)
+ if (getCompliancePriority(status) > getCompliancePriority(tenant.complianceStatus)) {
+ tenant.complianceStatus = status
+ }
+ standard.tenants.set(tenantKey, tenant)
+ })
+
+ return Array.from(groupedStandards.values())
+ .map((standard) => {
+ const tenants = Array.from(standard.tenants.values())
+ .map((tenant) => {
+ const templateNames = [
+ ...new Set(tenant.rows.map((row) => row.templateName).filter(Boolean)),
+ ]
+ const latestDataCollection = tenant.rows
+ .map((row) => row.latestDataCollection)
+ .filter(Boolean)
+ .sort((a, b) => new Date(b) - new Date(a))[0]
+
+ return {
+ tenantFilter: tenant.tenantFilter,
+ complianceStatus: tenant.complianceStatus,
+ templateName: templateNames.join(', ') || 'N/A',
+ latestDataCollection,
+ rowCount: tenant.rows.length,
+ rows: tenant.rows,
+ }
+ })
+ .sort((a, b) => a.tenantFilter.localeCompare(b.tenantFilter))
+
+ const counts = tenants.reduce(
+ (acc, tenant) => {
+ switch (getComplianceStatus(tenant.complianceStatus).toLowerCase()) {
+ case 'compliant':
+ acc.compliantCount += 1
+ break
+ case 'non-compliant':
+ acc.nonCompliantCount += 1
+ break
+ case 'accepted deviation':
+ acc.acceptedDeviationCount += 1
+ break
+ case 'customer specific':
+ acc.customerSpecificCount += 1
+ break
+ case 'license missing':
+ acc.licenseMissingCount += 1
+ break
+ case 'reporting disabled':
+ acc.reportingDisabledCount += 1
+ break
+ default:
+ acc.otherCount += 1
+ }
+ return acc
+ },
+ {
+ compliantCount: 0,
+ nonCompliantCount: 0,
+ acceptedDeviationCount: 0,
+ customerSpecificCount: 0,
+ licenseMissingCount: 0,
+ reportingDisabledCount: 0,
+ otherCount: 0,
+ }
+ )
+
+ const totalTenants = tenants.length
+ const alignedCount =
+ counts.compliantCount + counts.acceptedDeviationCount + counts.customerSpecificCount
+ const compliancePercentage = totalTenants
+ ? Math.round((alignedCount / totalTenants) * 100)
+ : 0
+ const licenseMissingPercentage = totalTenants
+ ? Math.round((counts.licenseMissingCount / totalTenants) * 100)
+ : 0
+
+ return {
+ standardId: standard.standardId,
+ standardName: standard.standardName,
+ category: standard.category,
+ standardType: Array.from(standard.standardTypes).sort().join(', ') || 'N/A',
+ totalTenants,
+ alignedCount,
+ compliancePercentage,
+ alignmentScore: compliancePercentage,
+ LicenseMissingPercentage: licenseMissingPercentage,
+ complianceScore: `${compliancePercentage}%`,
+ summaryStatus: compliancePercentage === 100 ? 'Fully Compliant' : 'Needs Attention',
+ hasNonCompliant: counts.nonCompliantCount > 0 ? 'Yes' : 'No',
+ hasLicenseMissing: counts.licenseMissingCount > 0 ? 'Yes' : 'No',
+ hasAcceptedDeviation: counts.acceptedDeviationCount > 0 ? 'Yes' : 'No',
+ isFullyCompliant: compliancePercentage === 100 ? 'Yes' : 'No',
+ tenants,
+ ...counts,
+ }
+ })
+ .sort((a, b) => a.standardName.localeCompare(b.standardName))
+ }, [byStandardSourceData])
const summaryFilterList = [
{
@@ -53,6 +285,29 @@ const Page = () => {
},
]
+ const byStandardFilterList = [
+ {
+ filterName: 'Fully Compliant',
+ value: [{ id: 'isFullyCompliant', value: 'Yes' }],
+ type: 'column',
+ },
+ {
+ filterName: 'Has Non-Compliant',
+ value: [{ id: 'hasNonCompliant', value: 'Yes' }],
+ type: 'column',
+ },
+ {
+ filterName: 'License Missing',
+ value: [{ id: 'hasLicenseMissing', value: 'Yes' }],
+ type: 'column',
+ },
+ {
+ filterName: 'Accepted Deviation',
+ value: [{ id: 'hasAcceptedDeviation', value: 'Yes' }],
+ type: 'column',
+ },
+ ]
+
const summaryActions = [
{
label: 'View Tenant Report',
@@ -178,16 +433,7 @@ const Page = () => {
standardsData.find((s) => s.name === baseName)?.label ??
row.standardName
- const complianceColors = {
- compliant: 'success',
- 'non-compliant': 'error',
- 'accepted deviation': 'info',
- 'customer specific': 'info',
- 'license missing': 'warning',
- 'reporting disabled': 'default',
- }
- const statusColor =
- complianceColors[String(row.complianceStatus ?? '').toLowerCase()] ?? 'default'
+ const statusColor = getComplianceColor(row.complianceStatus)
const properties = [
{ label: 'Standard', value: prettyName },
@@ -434,39 +680,245 @@ const Page = () => {
},
}
+ const byStandardOffCanvas = {
+ size: 'md',
+ title: 'Standard Tenant Summary',
+ contentPadding: 0,
+ children: (row) => {
+ const standardInfo = getStandardInfo(row.standardId)
+ const properties = [
+ { label: 'Standard', value: row.standardName },
+ { label: 'Category', value: row.category },
+ { label: 'Type', value: row.standardType },
+ { label: 'Tenants', value: row.totalTenants },
+ { label: 'Compliance', value: `${row.alignmentScore}%` },
+ { label: 'Licenses Missing', value: `${row.LicenseMissingPercentage}%` },
+ ]
+ const tenants = row.tenants ?? []
+ const compliantTenants = tenants.filter((tenant) =>
+ isAlignedComplianceStatus(tenant.complianceStatus)
+ )
+ const nonCompliantTenants = tenants.filter(
+ (tenant) => !isAlignedComplianceStatus(tenant.complianceStatus)
+ )
+ const filteredTenants =
+ byStandardTenantFilter === 'compliant'
+ ? compliantTenants
+ : byStandardTenantFilter === 'nonCompliant'
+ ? nonCompliantTenants
+ : tenants
+
+ return (
+
+ }
+ sx={{ borderBottom: '1px solid', borderColor: 'divider' }}
+ >
+ {properties.map(({ label, value }) => (
+
+
+ {label}
+
+
+ {value ?? 'N/A'}
+
+
+ ))}
+
+
+ {standardInfo?.helpText && (
+
+
+ Description
+
+
+ {standardInfo.helpText}
+
+
+ )}
+
+
+
+
+ Tenant Compliance
+
+ {
+ if (newFilter !== null) setByStandardTenantFilter(newFilter)
+ }}
+ sx={{
+ alignSelf: { xs: 'flex-start', sm: 'center' },
+ '& .MuiToggleButton-root': { py: 0.25, px: 1, fontSize: '0.75rem' },
+ }}
+ >
+ All ({tenants.length})
+ Compliant ({compliantTenants.length})
+
+ Noncompliant ({nonCompliantTenants.length})
+
+
+
+ {filteredTenants.length === 0 && (
+
+ No tenants match this filter.
+
+ )}
+ {filteredTenants.map((tenant) => (
+
+
+
+
+ {tenant.tenantFilter}
+
+
+ Template: {tenant.templateName}
+
+
+
+
+
+ Last Applied:{' '}
+ {tenant.latestDataCollection
+ ? new Date(tenant.latestDataCollection).toLocaleString()
+ : 'N/A'}
+ {tenant.rowCount > 1 ? ` (${tenant.rowCount} template matches)` : ''}
+
+
+ ))}
+
+
+ )
+ },
+ }
+
const modeToggle = (
-
-
- ) : (
-
- )
- }
- label={granular ? 'Per Standard' : 'Summary'}
- onClick={() => setGranular((v) => !v)}
- color="primary"
- variant="filled"
- size="small"
- clickable
- />
-
+ {
+ if (newViewMode !== null) setViewMode(newViewMode)
+ }}
+ sx={{ '& .MuiToggleButton-root': { py: 0.25, px: 1, fontSize: '0.75rem' } }}
+ >
+
+
+
+
+ Summary
+
+
+
+
+
+
+
+ Per Standard
+
+
+
+
+
+
+
+ By Standard
+
+
+
+
)
return (
{
'standardType',
'latestDataCollection',
]
- : [
- 'tenantFilter',
- 'standardName',
- 'standardType',
- 'alignmentScore',
- 'LicenseMissingPercentage',
- 'combinedAlignmentScore',
- 'currentDeviationsCount',
- ]
+ : isByStandard
+ ? [
+ 'standardName',
+ 'category',
+ 'standardType',
+ 'totalTenants',
+ 'tenants',
+ 'compliancePercentage',
+ 'LicenseMissingPercentage',
+ 'alignedCount',
+ 'compliantCount',
+ 'nonCompliantCount',
+ 'licenseMissingCount',
+ 'acceptedDeviationCount',
+ ]
+ : [
+ 'tenantFilter',
+ 'standardName',
+ 'standardType',
+ 'alignmentScore',
+ 'LicenseMissingPercentage',
+ 'combinedAlignmentScore',
+ 'pendingDeviationsCount',
+ 'deniedDeviationsCount',
+ ]
+ }
+ queryKey={
+ isGranular
+ ? 'listTenantAlignment-granular'
+ : isByStandard
+ ? 'listTenantAlignment-byStandard'
+ : 'listTenantAlignment'
}
- queryKey={granular ? 'listTenantAlignment-granular' : 'listTenantAlignment'}
- offCanvas={granular ? granularOffCanvas : undefined}
+ offCanvas={isGranular ? granularOffCanvas : isByStandard ? byStandardOffCanvas : undefined}
+ offCanvasOnRowClick={isByStandard}
cardButton={modeToggle}
/>
)
diff --git a/src/pages/tenant/standards/templates/index.js b/src/pages/tenant/standards/templates/index.js
index b23752c50d9b..e6b82787f7db 100644
--- a/src/pages/tenant/standards/templates/index.js
+++ b/src/pages/tenant/standards/templates/index.js
@@ -1,152 +1,151 @@
-import { Alert, Button } from "@mui/material";
-import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx";
-import { Layout as DashboardLayout } from "../../../../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 { TabbedLayout } from "../../../../layouts/TabbedLayout";
-import Link from "next/link";
-import { CopyAll, Delete, PlayArrow, AddBox, Edit, GitHub, ContentCopy } from "@mui/icons-material";
-import { ApiGetCall, ApiPostCall } from "../../../../api/ApiCall";
-import { Grid } from "@mui/system";
-import { CippApiResults } from "../../../../components/CippComponents/CippApiResults";
-import { EyeIcon } from "@heroicons/react/24/outline";
-import tabOptions from "../tabOptions.json";
-import { useSettings } from "../../../../hooks/use-settings.js";
-import { CippPolicyImportDrawer } from "../../../../components/CippComponents/CippPolicyImportDrawer.jsx";
-import { PermissionButton } from "../../../../utils/permissions.js";
+import { Alert, Button } from '@mui/material'
+import { CippTablePage } from '../../../../components/CippComponents/CippTablePage.jsx'
+import { Layout as DashboardLayout } from '../../../../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 { TabbedLayout } from '../../../../layouts/TabbedLayout'
+import Link from 'next/link'
+import { CopyAll, Delete, PlayArrow, AddBox, Edit, GitHub, ContentCopy } from '@mui/icons-material'
+import { ApiGetCall, ApiPostCall } from '../../../../api/ApiCall'
+import { Grid } from '@mui/system'
+import { CippApiResults } from '../../../../components/CippComponents/CippApiResults'
+import { EyeIcon } from '@heroicons/react/24/outline'
+import tabOptions from '../tabOptions.json'
+import { CippPolicyImportDrawer } from '../../../../components/CippComponents/CippPolicyImportDrawer.jsx'
+import { PermissionButton } from '../../../../utils/permissions.js'
+import { CippFormTemplateTenantSelector } from '../../../../components/CippComponents/CippFormTemplateTenantSelector.jsx'
const Page = () => {
- const oldStandards = ApiGetCall({ url: "/api/ListStandards", queryKey: "ListStandards-legacy" });
+ const oldStandards = ApiGetCall({ url: '/api/ListStandards', queryKey: 'ListStandards-legacy' })
const integrations = ApiGetCall({
- url: "/api/ListExtensionsConfig",
- queryKey: "Integrations",
+ url: '/api/ListExtensionsConfig',
+ queryKey: 'Integrations',
refetchOnMount: false,
refetchOnReconnect: false,
- });
+ })
- const currentTenant = useSettings().currentTenant;
- const pageTitle = "Templates";
- const cardButtonPermissions = ["Tenant.Standards.ReadWrite"];
+ const pageTitle = 'Templates'
+ const cardButtonPermissions = ['Tenant.Standards.ReadWrite']
const actions = [
{
- label: "View Tenant Report",
- link: "/tenant/manage/applied-standards/?templateId=[GUID]",
+ label: 'View Tenant Report',
+ link: '/tenant/manage/applied-standards/?templateId=[GUID]',
icon: ,
- color: "info",
- target: "_self",
+ color: 'info',
+ target: '_self',
},
{
- label: "Edit Template",
+ label: 'Edit Template',
//when using a link it must always be the full path /identity/administration/users/[id] for example.
- link: "/tenant/standards/templates/template?id=[GUID]&type=[type]",
+ link: '/tenant/standards/templates/template?id=[GUID]&type=[type]',
icon: ,
- color: "success",
- target: "_self",
+ color: 'success',
+ target: '_self',
},
{
- label: "Clone & Edit Template",
- link: "/tenant/standards/templates/template?id=[GUID]&clone=true&type=[type]",
+ label: 'Clone & Edit Template',
+ link: '/tenant/standards/templates/template?id=[GUID]&clone=true&type=[type]',
icon: ,
- color: "success",
- target: "_self",
+ color: 'success',
+ target: '_self',
},
{
- label: "Create Drift Clone",
- type: "POST",
- url: "/api/ExecDriftClone",
+ label: 'Create Drift Clone',
+ type: 'POST',
+ url: '/api/ExecDriftClone',
icon: ,
- color: "warning",
+ color: 'warning',
data: {
- id: "GUID",
+ id: 'GUID',
},
confirmText:
- "Are you sure you want to create a drift clone of [templateName]? This will create a new drift template based on this template.",
+ 'Are you sure you want to create a drift clone of [templateName]? This will create a new drift template based on this template.',
multiPost: false,
},
{
- label: `Run Template Now (${currentTenant || "Currently Selected Tenant"})`,
- type: "GET",
- url: "/api/ExecStandardsRun",
+ label: 'Run Template Now',
+ type: 'GET',
+ url: '/api/ExecStandardsRun',
icon: ,
data: {
- TemplateId: "GUID",
+ TemplateId: 'GUID',
},
- confirmText: "Are you sure you want to force a run of this template?",
+ allowResubmit: true,
+ customDataformatter: (row, action, formData) => ({
+ TemplateId: row.GUID,
+ tenantFilter: formData.tenantFilter?.value ?? formData.tenantFilter,
+ }),
+ children: ({ formHook, row }) => (
+
+ ),
+ confirmText: 'Are you sure you want to force a run of this template?',
multiPost: false,
},
{
- label: "Run Template Now (All Tenants in Template)",
- type: "GET",
- url: "/api/ExecStandardsRun",
- icon: ,
- data: {
- TemplateId: "GUID",
- tenantFilter: "allTenants",
- },
- confirmText: "Are you sure you want to force a run of this template?",
- multiPost: false,
- },
- {
- label: "Save to GitHub",
- type: "POST",
- url: "/api/ExecCommunityRepo",
+ label: 'Save to GitHub',
+ type: 'POST',
+ url: '/api/ExecCommunityRepo',
icon: ,
data: {
- Action: "UploadTemplate",
- GUID: "GUID",
+ Action: 'UploadTemplate',
+ GUID: 'GUID',
},
fields: [
{
- label: "Repository",
- name: "FullName",
- type: "select",
+ label: 'Repository',
+ name: 'FullName',
+ type: 'select',
api: {
- url: "/api/ListCommunityRepos",
+ url: '/api/ListCommunityRepos',
data: {
WriteAccess: true,
},
- queryKey: "CommunityRepos-Write",
- dataKey: "Results",
- valueField: "FullName",
- labelField: "FullName",
+ queryKey: 'CommunityRepos-Write',
+ dataKey: 'Results',
+ valueField: 'FullName',
+ labelField: 'FullName',
},
multiple: false,
creatable: false,
required: true,
validators: {
- required: { value: true, message: "This field is required" },
+ required: { value: true, message: 'This field is required' },
},
},
{
- label: "Commit Message",
- placeholder: "Enter a commit message for adding this file to GitHub",
- name: "Message",
- type: "textField",
+ label: 'Commit Message',
+ placeholder: 'Enter a commit message for adding this file to GitHub',
+ name: 'Message',
+ type: 'textField',
multiline: true,
required: true,
rows: 4,
},
],
- confirmText: "Are you sure you want to save this template to the selected repository?",
+ confirmText: 'Are you sure you want to save this template to the selected repository?',
condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled,
},
{
- label: "Delete Template",
- type: "POST",
- url: "/api/RemoveStandardTemplate",
+ label: 'Delete Template',
+ type: 'POST',
+ url: '/api/RemoveStandardTemplate',
icon: ,
data: {
- ID: "GUID",
+ ID: 'GUID',
},
- confirmText: "Are you sure you want to delete [templateName]?",
+ confirmText: 'Are you sure you want to delete [templateName]?',
multiPost: false,
},
- ];
- const conversionApi = ApiPostCall({ relatedQueryKeys: "listStandardTemplates" });
+ ]
+ const conversionApi = ApiPostCall({ relatedQueryKeys: 'listStandardTemplates' })
const handleConversion = () => {
conversionApi.mutate({
- url: "/api/execStandardConvert",
+ url: '/api/execStandardConvert',
data: {},
- });
- };
+ })
+ }
const tableFilter = (
{oldStandards.isSuccess && oldStandards.data.length !== 0 && (
@@ -154,7 +153,7 @@ const Page = () => {
You have legacy standards available. Press the button to convert these standards to
@@ -163,7 +162,7 @@ const Page = () => {
they are correct and re-enable the schedule.
- handleConversion()} variant={"contained"}>
+ handleConversion()} variant={'contained'}>
Convert Legacy Standards
@@ -175,7 +174,7 @@ const Page = () => {
)}
- );
+ )
return (
{
actions={actions}
tableFilter={tableFilter}
simpleColumns={[
- "templateName",
- "type",
- "tenantFilter",
- "excludedTenants",
- "updatedAt",
- "updatedBy",
- "runManually",
- "standards",
+ 'templateName',
+ 'type',
+ 'tenantFilter',
+ 'excludedTenants',
+ 'updatedAt',
+ 'updatedBy',
+ 'runManually',
+ 'standards',
]}
queryKey="listStandardTemplates"
/>
- );
-};
+ )
+}
Page.getLayout = (page) => (
{page}
-);
+)
-export default Page;
+export default Page
diff --git a/src/pages/tenant/standards/templates/template.jsx b/src/pages/tenant/standards/templates/template.jsx
index 19cf27c788f2..630fdee6f2ce 100644
--- a/src/pages/tenant/standards/templates/template.jsx
+++ b/src/pages/tenant/standards/templates/template.jsx
@@ -1,205 +1,205 @@
-import { Box, Button, Container, Stack, Typography, SvgIcon, Skeleton } from "@mui/material";
-import { Grid } from "@mui/system";
-import { Layout as DashboardLayout } from "../../../../layouts/index.js";
-import { useForm, useWatch } from "react-hook-form";
-import { useRouter } from "next/router";
-import { Add, SaveRounded } from "@mui/icons-material";
-import { useEffect, useState, useCallback, useMemo, useRef, lazy, Suspense } from "react";
-import standards from "../../../../data/standards";
-import CippStandardAccordion from "../../../../components/CippStandards/CippStandardAccordion";
+import { Box, Button, Container, Stack, Typography, SvgIcon, Skeleton } from '@mui/material'
+import { Grid } from '@mui/system'
+import { Layout as DashboardLayout } from '../../../../layouts/index.js'
+import { useForm, useWatch } from 'react-hook-form'
+import { useRouter } from 'next/router'
+import { Add, SaveRounded } from '@mui/icons-material'
+import { useEffect, useState, useCallback, useMemo, useRef, lazy, Suspense } from 'react'
+import standards from '../../../../data/standards'
+import CippStandardAccordion from '../../../../components/CippStandards/CippStandardAccordion'
// Lazy load the dialog to improve initial page load performance
const CippStandardDialog = lazy(
- () => import("../../../../components/CippStandards/CippStandardDialog"),
-);
-import CippStandardsSideBar from "../../../../components/CippStandards/CippStandardsSideBar";
-import { ArrowLeftIcon } from "@mui/x-date-pickers";
-import { useDialog } from "../../../../hooks/use-dialog";
-import { ApiGetCall } from "../../../../api/ApiCall";
-import _ from "lodash";
-import { createDriftManagementActions } from "../../manage/driftManagementActions";
-import { ActionsMenu } from "../../../../components/actions-menu";
-import { useSettings } from "../../../../hooks/use-settings";
-import { CippHead } from "../../../../components/CippComponents/CippHead";
+ () => import('../../../../components/CippStandards/CippStandardDialog')
+)
+import CippStandardsSideBar from '../../../../components/CippStandards/CippStandardsSideBar'
+import { ArrowLeftIcon } from '@mui/x-date-pickers'
+import { useDialog } from '../../../../hooks/use-dialog'
+import { ApiGetCall } from '../../../../api/ApiCall'
+import _ from 'lodash'
+import { createDriftManagementActions } from '../../manage/driftManagementActions'
+import { ActionsMenu } from '../../../../components/actions-menu'
+import { useSettings } from '../../../../hooks/use-settings'
+import { CippHead } from '../../../../components/CippComponents/CippHead'
const Page = () => {
- const router = useRouter();
- const [editMode, setEditMode] = useState(false);
- const formControl = useForm({ mode: "onBlur" });
- const { formState } = formControl;
- const [dialogOpen, setDialogOpen] = useState(false);
- const [expanded, setExpanded] = useState(null);
- const [searchQuery, setSearchQuery] = useState("");
- const [selectedStandards, setSelectedStandards] = useState({});
- const [updatedAt, setUpdatedAt] = useState(false);
- const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
- const [currentStep, setCurrentStep] = useState(0);
- const [hasDriftConflict, setHasDriftConflict] = useState(false);
- const initialStandardsRef = useRef({});
-
- const currentTenant = useSettings().currentTenant;
+ const router = useRouter()
+ const [editMode, setEditMode] = useState(false)
+ const formControl = useForm({ mode: 'onBlur' })
+ const { formState } = formControl
+ const [dialogOpen, setDialogOpen] = useState(false)
+ const [expanded, setExpanded] = useState(null)
+ const [searchQuery, setSearchQuery] = useState('')
+ const [selectedStandards, setSelectedStandards] = useState({})
+ const [updatedAt, setUpdatedAt] = useState(false)
+ const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false)
+ const [currentStep, setCurrentStep] = useState(0)
+ const [hasDriftConflict, setHasDriftConflict] = useState(false)
+ const initialStandardsRef = useRef({})
+
+ const currentTenant = useSettings().currentTenant
// Check if this is drift mode
- const isDriftMode = router.query.type === "drift";
+ const isDriftMode = router.query.type === 'drift'
// Set drift mode flag in form when in drift mode
useEffect(() => {
if (isDriftMode) {
- formControl.setValue("isDriftTemplate", true);
+ formControl.setValue('isDriftTemplate', true)
}
- }, [isDriftMode, formControl]);
+ }, [isDriftMode, formControl])
// Watch form values to check valid configuration
- const watchForm = useWatch({ control: formControl.control });
+ const watchForm = useWatch({ control: formControl.control })
const existingTemplate = ApiGetCall({
url: `/api/listStandardTemplates`,
data: { id: router.query.id },
queryKey: `listStandardTemplates-${router.query.id}`,
waiting: editMode,
- });
+ })
// Check if the template configuration is valid and update currentStep
useEffect(() => {
const stepsStatus = {
- step1: !!_.get(watchForm, "templateName"),
- step2: _.get(watchForm, "tenantFilter", []).length > 0,
+ step1: !!_.get(watchForm, 'templateName'),
+ step2: _.get(watchForm, 'tenantFilter', []).length > 0,
step3: Object.keys(selectedStandards).length > 0,
step4:
- _.get(watchForm, "standards") &&
+ _.get(watchForm, 'standards') &&
Object.keys(selectedStandards).length > 0 &&
Object.keys(selectedStandards).every((standardName) => {
- const standardValues = _.get(watchForm, standardName, {});
+ const standardValues = _.get(watchForm, standardName, {})
// Always require an action value which should be an array with at least one element
- const actionValue = _.get(standardValues, "action");
- return actionValue && (!Array.isArray(actionValue) || actionValue.length > 0);
+ const actionValue = _.get(standardValues, 'action')
+ return actionValue && (!Array.isArray(actionValue) || actionValue.length > 0)
}),
- };
+ }
- const completedSteps = Object.values(stepsStatus).filter(Boolean).length;
- setCurrentStep(completedSteps);
- }, [selectedStandards, watchForm, isDriftMode]);
+ const completedSteps = Object.values(stepsStatus).filter(Boolean).length
+ setCurrentStep(completedSteps)
+ }, [selectedStandards, watchForm, isDriftMode])
// Handle route change events
const handleRouteChange = useCallback(
(url) => {
if (hasUnsavedChanges) {
const confirmLeave = window.confirm(
- "You have unsaved changes. Are you sure you want to leave this page?",
- );
+ 'You have unsaved changes. Are you sure you want to leave this page?'
+ )
if (!confirmLeave) {
- router.events.emit("routeChangeError");
- throw "Route change was aborted";
+ router.events.emit('routeChangeError')
+ throw 'Route change was aborted'
}
}
},
- [hasUnsavedChanges, router],
- );
+ [hasUnsavedChanges, router]
+ )
// Handle browser back/forward navigation or tab close
useEffect(() => {
const handleBeforeUnload = (e) => {
if (hasUnsavedChanges) {
- e.preventDefault();
- e.returnValue = "You have unsaved changes. Are you sure you want to leave this page?";
- return e.returnValue;
+ e.preventDefault()
+ e.returnValue = 'You have unsaved changes. Are you sure you want to leave this page?'
+ return e.returnValue
}
- };
+ }
// Add event listeners
- window.addEventListener("beforeunload", handleBeforeUnload);
- router.events.on("routeChangeStart", handleRouteChange);
+ window.addEventListener('beforeunload', handleBeforeUnload)
+ router.events.on('routeChangeStart', handleRouteChange)
// Remove event listeners on cleanup
return () => {
- window.removeEventListener("beforeunload", handleBeforeUnload);
- router.events.off("routeChangeStart", handleRouteChange);
- };
- }, [hasUnsavedChanges, handleRouteChange, router.events]);
+ window.removeEventListener('beforeunload', handleBeforeUnload)
+ router.events.off('routeChangeStart', handleRouteChange)
+ }
+ }, [hasUnsavedChanges, handleRouteChange, router.events])
// Track form changes
useEffect(() => {
// Compare the current form values with the initial values to check for real changes
- const currentValues = formControl.getValues();
- const initialValues = initialStandardsRef.current;
+ const currentValues = formControl.getValues()
+ const initialValues = initialStandardsRef.current
if (
formState.isDirty ||
JSON.stringify(selectedStandards) !== JSON.stringify(initialStandardsRef.current)
) {
- setHasUnsavedChanges(true);
+ setHasUnsavedChanges(true)
} else {
- setHasUnsavedChanges(false);
+ setHasUnsavedChanges(false)
}
- }, [formState.isDirty, selectedStandards, formControl]);
+ }, [formState.isDirty, selectedStandards, formControl])
useEffect(() => {
if (router.query.id) {
- setEditMode(true);
+ setEditMode(true)
}
if (existingTemplate.isSuccess) {
//formControl.reset(existingTemplate.data?.[0]);
- const apiData = existingTemplate.data?.[0];
+ const apiData = existingTemplate.data?.[0]
Object.keys(apiData.standards).forEach((key) => {
if (Array.isArray(apiData.standards[key])) {
apiData.standards[key] = apiData.standards[key].filter(
- (value) => value !== null && value !== undefined,
- );
+ (value) => value !== null && value !== undefined
+ )
}
- });
+ })
- formControl.reset(apiData);
+ formControl.reset(apiData)
if (router.query.clone) {
- formControl.setValue("templateName", `${apiData.templateName} (Clone)`);
- formControl.setValue("GUID", "");
+ formControl.setValue('templateName', `${apiData.templateName} (Clone)`)
+ formControl.setValue('GUID', '')
}
//set the updated at date and user
setUpdatedAt({
date: apiData?.updatedAt,
user: apiData?.updatedBy,
- });
+ })
// Transform standards from the API to match the format for selectedStandards
- const standardsFromApi = apiData?.standards;
- const transformedStandards = {};
+ const standardsFromApi = apiData?.standards
+ const transformedStandards = {}
Object.keys(standardsFromApi).forEach((key) => {
if (Array.isArray(standardsFromApi[key])) {
standardsFromApi[key].forEach((_, index) => {
- transformedStandards[`standards.${key}[${index}]`] = true;
- });
+ transformedStandards[`standards.${key}[${index}]`] = true
+ })
} else {
- transformedStandards[`standards.${key}`] = true;
+ transformedStandards[`standards.${key}`] = true
}
- });
+ })
- setSelectedStandards(transformedStandards);
+ setSelectedStandards(transformedStandards)
// Store initial state for change detection
- initialStandardsRef.current = { ...transformedStandards };
- setHasUnsavedChanges(false);
+ initialStandardsRef.current = { ...transformedStandards }
+ setHasUnsavedChanges(false)
}
- }, [existingTemplate.isSuccess, router]);
+ }, [existingTemplate.isSuccess, router])
// Memoize categories to avoid unnecessary recalculations
const categories = useMemo(() => {
return standards.reduce((acc, standard) => {
- const { cat } = standard;
+ const { cat } = standard
if (!acc[cat]) {
- acc[cat] = [];
+ acc[cat] = []
}
- acc[cat].push(standard);
- return acc;
- }, {});
- }, []);
+ acc[cat].push(standard)
+ return acc
+ }, {})
+ }, [])
const handleOpenDialog = useCallback(() => {
- setDialogOpen(true);
- }, []);
+ setDialogOpen(true)
+ }, [])
const handleCloseDialog = useCallback(() => {
- setDialogOpen(false);
- setSearchQuery("");
- }, []);
+ setDialogOpen(false)
+ setSearchQuery('')
+ }, [])
const filterStandards = (standardsList) =>
standardsList.filter(
@@ -207,149 +207,161 @@ const Page = () => {
standard.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
standard.helpText.toLowerCase().includes(searchQuery.toLowerCase()) ||
(standard.tag &&
- standard.tag.some((tag) => tag.toLowerCase().includes(searchQuery.toLowerCase()))),
- );
+ standard.tag.some((tag) => tag.toLowerCase().includes(searchQuery.toLowerCase()))) ||
+ (standard.appliesToTest &&
+ standard.appliesToTest.some((testId) =>
+ testId.toLowerCase().includes(searchQuery.toLowerCase())
+ ))
+ )
const handleToggleStandard = (standardName) => {
setSelectedStandards((prev) => ({
...prev,
[standardName]: !prev[standardName],
- }));
- };
+ }))
+ }
const handleAddMultipleStandard = (standardName) => {
//if the standardname contains an array qualifier,e.g standardName[0], strip that away.
- const arrayPattern = /(.*)\[(\d+)\]$/;
- const match = standardName.match(arrayPattern);
+ const arrayPattern = /(.*)\[(\d+)\]$/
+ const match = standardName.match(arrayPattern)
if (match) {
- standardName = match[1];
+ standardName = match[1]
}
setSelectedStandards((prev) => {
- const existingInstances = Object.keys(prev).filter((name) => name.startsWith(standardName));
- const newIndex = existingInstances.length;
+ const existingInstances = Object.keys(prev).filter((name) => name.startsWith(standardName))
+ const newIndex = existingInstances.length
return {
...prev,
[`${standardName}[${newIndex}]`]: true,
- };
- });
- };
+ }
+ })
+ }
const handleRemoveStandard = (standardName) => {
- const arrayPattern = /(.*)\[(\d+)\]$/;
- const match = standardName.match(arrayPattern);
+ const arrayPattern = /(.*)\[(\d+)\]$/
+ const match = standardName.match(arrayPattern)
if (match) {
- const baseName = match[1];
- const removedIndex = parseInt(match[2]);
+ const baseName = match[1]
+ const removedIndex = parseInt(match[2])
// Remove the item from the form array
- const currentArray = formControl.getValues(baseName) || [];
- const updatedArray = currentArray.filter((_, i) => i !== removedIndex);
- formControl.setValue(baseName, updatedArray);
+ const currentArray = formControl.getValues(baseName) || []
+ const updatedArray = currentArray.filter((_, i) => i !== removedIndex)
+ formControl.setValue(baseName, updatedArray)
// Re-index selectedStandards to keep indices contiguous
- const escapedBaseName = baseName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
- const reindexPattern = new RegExp(`^${escapedBaseName}\\[(\\d+)\\]$`);
+ const escapedBaseName = baseName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+ const reindexPattern = new RegExp(`^${escapedBaseName}\\[(\\d+)\\]$`)
setSelectedStandards((prev) => {
- const newSelected = {};
+ const newSelected = {}
Object.keys(prev).forEach((key) => {
- const keyMatch = key.match(reindexPattern);
+ const keyMatch = key.match(reindexPattern)
if (keyMatch) {
- const idx = parseInt(keyMatch[1]);
+ const idx = parseInt(keyMatch[1])
if (idx < removedIndex) {
- newSelected[key] = prev[key];
+ newSelected[key] = prev[key]
} else if (idx > removedIndex) {
// Shift higher indices down by 1
- newSelected[`${baseName}[${idx - 1}]`] = prev[key];
+ newSelected[`${baseName}[${idx - 1}]`] = prev[key]
}
// Skip the removed index
} else {
- newSelected[key] = prev[key];
+ newSelected[key] = prev[key]
}
- });
- return newSelected;
- });
+ })
+ return newSelected
+ })
} else {
setSelectedStandards((prev) => {
- const newSelected = { ...prev };
- delete newSelected[standardName];
- return newSelected;
- });
- formControl.unregister(standardName);
+ const newSelected = { ...prev }
+ delete newSelected[standardName]
+ return newSelected
+ })
+ formControl.unregister(standardName)
}
- };
+ }
const handleAccordionToggle = (standardName) => {
- setExpanded((prev) => (prev === standardName ? null : standardName));
- };
+ setExpanded((prev) => (prev === standardName ? null : standardName))
+ }
- const createDialog = useDialog();
+ const createDialog = useDialog()
// Save action that will open the create dialog
const handleSave = () => {
- createDialog.handleOpen();
+ createDialog.handleOpen()
// Will be set to false after successful save in the dialog component
- };
+ }
// Determine if save button should be disabled based on configuration
const isSaveDisabled = isDriftMode
- ? !_.get(watchForm, "tenantFilter") ||
- !_.get(watchForm, "tenantFilter").length ||
+ ? !_.get(watchForm, 'tenantFilter') ||
+ !_.get(watchForm, 'tenantFilter').length ||
currentStep < 4 ||
hasDriftConflict // For drift mode, require all steps and no drift conflicts
- : !_.get(watchForm, "tenantFilter") ||
- !_.get(watchForm, "tenantFilter").length ||
- currentStep < 4;
+ : !_.get(watchForm, 'tenantFilter') ||
+ !_.get(watchForm, 'tenantFilter').length ||
+ currentStep < 4
// Create drift management actions (excluding refresh)
const driftActions = useMemo(() => {
- if (!editMode || !router.query.id) return [];
+ if (!editMode || !router.query.id) return []
const allActions = createDriftManagementActions({
templateId: router.query.id,
- onRefresh: () => {}, // Empty function since we're filtering out refresh
+ onRefresh: () => {},
currentTenant: currentTenant,
- });
+ templateTenants: Array.isArray(watchForm?.tenantFilter) ? watchForm.tenantFilter : [],
+ excludedTenants: Array.isArray(watchForm?.excludedTenants) ? watchForm.excludedTenants : [],
+ })
// Filter out the refresh action
- return allActions.filter((action) => action.label !== "Refresh Data");
- }, [editMode, router.query.id, currentTenant]);
+ return allActions.filter((action) => action.label !== 'Refresh Data')
+ }, [
+ editMode,
+ router.query.id,
+ currentTenant,
+ watchForm?.tenantFilter,
+ watchForm?.excludedTenants,
+ ])
- const actions = [];
+ const actions = []
const steps = [
- "Set a name for the Template",
- "Assigned Template to Tenants",
- "Added Standards to Template",
- "Configured all Standards",
- ];
+ 'Set a name for the Template',
+ 'Assigned Template to Tenants',
+ 'Added Standards to Template',
+ 'Configured all Standards',
+ ]
const handleSafeNavigation = (url) => {
if (hasUnsavedChanges) {
const confirmLeave = window.confirm(
- "You have unsaved changes. Are you sure you want to leave this page?",
- );
+ 'You have unsaved changes. Are you sure you want to leave this page?'
+ )
if (confirmLeave) {
- router.push(url);
+ router.push(url)
}
} else {
- router.push(url);
+ router.push(url)
}
- };
+ }
return (
-
+
@@ -364,11 +376,11 @@ const Page = () => {
{editMode
? isDriftMode
- ? "Edit Drift Template"
- : "Edit Standards Template"
+ ? 'Edit Drift Template'
+ : 'Edit Standards Template'
: isDriftMode
- ? "Add Drift Template"
- : "Add Standards Template"}
+ ? 'Add Drift Template'
+ : 'Add Standards Template'}
{
-
-
+
+
{/* Left Column for Accordions */}
-
+
{
onDriftConflictChange={setHasDriftConflict}
onSaveSuccess={() => {
// Reset unsaved changes flag
- setHasUnsavedChanges(false);
+ setHasUnsavedChanges(false)
// Update reference for future change detection
- initialStandardsRef.current = { ...selectedStandards };
+ initialStandardsRef.current = { ...selectedStandards }
}}
/>
-
+
{/* Show accordions based on selectedStandards (which is populated by API when editing) */}
{existingTemplate.isLoading ? (
@@ -462,9 +474,9 @@ const Page = () => {
)}
- );
-};
+ )
+}
-Page.getLayout = (page) => {page};
+Page.getLayout = (page) => {page}
-export default Page;
+export default Page
diff --git a/src/pages/tenant/tools/geoiplookup/index.js b/src/pages/tenant/tools/geoiplookup/index.js
index 162e93a3b5a1..8f6daa37ad3e 100644
--- a/src/pages/tenant/tools/geoiplookup/index.js
+++ b/src/pages/tenant/tools/geoiplookup/index.js
@@ -102,9 +102,9 @@ const Page = () => {
name="ipAddress"
type="textField"
validators={{
- validate: (value) => getCippValidator(value, "ip"),
+ validate: (value) => getCippValidator(value, "ipAny"),
}}
- placeholder="Enter IP Address"
+ placeholder="Enter IP Address (IPv4 or IPv6)"
required
/>
diff --git a/src/pages/tools/custom-tests/add.jsx b/src/pages/tools/custom-tests/add.jsx
index df7fe5f1cd50..e4da31a623c5 100644
--- a/src/pages/tools/custom-tests/add.jsx
+++ b/src/pages/tools/custom-tests/add.jsx
@@ -7,6 +7,8 @@ import {
Typography,
Box,
Button,
+ Chip,
+ Collapse,
Dialog,
DialogTitle,
DialogContent,
@@ -16,6 +18,8 @@ import {
AccordionDetails,
CircularProgress,
Divider,
+ IconButton,
+ Tooltip,
} from '@mui/material'
import { useEffect, useMemo, useRef, useState } from 'react'
import { Stack, Grid } from '@mui/system'
@@ -23,19 +27,20 @@ import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import {
ExpandMore,
- CheckCircleOutline,
- ErrorOutline,
NotificationsActive,
Code,
TableChart,
+ Visibility,
} from '@mui/icons-material'
import cacheTypes from '../../../data/CIPPDBCacheTypes.json'
import { renderCustomScriptMarkdownTemplate } from '../../../utils/customScriptTemplate'
import { useSettings } from '../../../hooks/use-settings'
import CippFormPage from '../../../components/CippFormPages/CippFormPage'
import CippFormComponent from '../../../components/CippComponents/CippFormComponent'
+import { CippFormCondition } from '../../../components/CippComponents/CippFormCondition'
import { CippApiResults } from '../../../components/CippComponents/CippApiResults'
import { CippCodeBlock } from '../../../components/CippComponents/CippCodeBlock'
+import { markdownStyles } from '../../../components/CippTestDetail/CippTestDetailOffCanvas'
const Page = () => {
const getValueType = (value) => {
@@ -89,12 +94,14 @@ const Page = () => {
const { ScriptGuid } = router.query
const isEdit = !!ScriptGuid
const [cacheTypesDialogOpen, setCacheTypesDialogOpen] = useState(false)
+ const [expandedCacheType, setExpandedCacheType] = useState(null)
const [testResults, setTestResults] = useState(null)
const [guidanceExpanded, setGuidanceExpanded] = useState(true)
const [configExpanded, setConfigExpanded] = useState(true)
const [scriptContentExpanded, setScriptContentExpanded] = useState(true)
const [testerExpanded, setTesterExpanded] = useState(true)
const markdownEditorRef = useRef(null)
+ const scriptEditorRef = useRef(null)
const toSelectOption = (value, fallback) =>
value
@@ -111,7 +118,9 @@ const Page = () => {
ScriptContent: '',
Enabled: false,
AlertOnFailure: false,
+ AlertStatuses: [{ value: 'Failed', label: 'Failed' }],
ReturnType: 'JSON',
+ ResultMode: { value: 'Auto', label: 'Auto' },
MarkdownTemplate: '',
Description: '',
Category: { value: 'General', label: 'General' },
@@ -139,7 +148,14 @@ const Page = () => {
ScriptContent: script.ScriptContent || '',
Enabled: script.Enabled || false,
AlertOnFailure: script.AlertOnFailure || false,
+ AlertStatuses: script.AlertStatuses
+ ? (typeof script.AlertStatuses === 'string'
+ ? JSON.parse(script.AlertStatuses)
+ : script.AlertStatuses
+ ).map((s) => ({ value: s, label: s }))
+ : [{ value: 'Failed', label: 'Failed' }],
ReturnType: script.ReturnType || 'JSON',
+ ResultMode: toSelectOption(script.ResultMode, 'Auto'),
MarkdownTemplate: script.MarkdownTemplate || '',
ResultSchema: script.ResultSchema || '',
Category: toSelectOption(script.Category, 'General'),
@@ -160,6 +176,27 @@ const Page = () => {
setTesterExpanded(true)
}, [isEdit])
+ const cacheExplorerTenant = router.query.tenantFilter || settings?.currentTenant
+
+ const variablesQuery = ApiGetCall({
+ url: `/api/ListCustomVariables?tenantFilter=${encodeURIComponent(cacheExplorerTenant || '')}`,
+ queryKey: `CustomVariables-${cacheExplorerTenant || 'global'}`,
+ waiting: !!cacheExplorerTenant,
+ staleTime: Infinity,
+ refetchOnMount: false,
+ })
+
+ const cacheExplorerApi = ApiGetCall({
+ url: '/api/ListDBCache',
+ data: { tenantFilter: cacheExplorerTenant, type: expandedCacheType },
+ queryKey: `CacheExplorer-${cacheExplorerTenant}-${expandedCacheType}`,
+ waiting: !!expandedCacheType && !!cacheExplorerTenant,
+ })
+
+ const handleExploreCache = (cacheType) => {
+ setExpandedCacheType(expandedCacheType === cacheType ? null : cacheType)
+ }
+
const testScriptApi = ApiPostCall({
urlFromData: true,
onResult: (result) => {
@@ -178,6 +215,8 @@ const Page = () => {
return
}
+ setTestResults(null)
+
let parsedParams = {}
const rawParams = formControl.getValues('TestParameters')
@@ -222,7 +261,11 @@ const Page = () => {
ScriptContent: data.ScriptContent,
Enabled: data.Enabled,
AlertOnFailure: data.AlertOnFailure,
+ AlertStatuses: data.AlertOnFailure
+ ? (data.AlertStatuses?.map(s => s.value) || ['Failed'])
+ : [],
ReturnType: data.ReturnType,
+ ResultMode: data.ResultMode?.value ?? data.ResultMode,
MarkdownTemplate: data.MarkdownTemplate,
ResultSchema: data.ResultSchema,
Description: data.Description,
@@ -275,6 +318,13 @@ const Page = () => {
{ value: 'Markdown', label: 'Markdown' },
]
+ const resultModeOptions = [
+ { value: 'Auto', label: 'Auto' },
+ { value: 'AlwaysPass', label: 'Always Pass' },
+ { value: 'AlwaysInfo', label: 'Always Info' },
+ { value: 'AlwaysInvestigate', label: 'Always Investigate' },
+ ]
+
const scriptNameField = {
name: 'ScriptName',
label: 'Script Name',
@@ -362,6 +412,20 @@ const Page = () => {
'When enabled, a failed test triggers an alert routed to your configured notification channels (email, webhook, or PSA).',
}
+ const alertStatusesField = {
+ name: 'AlertStatuses',
+ label: 'Alert on Status',
+ type: 'autoComplete',
+ multiple: true,
+ options: [
+ { label: 'Failed', value: 'Failed' },
+ { label: 'Passed', value: 'Passed' },
+ { label: 'Info', value: 'Info' },
+ { label: 'Investigate', value: 'Investigate' },
+ ],
+ helperText: 'Choose which test result statuses trigger an alert.',
+ }
+
const returnTypeField = {
name: 'ReturnType',
label: 'Result Display Type',
@@ -370,7 +434,21 @@ const Page = () => {
placeholder: 'Select how test results are rendered',
options: returnTypeOptions,
creatable: false,
- helperText: 'Choose how failed test results are rendered in CIPP test details.',
+ helperText:
+ 'Controls the default display when no CIPPResultMarkdown is returned by the script. If the script returns CIPPResultMarkdown, it takes priority over this setting.',
+ }
+
+ const resultModeField = {
+ name: 'ResultMode',
+ label: 'Result Mode',
+ type: 'autoComplete',
+ required: true,
+ multiple: false,
+ placeholder: 'Select result mode',
+ options: resultModeOptions,
+ creatable: false,
+ helperText:
+ 'Auto: script output determines pass/fail. Always Pass: result is always Passed. Always Info: result is always Info.',
}
const markdownTemplateField = {
@@ -404,29 +482,18 @@ All UPNs: {{join(Result[*].UserPrincipalName, ", ")}}`,
required: true,
multiline: true,
rows: 22,
- placeholder: `# Example: Find disabled users with licenses
-param($TenantFilter, $DaysThreshold = 30)
-
-$users = Get-CIPPTestData -TenantFilter $TenantFilter -Type 'Users'
-$results = $users | Where-Object {
- $_.assignedLicenses.Count -gt 0 -and
- $_.accountEnabled -eq $false
-} | ForEach-Object {
- [PSCustomObject]@{
- UserPrincipalName = $_.userPrincipalName
- DisplayName = $_.displayName
- Message = "User has license but is disabled"
- }
-}
-
-# Return is optional; pipeline output is also captured.
-return $results`,
disableVariables: true,
}
const selectedReturnType = formControl.watch('ReturnType')
const markdownTemplateValue = formControl.watch('MarkdownTemplate')
const resultSchemaValue = formControl.watch('ResultSchema')
+ const watchedScriptContent = formControl.watch('ScriptContent')
+
+ const hasTenantFilterParam = useMemo(() => {
+ if (!watchedScriptContent) return false
+ return /-TenantFilter\b/i.test(watchedScriptContent)
+ }, [watchedScriptContent])
const markdownAutocompleteOptions = useMemo(() => {
const suggestionsMap = new Map()
@@ -467,6 +534,71 @@ return $results`,
return Array.from(suggestionsMap.values())
}, [resultSchemaValue])
+ const handleScriptEditorMount = (_editor, monaco) => {
+ scriptEditorRef.current = _editor
+
+ const provider = monaco.languages.registerCompletionItemProvider('powershell', {
+ triggerCharacters: ['%'],
+ provideCompletionItems: (model, position) => {
+ const linePrefix = model.getValueInRange({
+ startLineNumber: position.lineNumber,
+ startColumn: 1,
+ endLineNumber: position.lineNumber,
+ endColumn: position.column,
+ })
+
+ const triggerIndex = linePrefix.lastIndexOf('%')
+ if (triggerIndex === -1) {
+ return { suggestions: [] }
+ }
+
+ const range = {
+ startLineNumber: position.lineNumber,
+ startColumn: triggerIndex + 1,
+ endLineNumber: position.lineNumber,
+ endColumn: position.column,
+ }
+
+ const vars = variablesQuery.data?.Results || []
+ const suggestions = vars.map((v) => ({
+ label: v.Variable,
+ kind: monaco.languages.CompletionItemKind.Variable,
+ insertText: v.Variable,
+ detail: v.Type === 'reserved' ? `Built-in (${v.Category})` : `Custom (${v.Category})`,
+ documentation: v.Description || '',
+ range,
+ }))
+
+ return { suggestions }
+ },
+ })
+
+ const contentListener = _editor.onDidChangeModelContent(() => {
+ const model = _editor.getModel()
+ const position = _editor.getPosition()
+ if (!model || !position) return
+
+ const linePrefix = model.getValueInRange({
+ startLineNumber: position.lineNumber,
+ startColumn: 1,
+ endLineNumber: position.lineNumber,
+ endColumn: position.column,
+ })
+
+ if (linePrefix.endsWith('%')) {
+ _editor.trigger('cipp-variables', 'editor.action.triggerSuggest', {})
+ }
+ })
+
+ _editor.onDidDispose(() => {
+ provider.dispose()
+ contentListener.dispose()
+ if (scriptEditorRef.current === _editor) {
+ scriptEditorRef.current = null
+ }
+ })
+ }
+
const handleMarkdownEditorMount = (_editor, monaco) => {
markdownEditorRef.current = _editor
@@ -566,103 +698,159 @@ return $results`,
}}
>
- Custom tests run PowerShell against each tenant. The script output determines pass or
- fail.
+ Custom tests run PowerShell against each tenant. The script output determines the
+ result status.
-
-
+
+
-
-
- Pass
+
+
+
+ Pass
+
+
+ Return $null, $false, empty string, or{' '}
+ @()
+
+
+
+
+
+ Fail
+
+
+ Return any non-empty value — the returned data becomes the test output
+
+
-
- Return $null, $false, an empty string, or{' '}
- @()
-
-
+
-
-
- Fail
-
+
+ Explicit Status
+
- Return any non-empty value — object, array, string, or $true. The
- returned data becomes the test output.
+ Return a hashtable with CIPPStatus (Passed/
+ Failed/Info/Investigate),{' '}
+ CIPPResults, and optional{' '}
+ CIPPResultMarkdown to control status and rendering directly (Auto
+ result mode only)
-
-
-
+
-
-
- Alerts
+
+
+
+ Alerts
+
-
- Enable "Notify on Alert" to create alerts on failure. Deduplicated per tenant
- per day, then routed to email, webhook, or PSA via Alert Configuration.
+
+ Enable "Notify on Alert" for failure alerts, deduplicated per tenant
+ per day.
-
-
- Scripting Rules
+
+
+
+ Scripting Rules
+
-
- PowerShell AST allowlist — only approved cmdlets (ForEach-Object, Where-Object,
- Select-Object, etc.). The += operator is blocked.{' '}
- $TenantFilter is available automatically.
+
+ AST allowlist — approved cmdlets only. += is blocked. Data access
+ is automatically tenant-locked — do not pass{' '}
+ -TenantFilter. Type % in the editor for replacement
+ variables.
-
-
- Data Access
+
+
+
+ Data Access
+
-
- Read-only via Get-CIPPTestData and Get-CIPPDbItem{' '}
- with a -Type parameter.
+
+ Read-only via Get-CIPPTestData with -Type.{' '}
+ Tenant is auto-locked — do not pass -TenantFilter. Use{' '}
+ %variable% syntax for replacement variables.
setCacheTypesDialogOpen(true)}
+ sx={{ mt: 0.5 }}
>
View Cached Types ({cacheTypes.length})
@@ -670,37 +858,380 @@ return $results`,
-
+
Manual testing on this page is preview-only. Results are persisted only during
scheduled tenant test runs with the script enabled.
+
+
+
+
+ Example Scripts
+
+
+
+ }>
+
+ Licensed Users with Resolved SKU Names
+
+
+
+
+ Lists all users with licenses, resolves SKU IDs to friendly names using the
+ license cache, and returns a markdown table with an explicit Passed status.
+ Demonstrates CIPPStatus, CIPPResults, and{' '}
+ CIPPResultMarkdown.
+
+ display name lookup hashtable
+$SkuLookup = @{}
+$Licenses | ForEach-Object {
+ $SkuLookup[$_.skuId] = $_.License
+}
+
+# Build results - users with their resolved license names
+$results = $Users | Where-Object {
+ $_.assignedLicenses.Count -gt 0
+} | ForEach-Object {
+ $user = $_
+ $licenseNames = @($user.assignedLicenses | ForEach-Object {
+ $name = $SkuLookup[$_.skuId]
+ if ($name) { $name } else { $_.skuId }
+ })
+ [PSCustomObject]@{
+ UserPrincipalName = $user.userPrincipalName
+ DisplayName = $user.displayName
+ AccountEnabled = $user.accountEnabled
+ LicenseCount = $licenseNames.Count
+ Licenses = $licenseNames -join ', '
+ }
+}
+
+# Build markdown table
+$header = "### Licensed Users: $($results.Count)\\n\\n| User | Display Name | Enabled | Licenses |\\n|---|---|---|---|"
+$rows = $results | ForEach-Object {
+ "| $($_.UserPrincipalName) | $($_.DisplayName) | $($_.AccountEnabled) | $($_.Licenses) |"
+}
+$md = @($header) + @($rows) -join "\\n"
+
+# Return with explicit pass + markdown
+@{
+ CIPPStatus = 'Passed'
+ CIPPResults = $results
+ CIPPResultMarkdown = $md
+}`}
+ language="powershell"
+ showLineNumbers={true}
+ />
+
+
+
+
+ }>
+
+ Disabled Users with Active Licenses
+
+
+
+
+ Finds disabled accounts that still have licenses assigned — a common cost waste
+ indicator. Returns failed rows as JSON (default Result Display Type behavior). No
+ wrapper needed — non-empty output automatically means fail.
+
+
+
+
+
+
+ }>
+
+ MFA Registration Gaps
+
+
+
+
+ Checks user registration details for accounts that haven't registered any MFA
+ method. Uses Info status so results are always informational rather
+ than a hard fail.
+
+
+
+
+
+
+ }>
+
+ Stale Guest Accounts
+
+
+
+
+ Identifies guest accounts that haven't signed in within 90 days. Uses a{' '}
+ param with a default so the threshold is configurable via Test
+ Parameters. Simple auto-detection — empty result = pass, non-empty = fail.
+
+
+
+
+
+
+ }>
+
+ Conditional Access Policy Summary
+
+
+
+
+ Provides an informational summary of all Conditional Access policies grouped by
+ state. Demonstrates using Group-Object, building a multi-section
+ markdown report, and %tenantname% replacement variables. Always
+ passes since it's informational.
+
+
+
+
@@ -766,6 +1297,13 @@ return $results`,
disabled={isScriptLoading}
/>
+
+
+
+
+
+
+
+
@@ -858,6 +1410,7 @@ return $results`,
showLineNumbers={true}
editorHeight="540px"
readOnly={isScriptLoading}
+ onMount={handleScriptEditorMount}
onChange={(value) => field.onChange(value || '')}
/>
{scriptContentField.placeholder}
+
+ Type % to insert replacement variables (e.g.{' '}
+ %tenantid%, %defaultdomain%, or custom variables).
+
+ {hasTenantFilterParam && (
+
+ -TenantFilter is not needed — data access functions are
+ automatically locked to the execution tenant. Remove{' '}
+ -TenantFilter $TenantFilter from your calls.
+
+ )}
{fieldState.error?.message && (
{fieldState.error.message}
@@ -940,12 +1508,39 @@ return $results`,
)}
- {testResults?.Results !== undefined && (
+ {(testResults?.Results !== undefined || testResults?.CIPPResultMarkdown) && (
-
- Test Results
-
- {selectedReturnType === 'Markdown' ? (
+
+ Test Results
+ {testResults?.CIPPStatus && (
+
+ )}
+
+ {testResults?.CIPPResultMarkdown ? (
+
+
+ {testResults.CIPPResultMarkdown.replace(/\\n/g, '\n')}
+
+
+ ) : selectedReturnType === 'Markdown' ? (
{
const pageTitle = "Custom Tests";
+ const [importDialogOpen, setImportDialogOpen] = useState(false);
+ const [selectedRepo, setSelectedRepo] = useState(null);
+ const [selectedBranch, setSelectedBranch] = useState(null);
+
+ const integrations = ApiGetCall({
+ url: "/api/ListExtensionsConfig",
+ queryKey: "Integrations",
+ refetchOnMount: false,
+ refetchOnReconnect: false,
+ });
+
+ const branchQuery = ApiGetCall({
+ url: "/api/ExecGitHubAction",
+ data: { Action: "GetBranches", FullName: selectedRepo },
+ queryKey: `${selectedRepo}-branches`,
+ waiting: !!selectedRepo,
+ });
+
+ const fileTreeQuery = ApiGetCall({
+ url: "/api/ExecGitHubAction",
+ data: {
+ Action: "GetFileTree",
+ FullName: selectedRepo,
+ Branch: selectedBranch,
+ },
+ queryKey: `${selectedRepo}-${selectedBranch}-filetree-customtests`,
+ waiting: !!selectedRepo && !!selectedBranch,
+ });
+
+ const importScriptApi = ApiPostCall({
+ relatedQueryKeys: ["Custom Tests"],
+ urlFromData: true,
+ });
+
+ const scriptFiles = (fileTreeQuery.data?.Results || []).filter(
+ (f) => f.path?.endsWith(".json") && f.path?.startsWith("CustomTests/")
+ );
+
+ const handleImportClose = () => {
+ setImportDialogOpen(false);
+ setSelectedRepo(null);
+ setSelectedBranch(null);
+ };
const simpleColumns = [
"ScriptName",
"Description",
"Enabled",
"AlertOnFailure",
+ "ResultMode",
"ReturnType",
"Category",
"Pillar",
@@ -35,18 +103,33 @@ const Page = () => {
title={pageTitle}
queryKey="Custom Tests"
cardButton={
-
-
-
-
- Add Test
-
+
+ {integrations.isSuccess && integrations?.data?.GitHub?.Enabled && (
+ setImportDialogOpen(true)}
+ >
+
+
+
+ Import from GitHub
+
+ )}
+
+
+
+
+ Add Test
+
+
}
tenantInTitle={false}
apiUrl="/api/ListCustomScripts"
@@ -126,8 +209,150 @@ const Page = () => {
confirmText:
"Are you sure you want to delete the test '[ScriptName]'? This will permanently delete ALL versions of this script.",
},
+ {
+ label: "Save to GitHub",
+ type: "POST",
+ url: "/api/ExecCommunityRepo",
+ icon: ,
+ data: {
+ Action: "UploadScript",
+ GUID: "ScriptGuid",
+ },
+ fields: [
+ {
+ label: "Repository",
+ name: "FullName",
+ type: "select",
+ api: {
+ url: "/api/ListCommunityRepos",
+ data: { WriteAccess: true },
+ queryKey: "CommunityRepos-Write",
+ dataKey: "Results",
+ valueField: "FullName",
+ labelField: "FullName",
+ },
+ multiple: false,
+ creatable: false,
+ required: true,
+ validators: {
+ required: { value: true, message: "This field is required" },
+ },
+ },
+ {
+ label: "Commit Message",
+ placeholder: "Enter a commit message for adding this script to GitHub",
+ name: "Message",
+ type: "textField",
+ multiline: true,
+ required: true,
+ rows: 4,
+ },
+ ],
+ confirmText:
+ "Are you sure you want to save '[ScriptName]' to the selected repository?",
+ condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled,
+ },
]}
/>
+
+
>
);
};
diff --git a/src/utils/get-cipp-column-size.js b/src/utils/get-cipp-column-size.js
index a62c579b8d48..d24c5d549b8c 100644
--- a/src/utils/get-cipp-column-size.js
+++ b/src/utils/get-cipp-column-size.js
@@ -15,6 +15,8 @@ export const getCippColumnSize = (accessorKey, header) => {
switch (accessorKey) {
case 'alignmentScore':
case 'combinedAlignmentScore':
+ case 'compliancePercentage':
+ case 'complianceScore':
case 'LicenseMissingPercentage':
case 'ScorePercentage':
return { size: 250, minSize: 250 }
diff --git a/src/utils/get-cipp-formatting.js b/src/utils/get-cipp-formatting.js
index 268c0f40b953..5580ab4b5de0 100644
--- a/src/utils/get-cipp-formatting.js
+++ b/src/utils/get-cipp-formatting.js
@@ -11,6 +11,7 @@ import {
BarChart,
} from '@mui/icons-material'
import { Chip, Link, SvgIcon, Tooltip } from '@mui/material'
+import NextLink from 'next/link'
import { alpha } from '@mui/material/styles'
import { Box } from '@mui/system'
import { CippCopyToClipBoard } from '../components/CippComponents/CippCopyToClipboard'
@@ -259,7 +260,11 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr
return isText ? data : data
}
- if (cellName === 'alignmentScore' || cellName === 'combinedAlignmentScore') {
+ if (
+ cellName === 'alignmentScore' ||
+ cellName === 'combinedAlignmentScore' ||
+ cellName === 'compliancePercentage'
+ ) {
// Handle alignment score, return a percentage with a label
return isText ? (
`${data}%`
@@ -269,7 +274,8 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr
}
if (cellName === 'currentDeviationsCount') {
- if (data === undefined || data === null) return isText ? 'N/A' :
+ if (data === undefined || data === null)
+ return isText ? 'N/A' :
const count = Number(data)
const color = count > 0 ? 'warning' : 'success'
const label = count > 0 ? `${count} Deviation${count !== 1 ? 's' : ''}` : 'None'
@@ -990,17 +996,12 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr
}
// ISO 8601 Duration Formatting
- // Add property names here to automatically format ISO 8601 duration strings (e.g., "PT1H23M30S")
- // into human-readable format (e.g., "1 hour 23 minutes 30 seconds") across all CIPP tables.
- // This works for any API response property that contains ISO 8601 duration format.
- const durationArray = [
- 'autoExtendDuration', // GDAP page (/tenant/gdap-management/relationships)
- 'deploymentDuration', // AutoPilot deployments (/endpoint/reports/autopilot-deployment)
- 'deploymentTotalDuration', // AutoPilot deployments (/endpoint/reports/autopilot-deployment)
- 'deviceSetupDuration', // AutoPilot deployments (/endpoint/reports/autopilot-deployment)
- 'accountSetupDuration', // AutoPilot deployments (/endpoint/reports/autopilot-deployment)
- ]
- if (durationArray.includes(cellName)) {
+ // Any property whose name ends in "Duration" is auto-formatted from ISO 8601 (e.g. "PT1H23M30S")
+ // into human-readable form (e.g. "1 hour 23 minutes 30 seconds") across all CIPP tables.
+ // The try/catch below handles same-suffixed fields that are not actually ISO 8601.
+ // Add explicit entries below for fields that don't follow the *Duration naming convention.
+ const durationArray = []
+ if (durationArray.includes(cellName) || cellName.endsWith('Duration')) {
isoDuration.setLocales(
{
en,
@@ -1009,8 +1010,26 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr
fallbackLocale: 'en',
}
)
- const duration = isoDuration(data)
- return duration.humanize('en')
+ try {
+ const duration = isoDuration(data)
+ const formattedDuration = duration.humanize('en')
+ if (formattedDuration) {
+ return formattedDuration
+ }
+ } catch {
+ // Fall through to the default formatter when a Duration-suffixed field is not ISO 8601.
+ }
+ }
+
+ // Internal CIPP navigation links
+ if ((cellName === 'cippLink') && typeof data === 'string') {
+ return isText ? (
+ data
+ ) : (
+
+ View
+
+ )
}
//if string starts with http, return a link
diff --git a/src/utils/get-cipp-validator.js b/src/utils/get-cipp-validator.js
index f5541e0dc25c..b6cff111b8a3 100644
--- a/src/utils/get-cipp-validator.js
+++ b/src/utils/get-cipp-validator.js
@@ -19,6 +19,12 @@ export const getCippValidator = (value, type) => {
return typeof value === "string" || "This is not a valid string";
case "ip":
return /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(value) || "This is not a valid IP address";
+ case "ipAny":
+ return (
+ /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(value) ||
+ /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))$/.test(value) ||
+ "This is not a valid IPv4 or IPv6 address"
+ );
case "ipv4cidr":
return /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}\/([0-9]|[12][0-9]|3[0-2])$/.test(value) || "This is not a valid IPv4 CIDR";
case "ipv6":
diff --git a/src/utils/intune-bind-helpers.js b/src/utils/intune-bind-helpers.js
new file mode 100644
index 000000000000..6ff61ff0d5bf
--- /dev/null
+++ b/src/utils/intune-bind-helpers.js
@@ -0,0 +1,14 @@
+// Parsers for Intune Admin Template @odata.bind refs (e.g. `groupPolicyDefinitions('GUID')`).
+// Shared so the hook and CippJSONView renderer can't drift.
+
+export const definitionBindPattern = /groupPolicyDefinitions\('([0-9a-f-]{36})'\)/i
+export const presentationBindPattern = /presentations\('([0-9a-f-]{36})'\)/i
+
+export const extractBindGuid = (value, pattern) => {
+ if (typeof value !== 'string') {
+ return null
+ }
+
+ const match = value.match(pattern)
+ return match?.[1]?.toLowerCase() || null
+}
diff --git a/yarn.lock b/yarn.lock
index 717cf1e70089..847ef311f58e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2107,10 +2107,10 @@
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.96.2.tgz#766dab253476afd0b27959b66abb606d8d2dd9f5"
integrity sha512-hzI6cTVh4KNRk8UtoIBS7Lv9g6BnJPXvBKsvYH1aGWvv0347jT3BnSvztOE+kD76XGvZnRC/t6qdW1CaIfwCeA==
-"@tanstack/query-devtools@5.93.0":
- version "5.93.0"
- resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.93.0.tgz#517f61d4e2cfb9af671e34ad5e7e871052bca814"
- integrity sha512-+kpsx1NQnOFTZsw6HAFCW3HkKg0+2cepGtAWXjiiSOJJ1CtQpt72EE2nyZb+AjAbLRPoeRmPJ8MtQd8r8gsPdg==
+"@tanstack/query-devtools@5.96.2":
+ version "5.96.2"
+ resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.96.2.tgz#6301662b95d4a7a8b9b53e53d3a9091ae45e4d25"
+ integrity sha512-vBTB1Qhbm3nHSbEUtQwks/EdcAtFfEapr1WyBW4w2ExYKuXVi3jIxUIHf5MlSltiHuL7zNyUuanqT/7sI2sb6g==
"@tanstack/query-persist-client-core@5.92.4":
version "5.92.4"
@@ -2119,6 +2119,13 @@
dependencies:
"@tanstack/query-core" "5.91.2"
+"@tanstack/query-persist-client-core@5.96.2":
+ version "5.96.2"
+ resolved "https://registry.yarnpkg.com/@tanstack/query-persist-client-core/-/query-persist-client-core-5.96.2.tgz#65ea5a2104a85a2f39ef1a007f6f0ad63fbf1c49"
+ integrity sha512-BYsP8folbvxzZsNnWJxSenEAdepGNfv809150U78D84yt/THi33EwfUCcdKWFbma5XKwlaFQGWMJKeWnVJ6GVA==
+ dependencies:
+ "@tanstack/query-core" "5.96.2"
+
"@tanstack/query-sync-storage-persister@^5.90.25":
version "5.90.27"
resolved "https://registry.yarnpkg.com/@tanstack/query-sync-storage-persister/-/query-sync-storage-persister-5.90.27.tgz#249c055565e31e0587c2b1900b0d7e0012982dd3"
@@ -2127,19 +2134,19 @@
"@tanstack/query-core" "5.91.2"
"@tanstack/query-persist-client-core" "5.92.4"
-"@tanstack/react-query-devtools@^5.51.11":
- version "5.91.3"
- resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.91.3.tgz#0f65340fa3f7e7d5575de928ad70cfa6b5f74ff1"
- integrity sha512-nlahjMtd/J1h7IzOOfqeyDh5LNfG0eULwlltPEonYy0QL+nqrBB+nyzJfULV+moL7sZyxc2sHdNJki+vLA9BSA==
+"@tanstack/react-query-devtools@^5.96.2":
+ version "5.96.2"
+ resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.96.2.tgz#b44a19f1ebdb45a3e59fcf4e4361ff37500f382c"
+ integrity sha512-nTFKLGuTOFvmFRvcyZ3ArWC/DnMNPoBh6h/2yD6rsf7TCTJCQt+oUWOp2uKPTIuEPtF/vN9Kw5tl5mD1Kbposw==
dependencies:
- "@tanstack/query-devtools" "5.93.0"
+ "@tanstack/query-devtools" "5.96.2"
-"@tanstack/react-query-persist-client@^5.76.0":
- version "5.90.27"
- resolved "https://registry.yarnpkg.com/@tanstack/react-query-persist-client/-/react-query-persist-client-5.90.27.tgz#6bf177ea728eec30df50d87f4151dfac8aeaf4f0"
- integrity sha512-rKiCZ2C0kzmyDoLfrPHz2UdEDKHo/oXkKVRbhgtHya/bWH6jWDFX5cSFc1SLB33FDrgR8uOG1MwVohBrI4+F8A==
+"@tanstack/react-query-persist-client@^5.96.2":
+ version "5.96.2"
+ resolved "https://registry.yarnpkg.com/@tanstack/react-query-persist-client/-/react-query-persist-client-5.96.2.tgz#b47d62fc990a9fd38ddcf4a080d1300ae887e5a0"
+ integrity sha512-smQ38oVPlnvkG+G7R60IAD9X6azJLRjHEd7twml9XBLYM31ncPDP0tUKy/Gv/4ItVmKTtjZ5VabXpVZxnaWSww==
dependencies:
- "@tanstack/query-persist-client-core" "5.92.4"
+ "@tanstack/query-persist-client-core" "5.96.2"
"@tanstack/react-query@^5.96.2":
version "5.96.2"
@@ -2779,10 +2786,10 @@
"@typescript-eslint/types" "8.57.1"
eslint-visitor-keys "^5.0.0"
-"@uiw/react-json-view@^2.0.0-alpha.41":
- version "2.0.0-alpha.41"
- resolved "https://registry.yarnpkg.com/@uiw/react-json-view/-/react-json-view-2.0.0-alpha.41.tgz#54425c948175df5fd2155fa22a12cfb023f98773"
- integrity sha512-botRpQ5AgymYEsqXSdT2/1LefAJEYfMntvdnx1SqhTQCTW9HygeFZXx9inkYqUmiQZ3+0QlZnodjBvwnUfZhVA==
+"@uiw/react-json-view@^2.0.0-alpha.42":
+ version "2.0.0-alpha.42"
+ resolved "https://registry.yarnpkg.com/@uiw/react-json-view/-/react-json-view-2.0.0-alpha.42.tgz#0830cfa6767debb621c10ff71201c2302605c096"
+ integrity sha512-PY7IF+zL3gYaW/FG3th0w6JG2SpkYqh/UZOgKm2XuY/UpCZ5inWlopR+pfRadRz/k/uTaOhsQa9jZnlp8QBJDA==
"@ungap/structured-clone@^1.0.0", "@ungap/structured-clone@^1.2.0":
version "1.3.0"
@@ -6295,10 +6302,10 @@ ms@^2.1.1, ms@^2.1.3:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
-mui-tiptap@^1.29.1:
- version "1.29.1"
- resolved "https://registry.yarnpkg.com/mui-tiptap/-/mui-tiptap-1.29.1.tgz#eaf54adebc4af14f55b55cb21ef91347b5074343"
- integrity sha512-FyOILZSirwYxXs+WJTVs/3QmycCTDJQ5rkBaZkkgtzskbF8fgCIzKUtuTk7bKRE17aWi+VRoGDAu/VVO4Xp6IA==
+mui-tiptap@^1.30.0:
+ version "1.30.0"
+ resolved "https://registry.yarnpkg.com/mui-tiptap/-/mui-tiptap-1.30.0.tgz#91257ebb32b12241fe27b24ded42804a3abe2c51"
+ integrity sha512-BVgv9JstoNsk1SudQuIGV58N7GHlWSdItc8Yxa2BXZ6GFjJ1q1QLFoD6mALRrsKEhxpRfkRHrGaOLlZ5KkO2cQ==
dependencies:
clsx "^2.1.1"
encodeurl "^2.0.0"
@@ -7234,10 +7241,10 @@ readable-stream@~1.0.17, readable-stream@~1.0.27-1:
isarray "0.0.1"
string_decoder "~0.10.x"
-recharts@^3.7.0:
- version "3.8.0"
- resolved "https://registry.yarnpkg.com/recharts/-/recharts-3.8.0.tgz#461025818cbb858e7ff2e5820b67c6143e9b418d"
- integrity sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ==
+recharts@^3.8.1:
+ version "3.8.1"
+ resolved "https://registry.yarnpkg.com/recharts/-/recharts-3.8.1.tgz#1784b14784dab9a27eb426c475e6a9187f14cf01"
+ integrity sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==
dependencies:
"@reduxjs/toolkit" "^1.9.0 || 2.x.x"
clsx "^2.1.1"