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}
);