diff --git a/public/version_latest.txt b/public/version_latest.txt index 3bff059174b8..7cbea073bea1 100644 --- a/public/version_latest.txt +++ b/public/version_latest.txt @@ -1 +1 @@ -5.1.1 \ No newline at end of file +5.2.0 \ No newline at end of file diff --git a/src/_nav.jsx b/src/_nav.jsx index a17d7e73a344..d701c055f36d 100644 --- a/src/_nav.jsx +++ b/src/_nav.jsx @@ -50,6 +50,11 @@ const _nav = [ name: 'Groups', to: '/identity/administration/groups', }, + { + component: CNavItem, + name: 'Devices', + to: '/identity/administration/devices', + }, { component: CNavItem, name: 'Deploy Group Template', @@ -737,6 +742,11 @@ const _nav = [ name: 'Logbook', to: '/cipp/logs', }, + { + component: CNavItem, + name: 'Statistics', + to: '/cipp/statistics', + }, { component: CNavItem, name: 'SAM Setup Wizard', diff --git a/src/components/tables/CippTable.jsx b/src/components/tables/CippTable.jsx index 99c36ae46708..74dbf52ecd53 100644 --- a/src/components/tables/CippTable.jsx +++ b/src/components/tables/CippTable.jsx @@ -389,14 +389,43 @@ export default function CippTable({ } const newModalBody = {} for (let [objName, objValue] of Object.entries(modalBody)) { - if (objValue.toString().startsWith('!')) { + if (typeof objValue === 'object' && objValue !== null) { + newModalBody[objName] = {} + for (let [nestedObjName, nestedObjValue] of Object.entries(objValue)) { + if (typeof nestedObjValue === 'string' && nestedObjValue.startsWith('!')) { + newModalBody[objName][nestedObjName] = row[nestedObjValue.replace('!', '')] + } else { + newModalBody[objName][nestedObjName] = nestedObjValue + } + } + } else if (typeof objValue === 'string' && objValue.startsWith('!')) { newModalBody[objName] = row[objValue.replace('!', '')] + } else { + newModalBody[objName] = objValue } } const NewModalUrl = `${modalUrl.split('?')[0]}?${urlParams.toString()}` + const selectedValue = inputRef.current.value + let additionalFields = {} + if (inputRef.current.nodeName === 'SELECT') { + const selectedItem = dropDownInfo.data.find( + (item) => item[modalDropdown.valueField] === selectedValue, + ) + if (selectedItem && modalDropdown.addedField) { + Object.keys(modalDropdown.addedField).forEach((key) => { + additionalFields[key] = selectedItem[modalDropdown.addedField[key]] + }) + } + } + const results = await genericPostRequest({ path: NewModalUrl, - values: { ...modalBody, ...newModalBody, ...{ input: inputRef.current.value } }, + values: { + ...modalBody, + ...newModalBody, + ...additionalFields, + ...{ input: inputRef.current.value }, + }, }) resultsarr.push(results) setMassResults(resultsarr) @@ -466,6 +495,7 @@ export default function CippTable({ } const executeselectedAction = (item) => { + console.log(item) setModalContent({ item, }) diff --git a/src/components/utilities/CippCodeOffcanvas.jsx b/src/components/utilities/CippCodeOffcanvas.jsx index 589c27ce3cb8..a9b5cf41a055 100644 --- a/src/components/utilities/CippCodeOffcanvas.jsx +++ b/src/components/utilities/CippCodeOffcanvas.jsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' import { CButton, CCallout, CCol, CRow, CSpinner } from '@coreui/react' import { CippOffcanvas } from 'src/components/utilities' import { useLazyGenericGetRequestQuery, useLazyGenericPostRequestQuery } from 'src/store/api/app' @@ -6,6 +6,8 @@ import { useLazyGenericGetRequestQuery, useLazyGenericPostRequestQuery } from 's import { Editor } from '@monaco-editor/react' import { useSelector } from 'react-redux' import PropTypes from 'prop-types' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import CopyToClipboard from 'react-copy-to-clipboard' function CippCodeOffCanvas({ row, @@ -14,11 +16,13 @@ function CippCodeOffCanvas({ type, title = 'Template JSON', hideButton = false, + path = `/api/ExecEditTemplate?type=${type}`, }) { const [SaveTemplate, templateDetails] = useLazyGenericPostRequestQuery() const currentTheme = useSelector((state) => state.app.currentTheme) const [templateData, setFormData] = useState(row) const [invalidJSON, setInvalid] = useState(false) + const [copied, setCopied] = useState(false) function handleEditorChange(value, event) { try { @@ -29,6 +33,10 @@ function CippCodeOffCanvas({ } } + useEffect(() => { + setCopied(false) + }, [setCopied, templateData]) + return ( <> {!hideButton && ( - - SaveTemplate({ - path: `/api/ExecEditTemplate?type=${type}`, - method: 'POST', - values: templateData, - }) - } - > - Save changes {templateDetails.isFetching && } - + <> + + SaveTemplate({ + path: path, + method: 'POST', + values: templateData, + }) + } + className="me-2" + > + {templateDetails.isFetching ? ( + + ) : ( + + )} + Save changes + + setCopied(true)}> + + Copy + to Clipboard + + + )} @@ -83,6 +105,7 @@ CippCodeOffCanvas.propTypes = { type: PropTypes.string, title: PropTypes.string, hideButton: PropTypes.bool, + path: PropTypes.string, } export default CippCodeOffCanvas diff --git a/src/components/utilities/SharedModal.jsx b/src/components/utilities/SharedModal.jsx index fb8501fc93ca..9167b7a2073b 100644 --- a/src/components/utilities/SharedModal.jsx +++ b/src/components/utilities/SharedModal.jsx @@ -29,7 +29,7 @@ function mapBodyComponent({ componentType, data, componentProps }) { } const sharedProps = { - componentType: PropTypes.oneOf(['table', 'list', 'text', 'confirm']), + componentType: PropTypes.oneOf(['table', 'list', 'text', 'confirm', 'codeblock']), componentProps: PropTypes.object, body: PropTypes.oneOfType([PropTypes.node, PropTypes.element]), data: PropTypes.any, diff --git a/src/data/standards.json b/src/data/standards.json index 5d8788197e96..01394297cd34 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -440,6 +440,16 @@ "impact": "Low Impact", "impactColour": "info" }, + { + "name": "standards.MessageExpiration", + "cat": "Exchange Standards", + "tag": ["lowimpact"], + "helpText": "Sets the transport message configuration to timeout a message at 12 hours.", + "addedComponent": [], + "label": "Lower Transport Message Expiration to 12 hours", + "impact": "Low Impact", + "impactColour": "info" + }, { "name": "standards.AutoExpandArchive", "cat": "Exchange Standards", diff --git a/src/routes.js b/src/routes.js index 2f41ef51dff5..4f119b52d8c8 100644 --- a/src/routes.js +++ b/src/routes.js @@ -3,6 +3,7 @@ import React from 'react' const Home = React.lazy(() => import('src/views/home/Home')) const Logs = React.lazy(() => import('src/views/cipp/Logs')) const Scheduler = React.lazy(() => import('src/views/cipp/Scheduler')) +const Statistics = React.lazy(() => import('src/views/cipp/Statistics')) const Users = React.lazy(() => import('src/views/identity/administration/Users')) const DeletedItems = React.lazy(() => import('src/views/identity/administration/Deleted')) const ViewBEC = React.lazy(() => import('src/views/identity/administration/ViewBEC')) @@ -30,6 +31,8 @@ const EditGroup = React.lazy(() => import('src/views/identity/administration/Edi const ViewGroup = React.lazy(() => import('src/views/identity/administration/ViewGroup')) const Roles = React.lazy(() => import('src/views/identity/administration/Roles')) const Devices = React.lazy(() => import('src/views/endpoint/intune/Devices')) +const allDevices = React.lazy(() => import('src/views/identity/administration/Devices')) + const PageLogOut = React.lazy(() => import('src/views/pages/LogoutRedirect/PageLogOut')) const Page404 = React.lazy(() => import('src/views/pages/page404/Page404')) @@ -242,7 +245,7 @@ const routes = [ { path: '/home', name: 'Home', component: Home }, { path: '/cipp/logs', name: 'Logs', component: Logs }, { path: '/cipp/scheduler', name: 'Scheduler', component: Scheduler }, - + { path: '/cipp/statistics', name: 'Statistics', component: Statistics }, { path: '/cipp/404', name: 'Error', component: Page404 }, { path: '/cipp/403', name: 'Error', component: Page403 }, { path: '/cipp/500', name: 'Error', component: Page500 }, @@ -258,6 +261,8 @@ const routes = [ { path: '/identity/administration/ViewBec', name: 'View BEC', component: ViewBEC }, { path: '/identity/administration', name: 'Administration' }, { path: '/identity/administration/users', name: 'Users', component: Users }, + { path: '/identity/administration/devices', name: 'Devices', component: allDevices }, + { path: '/identity/administration/groups/add', name: 'Add Group', component: AddGroup }, { path: '/identity/administration/group-templates', diff --git a/src/views/cipp/Statistics.jsx b/src/views/cipp/Statistics.jsx new file mode 100644 index 000000000000..6fba968ca726 --- /dev/null +++ b/src/views/cipp/Statistics.jsx @@ -0,0 +1,153 @@ +import React, { useState } from 'react' +import { CippPage } from 'src/components/layout' +import { + CButton, + CCard, + CCardBody, + CCardHeader, + CCardTitle, + CCol, + CCollapse, + CFormInput, + CFormLabel, + CFormSelect, + CRow, + CSpinner, +} from '@coreui/react' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faChevronRight, faChevronDown } from '@fortawesome/free-solid-svg-icons' +import { CippTable } from 'src/components/tables' +import { cellGenericFormatter } from 'src/components/tables/CellGenericFormat' +import { useSelector } from 'react-redux' +import { useGenericGetRequestQuery } from 'src/store/api/app' +import { RFFCFormSelect } from 'src/components/forms' + +const columns = [ + { + name: 'Name', + selector: (row) => row['Name'], + sortable: true, + cell: cellGenericFormatter(), + exportSelector: 'Name', + minWidth: '145px', + maxWidth: '145px', + }, + { + name: 'Executions', + selector: (row) => row['ExecutionCount'], + sortable: true, + exportSelector: 'ExecutionCount', + }, + { + name: 'Total (seconds)', + selector: (row) => row['TotalSeconds'], + sortable: true, + exportSelector: 'TotalSeconds', + }, + { + name: 'Max (seconds)', + selector: (row) => row['MaxSeconds'], + sortable: true, + exportSelector: 'MaxSeconds', + }, + { + name: 'Avg (seconds)', + selector: (row) => row['AvgSeconds'], + sortable: true, + exportSelector: 'AvgSeconds', + cell: (row) => Math.round(row['AvgSeconds'] * 100) / 100, + }, +] + +const Statistics = () => { + const [visibleA, setVisibleA] = useState(false) + const [type, setType] = useState('Functions') + const [interval, setInterval] = useState('Days') + const [time, setTime] = useState(1) + const tenant = useSelector((state) => state.app.currentTenant) + const { data, isFetching, error, isSuccess } = useGenericGetRequestQuery({ + path: '/api/ListFunctionStats', + params: { + FunctionType: 'Queue', + Interval: interval, + Time: time, + TenantFilter: tenant?.defaultDomainName, + }, + }) + + return ( + <> + + + + + + Options + setVisibleA(!visibleA)} + > + + + + + + + + + + + + Report + setType(e.target.value)}> + + + + + +
+ Interval + setInterval(e.target.value)}> + + + + +
+
+ Time + setTime(e.target.value)} + defaultValue={1} + /> +
+
+
+
+
+
+
+
+
+ + + + Statistics + + + {isFetching && } + {isSuccess && ( + + )} + + + + + ) +} + +export default Statistics diff --git a/src/views/email-exchange/connectors/ConnectorList.jsx b/src/views/email-exchange/connectors/ConnectorList.jsx index e4e887adbd71..990e1d555199 100644 --- a/src/views/email-exchange/connectors/ConnectorList.jsx +++ b/src/views/email-exchange/connectors/ConnectorList.jsx @@ -18,7 +18,7 @@ const Offcanvas = (row, rowIndex, formatExtraData) => { { { { { { + const [tenantColumnSet, setTenantColumn] = useState(true) + const tenant = useSelector((state) => state.app.currentTenant) + const Offcanvas = (row, rowIndex, formatExtraData) => { + const tenant = useSelector((state) => state.app.currentTenant) + const [ocVisible, setOCVisible] = useState(false) + const editLink = `/identity/administration/groups/edit?groupId=${row.id}&tenantDomain=${tenant.defaultDomainName}` + return ( + <> + setOCVisible(true)}> + + + setOCVisible(false)} + /> + + ) + } + const columns = [ + { + name: 'Tenant', + selector: (row) => row['Tenant'], + sortable: true, + cell: (row) => CellTip(row['Tenant']), + exportSelector: 'Tenant', + omit: tenantColumnSet, + }, + { + name: 'Retrieval Status', + selector: (row) => row['CippStatus'], + sortable: true, + cell: (row) => CellTip(row['CippStatus']), + exportSelector: 'CippStatus', + omit: tenantColumnSet, + }, + { + selector: (row) => row['displayName'], + name: 'Display Name', + sortable: true, + cell: (row) => CellTip(row['displayName']), + exportSelector: 'displayName', + }, + { + selector: (row) => row['deviceOwnership'], + name: 'Device Ownership', + sortable: true, + cell: (row) => CellTip(row['deviceOwnership']), + exportSelector: 'recipientType', + }, + { + selector: (row) => row['enrollmentType'], + name: 'Enrollment Type', + sortable: true, + exportSelector: 'enrollmentType', + }, + { + selector: (row) => row['manufacturer'], + name: 'Manufacturer', + sortable: true, + exportSelector: 'manufacturer', + }, + { + selector: (row) => row['model'], + name: 'Model', + sortable: true, + exportSelector: 'model', + }, + { + selector: (row) => row['operatingSystem'], + name: 'OS', + sortable: true, + exportSelector: 'operatingSystem', + }, + { + selector: (row) => row['operatingSystemVersion'], + name: 'Version', + sortable: true, + exportSelector: 'operatingSystemVersion', + }, + { + selector: (row) => row['profileType'], + name: 'Profile Type', + sortable: true, + exportSelector: 'profileType', + }, + { + name: 'Actions', + cell: Offcanvas, + maxWidth: '20px', + }, + ] + useEffect(() => { + if (tenant.defaultDomainName === 'AllTenants') { + setTenantColumn(false) + } + if (tenant.defaultDomainName !== 'AllTenants') { + setTenantColumn(true) + } + }, [tenant.defaultDomainName, tenantColumnSet]) + return ( + + ) +} + +export default DevicesList diff --git a/src/views/identity/administration/Users.jsx b/src/views/identity/administration/Users.jsx index e6e4cbc30539..774f25a29c2d 100644 --- a/src/views/identity/administration/Users.jsx +++ b/src/views/identity/administration/Users.jsx @@ -129,7 +129,7 @@ const Offcanvas = (row, rowIndex, formatExtraData) => { }, modalUrl: `/api/ExecOneDriveShortCut`, modalDropdown: { - url: `/api/listSites?TenantFilter=${tenant.defaultDomainName}&type=SharePointSiteUsage`, + url: `/api/listSites?TenantFilter=${tenant.defaultDomainName}&type=SharePointSiteUsage&URLOnly=true`, labelField: 'URL', valueField: 'URL', }, @@ -551,6 +551,58 @@ const Users = (row) => { valueField: 'URL', }, }, + { + label: 'Add to group', + color: 'info', + modal: true, + modalType: 'POST', + modalBody: { + username: '!userPrincipalName', + userid: '!id', + TenantId: tenant.defaultDomainName, + Addmember: { + value: '!userPrincipalName', + }, + }, + modalUrl: `/api/EditGroup`, + modalMessage: 'Select the group to add', + modalDropdown: { + url: `/api/listGroups?TenantFilter=${tenant.defaultDomainName}`, + labelField: 'displayName', + valueField: 'id', + addedField: { + groupId: 'id', + groupType: 'calculatedGroupType', + groupName: 'displayName', + }, + }, + }, + { + label: 'Remove from group', + color: 'info', + modal: true, + modalType: 'POST', + modalBody: { + username: '!userPrincipalName', + userid: '!id', + TenantId: tenant.defaultDomainName, + RemoveMember: { + value: '!userPrincipalName', + }, + }, + modalUrl: `/api/EditGroup`, + modalMessage: 'Select the group to remove', + modalDropdown: { + url: `/api/listGroups?TenantFilter=${tenant.defaultDomainName}`, + labelField: 'displayName', + valueField: 'id', + addedField: { + groupId: 'id', + groupType: 'calculatedGroupType', + groupName: 'displayName', + }, + }, + }, { label: 'Set Out of Office', color: 'info', diff --git a/src/views/teams-share/onedrive/OneDriveList.jsx b/src/views/teams-share/onedrive/OneDriveList.jsx index 4309c729bafb..cacd7d39d0fc 100644 --- a/src/views/teams-share/onedrive/OneDriveList.jsx +++ b/src/views/teams-share/onedrive/OneDriveList.jsx @@ -19,7 +19,7 @@ const OneDriveList = () => { { { const tenant = useSelector((state) => state.app.currentTenant) @@ -39,6 +39,7 @@ const GraphExplorer = () => { const [alertVisible, setAlertVisible] = useState() const [random, setRandom] = useState('') const [random2, setRandom2] = useState('') + const [ocVisible, setOCVisible] = useState(false) const [searchNow, setSearchNow] = useState(false) const [visibleA, setVisibleA] = useState(true) const handleSubmit = async (values) => { @@ -82,7 +83,7 @@ const GraphExplorer = () => { const handleManagePreset = ({ values, action, message }) => { var params = { action: action, - values: values, + preset: values, } ModalService.confirm({ title: 'Confirm', @@ -259,6 +260,15 @@ const GraphExplorer = () => { field: PropTypes.node, set: PropTypes.string, } + + function getPresetProps(values) { + var newvals = Object.assign({}, values) + delete newvals['reportTemplate'] + delete newvals['tenantFilter'] + delete newvals['IsShared'] + return newvals + } + console.log(graphrequest.data) return ( @@ -300,6 +310,7 @@ const GraphExplorer = () => { name="reportTemplate" label="Select a report preset" placeholder="Select a report" + retainInput={false} multi={false} values={presets.map((preset) => { return { @@ -316,7 +327,11 @@ const GraphExplorer = () => { - + + + + + {(props) => { @@ -326,6 +341,14 @@ const GraphExplorer = () => { return ( <>
+ + setOCVisible(true)} + className="me-2" + > + + + {!preset[0]?.isBuiltin && preset[0]?.id && preset[0]?.IsMyPreset && ( @@ -373,6 +396,7 @@ const GraphExplorer = () => { values: props.values, }) } + className="me-2" > @@ -394,6 +418,18 @@ const GraphExplorer = () => { {postResults.data?.Results} )} + { + setOCVisible(false) + setRandom2((Math.random() + 1).toString(36).substring(7)) + }} + /> ) }} diff --git a/src/views/tenant/administration/ListAlertsQueue.jsx b/src/views/tenant/administration/ListAlertsQueue.jsx index b0800dfaa7bf..23513f50a28d 100644 --- a/src/views/tenant/administration/ListAlertsQueue.jsx +++ b/src/views/tenant/administration/ListAlertsQueue.jsx @@ -93,7 +93,7 @@ const ListClassicAlerts = () => { ({ label: key, value: diff --git a/src/views/tenant/standards/ListAppliedStandards.jsx b/src/views/tenant/standards/ListAppliedStandards.jsx index 1d0979924189..61c50f1a0d99 100644 --- a/src/views/tenant/standards/ListAppliedStandards.jsx +++ b/src/views/tenant/standards/ListAppliedStandards.jsx @@ -207,10 +207,16 @@ const ApplyNewStandard = () => { (tenant) => tenant.displayName === 'AllTenants', ) - function getLabel(item) { + function getLabel(item, type) { + if (!item || !item.name) { + return '' + } const keys = item.name.split('.') let value = keys.reduce((prev, curr) => prev && prev[curr], allTenantsStandard) - return value ? `* Enabled via All Tenants` : '' + if (!value || !value[type]) { + return '' + } + return `* Enabled via All Tenants` } const groupedStandards = allStandardsList.reduce((acc, obj) => { @@ -437,7 +443,7 @@ const ApplyNewStandard = () => { name={`${obj.name}.report`} disabled={obj.disabledFeatures?.report} helpText="Report stores the data in the database to use in custom BPA reports." - sublabel={getLabel(obj)} + sublabel={getLabel(obj, 'report')} /> @@ -446,7 +452,7 @@ const ApplyNewStandard = () => { name={`${obj.name}.alert`} disabled={obj.disabledFeatures?.warn} helpText="Alert Generates an alert in the log, if remediate is enabled the log entry will also say if the remediation was successful." - sublabel={getLabel(obj)} + sublabel={getLabel(obj, 'alert')} /> @@ -455,7 +461,7 @@ const ApplyNewStandard = () => { name={`${obj.name}.remediate`} disabled={obj.disabledFeatures?.remediate} helpText={'Remediate executes the fix for standard.'} - sublabel={getLabel(obj)} + sublabel={getLabel(obj, 'remediate')} /> diff --git a/version_latest.txt b/version_latest.txt index 3bff059174b8..7cbea073bea1 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -5.1.1 \ No newline at end of file +5.2.0 \ No newline at end of file