diff --git a/catalog/ui/src/app/Catalog/CatalogItemForm.tsx b/catalog/ui/src/app/Catalog/CatalogItemForm.tsx
index f5596175..ba7bd4e0 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 13154695..1721abbf 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"
- />
- ) : (
- '-'
- )}
-
}>
-
-
-
-
- ) : null}
{workshopProvisions.length > 0 ? (
@@ -550,6 +489,36 @@ const WorkshopsItemDetails: React.FC<{
) : null}
+
+ {isAdmin ? (
+
+
+
+ {
+ onWorkshopUpdate(
+ await patchWorkshop({
+ name: workshop.metadata.name,
+ namespace: workshop.metadata.namespace,
+ patch: {
+ metadata: {
+ annotations: {
+ [`${DEMO_DOMAIN}/white-glove`]: String(isChecked),
+ },
+ },
+ },
+ })
+ );
+ }}
+ />
+
+
+ ) : null}
);
};
diff --git a/catalog/ui/src/app/api.ts b/catalog/ui/src/app/api.ts
index 35d417a9..ef056459 100644
--- a/catalog/ui/src/app/api.ts
+++ b/catalog/ui/src/app/api.ts
@@ -63,6 +63,7 @@ type CreateServiceRequestOpt = {
useAutoDetach: boolean;
email: string;
skippedSfdc: boolean;
+ whiteGloved: boolean;
};
type CreateWorkshopPovisionOpt = {
@@ -361,6 +362,7 @@ export async function createServiceRequest({
useAutoDetach,
email,
skippedSfdc,
+ whiteGloved,
}: CreateServiceRequestOpt): Promise {
const baseUrl = window.location.href.replace(/^([^/]+\/\/[^/]+)\/.*/, '$1');
const session = await getApiSession();
@@ -387,6 +389,7 @@ export async function createServiceRequest({
...(catalogItem.spec.multiuser && catalogItem.spec.messageTemplates?.user
? { [`${DEMO_DOMAIN}/user-message-template`]: JSON.stringify(catalogItem.spec.messageTemplates.user) }
: {}),
+ [`${DEMO_DOMAIN}/white-glove`]: String(whiteGloved),
},
labels: {
[`${BABYLON_DOMAIN}/catalogItemName`]: catalogItem.metadata.name,
@@ -491,6 +494,7 @@ export async function createWorkshop({
email,
parameterValues,
skippedSfdc,
+ whiteGloved,
}: {
accessPassword?: string;
catalogItem: CatalogItem;
@@ -504,6 +508,7 @@ export async function createWorkshop({
email: string;
parameterValues: any;
skippedSfdc: boolean;
+ whiteGloved: boolean;
}): Promise {
const session = await getApiSession();
const _definition: Workshop = {
@@ -530,6 +535,7 @@ export async function createWorkshop({
startDate && startDate.getTime() + parseDuration('6h') > Date.now() ? 'true' : 'false',
[`${DEMO_DOMAIN}/requester`]: serviceNamespace.requester || email,
[`${DEMO_DOMAIN}/orderedBy`]: session.user,
+ [`${DEMO_DOMAIN}/white-glove`]: String(whiteGloved),
},
},
spec: {
@@ -726,40 +732,6 @@ export async function createWorkshopProvision({
return await createK8sObject(definition);
}
-export async function openWorkshopSupportTicket(
- workshop: Workshop,
- { number_of_attendees, sfdc, name, event_name, url, start_date, end_date, email }
-) {
- function date_to_time(date: Date) {
- const offset = date.getTimezoneOffset();
- date = new Date(date.getTime() - offset * 60 * 1000);
- const d = date.toISOString().split('T')[0];
- const hh = date.toISOString().split('T')[1].split(':')[0];
- const mm = date.toISOString().split('T')[1].split(':')[1];
- return `${d} ${hh}:${mm}`;
- }
- const resp = await apiFetch(apiPaths.WORKSHOP_SUPPORT({}), {
- body: JSON.stringify({
- number_of_attendees,
- sfdc,
- name,
- event_name,
- url,
- start_time: date_to_time(start_date),
- end_time: date_to_time(end_date),
- email,
- }),
- headers: {
- 'Content-Type': 'application/json',
- },
- method: 'POST',
- });
- const workshopSuport = await resp.json();
- const w = await getWorkshop(workshop.metadata.namespace, workshop.metadata.name);
- w.metadata.annotations[`${BABYLON_DOMAIN}/servicenow`] = JSON.stringify(workshopSuport);
- return await updateWorkshop(w);
-}
-
export async function getApiSession(forceRefresh = false) {
const sessionPromise = window.sessionPromiseInstance;
let session: Session;
diff --git a/catalog/ui/src/app/util.ts b/catalog/ui/src/app/util.ts
index db6bed47..d6972bee 100644
--- a/catalog/ui/src/app/util.ts
+++ b/catalog/ui/src/app/util.ts
@@ -333,6 +333,11 @@ export function getCostTracker(resourceClaim?: ResourceClaim): CostTracker {
return JSON.parse(resourceClaim.metadata?.annotations?.[`${BABYLON_DOMAIN}/cost-tracker`]);
}
+export function getWhiteGloved(d?: ResourceClaim | Workshop): boolean {
+ if (!d || !d.metadata?.annotations?.[`${DEMO_DOMAIN}/white-glove`]) return false;
+ return d.metadata?.annotations?.[`${DEMO_DOMAIN}/white-glove`] === 'true';
+}
+
export function compareStringDates(stringDate1: string, stringDate2: string): number {
const date1 = new Date(stringDate1).getTime();
const date2 = new Date(stringDate2).getTime();
@@ -404,28 +409,6 @@ export function namespaceToServiceNamespaceMapper(ns: Namespace): ServiceNamespa
};
}
-export function getServiceNow({
- sys_id,
- request_number,
- request_id,
- number,
-}: {
- sys_id: string;
- request_number: string;
- request_id: string;
- number?: string;
-}) {
- return {
- url:
- number || request_number
- ? `https://redhat.service-now.com/help?id=rh_ticket&table=sc_req_item&number=${
- number || request_number
- }&view=ess`
- : null,
- id: number || request_number,
- };
-}
-
export function generateRandom5CharsSuffix() {
const validChars = 'bcdfghjklmnpqrstvwxz2456789';
let result = '';