diff --git a/Tools/Update-Version.ps1 b/Tools/Update-Version.ps1 new file mode 100644 index 000000000000..48ad6e6bf9ba --- /dev/null +++ b/Tools/Update-Version.ps1 @@ -0,0 +1,10 @@ +Param($Version) +Set-Location (Get-Item $PSScriptRoot).Parent.FullName +$Files = @('version_latest.txt', 'public/version_latest.txt') +foreach ($File in $Files) { + Set-Content $File -Value $Version +} + +$Package = Get-Content package.json | ConvertFrom-Json +$Package.version = $Version +$Package | ConvertTo-Json -Depth 10 | Set-Content package.json diff --git a/package.json b/package.json index 5103fa09b1bc..9e73b8d43a81 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cipp", - "version": "5.3.2", + "version": "5.4.0", "description": "The CyberDrain Improved Partner Portal is a portal to help manage administration for Microsoft Partners.", "homepage": "https://cipp.app/", "bugs": { diff --git a/public/version_latest.txt b/public/version_latest.txt index 84197c89467d..8a30e8f94a39 100644 --- a/public/version_latest.txt +++ b/public/version_latest.txt @@ -1 +1 @@ -5.3.2 +5.4.0 diff --git a/src/_nav.jsx b/src/_nav.jsx index d701c055f36d..f55c93339bd1 100644 --- a/src/_nav.jsx +++ b/src/_nav.jsx @@ -481,9 +481,19 @@ const _nav = [ }, { component: CNavItem, - name: 'MEM Policies', + name: 'Configuration Policies', to: '/endpoint/MEM/list-policies', }, + { + component: CNavItem, + name: 'Compliance Policies', + to: '/endpoint/MEM/list-compliance-policies', + }, + { + component: CNavItem, + name: 'Protection Policies', + to: '/endpoint/MEM/list-appprotection-policies', + }, { component: CNavItem, name: 'Apply Policy', @@ -612,6 +622,11 @@ const _nav = [ name: 'Mailbox Restores', to: '/email/tools/mailbox-restores', }, + { + component: CNavItem, + name: 'Mail Test', + to: '/email/tools/mail-test', + }, ], }, { diff --git a/src/components/forms/RFFComponents.jsx b/src/components/forms/RFFComponents.jsx index fef24a7cb418..b3d491caa5b7 100644 --- a/src/components/forms/RFFComponents.jsx +++ b/src/components/forms/RFFComponents.jsx @@ -14,7 +14,7 @@ import Select from 'react-select' import Creatable, { useCreatable } from 'react-select/creatable' import { Field } from 'react-final-form' import { FieldArray } from 'react-final-form-arrays' -import React, { useState, useMemo, useRef } from 'react' +import React, { useState, useMemo } from 'react' import PropTypes from 'prop-types' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { debounce } from 'lodash-es' @@ -37,6 +37,7 @@ const sharedPropTypes = { error: PropTypes.any, }), }), + onClick: PropTypes.func, } export const RFFCFormFeedback = ({ meta }) => { @@ -54,7 +55,14 @@ RFFCFormFeedback.propTypes = { }), } -export const RFFCFormCheck = ({ name, label, className = 'mb-3', validate, disabled = false }) => { +export const RFFCFormCheck = ({ + name, + label, + className = 'mb-3', + validate, + disabled = false, + onClick, +}) => { return ( {({ input, meta }) => ( @@ -67,6 +75,7 @@ export const RFFCFormCheck = ({ name, label, className = 'mb-3', validate, disab disabled={disabled} id={name} label={label} + onClick={onClick} /> @@ -92,6 +101,7 @@ export const RFFCFormSwitch = ({ validate, disabled = false, initialValue, + onClick, }) => { return ( @@ -113,6 +123,7 @@ export const RFFCFormSwitch = ({ disabled={disabled} id={name} label={label} + onClick={onClick} /> {input.value && } {sublabel} @@ -239,6 +250,7 @@ export const RFFCFormRadio = ({ className = 'mb-3', validate, disabled = false, + onClick, }) => { return ( @@ -252,6 +264,7 @@ export const RFFCFormRadio = ({ type="radio" name={name} label={label} + onClick={onClick} /> diff --git a/src/components/tables/CellTable.jsx b/src/components/tables/CellTable.jsx index e03cd95059bd..549e4805dfa0 100644 --- a/src/components/tables/CellTable.jsx +++ b/src/components/tables/CellTable.jsx @@ -21,7 +21,11 @@ export default function cellTable( } if (!Array.isArray(columnProp) && typeof columnProp === 'object') { - columnProp = [columnProp] + columnProp = Object.keys(columnProp).map((key) => { + return { + [key]: columnProp[key], + } + }) } if (Array.isArray(columnProp) && typeof columnProp[0] !== 'object') { diff --git a/src/components/tables/CippTable.jsx b/src/components/tables/CippTable.jsx index 7b155136c17b..3c246cbecb40 100644 --- a/src/components/tables/CippTable.jsx +++ b/src/components/tables/CippTable.jsx @@ -33,11 +33,11 @@ import { import { cellGenericFormatter } from './CellGenericFormat' import { ModalService } from '../utilities' import { useLazyGenericGetRequestQuery, useLazyGenericPostRequestQuery } from 'src/store/api/app' -import { debounce, update } from 'lodash-es' +import { debounce } 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' +import M365Licenses from 'src/data/M365Licenses' const FilterComponent = ({ filterText, onFilter, onClear, filterlist, onFilterPreset }) => ( <> @@ -277,28 +277,29 @@ export default function CippTable({ debounceSetGraphFilter(query) return data } else if (filterText.startsWith('Complex:')) { - const conditions = filterText.slice(9).split(';') + // Split conditions by ';' and 'or', and trim spaces + const conditions = filterText + .slice(9) + .split(/\s*or\s*|\s*;\s*/i) // Split by 'or' or ';', case insensitive, with optional spaces + .map((condition) => condition.trim()) - return conditions.reduce((filteredData, condition) => { - const match = condition.trim().match(/(\w+)\s*(eq|ne|like|notlike|gt|lt)\s*(.+)/) + return data.filter((item) => { + // Check if any condition is met for the item + return conditions.some((condition) => { + const match = condition.match(/(\w+)\s*(eq|ne|like|notlike|gt|lt)\s*(.+)/) - if (!match) { - return filteredData // Keep the current filtered data as is - } + if (!match) return false - let [property, operator, value] = match.slice(1) - value = escapeRegExp(value) // Escape special characters + let [property, operator, value] = match.slice(1) + value = escapeRegExp(value) // Escape special characters - return filteredData.filter((item) => { - // Find the actual key in the item that matches the property (case insensitive) const actualKey = Object.keys(item).find( (key) => key.toLowerCase() === property.toLowerCase(), ) if (!actualKey) { - //set the error message so the user understands the key is not found. console.error(`FilterError: Property "${property}" not found.`) - return false // Keep the item if the property is not found + return false } switch (operator) { @@ -315,10 +316,10 @@ export default function CippTable({ case 'lt': return parseFloat(item[actualKey]) < parseFloat(value) default: - return true + return false // Should not reach here normally } }) - }, data) + }) } else { return data.filter( (item) => JSON.stringify(item).toLowerCase().indexOf(filterText.toLowerCase()) !== -1, @@ -326,6 +327,8 @@ export default function CippTable({ } } + // Helper functions like `debounce` and `escapeRegExp` should be defined somewhere in your code + // For example, a simple escapeRegExp function could be: const filteredItems = Array.isArray(data) ? filterData(data, filterText) : [] const applyFilter = (e) => { @@ -630,74 +633,64 @@ export default function CippTable({ return null }) - var exportData = filteredItems - - var filtered = - Array.isArray(exportData) && exportData.length > 0 - ? exportData.map((obj) => - // eslint-disable-next-line no-sequences - /* keys.reduce((acc, curr) => ((acc[curr] = obj[curr]), acc), {}),*/ - keys.reduce((acc, curr) => { - const key = curr.split('/') - if (key.length > 1) { - let property = obj - for (let x = 0; x < key.length; x++) { - if ( - Object.prototype.hasOwnProperty.call(property, key[x]) && - property[key[x]] !== null - ) { - property = property[key[x]] - } else { - property = 'n/a' - break - } - } - acc[curr] = property - } else { - if (typeof exportFormatter[curr] === 'function') { - acc[curr] = exportFormatter[curr]({ cell: obj[curr] }) - } else { - acc[curr] = obj[curr] - } - } - return acc - }, {}), - ) - : [] + // Define the flatten function + const flatten = (obj, prefix = '') => { + return Object.keys(obj).reduce((output, key) => { + const newKey = prefix ? `${prefix}.${key}` : key + const value = obj[key] === null ? '' : obj[key] - const flatten = (obj, prefix) => { - let output = {} - for (let k in obj) { - let val = obj[k] - if (val === null) { - val = '' - } - const newKey = prefix ? prefix + '.' + k : k - if (typeof val === 'object') { - if (Array.isArray(val)) { - const { ...arrToObj } = val - const newObj = flatten(arrToObj, newKey) - output = { ...output, ...newObj } - } else { - const newObj = flatten(val, newKey) - output = { ...output, ...newObj } - } + if (typeof value === 'object' && !Array.isArray(value)) { + Object.assign(output, flatten(value, newKey)) } else { - output = { ...output, [newKey]: val } + output[newKey] = value } - } - return output + return output + }, {}) } - filtered = filtered.map((item) => flatten(item)) - let dataFlat + // Define the applyFormatter function + const applyFormatter = (obj) => { + return Object.keys(obj).reduce((acc, key) => { + const formatter = exportFormatter[key] + // Since the keys after flattening will be dot-separated, we need to adjust this to support nested keys if necessary. + const keyParts = key.split('.') + const finalKeyPart = keyParts[keyParts.length - 1] + const formattedValue = + typeof formatter === 'function' ? formatter({ cell: obj[key] }) : obj[key] + acc[key] = formattedValue + return acc + }, {}) + } - if (Array.isArray(data)) { - dataFlat = data.map((item) => flatten(item)) - } else { - dataFlat = [] + // Process exportData function + const processExportData = (exportData, selectedColumns) => { + //filter out the columns that are not selected via selectedColumns + exportData = exportData.map((item) => { + return Object.keys(item) + .filter((key) => selectedColumns.find((o) => o.exportSelector === key)) + .reduce((obj, key) => { + obj[key] = item[key] + return obj + }, {}) + }) + return Array.isArray(exportData) && exportData.length > 0 + ? exportData.map((obj) => { + const flattenedObj = flatten(obj) + return applyFormatter(flattenedObj) + }) + : [] } + // Applying the processExportData function to both filteredItems and data + var filtered = processExportData(filteredItems, updatedColumns) + + // Adjusted dataFlat processing to include formatting + let dataFlat = Array.isArray(data) + ? data.map((item) => { + const flattenedItem = flatten(item) + return applyFormatter(flattenedItem) + }) + : [] if (!disablePDFExport) { if (dynamicColumns === true) { defaultActions.push([ diff --git a/src/components/tables/WizardTableField.jsx b/src/components/tables/WizardTableField.jsx index bb6ba35a9da1..ac65a722029d 100644 --- a/src/components/tables/WizardTableField.jsx +++ b/src/components/tables/WizardTableField.jsx @@ -73,6 +73,7 @@ export default class WizardTableField extends React.Component { selectableRows: true, onSelectedRowsChange: this.handleSelect, }} + dynamicColumns={false} {...props} /> ) diff --git a/src/data/standards.json b/src/data/standards.json index 95cc1a746259..cab53f3144d6 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -232,6 +232,32 @@ "impact": "Low Impact", "impactColour": "info" }, + { + "name": "standards.ExternalMFATrusted", + "cat": "Entra (AAD) Standards", + "tag": ["lowimpact"], + "helpText": "Sets the state of the Cross-tenant access setting to trust external MFA. This allows guest users to use their home tenant MFA to access your tenant.", + "addedComponent": [ + { + "type": "Select", + "label": "Select value", + "name": "standards.ExternalMFATrusted.state", + "values": [ + { + "label": "Enabled", + "value": "true" + }, + { + "label": "Disabled", + "value": "false" + } + ] + } + ], + "label": "Sets the Cross-tenant access setting to trust external MFA", + "impact": "Low Impact", + "impactColour": "info" + }, { "name": "standards.DisableTenantCreation", "cat": "Entra (AAD) Standards", @@ -736,6 +762,252 @@ "impact": "Medium Impact", "impactColour": "warning" }, + { + "name": "standards.SafeLinksPolicy", + "cat": "Defender Standards", + "tag": ["lowimpact", "CIS"], + "helpText": "This creates a safelink policy that automatically scans, tracks, and and enables safe links for Email, Office, and Teams for both external and internal senders", + "addedComponent": [ + { + "type": "boolean", + "label": "AllowClickThrough", + "name": "standards.SafeLinksPolicy.AllowClickThrough" + }, + { + "type": "boolean", + "label": "DisableUrlRewrite", + "name": "standards.SafeLinksPolicy.DisableUrlRewrite" + }, + { + "type": "boolean", + "label": "EnableOrganizationBranding", + "name": "standards.SafeLinksPolicy.EnableOrganizationBranding" + } + ], + "label": "Default SafeLinks Policy", + "impact": "Low Impact", + "impactColour": "info" + }, + { + "name": "standards.AntiPhishPolicy", + "cat": "Defender Standards", + "tag": ["lowimpact", "CIS"], + "helpText": "This creates a Anti-Phishing policy that automatically enables Mailbox Intelligence and spoofing, optional switches for Mailtips.", + "addedComponent": [ + { + "type": "number", + "label": "Phishing email threshold. (Default 1)", + "name": "standards.AntiPhishPolicy.PhishThresholdLevel", + "default": 1 + }, + { + "type": "boolean", + "label": "Show first contact safety tip", + "name": "standards.AntiPhishPolicy.EnableFirstContactSafetyTips", + "default": true + }, + { + "type": "boolean", + "label": "Show user impersonation safety tip", + "name": "standards.AntiPhishPolicy.EnableSimilarUsersSafetyTips", + "default": true + }, + { + "type": "boolean", + "label": "Show domain impersonation safety tip", + "name": "standards.AntiPhishPolicy.EnableSimilarDomainsSafetyTips", + "default": true + }, + { + "type": "boolean", + "label": "Show user impersonation unusual characters safety tip", + "name": "standards.AntiPhishPolicy.EnableUnusualCharactersSafetyTips", + "default": true + }, + { + "type": "Select", + "label": "If Mailbox Intelligence detects an impersonated user", + "name": "standards.AntiPhishPolicy.MailboxIntelligenceProtectionAction", + "values": [ + { + "label": "Move to Junk Folder", + "value": "MoveToJmf" + }, + { + "label": "Delete the message before its delivered", + "value": "Delete" + }, + { + "label": "Quarantine the message", + "value": "Quarantine" + } + ] + }, + { + "type": "Select", + "label": "Apply quarantine policy", + "name": "standards.AntiPhishPolicy.MailboxIntelligenceQuarantineTag", + "values": [ + { + "label": "AdminOnlyAccessPolicy", + "value": "AdminOnlyAccessPolicy" + }, + { + "label": "DefaultFullAccessPolicy", + "value": "DefaultFullAccessPolicy" + }, + { + "label": "DefaultFullAccessWithNotificationPolicy", + "value": "DefaultFullAccessWithNotificationPolicy" + } + ] + } + ], + "label": "Default Anti-Phishing Policy", + "impact": "Low Impact", + "impactColour": "info" + }, + { + "name": "standards.SafeAttachmentPolicy", + "cat": "Defender Standards", + "tag": ["lowimpact", "CIS"], + "helpText": "This creates a Safe Attachment policy", + "addedComponent": [ + { + "type": "Select", + "label": "Action", + "name": "standards.SafeAttachmentPolicy.Action", + "values": [ + { + "label": "Allow", + "value": "Allow" + }, + { + "label": "Block", + "value": "Block" + }, + { + "label": "DynamicDelivery", + "value": "DynamicDelivery" + } + ] + }, + { + "type": "Select", + "label": "QuarantineTag", + "name": "standards.SafeAttachmentPolicy.QuarantineTag", + "values": [ + { + "label": "AdminOnlyAccessPolicy", + "value": "AdminOnlyAccessPolicy" + }, + { + "label": "DefaultFullAccessPolicy", + "value": "DefaultFullAccessPolicy" + }, + { + "label": "DefaultFullAccessWithNotificationPolicy", + "value": "DefaultFullAccessWithNotificationPolicy" + } + ] + }, + { + "type": "boolean", + "label": "Redirect", + "name": "standards.SafeAttachmentPolicy.Redirect" + }, + { + "type": "input", + "name": "standards.SafeAttachmentPolicy.RedirectAddress", + "label": "Redirect Address" + } + ], + "label": "Default Safe Attachment Policy", + "impact": "Low Impact", + "impactColour": "info" + }, + { + "name": "standards.AtpPolicyForO365", + "cat": "Defender Standards", + "tag": ["lowimpact", "CIS"], + "helpText": "This creates a Atp policy that enables Defender for Office 365 for Sharepoint, OneDrive and Microsoft Teams.", + "addedComponent": [ + { + "type": "boolean", + "label": "Allow people to click through Protected View even if Safe Documents identified the file as malicious", + "name": "standards.AtpPolicyForO365.AllowSafeDocsOpen", + "default": false + } + ], + "label": "Default Atp Policy For O365", + "impact": "Low Impact", + "impactColour": "info" + }, + { + "name": "standards.MalwareFilterPolicy", + "cat": "Defender Standards", + "tag": ["lowimpact", "CIS"], + "helpText": "This creates a Malware filter policy that enables the default File filter and Zero-hour auto purge for malware.", + "addedComponent": [ + { + "type": "Select", + "label": "FileTypeAction", + "name": "standards.MalwareFilterPolicy.FileTypeAction", + "values": [ + { + "label": "Reject", + "value": "Reject" + }, + { + "label": "Quarantine the message", + "value": "Quarantine" + } + ] + }, + { + "type": "Select", + "label": "QuarantineTag", + "name": "standards.MalwareFilterPolicy.QuarantineTag", + "values": [ + { + "label": "AdminOnlyAccessPolicy", + "value": "AdminOnlyAccessPolicy" + }, + { + "label": "DefaultFullAccessPolicy", + "value": "DefaultFullAccessPolicy" + }, + { + "label": "DefaultFullAccessWithNotificationPolicy", + "value": "DefaultFullAccessWithNotificationPolicy" + } + ] + }, + { + "type": "boolean", + "label": "Enable Internal Sender Admin Notifications", + "name": "standards.MalwareFilterPolicy.EnableInternalSenderAdminNotifications" + }, + { + "type": "input", + "name": "standards.MalwareFilterPolicy.InternalSenderAdminAddress", + "label": "Internal Sender Admin Address" + }, + { + "type": "boolean", + "label": "Enable Internal Sender Admin Notifications", + "name": "standards.MalwareFilterPolicy.EnableExternalSenderAdminNotifications" + }, + { + "type": "input", + "name": "standards.MalwareFilterPolicy.ExternalSenderAdminAddress", + "label": "External Sender Admin Address" + } + ], + "label": "Default Malware Filter Policy", + "impact": "Low Impact", + "impactColour": "info" + }, { "name": "standards.intuneDeviceRetirementDays", "cat": "Intune Standards", diff --git a/src/routes.js b/src/routes.js index 4f119b52d8c8..794ebb4b4c4d 100644 --- a/src/routes.js +++ b/src/routes.js @@ -1,4 +1,5 @@ import React from 'react' +import MailTest from 'src/views/email-exchange/tools/MailTest' const Home = React.lazy(() => import('src/views/home/Home')) const Logs = React.lazy(() => import('src/views/cipp/Logs')) @@ -8,6 +9,8 @@ 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')) const AddUser = React.lazy(() => import('src/views/identity/administration/AddUser')) +const AddUserBulk = React.lazy(() => import('src/views/identity/administration/AddUserBulk')) + const InviteGuest = React.lazy(() => import('src/views/identity/administration/InviteGuest')) const EditUser = React.lazy(() => import('src/views/identity/administration/EditUser')) const ViewUser = React.lazy(() => import('src/views/identity/administration/ViewUser')) @@ -128,6 +131,12 @@ const AutopilotListStatusPages = React.lazy(() => import('src/views/endpoint/autopilot/AutopilotListStatusPages'), ) const IntuneListPolicies = React.lazy(() => import('src/views/endpoint/intune/MEMListPolicies')) +const IntuneListCompliance = React.lazy(() => import('src/views/endpoint/intune/MEMListCompliance')) + +const IntuneListAppProtection = React.lazy(() => + import('src/views/endpoint/intune/MEMListAppProtection'), +) + const MEMEditPolicy = React.lazy(() => import('src/views/endpoint/intune/MEMEditPolicy')) const IntuneCAPolicies = React.lazy(() => import('src/views/endpoint/intune/MEMCAPolicies')) @@ -239,6 +248,7 @@ const MailboxRestoreWizard = React.lazy(() => import('src/views/email-exchange/tools/MailboxRestoreWizard'), ) const MailboxRestores = React.lazy(() => import('src/views/email-exchange/tools/MailboxRestores')) +const Mailtest = React.lazy(() => import('src/views/email-exchange/tools/MailTest')) const routes = [ // { path: '/', exact: true, name: 'Home' }, @@ -251,6 +261,12 @@ const routes = [ { path: '/cipp/500', name: 'Error', component: Page500 }, { path: '/identity', name: 'Identity' }, { path: '/identity/administration/users/add', name: 'Add User', component: AddUser }, + { + path: '/identity/administration/users/addbulk', + name: 'Add User Bulk', + component: AddUserBulk, + }, + { path: '/identity/administration/users/edit', name: 'Edit User', component: EditUser }, { path: '/identity/administration/users/view', name: 'View User', component: ViewUser }, { @@ -473,7 +489,22 @@ const routes = [ component: AutopilotListStatusPages, }, { path: '/endpoint/MEM', name: 'MEM' }, - { path: '/endpoint/MEM/list-policies', name: 'List MEM Policies', component: IntuneListPolicies }, + { + path: '/endpoint/MEM/list-policies', + name: 'List Intune Policies', + component: IntuneListPolicies, + }, + { + path: '/endpoint/MEM/list-compliance-policies', + name: 'List Intune Compliance Policies', + component: IntuneListCompliance, + }, + { + path: '/endpoint/MEM/list-appprotection-policies', + name: 'List App Protection Policies', + component: IntuneListAppProtection, + }, + { path: '/endpoint/MEM/edit-policy', name: 'Edit MEM Policy', component: MEMEditPolicy }, { path: '/endpoint/MEM/ca-policies', name: 'List Status Pages', component: IntuneCAPolicies }, { path: '/endpoint/MEM/add-policy', name: 'Add Intune Policy', component: IntuneAddPolicy }, @@ -582,6 +613,11 @@ const routes = [ name: 'Mailbox Restores', component: MailboxRestores, }, + { + path: '/email/tools/mail-test', + name: 'Mail Test', + component: MailTest, + }, { path: '/email/spamfilter/add-template', name: 'Add Spamfilter Template', diff --git a/src/views/cipp/app-settings/SettingsGeneral.jsx b/src/views/cipp/app-settings/SettingsGeneral.jsx index 887758f30abb..e2c887ecd806 100644 --- a/src/views/cipp/app-settings/SettingsGeneral.jsx +++ b/src/views/cipp/app-settings/SettingsGeneral.jsx @@ -85,13 +85,6 @@ export function SettingsGeneral() { omit: showExtendedInfo, exportSelector: 'GDAPRoles', }, - { - name: 'SAM User Roles', - selector: (row) => row?.SAMUserRoles, - cell: cellTableFormatter('SAMUserRoles', false, true), - omit: showExtendedInfo, - exportSelector: 'SAMUserRoles', - }, ] const checkGDAPColumns = [ @@ -439,6 +432,7 @@ export function SettingsGeneral() { columns={checkAccessColumns} tableProps={tableProps} data={accessCheckResult.data.Results} + dynamicColumns={false} /> )} diff --git a/src/views/email-exchange/tools/MailTest.jsx b/src/views/email-exchange/tools/MailTest.jsx new file mode 100644 index 000000000000..d433e658dba3 --- /dev/null +++ b/src/views/email-exchange/tools/MailTest.jsx @@ -0,0 +1,126 @@ +import { CButton, CSpinner } from '@coreui/react' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import React from 'react' +import { CippCallout, CippPageList } from 'src/components/layout' +import { cellBooleanFormatter, cellDateFormatter } from 'src/components/tables' +import { cellGenericFormatter } from 'src/components/tables/CellGenericFormat' +import { useGenericGetRequestQuery } from 'src/store/api/app' + +const MailTest = () => { + const configQuery = useGenericGetRequestQuery({ + path: '/api/ExecMailTest', + params: { Action: 'CheckConfig' }, + }) + + function handleConfigRetry() { + configQuery.refetch() + } + + const columns = [ + { + name: 'Received', + selector: (row) => row['Received'], + sortable: true, + exportSelector: 'Received', + cell: cellDateFormatter({ format: 'short' }), + }, + { + name: 'From', + selector: (row) => row['From'], + sortable: true, + exportSelector: 'From', + cell: cellGenericFormatter(), + }, + { + name: 'Subject', + selector: (row) => row['Subject'], + sortable: true, + exportSelector: 'Subject', + cell: cellGenericFormatter(), + }, + { + name: 'SPF', + selector: (row) => row?.AuthResult.filter((x) => x?.Name === 'spf')[0].Status == 'pass', + cell: cellBooleanFormatter(), + }, + { + name: 'DKIM', + selector: (row) => row?.AuthResult.filter((x) => x?.Name === 'dkim')[0].Status == 'pass', + cell: cellBooleanFormatter(), + }, + { + name: 'DMARC', + selector: (row) => row?.AuthResult.filter((x) => x?.Name === 'dmarc')[0].Status == 'pass', + cell: cellBooleanFormatter(), + }, + { + name: 'Comp Auth', + selector: (row) => row?.AuthResult.filter((x) => x?.Name === 'compauth')[0].Status == 'pass', + cell: cellBooleanFormatter(), + }, + { + name: 'Auth Details', + selector: (row) => row['AuthResult'], + exportSelector: 'AuthResult', + cell: cellGenericFormatter(), + }, + { + name: 'Headers', + selector: (row) => row['Headers'], + exportSelector: 'Headers', + cell: cellGenericFormatter(), + }, + { + name: 'Open Message', + selector: (row) => row['Link'], + exportSelector: 'Link', + cell: cellGenericFormatter(), + }, + ] + return ( +
+ {configQuery.isSuccess && ( + + {configQuery.data?.HasMailRead && ( + <> + Mail test email: + x?.IsPrimary)[0]?.Address + } + > + {configQuery.data?.MailAddresses.filter((x) => x?.IsPrimary)[0]?.Address} + + + )} + {configQuery.data?.HasMailRead == false && ( + <> + Permission Check: {configQuery.data?.Message}{' '} + handleConfigRetry()}> + {configQuery.isLoading ? : } Retry + + + )} + + )} + {configQuery.isLoading && } + {configQuery.isSuccess && configQuery.data?.HasMailRead === true && ( + + )} +
+ ) +} + +export default MailTest diff --git a/src/views/endpoint/autopilot/AutopilotListDevices.jsx b/src/views/endpoint/autopilot/AutopilotListDevices.jsx index f7c58629f4b8..486d289bb881 100644 --- a/src/views/endpoint/autopilot/AutopilotListDevices.jsx +++ b/src/views/endpoint/autopilot/AutopilotListDevices.jsx @@ -14,71 +14,73 @@ import { useLazyGenericGetRequestQuery } from 'src/store/api/app' import { CellTip } from 'src/components/tables' import { TitleButton } from 'src/components/buttons' -const AutopilotListDevices = () => { +const OffCanvas = (row, index, column) => { const tenant = useSelector((state) => state.app.currentTenant) - const [ExecuteGetRequest, getResults] = useLazyGenericGetRequestQuery() + const [ocVisible, setOCVisible] = useState(false) - const Actions = (row, index, column) => { - return ( - <> - setOCVisible(true)}> - - - + setOCVisible(true)}> + + + setOCVisible(false)} - /> - - ) - } + modalMessage: 'Select the user to assign', + }, + { + label: 'Delete Device', + color: 'danger', + modal: true, + modalUrl: `/api/RemoveAPDevice?ID=${row.id}&tenantFilter=${tenant.defaultDomainName}`, + modalMessage: 'Are you sure you want to delete this device?', + }, + ]} + placement="end" + visible={ocVisible} + id={row.id} + hideFunction={() => setOCVisible(false)} + /> + + ) +} +const AutopilotListDevices = () => { + const [ExecuteGetRequest, getResults] = useLazyGenericGetRequestQuery() + const tenant = useSelector((state) => state.app.currentTenant) const columns = [ { @@ -124,8 +126,8 @@ const AutopilotListDevices = () => { exportSelector: 'enrollmentState', }, { - name: (row) => row['Actions'], - cell: Actions, + name: 'Actions', + cell: OffCanvas, }, ] diff --git a/src/views/endpoint/intune/MEMAddPolicy.jsx b/src/views/endpoint/intune/MEMAddPolicy.jsx index 6c7efa88c689..9ab14f4840ca 100644 --- a/src/views/endpoint/intune/MEMAddPolicy.jsx +++ b/src/views/endpoint/intune/MEMAddPolicy.jsx @@ -170,6 +170,8 @@ const AddPolicy = () => { { label: 'Administrative Template', value: 'Admin' }, { label: 'Settings Catalog', value: 'Catalog' }, { label: 'Custom Configuration', value: 'Device' }, + { label: 'App Protection or Configuration Policy', value: 'AppProtection' }, + { label: 'Compliance Policy', value: 'deviceCompliancePolicies' }, ]} /> diff --git a/src/views/endpoint/intune/MEMListAppProtection.jsx b/src/views/endpoint/intune/MEMListAppProtection.jsx new file mode 100644 index 000000000000..9f63b4820fde --- /dev/null +++ b/src/views/endpoint/intune/MEMListAppProtection.jsx @@ -0,0 +1,131 @@ +import React, { useState } from 'react' +import { useSelector } from 'react-redux' +import { CButton } from '@coreui/react' +import { + faBook, + faEdit, + faEllipsisV, + faGlobeEurope, + faPager, + faTrashAlt, + faUser, +} from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { CippPageList } from 'src/components/layout' +import { Link } from 'react-router-dom' +import { CippActionsOffcanvas, CippCodeBlock } from 'src/components/utilities' +import { TitleButton } from 'src/components/buttons' +import { cellBooleanFormatter, cellDateFormatter } from 'src/components/tables' + +const Actions = (row, rowIndex, formatExtraData) => { + const [ocVisible, setOCVisible] = useState(false) + console.log(row) + const tenant = useSelector((state) => state.app.currentTenant) + return ( + <> + setOCVisible(true)}> + + + , + modalUrl: `/api/AddIntuneTemplate?TenantFilter=${tenant.defaultDomainName}&ID=${row.id}&URLName=managedAppPolicies`, + modalMessage: 'Are you sure you want to create a template based on this policy?', + }, + { + label: 'Delete Policy', + color: 'danger', + modal: true, + icon: , + modalUrl: `/api/RemovePolicy?TenantFilter=${tenant.defaultDomainName}&ID=${row.id}&URLName=${row.URLName}`, + modalMessage: 'Are you sure you want to delete this policy?', + }, + ]} + placement="end" + visible={ocVisible} + id={row.id} + hideFunction={() => setOCVisible(false)} + /> + + ) +} + +const columns = [ + { + selector: (row) => row['displayName'], + name: 'Name', + sortable: true, + exportSelector: 'displayName', + }, + { + selector: (row) => row['isAssigned'], + name: 'Is Assigned', + sortable: true, + exportSelector: 'isAssigned', + cell: cellBooleanFormatter(), + }, + { + selector: (row) => row['lastModifiedDateTime'], + name: 'Last Modified', + exportSelector: 'lastModifiedDateTime', + cell: cellDateFormatter({ format: 'relative' }), + }, + { + name: 'Actions', + cell: Actions, + maxWidth: '80px', + }, +] + +const AppProtectionList = () => { + const tenant = useSelector((state) => state.app.currentTenant) + + // eslint-disable-next-line react/prop-types + const ExpandedComponent = ({ data }) => ( + // eslint-disable-next-line react/prop-types + + ) + + return ( + + + + } + tenantSelector={true} + datatable={{ + path: '/api/ListGraphRequest', + params: { + TenantFilter: tenant?.defaultDomainName, + Endpoint: 'deviceAppManagement/managedAppPolicies', + $orderby: 'displayName', + }, + columns, + reportName: `${tenant?.defaultDomainName}-MEMPolicies-List`, + tableProps: { + expandableRows: true, + expandableRowsComponent: ExpandedComponent, + expandOnRowClicked: true, + }, + }} + /> + ) +} + +export default AppProtectionList diff --git a/src/views/endpoint/intune/MEMListCompliance.jsx b/src/views/endpoint/intune/MEMListCompliance.jsx new file mode 100644 index 000000000000..fed996b957b3 --- /dev/null +++ b/src/views/endpoint/intune/MEMListCompliance.jsx @@ -0,0 +1,155 @@ +import React, { useState } from 'react' +import { useSelector } from 'react-redux' +import { CButton } from '@coreui/react' +import { + faBook, + faEdit, + faEllipsisV, + faGlobeEurope, + faPager, + faTrashAlt, + faUser, +} from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { CippPageList } from 'src/components/layout' +import { Link } from 'react-router-dom' +import { CippActionsOffcanvas, CippCodeBlock } from 'src/components/utilities' +import { TitleButton } from 'src/components/buttons' +import { cellBooleanFormatter, cellDateFormatter } from 'src/components/tables' + +const Actions = (row, rowIndex, formatExtraData) => { + const [ocVisible, setOCVisible] = useState(false) + console.log(row) + const tenant = useSelector((state) => state.app.currentTenant) + return ( + <> + setOCVisible(true)}> + + + , + modalUrl: `/api/AddIntuneTemplate?TenantFilter=${tenant.defaultDomainName}&ID=${row.id}&URLName=deviceCompliancePolicies`, + modalMessage: 'Are you sure you want to create a template based on this policy?', + }, + { + icon: , + label: ' Assign to All Users', + color: 'info', + modal: true, + modalUrl: `/api/ExecAssignPolicy?AssignTo=allLicensedUsers&TenantFilter=${tenant.defaultDomainName}&ID=${row.id}&type=deviceCompliancePolicies`, + modalMessage: `Are you sure you want to assign ${row.displayName} to all users?`, + }, + { + icon: , + label: ' Assign to All Devices', + color: 'info', + modal: true, + modalUrl: `/api/ExecAssignPolicy?AssignTo=AllDevices&TenantFilter=${tenant.defaultDomainName}&ID=${row.id}&type=deviceCompliancePolicies`, + modalMessage: `Are you sure you want to assign ${row.displayName} to all devices?`, + }, + { + icon: , + label: ' Assign Globally (All Users / All Devices)', + color: 'info', + modal: true, + modalUrl: `/api/ExecAssignPolicy?AssignTo=AllDevicesAndUsers&TenantFilter=${tenant.defaultDomainName}&ID=${row.id}&type=deviceCompliancePolicies`, + modalMessage: `Are you sure you want to assign ${row.displayName} to all users and devices?`, + }, + { + label: 'Delete Policy', + color: 'danger', + modal: true, + icon: , + modalUrl: `/api/RemovePolicy?TenantFilter=${tenant.defaultDomainName}&ID=${row.id}&URLName=${row.URLName}`, + modalMessage: 'Are you sure you want to delete this policy?', + }, + ]} + placement="end" + visible={ocVisible} + id={row.id} + hideFunction={() => setOCVisible(false)} + /> + + ) +} + +const columns = [ + { + selector: (row) => row['displayName'], + name: 'Name', + sortable: true, + exportSelector: 'displayName', + }, + { + selector: (row) => row['description'], + name: 'Description', + sortable: true, + exportSelector: 'description', + }, + { + selector: (row) => row['lastModifiedDateTime'], + name: 'Last Modified', + exportSelector: 'lastModifiedDateTime', + cell: cellDateFormatter({ format: 'relative' }), + }, + { + name: 'Actions', + cell: Actions, + maxWidth: '80px', + }, +] + +const ComplianceList = () => { + const tenant = useSelector((state) => state.app.currentTenant) + + // eslint-disable-next-line react/prop-types + const ExpandedComponent = ({ data }) => ( + // eslint-disable-next-line react/prop-types + + ) + + return ( + + + + } + tenantSelector={true} + datatable={{ + path: '/api/ListGraphRequest', + params: { + TenantFilter: tenant?.defaultDomainName, + Endpoint: 'deviceManagement/deviceCompliancePolicies', + $orderby: 'displayName', + $count: true, + }, + columns, + reportName: `${tenant?.defaultDomainName}-MEMPolicies-List`, + tableProps: { + expandableRows: true, + expandableRowsComponent: ExpandedComponent, + expandOnRowClicked: true, + }, + }} + /> + ) +} + +export default ComplianceList diff --git a/src/views/endpoint/intune/MEMListPolicies.jsx b/src/views/endpoint/intune/MEMListPolicies.jsx index 980d68010132..cd54f68eb9f6 100644 --- a/src/views/endpoint/intune/MEMListPolicies.jsx +++ b/src/views/endpoint/intune/MEMListPolicies.jsx @@ -47,7 +47,7 @@ const Actions = (row, rowIndex, formatExtraData) => { label: ' Assign to All Users', color: 'info', modal: true, - modalUrl: `/api/ExecAssignPolicy?AssignTo=allLicensedUsers&TenantFilter=${tenant.defaultDomainName}&ID=${row.id}`, + modalUrl: `/api/ExecAssignPolicy?AssignTo=allLicensedUsers&TenantFilter=${tenant.defaultDomainName}&ID=${row.id}&type=${row.URLName}`, modalMessage: `Are you sure you want to assign ${row.displayName} to all users?`, }, { @@ -55,7 +55,7 @@ const Actions = (row, rowIndex, formatExtraData) => { label: ' Assign to All Devices', color: 'info', modal: true, - modalUrl: `/api/ExecAssignPolicy?AssignTo=AllDevices&TenantFilter=${tenant.defaultDomainName}&ID=${row.id}`, + modalUrl: `/api/ExecAssignPolicy?AssignTo=AllDevices&TenantFilter=${tenant.defaultDomainName}&ID=${row.id}&type=${row.URLName}`, modalMessage: `Are you sure you want to assign ${row.displayName} to all devices?`, }, { @@ -63,7 +63,7 @@ const Actions = (row, rowIndex, formatExtraData) => { label: ' Assign Globally (All Users / All Devices)', color: 'info', modal: true, - modalUrl: `/api/ExecAssignPolicy?AssignTo=AllDevicesAndUsers&TenantFilter=${tenant.defaultDomainName}&ID=${row.id}`, + modalUrl: `/api/ExecAssignPolicy?AssignTo=AllDevicesAndUsers&TenantFilter=${tenant.defaultDomainName}&ID=${row.id}&type=${row.URLName}`, modalMessage: `Are you sure you want to assign ${row.displayName} to all users and devices?`, }, { @@ -121,7 +121,7 @@ const IntuneList = () => { return ( { + + { } /> + + + diff --git a/src/views/identity/administration/AddGroupTemplate.jsx b/src/views/identity/administration/AddGroupTemplate.jsx index 80d54b54c575..1a6bd6ecf6e6 100644 --- a/src/views/identity/administration/AddGroupTemplate.jsx +++ b/src/views/identity/administration/AddGroupTemplate.jsx @@ -70,6 +70,11 @@ const AddGroupTemplate = () => { value="security" />{' '} + { } /> + + + + diff --git a/src/views/identity/administration/AddUserBulk.jsx b/src/views/identity/administration/AddUserBulk.jsx new file mode 100644 index 000000000000..9b1e6dd3a507 --- /dev/null +++ b/src/views/identity/administration/AddUserBulk.jsx @@ -0,0 +1,256 @@ +import React, { useState } from 'react' +import { CButton, CCallout, CCol, CRow, CSpinner } from '@coreui/react' +import { Field, FormSpy } from 'react-final-form' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faExclamationTriangle, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons' +import { CippCallout, CippWizard } from 'src/components/layout' +import PropTypes from 'prop-types' +import { RFFCFormInput } from 'src/components/forms' +import { CippTable } from 'src/components/tables' +import { CippCodeBlock, TenantSelector } from 'src/components/utilities' +import { CSVReader } from 'react-papaparse' +import { useLazyGenericPostRequestQuery } from 'src/store/api/app' +import { useSelector } from 'react-redux' + +const Error = ({ name }) => ( + + touched && error ? ( + + + {error} + + ) : null + } + /> +) + +Error.propTypes = { + name: PropTypes.string.isRequired, +} + +const AddUserBulk = () => { + const [genericPostRequest, postResults] = useLazyGenericPostRequestQuery() + const [BulkUser, setBulkUser] = useState([]) + const currentSettings = useSelector((state) => state.app) + const addedFields = currentSettings?.userSettingsDefaults?.defaultAttributes + ? //if we have default attributes, add the label object to the fields array + currentSettings.userSettingsDefaults.defaultAttributes.map((item) => item.label) + : [] + const fields = [ + 'givenName', + 'surName', + 'displayName', + 'mailNickName', + 'domain', + 'usageLocation', + 'JobTitle', + 'streetAddress', + 'PostalCode', + 'City', + 'State', + 'Department', + 'MobilePhone', + 'businessPhones', + ...addedFields, + ] + const columns = fields.map((field) => { + return { + name: field, + selector: (row) => row[field], + sortable: true, + } + }) + + const tableColumns = [ + ...columns, + { + name: 'Remove', + button: true, + cell: (row, index) => { + return ( + handleRemove(row)} size="sm" variant="ghost" color="danger"> + + + ) + }, + }, + ] + const valbutton = (value) => + BulkUser.length + ? undefined + : 'You must add at least one user. Did you forget to click add or upload the CSV?' + const handleOnDrop = (data) => { + const importdata = data.map((item) => { + //find any keys that have a null or blank string value, and remove them + Object.keys(item.data).forEach((key) => { + if (item.data[key] === null || item.data[key] === '') { + delete item.data[key] + } + }) + return item.data + }) + setBulkUser([...BulkUser, ...importdata]) + // console.log(importdata) + } + + const handleOnError = (err, file, inputElem, reason) => { + //set upload error + } + const tenantDomain = useSelector((state) => state.app.currentTenant.defaultDomainName) + + const handleSubmit = async (values) => { + const shippedValues = { + TenantFilter: tenantDomain, + BulkUser, + ...values, + } + //alert(JSON.stringify(values, null, 2)) + genericPostRequest({ path: '/api/AddUserBulk', values: shippedValues }) + } + const addRowtoData = (values) => { + setBulkUser((prevState) => { + if (prevState) { + return [values, ...prevState] + } else { + return [values] + } + }) + } + const handleRemove = async (itemindex) => { + let RemovedItems = BulkUser.filter((item) => item !== itemindex) + setBulkUser((prevState) => { + return RemovedItems + }) + } + return ( + + +
+

Step 1

+
Choose a tenant
+
+
+ {(props) => } + +
+
+ +
+

Step 2

+
Enter user information
+
+
+ + + + Drop CSV file here or click to upload. + + +

+ + {fields.map((field, idx) => { + return ( + + + + ) + })} + + + {/* eslint-disable react/prop-types */} + {(props) => { + return ( + <> + addRowtoData(props.values)} + name="addButton" + className="mb-3" + > + + Add + + + ) + }} + + + + + + + + {BulkUser && ( + + )} + + + +
+
+ +
+

Step 4

+
Confirm and apply
+
+ {postResults.isFetching && ( + + Loading + + )} +

+ {postResults.isSuccess && ( + { + return

  • {item}
  • + })} + callout={true} + calloutCopyValue={postResults.data?.Results} + /> + )} +

    + {BulkUser && ( + + )} +
    +
    +
    +
    + ) +} + +export default AddUserBulk diff --git a/src/views/identity/administration/DeployGroupTemplate.jsx b/src/views/identity/administration/DeployGroupTemplate.jsx index 64b730df4d43..e0524fa0d970 100644 --- a/src/views/identity/administration/DeployGroupTemplate.jsx +++ b/src/views/identity/administration/DeployGroupTemplate.jsx @@ -153,6 +153,8 @@ const ApplyGroupTemplate = () => { placeholder="Select a group type" values={[ { label: 'Dynamic Group', value: 'dynamic' }, + { label: 'Dynamic Distribution Group (Beta)', value: 'dynamicdistribution' }, + { label: 'Security Group', value: 'generic' }, { label: 'Distribution group', value: 'distribution' }, { label: 'Azure Role Group', value: 'azurerole' }, @@ -207,6 +209,14 @@ const ApplyGroupTemplate = () => { placeholder="Enter membership rule syntax" /> + + +

    diff --git a/src/views/identity/administration/EditUser.jsx b/src/views/identity/administration/EditUser.jsx index f456f6f9c4fd..4f2aef34773e 100644 --- a/src/views/identity/administration/EditUser.jsx +++ b/src/views/identity/administration/EditUser.jsx @@ -107,7 +107,7 @@ const EditUser = () => { PostalCode: values.postalCode, usageLocation: values.usageLocation ? values.usageLocation.value : '', UserID: userId, - Username: values.mailNickname, + Username: values.username, streetAddress: values.streetAddress, tenantID: tenantDomain, mustchangepass: values.RequirePasswordChange, @@ -222,7 +222,7 @@ const EditUser = () => { diff --git a/src/views/identity/administration/OffboardingWizard.jsx b/src/views/identity/administration/OffboardingWizard.jsx index 86ec8293eff6..688be60b885f 100644 --- a/src/views/identity/administration/OffboardingWizard.jsx +++ b/src/views/identity/administration/OffboardingWizard.jsx @@ -74,7 +74,7 @@ const OffboardingWizard = () => { RemoveLicenses: values.RemoveLicenses, ResetPass: values.ResetPass, RevokeSessions: values.RevokeSessions, - user: values.User.value, + user: values.User, deleteuser: values.DeleteUser, removeRules: values.RemoveRules, removeMobile: values.RemoveMobile, @@ -119,6 +119,7 @@ const OffboardingWizard = () => {
    ({ value: user.userPrincipalName, @@ -128,6 +129,17 @@ const OffboardingWizard = () => { name="User" /> {usersError && Failed to load list of users} + + {/* eslint-disable react/prop-types */} + {(props) => ( + <> + {console.log(props.values)} + {props.values.User?.length >= 3 && ( + A maximum of three users is recommend. + )} + + )} +

    @@ -278,10 +290,16 @@ const OffboardingWizard = () => {
    Selected Tenant:
    {tenantDomain} - -
    Selected User:
    - {props.values.User.value} -
    + + {props.values.User.map((user) => ( + +
    Selected User:
    + {user.value} +
    + ))}
    diff --git a/src/views/identity/administration/Users.jsx b/src/views/identity/administration/Users.jsx index 51cef1f0eede..dc3b5243815b 100644 --- a/src/views/identity/administration/Users.jsx +++ b/src/views/identity/administration/Users.jsx @@ -428,6 +428,13 @@ const Users = (row) => { title="Invite Guest" /> +
    + +
    ) diff --git a/src/views/tenant/administration/ListAlertsQueue.jsx b/src/views/tenant/administration/ListAlertsQueue.jsx index 1c155771a9bd..abce88eca036 100644 --- a/src/views/tenant/administration/ListAlertsQueue.jsx +++ b/src/views/tenant/administration/ListAlertsQueue.jsx @@ -39,6 +39,8 @@ const alertsList = [ inputName: 'SharePointQuotaQuota', }, { name: 'ExpiringLicenses', label: 'Alert on licenses expiring in 30 days' }, + { name: 'NewAppApproval', label: 'Alert on new apps in the application approval list' }, + { name: 'SecDefaultsUpsell', label: 'Alert on Security Defaults automatic enablement' }, { name: 'DefenderStatus', @@ -221,7 +223,6 @@ const ListClassicAlerts = () => { ) diff --git a/src/views/tenant/administration/Tenants.jsx b/src/views/tenant/administration/Tenants.jsx index 24b700a30f08..87ab0a9d12da 100644 --- a/src/views/tenant/administration/Tenants.jsx +++ b/src/views/tenant/administration/Tenants.jsx @@ -11,8 +11,9 @@ import { CippTenantOffcanvasRow } from 'src/components/utilities/CippTenantOffca const TenantsList = () => { const TenantListSelector = useSelector((state) => state.app.TenantListSelector) const tenant = useSelector((state) => state.app.currentTenant) + console.log('TenantListSelector', TenantListSelector) const [columnOmits, setOmitVisible] = useState(TenantListSelector) - + console.log('columnOmits', columnOmits) const generatePortalColumn = (portal) => ({ name: portal.label, omit: columnOmits, @@ -59,7 +60,7 @@ const TenantsList = () => { ] const titleButton = ( setOmitVisible(!columnOmits)} title={columnOmits ? 'Show Direct Links' : 'Hide Direct Links'} /> @@ -73,6 +74,7 @@ const TenantsList = () => { datatable={{ keyField: 'id', columns, + dynamicColumns: false, reportName: `${tenant.tenantId}-Tenants-List`, path: '/api/ListTenants', }} diff --git a/version_latest.txt b/version_latest.txt index 84197c89467d..8a30e8f94a39 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -5.3.2 +5.4.0