From 05918cddbeaf8554cbbeff823e7ffc48e9632cb0 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 13 Dec 2023 08:05:16 -0500 Subject: [PATCH] Tenant Onboarding Wizard Extend CippWizard - add hideSubmit flag Extend WizardTableField - allow for additional CippDatatable properties --- src/_nav.jsx | 5 + src/adminRoutes.js | 8 + src/components/layout/CippWizard.jsx | 6 +- src/components/tables/WizardTableField.jsx | 5 +- .../administration/TenantOnboardingWizard.jsx | 402 ++++++++++++++++++ 5 files changed, 423 insertions(+), 3 deletions(-) create mode 100644 src/views/tenant/administration/TenantOnboardingWizard.jsx diff --git a/src/_nav.jsx b/src/_nav.jsx index fc47f52686b5..1202c3838a9a 100644 --- a/src/_nav.jsx +++ b/src/_nav.jsx @@ -142,6 +142,11 @@ const _nav = [ name: 'App Consent Requests', to: '/tenant/administration/app-consent-requests', }, + { + component: CNavItem, + name: 'Tenant Onboarding', + to: '/tenant/administration/tenant-onboarding-wizard', + }, { component: CNavItem, name: 'Tenant Offboarding', diff --git a/src/adminRoutes.js b/src/adminRoutes.js index 5192ca20a203..ece783822eff 100644 --- a/src/adminRoutes.js +++ b/src/adminRoutes.js @@ -14,6 +14,9 @@ const appapproval = React.lazy(() => import('src/views/cipp/AppApproval')) const TenantOffboardingWizard = React.lazy(() => import('src/views/tenant/administration/TenantOffboardingWizard'), ) +const TenantOnboardingWizard = React.lazy(() => + import('src/views/tenant/administration/TenantOnboardingWizard'), +) const adminRoutes = [ { path: '/cipp', name: 'CIPP' }, @@ -46,6 +49,11 @@ const adminRoutes = [ name: 'Tenant Offboarding', component: TenantOffboardingWizard, }, + { + path: '/tenant/administration/tenant-onboarding-wizard', + name: 'Tenant Onboarding', + component: TenantOnboardingWizard, + }, ] export default adminRoutes diff --git a/src/components/layout/CippWizard.jsx b/src/components/layout/CippWizard.jsx index 02c4220b48a4..9ebfc1097fae 100644 --- a/src/components/layout/CippWizard.jsx +++ b/src/components/layout/CippWizard.jsx @@ -13,6 +13,7 @@ export default class CippWizard extends React.Component { onPageChange: PropTypes.func, nextPage: PropTypes.func, previousPage: PropTypes.func, + hideSubmit: PropTypes.bool, } static defaultProps = { @@ -27,6 +28,7 @@ export default class CippWizard extends React.Component { page: 0, values: props.initialValues, wizardTitle: props.wizardTitle, + hideSubmit: props.hideSubmit, } } @@ -64,7 +66,7 @@ export default class CippWizard extends React.Component { render() { const { children } = this.props - const { page, values, wizardTitle } = this.state + const { page, values, wizardTitle, hideSubmit } = this.state const activePage = React.Children.toArray(children)[page] const isLastPage = page === React.Children.count(children) - 1 @@ -104,7 +106,7 @@ export default class CippWizard extends React.Component { Next ยป )} - {isLastPage && ( + {isLastPage && !hideSubmit && ( <> Submit diff --git a/src/components/tables/WizardTableField.jsx b/src/components/tables/WizardTableField.jsx index 3e61e1e4d1a8..bb6ba35a9da1 100644 --- a/src/components/tables/WizardTableField.jsx +++ b/src/components/tables/WizardTableField.jsx @@ -13,6 +13,7 @@ export default class WizardTableField extends React.Component { reportName: PropTypes.string.isRequired, keyField: PropTypes.string.isRequired, path: PropTypes.string.isRequired, + params: PropTypes.object, columns: PropTypes.array.isRequired, fieldProps: PropTypes.object, } @@ -56,7 +57,7 @@ export default class WizardTableField extends React.Component { } render() { - const { reportName, keyField, columns, path } = this.props + const { reportName, keyField, columns, path, params, ...props } = this.props return ( ) } diff --git a/src/views/tenant/administration/TenantOnboardingWizard.jsx b/src/views/tenant/administration/TenantOnboardingWizard.jsx new file mode 100644 index 000000000000..879b2d8328b3 --- /dev/null +++ b/src/views/tenant/administration/TenantOnboardingWizard.jsx @@ -0,0 +1,402 @@ +import React, { useState, useRef, useEffect } from 'react' +import { + CAccordion, + CAccordionBody, + CAccordionHeader, + CAccordionItem, + CButton, + CCallout, + CCol, + CFormLabel, + CListGroup, + CListGroupItem, + CRow, + CSpinner, +} from '@coreui/react' +import { Field, FormSpy } from 'react-final-form' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faExclamationTriangle, faTimes, faCheck } from '@fortawesome/free-solid-svg-icons' +import { useSelector } from 'react-redux' +import { CippWizard } from 'src/components/layout' +import PropTypes from 'prop-types' +import { RFFCFormCheck, RFFCFormInput, RFFCFormSwitch, RFFSelectSearch } from 'src/components/forms' +import { CippCodeBlock, TenantSelector } from 'src/components/utilities' +import { useLazyGenericPostRequestQuery } from 'src/store/api/app' +import { + CellDate, + WizardTableField, + cellDateFormatter, + cellNullTextFormatter, +} from 'src/components/tables' +import ReactTimeAgo from 'react-time-ago' + +const Error = ({ name }) => ( + + touched && error ? ( + + + {error} + + ) : null + } + /> +) + +Error.propTypes = { + name: PropTypes.string.isRequired, +} + +function useInterval(callback, delay, state) { + const savedCallback = useRef() + + // Remember the latest callback. + useEffect(() => { + savedCallback.current = callback + }) + + // Set up the interval. + useEffect(() => { + function tick() { + savedCallback.current() + } + + if (delay !== null) { + let id = setInterval(tick, delay) + return () => clearInterval(id) + } + }, [delay, state]) +} + +const RelationshipOnboarding = ({ relationship, gdapRoles }) => { + const [relationshipReady, setRelationshipReady] = useState(false) + const [refreshGuid, setRefreshGuid] = useState(false) + const [getOnboardingStatus, onboardingStatus] = useLazyGenericPostRequestQuery() + var headerIcon = relationshipReady ? 'check-circle' : 'question-circle' + + useInterval( + async () => { + if (onboardingStatus.data?.Status == 'running' || onboardingStatus.data?.Status == 'queued') { + getOnboardingStatus({ + path: '/api/ExecOnboardTenant', + values: { id: relationship.id }, + }) + } + }, + 5000, + onboardingStatus.data, + ) + + return ( + + + {onboardingStatus?.data?.Status == 'running' ? ( + + ) : ( + + )} + Onboarding Relationship: {} + {relationship.displayName} + + + + {(relationship?.customer?.displayName || + onboardingStatus?.data?.Relationship?.customer?.displayName) && ( + +

Customer

+ {onboardingStatus?.data?.Relationship?.customer?.displayName + ? onboardingStatus?.data?.Relationship?.customer?.displayName + : relationship.customer.displayName} +
+ )} + {onboardingStatus?.data?.Timestamp && ( + +

Last Updated

+ +
+ )} + +

Relationship Status

+ {relationship.status} +
+ +

Creation Date

+ +
+ {relationship.status == 'approvalPending' && + onboardingStatus?.data?.Relationship?.status != 'active' && ( + +

Invite URL

+ +
+ )} +
+ {onboardingStatus.isUninitialized && + getOnboardingStatus({ + path: '/api/ExecOnboardTenant', + values: { id: relationship.id, gdapRoles }, + })} + {onboardingStatus.isSuccess && ( + <> + {onboardingStatus.data?.Status == 'failed' && ( + + getOnboardingStatus({ + path: '/api/ExecOnboardTenant?Retry=True', + values: { id: relationship.id, gdapRoles }, + }) + } + className="mb-3" + > + Retry + + )} +
+ {onboardingStatus.data?.OnboardingSteps?.map((step, idx) => ( + + + {step.Status == 'running' ? ( + + ) : ( + + )}{' '} + {step.Title} + + + {step.Message} + + + ))} + + )} +
+
+ ) +} + +const TenantOnboardingWizard = () => { + const tenantDomain = useSelector((state) => state.app.currentTenant.defaultDomainName) + const currentSettings = useSelector((state) => state.app) + const [genericPostRequest, postResults] = useLazyGenericPostRequestQuery() + + const handleSubmit = async (values) => {} + const columns = [ + { + name: 'Tenant', + selector: (row) => row.customer?.displayName, + sortable: true, + exportSelector: 'customer/displayName', + cell: cellNullTextFormatter(), + }, + { + name: 'Relationship Name', + selector: (row) => row['displayName'], + sortable: true, + exportSelector: 'displayName', + }, + { + name: 'Status', + selector: (row) => row['status'], + sortable: true, + exportSelector: 'status', + }, + { + name: 'Created', + selector: (row) => row['createdDateTime'], + sortable: true, + exportSelector: 'createdDateTime', + cell: cellDateFormatter({ format: 'short' }), + }, + { + name: 'Activated', + selector: (row) => row['activatedDateTime'], + sortable: true, + exportSelector: 'activatedDateTime', + cell: cellDateFormatter({ format: 'short' }), + }, + { + name: 'End', + selector: (row) => row['endDateTime'], + sortable: true, + exportSelector: 'endDateTime', + cell: cellDateFormatter({ format: 'short' }), + }, + { + name: 'Auto Extend', + selector: (row) => row['autoExtendDuration'], + sortable: true, + exportSelector: 'endDateTime', + cell: (row) => (row['autoExtendDuration'] === 'PT0S' ? 'No' : 'Yes'), + }, + { + name: 'Includes CA Role', + selector: (row) => row?.accessDetails, + sortable: true, + cell: (row) => + row?.accessDetails?.unifiedRoles?.filter( + (e) => e.roleDefinitionId === '62e90394-69f5-4237-9190-012177145e10', + ).length > 0 + ? 'Yes' + : 'No', + }, + ] + return ( + + +
+

Step 1

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

Step 2

+
Tenant Onboarding Options
+
+
+ + (Optional) Automatically map groups for relationships not created in CIPP. This will not + map groups that do not have a corresponding role in the relationship. + + + {(props) => ( + row['RoleName'], + sortable: true, + exportselector: 'Name', + }, + { + name: 'Group', + selector: (row) => row['GroupName'], + sortable: true, + }, + ]} + fieldProps={props} + /> + )} + +
+
+ +
+

Step 3

+
Tenant Onboarding
+
+
+
+ + {/* eslint-disable react/prop-types */} + {(props) => { + return ( + <> + + + +
Onboarding Status
+ + {props.values.selectedRelationships.map((relationship, idx) => ( + + ))} + +
+
+ + ) + }} +
+
+
+
+
+ ) +} + +export default TenantOnboardingWizard