From d30452b0c27397081c11974c091cf570a7179bae Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 3 Jul 2024 17:13:41 -0400 Subject: [PATCH] Extension tweaks Add Hudu Cleanup mappings --- src/data/Extensions.json | 57 +++- src/views/cipp/ExtensionMappings.jsx | 412 +++++++++++++++++++++++++++ src/views/cipp/Extensions.jsx | 14 +- 3 files changed, 469 insertions(+), 14 deletions(-) create mode 100644 src/views/cipp/ExtensionMappings.jsx diff --git a/src/data/Extensions.json b/src/data/Extensions.json index bc556899e4a8..e46565493f28 100644 --- a/src/data/Extensions.json +++ b/src/data/Extensions.json @@ -1,6 +1,6 @@ [ { - "name": "CIPP-API Integration", + "name": "CIPP-API", "type": "CIPP-API", "cat": "API", "forceSyncButton": false, @@ -21,7 +21,7 @@ "mappingRequired": false }, { - "name": "Gradient Integration", + "name": "Gradient", "type": "Gradient", "cat": "Billing & Invoicing", "forceSyncButton": true, @@ -55,7 +55,7 @@ "mappingRequired": false }, { - "name": "Halo PSA Ticketing Integration", + "name": "Halo PSA Ticketing", "type": "HaloPSA", "cat": "Ticketing", "forceSyncButton": false, @@ -112,7 +112,7 @@ "mappingRequired": true }, { - "name": "NinjaOne Integration", + "name": "NinjaOne", "type": "NinjaOne", "cat": "Documentation & Monitoring", "forceSyncButton": true, @@ -141,18 +141,18 @@ }, { "type": "checkbox", - "name": "NinjaOne.UserDocumentsEnabled", - "label": "Synchronize Detailed User Information (Requires NinjaOne Documentation)" + "name": "NinjaOne.LicenseDocumentsEnabled", + "label": "Sync Licenses (Requires NinjaOne Documentation)" }, { "type": "checkbox", - "name": "NinjaOne.LicenseDocumentsEnabled", - "label": "Synchronize Detailed License Information (Requires NinjaOne Documentation)" + "name": "NinjaOne.UserDocumentsEnabled", + "label": "Sync Users (Requires NinjaOne Documentation)" }, { "type": "checkbox", "name": "NinjaOne.LicensedOnly", - "label": "Only Synchronize Licensed Users" + "label": "Only Sync Licensed Users (Requires NinjaOne Documentation)" }, { "type": "checkbox", @@ -160,7 +160,44 @@ "label": "Enable Integration" } ], - "mappingRequired": true + "mappingRequired": true, + "fieldMapping": true, + "autoMapSyncApi": true + }, + { + "name": "Hudu", + "type": "Hudu", + "cat": "Documentation", + "forceSyncButton": true, + "helpText": "This integration allows you to populate custom asset layouts with Tenant information, monitor device compliance state, document other items and generate relationships inside Hudu.", + "SettingOptions": [ + { + "type": "input", + "fieldtype": "input", + "name": "Hudu.BaseUrl", + "label": "Please enter your Hudu URL", + "placeholder": "https://yourcompany.huducloud.com" + }, + { + "type": "input", + "fieldtype": "password", + "name": "Hudu.APIKey", + "label": "Hudu API Key", + "placeholder": "Enter your Hudu API Key" + }, + { + "type": "checkbox", + "name": "Hudu.LicensedUsersOnly", + "label": "Only Sync Licensed Users" + }, + { + "type": "checkbox", + "name": "Hudu.Enabled", + "label": "Enable Integration" + } + ], + "mappingRequired": true, + "fieldMapping": true }, { "name": "PasswordPusher", diff --git a/src/views/cipp/ExtensionMappings.jsx b/src/views/cipp/ExtensionMappings.jsx new file mode 100644 index 000000000000..131d04bece52 --- /dev/null +++ b/src/views/cipp/ExtensionMappings.jsx @@ -0,0 +1,412 @@ +import { useLazyGenericGetRequestQuery, useLazyGenericPostRequestQuery } from 'src/store/api/app.js' +import { CButton, CCallout, CCardText, CCol, CForm, CRow, CSpinner, CTooltip } 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, { useEffect } from 'react' +import { CippCallout } from 'src/components/layout/index.js' +import { CippTable } from 'src/components/tables' +import { CellTip, cellGenericFormatter } from 'src/components/tables/CellGenericFormat' +import CippButtonCard from 'src/components/contentcards/CippButtonCard' + +/** + * Retrieves and sets the extension mappings for HaloPSA and NinjaOne. + * + * @returns {JSX.Element} - JSX component representing the settings extension mappings. + */ +export default function ExtensionMappings({ type, fieldMappings = false, autoMapSyncApi = false }) { + const [mappingArray, setMappingArray] = React.useState('defaultMapping') + const [mappingValue, setMappingValue] = React.useState({}) + const [tenantMappingArray, setTenantMappingsArray] = React.useState([]) + const [autoMap, setAutoMap] = React.useState(false) + const [listMappingBackend, listMappingBackendResult = []] = useLazyGenericGetRequestQuery() + const [listFieldsBackend, listFieldsBackendResult] = useLazyGenericGetRequestQuery() + const [setExtensionConfig, extensionConfigResult = []] = useLazyGenericPostRequestQuery() + const [setExtensionAutomap, extensionAutomapResult] = useLazyGenericPostRequestQuery() + const [setFieldsExtensionConfig, extensionFieldsConfigResult] = useLazyGenericPostRequestQuery() + + const onOrgSubmit = () => { + console.log(mappingArray) + const originalFormat = mappingArray.reduce((acc, item) => { + acc[item.Tenant?.customerId] = { label: item.companyName, value: item.companyId } + return acc + }, {}) + setExtensionConfig({ + path: `api/ExecExtensionMapping?AddMapping=${type}`, + values: { mappings: originalFormat }, + }).then(() => { + listMappingBackend({ path: `api/ExecExtensionMapping?List=${type}` }) + setMappingValue({}) + }) + } + /*const onNinjaOrgsSubmit = () => { + const originalFormat = ninjaMappingsArray.reduce((acc, item) => { + acc[item.Tenant?.customerId] = { label: item.ninjaName, value: item.ninjaId } + return acc + }, {}) + + setNinjaOrgsExtensionconfig({ + path: 'api/ExecExtensionMapping?AddMapping=NinjaOrgs', + values: { mappings: originalFormat }, + }).then(() => { + listNinjaOrgsBackend({ path: 'api/ExecExtensionMapping?List=NinjaOrgs' }) + setMappingValue({}) + }) + }*/ + + const onOrgsAutomap = async (values) => { + if (autoMapSyncApi) { + await setExtensionAutomap({ + path: `api/ExecExtensionMapping?AutoMapping=${type}`, + values: { mappings: values }, + }) + await listMappingBackend({ + path: `api/ExecExtensionMapping?List=${type}`, + }) + } + + const newMappings = listMappingBackendResult.data?.Tenants.map((tenant) => { + const company = listMappingBackendResult.data?.Companies.find( + (client) => client.name === tenant.displayName, + ) + if (company) { + return { + Tenant: tenant, + companyName: company.name, + companyId: company.value, + } + } + }) + setMappingArray((currentMappings) => [...currentMappings, ...newMappings]) + setAutoMap(true) + } + + const onFieldsSubmit = (values) => { + setFieldsExtensionConfig({ + path: `api/ExecExtensionMapping?AddMapping=${type}Fields`, + values: { mappings: values }, + }) + } + + /*const onHaloAutomap = () => { + const newMappings = listBackendHaloResult.data?.Tenants.map( + (tenant) => { + const haloClient = listBackendHaloResult.data?.HaloClients.find( + (client) => client.name === tenant.displayName, + ) + if (haloClient) { + console.log(haloClient) + console.log(tenant) + return { + Tenant: tenant, + haloName: haloClient.name, + haloId: haloClient.value, + } + } + }, + //filter out any undefined values + ).filter((item) => item !== undefined) + setHaloMappingsArray((currentHaloMappings) => [...currentHaloMappings, ...newMappings]).then( + () => { + listHaloBackend({ path: 'api/ExecExtensionMapping?List=Halo' }) + }, + ) + setHaloAutoMap(true) + }*/ + + useEffect(() => { + if (listMappingBackendResult.isSuccess) { + setMappingArray( + Object.keys(listMappingBackendResult.data?.Mappings).map((key) => ({ + Tenant: listMappingBackendResult.data?.Tenants.find( + (tenant) => tenant.customerId === key, + ), + companyName: listMappingBackendResult.data?.Mappings[key].label, + companyId: listMappingBackendResult.data?.Mappings[key].value, + })), + ) + } + }, [listMappingBackendResult]) + + const Actions = (row, rowIndex, formatExtraData) => { + return ( + <> + + + setMappingArray((currentMappings) => currentMappings.filter((item) => item !== row)) + } + > + + + + + ) + } + const columns = [ + { + name: 'Tenant', + selector: (row) => row.Tenant?.displayName, + sortable: true, + cell: (row) => CellTip(row.Tenant?.displayName), + exportSelector: 'Tenant', + }, + { + name: 'TenantId', + selector: (row) => row.Tenant?.customerId, + sortable: true, + exportSelector: 'Tenant/customerId', + omit: true, + }, + { + name: `${type} Company Name`, + selector: (row) => row['companyName'], + sortable: true, + cell: cellGenericFormatter(), + exportSelector: 'companyName', + }, + { + name: `${type} Company ID`, + selector: (row) => row['companyId'], + sortable: true, + cell: (row) => CellTip(row['companyId']), + exportSelector: 'companyId', + }, + { + name: 'Actions', + cell: Actions, + maxWidth: '80px', + }, + ] + + return ( + + <> + {listMappingBackendResult.isUninitialized && + listMappingBackend({ path: `api/ExecExtensionMapping?List=${type}` })} + {listFieldsBackendResult.isUninitialized && + fieldMappings && + listFieldsBackend({ path: `api/ExecExtensionMapping?List=${type}Fields` })} + + + {extensionConfigResult.isFetching && ( + + )} + Set Mappings + + onOrgsAutomap()} className="me-2"> + {extensionAutomapResult.isFetching && ( + + )} + Automap {type} Organizations + + + } + > + {listMappingBackendResult.isFetching && listMappingBackendResult.isUninitialized ? ( + + ) : ( +
{ + return ( + + + Use the table below to map your client to the correct {type} Organization. + { + //load all the existing mappings and show them first in a table. + listMappingBackendResult.isSuccess && ( + + ) + } + + + { + return !Object.keys(listMappingBackendResult.data?.Mappings).includes( + tenant.customerId, + ) + }).map((tenant) => ({ + name: tenant.displayName, + value: tenant.customerId, + }))} + onChange={(e) => { + setMappingArray(e.value) + }} + isLoading={listMappingBackendResult.isFetching} + /> + + + + + + { + return !Object.values(listMappingBackendResult.data?.Mappings) + .map((value) => { + return value.value + }) + .includes(client.value.toString()) + }).map((client) => ({ + name: client.name, + value: client.value, + }))} + onChange={(e) => setMappingValue(e)} + placeholder={`Select a ${type} Organization`} + isLoading={listMappingBackendResult.isFetching} + /> + + { + //set the new mapping in the array + if ( + mappingValue.value !== undefined && + mappingValue.value !== '-1' && + Object.values(mappingArray) + .map((item) => item.companyId) + .includes(mappingValue.value) === false + ) { + setMappingArray([ + ...mappingArray, + { + Tenant: listMappingBackendResult.data?.Tenants.find( + (tenant) => tenant.customerId === mappingArray, + ), + companyName: mappingValue.label, + companyId: mappingValue.value, + }, + ]) + } + }} + className={`my-4 circular-button`} + title={'+'} + > + + + + + + {(extensionAutomapResult.isSuccess || extensionAutomapResult.isError) && + !extensionAutomapResult.isFetching && ( + + {extensionAutomapResult.isSuccess + ? extensionAutomapResult.data.Results + : 'Error'} + + )} + {(extensionConfigResult.isSuccess || extensionConfigResult.isError) && + !extensionConfigResult.isFetching && ( + + {extensionConfigResult.isSuccess + ? extensionConfigResult.data.Results + : 'Error'} + + )} + + + + After editing the mappings you must click Save Mappings for the changes to + take effect. The table will be saved exactly as presented. + + + ) + }} + /> + )} + + + {fieldMappings && ( + + {extensionFieldsConfigResult.isFetching && ( + + )} + Set Mappings + + } + > + {listFieldsBackendResult.isFetching && listFieldsBackendResult.isUninitialized && ( + + )} + {listFieldsBackendResult.isSuccess && listFieldsBackendResult.data?.Mappings && ( + { + return ( + + {listFieldsBackendResult?.data?.CIPPFieldHeaders?.map((header, key) => ( + +
{header.Title}
+

{header.Description}

+ {listFieldsBackendResult?.data?.CIPPFields?.filter( + (f) => f.FieldType == header.FieldType, + ).map((field, fieldkey) => ( + item.FieldType === field.FieldType || item.type === 'unset', + )} + placeholder="Select a Field" + /> + ))} +
+ ))} + + {(extensionFieldsConfigResult.isSuccess || + extensionFieldsConfigResult.isError) && + !extensionFieldsConfigResult.isFetching && ( + + {extensionFieldsConfigResult.isSuccess + ? extensionFieldsConfigResult.data.Results + : 'Error'} + + )} + +
+ ) + }} + /> + )} +
+ )} + + ) +} diff --git a/src/views/cipp/Extensions.jsx b/src/views/cipp/Extensions.jsx index 8a165cbc0dfd..d08ac0636174 100644 --- a/src/views/cipp/Extensions.jsx +++ b/src/views/cipp/Extensions.jsx @@ -21,7 +21,7 @@ import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' import CippButtonCard from 'src/components/contentcards/CippButtonCard.jsx' import { RFFCFormInput, RFFCFormSwitch } from 'src/components/forms/RFFComponents.jsx' import { Form } from 'react-final-form' -import { SettingsExtensionMappings } from './app-settings/SettingsExtensionMappings' +import ExtensionMappings from 'src/views/cipp/ExtensionMappings.jsx' export default function CIPPExtensions() { const [listBackend, listBackendResult] = useLazyGenericGetRequestQuery() @@ -191,9 +191,15 @@ export default function CIPPExtensions() { )} - - - + {integration.mappingRequired && ( + + + + )}