diff --git a/backend/src/routes/api/images/imageUtils.ts b/backend/src/routes/api/images/imageUtils.ts index 72736d48dd..ba29d04e9f 100644 --- a/backend/src/routes/api/images/imageUtils.ts +++ b/backend/src/routes/api/images/imageUtils.ts @@ -13,6 +13,13 @@ import { import { FastifyRequest } from 'fastify'; import createError from 'http-errors'; +const translateDisplayNameForK8s = (name: string): string => + name + .trim() + .toLowerCase() + .replace(/\s/g, '-') + .replace(/[^A-Za-z0-9-]/g, ''); + export const getImageList = async ( fastify: KubeFastifyInstance, labels: { [key: string]: string }, @@ -189,8 +196,14 @@ const packagesToString = (packages: BYONImagePackage[]): string => { const mapImageStreamToBYONImage = (is: ImageStream): BYONImage => ({ id: is.metadata.uid, name: is.metadata.name, - display_name: is.metadata.annotations['opendatahub.io/notebook-image-name'] || is.metadata.name, - description: is.metadata.annotations['opendatahub.io/notebook-image-desc'] || '', + display_name: + is.metadata.annotations['opendatahub.io/notebook-image-name'] || + is.metadata.annotations['openshift.io/display-name'] || + is.metadata.name, + description: + is.metadata.annotations['opendatahub.io/notebook-image-desc'] || + is.metadata.annotations['openshift.io/description'] || + '', visible: is.metadata.labels['opendatahub.io/notebook-image'] === 'true', error: getBYONImageErrorMessage(is), packages: JSON.parse( @@ -240,7 +253,7 @@ export const postImage = async ( 'opendatahub.io/notebook-image-url': fullUrl, 'opendatahub.io/notebook-image-creator': body.provider, }, - name: `byon-${Date.now()}`, + name: `custom-${translateDisplayNameForK8s(body.display_name)}`, namespace: namespace, labels: labels, }, diff --git a/backend/src/types.ts b/backend/src/types.ts index 254c0db880..856f93f171 100644 --- a/backend/src/types.ts +++ b/backend/src/types.ts @@ -466,6 +466,7 @@ export type ODHSegmentKey = { export type BYONImage = { id: string; + // FIXME: This shouldn't be a user defined value consumed from the request payload but should be a controlled value from an authentication middleware. provider: string; imported_time: string; error: string; diff --git a/frontend/src/pages/BYONImages/BYONImageDependenciesList.tsx b/frontend/src/pages/BYONImages/BYONImageDependenciesList.tsx new file mode 100644 index 0000000000..2607b7be96 --- /dev/null +++ b/frontend/src/pages/BYONImages/BYONImageDependenciesList.tsx @@ -0,0 +1,39 @@ +import * as React from 'react'; +import { + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + Text, + TextContent, +} from '@patternfly/react-core'; + +type BYONImageDependenciesListProps = { + dependencies: string[]; + term: string; +}; + +const BYONImageDependenciesList: React.FC = ({ + term, + dependencies, +}) => { + if (dependencies.length === 0) { + return null; + } + + return ( + + {term} + + + {dependencies.map((dep, i) => ( + + {dep} + + ))} + + + + ); +}; + +export default BYONImageDependenciesList; diff --git a/frontend/src/pages/BYONImages/DeleteBYONImageModal.tsx b/frontend/src/pages/BYONImages/BYONImageModal/DeleteBYONImageModal.tsx similarity index 100% rename from frontend/src/pages/BYONImages/DeleteBYONImageModal.tsx rename to frontend/src/pages/BYONImages/BYONImageModal/DeleteBYONImageModal.tsx diff --git a/frontend/src/pages/BYONImages/BYONImageModal/DisplayedContentTabContent.tsx b/frontend/src/pages/BYONImages/BYONImageModal/DisplayedContentTabContent.tsx new file mode 100644 index 0000000000..5fc9aa4fb7 --- /dev/null +++ b/frontend/src/pages/BYONImages/BYONImageModal/DisplayedContentTabContent.tsx @@ -0,0 +1,111 @@ +import * as React from 'react'; +import { + Button, + EmptyState, + EmptyStateBody, + EmptyStateIcon, + EmptyStatePrimary, + EmptyStateVariant, + TabContent, + Title, +} from '@patternfly/react-core'; +import { PlusCircleIcon } from '@patternfly/react-icons'; +import { BYONImagePackage } from '~/types'; +import { DisplayedContentTab } from './ManageBYONImageModal'; +import DisplayedContentTable from './DisplayedContentTable'; + +type DisplayedContentTabContentProps = { + activeKey: string | number; + tabKey: DisplayedContentTab; + resources: BYONImagePackage[]; + setResources: React.Dispatch>; + tempResources: BYONImagePackage[]; + setTempResources: React.Dispatch>; + editIndex?: number; + setEditIndex: (index?: number) => void; +}; + +const DisplayedContentTabContent: React.FC = ({ + activeKey, + tabKey, + resources, + setResources, + tempResources, + setTempResources, + editIndex, + setEditIndex, +}) => { + const resourceType = tabKey === DisplayedContentTab.SOFTWARE ? 'software' : 'packages'; + + const addEmptyRow = React.useCallback(() => { + setTempResources((prev) => [ + ...prev, + { + name: '', + version: '', + visible: true, + }, + ]); + setEditIndex(tempResources.length); + }, [tempResources.length, setTempResources, setEditIndex]); + + return ( + + ); +}; + +export default DisplayedContentTabContent; diff --git a/frontend/src/pages/BYONImages/BYONImageModal/DisplayedContentTable.tsx b/frontend/src/pages/BYONImages/BYONImageModal/DisplayedContentTable.tsx new file mode 100644 index 0000000000..39f1a3fceb --- /dev/null +++ b/frontend/src/pages/BYONImages/BYONImageModal/DisplayedContentTable.tsx @@ -0,0 +1,84 @@ +import * as React from 'react'; +import { Button, Panel, PanelFooter, PanelHeader, PanelMainBody } from '@patternfly/react-core'; +import { PlusCircleIcon } from '@patternfly/react-icons'; +import Table from '~/components/table/Table'; +import { BYONImagePackage } from '~/types'; +import { DisplayedContentTab } from './ManageBYONImageModal'; +import { getColumns } from './tableData'; +import DisplayedContentTableRow from './DisplayedContentTableRow'; + +type DisplayedContentTableProps = { + tabKey: DisplayedContentTab; + onReset: () => void; + onConfirm: (rowIndex: number, name: string, version: string) => void; + resources: BYONImagePackage[]; + onAdd: () => void; + editIndex?: number; + onEdit: (index: number) => void; + onDelete: (index: number) => void; +}; + +const DisplayedContentTable: React.FC = ({ + tabKey, + onReset, + onConfirm, + resources, + onAdd, + editIndex, + onEdit, + onDelete, +}) => { + const content = tabKey === DisplayedContentTab.SOFTWARE ? 'software' : 'packages'; + const columns = getColumns(tabKey); + + return ( + + + Add the {content} labels that will be displayed with this notebook image. Modifying the{' '} + {content} here does not effect the contents of the notebook image. + + + ( + onConfirm(rowIndex, name, version)} + onReset={onReset} + onEdit={() => onEdit(rowIndex)} + onDelete={() => onDelete(rowIndex)} + onMoveToNextRow={() => { + if (rowIndex === resources.length - 1) { + onAdd(); + } else { + onEdit(rowIndex + 1); + } + }} + /> + )} + /> + + + + + + ); +}; + +export default DisplayedContentTable; diff --git a/frontend/src/pages/BYONImages/BYONImageModal/DisplayedContentTableRow.tsx b/frontend/src/pages/BYONImages/BYONImageModal/DisplayedContentTableRow.tsx new file mode 100644 index 0000000000..683dc87bbd --- /dev/null +++ b/frontend/src/pages/BYONImages/BYONImageModal/DisplayedContentTableRow.tsx @@ -0,0 +1,128 @@ +import * as React from 'react'; +import { Tbody, Td, Tr } from '@patternfly/react-table'; +import { ActionList, ActionListItem, Button, TextInput } from '@patternfly/react-core'; +import { CheckIcon, MinusCircleIcon, PencilAltIcon, TimesIcon } from '@patternfly/react-icons'; +import { BYONImagePackage } from '~/types'; +import { DisplayedContentTab } from './ManageBYONImageModal'; + +type DisplayedContentTableRowProps = { + tabKey: DisplayedContentTab; + obj: BYONImagePackage; + isActive: boolean; + isEditing: boolean; + onConfirm: (name: string, version: string) => void; + onReset: () => void; + onEdit: () => void; + onDelete: () => void; + onMoveToNextRow: () => void; +}; + +const DisplayedContentTableRow: React.FC = ({ + tabKey, + obj, + isActive, + isEditing, + onConfirm, + onReset, + onEdit, + onDelete, + onMoveToNextRow, +}) => { + const [name, setName] = React.useState(obj.name); + const [version, setVersion] = React.useState(obj.version); + + const dataLabel = tabKey === DisplayedContentTab.SOFTWARE ? 'Software' : 'Packages'; + + const onKeyDown = React.useCallback( + (event: React.KeyboardEvent) => { + event.stopPropagation(); + if (event.key === 'Enter') { + onConfirm(name, version); + onMoveToNextRow(); + } + if (event.key === 'Escape') { + onReset(); + } + }, + [name, onConfirm, onMoveToNextRow, version, onReset], + ); + + return ( + + + + + + + + ); +}; + +export default DisplayedContentTableRow; diff --git a/frontend/src/pages/BYONImages/BYONImageModal/ImageLocationField.tsx b/frontend/src/pages/BYONImages/BYONImageModal/ImageLocationField.tsx new file mode 100644 index 0000000000..189f5ae99f --- /dev/null +++ b/frontend/src/pages/BYONImages/BYONImageModal/ImageLocationField.tsx @@ -0,0 +1,52 @@ +import * as React from 'react'; +import { FormGroup, FormHelperText, Popover, TextInput } from '@patternfly/react-core'; +import { HelpIcon } from '@patternfly/react-icons'; + +type ImageLocationFieldProps = { + location: string; + setLocation: (location: string) => void; +}; + +const ImageLocationField: React.FC = ({ location, setLocation }) => ( + + The address where the notebook image is located. See the help icon for examples. + + } + labelIcon={ + +

quay.io/my-repo/my-image:tag

+

quay.io/my-repo/my-image@sha256:xxxxxxxxxxxxx

+

docker.io/my-repo/my-image:tag

+ + } + > + +
+ } + > + { + setLocation(value); + }} + /> +
+); + +export default ImageLocationField; diff --git a/frontend/src/pages/BYONImages/BYONImageModal/ManageBYONImageModal.tsx b/frontend/src/pages/BYONImages/BYONImageModal/ManageBYONImageModal.tsx new file mode 100644 index 0000000000..ded49484c6 --- /dev/null +++ b/frontend/src/pages/BYONImages/BYONImageModal/ManageBYONImageModal.tsx @@ -0,0 +1,236 @@ +import React from 'react'; +import { + Button, + Form, + FormGroup, + TextInput, + Modal, + ModalVariant, + Tabs, + Tab, + TabTitleText, +} from '@patternfly/react-core'; +import { importBYONImage, updateBYONImage } from '~/services/imagesService'; +import { ResponseStatus, BYONImagePackage, BYONImage } from '~/types'; +import { addNotification } from '~/redux/actions/actions'; +import { useAppDispatch, useAppSelector } from '~/redux/hooks'; +import ImageLocationField from './ImageLocationField'; +import DisplayedContentTabContent from './DisplayedContentTabContent'; + +export type ManageBYONImageModalProps = { + existingImage?: BYONImage; + isOpen: boolean; + onClose: (submitted: boolean) => void; +}; + +export enum DisplayedContentTab { + SOFTWARE, + PACKAGES, +} + +export const ManageBYONImageModal: React.FC = ({ + existingImage, + isOpen, + onClose, +}) => { + const [activeTabKey, setActiveTabKey] = React.useState( + DisplayedContentTab.SOFTWARE, + ); + const [isProgress, setIsProgress] = React.useState(false); + const [repository, setRepository] = React.useState(''); + const [displayName, setDisplayName] = React.useState(''); + const [description, setDescription] = React.useState(''); + const [software, setSoftware] = React.useState([]); + const [packages, setPackages] = React.useState([]); + const [tempSoftware, setTempSoftware] = React.useState([]); + const [tempPackages, setTempPackages] = React.useState([]); + const [editIndex, setEditIndex] = React.useState(); + const userName = useAppSelector((state) => state.user || ''); + const dispatch = useAppDispatch(); + + const isEditing = editIndex !== undefined; + + React.useEffect(() => { + if (existingImage) { + setRepository(existingImage.url); + setDisplayName(existingImage.display_name); + setDescription(existingImage.description); + setPackages(existingImage.packages); + setSoftware(existingImage.software); + setTempPackages(existingImage.packages); + setTempSoftware(existingImage.software); + } + }, [existingImage]); + + const onBeforeClose = (submitted: boolean) => { + onClose(submitted); + setIsProgress(false); + setActiveTabKey(DisplayedContentTab.SOFTWARE); + setRepository(''); + setDisplayName(''); + setDescription(''); + setSoftware([]); + setPackages([]); + setTempSoftware([]); + setTempPackages([]); + }; + + const submit = () => { + if (existingImage) { + updateBYONImage({ + name: existingImage.name, + // eslint-disable-next-line camelcase + display_name: displayName, + description: description, + packages: packages, + software: software, + }).then((value) => { + if (value.success === false) { + dispatch( + addNotification({ + status: 'danger', + title: 'Error', + message: `Unable to update image ${displayName}`, + timestamp: new Date(), + }), + ); + } + onBeforeClose(true); + }); + } else { + importBYONImage({ + // eslint-disable-next-line camelcase + display_name: displayName, + url: repository, + description: description, + provider: userName, + software: software, + packages: packages, + }).then((value: ResponseStatus) => { + if (value.success === false) { + dispatch( + addNotification({ + status: 'danger', + title: `Unable to add notebook image ${displayName}`, + message: value.error, + timestamp: new Date(), + }), + ); + } + onBeforeClose(true); + }); + } + }; + + return ( + onBeforeClose(false)} + showClose={!isEditing} + actions={[ + , + , + ]} + > +
{ + e.preventDefault(); + submit(); + }} + > + {!existingImage && } + + { + setDisplayName(value); + }} + /> + + + { + setDescription(value); + }} + /> + + + { + setActiveTabKey(indexKey); + }} + > + Software} + aria-label="Displayed content software tab" + tabContentId={`tabContent-${DisplayedContentTab.SOFTWARE}`} + isDisabled={activeTabKey !== DisplayedContentTab.SOFTWARE && isEditing} + /> + Packages} + aria-label="Displayed content packages tab" + tabContentId={`tabContent-${DisplayedContentTab.PACKAGES}`} + isDisabled={activeTabKey !== DisplayedContentTab.PACKAGES && isEditing} + /> + + + + + +
+ ); +}; + +export default ManageBYONImageModal; diff --git a/frontend/src/pages/BYONImages/BYONImageModal/tableData.tsx b/frontend/src/pages/BYONImages/BYONImageModal/tableData.tsx new file mode 100644 index 0000000000..cc73a0010e --- /dev/null +++ b/frontend/src/pages/BYONImages/BYONImageModal/tableData.tsx @@ -0,0 +1,16 @@ +import { SortableData } from '~/components/table/useTableColumnSort'; +import { BYONImagePackage } from '~/types'; +import { DisplayedContentTab } from './ManageBYONImageModal'; + +export const getColumns = (tab: DisplayedContentTab): SortableData[] => [ + { + field: tab === DisplayedContentTab.SOFTWARE ? 'software' : 'packages', + label: tab === DisplayedContentTab.SOFTWARE ? 'Software' : 'Packages', + sortable: false, + }, + { + field: 'version', + label: 'Version', + sortable: false, + }, +]; diff --git a/frontend/src/pages/BYONImages/BYONImageStatusToggle.tsx b/frontend/src/pages/BYONImages/BYONImageStatusToggle.tsx index e7d84fdf9d..4af6f95d68 100644 --- a/frontend/src/pages/BYONImages/BYONImageStatusToggle.tsx +++ b/frontend/src/pages/BYONImages/BYONImageStatusToggle.tsx @@ -10,14 +10,14 @@ type BYONImageStatusToggleProps = { const BYONImageStatusToggle: React.FC = ({ image }) => { const [isLoading, setLoading] = React.useState(false); - const [isEnabled, setEnabled] = React.useState(image.visible); + const [isEnabled, setEnabled] = React.useState(!image.error && image.visible); const notification = useNotification(); + const handleChange = (checked: boolean) => { setLoading(true); updateBYONImage({ name: image.name, visible: checked, - packages: image.packages, }) .then(() => { setEnabled(checked); @@ -40,7 +40,7 @@ const BYONImageStatusToggle: React.FC = ({ image }) data-id={`enabled-disable-${image.id}`} isChecked={isEnabled} onChange={handleChange} - isDisabled={isLoading} + isDisabled={!!image.error || isLoading} /> ); }; diff --git a/frontend/src/pages/BYONImages/BYONImagesTable.tsx b/frontend/src/pages/BYONImages/BYONImagesTable.tsx index e288c222a7..48a0f6f3fc 100644 --- a/frontend/src/pages/BYONImages/BYONImagesTable.tsx +++ b/frontend/src/pages/BYONImages/BYONImagesTable.tsx @@ -4,8 +4,8 @@ import { BYONImage } from '~/types'; import Table from '~/components/table/Table'; import DashboardSearchField, { SearchType } from '~/concepts/dashboard/DashboardSearchField'; import DashboardEmptyTableView from '~/concepts/dashboard/DashboardEmptyTableView'; -import ManageBYONImageModal from './ManageBYONImageModal'; -import DeleteBYONImageModal from './DeleteBYONImageModal'; +import ManageBYONImageModal from './BYONImageModal/ManageBYONImageModal'; +import DeleteBYONImageModal from './BYONImageModal/DeleteBYONImageModal'; import { columns } from './tableData'; import BYONImagesTableRow from './BYONImagesTableRow'; import ImportBYONImageButton from './ImportBYONImageButton'; diff --git a/frontend/src/pages/BYONImages/BYONImagesTableRow.tsx b/frontend/src/pages/BYONImages/BYONImagesTableRow.tsx index af833ac01e..14993d1bfd 100644 --- a/frontend/src/pages/BYONImages/BYONImagesTableRow.tsx +++ b/frontend/src/pages/BYONImages/BYONImagesTableRow.tsx @@ -1,19 +1,13 @@ import * as React from 'react'; import { ActionsColumn, ExpandableRowContent, Tbody, Td, Tr } from '@patternfly/react-table'; -import { - DescriptionList, - DescriptionListDescription, - DescriptionListGroup, - DescriptionListTerm, - Flex, - FlexItem, - Timestamp, - TimestampTooltipVariant, -} from '@patternfly/react-core'; +import { DescriptionList, Flex, Timestamp, TimestampTooltipVariant } from '@patternfly/react-core'; import { BYONImage } from '~/types'; import { relativeTime } from '~/utilities/time'; +import ResourceNameTooltip from '~/components/ResourceNameTooltip'; import ImageErrorStatus from './ImageErrorStatus'; import BYONImageStatusToggle from './BYONImageStatusToggle'; +import { convertBYONImageToK8sResource } from './utils'; +import BYONImageDependenciesList from './BYONImageDependenciesList'; type BYONImagesTableRowProps = { obj: BYONImage; @@ -52,10 +46,10 @@ const BYONImagesTableRow: React.FC = ({ spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }} > - {obj.display_name} - - - + + {obj.display_name} + +
@@ -104,30 +98,15 @@ const BYONImagesTableRow: React.FC = ({ diff --git a/frontend/src/pages/BYONImages/EditStepTableRow.tsx b/frontend/src/pages/BYONImages/EditStepTableRow.tsx deleted file mode 100644 index cbd03ce3c0..0000000000 --- a/frontend/src/pages/BYONImages/EditStepTableRow.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import * as React from 'react'; -import { Tr, Td } from '@patternfly/react-table'; -import { Button, TextInput } from '@patternfly/react-core'; -import { PencilAltIcon, TimesIcon, CheckIcon, MinusCircleIcon } from '@patternfly/react-icons'; -import { BYONImagePackage } from '~/types'; - -interface EditStepTableRowProps { - imagePackage: BYONImagePackage; - setEditedValues: (values: BYONImagePackage) => void; - onDeleteHandler: () => void; -} - -export const EditStepTableRow: React.FunctionComponent = ({ - imagePackage, - setEditedValues, - onDeleteHandler, -}) => { - const [modifiedValue, setModifiedValue] = React.useState(imagePackage); - const [isEditMode, setIsEditMode] = React.useState(false); - - return ( - - - - - - ); -}; diff --git a/frontend/src/pages/BYONImages/ImportBYONImageButton.tsx b/frontend/src/pages/BYONImages/ImportBYONImageButton.tsx index cc70f41472..dd980ca247 100644 --- a/frontend/src/pages/BYONImages/ImportBYONImageButton.tsx +++ b/frontend/src/pages/BYONImages/ImportBYONImageButton.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { Button } from '@patternfly/react-core'; -import ManageBYONImageModal from './ManageBYONImageModal'; +import ManageBYONImageModal from './BYONImageModal/ManageBYONImageModal'; type ImportBYONImageButtonProps = { refresh: () => void; diff --git a/frontend/src/pages/BYONImages/ImportImageModal.scss b/frontend/src/pages/BYONImages/ImportImageModal.scss deleted file mode 100644 index f70d594109..0000000000 --- a/frontend/src/pages/BYONImages/ImportImageModal.scss +++ /dev/null @@ -1,3 +0,0 @@ -.empty-button { - margin-top: var(--pf-global--spacer--lg); -} \ No newline at end of file diff --git a/frontend/src/pages/BYONImages/ManageBYONImageModal.tsx b/frontend/src/pages/BYONImages/ManageBYONImageModal.tsx deleted file mode 100644 index 65350c1841..0000000000 --- a/frontend/src/pages/BYONImages/ManageBYONImageModal.tsx +++ /dev/null @@ -1,373 +0,0 @@ -import React from 'react'; -import { - Button, - EmptyState, - EmptyStateBody, - EmptyStateIcon, - EmptyStateVariant, - Form, - FormGroup, - TextInput, - Title, - Modal, - ModalVariant, - Tabs, - Tab, - TabTitleText, -} from '@patternfly/react-core'; -import { Caption, TableComposable, Tbody, Thead, Th, Tr } from '@patternfly/react-table'; -import { CubesIcon, PlusCircleIcon } from '@patternfly/react-icons'; -import { importBYONImage, updateBYONImage } from '~/services/imagesService'; -import { ResponseStatus, BYONImagePackage, BYONImage } from '~/types'; -import { addNotification } from '~/redux/actions/actions'; -import { useAppDispatch, useAppSelector } from '~/redux/hooks'; -import { EditStepTableRow } from './EditStepTableRow'; - -import './ImportImageModal.scss'; - -export type ManageBYONImageModalProps = { - existingImage?: BYONImage; - isOpen: boolean; - onClose: (submitted: boolean) => void; -}; - -export const ManageBYONImageModal: React.FC = ({ - existingImage, - isOpen, - onClose, -}) => { - const [isProgress, setIsProgress] = React.useState(false); - const [repository, setRepository] = React.useState(''); - const [displayName, setDisplayName] = React.useState(''); - const [description, setDescription] = React.useState(''); - const [software, setSoftware] = React.useState([]); - const [packages, setPackages] = React.useState([]); - const [activeTabKey, setActiveTabKey] = React.useState(0); - const userName = useAppSelector((state) => state.user || ''); - const dispatch = useAppDispatch(); - - const isDisabled = isProgress || displayName === '' || repository === ''; - - React.useEffect(() => { - if (existingImage) { - setRepository(existingImage.url); - setDisplayName(existingImage.display_name); - setDescription(existingImage.description); - setPackages(existingImage.packages); - setSoftware(existingImage.software); - } - }, [existingImage]); - - const onBeforeClose = (submitted: boolean) => { - onClose(submitted); - setIsProgress(false); - setRepository(''); - setDisplayName(''); - setDescription(''); - setSoftware([]); - setPackages([]); - }; - - const submit = () => { - if (existingImage) { - updateBYONImage({ - name: existingImage.name, - // eslint-disable-next-line camelcase - display_name: displayName, - description: description, - packages: packages, - software: software, - }).then((value) => { - if (value.success === false) { - dispatch( - addNotification({ - status: 'danger', - title: 'Error', - message: `Unable to update image ${name}`, - timestamp: new Date(), - }), - ); - } - onBeforeClose(true); - }); - } else { - importBYONImage({ - // eslint-disable-next-line camelcase - display_name: displayName, - url: repository, - description: description, - provider: userName, - software: software, - packages: packages, - }).then((value: ResponseStatus) => { - if (value.success === false) { - dispatch( - addNotification({ - status: 'danger', - title: `Unable to add notebook image ${name}`, - message: value.error, - timestamp: new Date(), - }), - ); - } - onBeforeClose(true); - }); - } - }; - - return ( - onBeforeClose(false)} - showClose - actions={[ - , - , - ]} - > -
{ - e.preventDefault(); - submit(); - }} - > - {!existingImage && ( - - { - setRepository(value); - }} - /> - - )} - - { - setDisplayName(value); - }} - /> - - - { - setDescription(value); - }} - /> - - - { - setActiveTabKey(indexKey as number); - }} - > - Software}> - {software.length > 0 ? ( - <> - -
- - - - - - - - {software.map((value, currentIndex) => ( - { - const updatedPackages = [...software]; - updatedPackages[currentIndex] = values; - setSoftware(updatedPackages); - }} - onDeleteHandler={() => { - setSoftware(software.filter((_value, index) => index !== currentIndex)); - }} - /> - ))} - - - - - ) : ( - - - - No software added - - - Add software to be advertised with your notebook image. Making changes here - won’t affect the contents of the image.{' '} - - - - )} - - Packages}> - {packages.length > 0 ? ( - <> - - - - - - - - - - {packages.map((value, currentIndex) => ( - { - const updatedPackages = [...packages]; - updatedPackages[currentIndex] = values; - setPackages(updatedPackages); - }} - onDeleteHandler={() => { - setPackages(packages.filter((_value, index) => index !== currentIndex)); - }} - /> - ))} - - - - - ) : ( - - - - No packages added - - - Add packages to be advertised with your notebook image. Making changes here - won’t affect the contents of the image.{' '} - - - - )} - - - - - - ); -}; - -export default ManageBYONImageModal; diff --git a/frontend/src/pages/BYONImages/tableData.tsx b/frontend/src/pages/BYONImages/tableData.tsx index 0168368a91..1719c64dc5 100644 --- a/frontend/src/pages/BYONImages/tableData.tsx +++ b/frontend/src/pages/BYONImages/tableData.tsx @@ -1,5 +1,6 @@ import { SortableData } from '~/components/table/useTableColumnSort'; import { BYONImage } from '~/types'; +import { getEnabledStatus } from './utils'; export const columns: SortableData[] = [ { @@ -20,7 +21,7 @@ export const columns: SortableData[] = [ { field: 'enable', label: 'Enable', - sortable: false, + sortable: (a, b) => getEnabledStatus(a) - getEnabledStatus(b), info: { tooltip: 'Enabled images are selectable when creating workbenches.', }, diff --git a/frontend/src/pages/BYONImages/utils.ts b/frontend/src/pages/BYONImages/utils.ts new file mode 100644 index 0000000000..28aa6a98d7 --- /dev/null +++ b/frontend/src/pages/BYONImages/utils.ts @@ -0,0 +1,26 @@ +import { K8sResourceCommon } from '@openshift/dynamic-plugin-sdk-utils'; +import { BYONImage } from '~/types'; + +export const convertBYONImageToK8sResource = (image: BYONImage): K8sResourceCommon => ({ + kind: 'ImageStream', + apiVersion: 'image.openshift.io/v1', + metadata: { + name: image.name, + annotations: { + 'openshift.io/display-name': image.display_name, + }, + }, +}); + +enum ImageEnabledStatus { + ENABLED = 1, + DISABLED = 0, + ERROR = -1, +} + +export const getEnabledStatus = (image: BYONImage): number => + image.visible && !image.error + ? ImageEnabledStatus.ENABLED + : image.error + ? ImageEnabledStatus.ERROR + : ImageEnabledStatus.DISABLED; diff --git a/frontend/src/types.ts b/frontend/src/types.ts index e3ea8c927f..16e4b1ae92 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -438,6 +438,7 @@ export type Route = { export type BYONImage = { id: string; + // FIXME: This shouldn't be a user defined value consumed from the request payload but should be a controlled value from an authentication middleware. provider: string; imported_time: string; error: string;
+ {isActive ? ( + + ) : ( + <>{obj.name} + )} + + {isActive ? ( + + ) : ( + <>{obj.version} + )} + + + {isActive ? ( + <> + + + + + + + + ) : ( + <> + + + + + + + + )} + +
{obj.description} - {obj.software.length > 0 && ( - - Displayed software - - {obj.software.map((s, i) => ( -

{`${s.name} ${s.version}`}

- ))} -
-
- )} - {obj.packages.length > 0 && ( - - Displayed packages - - {obj.packages.map((p, i) => ( -

{`${p.name} ${p.version}`}

- ))} -
-
- )} - - Image location - {obj.url} - + `${s.name} ${s.version}`)} + /> + `${p.name} ${p.version}`)} + /> +
- {isEditMode ? ( - { - setModifiedValue({ - name: value, - version: modifiedValue.version, - visible: modifiedValue.visible, - }); - }} - /> - ) : ( - imagePackage.name - )} - - {isEditMode ? ( - { - setModifiedValue({ - name: modifiedValue.name, - version: value, - visible: modifiedValue.visible, - }); - }} - /> - ) : ( - imagePackage.version - )} - - {!isEditMode ? ( -
- Add the advertised software shown with this notebook image. Modifying the - software here does not effect the contents of the notebook image. -
SoftwareVersion -
- Add the advertised packages shown with this notebook image. Modifying the - packages here does not effect the contents of the notebook image. -
PackageVersion -