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 && ( + + )} +
+ ) +} + +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 - - -
    true} - initialValues={{ - ...notificationListResult.data, - logsToInclude: notificationListResult.data?.logsToInclude?.map((m) => ({ - label: m, - value: m, - })), - Severity: notificationListResult.data?.Severity?.map((s) => ({ - label: s, - value: s, - })), - }} - onSubmit={onSubmit} - render={({ handleSubmit, submitting, values }) => { - return ( - - {notificationConfigResult.isFetching && ( - - Loading - - )} - {notificationConfigResult.isSuccess && ( - {notificationConfigResult.data?.Results} - )} - {notificationConfigResult.isError && ( - - Could not connect to API: {notificationConfigResult.error.message} - - )} - - - - - - - - - - - - - - - - - - - - - - - - Set Notification Settings - - - - ) - }} - /> - - - )} - - ) -} - -const LicenseSettings = () => { - const [setExclusion, setExclusionResults] = useLazyGenericPostRequestQuery() - const formRef = useRef(null) - - const handleAddLicense = (selected) => { - ModalService.confirm({ - body: ( -
    - { - formRef.current = values - return ( - <> -
    Add a license to exclude
    - - - - ) - }} - /> -
    - ), - 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}

    - { - return ( - - - - {integration.SettingOptions.map( - (integrationOptions, idx) => - integrationOptions.type === 'input' && ( - - - - ), - )} - {integration.SettingOptions.map( - (integrationOptions, idx) => - integrationOptions.type === 'checkbox' && ( - - - - ), - )} - - - - - - {extensionConfigResult.isFetching && ( - - )} - Set Extension Settings - - onSubmitTest(integration.type)} - className="me-2" - > - {listExtensionTestResult.isFetching && ( - - )} - Test Extension - - {integration.forceSyncButton && ( - - execSyncExtension({ - path: 'api/ExecExtensionSync?Extension=' + integration.type, - }) - } - className="me-2" - > - {listSyncExtensionResult.isFetching && ( - - )} - Force Sync - - )} - - - ) - }} - /> -
    -
    -
    - ))} -
    - -
    - ) -} - -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 ? ( - - ) : ( - { - return ( - - - Use the table below to map your client to the correct PSA client - {listBackendHaloResult.isSuccess && - listBackendHaloResult.data.Tenants?.map((tenant) => ( - - ))} - - - - {extensionHaloConfigResult.isFetching && ( - - )} - Set Mappings - - {(extensionHaloConfigResult.isSuccess || - extensionHaloConfigResult.isError) && ( - - {extensionHaloConfigResult.isSuccess - ? extensionHaloConfigResult.data.Results - : 'Error'} - - )} - - - ) - }} - /> - )} - - - - - NinjaOne Field Mapping Table - - - {listBackendNinjaFieldsResult.isFetching ? ( - - ) : ( - { - return ( - - -
    Organization Global Custom Field Mapping
    -

    - Use the table below to map your Organization Field to the correct NinjaOne - Field -

    - {listBackendNinjaFieldsResult.isSuccess && - listBackendNinjaFieldsResult.data.CIPPOrgFields.map((CIPPOrgFields) => ( - item.type === CIPPOrgFields.Type || item.type === 'unset', - )} - placeholder="Select a Field" - /> - ))} -
    - -
    Device Custom Field Mapping
    -

    - Use the table below to map your Device field to the correct NinjaOne - WYSIWYG Field -

    - {listBackendNinjaFieldsResult.isSuccess && - listBackendNinjaFieldsResult.data.CIPPNodeFields.map((CIPPNodeFields) => ( - - item.type === CIPPNodeFields.Type || item.type === 'unset', - )} - placeholder="Select a Field" - /> - ))} -
    - - - {extensionNinjaFieldsConfigResult.isFetching && ( - - )} - Set Mappings - - {(extensionNinjaFieldsConfigResult.isSuccess || - extensionNinjaFieldsConfigResult.isError) && ( - - {extensionNinjaFieldsConfigResult.isSuccess - ? extensionNinjaFieldsConfigResult.data.Results - : 'Error'} - - )} - -
    - ) - }} - /> - )} -
    -
    - - - NinjaOne Organization Mapping Table - - - {listBackendNinjaOrgsResult.isFetching ? ( - - ) : ( - { - return ( - - - Use the table below to map your client to the correct NinjaOne Organization - {listBackendNinjaOrgsResult.isSuccess && - listBackendNinjaOrgsResult.data.Tenants.map((tenant) => ( - - ))} - - - - {extensionNinjaOrgsConfigResult.isFetching && ( - - )} - Set Mappings - - onNinjaOrgsAutomap()} className="me-2"> - {extensionNinjaOrgsAutomapResult.isFetching && ( - - )} - Automap NinjaOne Organizations - - {(extensionNinjaOrgsConfigResult.isSuccess || - extensionNinjaOrgsConfigResult.isError) && ( - - {extensionNinjaOrgsConfigResult.isSuccess - ? extensionNinjaOrgsConfigResult.data.Results - : 'Error'} - - )} - {(extensionNinjaOrgsAutomapResult.isSuccess || - extensionNinjaOrgsAutomapResult.isError) && ( - - {extensionNinjaOrgsAutomapResult.isSuccess - ? extensionNinjaOrgsAutomapResult.data.Results - : 'Error'} - - )} - - - ) - }} - /> - )} - - - -
    - ) -} - -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 - - - { - return ( - - {listBackendResult.isFetching && ( - <> - - - - - - - )} - {!listBackendResult.isFetching && listBackendResult.isSuccess && ( - <> - - - - - - - - - - Load Script - - - - - )} - - ) - }} - /> - - - - - - - {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 ? ( + + ) : ( + { + return ( + + + Use the table below to map your client to the correct PSA client + {listBackendHaloResult.isSuccess && + listBackendHaloResult.data.Tenants?.map((tenant) => ( + + ))} + + + + {extensionHaloConfigResult.isFetching && ( + + )} + Set Mappings + + {(extensionHaloConfigResult.isSuccess || + extensionHaloConfigResult.isError) && + !extensionHaloConfigResult.isFetching && ( + + {extensionHaloConfigResult.isSuccess + ? extensionHaloConfigResult.data.Results + : 'Error'} + + )} + + + ) + }} + /> + )} + + + + + NinjaOne Field Mapping Table + + + {listBackendNinjaFieldsResult.isFetching ? ( + + ) : ( + { + return ( + + +
    Organization Global Custom Field Mapping
    +

    + Use the table below to map your Organization Field to the correct NinjaOne + Field +

    + {listBackendNinjaFieldsResult.isSuccess && + listBackendNinjaFieldsResult.data.CIPPOrgFields.map((CIPPOrgFields) => ( + item.type === CIPPOrgFields.Type || item.type === 'unset', + )} + placeholder="Select a Field" + /> + ))} +
    + +
    Device Custom Field Mapping
    +

    + Use the table below to map your Device field to the correct NinjaOne + WYSIWYG Field +

    + {listBackendNinjaFieldsResult.isSuccess && + listBackendNinjaFieldsResult.data.CIPPNodeFields.map((CIPPNodeFields) => ( + + item.type === CIPPNodeFields.Type || item.type === 'unset', + )} + placeholder="Select a Field" + /> + ))} +
    + + + {extensionNinjaFieldsConfigResult.isFetching && ( + + )} + Set Mappings + + {(extensionNinjaFieldsConfigResult.isSuccess || + extensionNinjaFieldsConfigResult.isError) && + !extensionNinjaFieldsConfigResult.isFetching && ( + + {extensionNinjaFieldsConfigResult.isSuccess + ? extensionNinjaFieldsConfigResult.data.Results + : 'Error'} + + )} + +
    + ) + }} + /> + )} +
    +
    + + + NinjaOne Organization Mapping Table + + + {listBackendNinjaOrgsResult.isFetching ? ( + + ) : ( + { + return ( + + + Use the table below to map your client to the correct NinjaOne Organization + {listBackendNinjaOrgsResult.isSuccess && + listBackendNinjaOrgsResult.data.Tenants.map((tenant) => ( + + ))} + + + + {extensionNinjaOrgsConfigResult.isFetching && ( + + )} + Set Mappings + + onNinjaOrgsAutomap()} className="me-2"> + {extensionNinjaOrgsAutomapResult.isFetching && ( + + )} + Automap NinjaOne Organizations + + {(extensionNinjaOrgsConfigResult.isSuccess || + extensionNinjaOrgsConfigResult.isError) && + !extensionNinjaFieldsConfigResult.isFetching && ( + + {extensionNinjaOrgsConfigResult.isSuccess + ? extensionNinjaOrgsConfigResult.data.Results + : 'Error'} + + )} + {(extensionNinjaOrgsAutomapResult.isSuccess || + extensionNinjaOrgsAutomapResult.isError) && ( + + {extensionNinjaOrgsAutomapResult.isSuccess + ? extensionNinjaOrgsAutomapResult.data.Results + : 'Error'} + + )} + + + ) + }} + /> + )} + + + +
    + ) +} 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}

    + { + return ( + + + + {integration.SettingOptions.map( + (integrationOptions, idx) => + integrationOptions.type === 'input' && ( + + + + ), + )} + {integration.SettingOptions.map( + (integrationOptions, idx) => + integrationOptions.type === 'checkbox' && ( + + + + ), + )} + + + + + + {extensionConfigResult.isFetching && ( + + )} + Set Extension Settings + + onSubmitTest(integration.type)} + className="me-2" + > + {listExtensionTestResult.isFetching && ( + + )} + Test Extension + + {integration.forceSyncButton && ( + + execSyncExtension({ + path: 'api/ExecExtensionSync?Extension=' + integration.type, + }) + } + className="me-2" + > + {listSyncExtensionResult.isFetching && ( + + )} + Force Sync + + )} + + + ) + }} + /> +
    +
    +
    + ))} +
    + +
    + ) +} 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: ( +
    + { + formRef.current = values + return ( + <> +
    Add a license to exclude
    + + + + ) + }} + /> +
    + ), + 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 + + + { + return ( + + {listBackendResult.isFetching && ( + <> + + + + + + + )} + {!listBackendResult.isFetching && listBackendResult.isSuccess && ( + <> + + + + + + + + + + Load Script + + + + + )} + + ) + }} + /> + + + + + + + {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 + + + true} + initialValues={{ + ...notificationListResult.data, + logsToInclude: notificationListResult.data?.logsToInclude?.map((m) => ({ + label: m, + value: m, + })), + Severity: notificationListResult.data?.Severity?.map((s) => ({ + label: s, + value: s, + })), + }} + onSubmit={onSubmit} + render={({ handleSubmit, submitting, values }) => { + return ( + + {notificationConfigResult.isFetching && ( + + Loading + + )} + {notificationConfigResult.isSuccess && !notificationConfigResult.isFetching && ( + + {notificationConfigResult.data?.Results} + + )} + {notificationConfigResult.isError && !notificationConfigResult.isFetching && ( + + Could not connect to API: {notificationConfigResult.error.message} + + )} + + + + + + + + + + + + + + + + + + + + + + + + Set Notification Settings + + + + ) + }} + /> + + + )} + + ) +} 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',