diff --git a/catalog/ui/package-lock.json b/catalog/ui/package-lock.json index c70f7b2e..82fb2cff 100644 --- a/catalog/ui/package-lock.json +++ b/catalog/ui/package-lock.json @@ -42,7 +42,7 @@ "@babel/preset-typescript": "^7.26.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", - "@testing-library/user-event": "^14.6.0", + "@testing-library/user-event": "^14.6.1", "@types/dompurify": "^3.2.0", "@types/jest": "^29.5.14", "@types/js-yaml": "^4.0.9", @@ -3818,10 +3818,11 @@ } }, "node_modules/@testing-library/user-event": { - "version": "14.6.0", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.0.tgz", - "integrity": "sha512-+jsfK7kVJbqnCYtLTln8Ja/NmVrZRwBJHmHR9IxIVccMWSOZ6Oy0FkDJNeyVu4QSpMNmRfy10Xb76ObRDlWWBQ==", + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", "dev": true, + "license": "MIT", "engines": { "node": ">=12", "npm": ">=6" diff --git a/catalog/ui/package.json b/catalog/ui/package.json index ebd469bc..5b2ad060 100644 --- a/catalog/ui/package.json +++ b/catalog/ui/package.json @@ -28,7 +28,7 @@ "@babel/preset-typescript": "^7.26.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", - "@testing-library/user-event": "^14.6.0", + "@testing-library/user-event": "^14.6.1", "@types/dompurify": "^3.2.0", "@types/jest": "^29.5.14", "@types/js-yaml": "^4.0.9", diff --git a/catalog/ui/src/app/Admin/Workshops.tsx b/catalog/ui/src/app/Admin/Workshops.tsx index fd848395..d57ea045 100644 --- a/catalog/ui/src/app/Admin/Workshops.tsx +++ b/catalog/ui/src/app/Admin/Workshops.tsx @@ -59,7 +59,7 @@ const Workshops: React.FC<{}> = () => { .split(/ +/) .filter((w) => w != '') : null, - [searchParams.get('search')], + [searchParams.get('search')] ); const [modalState, setModalState] = useState<{ action?: string; workshop?: Workshop }>({}); const [selectedUids, setSelectedUids] = useState([]); @@ -69,7 +69,7 @@ const Workshops: React.FC<{}> = () => { setModalState({ action, workshop }); openModalAction(); }, - [openModalAction], + [openModalAction] ); const { @@ -100,7 +100,7 @@ const Workshops: React.FC<{}> = () => { } return true; }, - }, + } ); const isReachingEnd = workshopsPages && !workshopsPages[workshopsPages.length - 1].metadata.continue; const isLoadingInitialData = !workshopsPages; @@ -126,7 +126,7 @@ const Workshops: React.FC<{}> = () => { } } }, - [mutate, workshopsPages], + [mutate, workshopsPages] ); const filterWorkshop = useCallback( (workshop: Workshop): boolean => { @@ -143,12 +143,12 @@ const Workshops: React.FC<{}> = () => { } return true; }, - [keywordFilter], + [keywordFilter] ); const workshops: Workshop[] = useMemo( () => [].concat(...workshopsPages.map((page) => page.items)).filter(filterWorkshop) || [], - [filterWorkshop, workshopsPages], + [filterWorkshop, workshopsPages] ); // Trigger continue fetching more resource claims on scroll. @@ -173,7 +173,7 @@ const Workshops: React.FC<{}> = () => { apiPaths.WORKSHOP({ namespace: workshop.metadata.namespace, workshopName: workshop.metadata.name, - }), + }) ); deletedWorkshops.push(workshop); } @@ -319,7 +319,7 @@ const Workshops: React.FC<{}> = () => { icon={TrashIcon} /> - , + ); return { cells: cells, diff --git a/catalog/ui/src/app/Services/renderWorkshopRow.tsx b/catalog/ui/src/app/Services/renderWorkshopRow.tsx index 6be41860..2f9cbb81 100644 --- a/catalog/ui/src/app/Services/renderWorkshopRow.tsx +++ b/catalog/ui/src/app/Services/renderWorkshopRow.tsx @@ -12,6 +12,7 @@ import { checkWorkshopCanStop, getWorkshopAutoStopTime, getWorkshopLifespan, + isWorkshopLocked, } from '@app/Workshops/workshops-utils'; import CheckCircleIcon from '@patternfly/react-icons/dist/js/icons/check-circle-icon'; import Label from '@app/components/Label'; @@ -37,6 +38,7 @@ const renderWorkshopRow = ({ }) => void; isAdmin: boolean; }) => { + const isLocked = isWorkshopLocked(workshop, isAdmin); const actionHandlers = { delete: () => showModal({ modal: 'action', action: 'delete', workshop }), lifespan: () => showModal({ action: 'retirement', modal: 'scheduleAction', workshop }), @@ -109,7 +111,7 @@ const renderWorkshopRow = ({ @@ -145,13 +147,19 @@ const renderWorkshopRow = ({ key="actions__start" /> - + ); diff --git a/catalog/ui/src/app/Workshops/WorkshopActions.tsx b/catalog/ui/src/app/Workshops/WorkshopActions.tsx index b571fc6b..e4ac854a 100644 --- a/catalog/ui/src/app/Workshops/WorkshopActions.tsx +++ b/catalog/ui/src/app/Workshops/WorkshopActions.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { DropdownPosition } from '@patternfly/react-core/deprecated'; import { ActionDropdown, ActionDropdownItem } from '@app/components/ActionDropdown'; +import { LockedIcon } from '@patternfly/react-icons'; const WorkshopActions: React.FC<{ actionHandlers: { @@ -13,13 +14,15 @@ const WorkshopActions: React.FC<{ isDisabled?: boolean; position?: DropdownPosition | 'right' | 'left'; workshopName?: string; -}> = ({ actionHandlers, className, isDisabled, position, workshopName }) => { + isLocked?: boolean; +}> = ({ actionHandlers, className, isDisabled, position, workshopName, isLocked = false }) => { const actionDropdownItems = [ : null} />, ]; actionHandlers.deleteService && @@ -29,7 +32,7 @@ const WorkshopActions: React.FC<{ isDisabled={!actionHandlers.deleteService} label="Delete Selected Services" onSelect={actionHandlers.deleteService} - />, + /> ); actionHandlers.start && actionDropdownItems.push( @@ -38,16 +41,17 @@ const WorkshopActions: React.FC<{ isDisabled={!actionHandlers.start} label="Start Workshop instances" onSelect={actionHandlers.start} - />, + /> ); actionHandlers.stop && actionDropdownItems.push( , + icon={isLocked ? : null} + /> ); return ( diff --git a/catalog/ui/src/app/Workshops/WorkshopsItem.tsx b/catalog/ui/src/app/Workshops/WorkshopsItem.tsx index 1e2c0fcb..9009633c 100644 --- a/catalog/ui/src/app/Workshops/WorkshopsItem.tsx +++ b/catalog/ui/src/app/Workshops/WorkshopsItem.tsx @@ -57,7 +57,7 @@ import WorkshopsItemProvisioning from './WorkshopsItemProvisioning'; import WorkshopsItemServices from './WorkshopsItemServices'; import WorkshopsItemUserAssignments from './WorkshopsItemUserAssignments'; import WorkshopScheduleAction from './WorkshopScheduleAction'; -import { checkWorkshopCanStart, checkWorkshopCanStop, isWorkshopStarted } from './workshops-utils'; +import { checkWorkshopCanStart, checkWorkshopCanStop, isWorkshopLocked, isWorkshopStarted } from './workshops-utils'; import Label from '@app/components/Label'; import ProjectSelector from '@app/components/ProjectSelector'; import ErrorBoundaryPage from '@app/components/ErrorBoundaryPage'; @@ -415,6 +415,7 @@ const WorkshopsItemComponent: React.FC<{ ? () => showModal({ action: 'stopServices', resourceClaims }) : null, }} + isLocked={isWorkshopLocked(workshop, isAdmin)} /> diff --git a/catalog/ui/src/app/Workshops/WorkshopsItemDetails.tsx b/catalog/ui/src/app/Workshops/WorkshopsItemDetails.tsx index b615eb34..905aef8f 100644 --- a/catalog/ui/src/app/Workshops/WorkshopsItemDetails.tsx +++ b/catalog/ui/src/app/Workshops/WorkshopsItemDetails.tsx @@ -32,7 +32,12 @@ import LoadingIcon from '@app/components/LoadingIcon'; import OpenshiftConsoleLink from '@app/components/OpenshiftConsoleLink'; import Editor from '@app/components/Editor/Editor'; import AutoStopDestroy from '@app/components/AutoStopDestroy'; -import { checkWorkshopCanStop, getWorkshopAutoStopTime, getWorkshopLifespan } from './workshops-utils'; +import { + checkWorkshopCanStop, + getWorkshopAutoStopTime, + getWorkshopLifespan, + isWorkshopLocked, +} from './workshops-utils'; import { ModalState } from './WorkshopsItem'; import WorkshopStatus from './WorkshopStatus'; import { useSWRConfig } from 'swr'; @@ -77,6 +82,7 @@ const WorkshopsItemDetails: React.FC<{ const debouncedApiFetch = useDebounce(apiFetch, 1000); const { cache } = useSWRConfig(); const whiteGloved = getWhiteGloved(workshop); + const isLocked = isWorkshopLocked(workshop, isAdmin); 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`]; @@ -200,6 +206,23 @@ const WorkshopsItemDetails: React.FC<{ } } + async function handleLockedChange(_: any, isChecked: boolean) { + const patchObj = { + metadata: { + labels: { + [`${DEMO_DOMAIN}/lock-enabled`]: String(isChecked), + }, + }, + }; + onWorkshopUpdate( + await patchWorkshop({ + name: workshop.metadata.name, + namespace: workshop.metadata.namespace, + patch: patchObj, + }) + ); + } + return ( @@ -367,8 +390,8 @@ const WorkshopsItemDetails: React.FC<{ (showModal ? showModal({ action: 'scheduleStop', resourceClaims }) : null)} - isDisabled={!showModal} + onClick={() => (showModal && !isLocked ? showModal({ action: 'scheduleStop', resourceClaims }) : null)} + isDisabled={isLocked || !showModal} time={autoStopTime} variant="extended" className="workshops-item__schedule-btn" @@ -385,12 +408,12 @@ const WorkshopsItemDetails: React.FC<{ { - if (showModal) { + if (showModal && !isLocked) { showModal({ resourceClaims, action: 'scheduleDelete' }); } }} time={autoDestroyTime} - isDisabled={!showModal} + isDisabled={isLocked || !showModal} variant="extended" className="workshops-item__schedule-btn" notDefinedMessage="- Not defined -" @@ -524,6 +547,22 @@ const WorkshopsItemDetails: React.FC<{ ) : null} + + {isAdmin ? ( + + + + + + + ) : null} ); }; diff --git a/catalog/ui/src/app/Workshops/WorkshopsItemUserAssignments.tsx b/catalog/ui/src/app/Workshops/WorkshopsItemUserAssignments.tsx index 071502bc..395cf55d 100644 --- a/catalog/ui/src/app/Workshops/WorkshopsItemUserAssignments.tsx +++ b/catalog/ui/src/app/Workshops/WorkshopsItemUserAssignments.tsx @@ -16,7 +16,7 @@ import { EmptyStateHeader, } from '@patternfly/react-core'; import { Table /* data-codemods */, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table'; -import { apiPaths, assignWorkshopUser, bulkAssignWorkshopUsers } from '@app/api'; +import { assignWorkshopUser, bulkAssignWorkshopUsers } from '@app/api'; import { WorkshopUserAssignment } from '@app/types'; import { renderContent } from '@app/util'; import BulkUserAssignmentModal from '@app/components/BulkUserAssignmentModal'; diff --git a/catalog/ui/src/app/Workshops/workshops-utils.tsx b/catalog/ui/src/app/Workshops/workshops-utils.tsx index aedbfdd4..9f24ce48 100644 --- a/catalog/ui/src/app/Workshops/workshops-utils.tsx +++ b/catalog/ui/src/app/Workshops/workshops-utils.tsx @@ -1,5 +1,5 @@ import { ResourceClaim, Workshop, WorkshopProvision } from '@app/types'; -import { canExecuteAction, checkResourceClaimCanStart, checkResourceClaimCanStop } from '@app/util'; +import { canExecuteAction, checkResourceClaimCanStart, checkResourceClaimCanStop, DEMO_DOMAIN } from '@app/util'; import { getAutoStopTime, getMinDefaultRuntime, getStartTime } from '@app/Services/service-utils'; import parseDuration from 'parse-duration'; @@ -75,3 +75,13 @@ export function checkWorkshopCanStart(resourceClaims: ResourceClaim[] = []) { return resourceClaimsCanStart && resourceClaimsCanStart.length > 0; } + +export function isWorkshopLocked(workshop: Workshop, isAdmin: boolean) { + if (isAdmin) { + return false; + } + if (workshop.metadata?.labels?.[`${DEMO_DOMAIN}/lock-enabled`]) { + return workshop.metadata?.labels?.[`${DEMO_DOMAIN}/lock-enabled`] === 'true'; + } + return false; +} diff --git a/catalog/ui/src/app/components/ActionDropdown.tsx b/catalog/ui/src/app/components/ActionDropdown.tsx index 586a07f0..25f95f9d 100644 --- a/catalog/ui/src/app/components/ActionDropdown.tsx +++ b/catalog/ui/src/app/components/ActionDropdown.tsx @@ -41,9 +41,16 @@ const ActionDropdownItem: React.FC<{ label: string; onSelect: () => void; className?: string; -}> = ({ label, className, isDisabled = false, onSelect }) => { + icon?: React.ReactNode; +}> = ({ label, className, isDisabled = false, onSelect, icon }) => { return ( - onSelect()}> + (isDisabled === true ? null : onSelect())} + icon={icon} + > {label} );