From c13137bb509b1b0a5b78f9335bcea67c23d37b08 Mon Sep 17 00:00:00 2001 From: Aleix Date: Wed, 20 Nov 2024 13:50:16 -0500 Subject: [PATCH] feat: White glove support (#2271) --- .../ui/src/app/Catalog/CatalogItemForm.tsx | 139 ++++++++---------- .../src/app/Catalog/CatalogItemFormReducer.ts | 14 +- catalog/ui/src/app/Services/ServicesItem.tsx | 98 +++++++----- .../app/Workshops/WorkshopsItemDetails.tsx | 95 ++++-------- catalog/ui/src/app/api.ts | 40 +---- catalog/ui/src/app/util.ts | 27 +--- 6 files changed, 178 insertions(+), 235 deletions(-) diff --git a/catalog/ui/src/app/Catalog/CatalogItemForm.tsx b/catalog/ui/src/app/Catalog/CatalogItemForm.tsx index f55961750..ba7bd4e03 100644 --- a/catalog/ui/src/app/Catalog/CatalogItemForm.tsx +++ b/catalog/ui/src/app/Catalog/CatalogItemForm.tsx @@ -35,7 +35,6 @@ import { createWorkshop, createWorkshopProvision, fetcher, - openWorkshopSupportTicket, } from '@app/api'; import { CatalogItem, TPurposeOpts } from '@app/types'; import { displayName, isLabDeveloper, randomString } from '@app/util'; @@ -52,7 +51,6 @@ import AutoStopDestroy from '@app/components/AutoStopDestroy'; import CatalogItemFormAutoStopDestroyModal, { TDates, TDatesTypes } from './CatalogItemFormAutoStopDestroyModal'; import { formatCurrency, getEstimatedCost, isAutoStopDisabled } from './catalog-utils'; import ErrorBoundaryPage from '@app/components/ErrorBoundaryPage'; -import useImpersonateUser from '@app/utils/useImpersonateUser'; import { SearchIcon } from '@patternfly/react-icons'; import SearchSalesforceIdModal from '@app/components/SearchSalesforceIdModal'; import useInterfaceConfig from '@app/utils/useInterfaceConfig'; @@ -70,12 +68,7 @@ const CatalogItemFormData: React.FC<{ catalogItemName: string; catalogNamespaceN const [searchSalesforceIdModal, openSearchSalesforceIdModal] = useState(false); const [isLoading, setIsLoading] = useState(false); const { isAdmin, groups, roles, serviceNamespaces, userNamespace, email } = useSession().getSession(); - const { userImpersonated } = useImpersonateUser(); const { sfdc_enabled } = useInterfaceConfig(); - let userEmail = email; - if (userImpersonated) { - userEmail = userImpersonated; - } const { data: catalogItem } = useSWRImmutable( apiPaths.CATALOG_ITEM({ namespace: catalogNamespaceName, name: catalogItemName }), fetcher @@ -92,7 +85,6 @@ const CatalogItemFormData: React.FC<{ catalogItemName: string; catalogNamespaceN provisionCount: 1, provisionConcurrency: catalogItem.spec.multiuser ? 1 : 10, provisionStartDelay: 30, - createTicket: false, }), [catalogItem] ); @@ -180,6 +172,7 @@ const CatalogItemFormData: React.FC<{ catalogItemName: string; catalogNamespaceN email, parameterValues, skippedSfdc: formState.salesforceId.skip, + whiteGloved: formState.whiteGloved, }); const redirectUrl = `/workshops/${workshop.metadata.namespace}/${workshop.metadata.name}`; await createWorkshopProvision({ @@ -192,20 +185,6 @@ const CatalogItemFormData: React.FC<{ catalogItemName: string; catalogNamespaceN useAutoDetach: formState.useAutoDetach, usePoolIfAvailable: formState.usePoolIfAvailable, }); - try { - if (formState.workshop.createTicket) { - await openWorkshopSupportTicket(workshop, { - number_of_attendees: provisionCount, - sfdc: formState.salesforceId.value, - name: catalogItemName, - event_name: displayName, - url: `${window.location.origin}${redirectUrl}`, - start_date: formState.startDate, - end_date: formState.endDate, - email: userEmail, - }); - } - } catch {} navigate(redirectUrl); } else { const resourceClaim = await createServiceRequest({ @@ -221,6 +200,7 @@ const CatalogItemFormData: React.FC<{ catalogItemName: string; catalogNamespaceN endDate: formState.endDate, email, skippedSfdc: formState.salesforceId.skip, + whiteGloved: formState.whiteGloved, }); navigate(`/services/${resourceClaim.metadata.namespace}/${resourceClaim.metadata.name}`); @@ -933,69 +913,70 @@ const CatalogItemFormData: React.FC<{ catalogItemName: string; catalogNamespaceN ) : null} - -
- { - dispatchFormState({ - type: 'workshop', - workshop: { ...formState.workshop, createTicket: isChecked }, - }); - }} - /> -
-
)} ) : null} + {isAdmin ? ( - <> - -
- - dispatchFormState({ - type: 'usePoolIfAvailable', - usePoolIfAvailable: isChecked, - }) - } - /> -
-
- + +
+ { + dispatchFormState({ + type: 'whiteGloved', + whiteGloved: isChecked, + }); + }} + /> +
+
+ ) : null} + + {isAdmin ? ( + +
+ + dispatchFormState({ + type: 'usePoolIfAvailable', + usePoolIfAvailable: isChecked, + }) + } + /> +
+
+ ) : null} + + {isAdmin || isLabDeveloper(groups) ? ( + +
+ { + dispatchFormState({ + type: 'useAutoDetach', + useAutoDetach: !isChecked, + }); + }} + /> +
+
) : null} - <> - {isAdmin || isLabDeveloper(groups) ? ( - -
- { - dispatchFormState({ - type: 'useAutoDetach', - useAutoDetach: !isChecked, - }); - }} - /> -
-
- ) : null} - {catalogItem.spec.termsOfService ? ( { - if (m.includes('~')) { - return `pass:[${m}]`; - } - return m; - }) - .join('\n') - .trim() - .replace(/([^\n])\n(?!\n)/g, '$1 +\n') - : null; + ? provisionMessages + .map((m) => { + if (m.includes('~')) { + return `pass:[${m}]`; + } + return m; + }) + .join('\n') + .trim() + .replace(/([^\n])\n(?!\n)/g, '$1 +\n') + : null; const provisionMessagesHtml = useMemo( () => _provisionMessages ? ( @@ -154,7 +156,7 @@ const ComponentDetailsList: React.FC<{ }} /> ) : null, - [_provisionMessages], + [_provisionMessages] ); return ( @@ -281,7 +283,7 @@ const ComponentDetailsList: React.FC<{ {stage} - ) : null, + ) : null )} @@ -302,7 +304,7 @@ function _reducer( salesforceId?: string; salesforceIdValid?: boolean; salesforceType?: SfdcType; - }, + } ) { switch (action.type) { case 'set_salesforceId': @@ -347,7 +349,7 @@ const ServicesItemComponent: React.FC<{ { refreshInterval: 8000, compare: compareK8sObjects, - }, + } ); useErrorHandler(error?.status === 404 ? error : null); @@ -375,7 +377,7 @@ const ServicesItemComponent: React.FC<{ if (!salesforceObj.completed) { checkSalesforceId(salesforceObj.salesforce_id, debouncedApiFetch, salesforceObj.salesforce_type).then( ({ valid, message }: { valid: boolean; message?: string }) => - dispatchSalesforceObj({ type: 'complete', salesforceIdValid: valid }), + dispatchSalesforceObj({ type: 'complete', salesforceIdValid: valid }) ); } else if ( resourceClaim.metadata.annotations?.[`${DEMO_DOMAIN}/salesforce-id`] !== salesforceObj.salesforce_id || @@ -393,16 +395,13 @@ const ServicesItemComponent: React.FC<{ }, [dispatchSalesforceObj, salesforceObj, debouncedApiFetch]); // As admin we need to fetch service namespaces for the service namespace dropdown - const enableFetchUserNamespaces = isAdmin; const { data: userNamespaceList } = useSWR( - enableFetchUserNamespaces ? apiPaths.NAMESPACES({ labelSelector: 'usernamespace.gpte.redhat.com/user-uid' }) : '', - fetcher, + isAdmin ? apiPaths.NAMESPACES({ labelSelector: 'usernamespace.gpte.redhat.com/user-uid' }) : '', + fetcher ); const serviceNamespaces = useMemo(() => { - return enableFetchUserNamespaces - ? userNamespaceList.items.map(namespaceToServiceNamespaceMapper) - : sessionServiceNamespaces; - }, [enableFetchUserNamespaces, sessionServiceNamespaces, userNamespaceList]); + return isAdmin ? userNamespaceList.items.map(namespaceToServiceNamespaceMapper) : sessionServiceNamespaces; + }, [isAdmin, sessionServiceNamespaces, userNamespaceList]); const serviceNamespace = serviceNamespaces.find((ns) => ns.name === serviceNamespaceName) || { name: serviceNamespaceName, displayName: serviceNamespaceName, @@ -410,6 +409,7 @@ const ServicesItemComponent: React.FC<{ const workshopName = resourceClaim.metadata?.labels?.[`${BABYLON_DOMAIN}/workshop`]; const externalPlatformUrl = resourceClaim.metadata?.annotations?.[`${BABYLON_DOMAIN}/internalPlatformUrl`]; const isPartOfWorkshop = isResourceClaimPartOfWorkshop(resourceClaim); + const whiteGloved = getWhiteGloved(resourceClaim); const resourcesK8sObj = (resourceClaim.status?.resources || []).map((r: { state?: K8sObject }) => r.state); const anarchySubjects = resourcesK8sObj .filter((r: K8sObject) => r?.kind === 'AnarchySubject') @@ -487,7 +487,7 @@ const ServicesItemComponent: React.FC<{ .find((u) => u != null); const serviceHasUsers = (resourceClaim.status?.resources || []).find( - (r) => r.state?.spec?.vars?.provision_data?.users, + (r) => r.state?.spec?.vars?.provision_data?.users ) ? true : false; @@ -498,7 +498,7 @@ const ServicesItemComponent: React.FC<{ { refreshInterval: 8000, compare: compareK8sObjects, - }, + } ); const { data: userAssigmentsList, mutate: mutateUserAssigmentsList } = useSWR( workshopName @@ -510,7 +510,7 @@ const ServicesItemComponent: React.FC<{ fetcher, { refreshInterval: 15000, - }, + } ); const costTracker = getCostTracker(resourceClaim); @@ -526,8 +526,8 @@ const ServicesItemComponent: React.FC<{ ? await scheduleStartResourceClaim(resourceClaim) : await startAllResourcesInResourceClaim(resourceClaim) : resourceClaim.status?.summary - ? await scheduleStopResourceClaim(resourceClaim) - : await stopAllResourcesInResourceClaim(resourceClaim); + ? await scheduleStopResourceClaim(resourceClaim) + : await stopAllResourcesInResourceClaim(resourceClaim); mutate(resourceClaimUpdate); globalMutate(SERVICES_KEY({ namespace: resourceClaim.metadata.namespace })); } @@ -537,7 +537,7 @@ const ServicesItemComponent: React.FC<{ resourceClaim.metadata.uid, modalState.rating.rate, modalState.rating.comment, - modalState.rating.useful, + modalState.rating.useful ); globalMutate(apiPaths.USER_RATING({ requestUuid: resourceClaim.metadata.uid })); } @@ -548,7 +548,7 @@ const ServicesItemComponent: React.FC<{ apiPaths.RESOURCE_CLAIM({ namespace: resourceClaim.metadata.namespace, resourceClaimName: resourceClaim.metadata.name, - }), + }) ); cache.delete(SERVICES_KEY({ namespace: resourceClaim.metadata.namespace })); navigate(`/services/${serviceNamespaceName}`); @@ -560,8 +560,8 @@ const ServicesItemComponent: React.FC<{ modalState.action === 'retirement' ? await setLifespanEndForResourceClaim(resourceClaim, date) : resourceClaim.status?.summary - ? await scheduleStopResourceClaim(resourceClaim, date) - : await scheduleStopForAllResourcesInResourceClaim(resourceClaim, date); + ? await scheduleStopResourceClaim(resourceClaim, date) + : await scheduleStopForAllResourcesInResourceClaim(resourceClaim, date); mutate(resourceClaimUpdate); } @@ -597,7 +597,7 @@ const ServicesItemComponent: React.FC<{ openModalCreateWorkshop(); } }, - [openModalAction, openModalCreateWorkshop, openModalScheduleAction], + [openModalAction, openModalCreateWorkshop, openModalScheduleAction] ); const toggle = (id: string) => { @@ -613,7 +613,7 @@ const ServicesItemComponent: React.FC<{ userAssigmentsListClone.items = Array.from(userAssigments); mutateUserAssigmentsList(userAssigmentsListClone); }, - [mutateUserAssigmentsList, userAssigmentsList], + [mutateUserAssigmentsList, userAssigmentsList] ); return ( @@ -945,8 +945,8 @@ const ServicesItemComponent: React.FC<{ ? salesforceObj.completed && salesforceObj.valid ? 'success' : salesforceObj.completed - ? 'error' - : 'default' + ? 'error' + : 'default' : 'default' } /> @@ -963,6 +963,32 @@ const ServicesItemComponent: React.FC<{ ) : null} + + {!isPartOfWorkshop && isAdmin ? ( + + + + { + mutate( + await patchResourceClaim(resourceClaim.metadata.namespace, resourceClaim.metadata.name, { + metadata: { + annotations: { + [`${DEMO_DOMAIN}/white-glove`]: String(isChecked), + }, + }, + }) + ); + }} + /> + + + ) : null} 1} wrapper={(children) => ( diff --git a/catalog/ui/src/app/Workshops/WorkshopsItemDetails.tsx b/catalog/ui/src/app/Workshops/WorkshopsItemDetails.tsx index 131546953..1721abbfb 100644 --- a/catalog/ui/src/app/Workshops/WorkshopsItemDetails.tsx +++ b/catalog/ui/src/app/Workshops/WorkshopsItemDetails.tsx @@ -25,7 +25,7 @@ import { patchWorkshopProvision, } from '@app/api'; import { ResourceClaim, SfdcType, Workshop, WorkshopProvision, WorkshopUserAssignment } from '@app/types'; -import { BABYLON_DOMAIN, DEMO_DOMAIN, getServiceNow } from '@app/util'; +import { BABYLON_DOMAIN, DEMO_DOMAIN, getWhiteGloved } from '@app/util'; import useDebounce from '@app/utils/useDebounce'; import useSession from '@app/utils/useSession'; import EditableText from '@app/components/EditableText'; @@ -36,7 +36,6 @@ import AutoStopDestroy from '@app/components/AutoStopDestroy'; import { checkWorkshopCanStop, getWorkshopAutoStopTime, getWorkshopLifespan } from './workshops-utils'; import { ModalState } from './WorkshopsItem'; import WorkshopStatus from './WorkshopStatus'; -import PencilAltIcon from '@patternfly/react-icons/dist/js/icons/pencil-alt-icon'; import { useSWRConfig } from 'swr'; import './workshops-item-details.css'; @@ -76,14 +75,9 @@ const WorkshopsItemDetails: React.FC<{ showModal?: ({ action, resourceClaims }: ModalState) => void; }> = ({ onWorkshopUpdate, workshopProvisions = [], resourceClaims, workshop, showModal, workshopUserAssignments }) => { const { isAdmin } = useSession().getSession(); - const [editingServiceNow, setEditingServiceNow] = useState(false); const debouncedApiFetch = useDebounce(apiFetch, 1000); const { cache } = useSWRConfig(); - const serviceNowJson = workshop.metadata.annotations?.[`${BABYLON_DOMAIN}/servicenow`]; - const { url: serviceNowUrl, id: serviceNowId } = serviceNowJson - ? getServiceNow(JSON.parse(serviceNowJson)) - : { url: null, id: null }; - const [serviceNowNumber, setServiceNowNumber] = useState(serviceNowId); + const whiteGloved = getWhiteGloved(workshop); const debouncedPatchWorkshop = useDebounce(patchWorkshop, 1000) as (...args: unknown[]) => Promise; const userRegistrationValue = workshop.spec.openRegistration === false ? 'pre' : 'open'; const workshopId = workshop.metadata.labels?.[`${BABYLON_DOMAIN}/workshop-id`]; @@ -187,16 +181,6 @@ const WorkshopsItemDetails: React.FC<{ } } - async function saveServiceNowNumber(serviceNowObj: any): Promise { - onWorkshopUpdate( - await patchWorkshop({ - name: workshop.metadata.name, - namespace: workshop.metadata.namespace, - patch: { metadata: { annotations: { [`${BABYLON_DOMAIN}/servicenow`]: JSON.stringify(serviceNowObj) } } }, - }) - ); - } - return ( @@ -395,51 +379,6 @@ const WorkshopsItemDetails: React.FC<{ ) : null} - {serviceNowUrl ? ( - - White-Glove Support - - {!editingServiceNow ? ( - - ) : editingServiceNow ? ( - setServiceNowNumber(v)} - style={{ width: 'auto', marginRight: '16px' }} - placeholder="RITM0000000" - /> - ) : ( - '-' - )} -