diff --git a/package-lock.json b/package-lock.json
index b5803ad11042..d2279b2b7eec 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "cipp",
- "version": "4.5.5",
+ "version": "5.2.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "cipp",
- "version": "4.5.5",
+ "version": "5.2.1",
"license": "AGPL-3.0",
"dependencies": {
"@coreui/chartjs": "^3.0.0",
@@ -14,11 +14,11 @@
"@coreui/react": "^4.11.0",
"@coreui/react-chartjs": "^2.1.3",
"@coreui/utils": "^1.3.1",
- "@fortawesome/fontawesome-svg-core": "^1.2.36",
- "@fortawesome/free-brands-svg-icons": "^5.15.4",
- "@fortawesome/free-regular-svg-icons": "^5.15.4",
- "@fortawesome/free-solid-svg-icons": "^5.15.4",
- "@fortawesome/react-fontawesome": "^0.1.16",
+ "@fortawesome/fontawesome-svg-core": "^6.5.1",
+ "@fortawesome/free-brands-svg-icons": "^6.5.1",
+ "@fortawesome/free-regular-svg-icons": "^6.5.1",
+ "@fortawesome/free-solid-svg-icons": "^6.5.1",
+ "@fortawesome/react-fontawesome": "^0.2.0",
"@monaco-editor/react": "^4.5.2",
"@popperjs/core": "^2.10.2",
"@reduxjs/toolkit": "^1.9.7",
@@ -1166,72 +1166,72 @@
"integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q=="
},
"node_modules/@fortawesome/fontawesome-common-types": {
- "version": "0.2.36",
- "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz",
- "integrity": "sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg==",
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz",
+ "integrity": "sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A==",
"hasInstallScript": true,
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/fontawesome-svg-core": {
- "version": "1.2.36",
- "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.36.tgz",
- "integrity": "sha512-YUcsLQKYb6DmaJjIHdDWpBIGCcyE/W+p/LMGvjQem55Mm2XWVAP5kWTMKWLv9lwpCVjpLxPyOMOyUocP1GxrtA==",
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.1.tgz",
+ "integrity": "sha512-MfRCYlQPXoLlpem+egxjfkEuP9UQswTrlCOsknus/NcMoblTH2g0jPrapbcIb04KGA7E2GZxbAccGZfWoYgsrQ==",
"hasInstallScript": true,
"dependencies": {
- "@fortawesome/fontawesome-common-types": "^0.2.36"
+ "@fortawesome/fontawesome-common-types": "6.5.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-brands-svg-icons": {
- "version": "5.15.4",
- "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-5.15.4.tgz",
- "integrity": "sha512-f1witbwycL9cTENJegcmcZRYyawAFbm8+c6IirLmwbbpqz46wyjbQYLuxOc7weXFXfB7QR8/Vd2u5R3q6JYD9g==",
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.5.1.tgz",
+ "integrity": "sha512-093l7DAkx0aEtBq66Sf19MgoZewv1zeY9/4C7vSKPO4qMwEsW/2VYTUTpBtLwfb9T2R73tXaRDPmE4UqLCYHfg==",
"hasInstallScript": true,
"dependencies": {
- "@fortawesome/fontawesome-common-types": "^0.2.36"
+ "@fortawesome/fontawesome-common-types": "6.5.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-regular-svg-icons": {
- "version": "5.15.4",
- "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.15.4.tgz",
- "integrity": "sha512-9VNNnU3CXHy9XednJ3wzQp6SwNwT3XaM26oS4Rp391GsxVYA+0oDR2J194YCIWf7jNRCYKjUCOduxdceLrx+xw==",
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.5.1.tgz",
+ "integrity": "sha512-m6ShXn+wvqEU69wSP84coxLbNl7sGVZb+Ca+XZq6k30SzuP3X4TfPqtycgUh9ASwlNh5OfQCd8pDIWxl+O+LlQ==",
"hasInstallScript": true,
"dependencies": {
- "@fortawesome/fontawesome-common-types": "^0.2.36"
+ "@fortawesome/fontawesome-common-types": "6.5.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-solid-svg-icons": {
- "version": "5.15.4",
- "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.4.tgz",
- "integrity": "sha512-JLmQfz6tdtwxoihXLg6lT78BorrFyCf59SAwBM6qV/0zXyVeDygJVb3fk+j5Qat+Yvcxp1buLTY5iDh1ZSAQ8w==",
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.1.tgz",
+ "integrity": "sha512-S1PPfU3mIJa59biTtXJz1oI0+KAXW6bkAb31XKhxdxtuXDiUIFsih4JR1v5BbxY7hVHsD1RKq+jRkVRaf773NQ==",
"hasInstallScript": true,
"dependencies": {
- "@fortawesome/fontawesome-common-types": "^0.2.36"
+ "@fortawesome/fontawesome-common-types": "6.5.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/react-fontawesome": {
- "version": "0.1.19",
- "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.19.tgz",
- "integrity": "sha512-Hyb+lB8T18cvLNX0S3llz7PcSOAJMLwiVKBuuzwM/nI5uoBw+gQjnf9il0fR1C3DKOI5Kc79pkJ4/xB0Uw9aFQ==",
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.0.tgz",
+ "integrity": "sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw==",
"dependencies": {
"prop-types": "^15.8.1"
},
"peerDependencies": {
"@fortawesome/fontawesome-svg-core": "~1 || ~6",
- "react": ">=16.x"
+ "react": ">=16.3"
}
},
"node_modules/@humanwhocodes/config-array": {
@@ -9749,46 +9749,46 @@
"integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q=="
},
"@fortawesome/fontawesome-common-types": {
- "version": "0.2.36",
- "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz",
- "integrity": "sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg=="
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz",
+ "integrity": "sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A=="
},
"@fortawesome/fontawesome-svg-core": {
- "version": "1.2.36",
- "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.36.tgz",
- "integrity": "sha512-YUcsLQKYb6DmaJjIHdDWpBIGCcyE/W+p/LMGvjQem55Mm2XWVAP5kWTMKWLv9lwpCVjpLxPyOMOyUocP1GxrtA==",
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.1.tgz",
+ "integrity": "sha512-MfRCYlQPXoLlpem+egxjfkEuP9UQswTrlCOsknus/NcMoblTH2g0jPrapbcIb04KGA7E2GZxbAccGZfWoYgsrQ==",
"requires": {
- "@fortawesome/fontawesome-common-types": "^0.2.36"
+ "@fortawesome/fontawesome-common-types": "6.5.1"
}
},
"@fortawesome/free-brands-svg-icons": {
- "version": "5.15.4",
- "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-5.15.4.tgz",
- "integrity": "sha512-f1witbwycL9cTENJegcmcZRYyawAFbm8+c6IirLmwbbpqz46wyjbQYLuxOc7weXFXfB7QR8/Vd2u5R3q6JYD9g==",
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.5.1.tgz",
+ "integrity": "sha512-093l7DAkx0aEtBq66Sf19MgoZewv1zeY9/4C7vSKPO4qMwEsW/2VYTUTpBtLwfb9T2R73tXaRDPmE4UqLCYHfg==",
"requires": {
- "@fortawesome/fontawesome-common-types": "^0.2.36"
+ "@fortawesome/fontawesome-common-types": "6.5.1"
}
},
"@fortawesome/free-regular-svg-icons": {
- "version": "5.15.4",
- "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.15.4.tgz",
- "integrity": "sha512-9VNNnU3CXHy9XednJ3wzQp6SwNwT3XaM26oS4Rp391GsxVYA+0oDR2J194YCIWf7jNRCYKjUCOduxdceLrx+xw==",
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.5.1.tgz",
+ "integrity": "sha512-m6ShXn+wvqEU69wSP84coxLbNl7sGVZb+Ca+XZq6k30SzuP3X4TfPqtycgUh9ASwlNh5OfQCd8pDIWxl+O+LlQ==",
"requires": {
- "@fortawesome/fontawesome-common-types": "^0.2.36"
+ "@fortawesome/fontawesome-common-types": "6.5.1"
}
},
"@fortawesome/free-solid-svg-icons": {
- "version": "5.15.4",
- "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.4.tgz",
- "integrity": "sha512-JLmQfz6tdtwxoihXLg6lT78BorrFyCf59SAwBM6qV/0zXyVeDygJVb3fk+j5Qat+Yvcxp1buLTY5iDh1ZSAQ8w==",
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.1.tgz",
+ "integrity": "sha512-S1PPfU3mIJa59biTtXJz1oI0+KAXW6bkAb31XKhxdxtuXDiUIFsih4JR1v5BbxY7hVHsD1RKq+jRkVRaf773NQ==",
"requires": {
- "@fortawesome/fontawesome-common-types": "^0.2.36"
+ "@fortawesome/fontawesome-common-types": "6.5.1"
}
},
"@fortawesome/react-fontawesome": {
- "version": "0.1.19",
- "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.19.tgz",
- "integrity": "sha512-Hyb+lB8T18cvLNX0S3llz7PcSOAJMLwiVKBuuzwM/nI5uoBw+gQjnf9il0fR1C3DKOI5Kc79pkJ4/xB0Uw9aFQ==",
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.0.tgz",
+ "integrity": "sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw==",
"requires": {
"prop-types": "^15.8.1"
}
diff --git a/package.json b/package.json
index bdff861cc3bf..176e2b10ec85 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "cipp",
- "version": "4.5.5",
+ "version": "5.2.1",
"description": "The CyberDrain Improved Partner Portal is a portal to help manage administration for Microsoft Partners.",
"homepage": "https://cipp.app/",
"bugs": {
@@ -31,11 +31,11 @@
"@coreui/react": "^4.11.0",
"@coreui/react-chartjs": "^2.1.3",
"@coreui/utils": "^1.3.1",
- "@fortawesome/fontawesome-svg-core": "^1.2.36",
- "@fortawesome/free-brands-svg-icons": "^5.15.4",
- "@fortawesome/free-regular-svg-icons": "^5.15.4",
- "@fortawesome/free-solid-svg-icons": "^5.15.4",
- "@fortawesome/react-fontawesome": "^0.1.16",
+ "@fortawesome/fontawesome-svg-core": "^6.5.1",
+ "@fortawesome/free-brands-svg-icons": "^6.5.1",
+ "@fortawesome/free-regular-svg-icons": "^6.5.1",
+ "@fortawesome/free-solid-svg-icons": "^6.5.1",
+ "@fortawesome/react-fontawesome": "^0.2.0",
"@monaco-editor/react": "^4.5.2",
"@popperjs/core": "^2.10.2",
"@reduxjs/toolkit": "^1.9.7",
diff --git a/src/adminRoutes.js b/src/adminRoutes.js
index ece783822eff..2f6f666ab905 100644
--- a/src/adminRoutes.js
+++ b/src/adminRoutes.js
@@ -1,5 +1,6 @@
import React from 'react'
-const CIPPSettings = React.lazy(() => import('src/views/cipp/CIPPSettings'))
+
+const CIPPSettings = React.lazy(() => import('src/views/cipp/app-settings/CIPPSettings'))
const Setup = React.lazy(() => import('src/views/cipp/Setup'))
const ApplyStandard = React.lazy(() => import('src/views/tenant/standards/ListStandards'))
const GDAPStatus = React.lazy(() => import('src/views/tenant/administration/ListGDAPQueue'))
@@ -25,7 +26,11 @@ const adminRoutes = [
{ path: '/cipp/setup', name: 'Setup', component: Setup },
{ path: '/tenant/administration/gdap', name: 'GDAP Wizard', component: GDAP },
- { path: '/tenant/administration/gdap-invite', name: 'GDAP Invite Wizard', component: GDAPInvite },
+ {
+ path: '/tenant/administration/gdap-invite',
+ name: 'GDAP Invite Wizard',
+ component: GDAPInvite,
+ },
{
path: '/tenant/administration/gdap-role-wizard',
name: 'GDAP Role Wizard',
@@ -41,9 +46,21 @@ const adminRoutes = [
name: 'GDAP Relationships',
component: GDAPRelationships,
},
- { path: '/tenant/administration/appapproval', name: 'App Approval', component: appapproval },
- { path: '/tenant/administration/gdap-status', name: 'GDAP Status', component: GDAPStatus },
- { path: '/tenant/standards/list-standards', name: 'List Standard', component: ApplyStandard },
+ {
+ path: '/tenant/administration/appapproval',
+ name: 'App Approval',
+ component: appapproval,
+ },
+ {
+ path: '/tenant/administration/gdap-status',
+ name: 'GDAP Status',
+ component: GDAPStatus,
+ },
+ {
+ path: '/tenant/standards/list-standards',
+ name: 'List Standard',
+ component: ApplyStandard,
+ },
{
path: '/tenant/administration/tenant-offboarding-wizard',
name: 'Tenant Offboarding',
diff --git a/src/components/forms/RFFComponents.jsx b/src/components/forms/RFFComponents.jsx
index 834c9688d2cc..fc98eec7af92 100644
--- a/src/components/forms/RFFComponents.jsx
+++ b/src/components/forms/RFFComponents.jsx
@@ -429,7 +429,12 @@ export const RFFSelectSearch = ({
{label}
{refreshFunction && (
-
+
diff --git a/src/components/layout/AppSidebarNav.jsx b/src/components/layout/AppSidebarNav.jsx
index 7a9082ac05e3..e76ac636d4f3 100644
--- a/src/components/layout/AppSidebarNav.jsx
+++ b/src/components/layout/AppSidebarNav.jsx
@@ -65,7 +65,7 @@ export const AppSidebarNav = ({ items }) => {
)
}
const navGroup = (item, index) => {
- const { component, name, icon, to, ...rest } = item
+ const { component, name, icon, to, items, ...rest } = item
const Component = component
const navGroupKey = `${item.name.toLowerCase().replace(' ', '-')}_${index}`
const navGroupIdx = `${item.section.toLowerCase().replace(' ', '-')}_${item.name
@@ -79,9 +79,7 @@ export const AppSidebarNav = ({ items }) => {
visible={location.pathname.startsWith(to)}
{...rest}
>
- {item.items?.map((item, index) =>
- item.items ? navGroup(item, index) : navItem(item, index),
- )}
+ {items?.map((item, index) => (item.items ? navGroup(item, index) : navItem(item, index)))}
)
}
diff --git a/src/components/layout/CippCallout.css b/src/components/layout/CippCallout.css
new file mode 100644
index 000000000000..f9b358d18f70
--- /dev/null
+++ b/src/components/layout/CippCallout.css
@@ -0,0 +1,28 @@
+.cipp-callout {
+ --cui-callout-padding-x: 1rem;
+ --cui-callout-padding-y: 1rem;
+ --cui-callout-border-width: var(--cui-border-width);
+ --cui-callout-border-color: var(--cui-border-color);
+ --cui-callout-border-left-width: calc(var(--cui-border-width) * 4);
+ --cui-callout-border-radius: var(--cui-border-radius);
+ border: var(--cui-callout-border-width) solid var(--cui-callout-border-color);
+ border-radius: var(--cui-callout-border-radius);
+ margin-bottom: 16px;
+ padding: var(--cui-callout-padding-y) var(--cui-callout-padding-x);
+}
+
+html:not([dir=rtl]) .cipp-callout {
+ border-left-color: var(--cui-callout-border-left-color);
+}
+
+html:not([dir=rtl]) .cipp-callout {
+ border-left-width: var(--cui-callout-border-left-width);
+}
+
+html:not([dir=rtl]) .cipp-callout-dismissible .btn {
+ right: 0;
+}
+
+.cipp-callout-dismissible .btn {
+ cursor: pointer;
+}
diff --git a/src/components/layout/CippCallout.jsx b/src/components/layout/CippCallout.jsx
new file mode 100644
index 000000000000..f771c0c085bc
--- /dev/null
+++ b/src/components/layout/CippCallout.jsx
@@ -0,0 +1,70 @@
+import React, { useState } from 'react'
+import { CAlert, CCallout } from '@coreui/react'
+import PropTypes from 'prop-types'
+import './CippCallout.css'
+import classNames from 'classnames'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { faXmark } from '@fortawesome/free-solid-svg-icons'
+
+export function CippCallout({
+ dismissible = false,
+ color = 'primary',
+ children = null,
+ className = '',
+ style = {},
+ ...rest
+}) {
+ const [open, setOpen] = useState(true)
+
+ if (!open) {
+ return null
+ }
+
+ return (
+
+
{children}
+ {dismissible && (
+
setOpen(false)}
+ style={{ padding: 0, margin: 0 }}
+ >
+
+
+ )}
+
+ )
+}
+
+CippCallout.propTypes = {
+ dismissible: PropTypes.bool,
+ color: PropTypes.oneOf([
+ 'primary',
+ 'secondary',
+ 'success',
+ 'warning',
+ 'danger',
+ 'info',
+ 'light',
+ 'dark',
+ ]),
+ children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]),
+ className: PropTypes.string,
+ style: PropTypes.object,
+}
diff --git a/src/components/layout/index.js b/src/components/layout/index.js
index 715c29a52e64..fd10309efc93 100644
--- a/src/components/layout/index.js
+++ b/src/components/layout/index.js
@@ -7,6 +7,7 @@ import CippContentCard from 'src/components/layout/CippContentCard'
import { CippMasonry, CippMasonryItem } from 'src/components/layout/CippMasonry'
import { CippPage, CippPageList } from 'src/components/layout/CippPage'
import CippWizard from 'src/components/layout/CippWizard'
+import { CippCallout } from 'src/components/layout/CippCallout.jsx'
export {
AppBreadcrumb,
@@ -19,5 +20,6 @@ export {
CippMasonryItem,
CippPage,
CippPageList,
+ CippCallout,
CippWizard,
}
diff --git a/src/components/tables/CippDatatable.jsx b/src/components/tables/CippDatatable.jsx
index 188c44542c82..cdca6a185cbb 100644
--- a/src/components/tables/CippDatatable.jsx
+++ b/src/components/tables/CippDatatable.jsx
@@ -6,13 +6,13 @@ import { CippTablePropTypes } from 'src/components/tables/CippTable'
import { CCallout } from '@coreui/react'
export default function CippDatatable({ path, params, ...rest }) {
- const [refreshGuid, setRefreshGuid] = React.useState('')
const [graphFilter, setGraphFilter] = React.useState(params?.Parameters?.$filter)
const {
data = [],
isFetching,
error,
- } = useListDatatableQuery({ path, params: { refreshGuid, $filter: graphFilter, ...params } })
+ refetch,
+ } = useListDatatableQuery({ path, params: { $filter: graphFilter, ...params } })
var defaultFilterText = ''
if (params?.Parameters?.$filter) {
@@ -23,11 +23,12 @@ export default function CippDatatable({ path, params, ...rest }) {
{data?.Metadata?.Queued && {data?.Metadata?.QueueMessage} }
refetch()}
graphFilterFunction={setGraphFilter}
/>
>
diff --git a/src/components/tables/CippTable.jsx b/src/components/tables/CippTable.jsx
index 74dbf52ecd53..5ccbe59792e3 100644
--- a/src/components/tables/CippTable.jsx
+++ b/src/components/tables/CippTable.jsx
@@ -1,5 +1,5 @@
import React, { useRef, useMemo, useState, useCallback, useEffect } from 'react'
-import { useSelector } from 'react-redux'
+import { useDispatch, useSelector } from 'react-redux'
import { ExportCsvButton, ExportPDFButton } from 'src/components/buttons'
import {
CSpinner,
@@ -10,9 +10,6 @@ import {
CDropdownMenu,
CDropdownItem,
CButton,
- CModal,
- CModalBody,
- CModalTitle,
CCallout,
CFormSelect,
CAccordion,
@@ -32,15 +29,15 @@ import {
faFilePdf,
faSearch,
faSync,
- faTasks,
} from '@fortawesome/free-solid-svg-icons'
import { cellGenericFormatter } from './CellGenericFormat'
import { ModalService } from '../utilities'
import { useLazyGenericGetRequestQuery, useLazyGenericPostRequestQuery } from 'src/store/api/app'
-import { ConfirmModal } from '../utilities/SharedModal'
-import { debounce } from 'lodash-es'
+import { debounce, update } from 'lodash-es'
import { useSearchParams } from 'react-router-dom'
import CopyToClipboard from 'react-copy-to-clipboard'
+import { setDefaultColumns } from 'src/store/features/app'
+import { end } from '@popperjs/core'
const FilterComponent = ({ filterText, onFilter, onClear, filterlist, onFilterPreset }) => (
<>
@@ -126,6 +123,7 @@ export default function CippTable({
exportFiltered = false,
filterlist,
showFilter = true,
+ endpointName,
tableProps: {
keyField = 'id',
theme = 'cyberdrain',
@@ -150,10 +148,43 @@ export default function CippTable({
}) {
const inputRef = useRef('')
const [loopRunning, setLoopRunning] = React.useState(false)
+ const defaultColumns = useSelector((state) => state.app.defaultColumns[endpointName])
+ const [defaultColumnsSet, setDefaultColumnsSet] = React.useState(false)
const [massResults, setMassResults] = React.useState([])
const [filterText, setFilterText] = React.useState(defaultFilterText)
const [filterviaURL, setFilterviaURL] = React.useState(false)
+ const [originalColumns, setOrginalColumns] = React.useState(columns)
const [updatedColumns, setUpdatedColumns] = React.useState(columns)
+ if (defaultColumns && defaultColumnsSet === false) {
+ const defaultColumnsArray = defaultColumns.split(',').filter((item) => item)
+
+ const actionsColumn = columns.length > 0 ? columns[columns.length - 1] : null
+
+ let tempColumns = actionsColumn ? columns.slice(0, -1) : [...columns]
+
+ defaultColumnsArray.forEach((columnName) => {
+ if (!tempColumns.find((c) => c.exportSelector === columnName && c?.omit !== true)) {
+ tempColumns.push({
+ name: columnName,
+ selector: (row) => row[columnName],
+ sortable: true,
+ exportSelector: columnName,
+ cell: cellGenericFormatter(),
+ })
+ }
+ })
+
+ if (actionsColumn) {
+ tempColumns.push(actionsColumn)
+ }
+ let newColumns = tempColumns.filter(
+ (column) => defaultColumnsArray.includes(column.exportSelector) || column === actionsColumn,
+ )
+
+ setUpdatedColumns(newColumns)
+ setDefaultColumnsSet(true)
+ }
+
const [selectedRows, setSelectedRows] = React.useState(false)
const [genericGetRequest, getResults] = useLazyGenericGetRequestQuery()
const [genericPostRequest, postResults] = useLazyGenericPostRequestQuery()
@@ -171,6 +202,32 @@ export default function CippTable({
searchParams.delete('updateTableFilter')
}
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ const addColumn = (columnname) => {
+ let alreadyInArray = updatedColumns.some(
+ (o) => o.exportSelector === columnname && o?.omit !== true,
+ )
+ let newColumns = [...updatedColumns]
+ const actionsColumn = newColumns.length > 0 ? newColumns.pop() : null
+
+ if (!alreadyInArray) {
+ const newColumn = {
+ name: columnname,
+ selector: (row) => row[columnname],
+ sortable: true,
+ exportSelector: columnname,
+ cell: cellGenericFormatter(),
+ }
+ newColumns.push(newColumn)
+ } else {
+ newColumns = newColumns.filter((o) => o.exportSelector !== columnname)
+ }
+ if (actionsColumn) {
+ newColumns.push(actionsColumn)
+ }
+ setUpdatedColumns(newColumns)
+ }
+
const handleSelectedChange = ({ selectedRows }) => {
setSelectedRows(selectedRows)
if (selectedRows.length < 1) {
@@ -272,12 +329,34 @@ export default function CippTable({
const applyFilter = (e) => {
setFilterText(e.target.value)
}
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ const setColumnDefaultLayout = (endpoint, columns) => {
+ dispatch(setDefaultColumns({ endpoint, columns }))
+ }
+ const resetDropdown = () => {
+ setUpdatedColumns(originalColumns)
+ setColumnDefaultLayout(endpointName, null)
+ }
+ const dispatch = useDispatch()
useEffect(() => {
- if (columns !== updatedColumns) {
- setUpdatedColumns(columns)
+ if (columns.length !== updatedColumns.length) {
+ setUpdatedColumns(updatedColumns)
+ setColumnDefaultLayout(
+ endpointName,
+ updatedColumns.map((column) => column.exportSelector).join(','),
+ )
}
- }, [columns, updatedColumns])
+ }, [
+ columns,
+ defaultColumns,
+ dispatch,
+ dynamicColumns,
+ originalColumns,
+ endpointName,
+ setColumnDefaultLayout,
+ updatedColumns,
+ ])
createTheme(
'cyberdrain',
@@ -495,7 +574,7 @@ export default function CippTable({
}
const executeselectedAction = (item) => {
- console.log(item)
+ // console.log(item)
setModalContent({
item,
})
@@ -617,24 +696,6 @@ export default function CippTable({
if (!disablePDFExport) {
if (dynamicColumns === true) {
- const addColumn = (columnname) => {
- var index = columns.length - 1
- let alreadyInArray = columns.find((o) => o.exportSelector === columnname)
- if (!alreadyInArray) {
- columns.splice(index, 0, {
- name: columnname,
- selector: (row) => row[columnname],
- sortable: true,
- exportSelector: columnname,
- cell: cellGenericFormatter(),
- })
- } else {
- let indexOfExisting = columns.findIndex((o) => o.exportSelector === columnname)
- columns = columns.splice(indexOfExisting, 1)
- }
- setUpdatedColumns(Date())
- }
-
defaultActions.push([
+ resetDropdown()}>Reset to default
{dataKeys() &&
dataKeys().map((item, idx) => {
return (
addColumn(item)}>
- {columns.find((o) => o.exportSelector === item) && (
-
- )}{' '}
+ {updatedColumns.find(
+ (o) => o.exportSelector === item && o?.omit !== true,
+ ) && }{' '}
{item}
)
@@ -778,18 +840,27 @@ export default function CippTable({
>
)
}, [
+ refreshFunction,
actions,
- selectedRows,
disablePDFExport,
disableCSVExport,
+ selectedRows,
+ actionsList,
+ showFilter,
filterText,
filterlist,
resetPaginationToggle,
- data,
+ handleModal,
+ getDrowndownInfo,
+ filteredItems,
columns,
+ data,
+ dynamicColumns,
reportName,
- selectedRows,
- filteredItems,
+ resetDropdown,
+ updatedColumns,
+ addColumn,
+ setGraphFilter,
])
const tablePageSize = useSelector((state) => state.app.tablePageSize)
const [codeCopied, setCodeCopied] = useState(false)
@@ -803,7 +874,7 @@ export default function CippTable({
{!isFetching && error &&
Error loading data }
- {(columns.length === updatedColumns.length || !dynamicColumns) && (
+ {(updatedColumns || !dynamicColumns) && (
<>
{(massResults.length >= 1 || loopRunning) && (
@@ -900,7 +971,7 @@ export default function CippTable({
responsive={responsive}
dense={dense}
striped={striped}
- columns={columns}
+ columns={updatedColumns}
data={filteredItems}
expandableRows={expandableRows}
expandableRowsComponent={expandableRowsComponent}
diff --git a/src/components/utilities/CippLazy.jsx b/src/components/utilities/CippLazy.jsx
new file mode 100644
index 000000000000..0f376e722ee6
--- /dev/null
+++ b/src/components/utilities/CippLazy.jsx
@@ -0,0 +1,19 @@
+import { useRef } from 'react'
+import PropTypes from 'prop-types'
+
+export function CippLazy({ visible, children }) {
+ const rendered = useRef(visible)
+
+ if (visible && !rendered.current) {
+ rendered.current = true
+ }
+
+ if (!rendered.current) return null
+
+ return {children}
+}
+
+CippLazy.propTypes = {
+ visible: PropTypes.bool,
+ children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]),
+}
diff --git a/src/components/utilities/index.js b/src/components/utilities/index.js
index cf22133abbbc..5015ae24ad70 100644
--- a/src/components/utilities/index.js
+++ b/src/components/utilities/index.js
@@ -1,6 +1,7 @@
import CippActionsOffcanvas from 'src/components/utilities/CippActionsOffcanvas'
import CippCodeOffCanvas from 'src/components/utilities/CippCodeOffcanvas.jsx'
import CippCodeBlock from 'src/components/utilities/CippCodeBlock'
+import { CippLazy } from 'src/components/utilities/CippLazy'
import CippOffcanvas from 'src/components/utilities/CippOffcanvas'
import CippProfile from 'src/components/utilities/CippProfile'
import ErrorBoundary from 'src/components/utilities/ErrorBoundary'
@@ -21,6 +22,7 @@ export {
CippActionsOffcanvas,
CippCodeBlock,
CippCodeOffCanvas,
+ CippLazy,
CippOffcanvas,
CippProfile,
ErrorBoundary,
diff --git a/src/store/features/app.js b/src/store/features/app.js
index 8325058bf3f0..994c58314128 100644
--- a/src/store/features/app.js
+++ b/src/store/features/app.js
@@ -11,6 +11,7 @@ const initialState = {
tablePageSize: 25,
pageSizes: [25, 50, 100, 200, 500],
TenantListSelector: false,
+ defaultColumns: {},
}
export const appSlice = createSlice({
@@ -47,8 +48,8 @@ export const appSlice = createSlice({
setOffboardingDefaults: (state, action) => {
state.offboardingDefaults = action.payload?.offboardingDefaults
},
- setNewUserDefaults: (state, action) => {
- state.setNewUserDefaults = action.payload?.setNewUserDefaults
+ setDefaultColumns: (state, action) => {
+ state.defaultColumns[action.payload.endpoint] = action.payload?.columns
},
setUserSettings: (state, action) => {
//foreach key in the userSettings, set the state key to the value of that setting
@@ -70,8 +71,8 @@ export const {
setDefaultusageLocation,
setReportImage,
setOffboardingDefaults,
- setNewUserDefaults,
setUserSettings,
+ setDefaultColumns,
} = appSlice.actions
export default persistReducer(
diff --git a/src/views/cipp/CIPPSettings.jsx b/src/views/cipp/CIPPSettings.jsx
deleted file mode 100644
index 4c74b28bbcc9..000000000000
--- a/src/views/cipp/CIPPSettings.jsx
+++ /dev/null
@@ -1,2155 +0,0 @@
-import React, { useEffect, useRef, useState } from 'react'
-import {
- CButton,
- CButtonGroup,
- CCallout,
- CCard,
- CCardBody,
- CCardHeader,
- CCardTitle,
- CCol,
- CFormLabel,
- CNav,
- CNavItem,
- CRow,
- CTabContent,
- CTabPane,
- CForm,
- CListGroup,
- CListGroupItem,
- CLink,
- CSpinner,
- CCardText,
- CTooltip,
- CFormSwitch,
-} from '@coreui/react'
-import {
- useGenericGetRequestQuery,
- useLazyExecClearCacheQuery,
- useLazyExecNotificationConfigQuery,
- useLazyExecPermissionsAccessCheckQuery,
- useLazyExecTenantsAccessCheckQuery,
- useLazyGenericGetRequestQuery,
- useLazyGenericPostRequestQuery,
- useLazyListNotificationConfigQuery,
- useLoadVersionsQuery,
-} from 'src/store/api/app'
-import {
- useExecAddExcludeTenantMutation,
- useExecRemoveExcludeTenantMutation,
- useListTenantsQuery,
-} from 'src/store/api/tenants'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import {
- faCheckCircle,
- faCircleNotch,
- faExclamationTriangle,
- faEye,
- faEyeSlash,
- faLink,
- faRecycle,
- faScroll,
- faTrash,
-} from '@fortawesome/free-solid-svg-icons'
-import { useLazyEditDnsConfigQuery, useLazyGetDnsConfigQuery } from 'src/store/api/domains'
-import { useDispatch, useSelector } from 'react-redux'
-import {
- CellBadge,
- cellBooleanFormatter,
- CellTip,
- CellTipIcon,
- CippTable,
-} from 'src/components/tables'
-import { CippPage, CippPageList } from 'src/components/layout'
-import {
- RFFCFormSwitch,
- RFFCFormInput,
- RFFCFormSelect,
- RFFSelectSearch,
-} from 'src/components/forms'
-import { Form } from 'react-final-form'
-import useConfirmModal from 'src/hooks/useConfirmModal'
-import { setCurrentTenant } from 'src/store/features/app'
-import {
- CippOffcanvas,
- CippCodeBlock,
- ModalService,
- StatusIcon,
- TenantSelectorMultiple,
-} from 'src/components/utilities'
-import CippListOffcanvas from 'src/components/utilities/CippListOffcanvas'
-import { TitleButton, TableModalButton } from 'src/components/buttons'
-import Skeleton from 'react-loading-skeleton'
-import { Buffer } from 'buffer'
-import Extensions from 'src/data/Extensions.json'
-import { CellDelegatedPrivilege } from 'src/components/tables/CellDelegatedPrivilege'
-import { cellTableFormatter } from 'src/components/tables/CellTable'
-import { cellGenericFormatter } from 'src/components/tables/CellGenericFormat'
-import PropTypes from 'prop-types'
-
-function Lazy({ visible, children }) {
- const rendered = useRef(visible)
-
- if (visible && !rendered.current) {
- rendered.current = true
- }
-
- if (!rendered.current) return null
-
- return {children}
-}
-
-Lazy.propTypes = {
- visible: PropTypes.bool,
- children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]),
-}
-
-const CIPPSettings = () => {
- const [active, setActive] = useState(1)
- return (
-
-
- setActive(1)} href="#">
- General
-
- setActive(2)} href="#">
- Tenants
-
- setActive(3)} href="#">
- Backend
-
- setActive(4)} href="#">
- Notifications
-
- setActive(5)} href="#">
- Licenses
-
- setActive(6)} href="#">
- Maintenance
-
- setActive(7)} href="#">
- Extensions
-
- setActive(8)} href="#">
- Extension Mappings
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- )
-}
-
-export default CIPPSettings
-
-const GeneralSettings = () => {
- const { data: tenants = [] } = useListTenantsQuery({ AllTenantSelector: false })
- const [checkPermissions, permissionsResult] = useLazyExecPermissionsAccessCheckQuery()
- const [checkGDAP, GDAPResult] = useLazyGenericGetRequestQuery()
-
- const [clearCache, clearCacheResult] = useLazyExecClearCacheQuery()
- const [checkAccess, accessCheckResult] = useLazyExecTenantsAccessCheckQuery()
- const [selectedTenants, setSelectedTenants] = useState([])
- const [showMaxSelected, setShowMaxSelected] = useState(false)
- const [tokenOffcanvasVisible, setTokenOffcanvasVisible] = useState(false)
- const [showExtendedInfo, setShowExtendedInfo] = useState(true)
-
- const maxSelected = 2
- const tenantSelectorRef = useRef(null)
-
- const handleSetSelectedTenants = (value) => {
- if (value.length <= maxSelected) {
- setSelectedTenants(value)
- setShowMaxSelected(false)
- } else {
- setSelectedTenants(value)
- setShowMaxSelected(true)
- }
- }
-
- const checkAccessColumns = [
- {
- name: 'Tenant Domain',
- selector: (row) => row['TenantName'],
- grow: 0,
- cell: cellGenericFormatter(),
- },
- {
- name: 'Result',
- selector: (row) => row['Status'],
- minWidth: '380px',
- maxWidth: '380px',
- cell: cellGenericFormatter(),
- },
- {
- name: 'Missing GDAP Roles',
- selector: (row) => row?.MissingRoles,
- cell: cellTableFormatter('MissingRoles', true, false, true),
- },
- {
- name: 'Roles available',
- selector: (row) => row?.GDAPRoles,
- cell: cellTableFormatter('GDAPRoles', false, true),
- omit: showExtendedInfo,
- },
- {
- name: 'SAM User Roles',
- selector: (row) => row?.SAMUserRoles,
- cell: cellTableFormatter('SAMUserRoles', false, true),
- omit: showExtendedInfo,
- },
- ]
-
- const checkGDAPColumns = [
- {
- name: 'Tenant',
- selector: (row) => row['Tenant'],
- sortable: true,
- cell: cellGenericFormatter(),
- minWidth: '200px',
- maxWidth: '200px',
- },
- {
- name: 'Error Type',
- selector: (row) => row['Type'],
- sortable: true,
- cell: cellGenericFormatter(),
- minWidth: '100px',
- maxWidth: '100px',
- },
- {
- name: 'Issue',
- selector: (row) => row?.Issue,
- sortable: true,
- cell: cellGenericFormatter(),
- },
- {
- name: 'Resolution Link',
- sortable: true,
- selector: (row) => row?.Link,
- cell: cellGenericFormatter(),
- },
- {
- name: 'Relationship ID',
- sortable: true,
- selector: (row) => row?.Relationship,
- cell: cellGenericFormatter(),
- },
- ]
-
- const handleCheckAccess = () => {
- const mapped = tenants.reduce(
- (current, { customerId, ...rest }) => ({
- ...current,
- [customerId]: { ...rest },
- }),
- {},
- )
- const AllTenantSelector = selectedTenants.map(
- (customerId) => mapped[customerId].defaultDomainName,
- )
- checkAccess({ tenantDomains: AllTenantSelector })
- }
-
- function getTokenOffcanvasProps({ tokenResults }) {
- let tokenDetails = tokenResults.AccessTokenDetails
- let helpLinks = tokenResults.Links
- let tokenOffcanvasGroups = []
- if (tokenDetails?.Name !== '') {
- let tokenItems = []
- let tokenOffcanvasGroup = {}
- tokenItems.push({
- heading: 'User',
- content: tokenDetails?.Name,
- })
- tokenItems.push({
- heading: 'UPN',
- content: tokenDetails?.UserPrincipalName,
- })
- tokenItems.push({
- heading: 'App Registration',
- content: (
-
- {tokenDetails?.AppName}
-
- ),
- })
- tokenItems.push({
- heading: 'IP Address',
- content: tokenDetails?.IPAddress,
- })
- tokenItems.push({
- heading: 'Auth Methods',
- content: tokenDetails?.AuthMethods.join(', '),
- })
- tokenItems.push({
- heading: 'Tenant ID',
- content: tokenDetails?.TenantId,
- })
- tokenOffcanvasGroup.items = tokenItems
- tokenOffcanvasGroup.title = 'Claims'
- tokenOffcanvasGroups.push(tokenOffcanvasGroup)
- }
-
- if (helpLinks.length > 0) {
- let linkItems = []
- let linkItemGroup = {}
- helpLinks.map((link, idx) =>
- linkItems.push({
- heading: '',
- content: (
-
- {link.Text}
-
- ),
- }),
- )
- linkItemGroup.title = 'Help Links'
- linkItemGroup.items = linkItems
- if (linkItemGroup.items.length > 0) {
- tokenOffcanvasGroups.push(linkItemGroup)
- }
- }
-
- return tokenOffcanvasGroups
- }
-
- const tableProps = {
- pagination: false,
- actions: [
- {
- console.log(e)
- setShowExtendedInfo(!e.target.checked)
- }}
- key={'Show Extended Info'}
- />,
- ],
- }
-
- return (
-
-
-
-
-
-
-
-
-
-
-
- Permissions Check
- Click the button below to start a permissions check.
- checkPermissions()}
- disabled={permissionsResult.isFetching}
- className="mb-3 me-2"
- >
- {permissionsResult.isFetching && (
-
- )}
- Run Permissions Check
-
- {permissionsResult.isSuccess && (
- <>
- {permissionsResult.data.Results?.AccessTokenDetails?.Name !== '' && (
- <>
- setTokenOffcanvasVisible(true)}>
- Details
-
- setTokenOffcanvasVisible(false)}
- />
- >
- )}
-
- {permissionsResult.data.Results?.Messages && (
- <>
- {permissionsResult.data.Results?.Messages?.map((m, idx) => (
- {m}
- ))}
- >
- )}
- {permissionsResult.data.Results?.MissingPermissions.length > 0 && (
- <>
- Your Secure Application Model is missing the following permissions. See the
- documentation on how to add permissions{' '}
-
- here
-
- .
-
- {permissionsResult.data.Results?.MissingPermissions?.map((r, index) => (
- {r}
- ))}
-
- >
- )}
-
- >
- )}
-
-
-
-
-
-
-
- GDAP Check
- Click the button below to start a check for general GDAP settings.
- checkGDAP({ path: '/api/ExecAccessChecks?GDAP=true' })}
- disabled={GDAPResult.isFetching}
- className="mb-3 me-2"
- >
- {GDAPResult.isFetching && (
-
- )}
- Run GDAP Check
-
- {GDAPResult.isSuccess && (
- <>
- p['@odata.type'] == '#microsoft.graph.group',
- )}
- title="Groups"
- />
- p['@odata.type'] == '#microsoft.graph.directoryRole',
- )}
- title="Roles"
- />
- >
- )}
-
-
- {GDAPResult.isSuccess && GDAPResult.data.Results.GDAPIssues?.length > 0 && (
- <>
- {GDAPResult.data.Results.GDAPIssues?.filter((e) => e.Type === 'Error')
- .length > 0 && (
-
- Relationship errors detected. Review the table below for more details.
-
- )}
- {GDAPResult.data.Results.GDAPIssues?.filter((e) => e.Type === 'Warning')
- .length > 0 && (
-
- Relationship warnings detected. Review the table below for more details.
-
- )}
-
- >
- )}
- {GDAPResult.isSuccess && GDAPResult.data.Results.GDAPIssues?.length === 0 && (
-
- No relationships with issues found. Please perform a Permissions Check or
- Tenant Access Check if you are experiencing issues.
-
- )}
-
-
-
-
-
-
-
-
-
-
-
- Tenant Access Check
-
-
-
- Click the button below to start a tenant access check. You can select multiple,
- but a maximum of {maxSelected + 1} tenants is recommended.
-
-
-
- handleSetSelectedTenants(
- value.map((val) => {
- return val.value
- }),
- )
- }
- />
- {showMaxSelected && (
-
- A maximum of {maxSelected + 1} tenants is recommended.
-
- )}
-
-
-
-
-
- handleCheckAccess()}
- disabled={accessCheckResult.isFetching || selectedTenants.length < 1}
- >
- {accessCheckResult.isFetching && (
-
- )}
- Run access check
-
-
-
-
-
- {accessCheckResult.isSuccess && (
-
- )}
-
-
-
-
-
-
-
- )
-}
-
-const ExcludedTenantsSettings = () => {
- const dispatch = useDispatch()
- const currentTenant = useSelector((state) => state.app.currentTenant)
- const [removeExcludeTenant, removeExcludeTenantResult] = useExecRemoveExcludeTenantMutation()
- const [addExcludeTenant, addExcludeTenantResult] = useExecAddExcludeTenantMutation()
- const [refreshPermissions, refreshPermissionsResults] = useLazyGenericGetRequestQuery()
-
- // const [selectedTenant, setSelectedTenant] = useState()
- const selectedTenant = useRef()
-
- useEffect(() => {
- // if a tenant is already selected and that's the tenant the
- // user wants to exclude, we need to set that to the current state
- selectedTenant.current = currentTenant
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [])
-
- const handleRemoveExclusion = (domain) =>
- ModalService.confirm({
- title: 'Remove Exclusion',
- body: Are you sure you want to remove the exclusion for {domain}?
,
- onConfirm: () => removeExcludeTenant(domain),
- })
-
- const handleCPVPermissions = (domain, resetsp = false) =>
- ModalService.confirm({
- title: 'Refresh Permissions',
- body: Are you sure you want to refresh permissions for {domain.defaultDomainName}?
,
- onConfirm: () =>
- refreshPermissions({
- path: `/api/ExecCPVPermissions?TenantFilter=${domain.customerId}&ResetSP=${resetsp}`,
- }),
- })
- const handleConfirmExcludeTenant = (tenant) => {
- ModalService.confirm({
- title: 'Exclude Tenant',
- body: Are you sure you want to exclude this tenant?
,
- onConfirm: () => addExcludeTenant(tenant),
- })
- .unwrap()
- .then(() => {
- dispatch(setCurrentTenant({}))
- })
- }
-
- const handleExcludeTenant = (selected) => {
- ModalService.confirm({
- body: (
-
-
Select a tenant to exclude
-
(selected = tenant)} />
-
- ),
- title: 'Add Exclusion',
- onConfirm: () => handleConfirmExcludeTenant(selected),
- })
- }
- const titleButton = (
- handleExcludeTenant(selectedTenant)}
- >
- Add Excluded Tenant
-
- )
-
- function StatusIcon(graphErrorCount) {
- if (graphErrorCount > 0) {
- return
- } else {
- return
- }
- }
-
- function StatusText(graphErrorCount, lastGraphError) {
- if (graphErrorCount > 0) {
- return 'Error Count: ' + graphErrorCount + ' - Last Error: ' + lastGraphError
- } else {
- return 'No errors detected with this tenant'
- }
- }
-
- const Offcanvas = (row, rowIndex, formatExtraData) => {
- return (
- <>
- {row.Excluded && (
-
- handleRemoveExclusion(row.defaultDomainName)}
- >
-
-
-
- )}
- {!row.Excluded && (
-
- handleConfirmExcludeTenant({ value: row.customerId })}
- >
-
-
-
- )}
-
- handleCPVPermissions(row, false)}
- >
-
-
-
- >
- )
- }
- const columns = [
- {
- name: 'Name',
- selector: (row) => row['displayName'],
- sortable: true,
- cell: (row) => CellTip(row['displayName']),
- exportSelector: 'displayName',
- },
- {
- name: 'Default Domain',
- selector: (row) => row['defaultDomainName'],
- sortable: true,
- cell: (row) => CellTip(row['defaultDomainName']),
- exportSelector: 'defaultDomainName',
- },
- {
- name: 'Excluded',
- selector: (row) => row['Excluded'],
- sortable: true,
- cell: cellBooleanFormatter({ colourless: true }),
- exportSelector: 'Excluded',
- maxWidth: '100px',
- minWidth: '100px',
- },
- {
- name: 'Exclude Date',
- selector: (row) => row['ExcludeDate'],
- sortable: true,
- exportSelector: 'ExcludeDate',
- maxWidth: '150px',
- minWidth: '150px',
- },
- {
- name: 'Exclude User',
- selector: (row) => row['ExcludeUser'],
- sortable: true,
- exportSelector: 'ExcludeUser',
- maxWidth: '130px',
- minWidth: '130px',
- },
- {
- name: 'Actions',
- cell: Offcanvas,
- maxWidth: '80px',
- },
- ]
- return (
- <>
- {(refreshPermissionsResults.isFetching || removeExcludeTenantResult.isFetching) && (
-
-
-
- )}
- {removeExcludeTenantResult.isSuccess && (
-
- {removeExcludeTenantResult.data?.Results}
-
- )}
- {refreshPermissionsResults.isSuccess &&
- refreshPermissionsResults.data?.Results &&
- Array.isArray(refreshPermissionsResults.data.Results) ? (
-
- {refreshPermissionsResults.data.Results.map((result, idx) => (
- {result}
- ))}
-
- ) : null}
- {addExcludeTenantResult.isSuccess && (
-
- {addExcludeTenantResult.data?.Results}
-
- )}
-
- >
- )
-}
-const SecuritySettings = () => {
- const [listBackend, listBackendResult] = useLazyGenericGetRequestQuery()
- const [visible, setVisible] = useState(false)
- return (
-
- {listBackendResult.isUninitialized && listBackend({ path: 'api/ExecBackendURLs' })}
- <>
-
-
-
-
- Resource Group
-
-
-
- The Resource group contains all the CIPP resources in your tenant, except the SAM
- Application
-
-
- Go to Resource Group
-
-
-
-
-
-
-
- Key Vault
-
-
-
- The keyvault allows you to check token information. By default you do not have
- access.
-
-
- Go to Keyvault
-
-
-
-
-
-
-
- Static Web App (Role Management)
-
-
-
- The Static Web App role management allows you to invite other users to the
- application.
-
-
- Go to Role Management
-
-
-
-
-
-
-
-
-
- Function App (Deployment Center)
-
-
- The Function App Deployment Center allows you to run updates on the API
-
- Go to Function App Deployment Center
-
-
-
-
-
-
-
- Function App (Configuration)
-
-
-
- At the Function App Configuration you can check the status of the API access to
- your keyvault
-
-
- Go to Function App Configuration
-
-
-
-
-
-
-
- Function App (Overview)
-
-
- At the function App Overview, you can stop and start the backend API
-
- Go to Function App Overview
-
-
-
-
-
-
-
-
-
- Cloud Shell
-
-
- Launch an Azure Cloud Shell Window
-
- window.open(
- 'https://shell.azure.com/powershell',
- '_blank',
- 'toolbar=no,scrollbars=yes,resizable=yes,menubar=no,location=no,status=no',
- )
- }
- rel="noreferrer"
- >
- Cloud Shell
-
- setVisible(true)} className="mb-3">
- Command Reference
-
-
-
-
-
-
setVisible(false)}
- title="Command Reference"
- >
- Function App Config
-
- Function App Deployment
-
- Watch Function Logs
-
- Static Web App Config
-
- List CIPP Users
-
-
- >
-
- )
-}
-
-const NotificationsSettings = () => {
- const [configNotifications, notificationConfigResult] = useLazyExecNotificationConfigQuery()
- const [listNotification, notificationListResult] = useLazyListNotificationConfigQuery()
- const onSubmit = (values) => {
- configNotifications(values)
- }
- return (
- <>
- {notificationListResult.isUninitialized && listNotification()}
- {notificationListResult.isFetching && (
-
- )}
- {!notificationListResult.isFetching && notificationListResult.error && (
- Error loading data
- )}
- {notificationListResult.isSuccess && (
-
-
- Notifications
-
-
-
-
- )}
- >
- )
-}
-
-const LicenseSettings = () => {
- const [setExclusion, setExclusionResults] = useLazyGenericPostRequestQuery()
- const formRef = useRef(null)
-
- const handleAddLicense = (selected) => {
- ModalService.confirm({
- body: (
-
- ),
- title: 'Add Exclusion',
- onConfirm: () =>
- setExclusion({
- path: '/api/ExecExcludeLicenses?AddExclusion=true',
- values: { ...formRef.current },
- }),
- })
- }
-
- const titleButton =
- const [ExecuteGetRequest, getResults] = useLazyGenericGetRequestQuery()
-
- const Offcanvas = (row, rowIndex, formatExtraData) => {
- const handleDeleteIntuneTemplate = (apiurl, message) => {
- ModalService.confirm({
- title: 'Confirm',
- body: {message}
,
- onConfirm: () => ExecuteGetRequest({ path: apiurl }),
- confirmLabel: 'Continue',
- cancelLabel: 'Cancel',
- })
- }
- return (
- <>
-
- handleDeleteIntuneTemplate(
- `/api/ExecExcludeLicenses?RemoveExclusion=true&GUID=${row.GUID}`,
- 'Do you want to delete this exclusion?',
- )
- }
- >
-
-
- >
- )
- }
-
- const columns = [
- {
- name: 'Display Name',
- selector: (row) => row['Product_Display_Name'],
- exportSelector: 'Product_Display_Name',
- sortable: true,
- minWidth: '300px',
- },
- {
- name: 'License ID',
- selector: (row) => row['GUID'],
- exportSelector: 'GUID',
- sortable: true,
- minWidth: '350px',
- },
- {
- name: 'Actions',
- cell: Offcanvas,
- },
- ]
- return (
- <>
- {setExclusionResults.isFetching ||
- (getResults.isFetching && (
-
- Loading
-
- ))}
- {setExclusionResults.isSuccess && (
- {setExclusionResults.data?.Results}
- )}
- {setExclusionResults.isError && (
-
- Could not connect to API: {setExclusionResults.error.message}
-
- )}
- {getResults.isError && (
- Could not connect to API: {getResults.error.message}
- )}
- {getResults.isSuccess && {getResults.data?.Results} }
-
- >
- )
-}
-const PasswordSettings = () => {
- const [getPasswordConfig, getPasswordConfigResult] = useLazyGenericGetRequestQuery()
- const [editPasswordConfig, editPasswordConfigResult] = useLazyGenericPostRequestQuery()
-
- const [passAlertVisible, setPassAlertVisible] = useState(false)
-
- const switchResolver = (resolver) => {
- editPasswordConfig({ path: '/api/ExecPasswordconfig', values: { passwordType: resolver } })
- getPasswordConfig()
- setPassAlertVisible(true)
- }
-
- const resolvers = ['Classic', 'Correct-Battery-Horse']
-
- return (
- <>
- {getPasswordConfigResult.isUninitialized &&
- getPasswordConfig({ path: '/api/ExecPasswordConfig?list=true' })}
- Password Style
-
- {resolvers.map((r, index) => (
- switchResolver(r)}
- color={
- r === getPasswordConfigResult.data?.Results?.passwordType ? 'primary' : 'secondary'
- }
- key={index}
- >
- {r}
-
- ))}
-
- {(editPasswordConfigResult.isSuccess || editPasswordConfigResult.isError) && (
-
- {editPasswordConfigResult.isSuccess
- ? editPasswordConfigResult.data.Results
- : 'Error setting password style'}
-
- )}
- >
- )
-}
-
-const DNSSettings = () => {
- const [runBackup, RunBackupResult] = useLazyGenericGetRequestQuery()
- const [restoreBackup, restoreBackupResult] = useLazyGenericPostRequestQuery()
- const [getDnsConfig, getDnsConfigResult] = useLazyGetDnsConfigQuery()
- const [editDnsConfig, editDnsConfigResult] = useLazyEditDnsConfigQuery()
- const inputRef = useRef(null)
- const [clearCache, clearCacheResult] = useLazyExecClearCacheQuery()
- const { data: versions, isSuccess: isSuccessVersion } = useLoadVersionsQuery()
-
- const [alertVisible, setAlertVisible] = useState(false)
- const downloadTxtFile = (data) => {
- const txtdata = [JSON.stringify(RunBackupResult.data.backup)]
- const file = new Blob(txtdata, { type: 'text/plain' })
- const element = document.createElement('a')
- element.href = URL.createObjectURL(file)
- element.download = 'CIPP-Backup' + Date.now() + '.json'
- document.body.appendChild(element)
- element.click()
- }
- const handleChange = (e) => {
- const fileReader = new FileReader()
- fileReader.readAsText(e.target.files[0], 'UTF-8')
- fileReader.onload = (e) => {
- restoreBackup({ path: '/api/ExecRestoreBackup', values: e.target.result })
- }
- }
- const switchResolver = (resolver) => {
- editDnsConfig({ resolver })
- getDnsConfig()
- setAlertVisible(true)
- setTimeout(() => {
- setAlertVisible(false)
- }, 2000)
- }
- const handleClearCache = useConfirmModal({
- body: Are you sure you want to clear the cache?
,
- onConfirm: () => {
- clearCache({ tenantsOnly: false })
- localStorage.clear()
- },
- })
-
- const handleClearCacheTenant = useConfirmModal({
- body: Are you sure you want to clear the cache?
,
- onConfirm: () => {
- clearCache({ tenantsOnly: true })
- },
- })
- const resolvers = ['Google', 'Cloudflare', 'Quad9']
-
- return (
- <>
- {getDnsConfigResult.isUninitialized && getDnsConfig()}
- {getDnsConfigResult.isSuccess && (
-
-
-
-
-
-
-
-
- DNS Resolver
-
- {resolvers.map((r, index) => (
- switchResolver(r)}
- color={r === getDnsConfigResult.data.Resolver ? 'primary' : 'secondary'}
- key={index}
- >
- {r}
-
- ))}
-
- {(editDnsConfigResult.isSuccess || editDnsConfigResult.isError) && (
-
- {editDnsConfigResult.isSuccess
- ? editDnsConfigResult.data.Results
- : 'Error setting resolver'}
-
- )}
-
-
- Frontend Version
-
- Latest: {isSuccessVersion ? versions.RemoteCIPPVersion : }
-
- Current: {isSuccessVersion ? versions.LocalCIPPVersion : }
-
-
-
- Clear Caches
- handleClearCache()}
- disabled={clearCacheResult.isFetching}
- >
- {clearCacheResult.isFetching && (
-
- )}
- Clear All Cache
-
- handleClearCacheTenant()}
- disabled={clearCacheResult.isFetching}
- >
- {clearCacheResult.isFetching && (
-
- )}
- Clear Tenant Cache
-
- {clearCacheResult.isSuccess && (
- {clearCacheResult.data?.Results}
- )}
-
-
- Settings Backup
- runBackup({ path: '/api/ExecRunBackup' })}
- disabled={RunBackupResult.isFetching}
- >
- {RunBackupResult.isFetching && (
-
- )}
- Run backup
-
- handleChange(e)}
- />
- inputRef.current.click()}
- disabled={restoreBackupResult.isFetching}
- >
- {restoreBackupResult.isFetching && (
-
- )}
- Restore backup
-
- {restoreBackupResult.isSuccess && (
- <>
- {restoreBackupResult.data.Results}
- >
- )}
- {RunBackupResult.isSuccess && (
- <>
-
- downloadTxtFile(RunBackupResult.data.backup)}>
- Download Backup
-
-
- >
- )}
-
-
- Backend API Version
-
- Latest: {isSuccessVersion ? versions.RemoteCIPPAPIVersion : }
-
- Current: {isSuccessVersion ? versions.LocalCIPPAPIVersion : }
-
-
-
-
-
- )}
- >
- )
-}
-const ExtensionsTab = () => {
- const [listBackend, listBackendResult] = useLazyGenericGetRequestQuery()
- const inputRef = useRef(null)
- const [setExtensionconfig, extensionConfigResult] = useLazyGenericPostRequestQuery()
- const [execTestExtension, listExtensionTestResult] = useLazyGenericGetRequestQuery()
- const [execSyncExtension, listSyncExtensionResult] = useLazyGenericGetRequestQuery()
-
- const onSubmitTest = (integrationName) => {
- execTestExtension({
- path: 'api/ExecExtensionTest?extensionName=' + integrationName,
- })
- }
- const onSubmit = (values) => {
- setExtensionconfig({
- path: 'api/ExecExtensionsConfig',
- values: values,
- })
- }
- return (
-
- {listBackendResult.isUninitialized && listBackend({ path: 'api/ListExtensionsConfig' })}
- <>
- {(listBackendResult.isFetching ||
- extensionConfigResult.isFetching ||
- listExtensionTestResult.isFetching ||
- listSyncExtensionResult.isFetching) &&
}
- {listSyncExtensionResult.isSuccess && (
-
-
- Results
-
-
- {listSyncExtensionResult.data.Results}
-
-
- )}
-
- {listExtensionTestResult.isSuccess && (
-
-
- Results
-
-
- <>
- {listExtensionTestResult.data.Results}
- >
-
-
- )}
- {extensionConfigResult.isSuccess && (
-
-
- Results
-
-
- <>
- {extensionConfigResult.data.Results}
- >
-
-
- )}
-
- {Extensions.map((integration, idx) => (
-
-
-
- {integration.name}
-
-
- {integration.helpText}
-
-
-
- ))}
-
- >
-
- )
-}
-
-const MappingsTab = () => {
- const [listHaloBackend, listBackendHaloResult = []] = useLazyGenericGetRequestQuery()
- const [listNinjaOrgsBackend, listBackendNinjaOrgsResult] = useLazyGenericGetRequestQuery()
- const [listNinjaFieldsBackend, listBackendNinjaFieldsResult] = useLazyGenericGetRequestQuery()
- const [setHaloExtensionconfig, extensionHaloConfigResult = []] = useLazyGenericPostRequestQuery()
- const [setNinjaOrgsExtensionconfig, extensionNinjaOrgsConfigResult] =
- useLazyGenericPostRequestQuery()
- const [setNinjaOrgsExtensionAutomap, extensionNinjaOrgsAutomapResult] =
- useLazyGenericPostRequestQuery()
- const [setNinjaFieldsExtensionconfig, extensionNinjaFieldsConfigResult] =
- useLazyGenericPostRequestQuery()
-
- const onHaloSubmit = (values) => {
- setHaloExtensionconfig({
- path: 'api/ExecExtensionMapping?AddMapping=Halo',
- values: { mappings: values },
- })
- }
- const onNinjaOrgsSubmit = (values) => {
- setNinjaOrgsExtensionconfig({
- path: 'api/ExecExtensionMapping?AddMapping=NinjaOrgs',
- values: { mappings: values },
- })
- }
-
- const onNinjaOrgsAutomap = async (values) => {
- await setNinjaOrgsExtensionAutomap({
- path: 'api/ExecExtensionMapping?AutoMapping=NinjaOrgs',
- values: { mappings: values },
- })
- await listNinjaOrgsBackend({
- path: 'api/ExecExtensionMapping?List=NinjaOrgs',
- })
- }
-
- const onNinjaFieldsSubmit = (values) => {
- setNinjaFieldsExtensionconfig({
- path: 'api/ExecExtensionMapping?AddMapping=NinjaFields',
-
- values: { mappings: values },
- })
- }
- return (
-
- {listBackendHaloResult.isUninitialized &&
- listHaloBackend({ path: 'api/ExecExtensionMapping?List=Halo' })}
- {listBackendNinjaOrgsResult.isUninitialized &&
- listNinjaOrgsBackend({ path: 'api/ExecExtensionMapping?List=NinjaOrgs' })}
- {listBackendNinjaFieldsResult.isUninitialized &&
- listNinjaFieldsBackend({ path: 'api/ExecExtensionMapping?List=NinjaFields' })}
- <>
-
-
- HaloPSA Mapping Table
-
-
- {listBackendHaloResult.isFetching ? (
-
- ) : (
-
-
-
-
- NinjaOne Field Mapping Table
-
-
- {listBackendNinjaFieldsResult.isFetching ? (
-
- ) : (
-
-
-
-
- NinjaOne Organization Mapping Table
-
-
- {listBackendNinjaOrgsResult.isFetching ? (
-
- ) : (
-
-
- >
-
- )
-}
-
-const Maintenance = () => {
- const [selectedScript, setSelectedScript] = useState()
- const [listBackend, listBackendResult] = useLazyGenericGetRequestQuery()
- const [listScript, listScriptResult] = useLazyGenericGetRequestQuery()
- const [listScriptLink, listScriptLinkResult] = useLazyGenericGetRequestQuery()
-
- const handleSubmit = async (values) => {
- listScript({ path: 'api/ExecMaintenanceScripts', params: values })
- setSelectedScript(values.ScriptFile)
- }
-
- const handleGetLink = () => {
- listScriptLink({
- path: 'api/ExecMaintenanceScripts',
- params: { ScriptFile: selectedScript, MakeLink: 'True' },
- })
- }
- return (
- <>
- {listBackendResult.isUninitialized && listBackend({ path: 'api/ExecMaintenanceScripts' })}
-
-
-
-
- Maintenance
-
-
-
-
-
-
-
-
- {listScriptResult.isFetching && (
-
-
-
-
-
- )}
- {!listScriptResult.isFetching && listScriptResult.isSuccess && (
-
-
- Script Details
-
-
-
-
-
- Create Link
-
-
- {listScriptLinkResult.isSuccess && (
-
- {listScriptLinkResult.data.Link !== undefined && (
- <>
-
- Copy this text into a PowerShell terminal, we recommend Azure Cloud Shell.
- Azure modules and the az command line utilties are required for these
- scripts to work. The link is valid for 5 minutes.
-
-
- >
- )}
-
- )}
- {listScriptResult.data.ScriptContent !== undefined && (
-
-
Maintenance Script Contents
-
-
- )}
-
-
- )}
-
-
- >
- )
-}
diff --git a/src/views/cipp/app-settings/CIPPSettings.jsx b/src/views/cipp/app-settings/CIPPSettings.jsx
new file mode 100644
index 000000000000..ff43ad2a5a94
--- /dev/null
+++ b/src/views/cipp/app-settings/CIPPSettings.jsx
@@ -0,0 +1,93 @@
+import React, { useState } from 'react'
+import { CNav, CNavItem, CTabContent, CTabPane } from '@coreui/react'
+import { CippPage } from 'src/components/layout'
+import { CippLazy } from 'src/components/utilities'
+
+import { SettingsGeneral } from './SettingsGeneral.jsx'
+import { SettingsTenants } from 'src/views/cipp/app-settings/SettingsTenants.jsx'
+import { SettingsBackend } from 'src/views/cipp/app-settings/SettingsBackend.jsx'
+import { SettingsNotifications } from 'src/views/cipp/app-settings/SettingsNotifications.jsx'
+import { SettingsLicenses } from 'src/views/cipp/app-settings/SettingsLicenses.jsx'
+import { SettingsExtensions } from 'src/views/cipp/app-settings/SettingsExtensions.jsx'
+import { SettingsMaintenance } from 'src/views/cipp/app-settings/SettingsMaintenance.jsx'
+import { SettingsExtensionMappings } from 'src/views/cipp/app-settings/SettingsExtensionMappings.jsx'
+
+/**
+ * This function returns the settings page content for CIPP.
+ *
+ * @returns {JSX.Element} The settings page content.
+ */
+export default function CIPPSettings() {
+ const [active, setActive] = useState(1)
+ return (
+
+
+ setActive(1)} href="#">
+ General
+
+ setActive(2)} href="#">
+ Tenants
+
+ setActive(3)} href="#">
+ Backend
+
+ setActive(4)} href="#">
+ Notifications
+
+ setActive(5)} href="#">
+ Licenses
+
+ setActive(6)} href="#">
+ Maintenance
+
+ setActive(7)} href="#">
+ Extensions
+
+ setActive(8)} href="#">
+ Extension Mappings
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/views/cipp/app-settings/SettingsBackend.jsx b/src/views/cipp/app-settings/SettingsBackend.jsx
new file mode 100644
index 000000000000..58e3bc2b594b
--- /dev/null
+++ b/src/views/cipp/app-settings/SettingsBackend.jsx
@@ -0,0 +1,251 @@
+import { useLazyGenericGetRequestQuery } from 'src/store/api/app.js'
+import React, { useState } from 'react'
+import {
+ CButton,
+ CCard,
+ CCardBody,
+ CCardHeader,
+ CCardTitle,
+ CCol,
+ CLink,
+ CRow,
+} from '@coreui/react'
+import { CippCodeBlock, CippOffcanvas } from 'src/components/utilities/index.js'
+
+/**
+ * The SettingsBackend method is responsible for rendering a settings panel that contains several resource
+ * groups and corresponding links to access them.
+ * The panel displays information about Resource Group, Key Vault, Static Web App (Role Management),
+ * Function App (Deployment Center), Function App (Configuration), Function App (Overview), and Cloud Shell.
+ *
+ * @returns {JSX.Element} The settings panel component.
+ */
+export function SettingsBackend() {
+ const [listBackend, listBackendResult] = useLazyGenericGetRequestQuery()
+ const [visible, setVisible] = useState(false)
+ return (
+
+ {listBackendResult.isUninitialized && listBackend({ path: 'api/ExecBackendURLs' })}
+ <>
+
+
+
+
+ Resource Group
+
+
+
+ The Resource group contains all the CIPP resources in your tenant, except the SAM
+ Application
+
+
+ Go to Resource Group
+
+
+
+
+
+
+
+ Key Vault
+
+
+
+ The keyvault allows you to check token information. By default you do not have
+ access.
+
+
+ Go to Keyvault
+
+
+
+
+
+
+
+ Static Web App (Role Management)
+
+
+
+ The Static Web App role management allows you to invite other users to the
+ application.
+
+
+ Go to Role Management
+
+
+
+
+
+
+
+
+
+ Function App (Deployment Center)
+
+
+ The Function App Deployment Center allows you to run updates on the API
+
+ Go to Function App Deployment Center
+
+
+
+
+
+
+
+ Function App (Configuration)
+
+
+
+ At the Function App Configuration you can check the status of the API access to
+ your keyvault
+
+
+ Go to Function App Configuration
+
+
+
+
+
+
+
+ Function App (Overview)
+
+
+ At the function App Overview, you can stop and start the backend API
+
+ Go to Function App Overview
+
+
+
+
+
+
+
+
+
+ Cloud Shell
+
+
+ Launch an Azure Cloud Shell Window
+
+ window.open(
+ 'https://shell.azure.com/powershell',
+ '_blank',
+ 'toolbar=no,scrollbars=yes,resizable=yes,menubar=no,location=no,status=no',
+ )
+ }
+ rel="noreferrer"
+ >
+ Cloud Shell
+
+ setVisible(true)} className="mb-3">
+ Command Reference
+
+
+
+
+
+
setVisible(false)}
+ title="Command Reference"
+ >
+ Function App Config
+
+ Function App Deployment
+
+ Watch Function Logs
+
+ Static Web App Config
+
+ List CIPP Users
+
+
+ >
+
+ )
+}
diff --git a/src/views/cipp/app-settings/SettingsExtensionMappings.jsx b/src/views/cipp/app-settings/SettingsExtensionMappings.jsx
new file mode 100644
index 000000000000..5386afbf1fbd
--- /dev/null
+++ b/src/views/cipp/app-settings/SettingsExtensionMappings.jsx
@@ -0,0 +1,291 @@
+import { useLazyGenericGetRequestQuery, useLazyGenericPostRequestQuery } from 'src/store/api/app.js'
+import {
+ CButton,
+ CCallout,
+ CCard,
+ CCardBody,
+ CCardHeader,
+ CCardText,
+ CCardTitle,
+ CCol,
+ CForm,
+ CSpinner,
+} from '@coreui/react'
+import { Form } from 'react-final-form'
+import { RFFSelectSearch } from 'src/components/forms/index.js'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
+import React from 'react'
+import { CippCallout } from 'src/components/layout/index.js'
+
+/**
+ * Retrieves and sets the extension mappings for HaloPSA and NinjaOne.
+ *
+ * @returns {JSX.Element} - JSX component representing the settings extension mappings.
+ */
+export function SettingsExtensionMappings() {
+ const [listHaloBackend, listBackendHaloResult = []] = useLazyGenericGetRequestQuery()
+ const [listNinjaOrgsBackend, listBackendNinjaOrgsResult] = useLazyGenericGetRequestQuery()
+ const [listNinjaFieldsBackend, listBackendNinjaFieldsResult] = useLazyGenericGetRequestQuery()
+ const [setHaloExtensionconfig, extensionHaloConfigResult = []] = useLazyGenericPostRequestQuery()
+ const [setNinjaOrgsExtensionconfig, extensionNinjaOrgsConfigResult] =
+ useLazyGenericPostRequestQuery()
+ const [setNinjaOrgsExtensionAutomap, extensionNinjaOrgsAutomapResult] =
+ useLazyGenericPostRequestQuery()
+ const [setNinjaFieldsExtensionconfig, extensionNinjaFieldsConfigResult] =
+ useLazyGenericPostRequestQuery()
+
+ const onHaloSubmit = (values) => {
+ setHaloExtensionconfig({
+ path: 'api/ExecExtensionMapping?AddMapping=Halo',
+ values: { mappings: values },
+ })
+ }
+ const onNinjaOrgsSubmit = (values) => {
+ setNinjaOrgsExtensionconfig({
+ path: 'api/ExecExtensionMapping?AddMapping=NinjaOrgs',
+ values: { mappings: values },
+ })
+ }
+
+ const onNinjaOrgsAutomap = async (values) => {
+ await setNinjaOrgsExtensionAutomap({
+ path: 'api/ExecExtensionMapping?AutoMapping=NinjaOrgs',
+ values: { mappings: values },
+ })
+ await listNinjaOrgsBackend({
+ path: 'api/ExecExtensionMapping?List=NinjaOrgs',
+ })
+ }
+
+ const onNinjaFieldsSubmit = (values) => {
+ setNinjaFieldsExtensionconfig({
+ path: 'api/ExecExtensionMapping?AddMapping=NinjaFields',
+
+ values: { mappings: values },
+ })
+ }
+ return (
+
+ {listBackendHaloResult.isUninitialized &&
+ listHaloBackend({ path: 'api/ExecExtensionMapping?List=Halo' })}
+ {listBackendNinjaOrgsResult.isUninitialized &&
+ listNinjaOrgsBackend({ path: 'api/ExecExtensionMapping?List=NinjaOrgs' })}
+ {listBackendNinjaFieldsResult.isUninitialized &&
+ listNinjaFieldsBackend({ path: 'api/ExecExtensionMapping?List=NinjaFields' })}
+ <>
+
+
+ HaloPSA Mapping Table
+
+
+ {listBackendHaloResult.isFetching ? (
+
+ ) : (
+
+
+
+
+ NinjaOne Field Mapping Table
+
+
+ {listBackendNinjaFieldsResult.isFetching ? (
+
+ ) : (
+
+
+
+
+ NinjaOne Organization Mapping Table
+
+
+ {listBackendNinjaOrgsResult.isFetching ? (
+
+ ) : (
+
+
+ >
+
+ )
+}
diff --git a/src/views/cipp/app-settings/SettingsExtensions.jsx b/src/views/cipp/app-settings/SettingsExtensions.jsx
new file mode 100644
index 000000000000..44569219d704
--- /dev/null
+++ b/src/views/cipp/app-settings/SettingsExtensions.jsx
@@ -0,0 +1,183 @@
+import { useLazyGenericGetRequestQuery, useLazyGenericPostRequestQuery } from 'src/store/api/app.js'
+import React, { useRef } from 'react'
+import {
+ CAlert,
+ CButton,
+ CCallout,
+ CCard,
+ CCardBody,
+ CCardHeader,
+ CCardText,
+ CCardTitle,
+ CCol,
+ CForm,
+ CRow,
+ CSpinner,
+} from '@coreui/react'
+import Extensions from 'src/data/Extensions.json'
+import { Form } from 'react-final-form'
+import { RFFCFormInput, RFFCFormSwitch } from 'src/components/forms/index.js'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
+import { CippCallout } from 'src/components/layout/index.js'
+
+/**
+ * Executes various operations related to settings and extensions.
+ *
+ * @returns {JSX.Element} - The rendered component.
+ */
+export function SettingsExtensions() {
+ const [listBackend, listBackendResult] = useLazyGenericGetRequestQuery()
+ const inputRef = useRef(null)
+ const [setExtensionconfig, extensionConfigResult] = useLazyGenericPostRequestQuery()
+ const [execTestExtension, listExtensionTestResult] = useLazyGenericGetRequestQuery()
+ const [execSyncExtension, listSyncExtensionResult] = useLazyGenericGetRequestQuery()
+
+ const onSubmitTest = (integrationName) => {
+ execTestExtension({
+ path: 'api/ExecExtensionTest?extensionName=' + integrationName,
+ })
+ }
+ const onSubmit = (values) => {
+ setExtensionconfig({
+ path: 'api/ExecExtensionsConfig',
+ values: values,
+ })
+ }
+ return (
+
+ {listBackendResult.isUninitialized && listBackend({ path: 'api/ListExtensionsConfig' })}
+ <>
+ {(listBackendResult.isFetching ||
+ extensionConfigResult.isFetching ||
+ listExtensionTestResult.isFetching ||
+ listSyncExtensionResult.isFetching) && (
+
+
+
+ )}
+ {listSyncExtensionResult.isSuccess && !listSyncExtensionResult.isFetching && (
+
+ {listSyncExtensionResult.data.Results}
+
+ )}
+ {listExtensionTestResult.isSuccess && !listExtensionTestResult.isFetching && (
+
+ {listExtensionTestResult.data.Results}
+
+ )}
+ {extensionConfigResult.isSuccess && !extensionConfigResult.isFetching && (
+
+ {extensionConfigResult.data.Results}
+
+ )}
+
+ {Extensions.map((integration, idx) => (
+
+
+
+ {integration.name}
+
+
+ {integration.helpText}
+
+
+
+ ))}
+
+ >
+
+ )
+}
diff --git a/src/views/cipp/app-settings/SettingsGeneral.jsx b/src/views/cipp/app-settings/SettingsGeneral.jsx
new file mode 100644
index 000000000000..887758f30abb
--- /dev/null
+++ b/src/views/cipp/app-settings/SettingsGeneral.jsx
@@ -0,0 +1,452 @@
+import { useListTenantsQuery } from 'src/store/api/tenants.js'
+import {
+ useLazyExecClearCacheQuery,
+ useLazyExecPermissionsAccessCheckQuery,
+ useLazyExecTenantsAccessCheckQuery,
+ useLazyGenericGetRequestQuery,
+} from 'src/store/api/app.js'
+import React, { useRef, useState } from 'react'
+import { cellGenericFormatter } from 'src/components/tables/CellGenericFormat.jsx'
+import { cellTableFormatter } from 'src/components/tables/CellTable.jsx'
+import {
+ CButton,
+ CCallout,
+ CCard,
+ CCardBody,
+ CCardHeader,
+ CCol,
+ CFormSwitch,
+ CLink,
+ CListGroup,
+ CListGroupItem,
+ CRow,
+} from '@coreui/react'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
+import CippListOffcanvas from 'src/components/utilities/CippListOffcanvas.jsx'
+import { TableModalButton } from 'src/components/buttons/index.js'
+import { CippTable } from 'src/components/tables/index.js'
+import { TenantSelectorMultiple } from 'src/components/utilities/index.js'
+import { SettingsGeneralRow } from 'src/views/cipp/app-settings/components/SettingsGeneralRow.jsx'
+
+/**
+ * SettingsGeneral component.
+ * This method is responsible for managing general settings.
+ * @returns {JSX.Element}
+ */
+export function SettingsGeneral() {
+ const { data: tenants = [] } = useListTenantsQuery({ AllTenantSelector: false })
+ const [checkPermissions, permissionsResult] = useLazyExecPermissionsAccessCheckQuery()
+ const [checkGDAP, GDAPResult] = useLazyGenericGetRequestQuery()
+
+ const [clearCache, clearCacheResult] = useLazyExecClearCacheQuery()
+ const [checkAccess, accessCheckResult] = useLazyExecTenantsAccessCheckQuery()
+ const [selectedTenants, setSelectedTenants] = useState([])
+ const [showMaxSelected, setShowMaxSelected] = useState(false)
+ const [tokenOffcanvasVisible, setTokenOffcanvasVisible] = useState(false)
+ const [showExtendedInfo, setShowExtendedInfo] = useState(true)
+
+ const maxSelected = 2
+ const tenantSelectorRef = useRef(null)
+
+ const handleSetSelectedTenants = (value) => {
+ if (value.length <= maxSelected) {
+ setSelectedTenants(value)
+ setShowMaxSelected(false)
+ } else {
+ setSelectedTenants(value)
+ setShowMaxSelected(true)
+ }
+ }
+
+ const checkAccessColumns = [
+ {
+ name: 'Tenant Domain',
+ selector: (row) => row['TenantName'],
+ grow: 0,
+ cell: cellGenericFormatter(),
+ },
+ {
+ name: 'Result',
+ selector: (row) => row['Status'],
+ minWidth: '380px',
+ maxWidth: '380px',
+ cell: cellGenericFormatter(),
+ },
+ {
+ name: 'Missing GDAP Roles',
+ selector: (row) => row?.MissingRoles,
+ cell: cellTableFormatter('MissingRoles', true, false, true),
+ },
+ {
+ name: 'Roles available',
+ selector: (row) => row?.GDAPRoles,
+ cell: cellTableFormatter('GDAPRoles', false, true),
+ omit: showExtendedInfo,
+ exportSelector: 'GDAPRoles',
+ },
+ {
+ name: 'SAM User Roles',
+ selector: (row) => row?.SAMUserRoles,
+ cell: cellTableFormatter('SAMUserRoles', false, true),
+ omit: showExtendedInfo,
+ exportSelector: 'SAMUserRoles',
+ },
+ ]
+
+ const checkGDAPColumns = [
+ {
+ name: 'Tenant',
+ selector: (row) => row['Tenant'],
+ sortable: true,
+ cell: cellGenericFormatter(),
+ minWidth: '200px',
+ maxWidth: '200px',
+ },
+ {
+ name: 'Error Type',
+ selector: (row) => row['Type'],
+ sortable: true,
+ cell: cellGenericFormatter(),
+ minWidth: '100px',
+ maxWidth: '100px',
+ },
+ {
+ name: 'Issue',
+ selector: (row) => row?.Issue,
+ sortable: true,
+ cell: cellGenericFormatter(),
+ },
+ {
+ name: 'Resolution Link',
+ sortable: true,
+ selector: (row) => row?.Link,
+ cell: cellGenericFormatter(),
+ },
+ {
+ name: 'Relationship ID',
+ sortable: true,
+ selector: (row) => row?.Relationship,
+ cell: cellGenericFormatter(),
+ },
+ ]
+
+ const handleCheckAccess = () => {
+ const mapped = tenants.reduce(
+ (current, { customerId, ...rest }) => ({
+ ...current,
+ [customerId]: { ...rest },
+ }),
+ {},
+ )
+ const AllTenantSelector = selectedTenants.map(
+ (customerId) => mapped[customerId].defaultDomainName,
+ )
+ checkAccess({ tenantDomains: AllTenantSelector })
+ }
+
+ function getTokenOffcanvasProps({ tokenResults }) {
+ let tokenDetails = tokenResults.AccessTokenDetails
+ let helpLinks = tokenResults.Links
+ let tokenOffcanvasGroups = []
+ if (tokenDetails?.Name !== '') {
+ let tokenItems = []
+ let tokenOffcanvasGroup = {}
+ tokenItems.push({
+ heading: 'User',
+ content: tokenDetails?.Name,
+ })
+ tokenItems.push({
+ heading: 'UPN',
+ content: tokenDetails?.UserPrincipalName,
+ })
+ tokenItems.push({
+ heading: 'App Registration',
+ content: (
+
+ {tokenDetails?.AppName}
+
+ ),
+ })
+ tokenItems.push({
+ heading: 'IP Address',
+ content: tokenDetails?.IPAddress,
+ })
+ tokenItems.push({
+ heading: 'Auth Methods',
+ content: tokenDetails?.AuthMethods.join(', '),
+ })
+ tokenItems.push({
+ heading: 'Tenant ID',
+ content: tokenDetails?.TenantId,
+ })
+ tokenOffcanvasGroup.items = tokenItems
+ tokenOffcanvasGroup.title = 'Claims'
+ tokenOffcanvasGroups.push(tokenOffcanvasGroup)
+ }
+
+ if (helpLinks.length > 0) {
+ let linkItems = []
+ let linkItemGroup = {}
+ helpLinks.map((link, idx) =>
+ linkItems.push({
+ heading: '',
+ content: (
+
+ {link.Text}
+
+ ),
+ }),
+ )
+ linkItemGroup.title = 'Help Links'
+ linkItemGroup.items = linkItems
+ if (linkItemGroup.items.length > 0) {
+ tokenOffcanvasGroups.push(linkItemGroup)
+ }
+ }
+
+ return tokenOffcanvasGroups
+ }
+
+ const tableProps = {
+ pagination: false,
+ actions: [
+ {
+ console.log(e)
+ setShowExtendedInfo(!e.target.checked)
+ }}
+ key={'Show Extended Info'}
+ />,
+ ],
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ Permissions Check
+ Click the button below to start a permissions check.
+ checkPermissions()}
+ disabled={permissionsResult.isFetching}
+ className="mb-3 me-2"
+ >
+ {permissionsResult.isFetching && (
+
+ )}
+ Run Permissions Check
+
+ {permissionsResult.isSuccess && (
+ <>
+ {permissionsResult.data.Results?.AccessTokenDetails?.Name !== '' && (
+ <>
+ setTokenOffcanvasVisible(true)}>
+ Details
+
+ setTokenOffcanvasVisible(false)}
+ />
+ >
+ )}
+
+ {permissionsResult.data.Results?.Messages && (
+ <>
+ {permissionsResult.data.Results?.Messages?.map((m, idx) => (
+ {m}
+ ))}
+ >
+ )}
+ {permissionsResult.data.Results?.MissingPermissions.length > 0 && (
+ <>
+ Your Secure Application Model is missing the following permissions. See the
+ documentation on how to add permissions{' '}
+
+ here
+
+ .
+
+ {permissionsResult.data.Results?.MissingPermissions?.map((r, index) => (
+ {r}
+ ))}
+
+ >
+ )}
+
+ >
+ )}
+
+
+
+
+
+
+
+ GDAP Check
+ Click the button below to start a check for general GDAP settings.
+ checkGDAP({ path: '/api/ExecAccessChecks?GDAP=true' })}
+ disabled={GDAPResult.isFetching}
+ className="mb-3 me-2"
+ >
+ {GDAPResult.isFetching && (
+
+ )}
+ Run GDAP Check
+
+ {GDAPResult.isSuccess && (
+ <>
+ p['@odata.type'] == '#microsoft.graph.group',
+ )}
+ title="Groups"
+ />
+ p['@odata.type'] == '#microsoft.graph.directoryRole',
+ )}
+ title="Roles"
+ />
+ >
+ )}
+
+
+ {GDAPResult.isSuccess && GDAPResult.data.Results.GDAPIssues?.length > 0 && (
+ <>
+ {GDAPResult.data.Results.GDAPIssues?.filter((e) => e.Type === 'Error')
+ .length > 0 && (
+
+ Relationship errors detected. Review the table below for more details.
+
+ )}
+ {GDAPResult.data.Results.GDAPIssues?.filter((e) => e.Type === 'Warning')
+ .length > 0 && (
+
+ Relationship warnings detected. Review the table below for more details.
+
+ )}
+
+ >
+ )}
+ {GDAPResult.isSuccess && GDAPResult.data.Results.GDAPIssues?.length === 0 && (
+
+ No relationships with issues found. Please perform a Permissions Check or
+ Tenant Access Check if you are experiencing issues.
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ Tenant Access Check
+
+
+
+ Click the button below to start a tenant access check. You can select multiple,
+ but a maximum of {maxSelected + 1} tenants is recommended.
+
+
+
+ handleSetSelectedTenants(
+ value.map((val) => {
+ return val.value
+ }),
+ )
+ }
+ />
+ {showMaxSelected && (
+
+ A maximum of {maxSelected + 1} tenants is recommended.
+
+ )}
+
+
+
+
+
+ handleCheckAccess()}
+ disabled={accessCheckResult.isFetching || selectedTenants.length < 1}
+ >
+ {accessCheckResult.isFetching && (
+
+ )}
+ Run access check
+
+
+
+
+
+ {accessCheckResult.isSuccess && (
+
+ )}
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/views/cipp/app-settings/SettingsLicenses.jsx b/src/views/cipp/app-settings/SettingsLicenses.jsx
new file mode 100644
index 000000000000..dd9695b6ca6c
--- /dev/null
+++ b/src/views/cipp/app-settings/SettingsLicenses.jsx
@@ -0,0 +1,142 @@
+import { useLazyGenericGetRequestQuery, useLazyGenericPostRequestQuery } from 'src/store/api/app.js'
+import React, { useRef } from 'react'
+import { ModalService } from 'src/components/utilities/index.js'
+import { Form } from 'react-final-form'
+import { RFFCFormInput } from 'src/components/forms/index.js'
+import { TitleButton } from 'src/components/buttons/index.js'
+import { CButton, CCallout, CSpinner } from '@coreui/react'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { faTrash } from '@fortawesome/free-solid-svg-icons'
+import { CippCallout, CippPageList } from 'src/components/layout/index.js'
+
+/**
+ * SettingsLicenses component is used to manage excluded licenses in a settings page.
+ *
+ * @returns {JSX.Element} The generated settings page component.
+ */
+export function SettingsLicenses() {
+ const [setExclusion, setExclusionResults] = useLazyGenericPostRequestQuery()
+ const formRef = useRef(null)
+
+ const handleAddLicense = (selected) => {
+ ModalService.confirm({
+ body: (
+
+ ),
+ title: 'Add Exclusion',
+ onConfirm: () =>
+ setExclusion({
+ path: '/api/ExecExcludeLicenses?AddExclusion=true',
+ values: { ...formRef.current },
+ }),
+ })
+ }
+
+ const titleButton =
+ const [executeGetRequest, getResults] = useLazyGenericGetRequestQuery()
+
+ const Offcanvas = (row, rowIndex, formatExtraData) => {
+ const handleDeleteIntuneTemplate = (apiurl, message) => {
+ ModalService.confirm({
+ title: 'Confirm',
+ body: {message}
,
+ onConfirm: () => executeGetRequest({ path: apiurl }),
+ confirmLabel: 'Continue',
+ cancelLabel: 'Cancel',
+ })
+ }
+ return (
+ <>
+
+ handleDeleteIntuneTemplate(
+ `/api/ExecExcludeLicenses?RemoveExclusion=true&GUID=${row.GUID}`,
+ 'Do you want to delete this exclusion?',
+ )
+ }
+ >
+
+
+ >
+ )
+ }
+
+ const columns = [
+ {
+ name: 'Display Name',
+ selector: (row) => row['Product_Display_Name'],
+ exportSelector: 'Product_Display_Name',
+ sortable: true,
+ minWidth: '300px',
+ },
+ {
+ name: 'License ID',
+ selector: (row) => row['GUID'],
+ exportSelector: 'GUID',
+ sortable: true,
+ minWidth: '350px',
+ },
+ {
+ name: 'Actions',
+ cell: Offcanvas,
+ },
+ ]
+ return (
+ <>
+ {setExclusionResults.isFetching ||
+ (getResults.isFetching && (
+
+ Loading
+
+ ))}
+ {setExclusionResults.isSuccess && !setExclusionResults.isFetching && (
+
+ {setExclusionResults.data?.Results}
+
+ )}
+ {setExclusionResults.isError && !setExclusionResults.isFetching && (
+
+ Could not connect to API: {setExclusionResults.error.message}
+
+ )}
+ {getResults.isError && !getResults.isFetching && (
+
+ Could not connect to API: {getResults.error.message}
+
+ )}
+ {getResults.isSuccess && !getResults.isFetching && (
+
+ {getResults.data?.Results}
+
+ )}
+
+ >
+ )
+}
diff --git a/src/views/cipp/app-settings/SettingsMaintenance.jsx b/src/views/cipp/app-settings/SettingsMaintenance.jsx
new file mode 100644
index 000000000000..d7387f849400
--- /dev/null
+++ b/src/views/cipp/app-settings/SettingsMaintenance.jsx
@@ -0,0 +1,161 @@
+import React, { useState } from 'react'
+import { useLazyGenericGetRequestQuery } from 'src/store/api/app.js'
+import {
+ CButton,
+ CCard,
+ CCardBody,
+ CCardHeader,
+ CCardTitle,
+ CCol,
+ CForm,
+ CRow,
+} from '@coreui/react'
+import { Form } from 'react-final-form'
+import Skeleton from 'react-loading-skeleton'
+import { RFFCFormSelect } from 'src/components/forms/index.js'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { faLink, faScroll } from '@fortawesome/free-solid-svg-icons'
+import { CippCodeBlock } from 'src/components/utilities/index.js'
+import { Buffer } from 'buffer'
+
+/**
+ * Performs maintenance operations on settings.
+ *
+ * @returns {JSX.Element} The JSX element representing the settings maintenance component.
+ */
+export function SettingsMaintenance() {
+ const [selectedScript, setSelectedScript] = useState()
+ const [listBackend, listBackendResult] = useLazyGenericGetRequestQuery()
+ const [listScript, listScriptResult] = useLazyGenericGetRequestQuery()
+ const [listScriptLink, listScriptLinkResult] = useLazyGenericGetRequestQuery()
+
+ const handleSubmit = async (values) => {
+ listScript({ path: 'api/ExecMaintenanceScripts', params: values })
+ setSelectedScript(values.ScriptFile)
+ }
+
+ const handleGetLink = () => {
+ listScriptLink({
+ path: 'api/ExecMaintenanceScripts',
+ params: { ScriptFile: selectedScript, MakeLink: 'True' },
+ })
+ }
+ return (
+ <>
+ {listBackendResult.isUninitialized && listBackend({ path: 'api/ExecMaintenanceScripts' })}
+
+
+
+
+ Maintenance
+
+
+
+
+
+
+
+
+ {listScriptResult.isFetching && (
+
+
+
+
+
+ )}
+ {!listScriptResult.isFetching && listScriptResult.isSuccess && (
+
+
+ Script Details
+
+
+
+
+
+ Create Link
+
+
+ {listScriptLinkResult.isSuccess && (
+
+ {listScriptLinkResult.data.Link !== undefined && (
+ <>
+
+ Copy this text into a PowerShell terminal, we recommend Azure Cloud Shell.
+ Azure modules and the az command line utilties are required for these
+ scripts to work. The link is valid for 5 minutes.
+
+
+ >
+ )}
+
+ )}
+ {listScriptResult.data.ScriptContent !== undefined && (
+
+
Maintenance Script Contents
+
+
+ )}
+
+
+ )}
+
+
+ >
+ )
+}
diff --git a/src/views/cipp/app-settings/SettingsNotifications.jsx b/src/views/cipp/app-settings/SettingsNotifications.jsx
new file mode 100644
index 000000000000..6a0b1b73450a
--- /dev/null
+++ b/src/views/cipp/app-settings/SettingsNotifications.jsx
@@ -0,0 +1,182 @@
+import {
+ useLazyExecNotificationConfigQuery,
+ useLazyListNotificationConfigQuery,
+} from 'src/store/api/app.js'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
+import {
+ CButton,
+ CCallout,
+ CCard,
+ CCardBody,
+ CCardHeader,
+ CCardTitle,
+ CCol,
+ CForm,
+ CSpinner,
+} from '@coreui/react'
+import { Form, useForm } from 'react-final-form'
+import { RFFCFormInput, RFFCFormSwitch, RFFSelectSearch } from 'src/components/forms/index.js'
+import React from 'react'
+import { CippCallout } from 'src/components/layout/index.js'
+
+/**
+ * Sets the notification settings.
+ * @returns {JSX.Element} The notification settings component.
+ */
+export function SettingsNotifications() {
+ const [configNotifications, notificationConfigResult] = useLazyExecNotificationConfigQuery()
+ const [listNotification, notificationListResult] = useLazyListNotificationConfigQuery()
+
+ const onSubmit = (values) => {
+ configNotifications(values)
+ }
+ return (
+ <>
+ {notificationListResult.isUninitialized && listNotification()}
+ {notificationListResult.isFetching && (
+
+ )}
+ {!notificationListResult.isFetching && notificationListResult.error && (
+ Error loading data
+ )}
+ {notificationListResult.isSuccess && (
+
+
+ Notifications
+
+
+
+
+ )}
+ >
+ )
+}
diff --git a/src/views/cipp/app-settings/SettingsTenants.jsx b/src/views/cipp/app-settings/SettingsTenants.jsx
new file mode 100644
index 000000000000..c425f3c1ae97
--- /dev/null
+++ b/src/views/cipp/app-settings/SettingsTenants.jsx
@@ -0,0 +1,285 @@
+import { useDispatch, useSelector } from 'react-redux'
+import {
+ useExecAddExcludeTenantMutation,
+ useExecRemoveExcludeTenantMutation,
+} from 'src/store/api/tenants.js'
+import { useLazyGenericGetRequestQuery } from 'src/store/api/app.js'
+import React, { useEffect, useRef } from 'react'
+import { ModalService, TenantSelectorMultiple } from 'src/components/utilities/index.js'
+import { setCurrentTenant } from 'src/store/features/app.js'
+import { CAlert, CButton, CCallout, CSpinner, CTooltip } from '@coreui/react'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import {
+ faCheckCircle,
+ faExclamationTriangle,
+ faEye,
+ faEyeSlash,
+ faRecycle,
+} from '@fortawesome/free-solid-svg-icons'
+import { cellBooleanFormatter, CellTip } from 'src/components/tables/index.js'
+import { CippCallout, CippPageList } from 'src/components/layout/index.js'
+
+/**
+ * The SettingsTenants method is used to manage the tenants in the application. It allows the user to add or
+ * remove exclusions, refresh permissions for a tenant, and view the list of excluded tenants.
+ *
+ * @return {JSXElement} The rendered component for managing the excluded tenants.
+ */
+export function SettingsTenants() {
+ const dispatch = useDispatch()
+ const currentTenant = useSelector((state) => state.app.currentTenant)
+ const [removeExcludeTenant, removeExcludeTenantResult] = useExecRemoveExcludeTenantMutation()
+ const [addExcludeTenant, addExcludeTenantResult] = useExecAddExcludeTenantMutation()
+ const [refreshPermissions, refreshPermissionsResults] = useLazyGenericGetRequestQuery()
+
+ // const [selectedTenant, setSelectedTenant] = useState()
+ const selectedTenant = useRef()
+
+ useEffect(() => {
+ // if a tenant is already selected and that's the tenant the
+ // user wants to exclude, we need to set that to the current state
+ selectedTenant.current = currentTenant
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ const handleRemoveExclusion = (domain) =>
+ ModalService.confirm({
+ title: 'Remove Exclusion',
+ body: Are you sure you want to remove the exclusion for {domain}?
,
+ onConfirm: () => removeExcludeTenant(domain),
+ })
+
+ const handleCPVPermissions = (domain, resetsp = false) =>
+ ModalService.confirm({
+ title: 'Refresh Permissions',
+ body: Are you sure you want to refresh permissions for {domain.defaultDomainName}?
,
+ onConfirm: () =>
+ refreshPermissions({
+ path: `/api/ExecCPVPermissions?TenantFilter=${domain.customerId}&ResetSP=${resetsp}`,
+ }),
+ })
+ const handleConfirmExcludeTenant = (tenant) => {
+ ModalService.confirm({
+ title: 'Exclude Tenant',
+ body: Are you sure you want to exclude this tenant?
,
+ onConfirm: () => addExcludeTenant(tenant),
+ })
+ .unwrap()
+ .then(() => {
+ dispatch(setCurrentTenant({}))
+ })
+ }
+
+ const handleExcludeTenant = (selected) => {
+ ModalService.confirm({
+ body: (
+
+
Select a tenant to exclude
+
(selected = tenant)} />
+
+ ),
+ title: 'Add Exclusion',
+ onConfirm: () => handleConfirmExcludeTenant(selected),
+ })
+ }
+ const titleButton = (
+ handleExcludeTenant(selectedTenant)}
+ >
+ Add Excluded Tenant
+
+ )
+
+ function StatusIcon(graphErrorCount) {
+ if (graphErrorCount > 0) {
+ return
+ } else {
+ return
+ }
+ }
+
+ function StatusText(graphErrorCount, lastGraphError) {
+ if (graphErrorCount > 0) {
+ return 'Error Count: ' + graphErrorCount + ' - Last Error: ' + lastGraphError
+ } else {
+ return 'No errors detected with this tenant'
+ }
+ }
+
+ const Offcanvas = (row, rowIndex, formatExtraData) => {
+ return (
+ <>
+ {row.Excluded && (
+
+ handleRemoveExclusion(row.defaultDomainName)}
+ >
+
+
+
+ )}
+ {!row.Excluded && (
+
+ handleConfirmExcludeTenant({ value: row.customerId })}
+ >
+
+
+
+ )}
+
+ handleCPVPermissions(row, false)}
+ >
+
+
+
+ >
+ )
+ }
+ const columns = [
+ {
+ name: 'Name',
+ selector: (row) => row['displayName'],
+ sortable: true,
+ cell: (row) => CellTip(row['displayName']),
+ exportSelector: 'displayName',
+ },
+ {
+ name: 'Default Domain',
+ selector: (row) => row['defaultDomainName'],
+ sortable: true,
+ cell: (row) => CellTip(row['defaultDomainName']),
+ exportSelector: 'defaultDomainName',
+ },
+ {
+ name: 'Excluded',
+ selector: (row) => row['Excluded'],
+ sortable: true,
+ cell: cellBooleanFormatter({ colourless: true }),
+ exportSelector: 'Excluded',
+ maxWidth: '100px',
+ minWidth: '100px',
+ },
+ {
+ name: 'Exclude Date',
+ selector: (row) => row['ExcludeDate'],
+ sortable: true,
+ exportSelector: 'ExcludeDate',
+ maxWidth: '150px',
+ minWidth: '150px',
+ },
+ {
+ name: 'Exclude User',
+ selector: (row) => row['ExcludeUser'],
+ sortable: true,
+ exportSelector: 'ExcludeUser',
+ maxWidth: '130px',
+ minWidth: '130px',
+ },
+ {
+ name: 'Actions',
+ cell: Offcanvas,
+ maxWidth: '80px',
+ },
+ ]
+ return (
+ <>
+ {(refreshPermissionsResults.isFetching || removeExcludeTenantResult.isFetching) && (
+
+
+
+ )}
+ {removeExcludeTenantResult.isSuccess && !removeExcludeTenantResult.isFetching && (
+
+ {removeExcludeTenantResult.data?.Results}
+
+ )}
+ {refreshPermissionsResults.isSuccess &&
+ refreshPermissionsResults.data?.Results &&
+ !refreshPermissionsResults.isFetching &&
+ Array.isArray(refreshPermissionsResults.data.Results) ? (
+
+ {refreshPermissionsResults.data.Results.map((result, idx) => (
+ {result}
+ ))}
+
+ ) : null}
+ {addExcludeTenantResult.isSuccess && !addExcludeTenantResult.isFetching && (
+
+ {addExcludeTenantResult.data?.Results}
+
+ )}
+
+ >
+ )
+}
diff --git a/src/views/cipp/app-settings/components/SettingsDNSResolver.jsx b/src/views/cipp/app-settings/components/SettingsDNSResolver.jsx
new file mode 100644
index 000000000000..a656578a185a
--- /dev/null
+++ b/src/views/cipp/app-settings/components/SettingsDNSResolver.jsx
@@ -0,0 +1,50 @@
+import { CAlert, CButton, CButtonGroup } from '@coreui/react'
+import React, { useState } from 'react'
+import { useLazyEditDnsConfigQuery, useLazyGetDnsConfigQuery } from 'src/store/api/domains.js'
+import { CippCallout } from 'src/components/layout/index.js'
+
+/**
+ * Sets the DNS resolver based on user selection.
+ *
+ * @return {JSX.Element} - The component that renders the DNS resolver settings.
+ */
+export function SettingsDNSResolver() {
+ const resolvers = ['Google', 'Cloudflare', 'Quad9']
+ const [getDnsConfig, getDnsConfigResult] = useLazyGetDnsConfigQuery()
+ const [editDnsConfig, editDnsConfigResult] = useLazyEditDnsConfigQuery()
+
+ const switchResolver = async (resolver) => {
+ await editDnsConfig({ resolver })
+ await getDnsConfig()
+ }
+
+ return (
+ <>
+ {getDnsConfigResult.isUninitialized && getDnsConfig()}
+ {getDnsConfigResult.isSuccess && (
+ <>
+ DNS Resolver
+
+ {resolvers.map((resolver, index) => (
+ switchResolver(resolver)}
+ color={resolver === getDnsConfigResult.data.Resolver ? 'primary' : 'secondary'}
+ key={index}
+ >
+ {resolver}
+
+ ))}
+
+ {(editDnsConfigResult.isSuccess || editDnsConfigResult.isError) &&
+ !editDnsConfigResult.isFetching && (
+
+ {editDnsConfigResult.isSuccess
+ ? editDnsConfigResult.data.Results
+ : 'Error setting resolver'}
+
+ )}
+ >
+ )}
+ >
+ )
+}
diff --git a/src/views/cipp/app-settings/components/SettingsGeneralRow.jsx b/src/views/cipp/app-settings/components/SettingsGeneralRow.jsx
new file mode 100644
index 000000000000..5e1f01f91e39
--- /dev/null
+++ b/src/views/cipp/app-settings/components/SettingsGeneralRow.jsx
@@ -0,0 +1,178 @@
+import {
+ useLazyExecClearCacheQuery,
+ useLazyGenericGetRequestQuery,
+ useLazyGenericPostRequestQuery,
+ useLoadVersionsQuery,
+} from 'src/store/api/app.js'
+import React, { useRef } from 'react'
+import useConfirmModal from 'src/hooks/useConfirmModal.jsx'
+import { CButton, CCard, CCardBody, CCardHeader, CCol, CRow } from '@coreui/react'
+import { StatusIcon } from 'src/components/utilities/index.js'
+import { CippCallout } from 'src/components/layout/index.js'
+import Skeleton from 'react-loading-skeleton'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
+import { SettingsPassword } from 'src/views/cipp/app-settings/components/SettingsPassword.jsx'
+import { SettingsDNSResolver } from 'src/views/cipp/app-settings/components/SettingsDNSResolver.jsx'
+
+/**
+ * Fetches and maintains DNS configuration settings for the application.
+ *
+ * @return {JSX.Element | void} The settings DNS component or nothing if data not ready.
+ */
+export function SettingsGeneralRow() {
+ const [runBackup, RunBackupResult] = useLazyGenericGetRequestQuery()
+ const [restoreBackup, restoreBackupResult] = useLazyGenericPostRequestQuery()
+
+ const inputRef = useRef(null)
+ const [clearCache, clearCacheResult] = useLazyExecClearCacheQuery()
+ const { data: versions, isSuccess: isSuccessVersion } = useLoadVersionsQuery()
+
+ const downloadTxtFile = (data) => {
+ const txtdata = [JSON.stringify(RunBackupResult.data.backup)]
+ const file = new Blob(txtdata, { type: 'text/plain' })
+ const element = document.createElement('a')
+ element.href = URL.createObjectURL(file)
+ element.download = 'CIPP-Backup' + Date.now() + '.json'
+ document.body.appendChild(element)
+ element.click()
+ }
+ const handleChange = (e) => {
+ const fileReader = new FileReader()
+ fileReader.readAsText(e.target.files[0], 'UTF-8')
+ fileReader.onload = (e) => {
+ restoreBackup({ path: '/api/ExecRestoreBackup', values: e.target.result })
+ }
+ }
+
+ const handleClearCache = useConfirmModal({
+ body: Are you sure you want to clear the cache?
,
+ onConfirm: () => {
+ clearCache({ tenantsOnly: false })
+ localStorage.clear()
+ },
+ })
+
+ const handleClearCacheTenant = useConfirmModal({
+ body: Are you sure you want to clear the cache?
,
+ onConfirm: () => {
+ clearCache({ tenantsOnly: true })
+ },
+ })
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ Frontend Version
+
+ Latest: {isSuccessVersion ? versions.RemoteCIPPVersion : }
+
+ Current: {isSuccessVersion ? versions.LocalCIPPVersion : }
+
+
+
+ Clear Caches
+ handleClearCache()}
+ disabled={clearCacheResult.isFetching}
+ >
+ {clearCacheResult.isFetching && (
+
+ )}
+ Clear All Cache
+
+ handleClearCacheTenant()}
+ disabled={clearCacheResult.isFetching}
+ >
+ {clearCacheResult.isFetching && (
+
+ )}
+ Clear Tenant Cache
+
+ {clearCacheResult.isSuccess && !clearCacheResult.isFetching && (
+
+ {clearCacheResult.data?.Results}
+
+ )}
+
+
+ Settings Backup
+ runBackup({ path: '/api/ExecRunBackup' })}
+ disabled={RunBackupResult.isFetching}
+ >
+ {RunBackupResult.isFetching && (
+
+ )}
+ Run backup
+
+ handleChange(e)}
+ />
+ inputRef.current.click()}
+ disabled={restoreBackupResult.isFetching}
+ >
+ {restoreBackupResult.isFetching && (
+
+ )}
+ Restore backup
+
+ {restoreBackupResult.isSuccess && !restoreBackupResult.isFetching && (
+
+ {restoreBackupResult.data.Results}
+
+ )}
+ {RunBackupResult.isSuccess && !restoreBackupResult.isFetching && (
+
+ downloadTxtFile(RunBackupResult.data.backup)}>
+ Download Backup
+
+
+ )}
+
+
+ Backend API Version
+
+ Latest: {isSuccessVersion ? versions.RemoteCIPPAPIVersion : }
+
+ Current: {isSuccessVersion ? versions.LocalCIPPAPIVersion : }
+
+
+
+
+
+ >
+ )
+}
diff --git a/src/views/cipp/app-settings/components/SettingsPassword.jsx b/src/views/cipp/app-settings/components/SettingsPassword.jsx
new file mode 100644
index 000000000000..aa8ed046d2b5
--- /dev/null
+++ b/src/views/cipp/app-settings/components/SettingsPassword.jsx
@@ -0,0 +1,68 @@
+import { useLazyGenericGetRequestQuery, useLazyGenericPostRequestQuery } from 'src/store/api/app.js'
+import React, { useState } from 'react'
+import { CButton, CButtonGroup, CCallout } from '@coreui/react'
+import { CippCallout } from 'src/components/layout/index.js'
+
+/**
+ * This method is responsible for handling password settings in the application.
+ * It uses two custom hooks, `useLazyGenericGetRequestQuery()` and `useLazyGenericPostRequestQuery()`,
+ * to fetch and update password configuration data respectively.
+ *
+ * The method maintains the state of a password alert visibility using the `useState()` hook.
+ *
+ * It also provides a switchResolver function that updates the password configuration using the editPasswordConfig function.
+ * After updating the configuration, it fetches the updated configuration using getPasswordConfig to reflect the changes.
+ * Finally, it sets the password alert visibility to true.
+ *
+ * The method renders a password style section in the UI which displays a list of resolvers.
+ * The resolver that matches the current password configuration is highlighted with a primary color button.
+ * By clicking on a resolver button, the switchResolver function is called to update the password configuration and show the password alert.
+ *
+ * @returns {JSXElement} The rendered password settings component with the password style section and password alert section.
+ */
+export function SettingsPassword() {
+ const [getPasswordConfig, getPasswordConfigResult] = useLazyGenericGetRequestQuery()
+ const [editPasswordConfig, editPasswordConfigResult] = useLazyGenericPostRequestQuery()
+
+ const switchResolver = async (resolver) => {
+ await editPasswordConfig({
+ path: '/api/ExecPasswordConfig',
+ values: { passwordType: resolver },
+ })
+ await getPasswordConfig({ path: '/api/ExecPasswordConfig', params: { list: true } })
+ }
+
+ const resolvers = ['Classic', 'Correct-Battery-Horse']
+
+ return (
+ <>
+ {getPasswordConfigResult.isUninitialized &&
+ getPasswordConfig({ path: '/api/ExecPasswordConfig?list=true' })}
+ Password Style
+
+ {resolvers.map((r, index) => (
+ switchResolver(r)}
+ color={
+ r === getPasswordConfigResult.data?.Results?.passwordType ? 'primary' : 'secondary'
+ }
+ key={index}
+ >
+ {r}
+
+ ))}
+
+ {(editPasswordConfigResult.isSuccess || editPasswordConfigResult.isError) &&
+ !editPasswordConfigResult.isFetching && (
+
+ {editPasswordConfigResult.isSuccess
+ ? editPasswordConfigResult.data.Results
+ : 'Error setting password style'}
+
+ )}
+ >
+ )
+}
diff --git a/src/views/email-exchange/administration/ContactsList.jsx b/src/views/email-exchange/administration/ContactsList.jsx
index 094f3c6930f3..f0709de26be3 100644
--- a/src/views/email-exchange/administration/ContactsList.jsx
+++ b/src/views/email-exchange/administration/ContactsList.jsx
@@ -80,6 +80,7 @@ const columns = [
selector: (row) => row['id'],
name: 'id',
omit: true,
+ exportSelector: 'id',
},
{
selector: (row) => row['onPremisesSyncEnabled'],
diff --git a/src/views/email-exchange/administration/EditMailboxPermissions.jsx b/src/views/email-exchange/administration/EditMailboxPermissions.jsx
index 945129569e61..3f16a1e389d9 100644
--- a/src/views/email-exchange/administration/EditMailboxPermissions.jsx
+++ b/src/views/email-exchange/administration/EditMailboxPermissions.jsx
@@ -19,7 +19,7 @@ import useQuery from 'src/hooks/useQuery'
import { useDispatch } from 'react-redux'
import { Form, Field } from 'react-final-form'
import { RFFSelectSearch, RFFCFormCheck, RFFCFormInput, RFFCFormSwitch } from 'src/components/forms'
-import { ModalService } from 'src/components/utilities'
+import { CippLazy, ModalService } from 'src/components/utilities'
import {
useLazyGenericPostRequestQuery,
useLazyGenericGetRequestQuery,
@@ -36,23 +36,6 @@ import PropTypes from 'prop-types'
const formatter = (cell, warning = false, reverse = false, colourless = false) =>
CellBoolean({ cell, warning, reverse, colourless })
-function Lazy({ visible, children }) {
- const rendered = useRef(visible)
-
- if (visible && !rendered.current) {
- rendered.current = true
- }
-
- if (!rendered.current) return null
-
- return {children}
-}
-
-Lazy.propTypes = {
- visible: PropTypes.bool,
- children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]),
-}
-
const MailboxSettings = () => {
const dispatch = useDispatch()
let query = useQuery()
@@ -128,24 +111,24 @@ const MailboxSettings = () => {
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -531,8 +514,14 @@ const CalendarPermissions = () => {
name: 'Publishing Editor',
},
{ value: 'Reviewer', name: 'Reviewer' },
- { value: 'LimitedDetails', name: 'Limited Details' },
- { value: 'AvailabilityOnly', name: 'Availability Only' },
+ {
+ value: 'LimitedDetails',
+ name: 'Limited Details',
+ },
+ {
+ value: 'AvailabilityOnly',
+ name: 'Availability Only',
+ },
]}
placeholder="Select a permission level"
name="Permissions"
diff --git a/src/views/email-exchange/transport/TransportRules.jsx b/src/views/email-exchange/transport/TransportRules.jsx
index 1a32de02b5e3..4e6f3f68a35d 100644
--- a/src/views/email-exchange/transport/TransportRules.jsx
+++ b/src/views/email-exchange/transport/TransportRules.jsx
@@ -100,11 +100,13 @@ const columns = [
name: 'description',
selector: (row) => row['Description'],
omit: true,
+ exportSelector: 'Description',
},
{
name: 'GUID',
selector: (row) => row['Guid'],
omit: true,
+ exportSelector: 'Guid',
},
{
name: 'Actions',
diff --git a/src/views/endpoint/intune/MEMListPolicies.jsx b/src/views/endpoint/intune/MEMListPolicies.jsx
index ad45fc5485fe..c07216317afc 100644
--- a/src/views/endpoint/intune/MEMListPolicies.jsx
+++ b/src/views/endpoint/intune/MEMListPolicies.jsx
@@ -101,6 +101,7 @@ const columns = [
selector: (row) => row['id'],
name: 'id',
omit: true,
+ exportSelector: 'id',
},
{
name: 'Actions',
diff --git a/src/views/identity/administration/Deleted.jsx b/src/views/identity/administration/Deleted.jsx
index 1c7e35c72987..2e925eed5bb6 100644
--- a/src/views/identity/administration/Deleted.jsx
+++ b/src/views/identity/administration/Deleted.jsx
@@ -93,6 +93,7 @@ const columns = [
name: 'id',
selector: (row) => row['id'],
omit: true,
+ exportSelector: 'id',
},
{
name: 'Actions',
diff --git a/src/views/identity/administration/Devices.jsx b/src/views/identity/administration/Devices.jsx
index aa608f343977..b67740e3136d 100644
--- a/src/views/identity/administration/Devices.jsx
+++ b/src/views/identity/administration/Devices.jsx
@@ -7,6 +7,7 @@ import { CButton } from '@coreui/react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faEdit, faEllipsisV } from '@fortawesome/free-solid-svg-icons'
import { CippActionsOffcanvas } from 'src/components/utilities'
+import { cellGenericFormatter } from 'src/components/tables/CellGenericFormat'
const DevicesList = () => {
const [tenantColumnSet, setTenantColumn] = useState(true)
@@ -28,6 +29,13 @@ const DevicesList = () => {
{ label: 'Unique ID', value: `${row.id ?? ' '}` },
]}
actions={[
+ {
+ label: 'Enable Device',
+ color: 'info',
+ modal: true,
+ modalUrl: `/api/ExecDeviceDelete?TenantFilter=${tenant.defaultDomainName}&ID=${row.id}&Action=Enable`,
+ modalMessage: 'Are you sure you want to enable this device.',
+ },
{
label: 'Disable Device',
color: 'info',
@@ -39,7 +47,7 @@ const DevicesList = () => {
label: 'Delete Device',
color: 'warning',
modal: true,
- modalUrl: `/api/ExecGroupsDelete?TenantFilter=${tenant.defaultDomainName}&ID=${row.id}&Action=Enable`,
+ modalUrl: `/api/ExecDeviceDelete?TenantFilter=${tenant.defaultDomainName}&ID=${row.id}&Action=Delete`,
modalMessage: 'Are you sure you want to delete this device.',
},
]}
@@ -75,6 +83,13 @@ const DevicesList = () => {
cell: (row) => CellTip(row['displayName']),
exportSelector: 'displayName',
},
+ {
+ selector: (row) => row['accountEnabled'],
+ name: 'Enabled',
+ sortable: true,
+ cell: cellGenericFormatter(),
+ exportSelector: 'accountEnabled',
+ },
{
selector: (row) => row['deviceOwnership'],
name: 'Device Ownership',
@@ -146,6 +161,29 @@ const DevicesList = () => {
$format: 'application/json',
},
columns,
+ tableProps: {
+ selectableRows: true,
+ actionsList: [
+ {
+ label: 'Enable Device',
+ modal: true,
+ modalUrl: `/api/ExecDeviceDelete?TenantFilter=!Tenant&ID=!id&Action=Enable`,
+ modalMessage: 'Are you sure you want to enable this device?',
+ },
+ {
+ label: 'Disable Device',
+ modal: true,
+ modalUrl: `/api/ExecDeviceDelete?TenantFilter=!Tenant&ID=!id&Action=Disable`,
+ modalMessage: 'Are you sure you want to disable this device?',
+ },
+ {
+ label: 'Delete Device',
+ modal: true,
+ modalUrl: `/api/ExecDeviceDelete?TenantFilter=!Tenant&ID=!id&Action=Delete`,
+ modalMessage: 'Are you sure you want to delete this device?',
+ },
+ ],
+ },
}}
/>
)
diff --git a/src/views/identity/administration/UserDetails.jsx b/src/views/identity/administration/UserDetails.jsx
index bcf1ab9f0760..51d5837052ae 100644
--- a/src/views/identity/administration/UserDetails.jsx
+++ b/src/views/identity/administration/UserDetails.jsx
@@ -58,7 +58,7 @@ export default function UserDetails({ tenantDomain, userId, className = null })
},
{
heading: 'Postcode',
- dataField: user.postalCode,
+ body: user.postalCode,
},
{
heading: 'Country',
diff --git a/src/views/identity/administration/Users.jsx b/src/views/identity/administration/Users.jsx
index 774f25a29c2d..ef2ce6722b7a 100644
--- a/src/views/identity/administration/Users.jsx
+++ b/src/views/identity/administration/Users.jsx
@@ -399,6 +399,7 @@ const Users = (row) => {
name: 'id',
selector: (row) => row['id'],
omit: true,
+ exportSelector: 'id',
},
{
name: 'Actions',
diff --git a/src/views/teams-share/teams/TeamsListTeam.jsx b/src/views/teams-share/teams/TeamsListTeam.jsx
index cbbf0a818d9e..c4d59d7dd9a3 100644
--- a/src/views/teams-share/teams/TeamsListTeam.jsx
+++ b/src/views/teams-share/teams/TeamsListTeam.jsx
@@ -62,6 +62,7 @@ const TeamsList = () => {
name: 'ID',
selector: (row) => row['id'],
omit: true,
+ exportSelector: 'id',
},
{
name: 'Actions',
diff --git a/src/views/tenant/administration/GraphExplorer.jsx b/src/views/tenant/administration/GraphExplorer.jsx
index 8f38821ec387..42cc4ac2ce8c 100644
--- a/src/views/tenant/administration/GraphExplorer.jsx
+++ b/src/views/tenant/administration/GraphExplorer.jsx
@@ -40,7 +40,6 @@ const GraphExplorer = () => {
const [alertVisible, setAlertVisible] = useState()
const [random, setRandom] = useState('')
const [random2, setRandom2] = useState('')
- const [random3, setRandom3] = useState('')
const [ocVisible, setOCVisible] = useState(false)
const [searchNow, setSearchNow] = useState(false)
const [visibleA, setVisibleA] = useState(true)
@@ -59,21 +58,21 @@ const GraphExplorer = () => {
} = useGenericGetRequestQuery({ path: '/api/ListGraphExplorerPresets', params: { random2 } })
const QueryColumns = { set: false, data: [] }
- function endpointChange(value) {
- execPropRequest({
- path: '/api/ListGraphRequest',
- params: {
- Endpoint: value,
- ListProperties: true,
- TenantFilter: tenant.defaultDomainName,
- IgnoreErrors: true,
- random: (Math.random() + 1).toString(36).substring(7),
- },
- })
- }
const debounceEndpointChange = useMemo(() => {
+ function endpointChange(value) {
+ execPropRequest({
+ path: '/api/ListGraphRequest',
+ params: {
+ Endpoint: value,
+ ListProperties: true,
+ TenantFilter: tenant.defaultDomainName,
+ IgnoreErrors: true,
+ random: (Math.random() + 1).toString(36).substring(7),
+ },
+ })
+ }
return debounce(endpointChange, 1000)
- }, [endpointChange])
+ }, [])
if (graphrequest.isSuccess) {
if (graphrequest.data?.Results?.length > 0) {
@@ -483,6 +482,7 @@ const GraphExplorer = () => {
)
}}
+
@@ -504,13 +504,6 @@ const GraphExplorer = () => {
/>
-
-
{
: []
}
allowCreate={true}
- refreshFunction={() =>
- setRandom3((Math.random() + 1).toString(36).substring(7))
- }
isLoading={availableProperties.isFetching}
/>
+
+
{
{!searchNow && Execute a search to get started. }
- {graphrequest.isFetching && (
+ {graphrequest.isFetching && !QueryColumns.set && (
Loading Data
@@ -593,6 +590,7 @@ const GraphExplorer = () => {
columns={QueryColumns.data}
data={graphrequest?.data?.Results}
isFetching={graphrequest.isFetching}
+ refreshFunction={() => setRandom((Math.random() + 1).toString(36).substring(7))}
/>
>
diff --git a/src/views/tenant/administration/ListAlertsQueue.jsx b/src/views/tenant/administration/ListAlertsQueue.jsx
index a952a7e414d0..1c155771a9bd 100644
--- a/src/views/tenant/administration/ListAlertsQueue.jsx
+++ b/src/views/tenant/administration/ListAlertsQueue.jsx
@@ -2,7 +2,7 @@ import React, { useState } from 'react'
import { CButton, CCallout, CCol, CForm, CRow, CSpinner, CTooltip } from '@coreui/react'
import { useSelector } from 'react-redux'
import { Field, Form, FormSpy } from 'react-final-form'
-import { RFFCFormSwitch } from 'src/components/forms'
+import { RFFCFormInput, RFFCFormSwitch } from 'src/components/forms'
import {
useGenericGetRequestQuery,
useLazyGenericGetRequestQuery,
@@ -14,7 +14,6 @@ import { CippContentCard, CippPage, CippPageList } from 'src/components/layout'
import { CellTip } from 'src/components/tables/CellGenericFormat'
import 'react-datepicker/dist/react-datepicker.css'
import { CippActionsOffcanvas, ModalService, TenantSelector } from 'src/components/utilities'
-import CippCodeOffCanvas from 'src/components/utilities/CippCodeOffcanvas'
import arrayMutators from 'final-form-arrays'
const alertsList = [
{ name: 'MFAAlertUsers', label: 'Alert on users without any form of MFA' },
@@ -30,12 +29,14 @@ const alertsList = [
label: 'Alert on % mailbox quota used',
requiresInput: true,
inputLabel: 'Enter quota percentage',
+ inputName: 'QuotaUsedQuota',
},
{
name: 'SharePointQuota',
label: 'Alert on % SharePoint quota used',
requiresInput: true,
inputLabel: 'Enter quota percentage',
+ inputName: 'SharePointQuotaQuota',
},
{ name: 'ExpiringLicenses', label: 'Alert on licenses expiring in 30 days' },
{ name: 'SecDefaultsUpsell', label: 'Alert on Security Defaults automatic enablement' },
@@ -215,16 +216,13 @@ const ListClassicAlerts = () => {
{alert.requiresInput && (
{({ values }) => {
- // Check if the switch for this alert is turned on before showing the input
if (values[alert.name]) {
return (
- Number(value)} // Ensure value is a number
+ type="number"
/>
)
}
diff --git a/src/views/tenant/conditional/ConditionalAccess.jsx b/src/views/tenant/conditional/ConditionalAccess.jsx
index 0684781a6928..650daa4e4fa4 100644
--- a/src/views/tenant/conditional/ConditionalAccess.jsx
+++ b/src/views/tenant/conditional/ConditionalAccess.jsx
@@ -206,6 +206,7 @@ const columns = [
name: 'rawjson',
selector: (row) => row['rawjson'],
omit: true,
+ exportSelector: 'rawjson',
},
{
name: 'Actions',