Merge branch 'notebooks-v2' into workspaceKindSummary

Signed-off-by: Dominik Kawka  <31955648+dominikkawka@users.noreply.github.com>
This commit is contained in:
Dominik Kawka 2025-07-24 15:30:25 +01:00 committed by GitHub
commit aa3f270e54
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
84 changed files with 1572 additions and 1035 deletions

View File

@ -208,6 +208,28 @@ module.exports = {
name: 'react-router', name: 'react-router',
message: 'Use react-router-dom instead.', message: 'Use react-router-dom instead.',
}, },
{
name: '@patternfly/react-core',
message:
'Use specific component imports: @patternfly/react-core/dist/esm/components/ComponentName',
},
{
name: '@patternfly/react-table',
message:
'Use specific component imports: @patternfly/react-table/dist/esm/components/ComponentName',
},
{
name: '@patternfly/react-icons',
message: 'Use specific icon imports: @patternfly/react-icons/dist/esm/icons/IconName',
},
{
name: 'date-fns',
message: 'Use specific function imports: date-fns/functionName',
},
{
name: 'lodash',
message: 'Use specific function imports: lodash/functionName',
},
], ],
}, },
], ],

View File

@ -10372,13 +10372,15 @@
} }
}, },
"node_modules/es-set-tostringtag": { "node_modules/es-set-tostringtag": {
"version": "2.0.3", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": { "dependencies": {
"get-intrinsic": "^1.2.4", "es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2", "has-tostringtag": "^1.0.2",
"hasown": "^2.0.1" "hasown": "^2.0.2"
}, },
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -12229,13 +12231,16 @@
} }
}, },
"node_modules/form-data": { "node_modules/form-data": {
"version": "4.0.0", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
"combined-stream": "^1.0.8", "combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12" "mime-types": "^2.1.12"
}, },
"engines": { "engines": {

View File

@ -4,7 +4,7 @@ class Home {
} }
findButton() { findButton() {
return cy.get('button:contains("Create Workspace")'); return cy.get('button:contains("Create workspace")');
} }
} }

View File

@ -2,20 +2,19 @@ import React, { useEffect } from 'react';
import '@patternfly/patternfly/patternfly-addons.css'; import '@patternfly/patternfly/patternfly-addons.css';
import '@patternfly/react-core/dist/styles/base.css'; import '@patternfly/react-core/dist/styles/base.css';
import './app.css'; import './app.css';
import { Brand } from '@patternfly/react-core/dist/esm/components/Brand';
import { Flex } from '@patternfly/react-core/dist/esm/layouts/Flex';
import { import {
Brand,
Flex,
Masthead, Masthead,
MastheadBrand, MastheadBrand,
MastheadContent, MastheadContent,
MastheadLogo, MastheadLogo,
MastheadMain, MastheadMain,
MastheadToggle, MastheadToggle,
Page, } from '@patternfly/react-core/dist/esm/components/Masthead';
PageToggleButton, import { Page, PageToggleButton } from '@patternfly/react-core/dist/esm/components/Page';
Title, import { Title } from '@patternfly/react-core/dist/esm/components/Title';
} from '@patternfly/react-core'; import { BarsIcon } from '@patternfly/react-icons/dist/esm/icons/bars-icon';
import { BarsIcon } from '@patternfly/react-icons';
import ErrorBoundary from '~/app/error/ErrorBoundary'; import ErrorBoundary from '~/app/error/ErrorBoundary';
import NamespaceSelector from '~/shared/components/NamespaceSelector'; import NamespaceSelector from '~/shared/components/NamespaceSelector';
import logoDarkTheme from '~/images/logo-dark-theme.svg'; import logoDarkTheme from '~/images/logo-dark-theme.svg';

View File

@ -43,7 +43,7 @@ export const useAdminDebugSettings = (): NavDataItem[] => {
children: [{ label: 'Notebooks', path: '/notebookDebugSettings' }], children: [{ label: 'Notebooks', path: '/notebookDebugSettings' }],
}, },
{ {
label: 'Workspace Kinds', label: 'Workspace kinds',
path: AppRoutePaths.workspaceKinds, path: AppRoutePaths.workspaceKinds,
}, },
]; ];
@ -51,7 +51,7 @@ export const useAdminDebugSettings = (): NavDataItem[] => {
export const useNavData = (): NavDataItem[] => [ export const useNavData = (): NavDataItem[] => [
{ {
label: 'Notebooks', label: 'Workspaces',
path: AppRoutePaths.workspaces, path: AppRoutePaths.workspaces,
}, },
...useAdminDebugSettings(), ...useAdminDebugSettings(),

View File

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { Bullseye, Spinner } from '@patternfly/react-core'; import { Bullseye } from '@patternfly/react-core/dist/esm/layouts/Bullseye';
import { Spinner } from '@patternfly/react-core/dist/esm/components/Spinner';
import { useNotebookAPI } from './hooks/useNotebookAPI'; import { useNotebookAPI } from './hooks/useNotebookAPI';
interface EnsureAPIAvailabilityProps { interface EnsureAPIAvailabilityProps {

View File

@ -1,14 +1,13 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import { Brand } from '@patternfly/react-core/dist/esm/components/Brand';
import { import {
Brand,
Nav, Nav,
NavExpandable, NavExpandable,
NavItem, NavItem,
NavList, NavList,
PageSidebar, } from '@patternfly/react-core/dist/esm/components/Nav';
PageSidebarBody, import { PageSidebar, PageSidebarBody } from '@patternfly/react-core/dist/esm/components/Page';
} from '@patternfly/react-core';
import { useTypedLocation } from '~/app/routerHelper'; import { useTypedLocation } from '~/app/routerHelper';
import { useNavData, isNavDataGroup, NavDataHref, NavDataGroup } from './AppRoutes'; import { useNavData, isNavDataGroup, NavDataHref, NavDataGroup } from './AppRoutes';
import { isMUITheme, LOGO_LIGHT } from './const'; import { isMUITheme, LOGO_LIGHT } from './const';

View File

@ -1,5 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { Alert, Bullseye } from '@patternfly/react-core'; import { Alert } from '@patternfly/react-core/dist/esm/components/Alert';
import { Bullseye } from '@patternfly/react-core/dist/esm/layouts/Bullseye';
interface LoadErrorProps { interface LoadErrorProps {
error: Error; error: Error;

View File

@ -1,5 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { Bullseye, Spinner } from '@patternfly/react-core'; import { Bullseye } from '@patternfly/react-core/dist/esm/layouts/Bullseye';
import { Spinner } from '@patternfly/react-core/dist/esm/components/Spinner';
// TODO: simple LoadingSpinner component -- we should improve this later // TODO: simple LoadingSpinner component -- we should improve this later

View File

@ -1,5 +1,9 @@
import React from 'react'; import React from 'react';
import { SearchInput, SearchInputProps, TextInput } from '@patternfly/react-core'; import {
SearchInput,
SearchInputProps,
} from '@patternfly/react-core/dist/esm/components/SearchInput';
import { TextInput } from '@patternfly/react-core/dist/esm/components/TextInput';
import FormFieldset from 'app/components/FormFieldset'; import FormFieldset from 'app/components/FormFieldset';
import { isMUITheme } from 'app/const'; import { isMUITheme } from 'app/const';

View File

@ -1,10 +1,12 @@
import React from 'react'; import React from 'react';
import { Alert, List, ListItem } from '@patternfly/react-core'; import { Alert } from '@patternfly/react-core/dist/esm/components/Alert';
import { List, ListItem } from '@patternfly/react-core/dist/esm/components/List';
import { ValidationError } from '~/shared/api/backendApiTypes'; import { ValidationError } from '~/shared/api/backendApiTypes';
import { ErrorEnvelopeException } from '~/shared/api/apiUtils';
interface ValidationErrorAlertProps { interface ValidationErrorAlertProps {
title: string; title: string;
errors: ValidationError[]; errors: (ValidationError | ErrorEnvelopeException)[];
} }
export const ValidationErrorAlert: React.FC<ValidationErrorAlertProps> = ({ title, errors }) => { export const ValidationErrorAlert: React.FC<ValidationErrorAlertProps> = ({ title, errors }) => {

View File

@ -1,17 +1,19 @@
import React, { useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react'; import React, { useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react';
import { PageSection } from '@patternfly/react-core/dist/esm/components/Page';
import { import {
PageSection,
TimestampTooltipVariant, TimestampTooltipVariant,
Timestamp, Timestamp,
Label, } from '@patternfly/react-core/dist/esm/components/Timestamp';
import { Label } from '@patternfly/react-core/dist/esm/components/Label';
import {
PaginationVariant, PaginationVariant,
Pagination, Pagination,
Content, } from '@patternfly/react-core/dist/esm/components/Pagination';
Tooltip, import { Content } from '@patternfly/react-core/dist/esm/components/Content';
Bullseye, import { Tooltip } from '@patternfly/react-core/dist/esm/components/Tooltip';
Button, import { Bullseye } from '@patternfly/react-core/dist/esm/layouts/Bullseye';
Icon, import { Button } from '@patternfly/react-core/dist/esm/components/Button';
} from '@patternfly/react-core'; import { Icon } from '@patternfly/react-core/dist/esm/components/Icon';
import { import {
Table, Table,
Thead, Thead,
@ -22,14 +24,12 @@ import {
ThProps, ThProps,
ActionsColumn, ActionsColumn,
IActions, IActions,
} from '@patternfly/react-table'; } from '@patternfly/react-table/dist/esm/components/Table';
import { import { InfoCircleIcon } from '@patternfly/react-icons/dist/esm/icons/info-circle-icon';
InfoCircleIcon, import { ExclamationTriangleIcon } from '@patternfly/react-icons/dist/esm/icons/exclamation-triangle-icon';
ExclamationTriangleIcon, import { TimesCircleIcon } from '@patternfly/react-icons/dist/esm/icons/times-circle-icon';
TimesCircleIcon, import { QuestionCircleIcon } from '@patternfly/react-icons/dist/esm/icons/question-circle-icon';
QuestionCircleIcon, import { formatDistanceToNow } from 'date-fns/formatDistanceToNow';
} from '@patternfly/react-icons';
import { formatDistanceToNow } from 'date-fns';
import { Workspace, WorkspaceState } from '~/shared/api/backendApiTypes'; import { Workspace, WorkspaceState } from '~/shared/api/backendApiTypes';
import { import {
DataFieldKey, DataFieldKey,
@ -394,7 +394,7 @@ const WorkspaceTable = React.forwardRef<WorkspaceTableRef, WorkspaceTableProps>(
toolbarActions={ toolbarActions={
canCreateWorkspaces && ( canCreateWorkspaces && (
<Button variant="primary" ouiaId="Primary" onClick={createWorkspace}> <Button variant="primary" ouiaId="Primary" onClick={createWorkspace}>
Create Workspace Create workspace
</Button> </Button>
) )
} }

View File

@ -1,5 +1,9 @@
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { Drawer, DrawerContent, DrawerContentBody } from '@patternfly/react-core'; import {
Drawer,
DrawerContent,
DrawerContentBody,
} from '@patternfly/react-core/dist/esm/components/Drawer';
import { useNamespaceContext } from '~/app/context/NamespaceContextProvider'; import { useNamespaceContext } from '~/app/context/NamespaceContextProvider';
import { useNotebookAPI } from '~/app/hooks/useNotebookAPI'; import { useNotebookAPI } from '~/app/hooks/useNotebookAPI';
import { WorkspaceDetails } from '~/app/pages/Workspaces/Details/WorkspaceDetails'; import { WorkspaceDetails } from '~/app/pages/Workspaces/Details/WorkspaceDetails';
@ -209,7 +213,7 @@ export const WorkspaceActionsContextProvider: React.FC<WorkspaceActionsContextPr
isOpen isOpen
resourceName={activeWsAction.workspace.name} resourceName={activeWsAction.workspace.name}
namespace={activeWsAction.workspace.namespace} namespace={activeWsAction.workspace.namespace}
title="Delete Workspace?" title="Delete workspace?"
onClose={() => setActiveWsAction(null)} onClose={() => setActiveWsAction(null)}
onDelete={async () => executeDeleteAction()} onDelete={async () => executeDeleteAction()}
/> />

View File

@ -1,7 +1,9 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Button, Split, SplitItem, Title } from '@patternfly/react-core'; import { Button } from '@patternfly/react-core/dist/esm/components/Button';
import { TimesIcon } from '@patternfly/react-icons'; import { Split, SplitItem } from '@patternfly/react-core/dist/esm/layouts/Split';
import { Title } from '@patternfly/react-core/dist/esm/components/Title';
import { TimesIcon } from '@patternfly/react-icons/dist/esm/icons/times-icon';
import { AppRoutePaths } from '~/app/routes'; import { AppRoutePaths } from '~/app/routes';
import ErrorDetails from '~/app/error/ErrorDetails'; import ErrorDetails from '~/app/error/ErrorDetails';
import UpdateState from '~/app/error/UpdateState'; import UpdateState from '~/app/error/UpdateState';

View File

@ -2,12 +2,14 @@ import React from 'react';
import { import {
ClipboardCopy, ClipboardCopy,
ClipboardCopyVariant, ClipboardCopyVariant,
} from '@patternfly/react-core/dist/esm/components/ClipboardCopy';
import {
DescriptionList, DescriptionList,
DescriptionListDescription, DescriptionListDescription,
DescriptionListGroup, DescriptionListGroup,
DescriptionListTerm, DescriptionListTerm,
Title, } from '@patternfly/react-core/dist/esm/components/DescriptionList';
} from '@patternfly/react-core'; import { Title } from '@patternfly/react-core/dist/esm/components/Title';
type ErrorDetailsProps = { type ErrorDetailsProps = {
title: string; title: string;

View File

@ -1,14 +1,14 @@
import React from 'react'; import React from 'react';
import { Button } from '@patternfly/react-core/dist/esm/components/Button';
import { import {
Button,
EmptyState, EmptyState,
EmptyStateActions,
EmptyStateBody, EmptyStateBody,
EmptyStateFooter,
EmptyStateVariant, EmptyStateVariant,
PageSection, EmptyStateActions,
} from '@patternfly/react-core'; EmptyStateFooter,
import { PathMissingIcon } from '@patternfly/react-icons'; } from '@patternfly/react-core/dist/esm/components/EmptyState';
import { PageSection } from '@patternfly/react-core/dist/esm/components/Page';
import { PathMissingIcon } from '@patternfly/react-icons/dist/esm/icons/path-missing-icon';
type Props = { type Props = {
onClose: () => void; onClose: () => void;

View File

@ -1,5 +1,5 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { IActions } from '@patternfly/react-table'; import { IActions } from '@patternfly/react-table/dist/esm/components/Table';
import { Workspace } from '~/shared/api/backendApiTypes'; import { Workspace } from '~/shared/api/backendApiTypes';
import { useWorkspaceActionsContext, WorkspaceAction } from '~/app/context/WorkspaceActionsContext'; import { useWorkspaceActionsContext, WorkspaceAction } from '~/app/context/WorkspaceActionsContext';

View File

@ -1,13 +1,13 @@
import React from 'react'; import React from 'react';
import { CubesIcon } from '@patternfly/react-icons'; import { CubesIcon } from '@patternfly/react-icons/dist/esm/icons/cubes-icon';
import { Button } from '@patternfly/react-core/dist/esm/components/Button';
import { import {
Button,
EmptyState, EmptyState,
EmptyStateBody, EmptyStateBody,
EmptyStateFooter,
EmptyStateVariant, EmptyStateVariant,
PageSection, EmptyStateFooter,
} from '@patternfly/react-core'; } from '@patternfly/react-core/dist/esm/components/EmptyState';
import { PageSection } from '@patternfly/react-core/dist/esm/components/Page';
const Debug: React.FunctionComponent = () => ( const Debug: React.FunctionComponent = () => (
<PageSection> <PageSection>

View File

@ -1,15 +1,16 @@
/* eslint-disable @typescript-eslint/no-unused-expressions */ /* eslint-disable @typescript-eslint/no-unused-expressions */
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table'; import { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table/dist/esm/components/Table';
import { Button } from '@patternfly/react-core/dist/esm/components/Button';
import { import {
Button,
FormFieldGroupExpandable, FormFieldGroupExpandable,
FormFieldGroupHeader, FormFieldGroupHeader,
TextInput, } from '@patternfly/react-core/dist/esm/components/Form';
} from '@patternfly/react-core'; import { TextInput } from '@patternfly/react-core/dist/esm/components/TextInput';
import inlineEditStyles from '@patternfly/react-styles/css/components/InlineEdit/inline-edit'; import inlineEditStyles from '@patternfly/react-styles/css/components/InlineEdit/inline-edit';
import { css } from '@patternfly/react-styles'; import { css } from '@patternfly/react-styles';
import { PlusCircleIcon, TrashAltIcon } from '@patternfly/react-icons'; import { PlusCircleIcon } from '@patternfly/react-icons/dist/esm/icons/plus-circle-icon';
import { TrashAltIcon } from '@patternfly/react-icons/dist/esm/icons/trash-alt-icon';
import { WorkspaceOptionLabel } from '~/shared/api/backendApiTypes'; import { WorkspaceOptionLabel } from '~/shared/api/backendApiTypes';
interface EditableRowInterface { interface EditableRowInterface {
@ -71,9 +72,18 @@ type ColumnNames<T> = { [K in keyof T]: string };
interface EditableLabelsProps { interface EditableLabelsProps {
rows: WorkspaceOptionLabel[]; rows: WorkspaceOptionLabel[];
setRows: (value: WorkspaceOptionLabel[]) => void; setRows: (value: WorkspaceOptionLabel[]) => void;
title?: string;
description?: string;
buttonLabel?: string;
} }
export const EditableLabels: React.FC<EditableLabelsProps> = ({ rows, setRows }) => { export const EditableLabels: React.FC<EditableLabelsProps> = ({
rows,
setRows,
title = 'Labels',
description,
buttonLabel = 'Label',
}) => {
const columnNames: ColumnNames<WorkspaceOptionLabel> = { const columnNames: ColumnNames<WorkspaceOptionLabel> = {
key: 'Key', key: 'Key',
value: 'Value', value: 'Value',
@ -86,12 +96,15 @@ export const EditableLabels: React.FC<EditableLabelsProps> = ({ rows, setRows })
header={ header={
<FormFieldGroupHeader <FormFieldGroupHeader
titleText={{ titleText={{
text: 'Labels', text: title,
id: 'workspace-kind-image-ports', id: `${title}-labels`,
}} }}
titleDescription={ titleDescription={
<> <>
<div>Labels are key/value pairs that are attached to Kubernetes objects.</div> <div>
{description ||
'Labels are key/value pairs that are attached to Kubernetes objects.'}
</div>
<div className="pf-u-font-size-sm"> <div className="pf-u-font-size-sm">
<strong>{rows.length} added</strong> <strong>{rows.length} added</strong>
</div> </div>
@ -141,7 +154,7 @@ export const EditableLabels: React.FC<EditableLabelsProps> = ({ rows, setRows })
]); ]);
}} }}
> >
Add Label {`Add ${buttonLabel}`}
</Button> </Button>
</FormFieldGroupExpandable> </FormFieldGroupExpandable>
); );

View File

@ -1,28 +1,27 @@
import React, { useCallback, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { import { Button } from '@patternfly/react-core/dist/esm/components/Button';
Button, import { Content, ContentVariants } from '@patternfly/react-core/dist/esm/components/Content';
Content, import { Flex, FlexItem } from '@patternfly/react-core/dist/esm/layouts/Flex';
ContentVariants, import { PageGroup, PageSection } from '@patternfly/react-core/dist/esm/components/Page';
Flex, import { Stack, StackItem } from '@patternfly/react-core/dist/esm/layouts/Stack';
FlexItem,
PageGroup,
PageSection,
Stack,
StackItem,
} from '@patternfly/react-core';
import { t_global_spacer_sm as SmallPadding } from '@patternfly/react-tokens'; import { t_global_spacer_sm as SmallPadding } from '@patternfly/react-tokens';
import { ExclamationCircleIcon } from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon';
import { EmptyState, EmptyStateBody } from '@patternfly/react-core/dist/esm/components/EmptyState';
import { ValidationErrorAlert } from '~/app/components/ValidationErrorAlert'; import { ValidationErrorAlert } from '~/app/components/ValidationErrorAlert';
import { useTypedNavigate } from '~/app/routerHelper'; import useWorkspaceKindByName from '~/app/hooks/useWorkspaceKindByName';
import { WorkspaceKind, ValidationError } from '~/shared/api/backendApiTypes';
import { useTypedNavigate, useTypedParams } from '~/app/routerHelper';
import { useCurrentRouteKey } from '~/app/hooks/useCurrentRouteKey'; import { useCurrentRouteKey } from '~/app/hooks/useCurrentRouteKey';
import useGenericObjectState from '~/app/hooks/useGenericObjectState'; import useGenericObjectState from '~/app/hooks/useGenericObjectState';
import { useNotebookAPI } from '~/app/hooks/useNotebookAPI'; import { useNotebookAPI } from '~/app/hooks/useNotebookAPI';
import { WorkspaceKindFormData } from '~/app/types'; import { WorkspaceKindFormData } from '~/app/types';
import { ErrorEnvelopeException } from '~/shared/api/apiUtils'; import { ErrorEnvelopeException } from '~/shared/api/apiUtils';
import { ValidationError } from '~/shared/api/backendApiTypes';
import { WorkspaceKindFileUpload } from './fileUpload/WorkspaceKindFileUpload'; import { WorkspaceKindFileUpload } from './fileUpload/WorkspaceKindFileUpload';
import { WorkspaceKindFormProperties } from './properties/WorkspaceKindFormProperties'; import { WorkspaceKindFormProperties } from './properties/WorkspaceKindFormProperties';
import { WorkspaceKindFormImage } from './image/WorkspaceKindFormImage'; import { WorkspaceKindFormImage } from './image/WorkspaceKindFormImage';
import { WorkspaceKindFormPodConfig } from './podConfig/WorkspaceKindFormPodConfig'; import { WorkspaceKindFormPodConfig } from './podConfig/WorkspaceKindFormPodConfig';
import { WorkspaceKindFormPodTemplate } from './podTemplate/WorkspaceKindFormPodTemplate';
import { EMPTY_WORKSPACE_KIND_FORM_DATA } from './helpers';
export enum WorkspaceKindFormView { export enum WorkspaceKindFormView {
Form, Form,
@ -30,6 +29,19 @@ export enum WorkspaceKindFormView {
} }
export type ValidationStatus = 'success' | 'error' | 'default'; export type ValidationStatus = 'success' | 'error' | 'default';
export type FormMode = 'edit' | 'create';
const convertToFormData = (initialData: WorkspaceKind): WorkspaceKindFormData => {
const { podTemplate, ...properties } = initialData;
const { options, ...spec } = podTemplate;
const { podConfig, imageConfig } = options;
return {
properties,
podConfig,
imageConfig,
podTemplate: spec,
};
};
export const WorkspaceKindForm: React.FC = () => { export const WorkspaceKindForm: React.FC = () => {
const navigate = useTypedNavigate(); const navigate = useTypedNavigate();
@ -38,47 +50,48 @@ export const WorkspaceKindForm: React.FC = () => {
const [yamlValue, setYamlValue] = useState(''); const [yamlValue, setYamlValue] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [validated, setValidated] = useState<ValidationStatus>('default'); const [validated, setValidated] = useState<ValidationStatus>('default');
const mode = useCurrentRouteKey() === 'workspaceKindCreate' ? 'create' : 'edit'; const mode: FormMode = useCurrentRouteKey() === 'workspaceKindCreate' ? 'create' : 'edit';
const [specErrors, setSpecErrors] = useState<ValidationError[]>([]); const [specErrors, setSpecErrors] = useState<(ValidationError | ErrorEnvelopeException)[]>([]);
const [data, setData, resetData] = useGenericObjectState<WorkspaceKindFormData>({ const { kind } = useTypedParams<'workspaceKindEdit'>();
properties: { const [initialFormData, initialFormDataLoaded, initialFormDataError] =
displayName: '', useWorkspaceKindByName(kind);
description: '',
deprecated: false, const [data, setData, resetData, replaceData] = useGenericObjectState<WorkspaceKindFormData>(
deprecationMessage: '', initialFormData ? convertToFormData(initialFormData) : EMPTY_WORKSPACE_KIND_FORM_DATA,
hidden: false, );
icon: { url: '' },
logo: { url: '' }, useEffect(() => {
}, if (!initialFormDataLoaded || initialFormData === null || mode === 'create') {
imageConfig: { return;
default: '', }
values: [], replaceData(convertToFormData(initialFormData));
}, }, [initialFormData, initialFormDataLoaded, mode, replaceData]);
podConfig: {
default: '',
values: [],
},
});
const handleSubmit = useCallback(async () => { const handleSubmit = useCallback(async () => {
setIsSubmitting(true); setIsSubmitting(true);
// TODO: Complete handleCreate with API call to create a new WS kind // TODO: Complete handleCreate with API call to create a new WS kind
try { try {
if (mode === 'create') { if (mode === 'create') {
const newWorkspaceKind = await api.createWorkspaceKind({}, yamlValue); const newWorkspaceKind = await api.createWorkspaceKind({ directYAML: true }, yamlValue);
// TODO: alert user about success // TODO: alert user about success
console.info('New workspace kind created:', JSON.stringify(newWorkspaceKind)); console.info('New workspace kind created:', JSON.stringify(newWorkspaceKind));
navigate('workspaceKinds'); navigate('workspaceKinds');
} }
// TODO: Finish when WSKind API is finalized
// const updatedWorkspace = await api.updateWorkspaceKind({}, kind, { data: {} });
// console.info('Workspace Kind updated:', JSON.stringify(updatedWorkspace));
// navigate('workspaceKinds');
} catch (err) { } catch (err) {
if (err instanceof ErrorEnvelopeException) { if (err instanceof ErrorEnvelopeException) {
const validationErrors = err.envelope.error?.cause?.validation_errors; const validationErrors = err.envelope.error?.cause?.validation_errors;
if (validationErrors && validationErrors.length > 0) { if (validationErrors && validationErrors.length > 0) {
setSpecErrors(validationErrors); setSpecErrors((prev) => [...prev, ...validationErrors]);
setValidated('error'); setValidated('error');
return; return;
} }
setSpecErrors((prev) => [...prev, err]);
setValidated('error');
} }
// TODO: alert user about error // TODO: alert user about error
console.error(`Error ${mode === 'edit' ? 'editing' : 'creating'} workspace kind: ${err}`); console.error(`Error ${mode === 'edit' ? 'editing' : 'creating'} workspace kind: ${err}`);
@ -88,14 +101,26 @@ export const WorkspaceKindForm: React.FC = () => {
}, [navigate, mode, api, yamlValue]); }, [navigate, mode, api, yamlValue]);
const canSubmit = useMemo( const canSubmit = useMemo(
() => !isSubmitting && yamlValue.length > 0 && validated === 'success', () => !isSubmitting && validated === 'success',
[yamlValue, isSubmitting, validated], [isSubmitting, validated],
); );
const cancel = useCallback(() => { const cancel = useCallback(() => {
navigate('workspaceKinds'); navigate('workspaceKinds');
}, [navigate]); }, [navigate]);
if (mode === 'edit' && initialFormDataError) {
return (
<EmptyState
titleText="Error loading Workspace Kind data"
headingLevel="h4"
icon={ExclamationCircleIcon}
status="danger"
>
<EmptyStateBody>{initialFormDataError.message}</EmptyStateBody>
</EmptyState>
);
}
return ( return (
<> <>
<PageGroup isFilled={false} stickyOnBreakpoint={{ default: 'top' }}> <PageGroup isFilled={false} stickyOnBreakpoint={{ default: 'top' }}>
@ -159,6 +184,12 @@ export const WorkspaceKindForm: React.FC = () => {
setData('podConfig', podConfig); setData('podConfig', podConfig);
}} }}
/> />
<WorkspaceKindFormPodTemplate
podTemplate={data.podTemplate}
updatePodTemplate={(podTemplate) => {
setData('podTemplate', podTemplate);
}}
/>
</> </>
)} )}
</PageSection> </PageSection>
@ -169,9 +200,10 @@ export const WorkspaceKindForm: React.FC = () => {
variant="primary" variant="primary"
ouiaId="Primary" ouiaId="Primary"
onClick={handleSubmit} onClick={handleSubmit}
isDisabled={!canSubmit} // TODO: button is always disabled on edit mode. Need to modify when WorkspaceKind edit is finalized
isDisabled={!canSubmit || mode === 'edit'}
> >
{mode === 'create' ? 'Create' : 'Edit'} {mode === 'create' ? 'Create' : 'Save'}
</Button> </Button>
</FlexItem> </FlexItem>
<FlexItem> <FlexItem>

View File

@ -0,0 +1,141 @@
import React, { useMemo, useState } from 'react';
import { Table, Thead, Tr, Td, Tbody, Th } from '@patternfly/react-table/dist/esm/components/Table';
import { getUniqueId } from '@patternfly/react-core/helpers';
import { Label } from '@patternfly/react-core/dist/esm/components/Label';
import { MenuToggle } from '@patternfly/react-core/dist/esm/components/MenuToggle';
import { PageSection } from '@patternfly/react-core/dist/esm/components/Page';
import {
Pagination,
PaginationVariant,
} from '@patternfly/react-core/dist/esm/components/Pagination';
import { Radio } from '@patternfly/react-core/dist/esm/components/Radio';
import { Dropdown, DropdownItem } from '@patternfly/react-core/dist/esm/components/Dropdown';
import { EllipsisVIcon } from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon';
import { WorkspaceKindImageConfigValue } from '~/app/types';
import { WorkspacePodConfigValue } from '~/shared/api/backendApiTypes';
interface PaginatedTableProps {
rows: WorkspaceKindImageConfigValue[] | WorkspacePodConfigValue[];
defaultId: string;
setDefaultId: (id: string) => void;
handleEdit: (index: number) => void;
openDeleteModal: (index: number) => void;
ariaLabel: string;
}
export const WorkspaceKindFormPaginatedTable: React.FC<PaginatedTableProps> = ({
rows,
defaultId,
setDefaultId,
handleEdit,
openDeleteModal,
ariaLabel,
}) => {
const [dropdownOpen, setDropdownOpen] = useState<number | null>(null);
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(10);
const rowPages = useMemo(() => {
const pages = [];
for (let i = 0; i < rows.length; i += perPage) {
pages.push(rows.slice(i, i + perPage));
}
return pages;
}, [perPage, rows]);
const onSetPage = (
_event: React.MouseEvent | React.KeyboardEvent | MouseEvent,
newPage: number,
) => {
setPage(newPage);
};
const onPerPageSelect = (
_event: React.MouseEvent | React.KeyboardEvent | MouseEvent,
newPerPage: number,
newPage: number,
) => {
setPerPage(newPerPage);
setPage(newPage);
};
return (
<PageSection>
<Table aria-label={ariaLabel}>
<Thead>
<Tr>
<Th>Display Name</Th>
<Th>ID</Th>
<Th screenReaderText="Row select">Default</Th>
<Th>Labels</Th>
<Th aria-label="Actions" />
</Tr>
</Thead>
<Tbody>
{rowPages[page - 1].map((row, index) => (
<Tr key={row.id}>
<Td>{row.displayName}</Td>
<Td>{row.id}</Td>
<Td>
<Radio
className="workspace-kind-form-radio"
id={`default-${ariaLabel}-${index}`}
name={`default-${ariaLabel}-${index}-radio`}
isChecked={defaultId === row.id}
onChange={() => {
console.log(row.id);
setDefaultId(row.id);
}}
aria-label={`Select ${row.id} as default`}
/>
</Td>
<Td>
{row.labels.length > 0 &&
row.labels.map((label) => (
<Label
style={{ marginRight: '4px', marginTop: '4px' }}
key={getUniqueId()}
>{`${label.key}: ${label.value}`}</Label>
))}
</Td>
<Td isActionCell>
<Dropdown
toggle={(toggleRef) => (
<MenuToggle
ref={toggleRef}
isExpanded={dropdownOpen === index}
onClick={() => setDropdownOpen(dropdownOpen === index ? null : index)}
variant="plain"
aria-label="plain kebab"
>
<EllipsisVIcon />
</MenuToggle>
)}
isOpen={dropdownOpen === index}
onSelect={() => setDropdownOpen(null)}
popperProps={{ position: 'right' }}
>
<DropdownItem onClick={() => handleEdit(perPage * (page - 1) + index)}>
Edit
</DropdownItem>
<DropdownItem onClick={() => openDeleteModal(perPage * (page - 1) + index)}>
Remove
</DropdownItem>
</Dropdown>
</Td>
</Tr>
))}
</Tbody>
</Table>
<Pagination
itemCount={rows.length}
widgetId="pagination-bottom"
perPage={perPage}
page={page}
variant={PaginationVariant.bottom}
isCompact
onSetPage={onSetPage}
onPerPageSelect={onPerPageSelect}
/>
</PageSection>
);
};

View File

@ -2,13 +2,12 @@ import React, { useCallback, useState } from 'react';
import yaml, { YAMLException } from 'js-yaml'; import yaml, { YAMLException } from 'js-yaml';
import { import {
FileUpload, FileUpload,
DropEvent,
FileUploadHelperText,
HelperText,
HelperTextItem,
Content,
DropzoneErrorCode, DropzoneErrorCode,
} from '@patternfly/react-core'; FileUploadHelperText,
} from '@patternfly/react-core/dist/esm/components/FileUpload';
import { DropEvent } from '@patternfly/react-core/dist/esm/helpers/typeUtils';
import { HelperText, HelperTextItem } from '@patternfly/react-core/dist/esm/components/HelperText';
import { Content } from '@patternfly/react-core/dist/esm/components/Content';
import { isValidWorkspaceKindYaml } from '~/app/pages/WorkspaceKinds/Form/helpers'; import { isValidWorkspaceKindYaml } from '~/app/pages/WorkspaceKinds/Form/helpers';
import { ValidationStatus } from '~/app/pages/WorkspaceKinds/Form/WorkspaceKindForm'; import { ValidationStatus } from '~/app/pages/WorkspaceKinds/Form/WorkspaceKindForm';

View File

@ -104,6 +104,45 @@ export const emptyPodConfig: WorkspacePodConfigValue = {
to: '', to: '',
}, },
}; };
export const EMPTY_WORKSPACE_KIND_FORM_DATA = {
properties: {
displayName: '',
description: '',
deprecated: false,
deprecationMessage: '',
hidden: false,
icon: { url: '' },
logo: { url: '' },
},
imageConfig: {
default: '',
values: [],
},
podConfig: {
default: '',
values: [],
},
podTemplate: {
podMetadata: {
labels: {},
annotations: {},
},
volumeMounts: {
home: '',
},
extraVolumeMounts: [],
culling: {
enabled: false,
maxInactiveSeconds: 86400,
activityProbe: {
jupyter: {
lastActivity: true,
},
},
},
},
};
// convert from k8s resource object {limits: {}, requests{}} to array of {type: '', limit: '', request: ''} for each type of resource (e.g. CPU, memory, nvidia.com/gpu) // convert from k8s resource object {limits: {}, requests{}} to array of {type: '', limit: '', request: ''} for each type of resource (e.g. CPU, memory, nvidia.com/gpu)
export const getResources = (currConfig: WorkspaceKindPodConfigValue): PodResourceEntry[] => { export const getResources = (currConfig: WorkspaceKindPodConfigValue): PodResourceEntry[] => {
const grouped = new Map<string, { request: string; limit: string }>([ const grouped = new Map<string, { request: string; limit: string }>([

View File

@ -1,26 +1,24 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { Button } from '@patternfly/react-core/dist/esm/components/Button';
import { Content } from '@patternfly/react-core/dist/esm/components/Content';
import { import {
Button,
Content,
Dropdown,
MenuToggle,
DropdownItem,
Modal, Modal,
ModalHeader, ModalHeader,
ModalFooter, ModalFooter,
ModalVariant, ModalVariant,
EmptyState, } from '@patternfly/react-core/dist/esm/components/Modal';
import {
EmptyStateFooter, EmptyStateFooter,
EmptyStateActions, EmptyStateActions,
EmptyState,
EmptyStateBody, EmptyStateBody,
Label, } from '@patternfly/react-core/dist/esm/components/EmptyState';
getUniqueId, import { ExpandableSection } from '@patternfly/react-core/dist/esm/components/ExpandableSection';
ExpandableSection, import { PlusCircleIcon } from '@patternfly/react-icons/dist/esm/icons/plus-circle-icon';
} from '@patternfly/react-core'; import { CubesIcon } from '@patternfly/react-icons/dist/esm/icons/cubes-icon';
import { Table, Thead, Tbody, Tr, Th, Td } from '@patternfly/react-table';
import { PlusCircleIcon, EllipsisVIcon, CubesIcon } from '@patternfly/react-icons';
import { WorkspaceKindImageConfigData, WorkspaceKindImageConfigValue } from '~/app/types'; import { WorkspaceKindImageConfigData, WorkspaceKindImageConfigValue } from '~/app/types';
import { emptyImage } from '~/app/pages/WorkspaceKinds/Form/helpers'; import { emptyImage } from '~/app/pages/WorkspaceKinds/Form/helpers';
import { WorkspaceKindFormPaginatedTable } from '~/app/pages/WorkspaceKinds/Form/WorkspaceKindFormPaginatedTable';
import { WorkspaceKindFormImageModal } from './WorkspaceKindFormImageModal'; import { WorkspaceKindFormImageModal } from './WorkspaceKindFormImageModal';
interface WorkspaceKindFormImageProps { interface WorkspaceKindFormImageProps {
@ -38,7 +36,6 @@ export const WorkspaceKindFormImage: React.FC<WorkspaceKindFormImageProps> = ({
const [defaultId, setDefaultId] = useState(imageConfig.default || ''); const [defaultId, setDefaultId] = useState(imageConfig.default || '');
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [dropdownOpen, setDropdownOpen] = useState<number | null>(null);
const [editIndex, setEditIndex] = useState<number | null>(null); const [editIndex, setEditIndex] = useState<number | null>(null);
const [deleteIndex, setDeleteIndex] = useState<number | null>(null); const [deleteIndex, setDeleteIndex] = useState<number | null>(null);
const [image, setImage] = useState<WorkspaceKindImageConfigValue>({ ...emptyImage }); const [image, setImage] = useState<WorkspaceKindImageConfigValue>({ ...emptyImage });
@ -125,70 +122,17 @@ export const WorkspaceKindFormImage: React.FC<WorkspaceKindFormImageProps> = ({
)} )}
{imageConfig.values.length > 0 && ( {imageConfig.values.length > 0 && (
<div> <div>
<Table aria-label="Images table"> <WorkspaceKindFormPaginatedTable
<Thead> ariaLabel="Images table"
<Tr> rows={imageConfig.values}
<Th>Display Name</Th> defaultId={defaultId}
<Th>ID</Th> setDefaultId={(id) => {
<Th screenReaderText="Row select">Default</Th> updateImageConfig({ ...imageConfig, default: id });
<Th>Hidden</Th> setDefaultId(id);
<Th>Labels</Th> }}
<Th aria-label="Actions" /> handleEdit={handleEdit}
</Tr> openDeleteModal={openDeleteModal}
</Thead> />
<Tbody>
{imageConfig.values.map((img, index) => (
<Tr key={img.id}>
<Td>{img.displayName}</Td>
<Td>{img.id}</Td>
<Td>
<input
type="radio"
name="default-image"
checked={defaultId === img.id}
onChange={() => {
setDefaultId(img.id);
updateImageConfig({ ...imageConfig, default: img.id });
}}
aria-label={`Select ${img.id} as default`}
/>
</Td>
<Td>{img.hidden ? 'Yes' : 'No'}</Td>
<Td>
{img.labels.length > 0 &&
img.labels.map((label) => (
<Label
style={{ marginRight: '4px', marginTop: '4px' }}
key={getUniqueId()}
>{`${label.key}: ${label.value}`}</Label>
))}
</Td>
<Td isActionCell>
<Dropdown
toggle={(toggleRef) => (
<MenuToggle
ref={toggleRef}
isExpanded={dropdownOpen === index}
onClick={() => setDropdownOpen(dropdownOpen === index ? null : index)}
variant="plain"
aria-label="plain kebab"
>
<EllipsisVIcon />
</MenuToggle>
)}
isOpen={dropdownOpen === index}
onSelect={() => setDropdownOpen(null)}
popperProps={{ position: 'right' }}
>
<DropdownItem onClick={() => handleEdit(index)}>Edit</DropdownItem>
<DropdownItem onClick={() => openDeleteModal(index)}>Remove</DropdownItem>
</Dropdown>
</Td>
</Tr>
))}
</Tbody>
</Table>
{addImageBtn} {addImageBtn}
</div> </div>
)} )}

View File

@ -4,15 +4,16 @@ import {
ModalHeader, ModalHeader,
ModalBody, ModalBody,
ModalFooter, ModalFooter,
Button, } from '@patternfly/react-core/dist/esm/components/Modal';
Form, import { Button } from '@patternfly/react-core/dist/esm/components/Button';
FormGroup, import { Form, FormGroup } from '@patternfly/react-core/dist/esm/components/Form';
TextInput, import { TextInput } from '@patternfly/react-core/dist/esm/components/TextInput';
import {
FormSelect, FormSelect,
FormSelectOption, FormSelectOption,
Switch, } from '@patternfly/react-core/dist/esm/components/FormSelect';
HelperText, import { Switch } from '@patternfly/react-core/dist/esm/components/Switch';
} from '@patternfly/react-core'; import { HelperText } from '@patternfly/react-core/dist/esm/components/HelperText';
import { WorkspaceKindImageConfigValue, ImagePullPolicy } from '~/app/types'; import { WorkspaceKindImageConfigValue, ImagePullPolicy } from '~/app/types';
import { EditableLabels } from '~/app/pages/WorkspaceKinds/Form/EditableLabels'; import { EditableLabels } from '~/app/pages/WorkspaceKinds/Form/EditableLabels';
import { emptyImage } from '~/app/pages/WorkspaceKinds/Form/helpers'; import { emptyImage } from '~/app/pages/WorkspaceKinds/Form/helpers';
@ -138,7 +139,7 @@ export const WorkspaceKindFormImageModal: React.FC<WorkspaceKindFormImageModalPr
</FormSelect> </FormSelect>
</FormGroup> </FormGroup>
<WorkspaceKindFormImagePort <WorkspaceKindFormImagePort
ports={image.ports} ports={image.ports || []}
setPorts={(ports) => setImage({ ...image, ports })} setPorts={(ports) => setImage({ ...image, ports })}
/> />
{mode === 'edit' && ( {mode === 'edit' && (

View File

@ -1,12 +1,11 @@
import React from 'react'; import React from 'react';
import { import {
FormGroup,
FormFieldGroupExpandable, FormFieldGroupExpandable,
FormFieldGroupHeader, FormFieldGroupHeader,
FormGroup, } from '@patternfly/react-core/dist/esm/components/Form';
Grid, import { Grid, GridItem } from '@patternfly/react-core/dist/esm/layouts/Grid';
GridItem, import { TextInput } from '@patternfly/react-core/dist/esm/components/TextInput';
TextInput,
} from '@patternfly/react-core';
import { WorkspaceKindImagePort } from '~/app/types'; import { WorkspaceKindImagePort } from '~/app/types';
interface WorkspaceKindFormImagePortProps { interface WorkspaceKindFormImagePortProps {

View File

@ -1,12 +1,14 @@
import React from 'react'; import React from 'react';
import { import {
FormGroup,
FormFieldGroupExpandable, FormFieldGroupExpandable,
FormFieldGroupHeader, FormFieldGroupHeader,
FormGroup, } from '@patternfly/react-core/dist/esm/components/Form';
import {
FormSelect, FormSelect,
FormSelectOption, FormSelectOption,
TextInput, } from '@patternfly/react-core/dist/esm/components/FormSelect';
} from '@patternfly/react-core'; import { TextInput } from '@patternfly/react-core/dist/esm/components/TextInput';
import { import {
WorkspaceOptionRedirect, WorkspaceOptionRedirect,
WorkspaceRedirectMessageLevel, WorkspaceRedirectMessageLevel,

View File

@ -1,18 +1,22 @@
import React, { useState, useEffect, useCallback, useMemo } from 'react'; import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { import {
FormSelect, FormSelect,
FormSelectOption, FormSelectOption,
NumberInput, } from '@patternfly/react-core/dist/esm/components/FormSelect';
Split, import { NumberInput } from '@patternfly/react-core/dist/esm/components/NumberInput';
SplitItem, import { Split, SplitItem } from '@patternfly/react-core/dist/esm/layouts/Split';
} from '@patternfly/react-core'; import {
import { CPU_UNITS, MEMORY_UNITS_FOR_SELECTION, UnitOption } from '~/shared/utilities/valueUnits'; CPU_UNITS,
MEMORY_UNITS_FOR_SELECTION,
TIME_UNIT_FOR_SELECTION,
UnitOption,
} from '~/shared/utilities/valueUnits';
import { parseResourceValue } from '~/shared/utilities/WorkspaceUtils'; import { parseResourceValue } from '~/shared/utilities/WorkspaceUtils';
interface ResourceInputWrapperProps { interface ResourceInputWrapperProps {
value: string; value: string;
onChange: (value: string) => void; onChange: (value: string) => void;
type: 'cpu' | 'memory' | 'custom'; type: 'cpu' | 'memory' | 'time' | 'custom';
min?: number; min?: number;
max?: number; max?: number;
step?: number; step?: number;
@ -26,6 +30,7 @@ const unitMap: {
} = { } = {
memory: MEMORY_UNITS_FOR_SELECTION, memory: MEMORY_UNITS_FOR_SELECTION,
cpu: CPU_UNITS, cpu: CPU_UNITS,
time: TIME_UNIT_FOR_SELECTION,
}; };
const DEFAULT_STEP = 1; const DEFAULT_STEP = 1;
@ -34,7 +39,6 @@ const DEFAULT_UNITS = {
memory: 'Mi', memory: 'Mi',
cpu: '', cpu: '',
}; };
export const ResourceInputWrapper: React.FC<ResourceInputWrapperProps> = ({ export const ResourceInputWrapper: React.FC<ResourceInputWrapperProps> = ({
value, value,
onChange, onChange,
@ -48,22 +52,47 @@ export const ResourceInputWrapper: React.FC<ResourceInputWrapperProps> = ({
}) => { }) => {
const [inputValue, setInputValue] = useState(value); const [inputValue, setInputValue] = useState(value);
const [unit, setUnit] = useState<string>(''); const [unit, setUnit] = useState<string>('');
const isTimeInitialized = useRef(false);
useEffect(() => { useEffect(() => {
if (type === 'custom') { if (type === 'time') {
setInputValue(value); // Initialize time only once
return; if (!isTimeInitialized.current) {
const seconds = parseFloat(value) || 0;
let defaultUnit = 60; // Default to minutes
if (seconds >= 86400) {
defaultUnit = 86400; // Days
} else if (seconds >= 3600) {
defaultUnit = 3600; // Hours
} else if (seconds >= 60) {
defaultUnit = 60; // Minutes
} else {
defaultUnit = 1; // Seconds
}
setUnit(defaultUnit.toString());
setInputValue((seconds / defaultUnit).toString());
isTimeInitialized.current = true;
}
} else {
if (type === 'custom') {
setInputValue(value);
return;
}
const [numericValue, extractedUnit] = parseResourceValue(value, type);
setInputValue(String(numericValue || ''));
setUnit(extractedUnit?.unit || DEFAULT_UNITS[type]);
} }
const [numericValue, extractedUnit] = parseResourceValue(value, type); }, [type, value]);
setInputValue(String(numericValue || ''));
setUnit(extractedUnit?.unit || DEFAULT_UNITS[type]);
}, [value, type]);
const handleInputChange = useCallback( const handleInputChange = useCallback(
(newValue: string) => { (newValue: string) => {
setInputValue(newValue); setInputValue(newValue);
if (type === 'custom') { if (type === 'custom') {
onChange(newValue); onChange(newValue);
} else if (type === 'time') {
const numericValue = parseFloat(newValue) || 0;
const unitMultiplier = parseFloat(unit) || 1;
onChange(String(numericValue * unitMultiplier));
} else { } else {
onChange(newValue ? `${newValue}${unit}` : ''); onChange(newValue ? `${newValue}${unit}` : '');
} }
@ -73,12 +102,24 @@ export const ResourceInputWrapper: React.FC<ResourceInputWrapperProps> = ({
const handleUnitChange = useCallback( const handleUnitChange = useCallback(
(newUnit: string) => { (newUnit: string) => {
setUnit(newUnit); if (type === 'time') {
if (inputValue) { const currentValue = parseFloat(inputValue) || 0;
onChange(`${inputValue}${newUnit}`); const oldUnitMultiplier = parseFloat(unit) || 1;
const newUnitMultiplier = parseFloat(newUnit) || 1;
// Convert the current value to the new unit
const valueInSeconds = currentValue * oldUnitMultiplier;
const valueInNewUnit = valueInSeconds / newUnitMultiplier;
setUnit(newUnit);
setInputValue(valueInNewUnit.toString());
onChange(String(valueInSeconds));
} else {
setUnit(newUnit);
if (inputValue) {
onChange(`${inputValue}${newUnit}`);
}
} }
}, },
[inputValue, onChange], [inputValue, onChange, type, unit],
); );
const handleIncrement = useCallback(() => { const handleIncrement = useCallback(() => {
@ -104,7 +145,13 @@ export const ResourceInputWrapper: React.FC<ResourceInputWrapperProps> = ({
const unitOptions = useMemo( const unitOptions = useMemo(
() => () =>
type !== 'custom' type !== 'custom'
? unitMap[type].map((u) => <FormSelectOption label={u.name} key={u.name} value={u.unit} />) ? unitMap[type].map((u) => (
<FormSelectOption
label={u.name}
key={u.name}
value={type === 'time' ? u.weight : u.unit}
/>
))
: [], : [],
[type], [type],
); );
@ -136,6 +183,7 @@ export const ResourceInputWrapper: React.FC<ResourceInputWrapperProps> = ({
onChange={(_, v) => handleUnitChange(v)} onChange={(_, v) => handleUnitChange(v)}
id={`${ariaLabel}-unit-select`} id={`${ariaLabel}-unit-select`}
isDisabled={isDisabled} isDisabled={isDisabled}
className="workspace-kind-unit-select"
> >
{unitOptions} {unitOptions}
</FormSelect> </FormSelect>

View File

@ -1,27 +1,24 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { Button } from '@patternfly/react-core/dist/esm/components/Button';
import { Content } from '@patternfly/react-core/dist/esm/components/Content';
import { import {
Button,
Content,
Dropdown,
MenuToggle,
DropdownItem,
Modal, Modal,
ModalHeader, ModalHeader,
ModalFooter, ModalFooter,
ModalVariant, ModalVariant,
} from '@patternfly/react-core/dist/esm/components/Modal';
import {
EmptyState, EmptyState,
EmptyStateBody,
EmptyStateFooter, EmptyStateFooter,
EmptyStateActions, EmptyStateActions,
ExpandableSection, } from '@patternfly/react-core/dist/esm/components/EmptyState';
EmptyStateBody, import { ExpandableSection } from '@patternfly/react-core/dist/esm/components/ExpandableSection';
Label, import { PlusCircleIcon } from '@patternfly/react-icons/dist/esm/icons/plus-circle-icon';
getUniqueId, import { CubesIcon } from '@patternfly/react-icons/dist/esm/icons/cubes-icon';
} from '@patternfly/react-core';
import { Table, Thead, Tbody, Tr, Th, Td } from '@patternfly/react-table';
import { PlusCircleIcon, EllipsisVIcon, CubesIcon } from '@patternfly/react-icons';
import { emptyPodConfig } from '~/app/pages/WorkspaceKinds/Form/helpers'; import { emptyPodConfig } from '~/app/pages/WorkspaceKinds/Form/helpers';
import { WorkspaceKindPodConfigValue, WorkspaceKindPodConfigData } from '~/app/types'; import { WorkspaceKindPodConfigValue, WorkspaceKindPodConfigData } from '~/app/types';
import { WorkspaceKindFormPaginatedTable } from '~/app/pages/WorkspaceKinds/Form/WorkspaceKindFormPaginatedTable';
import { WorkspaceKindFormPodConfigModal } from './WorkspaceKindFormPodConfigModal'; import { WorkspaceKindFormPodConfigModal } from './WorkspaceKindFormPodConfigModal';
interface WorkspaceKindFormPodConfigProps { interface WorkspaceKindFormPodConfigProps {
@ -37,7 +34,6 @@ export const WorkspaceKindFormPodConfig: React.FC<WorkspaceKindFormPodConfigProp
const [defaultId, setDefaultId] = useState(podConfig.default || ''); const [defaultId, setDefaultId] = useState(podConfig.default || '');
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [dropdownOpen, setDropdownOpen] = useState<number | null>(null);
const [editIndex, setEditIndex] = useState<number | null>(null); const [editIndex, setEditIndex] = useState<number | null>(null);
const [deleteIndex, setDeleteIndex] = useState<number | null>(null); const [deleteIndex, setDeleteIndex] = useState<number | null>(null);
const [currConfig, setCurrConfig] = useState<WorkspaceKindPodConfigValue>({ ...emptyPodConfig }); const [currConfig, setCurrConfig] = useState<WorkspaceKindPodConfigValue>({ ...emptyPodConfig });
@ -128,69 +124,17 @@ export const WorkspaceKindFormPodConfig: React.FC<WorkspaceKindFormPodConfigProp
)} )}
{podConfig.values.length > 0 && ( {podConfig.values.length > 0 && (
<> <>
<Table aria-label="pod configs table"> <WorkspaceKindFormPaginatedTable
<Thead> ariaLabel="Pod Configs Table"
<Tr> rows={podConfig.values}
<Th>Display Name</Th> defaultId={defaultId}
<Th>ID</Th> setDefaultId={(id) => {
<Th screenReaderText="Row select">Default</Th> updatePodConfig({ ...podConfig, default: id });
<Th>Hidden</Th> setDefaultId(id);
<Th>Labels</Th> }}
<Th aria-label="Actions" /> handleEdit={handleEdit}
</Tr> openDeleteModal={openDeleteModal}
</Thead> />
<Tbody>
{podConfig.values.map((config, index) => (
<Tr key={config.id}>
<Td>{config.displayName}</Td>
<Td>{config.id}</Td>
<Td>
<input
type="radio"
name="default-podConfig"
checked={defaultId === config.id}
onChange={() => {
setDefaultId(config.id);
updatePodConfig({ ...podConfig, default: config.id });
}}
aria-label={`Select ${config.id} as default`}
/>
</Td>
<Td>{config.hidden ? 'Yes' : 'No'}</Td>
<Td>
{config.labels.length > 0 &&
config.labels.map((label) => (
<Label
style={{ marginRight: '4px', marginTop: '4px' }}
key={getUniqueId()}
>{`${label.key}: ${label.value}`}</Label>
))}
</Td>
<Td isActionCell>
<Dropdown
toggle={(toggleRef) => (
<MenuToggle
ref={toggleRef}
isExpanded={dropdownOpen === index}
onClick={() => setDropdownOpen(dropdownOpen === index ? null : index)}
variant="plain"
aria-label="plain kebab"
>
<EllipsisVIcon />
</MenuToggle>
)}
isOpen={dropdownOpen === index}
onSelect={() => setDropdownOpen(null)}
popperProps={{ position: 'right' }}
>
<DropdownItem onClick={() => handleEdit(index)}>Edit</DropdownItem>
<DropdownItem onClick={() => openDeleteModal(index)}>Remove</DropdownItem>
</Dropdown>
</Td>
</Tr>
))}
</Tbody>
</Table>
{addConfigBtn} {addConfigBtn}
</> </>
)} )}

View File

@ -4,13 +4,12 @@ import {
ModalHeader, ModalHeader,
ModalBody, ModalBody,
ModalFooter, ModalFooter,
Button, } from '@patternfly/react-core/dist/esm/components/Modal';
Form, import { Button } from '@patternfly/react-core/dist/esm/components/Button';
FormGroup, import { Form, FormGroup } from '@patternfly/react-core/dist/esm/components/Form';
TextInput, import { TextInput } from '@patternfly/react-core/dist/esm/components/TextInput';
Switch, import { Switch } from '@patternfly/react-core/dist/esm/components/Switch';
HelperText, import { HelperText } from '@patternfly/react-core/dist/esm/components/HelperText';
} from '@patternfly/react-core';
import { WorkspaceKindPodConfigValue } from '~/app/types'; import { WorkspaceKindPodConfigValue } from '~/app/types';
import { WorkspaceOptionLabel } from '~/shared/api/backendApiTypes'; import { WorkspaceOptionLabel } from '~/shared/api/backendApiTypes';
import { EditableLabels } from '~/app/pages/WorkspaceKinds/Form/EditableLabels'; import { EditableLabels } from '~/app/pages/WorkspaceKinds/Form/EditableLabels';

View File

@ -1,17 +1,16 @@
import React, { useCallback, useEffect, useState, useMemo } from 'react'; import React, { useCallback, useEffect, useState, useMemo } from 'react';
import { Button } from '@patternfly/react-core/dist/esm/components/Button';
import { Grid, GridItem } from '@patternfly/react-core/dist/esm/layouts/Grid';
import { Title } from '@patternfly/react-core/dist/esm/components/Title';
import { import {
Button,
Grid,
GridItem,
Title,
FormFieldGroupExpandable, FormFieldGroupExpandable,
FormFieldGroupHeader, FormFieldGroupHeader,
TextInput, } from '@patternfly/react-core/dist/esm/components/Form';
Checkbox, import { TextInput } from '@patternfly/react-core/dist/esm/components/TextInput';
HelperText, import { Checkbox } from '@patternfly/react-core/dist/esm/components/Checkbox';
HelperTextItem, import { HelperText, HelperTextItem } from '@patternfly/react-core/dist/esm/components/HelperText';
} from '@patternfly/react-core'; import { PlusCircleIcon } from '@patternfly/react-icons/dist/esm/icons/plus-circle-icon';
import { PlusCircleIcon, TrashAltIcon } from '@patternfly/react-icons'; import { TrashAltIcon } from '@patternfly/react-icons/dist/esm/icons/trash-alt-icon';
import { generateUniqueId } from '~/app/pages/WorkspaceKinds/Form/helpers'; import { generateUniqueId } from '~/app/pages/WorkspaceKinds/Form/helpers';
import { isMemoryLimitLarger } from '~/shared/utilities/valueUnits'; import { isMemoryLimitLarger } from '~/shared/utilities/valueUnits';
import { ResourceInputWrapper } from './ResourceInputWrapper'; import { ResourceInputWrapper } from './ResourceInputWrapper';
@ -312,6 +311,7 @@ export const WorkspaceKindFormResource: React.FC<WorkspaceKindFormResourceProps>
onChange={(_event, value) => handleChange(res.id, 'type', value)} onChange={(_event, value) => handleChange(res.id, 'type', value)}
/> />
</GridItem> </GridItem>
<GridItem span={2}> <GridItem span={2}>
<Button <Button
variant="link" variant="link"

View File

@ -0,0 +1,196 @@
import React, { useCallback, useState } from 'react';
import {
Form,
FormFieldGroup,
FormFieldGroupHeader,
FormGroup,
} from '@patternfly/react-core/dist/esm/components/Form';
import { ExpandableSection } from '@patternfly/react-core/dist/esm/components/ExpandableSection';
import { HelperText, HelperTextItem } from '@patternfly/react-core/dist/esm/components/HelperText';
import { Switch } from '@patternfly/react-core/dist/esm/components/Switch';
import { WorkspaceKindPodTemplateData } from '~/app/types';
import { EditableLabels } from '~/app/pages/WorkspaceKinds/Form/EditableLabels';
import { WorkspacePodVolumeMount } from '~/shared/api/backendApiTypes';
import { ResourceInputWrapper } from '~/app/pages/WorkspaceKinds/Form/podConfig/ResourceInputWrapper';
import { WorkspaceFormPropertiesVolumes } from '~/app/pages/Workspaces/Form/properties/WorkspaceFormPropertiesVolumes';
interface WorkspaceKindFormPodTemplateProps {
podTemplate: WorkspaceKindPodTemplateData;
updatePodTemplate: (template: WorkspaceKindPodTemplateData) => void;
}
export const WorkspaceKindFormPodTemplate: React.FC<WorkspaceKindFormPodTemplateProps> = ({
podTemplate,
updatePodTemplate,
}) => {
const [isExpanded, setIsExpanded] = useState(false);
const [volumes, setVolumes] = useState<WorkspacePodVolumeMount[]>([]);
const toggleCullingEnabled = useCallback(
(checked: boolean) => {
if (podTemplate.culling) {
updatePodTemplate({
...podTemplate,
culling: {
...podTemplate.culling,
enabled: checked,
},
});
}
},
[podTemplate, updatePodTemplate],
);
const handleVolumes = useCallback(
(newVolumes: WorkspacePodVolumeMount[]) => {
setVolumes(newVolumes);
updatePodTemplate({
...podTemplate,
extraVolumeMounts: volumes,
});
},
[podTemplate, updatePodTemplate, volumes],
);
return (
<div className="pf-u-mb-0">
<ExpandableSection
toggleText="Pod Lifecycle & Customization"
onToggle={() => setIsExpanded((prev) => !prev)}
isExpanded={isExpanded}
isIndented
>
<Form>
<FormFieldGroup
aria-label="Pod Metadata"
header={
<FormFieldGroupHeader
titleText={{
text: 'Pod Metadata',
id: 'workspace-kind-pod-metadata',
}}
titleDescription={
<HelperText>
Edit mutable metadata of all pods created with this Workspace Kind.
</HelperText>
}
/>
}
>
<EditableLabels
rows={Object.entries(podTemplate.podMetadata.labels).map((entry) => ({
key: entry[0],
value: entry[1],
}))}
setRows={(newLabels) => {
updatePodTemplate({
...podTemplate,
podMetadata: {
...podTemplate.podMetadata,
labels: newLabels.reduce((acc: { [k: string]: string }, { key, value }) => {
acc[key] = value;
return acc;
}, {}),
},
});
}}
/>
<EditableLabels
title="Annotations"
description="Use annotations to attach arbitrary non-identifying metadata to Kubernetes objects."
buttonLabel="Annotation"
rows={Object.entries(podTemplate.podMetadata.annotations).map((entry) => ({
key: entry[0],
value: entry[1],
}))}
setRows={(newAnnotations) => {
updatePodTemplate({
...podTemplate,
podMetadata: {
...podTemplate.podMetadata,
annotations: newAnnotations.reduce(
(acc: { [k: string]: string }, { key, value }) => {
acc[key] = value;
return acc;
},
{},
),
},
});
}}
/>
</FormFieldGroup>
{/* podTemplate.culling is currently not developed in the backend */}
{podTemplate.culling && (
<FormFieldGroup
aria-label="Pod Culling"
header={
<FormFieldGroupHeader
titleText={{
text: 'Pod Culling',
id: 'workspace-kind-pod-culling',
}}
titleDescription={
<HelperText>
<HelperTextItem variant="warning">
Warning: Only for JupyterLab deployments
</HelperTextItem>
Culling scales the number of pods in a Workspace to zero based on its last
activity by polling Jupyter&apos;s status endpoint.
</HelperText>
}
/>
}
>
<FormGroup>
<Switch
isChecked={podTemplate.culling.enabled || false}
label="Enabled"
aria-label="pod template enable culling controlled check"
onChange={(_, checked) => toggleCullingEnabled(checked)}
id="workspace-kind-pod-template-culling-enabled"
name="culling-enabled"
/>
</FormGroup>
<FormGroup label="Max Inactive Period">
<ResourceInputWrapper
value={String(podTemplate.culling.maxInactiveSeconds || 86400)}
type="time"
onChange={(value) =>
podTemplate.culling &&
updatePodTemplate({
...podTemplate,
culling: {
...podTemplate.culling,
maxInactiveSeconds: Number(value),
},
})
}
step={1}
aria-label="max inactive period input"
isDisabled={!podTemplate.culling.enabled}
/>
</FormGroup>
</FormFieldGroup>
)}
<FormFieldGroup
aria-label="Additional Volumes"
header={
<FormFieldGroupHeader
titleText={{
text: 'Additional Volumes',
id: 'workspace-kind-extra-volume',
}}
titleDescription={
<HelperText>Configure the paths to mount additional PVCs.</HelperText>
}
/>
}
>
<WorkspaceFormPropertiesVolumes volumes={volumes} setVolumes={handleVolumes} />
</FormFieldGroup>
</Form>
</ExpandableSection>
</div>
);
};

View File

@ -1,13 +1,10 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { import { Content } from '@patternfly/react-core/dist/esm/components/Content';
Content, import { ExpandableSection } from '@patternfly/react-core/dist/esm/components/ExpandableSection';
ExpandableSection, import { Form, FormGroup } from '@patternfly/react-core/dist/esm/components/Form';
Form, import { HelperText } from '@patternfly/react-core/dist/esm/components/HelperText';
FormGroup, import { Switch } from '@patternfly/react-core/dist/esm/components/Switch';
HelperText, import { TextInput } from '@patternfly/react-core/dist/esm/components/TextInput';
Switch,
TextInput,
} from '@patternfly/react-core';
import { WorkspaceKindProperties } from '~/app/types'; import { WorkspaceKindProperties } from '~/app/types';
interface WorkspaceKindFormPropertiesProps { interface WorkspaceKindFormPropertiesProps {
@ -79,9 +76,9 @@ export const WorkspaceKindFormProperties: React.FC<WorkspaceKindFormPropertiesPr
<TextInput <TextInput
isDisabled={!properties.deprecated} isDisabled={!properties.deprecated}
type="text" type="text"
label="Deprecation Message" label="Deprecation message"
value={properties.deprecationMessage} value={properties.deprecationMessage}
placeholder="Deprecation Message" placeholder="Deprecation message"
onChange={(_, value) => updateField({ ...properties, deprecationMessage: value })} onChange={(_, value) => updateField({ ...properties, deprecationMessage: value })}
id="workspace-kind-deprecated-msg" id="workspace-kind-deprecated-msg"
/> />

View File

@ -3,25 +3,29 @@ import {
Drawer, Drawer,
DrawerContent, DrawerContent,
DrawerContentBody, DrawerContentBody,
PageSection, } from '@patternfly/react-core/dist/esm/components/Drawer';
Content, import { PageSection } from '@patternfly/react-core/dist/esm/components/Page';
Tooltip, import { Content } from '@patternfly/react-core/dist/esm/components/Content';
Label, import { Tooltip } from '@patternfly/react-core/dist/esm/components/Tooltip';
import { Label } from '@patternfly/react-core/dist/esm/components/Label';
import {
Toolbar, Toolbar,
ToolbarContent, ToolbarContent,
ToolbarItem, ToolbarItem,
ToolbarGroup,
ToolbarFilter,
ToolbarToggleGroup,
} from '@patternfly/react-core/dist/esm/components/Toolbar';
import {
Menu, Menu,
MenuContent, MenuContent,
MenuList, MenuList,
MenuItem, MenuItem,
MenuToggle, } from '@patternfly/react-core/dist/esm/components/Menu';
Popper, import { MenuToggle } from '@patternfly/react-core/dist/esm/components/MenuToggle';
ToolbarGroup, import { Popper } from '@patternfly/react-core/helpers';
ToolbarFilter, import { Bullseye } from '@patternfly/react-core/dist/esm/layouts/Bullseye';
ToolbarToggleGroup, import { Button } from '@patternfly/react-core/dist/esm/components/Button';
Bullseye,
Button,
} from '@patternfly/react-core';
import { import {
Table, Table,
Thead, Thead,
@ -32,8 +36,8 @@ import {
ThProps, ThProps,
ActionsColumn, ActionsColumn,
IActions, IActions,
} from '@patternfly/react-table'; } from '@patternfly/react-table/dist/esm/components/Table';
import { FilterIcon } from '@patternfly/react-icons'; import { FilterIcon } from '@patternfly/react-icons/dist/esm/icons/filter-icon';
import { WorkspaceKind } from '~/shared/api/backendApiTypes'; import { WorkspaceKind } from '~/shared/api/backendApiTypes';
import useWorkspaceKinds from '~/app/hooks/useWorkspaceKinds'; import useWorkspaceKinds from '~/app/hooks/useWorkspaceKinds';
import { useWorkspaceCountPerKind } from '~/app/hooks/useWorkspaceCountPerKind'; import { useWorkspaceCountPerKind } from '~/app/hooks/useWorkspaceCountPerKind';
@ -58,9 +62,9 @@ export const WorkspaceKinds: React.FunctionComponent = () => {
description: { name: 'Description', label: 'Description', id: 'description' }, description: { name: 'Description', label: 'Description', id: 'description' },
deprecated: { name: 'Status', label: 'Status', id: 'status' }, deprecated: { name: 'Status', label: 'Status', id: 'status' },
numberOfWorkspaces: { numberOfWorkspaces: {
name: 'Number of workspaces', name: 'Workspaces',
label: 'Number of workspaces', label: 'Workspaces',
id: 'number-of-workspaces', id: 'workspaces',
}, },
}), }),
[], [],
@ -430,8 +434,17 @@ export const WorkspaceKinds: React.FunctionComponent = () => {
title: 'View Details', title: 'View Details',
onClick: () => viewDetailsClick(workspaceKind), onClick: () => viewDetailsClick(workspaceKind),
}, },
{
id: 'edit-workspace-kind',
title: 'Edit',
onClick: () =>
navigate('workspaceKindEdit', {
params: { kind: workspaceKind.name },
state: { workspaceKindName: workspaceKind.name },
}),
},
], ],
[viewDetailsClick], [navigate, viewDetailsClick],
); );
const workspaceDetailsContent = ( const workspaceDetailsContent = (
@ -465,7 +478,7 @@ export const WorkspaceKinds: React.FunctionComponent = () => {
<DrawerContentBody> <DrawerContentBody>
<PageSection isFilled> <PageSection isFilled>
<Content> <Content>
<h1>Kubeflow Workspace Kinds</h1> <h1>Workspace kinds</h1>
<p>View your existing workspace kinds.</p> <p>View your existing workspace kinds.</p>
</Content> </Content>
<br /> <br />
@ -486,9 +499,9 @@ export const WorkspaceKinds: React.FunctionComponent = () => {
<ThemeAwareSearchInput <ThemeAwareSearchInput
value={searchNameValue} value={searchNameValue}
onChange={onSearchNameChange} onChange={onSearchNameChange}
placeholder="Filter by Name" placeholder="Filter by name"
fieldLabel="Find by Name" fieldLabel="Find by name"
aria-label="Filter by Name" aria-label="Filter by name"
/> />
</ToolbarItem> </ToolbarItem>
</ToolbarFilter> </ToolbarFilter>
@ -507,9 +520,9 @@ export const WorkspaceKinds: React.FunctionComponent = () => {
<ThemeAwareSearchInput <ThemeAwareSearchInput
value={searchDescriptionValue} value={searchDescriptionValue}
onChange={onSearchDescriptionChange} onChange={onSearchDescriptionChange}
placeholder="Filter by Description" placeholder="Filter by description"
fieldLabel="Find by Description" fieldLabel="Find by description"
aria-label="Filter by Description" aria-label="Filter by description"
/> />
</ToolbarItem> </ToolbarItem>
</ToolbarFilter> </ToolbarFilter>
@ -524,7 +537,7 @@ export const WorkspaceKinds: React.FunctionComponent = () => {
</ToolbarFilter> </ToolbarFilter>
<ToolbarItem> <ToolbarItem>
<Button variant="primary" ouiaId="Primary" onClick={createWorkspaceKind}> <Button variant="primary" ouiaId="Primary" onClick={createWorkspaceKind}>
Create Workspace Kind Create workspace kind
</Button> </Button>
</ToolbarItem> </ToolbarItem>
</ToolbarGroup> </ToolbarGroup>
@ -595,6 +608,7 @@ export const WorkspaceKinds: React.FunctionComponent = () => {
<Td dataLabel={columns.numberOfWorkspaces.name}> <Td dataLabel={columns.numberOfWorkspaces.name}>
<Button <Button
variant="link" variant="link"
className="workspace-kind-summary-button"
isInline isInline
onClick={() => onClick={() =>
navigate('workspaceKindSummary', { navigate('workspaceKindSummary', {

View File

@ -3,15 +3,17 @@ import {
DrawerActions, DrawerActions,
DrawerCloseButton, DrawerCloseButton,
DrawerHead, DrawerHead,
DrawerPanelBody,
DrawerPanelContent, DrawerPanelContent,
DrawerPanelBody,
} from '@patternfly/react-core/dist/esm/components/Drawer';
import {
Tabs, Tabs,
Tab, Tab,
TabTitleText, TabTitleText,
Title,
TabContentBody, TabContentBody,
TabContent, TabContent,
} from '@patternfly/react-core'; } from '@patternfly/react-core/dist/esm/components/Tabs';
import { Title } from '@patternfly/react-core/dist/esm/components/Title';
import { WorkspaceKind } from '~/shared/api/backendApiTypes'; import { WorkspaceKind } from '~/shared/api/backendApiTypes';
import { WorkspaceCountPerKind } from '~/app/hooks/useWorkspaceCountPerKind'; import { WorkspaceCountPerKind } from '~/app/hooks/useWorkspaceCountPerKind';
import { WorkspaceKindDetailsNamespaces } from '~/app/pages/WorkspaceKinds/details/WorkspaceKindDetailsNamespaces'; import { WorkspaceKindDetailsNamespaces } from '~/app/pages/WorkspaceKinds/details/WorkspaceKindDetailsNamespaces';
@ -45,7 +47,7 @@ export const WorkspaceKindDetails: React.FunctionComponent<WorkspaceKindDetailsP
}; };
return ( return (
<DrawerPanelContent data-testid="workspaceDetails"> <DrawerPanelContent minSize="45%" isResizable data-testid="workspaceDetails">
<DrawerHead> <DrawerHead>
<Title headingLevel="h6">{workspaceKind.name}</Title> <Title headingLevel="h6">{workspaceKind.name}</Title>
<DrawerActions> <DrawerActions>
@ -69,7 +71,7 @@ export const WorkspaceKindDetails: React.FunctionComponent<WorkspaceKindDetailsP
/> />
<Tab <Tab
eventKey={podConfigsTabKey} eventKey={podConfigsTabKey}
title={<TabTitleText>Pod Configs</TabTitleText>} title={<TabTitleText>Pod configs</TabTitleText>}
tabContentId="podConfigsTabContent" tabContentId="podConfigsTabContent"
aria-label="Pod Configs" aria-label="Pod Configs"
/> />

View File

@ -1,8 +1,7 @@
import React from 'react'; import React from 'react';
import { Button, List, ListItem } from '@patternfly/react-core';
import { WorkspaceKind } from '~/shared/api/backendApiTypes'; import { WorkspaceKind } from '~/shared/api/backendApiTypes';
import { WorkspaceCountPerKind } from '~/app/hooks/useWorkspaceCountPerKind'; import { WorkspaceCountPerKind } from '~/app/hooks/useWorkspaceCountPerKind';
import { useTypedNavigate } from '~/app/routerHelper'; import { WorkspaceKindDetailsTable } from './WorkspaceKindDetailsTable';
type WorkspaceDetailsImagesProps = { type WorkspaceDetailsImagesProps = {
workspaceKind: WorkspaceKind; workspaceKind: WorkspaceKind;
@ -12,36 +11,21 @@ type WorkspaceDetailsImagesProps = {
export const WorkspaceKindDetailsImages: React.FunctionComponent<WorkspaceDetailsImagesProps> = ({ export const WorkspaceKindDetailsImages: React.FunctionComponent<WorkspaceDetailsImagesProps> = ({
workspaceKind, workspaceKind,
workspaceCountPerKind, workspaceCountPerKind,
}) => { }) => (
const navigate = useTypedNavigate(); <WorkspaceKindDetailsTable
rows={workspaceKind.podTemplate.options.imageConfig.values.map((image) => ({
return ( id: image.id,
<List isPlain> displayName: image.displayName,
{workspaceKind.podTemplate.options.imageConfig.values.map((image, rowIndex) => ( kindName: workspaceKind.name,
<ListItem key={rowIndex}> workspaceCountRouteState: {
{image.displayName}:{' '} imageId: image.id,
<Button },
variant="link" workspaceCount:
isInline // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
onClick={() => workspaceCountPerKind[workspaceKind.name]
navigate('workspaceKindSummary', { ? workspaceCountPerKind[workspaceKind.name].countByImage[image.id] ?? 0
params: { kind: workspaceKind.name }, : 0,
state: { }))}
imageId: image.id, tableKind="image"
}, />
}) );
}
>
{
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
workspaceCountPerKind[workspaceKind.name]
? workspaceCountPerKind[workspaceKind.name].countByImage[image.id] ?? 0
: 0
}
{' Workspaces'}
</Button>
</ListItem>
))}
</List>
);
};

View File

@ -1,8 +1,7 @@
import React from 'react'; import React from 'react';
import { Button, List, ListItem } from '@patternfly/react-core';
import { WorkspaceKind } from '~/shared/api/backendApiTypes'; import { WorkspaceKind } from '~/shared/api/backendApiTypes';
import { WorkspaceCountPerKind } from '~/app/hooks/useWorkspaceCountPerKind'; import { WorkspaceCountPerKind } from '~/app/hooks/useWorkspaceCountPerKind';
import { useTypedNavigate } from '~/app/routerHelper'; import { WorkspaceKindDetailsTable } from './WorkspaceKindDetailsTable';
type WorkspaceDetailsNamespacesProps = { type WorkspaceDetailsNamespacesProps = {
workspaceKind: WorkspaceKind; workspaceKind: WorkspaceKind;
@ -11,41 +10,25 @@ type WorkspaceDetailsNamespacesProps = {
export const WorkspaceKindDetailsNamespaces: React.FunctionComponent< export const WorkspaceKindDetailsNamespaces: React.FunctionComponent<
WorkspaceDetailsNamespacesProps WorkspaceDetailsNamespacesProps
> = ({ workspaceKind, workspaceCountPerKind }) => { > = ({ workspaceKind, workspaceCountPerKind }) => (
const navigate = useTypedNavigate(); <WorkspaceKindDetailsTable
rows={Object.keys(
return ( // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
<List isPlain> workspaceCountPerKind[workspaceKind.name]
{Object.keys( ? workspaceCountPerKind[workspaceKind.name].countByNamespace
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition : [],
workspaceCountPerKind[workspaceKind.name] ).map((namespace, rowIndex) => ({
? workspaceCountPerKind[workspaceKind.name].countByNamespace id: String(rowIndex),
: [], displayName: namespace,
).map((namespace, rowIndex) => ( kindName: workspaceKind.name,
<ListItem key={rowIndex}> // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
{namespace}:{' '} workspaceCount: workspaceCountPerKind[workspaceKind.name]
<Button ? workspaceCountPerKind[workspaceKind.name].countByNamespace[namespace]
variant="link" : 0,
isInline workspaceCountRouteState: {
onClick={() => namespace,
navigate('workspaceKindSummary', { },
params: { kind: workspaceKind.name }, }))}
state: { tableKind="namespace"
namespace, />
}, );
})
}
>
{
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
workspaceCountPerKind[workspaceKind.name]
? workspaceCountPerKind[workspaceKind.name].countByNamespace[namespace]
: 0
}
{' Workspaces'}
</Button>
</ListItem>
))}
</List>
);
};

View File

@ -4,8 +4,8 @@ import {
DescriptionListTerm, DescriptionListTerm,
DescriptionListGroup, DescriptionListGroup,
DescriptionListDescription, DescriptionListDescription,
Divider, } from '@patternfly/react-core/dist/esm/components/DescriptionList';
} from '@patternfly/react-core'; import { Divider } from '@patternfly/react-core/dist/esm/components/Divider';
import { WorkspaceKind } from '~/shared/api/backendApiTypes'; import { WorkspaceKind } from '~/shared/api/backendApiTypes';
import ImageFallback from '~/shared/components/ImageFallback'; import ImageFallback from '~/shared/components/ImageFallback';
import WithValidImage from '~/shared/components/WithValidImage'; import WithValidImage from '~/shared/components/WithValidImage';
@ -42,7 +42,7 @@ export const WorkspaceKindDetailsOverview: React.FunctionComponent<
</DescriptionListGroup> </DescriptionListGroup>
<Divider /> <Divider />
<DescriptionListGroup> <DescriptionListGroup>
<DescriptionListTerm>Deprecation Message</DescriptionListTerm> <DescriptionListTerm>Deprecation message</DescriptionListTerm>
<DescriptionListDescription>{workspaceKind.deprecationMessage}</DescriptionListDescription> <DescriptionListDescription>{workspaceKind.deprecationMessage}</DescriptionListDescription>
</DescriptionListGroup> </DescriptionListGroup>
<Divider /> <Divider />

View File

@ -1,8 +1,7 @@
import React from 'react'; import React from 'react';
import { Button, List, ListItem } from '@patternfly/react-core';
import { WorkspaceKind } from '~/shared/api/backendApiTypes'; import { WorkspaceKind } from '~/shared/api/backendApiTypes';
import { WorkspaceCountPerKind } from '~/app/hooks/useWorkspaceCountPerKind'; import { WorkspaceCountPerKind } from '~/app/hooks/useWorkspaceCountPerKind';
import { useTypedNavigate } from '~/app/routerHelper'; import { WorkspaceKindDetailsTable } from './WorkspaceKindDetailsTable';
type WorkspaceDetailsPodConfigsProps = { type WorkspaceDetailsPodConfigsProps = {
workspaceKind: WorkspaceKind; workspaceKind: WorkspaceKind;
@ -11,36 +10,20 @@ type WorkspaceDetailsPodConfigsProps = {
export const WorkspaceKindDetailsPodConfigs: React.FunctionComponent< export const WorkspaceKindDetailsPodConfigs: React.FunctionComponent<
WorkspaceDetailsPodConfigsProps WorkspaceDetailsPodConfigsProps
> = ({ workspaceKind, workspaceCountPerKind }) => { > = ({ workspaceKind, workspaceCountPerKind }) => (
const navigate = useTypedNavigate(); <WorkspaceKindDetailsTable
rows={workspaceKind.podTemplate.options.podConfig.values.map((podConfig) => ({
return ( id: podConfig.id,
<List isPlain> displayName: podConfig.displayName,
{workspaceKind.podTemplate.options.podConfig.values.map((podConfig, rowIndex) => ( kindName: workspaceKind.name,
<ListItem key={rowIndex}> // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
{podConfig.displayName}:{' '} workspaceCount: workspaceCountPerKind[workspaceKind.name]
<Button ? workspaceCountPerKind[workspaceKind.name].countByPodConfig[podConfig.id] ?? 0
variant="link" : 0,
isInline workspaceCountRouteState: {
onClick={() => podConfigId: podConfig.id,
navigate('workspaceKindSummary', { },
params: { kind: workspaceKind.name }, }))}
state: { tableKind="podConfig"
podConfigId: podConfig.id, />
}, );
})
}
>
{
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
workspaceCountPerKind[workspaceKind.name]
? workspaceCountPerKind[workspaceKind.name].countByPodConfig[podConfig.id] ?? 0
: 0
}
{' Workspaces'}
</Button>
</ListItem>
))}
</List>
);
};

View File

@ -0,0 +1,100 @@
import React, { useMemo, useState } from 'react';
import { Table, Thead, Tr, Td, Tbody, Th } from '@patternfly/react-table/dist/esm/components/Table';
import {
Pagination,
PaginationVariant,
} from '@patternfly/react-core/dist/esm/components/Pagination';
import { Content } from '@patternfly/react-core/dist/esm/components/Content';
import { Button } from '@patternfly/react-core/dist/esm/components/Button';
import { useTypedNavigate } from '~/app/routerHelper';
import { RouteStateMap } from '~/app/routes';
export interface WorkspaceKindDetailsTableRow {
id: string;
displayName: string;
kindName: string;
workspaceCount: number;
workspaceCountRouteState: RouteStateMap['workspaceKindSummary'];
}
interface WorkspaceKindDetailsTableProps {
rows: WorkspaceKindDetailsTableRow[];
tableKind: 'image' | 'podConfig' | 'namespace';
}
export const WorkspaceKindDetailsTable: React.FC<WorkspaceKindDetailsTableProps> = ({
rows,
tableKind,
}) => {
const navigate = useTypedNavigate();
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(10);
const rowPages = useMemo(() => {
const pages = [];
for (let i = 0; i < rows.length; i += perPage) {
pages.push(rows.slice(i, i + perPage));
}
return pages;
}, [perPage, rows]);
const onSetPage = (
_event: React.MouseEvent | React.KeyboardEvent | MouseEvent,
newPage: number,
) => {
setPage(newPage);
};
const onPerPageSelect = (
_event: React.MouseEvent | React.KeyboardEvent | MouseEvent,
newPerPage: number,
newPage: number,
) => {
setPerPage(newPerPage);
setPage(newPage);
};
return (
<Content>
<Table aria-label={`workspace-kind-details-${tableKind}`}>
<Thead>
<Tr>
<Th>Name</Th>
<Th>Workspaces</Th>
</Tr>
</Thead>
<Tbody>
{rowPages[page - 1].map((row) => (
<Tr key={row.id}>
<Td>{row.displayName}</Td>
<Td>
<Button
variant="link"
isInline
className="workspace-kind-summary-button"
onClick={() =>
navigate('workspaceKindSummary', {
params: { kind: row.kindName },
state: row.workspaceCountRouteState,
})
}
>
{row.workspaceCount} Workspaces
</Button>
</Td>
</Tr>
))}
</Tbody>
</Table>
<Pagination
itemCount={rows.length}
widgetId="pagination-bottom"
perPage={perPage}
page={page}
variant={PaginationVariant.bottom}
isCompact
onSetPage={onSetPage}
onPerPageSelect={onPerPageSelect}
/>
</Content>
);
};

View File

@ -1,10 +1,10 @@
import React, { useCallback, useRef, useState } from 'react'; import React, { useCallback, useRef, useState } from 'react';
import { Button } from '@patternfly/react-core/dist/esm/components/Button';
import { Content, ContentVariants } from '@patternfly/react-core/dist/esm/components/Content'; import { Content, ContentVariants } from '@patternfly/react-core/dist/esm/components/Content';
import { PageSection } from '@patternfly/react-core/dist/esm/components/Page'; import { PageSection } from '@patternfly/react-core/dist/esm/components/Page';
import { Stack, StackItem } from '@patternfly/react-core/dist/esm/layouts/Stack'; import { Stack, StackItem } from '@patternfly/react-core/dist/esm/layouts/Stack';
import { Breadcrumb } from '@patternfly/react-core/dist/esm/components/Breadcrumb'; import { ArrowLeftIcon } from '@patternfly/react-icons/dist/esm/icons/arrow-left-icon';
import { BreadcrumbItem } from '@patternfly/react-core/dist/esm/components/Breadcrumb/BreadcrumbItem'; import { useTypedLocation, useTypedNavigate, useTypedParams } from '~/app/routerHelper';
import { useTypedLocation, useTypedParams } from '~/app/routerHelper';
import WorkspaceTable, { import WorkspaceTable, {
WorkspaceTableFilteredColumn, WorkspaceTableFilteredColumn,
WorkspaceTableRef, WorkspaceTableRef,

View File

@ -2,18 +2,19 @@ import React from 'react';
import { import {
ClipboardCopy, ClipboardCopy,
ClipboardCopyVariant, ClipboardCopyVariant,
Content, } from '@patternfly/react-core/dist/esm/components/ClipboardCopy';
import { Content } from '@patternfly/react-core/dist/esm/components/Content';
import {
DescriptionList, DescriptionList,
DescriptionListGroup, DescriptionListGroup,
DescriptionListTerm, DescriptionListTerm,
DescriptionListDescription, DescriptionListDescription,
Flex, } from '@patternfly/react-core/dist/esm/components/DescriptionList';
FlexItem, import { Flex, FlexItem } from '@patternfly/react-core/dist/esm/layouts/Flex';
List, import { List, ListItem } from '@patternfly/react-core/dist/esm/components/List';
ListItem, import { Tooltip } from '@patternfly/react-core/dist/esm/components/Tooltip';
Tooltip, import { DatabaseIcon } from '@patternfly/react-icons/dist/esm/icons/database-icon';
} from '@patternfly/react-core'; import { LockedIcon } from '@patternfly/react-icons/dist/esm/icons/locked-icon';
import { DatabaseIcon, LockedIcon } from '@patternfly/react-icons';
import { Workspace } from '~/shared/api/backendApiTypes'; import { Workspace } from '~/shared/api/backendApiTypes';
interface DataVolumesListProps { interface DataVolumesListProps {

View File

@ -3,15 +3,17 @@ import {
DrawerActions, DrawerActions,
DrawerCloseButton, DrawerCloseButton,
DrawerHead, DrawerHead,
DrawerPanelBody,
DrawerPanelContent, DrawerPanelContent,
DrawerPanelBody,
} from '@patternfly/react-core/dist/esm/components/Drawer';
import {
Tabs, Tabs,
Tab, Tab,
TabTitleText, TabTitleText,
Title,
TabContentBody, TabContentBody,
TabContent, TabContent,
} from '@patternfly/react-core'; } from '@patternfly/react-core/dist/esm/components/Tabs';
import { Title } from '@patternfly/react-core/dist/esm/components/Title';
import { Workspace } from '~/shared/api/backendApiTypes'; import { Workspace } from '~/shared/api/backendApiTypes';
import { WorkspaceDetailsOverview } from '~/app/pages/Workspaces/Details/WorkspaceDetailsOverview'; import { WorkspaceDetailsOverview } from '~/app/pages/Workspaces/Details/WorkspaceDetailsOverview';
import { WorkspaceDetailsActions } from '~/app/pages/Workspaces/Details/WorkspaceDetailsActions'; import { WorkspaceDetailsActions } from '~/app/pages/Workspaces/Details/WorkspaceDetailsActions';

View File

@ -1,12 +1,11 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { import {
Dropdown, Dropdown,
DropdownList,
MenuToggle,
DropdownItem, DropdownItem,
Flex, DropdownList,
FlexItem, } from '@patternfly/react-core/dist/esm/components/Dropdown';
} from '@patternfly/react-core'; import { MenuToggle } from '@patternfly/react-core/dist/esm/components/MenuToggle';
import { Flex, FlexItem } from '@patternfly/react-core/dist/esm/layouts/Flex';
interface WorkspaceDetailsActionsProps { interface WorkspaceDetailsActionsProps {
// TODO: Uncomment when edit action is fully supported // TODO: Uncomment when edit action is fully supported

View File

@ -1,12 +1,12 @@
import React from 'react'; import React from 'react';
import { format } from 'date-fns'; import { format } from 'date-fns/format';
import { import {
DescriptionList, DescriptionList,
DescriptionListTerm, DescriptionListTerm,
DescriptionListGroup, DescriptionListGroup,
DescriptionListDescription, DescriptionListDescription,
Divider, } from '@patternfly/react-core/dist/esm/components/DescriptionList';
} from '@patternfly/react-core'; import { Divider } from '@patternfly/react-core/dist/esm/components/Divider';
import { Workspace } from '~/shared/api/backendApiTypes'; import { Workspace } from '~/shared/api/backendApiTypes';
const DATE_FORMAT = 'PPpp'; const DATE_FORMAT = 'PPpp';
@ -23,28 +23,28 @@ export const WorkspaceDetailsActivity: React.FunctionComponent<WorkspaceDetailsA
return ( return (
<DescriptionList isHorizontal> <DescriptionList isHorizontal>
<DescriptionListGroup> <DescriptionListGroup>
<DescriptionListTerm>Last Activity</DescriptionListTerm> <DescriptionListTerm>Last activity</DescriptionListTerm>
<DescriptionListDescription data-testid="lastActivity"> <DescriptionListDescription data-testid="lastActivity">
{format(activity.lastActivity, DATE_FORMAT)} {format(activity.lastActivity, DATE_FORMAT)}
</DescriptionListDescription> </DescriptionListDescription>
</DescriptionListGroup> </DescriptionListGroup>
<Divider /> <Divider />
<DescriptionListGroup> <DescriptionListGroup>
<DescriptionListTerm>Last Update</DescriptionListTerm> <DescriptionListTerm>Last update</DescriptionListTerm>
<DescriptionListDescription data-testid="lastUpdate"> <DescriptionListDescription data-testid="lastUpdate">
{format(activity.lastUpdate, DATE_FORMAT)} {format(activity.lastUpdate, DATE_FORMAT)}
</DescriptionListDescription> </DescriptionListDescription>
</DescriptionListGroup> </DescriptionListGroup>
<Divider /> <Divider />
<DescriptionListGroup> <DescriptionListGroup>
<DescriptionListTerm>Pause Time</DescriptionListTerm> <DescriptionListTerm>Pause time</DescriptionListTerm>
<DescriptionListDescription data-testid="pauseTime"> <DescriptionListDescription data-testid="pauseTime">
{format(pausedTime, DATE_FORMAT)} {format(pausedTime, DATE_FORMAT)}
</DescriptionListDescription> </DescriptionListDescription>
</DescriptionListGroup> </DescriptionListGroup>
<Divider /> <Divider />
<DescriptionListGroup> <DescriptionListGroup>
<DescriptionListTerm>Pending Restart</DescriptionListTerm> <DescriptionListTerm>Pending restart</DescriptionListTerm>
<DescriptionListDescription data-testid="pendingRestart"> <DescriptionListDescription data-testid="pendingRestart">
{pendingRestart ? 'Yes' : 'No'} {pendingRestart ? 'Yes' : 'No'}
</DescriptionListDescription> </DescriptionListDescription>

View File

@ -4,8 +4,8 @@ import {
DescriptionListTerm, DescriptionListTerm,
DescriptionListGroup, DescriptionListGroup,
DescriptionListDescription, DescriptionListDescription,
Divider, } from '@patternfly/react-core/dist/esm/components/DescriptionList';
} from '@patternfly/react-core'; import { Divider } from '@patternfly/react-core/dist/esm/components/Divider';
import { Workspace } from '~/shared/api/backendApiTypes'; import { Workspace } from '~/shared/api/backendApiTypes';
type WorkspaceDetailsOverviewProps = { type WorkspaceDetailsOverviewProps = {

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Tr, Td, ExpandableRowContent } from '@patternfly/react-table'; import { Tr, Td, ExpandableRowContent } from '@patternfly/react-table/dist/esm/components/Table';
import { Workspace } from '~/shared/api/backendApiTypes'; import { Workspace } from '~/shared/api/backendApiTypes';
import { WorkspaceTableColumnKeys } from '~/app/components/WorkspaceTable'; import { WorkspaceTableColumnKeys } from '~/app/components/WorkspaceTable';
import { WorkspaceStorage } from './WorkspaceStorage'; import { WorkspaceStorage } from './WorkspaceStorage';

View File

@ -1,16 +1,24 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Button } from '@patternfly/react-core/dist/esm/components/Button';
import { Content } from '@patternfly/react-core/dist/esm/components/Content';
import { Flex, FlexItem } from '@patternfly/react-core/dist/esm/layouts/Flex';
import { PageSection } from '@patternfly/react-core/dist/esm/components/Page';
import { import {
Button,
Content,
Flex,
FlexItem,
PageGroup,
PageSection,
ProgressStep, ProgressStep,
ProgressStepper, ProgressStepper,
Stack, } from '@patternfly/react-core/dist/esm/components/ProgressStepper';
StackItem, import { Stack } from '@patternfly/react-core/dist/esm/layouts/Stack';
} from '@patternfly/react-core'; import {
Drawer,
DrawerActions,
DrawerCloseButton,
DrawerContent,
DrawerContentBody,
DrawerHead,
DrawerPanelBody,
DrawerPanelContent,
} from '@patternfly/react-core/dist/esm/components/Drawer';
import { Title } from '@patternfly/react-core/dist/esm/components/Title';
import useGenericObjectState from '~/app/hooks/useGenericObjectState'; import useGenericObjectState from '~/app/hooks/useGenericObjectState';
import { useNotebookAPI } from '~/app/hooks/useNotebookAPI'; import { useNotebookAPI } from '~/app/hooks/useNotebookAPI';
import { WorkspaceFormImageSelection } from '~/app/pages/Workspaces/Form/image/WorkspaceFormImageSelection'; import { WorkspaceFormImageSelection } from '~/app/pages/Workspaces/Form/image/WorkspaceFormImageSelection';
@ -18,10 +26,18 @@ import { WorkspaceFormKindSelection } from '~/app/pages/Workspaces/Form/kind/Wor
import { WorkspaceFormPodConfigSelection } from '~/app/pages/Workspaces/Form/podConfig/WorkspaceFormPodConfigSelection'; import { WorkspaceFormPodConfigSelection } from '~/app/pages/Workspaces/Form/podConfig/WorkspaceFormPodConfigSelection';
import { WorkspaceFormPropertiesSelection } from '~/app/pages/Workspaces/Form/properties/WorkspaceFormPropertiesSelection'; import { WorkspaceFormPropertiesSelection } from '~/app/pages/Workspaces/Form/properties/WorkspaceFormPropertiesSelection';
import { WorkspaceFormData } from '~/app/types'; import { WorkspaceFormData } from '~/app/types';
import { WorkspaceCreate } from '~/shared/api/backendApiTypes'; import {
WorkspaceCreate,
WorkspaceKind,
WorkspaceImageConfigValue,
WorkspacePodConfigValue,
} from '~/shared/api/backendApiTypes';
import useWorkspaceFormData from '~/app/hooks/useWorkspaceFormData'; import useWorkspaceFormData from '~/app/hooks/useWorkspaceFormData';
import { useTypedNavigate } from '~/app/routerHelper'; import { useTypedNavigate } from '~/app/routerHelper';
import { useWorkspaceFormLocationData } from '~/app/hooks/useWorkspaceFormLocationData'; import { useWorkspaceFormLocationData } from '~/app/hooks/useWorkspaceFormLocationData';
import { WorkspaceFormKindDetails } from '~/app/pages/Workspaces/Form/kind/WorkspaceFormKindDetails';
import { WorkspaceFormImageDetails } from '~/app/pages/Workspaces/Form/image/WorkspaceFormImageDetails';
import { WorkspaceFormPodConfigDetails } from '~/app/pages/Workspaces/Form/podConfig/WorkspaceFormPodConfigDetails';
enum WorkspaceFormSteps { enum WorkspaceFormSteps {
KindSelection, KindSelection,
@ -52,6 +68,7 @@ const WorkspaceForm: React.FC = () => {
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [currentStep, setCurrentStep] = useState(WorkspaceFormSteps.KindSelection); const [currentStep, setCurrentStep] = useState(WorkspaceFormSteps.KindSelection);
const [drawerExpanded, setDrawerExpanded] = useState(false);
const [data, setData, resetData, replaceData] = const [data, setData, resetData, replaceData] =
useGenericObjectState<WorkspaceFormData>(initialFormData); useGenericObjectState<WorkspaceFormData>(initialFormData);
@ -76,30 +93,46 @@ const WorkspaceForm: React.FC = () => {
[currentStep], [currentStep],
); );
const isStepValid = useCallback(
(step: WorkspaceFormSteps) => {
switch (step) {
case WorkspaceFormSteps.KindSelection:
return !!data.kind;
case WorkspaceFormSteps.ImageSelection:
return !!data.image;
case WorkspaceFormSteps.PodConfigSelection:
return !!data.podConfig;
case WorkspaceFormSteps.Properties:
return !!data.properties.workspaceName.trim();
default:
return false;
}
},
[data.kind, data.image, data.podConfig, data.properties.workspaceName],
);
const showDrawer = useCallback(
(step: WorkspaceFormSteps) =>
// Only show drawer for steps that have drawer content
step !== WorkspaceFormSteps.Properties && isStepValid(step),
[isStepValid],
);
const previousStep = useCallback(() => { const previousStep = useCallback(() => {
setCurrentStep(currentStep - 1); const newStep = currentStep - 1;
}, [currentStep]); setCurrentStep(newStep);
setDrawerExpanded(showDrawer(newStep));
}, [currentStep, showDrawer]);
const nextStep = useCallback(() => { const nextStep = useCallback(() => {
setCurrentStep(currentStep + 1); const newStep = currentStep + 1;
}, [currentStep]); setCurrentStep(newStep);
setDrawerExpanded(showDrawer(newStep));
}, [currentStep, showDrawer]);
const canGoToPreviousStep = useMemo(() => currentStep > 0, [currentStep]); const canGoToPreviousStep = useMemo(() => currentStep > 0, [currentStep]);
const isCurrentStepValid = useMemo(() => { const isCurrentStepValid = useMemo(() => isStepValid(currentStep), [isStepValid, currentStep]);
switch (currentStep) {
case WorkspaceFormSteps.KindSelection:
return !!data.kind;
case WorkspaceFormSteps.ImageSelection:
return !!data.image;
case WorkspaceFormSteps.PodConfigSelection:
return !!data.podConfig;
case WorkspaceFormSteps.Properties:
return !!data.properties.workspaceName.trim();
default:
return false;
}
}, [currentStep, data]);
const canGoToNextStep = useMemo( const canGoToNextStep = useMemo(
() => currentStep < Object.keys(WorkspaceFormSteps).length / 2 - 1, () => currentStep < Object.keys(WorkspaceFormSteps).length / 2 - 1,
@ -168,6 +201,63 @@ const WorkspaceForm: React.FC = () => {
navigate('workspaces'); navigate('workspaces');
}, [navigate]); }, [navigate]);
const handleKindSelect = useCallback(
(kind: WorkspaceKind | undefined) => {
if (kind) {
resetData();
setData('kind', kind);
setDrawerExpanded(true);
}
},
[resetData, setData],
);
const handleImageSelect = useCallback(
(image: WorkspaceImageConfigValue | undefined) => {
if (image) {
setData('image', image);
setDrawerExpanded(true);
}
},
[setData],
);
const handlePodConfigSelect = useCallback(
(podConfig: WorkspacePodConfigValue | undefined) => {
if (podConfig) {
setData('podConfig', podConfig);
setDrawerExpanded(true);
}
},
[setData],
);
const getDrawerContent = () => {
switch (currentStep) {
case WorkspaceFormSteps.KindSelection:
return <WorkspaceFormKindDetails workspaceKind={data.kind} />;
case WorkspaceFormSteps.ImageSelection:
return <WorkspaceFormImageDetails workspaceImage={data.image} />;
case WorkspaceFormSteps.PodConfigSelection:
return <WorkspaceFormPodConfigDetails workspacePodConfig={data.podConfig} />;
default:
return null;
}
};
const getDrawerTitle = () => {
switch (currentStep) {
case WorkspaceFormSteps.KindSelection:
return 'Workspace Kind';
case WorkspaceFormSteps.ImageSelection:
return 'Image';
case WorkspaceFormSteps.PodConfigSelection:
return 'Pod Config';
default:
return '';
}
};
if (initialFormDataError) { if (initialFormDataError) {
return <p>Error loading workspace data: {initialFormDataError.message}</p>; // TODO: UX for error state return <p>Error loading workspace data: {initialFormDataError.message}</p>; // TODO: UX for error state
} }
@ -176,137 +266,160 @@ const WorkspaceForm: React.FC = () => {
return <p>Loading...</p>; // TODO: UX for loading state return <p>Loading...</p>; // TODO: UX for loading state
} }
const panelContent = (
<DrawerPanelContent>
<DrawerHead>
<Title headingLevel="h1">{getDrawerTitle()}</Title>
<DrawerActions>
<DrawerCloseButton onClick={() => setDrawerExpanded(false)} />
</DrawerActions>
</DrawerHead>
<DrawerPanelBody className="workspace-form__drawer-panel-body">
{getDrawerContent()}
</DrawerPanelBody>
</DrawerPanelContent>
);
return ( return (
<> <Drawer isInline isExpanded={drawerExpanded}>
<PageGroup isFilled={false} stickyOnBreakpoint={{ default: 'top' }}> <DrawerContent panelContent={panelContent}>
<PageSection> <DrawerContentBody>
<Stack hasGutter> <Flex
<Flex direction={{ default: 'column' }} rowGap={{ default: 'rowGapXl' }}> direction={{ default: 'column' }}
<FlexItem> flexWrap={{ default: 'nowrap' }}
<Content> style={{ height: '100%' }}
<h1>{`${mode === 'create' ? 'Create' : 'Edit'} workspace`}</h1> >
</Content> <FlexItem>
</FlexItem> <PageSection>
<FlexItem> <Stack hasGutter>
<ProgressStepper aria-label="Workspace form stepper"> <Flex direction={{ default: 'column' }} rowGap={{ default: 'rowGapXl' }}>
<ProgressStep <FlexItem>
variant={getStepVariant(WorkspaceFormSteps.KindSelection)} <Content>
isCurrent={currentStep === WorkspaceFormSteps.KindSelection} <h1>{`${mode === 'create' ? 'Create' : 'Edit'} workspace`}</h1>
id="kind-selection-step" <p>{stepDescriptions[currentStep]}</p>
titleId="kind-selection-step-title" </Content>
aria-label="Kind selection step" </FlexItem>
> <FlexItem>
Workspace Kind <ProgressStepper aria-label="Workspace form stepper">
</ProgressStep> <ProgressStep
<ProgressStep variant={getStepVariant(WorkspaceFormSteps.KindSelection)}
variant={getStepVariant(WorkspaceFormSteps.ImageSelection)} isCurrent={currentStep === WorkspaceFormSteps.KindSelection}
isCurrent={currentStep === WorkspaceFormSteps.ImageSelection} id="kind-selection-step"
id="image-selection-step" titleId="kind-selection-step-title"
titleId="image-selection-step-title" aria-label="Kind selection step"
aria-label="Image selection step" >
> Workspace Kind
Image </ProgressStep>
</ProgressStep> <ProgressStep
<ProgressStep variant={getStepVariant(WorkspaceFormSteps.ImageSelection)}
variant={getStepVariant(WorkspaceFormSteps.PodConfigSelection)} isCurrent={currentStep === WorkspaceFormSteps.ImageSelection}
isCurrent={currentStep === WorkspaceFormSteps.PodConfigSelection} id="image-selection-step"
id="pod-config-selection-step" titleId="image-selection-step-title"
titleId="pod-config-selection-step-title" aria-label="Image selection step"
aria-label="Pod config selection step" >
> Image
Pod Config </ProgressStep>
</ProgressStep> <ProgressStep
<ProgressStep variant={getStepVariant(WorkspaceFormSteps.PodConfigSelection)}
variant={getStepVariant(WorkspaceFormSteps.Properties)} isCurrent={currentStep === WorkspaceFormSteps.PodConfigSelection}
isCurrent={currentStep === WorkspaceFormSteps.Properties} id="pod-config-selection-step"
id="properties-step" titleId="pod-config-selection-step-title"
titleId="properties-step-title" aria-label="Pod config selection step"
aria-label="Properties step" >
> Pod Config
Properties </ProgressStep>
</ProgressStep> <ProgressStep
</ProgressStepper> variant={getStepVariant(WorkspaceFormSteps.Properties)}
</FlexItem> isCurrent={currentStep === WorkspaceFormSteps.Properties}
</Flex> id="properties-step"
<StackItem> titleId="properties-step-title"
<p>{stepDescriptions[currentStep]}</p> aria-label="Properties step"
</StackItem> >
</Stack> Properties
</PageSection> </ProgressStep>
</PageGroup> </ProgressStepper>
<PageSection isFilled> </FlexItem>
{currentStep === WorkspaceFormSteps.KindSelection && ( </Flex>
<WorkspaceFormKindSelection </Stack>
selectedKind={data.kind} </PageSection>
onSelect={(kind) => { </FlexItem>
resetData(); <FlexItem flex={{ default: 'flex_1' }}>
setData('kind', kind); <PageSection isFilled>
}} {currentStep === WorkspaceFormSteps.KindSelection && (
/> <WorkspaceFormKindSelection
)} selectedKind={data.kind}
{currentStep === WorkspaceFormSteps.ImageSelection && ( onSelect={handleKindSelect}
<WorkspaceFormImageSelection />
selectedImage={data.image} )}
onSelect={(image) => setData('image', image)} {currentStep === WorkspaceFormSteps.ImageSelection && (
images={data.kind?.podTemplate.options.imageConfig.values ?? []} <WorkspaceFormImageSelection
/> selectedImage={data.image}
)} onSelect={handleImageSelect}
{currentStep === WorkspaceFormSteps.PodConfigSelection && ( images={data.kind?.podTemplate.options.imageConfig.values ?? []}
<WorkspaceFormPodConfigSelection />
selectedPodConfig={data.podConfig} )}
onSelect={(podConfig) => setData('podConfig', podConfig)} {currentStep === WorkspaceFormSteps.PodConfigSelection && (
podConfigs={data.kind?.podTemplate.options.podConfig.values ?? []} <WorkspaceFormPodConfigSelection
/> selectedPodConfig={data.podConfig}
)} onSelect={handlePodConfigSelect}
{currentStep === WorkspaceFormSteps.Properties && ( podConfigs={data.kind?.podTemplate.options.podConfig.values ?? []}
<WorkspaceFormPropertiesSelection />
selectedProperties={data.properties} )}
onSelect={(properties) => setData('properties', properties)} {currentStep === WorkspaceFormSteps.Properties && (
selectedImage={data.image} <WorkspaceFormPropertiesSelection
/> selectedProperties={data.properties}
)} onSelect={(properties) => setData('properties', properties)}
</PageSection> selectedImage={data.image}
<PageSection isFilled={false} stickyOnBreakpoint={{ default: 'bottom' }}> />
<Flex> )}
<FlexItem> </PageSection>
<Button </FlexItem>
variant="secondary" <FlexItem>
ouiaId="Secondary" <PageSection>
onClick={previousStep} <Flex>
isDisabled={!canGoToPreviousStep} <FlexItem>
> <Button
Previous variant="secondary"
</Button> ouiaId="Secondary"
</FlexItem> onClick={previousStep}
<FlexItem> isDisabled={!canGoToPreviousStep}
{canGoToNextStep ? ( >
<Button Previous
variant="primary" </Button>
ouiaId="Primary" </FlexItem>
onClick={nextStep} <FlexItem>
isDisabled={!isCurrentStepValid} {canGoToNextStep ? (
> <Button
Next variant="primary"
</Button> ouiaId="Primary"
) : ( onClick={nextStep}
<Button isDisabled={!isCurrentStepValid}
variant="primary" >
ouiaId="Primary" Next
onClick={handleSubmit} </Button>
isDisabled={!canSubmit} ) : (
> <Button
{mode === 'create' ? 'Create' : 'Save'} variant="primary"
</Button> ouiaId="Primary"
)} onClick={handleSubmit}
</FlexItem> isDisabled={!canSubmit}
<FlexItem> >
<Button variant="link" isInline onClick={cancel}> {mode === 'create' ? 'Create' : 'Save'}
Cancel </Button>
</Button> )}
</FlexItem> </FlexItem>
</Flex> <FlexItem>
</PageSection> <Button variant="link" isInline onClick={cancel}>
</> Cancel
</Button>
</FlexItem>
</Flex>
</PageSection>
</FlexItem>
</Flex>
</DrawerContentBody>
</DrawerContent>
</Drawer>
); );
}; };

View File

@ -7,8 +7,8 @@ import {
DrawerHead, DrawerHead,
DrawerActions, DrawerActions,
DrawerCloseButton, DrawerCloseButton,
Title, } from '@patternfly/react-core/dist/esm/components/Drawer';
} from '@patternfly/react-core'; import { Title } from '@patternfly/react-core/dist/esm/components/Title';
interface WorkspaceFormDrawerProps { interface WorkspaceFormDrawerProps {
children: React.ReactNode; children: React.ReactNode;

View File

@ -4,8 +4,8 @@ import {
DescriptionListTerm, DescriptionListTerm,
DescriptionListGroup, DescriptionListGroup,
DescriptionListDescription, DescriptionListDescription,
Title, } from '@patternfly/react-core/dist/esm/components/DescriptionList';
} from '@patternfly/react-core'; import { Title } from '@patternfly/react-core/dist/esm/components/Title';
import { WorkspacePodConfigValue } from '~/shared/api/backendApiTypes'; import { WorkspacePodConfigValue } from '~/shared/api/backendApiTypes';
import { formatLabelKey } from '~/shared/utilities/WorkspaceUtils'; import { formatLabelKey } from '~/shared/utilities/WorkspaceUtils';
@ -16,7 +16,7 @@ type WorkspaceFormImageDetailsProps = {
export const WorkspaceFormImageDetails: React.FunctionComponent<WorkspaceFormImageDetailsProps> = ({ export const WorkspaceFormImageDetails: React.FunctionComponent<WorkspaceFormImageDetailsProps> = ({
workspaceImage, workspaceImage,
}) => ( }) => (
<div style={{ marginLeft: 'var(--pf-t--global--spacer--md)' }}> <>
{workspaceImage && ( {workspaceImage && (
<> <>
<Title headingLevel="h3">{workspaceImage.displayName}</Title> <Title headingLevel="h3">{workspaceImage.displayName}</Title>
@ -38,5 +38,5 @@ export const WorkspaceFormImageDetails: React.FunctionComponent<WorkspaceFormIma
))} ))}
</> </>
)} )}
</div> </>
); );

View File

@ -1,14 +1,13 @@
import React, { useCallback, useMemo, useRef, useState } from 'react'; import React, { useCallback, useMemo, useRef, useState } from 'react';
import { import {
CardTitle, CardTitle,
Gallery,
PageSection,
Toolbar,
ToolbarContent,
Card, Card,
CardHeader, CardHeader,
CardBody, CardBody,
} from '@patternfly/react-core'; } from '@patternfly/react-core/dist/esm/components/Card';
import { Gallery } from '@patternfly/react-core/dist/esm/layouts/Gallery';
import { PageSection } from '@patternfly/react-core/dist/esm/components/Page';
import { Toolbar, ToolbarContent } from '@patternfly/react-core/dist/esm/components/Toolbar';
import Filter, { FilteredColumn, FilterRef } from '~/shared/components/Filter'; import Filter, { FilteredColumn, FilterRef } from '~/shared/components/Filter';
import { WorkspaceImageConfigValue } from '~/shared/api/backendApiTypes'; import { WorkspaceImageConfigValue } from '~/shared/api/backendApiTypes';
import CustomEmptyState from '~/shared/components/CustomEmptyState'; import CustomEmptyState from '~/shared/components/CustomEmptyState';

View File

@ -1,10 +1,9 @@
import React, { useCallback, useMemo, useRef, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { Content, Split, SplitItem } from '@patternfly/react-core'; import { Content } from '@patternfly/react-core/dist/esm/components/Content';
import { WorkspaceFormImageDetails } from '~/app/pages/Workspaces/Form/image/WorkspaceFormImageDetails'; import { Split, SplitItem } from '@patternfly/react-core/dist/esm/layouts/Split';
import { WorkspaceFormImageList } from '~/app/pages/Workspaces/Form/image/WorkspaceFormImageList'; import { WorkspaceFormImageList } from '~/app/pages/Workspaces/Form/image/WorkspaceFormImageList';
import { FilterByLabels } from '~/app/pages/Workspaces/Form/labelFilter/FilterByLabels'; import { FilterByLabels } from '~/app/pages/Workspaces/Form/labelFilter/FilterByLabels';
import { WorkspaceImageConfigValue } from '~/shared/api/backendApiTypes'; import { WorkspaceImageConfigValue } from '~/shared/api/backendApiTypes';
import { WorkspaceFormDrawer } from '~/app/pages/Workspaces/Form/WorkspaceFormDrawer';
interface WorkspaceFormImageSelectionProps { interface WorkspaceFormImageSelectionProps {
images: WorkspaceImageConfigValue[]; images: WorkspaceImageConfigValue[];
@ -18,26 +17,6 @@ const WorkspaceFormImageSelection: React.FunctionComponent<WorkspaceFormImageSel
onSelect, onSelect,
}) => { }) => {
const [selectedLabels, setSelectedLabels] = useState<Map<string, Set<string>>>(new Map()); const [selectedLabels, setSelectedLabels] = useState<Map<string, Set<string>>>(new Map());
const [isExpanded, setIsExpanded] = useState(false);
const drawerRef = useRef<HTMLSpanElement>(undefined);
const onExpand = useCallback(() => {
if (drawerRef.current) {
drawerRef.current.focus();
}
}, []);
const onClick = useCallback(
(image?: WorkspaceImageConfigValue) => {
setIsExpanded(true);
onSelect(image);
},
[onSelect],
);
const onCloseClick = useCallback(() => {
setIsExpanded(false);
}, []);
const imageFilterContent = useMemo( const imageFilterContent = useMemo(
() => ( () => (
@ -50,32 +29,19 @@ const WorkspaceFormImageSelection: React.FunctionComponent<WorkspaceFormImageSel
[images, selectedLabels, setSelectedLabels], [images, selectedLabels, setSelectedLabels],
); );
const imageDetailsContent = useMemo(
() => <WorkspaceFormImageDetails workspaceImage={selectedImage} />,
[selectedImage],
);
return ( return (
<Content style={{ height: '100%' }}> <Content style={{ height: '100%' }}>
<WorkspaceFormDrawer <Split hasGutter>
title="Image" <SplitItem style={{ minWidth: '200px' }}>{imageFilterContent}</SplitItem>
info={imageDetailsContent} <SplitItem isFilled>
isExpanded={isExpanded} <WorkspaceFormImageList
onCloseClick={onCloseClick} images={images}
onExpand={onExpand} selectedLabels={selectedLabels}
> selectedImage={selectedImage}
<Split hasGutter> onSelect={onSelect}
<SplitItem style={{ minWidth: '200px' }}>{imageFilterContent}</SplitItem> />
<SplitItem isFilled> </SplitItem>
<WorkspaceFormImageList </Split>
images={images}
selectedLabels={selectedLabels}
selectedImage={selectedImage}
onSelect={onClick}
/>
</SplitItem>
</Split>
</WorkspaceFormDrawer>
</Content> </Content>
); );
}; };

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Title } from '@patternfly/react-core'; import { Title } from '@patternfly/react-core/dist/esm/components/Title';
import { WorkspaceKind } from '~/shared/api/backendApiTypes'; import { WorkspaceKind } from '~/shared/api/backendApiTypes';
type WorkspaceFormKindDetailsProps = { type WorkspaceFormKindDetailsProps = {
@ -9,12 +9,12 @@ type WorkspaceFormKindDetailsProps = {
export const WorkspaceFormKindDetails: React.FunctionComponent<WorkspaceFormKindDetailsProps> = ({ export const WorkspaceFormKindDetails: React.FunctionComponent<WorkspaceFormKindDetailsProps> = ({
workspaceKind, workspaceKind,
}) => ( }) => (
<div style={{ marginLeft: 'var(--pf-t--global--spacer--md)' }}> <>
{workspaceKind && ( {workspaceKind && (
<> <>
<Title headingLevel="h3">{workspaceKind.displayName}</Title> <Title headingLevel="h3">{workspaceKind.displayName}</Title>
<p>{workspaceKind.description}</p> <p>{workspaceKind.description}</p>
</> </>
)} )}
</div> </>
); );

View File

@ -2,13 +2,12 @@ import React, { useCallback, useMemo, useRef, useState } from 'react';
import { import {
CardBody, CardBody,
CardTitle, CardTitle,
Gallery,
PageSection,
Toolbar,
ToolbarContent,
Card, Card,
CardHeader, CardHeader,
} from '@patternfly/react-core'; } from '@patternfly/react-core/dist/esm/components/Card';
import { Gallery } from '@patternfly/react-core/dist/esm/layouts/Gallery';
import { PageSection } from '@patternfly/react-core/dist/esm/components/Page';
import { Toolbar, ToolbarContent } from '@patternfly/react-core/dist/esm/components/Toolbar';
import { WorkspaceKind } from '~/shared/api/backendApiTypes'; import { WorkspaceKind } from '~/shared/api/backendApiTypes';
import Filter, { FilteredColumn, FilterRef } from '~/shared/components/Filter'; import Filter, { FilteredColumn, FilterRef } from '~/shared/components/Filter';
import CustomEmptyState from '~/shared/components/CustomEmptyState'; import CustomEmptyState from '~/shared/components/CustomEmptyState';

View File

@ -1,10 +1,8 @@
import React, { useCallback, useMemo, useRef, useState } from 'react'; import React from 'react';
import { Content } from '@patternfly/react-core'; import { Content } from '@patternfly/react-core/dist/esm/components/Content';
import { WorkspaceKind } from '~/shared/api/backendApiTypes'; import { WorkspaceKind } from '~/shared/api/backendApiTypes';
import useWorkspaceKinds from '~/app/hooks/useWorkspaceKinds'; import useWorkspaceKinds from '~/app/hooks/useWorkspaceKinds';
import { WorkspaceFormKindDetails } from '~/app/pages/Workspaces/Form/kind/WorkspaceFormKindDetails';
import { WorkspaceFormKindList } from '~/app/pages/Workspaces/Form/kind/WorkspaceFormKindList'; import { WorkspaceFormKindList } from '~/app/pages/Workspaces/Form/kind/WorkspaceFormKindList';
import { WorkspaceFormDrawer } from '~/app/pages/Workspaces/Form/WorkspaceFormDrawer';
interface WorkspaceFormKindSelectionProps { interface WorkspaceFormKindSelectionProps {
selectedKind: WorkspaceKind | undefined; selectedKind: WorkspaceKind | undefined;
@ -16,31 +14,6 @@ const WorkspaceFormKindSelection: React.FunctionComponent<WorkspaceFormKindSelec
onSelect, onSelect,
}) => { }) => {
const [workspaceKinds, loaded, error] = useWorkspaceKinds(); const [workspaceKinds, loaded, error] = useWorkspaceKinds();
const [isExpanded, setIsExpanded] = useState(false);
const drawerRef = useRef<HTMLSpanElement>(undefined);
const onExpand = useCallback(() => {
if (drawerRef.current) {
drawerRef.current.focus();
}
}, []);
const onClick = useCallback(
(kind?: WorkspaceKind) => {
setIsExpanded(true);
onSelect(kind);
},
[onSelect],
);
const onCloseClick = useCallback(() => {
setIsExpanded(false);
}, []);
const kindDetailsContent = useMemo(
() => <WorkspaceFormKindDetails workspaceKind={selectedKind} />,
[selectedKind],
);
if (error) { if (error) {
return <p>Error loading workspace kinds: {error.message}</p>; // TODO: UX for error state return <p>Error loading workspace kinds: {error.message}</p>; // TODO: UX for error state
@ -52,19 +25,11 @@ const WorkspaceFormKindSelection: React.FunctionComponent<WorkspaceFormKindSelec
return ( return (
<Content style={{ height: '100%' }}> <Content style={{ height: '100%' }}>
<WorkspaceFormDrawer <WorkspaceFormKindList
title="Workspace kind" allWorkspaceKinds={workspaceKinds}
info={kindDetailsContent} selectedKind={selectedKind}
isExpanded={isExpanded} onSelect={onSelect}
onCloseClick={onCloseClick} />
onExpand={onExpand}
>
<WorkspaceFormKindList
allWorkspaceKinds={workspaceKinds}
selectedKind={selectedKind}
onSelect={onClick}
/>
</WorkspaceFormDrawer>
</Content> </Content>
); );
}; };

View File

@ -4,9 +4,9 @@ import {
DescriptionListTerm, DescriptionListTerm,
DescriptionListGroup, DescriptionListGroup,
DescriptionListDescription, DescriptionListDescription,
Title, } from '@patternfly/react-core/dist/esm/components/DescriptionList';
Divider, import { Title } from '@patternfly/react-core/dist/esm/components/Title';
} from '@patternfly/react-core'; import { Divider } from '@patternfly/react-core/dist/esm/components/Divider';
import { WorkspacePodConfigValue } from '~/shared/api/backendApiTypes'; import { WorkspacePodConfigValue } from '~/shared/api/backendApiTypes';
import { formatLabelKey } from '~/shared/utilities/WorkspaceUtils'; import { formatLabelKey } from '~/shared/utilities/WorkspaceUtils';
@ -19,10 +19,12 @@ export const WorkspaceFormPodConfigDetails: React.FunctionComponent<
> = ({ workspacePodConfig }) => ( > = ({ workspacePodConfig }) => (
<> <>
{workspacePodConfig && ( {workspacePodConfig && (
<div style={{ marginLeft: 'var(--pf-t--global--spacer--md)' }}> <>
<Title headingLevel="h3">{workspacePodConfig.displayName}</Title>{' '} <Title headingLevel="h3">{workspacePodConfig.displayName}</Title>{' '}
<p>{workspacePodConfig.description}</p> <p>{workspacePodConfig.description}</p>
<br />
<Divider /> <Divider />
<br />
{workspacePodConfig.labels.map((label) => ( {workspacePodConfig.labels.map((label) => (
<DescriptionList <DescriptionList
key={label.key} key={label.key}
@ -37,7 +39,7 @@ export const WorkspaceFormPodConfigDetails: React.FunctionComponent<
</DescriptionListGroup> </DescriptionListGroup>
</DescriptionList> </DescriptionList>
))} ))}
</div> </>
)} )}
</> </>
); );

View File

@ -1,14 +1,13 @@
import React, { useCallback, useMemo, useRef, useState } from 'react'; import React, { useCallback, useMemo, useRef, useState } from 'react';
import { import {
CardTitle, CardTitle,
Gallery,
PageSection,
Toolbar,
ToolbarContent,
Card, Card,
CardHeader, CardHeader,
CardBody, CardBody,
} from '@patternfly/react-core'; } from '@patternfly/react-core/dist/esm/components/Card';
import { Gallery } from '@patternfly/react-core/dist/esm/layouts/Gallery';
import { PageSection } from '@patternfly/react-core/dist/esm/components/Page';
import { Toolbar, ToolbarContent } from '@patternfly/react-core/dist/esm/components/Toolbar';
import { WorkspacePodConfigValue } from '~/shared/api/backendApiTypes'; import { WorkspacePodConfigValue } from '~/shared/api/backendApiTypes';
import Filter, { FilteredColumn, FilterRef } from '~/shared/components/Filter'; import Filter, { FilteredColumn, FilterRef } from '~/shared/components/Filter';
import CustomEmptyState from '~/shared/components/CustomEmptyState'; import CustomEmptyState from '~/shared/components/CustomEmptyState';

View File

@ -1,9 +1,8 @@
import React, { useCallback, useMemo, useRef, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { Content, Split, SplitItem } from '@patternfly/react-core'; import { Content } from '@patternfly/react-core/dist/esm/components/Content';
import { WorkspaceFormPodConfigDetails } from '~/app/pages/Workspaces/Form/podConfig/WorkspaceFormPodConfigDetails'; import { Split, SplitItem } from '@patternfly/react-core/dist/esm/layouts/Split';
import { WorkspaceFormPodConfigList } from '~/app/pages/Workspaces/Form/podConfig/WorkspaceFormPodConfigList'; import { WorkspaceFormPodConfigList } from '~/app/pages/Workspaces/Form/podConfig/WorkspaceFormPodConfigList';
import { FilterByLabels } from '~/app/pages/Workspaces/Form/labelFilter/FilterByLabels'; import { FilterByLabels } from '~/app/pages/Workspaces/Form/labelFilter/FilterByLabels';
import { WorkspaceFormDrawer } from '~/app/pages/Workspaces/Form/WorkspaceFormDrawer';
import { WorkspacePodConfigValue } from '~/shared/api/backendApiTypes'; import { WorkspacePodConfigValue } from '~/shared/api/backendApiTypes';
interface WorkspaceFormPodConfigSelectionProps { interface WorkspaceFormPodConfigSelectionProps {
@ -16,26 +15,6 @@ const WorkspaceFormPodConfigSelection: React.FunctionComponent<
WorkspaceFormPodConfigSelectionProps WorkspaceFormPodConfigSelectionProps
> = ({ podConfigs, selectedPodConfig, onSelect }) => { > = ({ podConfigs, selectedPodConfig, onSelect }) => {
const [selectedLabels, setSelectedLabels] = useState<Map<string, Set<string>>>(new Map()); const [selectedLabels, setSelectedLabels] = useState<Map<string, Set<string>>>(new Map());
const [isExpanded, setIsExpanded] = useState(false);
const drawerRef = useRef<HTMLSpanElement>(undefined);
const onExpand = useCallback(() => {
if (drawerRef.current) {
drawerRef.current.focus();
}
}, []);
const onClick = useCallback(
(podConfig?: WorkspacePodConfigValue) => {
setIsExpanded(true);
onSelect(podConfig);
},
[onSelect],
);
const onCloseClick = useCallback(() => {
setIsExpanded(false);
}, []);
const podConfigFilterContent = useMemo( const podConfigFilterContent = useMemo(
() => ( () => (
@ -48,32 +27,19 @@ const WorkspaceFormPodConfigSelection: React.FunctionComponent<
[podConfigs, selectedLabels, setSelectedLabels], [podConfigs, selectedLabels, setSelectedLabels],
); );
const podConfigDetailsContent = useMemo(
() => <WorkspaceFormPodConfigDetails workspacePodConfig={selectedPodConfig} />,
[selectedPodConfig],
);
return ( return (
<Content style={{ height: '100%' }}> <Content style={{ height: '100%' }}>
<WorkspaceFormDrawer <Split hasGutter>
title="Pod config" <SplitItem style={{ minWidth: '200px' }}>{podConfigFilterContent}</SplitItem>
info={podConfigDetailsContent} <SplitItem isFilled>
isExpanded={isExpanded} <WorkspaceFormPodConfigList
onCloseClick={onCloseClick} podConfigs={podConfigs}
onExpand={onExpand} selectedLabels={selectedLabels}
> selectedPodConfig={selectedPodConfig}
<Split hasGutter> onSelect={onSelect}
<SplitItem style={{ minWidth: '200px' }}>{podConfigFilterContent}</SplitItem> />
<SplitItem isFilled> </SplitItem>
<WorkspaceFormPodConfigList </Split>
podConfigs={podConfigs}
selectedLabels={selectedLabels}
selectedPodConfig={selectedPodConfig}
onSelect={onClick}
/>
</SplitItem>
</Split>
</WorkspaceFormDrawer>
</Content> </Content>
); );
}; };

View File

@ -1,23 +1,29 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { EllipsisVIcon } from '@patternfly/react-icons'; import { EllipsisVIcon } from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon';
import { Table, Thead, Tbody, Tr, Th, Td, TableVariant } from '@patternfly/react-table'; import {
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableVariant,
} from '@patternfly/react-table/dist/esm/components/Table';
import { Button } from '@patternfly/react-core/dist/esm/components/Button';
import { import {
Button,
Modal, Modal,
ModalVariant,
TextInput,
Dropdown,
DropdownItem,
MenuToggle,
ModalBody, ModalBody,
ModalFooter, ModalFooter,
Form,
FormGroup,
ModalHeader, ModalHeader,
ValidatedOptions, ModalVariant,
HelperText, } from '@patternfly/react-core/dist/esm/components/Modal';
HelperTextItem, import { ValidatedOptions } from '@patternfly/react-core/helpers';
} from '@patternfly/react-core'; import { TextInput } from '@patternfly/react-core/dist/esm/components/TextInput';
import { Dropdown, DropdownItem } from '@patternfly/react-core/dist/esm/components/Dropdown';
import { MenuToggle } from '@patternfly/react-core/dist/esm/components/MenuToggle';
import { Form, FormGroup } from '@patternfly/react-core/dist/esm/components/Form';
import { HelperText, HelperTextItem } from '@patternfly/react-core/dist/esm/components/HelperText';
import { PlusCircleIcon } from '@patternfly/react-icons/dist/esm/icons/plus-circle-icon';
import { WorkspacePodSecretMount } from '~/shared/api/backendApiTypes'; import { WorkspacePodSecretMount } from '~/shared/api/backendApiTypes';
interface WorkspaceFormPropertiesSecretsProps { interface WorkspaceFormPropertiesSecretsProps {
@ -152,7 +158,12 @@ export const WorkspaceFormPropertiesSecrets: React.FC<WorkspaceFormPropertiesSec
</Tbody> </Tbody>
</Table> </Table>
)} )}
<Button variant="primary" onClick={() => setIsModalOpen(true)} style={{ marginTop: '1rem' }}> <Button
variant="primary"
icon={<PlusCircleIcon />}
onClick={() => setIsModalOpen(true)}
style={{ marginTop: '1rem', width: 'fit-content' }}
>
Create Secret Create Secret
</Button> </Button>
<Modal isOpen={isModalOpen} onClose={clearForm} variant={ModalVariant.small}> <Modal isOpen={isModalOpen} onClose={clearForm} variant={ModalVariant.small}>

View File

@ -1,15 +1,11 @@
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { import { Checkbox } from '@patternfly/react-core/dist/esm/components/Checkbox';
Checkbox, import { Content } from '@patternfly/react-core/dist/esm/components/Content';
Content, import { Divider } from '@patternfly/react-core/dist/esm/components/Divider';
Divider, import { ExpandableSection } from '@patternfly/react-core/dist/esm/components/ExpandableSection';
ExpandableSection, import { Form, FormGroup } from '@patternfly/react-core/dist/esm/components/Form';
Form, import { Split, SplitItem } from '@patternfly/react-core/dist/esm/layouts/Split';
FormGroup, import { TextInput } from '@patternfly/react-core/dist/esm/components/TextInput';
Split,
SplitItem,
TextInput,
} from '@patternfly/react-core';
import { WorkspaceFormImageDetails } from '~/app/pages/Workspaces/Form/image/WorkspaceFormImageDetails'; import { WorkspaceFormImageDetails } from '~/app/pages/Workspaces/Form/image/WorkspaceFormImageDetails';
import { WorkspaceFormPropertiesVolumes } from '~/app/pages/Workspaces/Form/properties/WorkspaceFormPropertiesVolumes'; import { WorkspaceFormPropertiesVolumes } from '~/app/pages/Workspaces/Form/properties/WorkspaceFormPropertiesVolumes';
import { WorkspaceFormProperties } from '~/app/types'; import { WorkspaceFormProperties } from '~/app/types';

View File

@ -1,21 +1,28 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { Button } from '@patternfly/react-core/dist/esm/components/Button';
import { Dropdown, DropdownItem } from '@patternfly/react-core/dist/esm/components/Dropdown';
import { Form, FormGroup } from '@patternfly/react-core/dist/esm/components/Form';
import { MenuToggle } from '@patternfly/react-core/dist/esm/components/MenuToggle';
import { import {
Button,
Dropdown,
DropdownItem,
Form,
FormGroup,
MenuToggle,
Modal, Modal,
ModalBody, ModalBody,
ModalFooter, ModalFooter,
ModalHeader, ModalHeader,
ModalVariant, ModalVariant,
Switch, } from '@patternfly/react-core/dist/esm/components/Modal';
TextInput, import { Switch } from '@patternfly/react-core/dist/esm/components/Switch';
} from '@patternfly/react-core'; import { TextInput } from '@patternfly/react-core/dist/esm/components/TextInput';
import { EllipsisVIcon } from '@patternfly/react-icons'; import { EllipsisVIcon } from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon';
import { Table, TableVariant, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; import {
Table,
TableVariant,
Tbody,
Td,
Th,
Thead,
Tr,
} from '@patternfly/react-table/dist/esm/components/Table';
import { PlusCircleIcon } from '@patternfly/react-icons/dist/esm/icons/plus-circle-icon';
import { WorkspacePodVolumeMount } from '~/shared/api/backendApiTypes'; import { WorkspacePodVolumeMount } from '~/shared/api/backendApiTypes';
interface WorkspaceFormPropertiesVolumesProps { interface WorkspaceFormPropertiesVolumesProps {
@ -126,9 +133,10 @@ export const WorkspaceFormPropertiesVolumes: React.FC<WorkspaceFormPropertiesVol
</Table> </Table>
)} )}
<Button <Button
variant="primary" variant="link"
icon={<PlusCircleIcon />}
onClick={() => setIsModalOpen(true)} onClick={() => setIsModalOpen(true)}
style={{ marginTop: '1rem' }} style={{ marginTop: '1rem', width: 'fit-content' }}
className="pf-u-mt-md" className="pf-u-mt-md"
> >
Create Volume Create Volume

View File

@ -4,7 +4,7 @@ import {
DescriptionListTerm, DescriptionListTerm,
DescriptionListGroup, DescriptionListGroup,
DescriptionListDescription, DescriptionListDescription,
} from '@patternfly/react-core'; } from '@patternfly/react-core/dist/esm/components/DescriptionList';
import { Workspace } from '~/shared/api/backendApiTypes'; import { Workspace } from '~/shared/api/backendApiTypes';
import { formatResourceFromWorkspace } from '~/shared/utilities/WorkspaceUtils'; import { formatResourceFromWorkspace } from '~/shared/utilities/WorkspaceUtils';

View File

@ -3,10 +3,12 @@ import {
Dropdown, Dropdown,
DropdownItem, DropdownItem,
DropdownList, DropdownList,
} from '@patternfly/react-core/dist/esm/components/Dropdown';
import {
MenuToggle, MenuToggle,
MenuToggleElement, MenuToggleElement,
MenuToggleAction, MenuToggleAction,
} from '@patternfly/react-core'; } from '@patternfly/react-core/dist/esm/components/MenuToggle';
import { Workspace, WorkspaceState } from '~/shared/api/backendApiTypes'; import { Workspace, WorkspaceState } from '~/shared/api/backendApiTypes';
type WorkspaceConnectActionProps = { type WorkspaceConnectActionProps = {

View File

@ -3,10 +3,9 @@ import {
DescriptionList, DescriptionList,
DescriptionListTerm, DescriptionListTerm,
DescriptionListDescription, DescriptionListDescription,
ListItem,
List,
DescriptionListGroup, DescriptionListGroup,
} from '@patternfly/react-core'; } from '@patternfly/react-core/dist/esm/components/DescriptionList';
import { ListItem, List } from '@patternfly/react-core/dist/esm/components/List';
import { Workspace } from '~/shared/api/backendApiTypes'; import { Workspace } from '~/shared/api/backendApiTypes';
import { extractPackageLabels, formatLabelKey } from '~/shared/utilities/WorkspaceUtils'; import { extractPackageLabels, formatLabelKey } from '~/shared/utilities/WorkspaceUtils';

View File

@ -4,7 +4,7 @@ import {
DescriptionListTerm, DescriptionListTerm,
DescriptionListGroup, DescriptionListGroup,
DescriptionListDescription, DescriptionListDescription,
} from '@patternfly/react-core'; } from '@patternfly/react-core/dist/esm/components/DescriptionList';
import { Workspace } from '~/shared/api/backendApiTypes'; import { Workspace } from '~/shared/api/backendApiTypes';
import { DataVolumesList } from '~/app/pages/Workspaces/DataVolumesList'; import { DataVolumesList } from '~/app/pages/Workspaces/DataVolumesList';

View File

@ -1,5 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import { Content, ContentVariants, PageSection, Stack, StackItem } from '@patternfly/react-core'; import { Content, ContentVariants } from '@patternfly/react-core/dist/esm/components/Content';
import { PageSection } from '@patternfly/react-core/dist/esm/components/Page';
import { Stack, StackItem } from '@patternfly/react-core/dist/esm/layouts/Stack';
import WorkspaceTable from '~/app/components/WorkspaceTable'; import WorkspaceTable from '~/app/components/WorkspaceTable';
import { useNamespaceContext } from '~/app/context/NamespaceContextProvider'; import { useNamespaceContext } from '~/app/context/NamespaceContextProvider';
import { useWorkspacesByNamespace } from '~/app/hooks/useWorkspaces'; import { useWorkspacesByNamespace } from '~/app/hooks/useWorkspaces';
@ -52,7 +54,7 @@ export const Workspaces: React.FunctionComponent = () => {
<PageSection isFilled> <PageSection isFilled>
<Stack hasGutter> <Stack hasGutter>
<StackItem> <StackItem>
<Content component={ContentVariants.h1}>Kubeflow Workspaces</Content> <Content component={ContentVariants.h1}>Workspaces</Content>
</StackItem> </StackItem>
<StackItem> <StackItem>
<Content component={ContentVariants.p}> <Content component={ContentVariants.p}>

View File

@ -1,10 +1,11 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { ExpandableSection, Icon, Tab, Tabs, TabTitleText, Content } from '@patternfly/react-core'; import { ExpandableSection } from '@patternfly/react-core/dist/esm/components/ExpandableSection';
import { import { Icon } from '@patternfly/react-core/dist/esm/components/Icon';
ExclamationCircleIcon, import { Tab, Tabs, TabTitleText } from '@patternfly/react-core/dist/esm/components/Tabs';
ExclamationTriangleIcon, import { Content } from '@patternfly/react-core/dist/esm/components/Content';
InfoCircleIcon, import { ExclamationCircleIcon } from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon';
} from '@patternfly/react-icons'; import { ExclamationTriangleIcon } from '@patternfly/react-icons/dist/esm/icons/exclamation-triangle-icon';
import { InfoCircleIcon } from '@patternfly/react-icons/dist/esm/icons/info-circle-icon';
import useWorkspaceKindByName from '~/app/hooks/useWorkspaceKindByName'; import useWorkspaceKindByName from '~/app/hooks/useWorkspaceKindByName';
import { WorkspaceKind } from '~/shared/api/backendApiTypes'; import { WorkspaceKind } from '~/shared/api/backendApiTypes';
@ -92,7 +93,7 @@ export const WorkspaceRedirectInformationView: React.FC<WorkspaceRedirectInforma
eventKey={0} eventKey={0}
title={ title={
<TabTitleText> <TabTitleText>
Image Config {getLevelIcon(getMaxLevel(imageConfigRedirects))} Image config {getLevelIcon(getMaxLevel(imageConfigRedirects))}
</TabTitleText> </TabTitleText>
} }
> >
@ -110,7 +111,7 @@ export const WorkspaceRedirectInformationView: React.FC<WorkspaceRedirectInforma
<Tab <Tab
eventKey={1} eventKey={1}
title={ title={
<TabTitleText>Pod Config {getLevelIcon(getMaxLevel(podConfigRedirects))}</TabTitleText> <TabTitleText>Pod config {getLevelIcon(getMaxLevel(podConfigRedirects))}</TabTitleText>
} }
> >
{podConfigRedirects.map((redirect, index) => ( {podConfigRedirects.map((redirect, index) => (

View File

@ -1,13 +1,13 @@
import React from 'react'; import React from 'react';
import { Button } from '@patternfly/react-core/dist/esm/components/Button';
import { Content } from '@patternfly/react-core/dist/esm/components/Content';
import { import {
Button,
Content,
Modal, Modal,
ModalBody, ModalBody,
ModalFooter, ModalFooter,
ModalHeader, ModalHeader,
TabTitleText, } from '@patternfly/react-core/dist/esm/components/Modal';
} from '@patternfly/react-core'; import { TabTitleText } from '@patternfly/react-core/dist/esm/components/Tabs';
import { Workspace } from '~/shared/api/backendApiTypes'; import { Workspace } from '~/shared/api/backendApiTypes';
import { WorkspaceRedirectInformationView } from '~/app/pages/Workspaces/workspaceActions/WorkspaceRedirectInformationView'; import { WorkspaceRedirectInformationView } from '~/app/pages/Workspaces/workspaceActions/WorkspaceRedirectInformationView';
@ -38,7 +38,7 @@ export const WorkspaceRestartActionModal: React.FC<RestartActionAlertProps> = ({
aria-labelledby="title-icon-modal-title" aria-labelledby="title-icon-modal-title"
onClose={onClose} onClose={onClose}
> >
<ModalHeader title="Restart Workspace" /> <ModalHeader title="Restart workspace" />
<ModalBody> <ModalBody>
{workspacePendingUpdate ? ( {workspacePendingUpdate ? (
<> <>
@ -54,7 +54,7 @@ export const WorkspaceRestartActionModal: React.FC<RestartActionAlertProps> = ({
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
{workspacePendingUpdate && ( {workspacePendingUpdate && (
<Button onClick={() => handleClick(true)}>Update and Restart</Button> <Button onClick={() => handleClick(true)}>Update and restart</Button>
)} )}
<Button <Button
onClick={() => handleClick(false)} onClick={() => handleClick(false)}

View File

@ -1,12 +1,12 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { Button } from '@patternfly/react-core/dist/esm/components/Button';
import { import {
Button,
Modal, Modal,
ModalBody, ModalBody,
ModalFooter, ModalFooter,
ModalHeader, ModalHeader,
TabTitleText, } from '@patternfly/react-core/dist/esm/components/Modal';
} from '@patternfly/react-core'; import { TabTitleText } from '@patternfly/react-core/dist/esm/components/Tabs';
import { WorkspaceRedirectInformationView } from '~/app/pages/Workspaces/workspaceActions/WorkspaceRedirectInformationView'; import { WorkspaceRedirectInformationView } from '~/app/pages/Workspaces/workspaceActions/WorkspaceRedirectInformationView';
import { Workspace, WorkspacePauseState } from '~/shared/api/backendApiTypes'; import { Workspace, WorkspacePauseState } from '~/shared/api/backendApiTypes';
import { ActionButton } from '~/shared/components/ActionButton'; import { ActionButton } from '~/shared/components/ActionButton';

View File

@ -1,13 +1,13 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { Button } from '@patternfly/react-core/dist/esm/components/Button';
import { Content } from '@patternfly/react-core/dist/esm/components/Content';
import { import {
Button,
Content,
Modal, Modal,
ModalBody, ModalBody,
ModalFooter, ModalFooter,
ModalHeader, ModalHeader,
TabTitleText, } from '@patternfly/react-core/dist/esm/components/Modal';
} from '@patternfly/react-core'; import { TabTitleText } from '@patternfly/react-core/dist/esm/components/Tabs';
import { WorkspaceRedirectInformationView } from '~/app/pages/Workspaces/workspaceActions/WorkspaceRedirectInformationView'; import { WorkspaceRedirectInformationView } from '~/app/pages/Workspaces/workspaceActions/WorkspaceRedirectInformationView';
import { Workspace, WorkspacePauseState } from '~/shared/api/backendApiTypes'; import { Workspace, WorkspacePauseState } from '~/shared/api/backendApiTypes';
import { ActionButton } from '~/shared/components/ActionButton'; import { ActionButton } from '~/shared/components/ActionButton';
@ -86,7 +86,7 @@ export const WorkspaceStopActionModal: React.FC<StopActionAlertProps> = ({
aria-labelledby="title-icon-modal-title" aria-labelledby="title-icon-modal-title"
onClose={onClose} onClose={onClose}
> >
<ModalHeader title="Stop Workspace" /> <ModalHeader title="Stop workspace" />
<ModalBody> <ModalBody>
{workspacePendingUpdate ? ( {workspacePendingUpdate ? (
<> <>
@ -103,11 +103,11 @@ export const WorkspaceStopActionModal: React.FC<StopActionAlertProps> = ({
<ModalFooter> <ModalFooter>
{shouldShowActionButton('updateAndStop') && workspacePendingUpdate && ( {shouldShowActionButton('updateAndStop') && workspacePendingUpdate && (
<ActionButton <ActionButton
action="Update and Stop" action="Update and stop"
titleOnLoading="Stopping ..." titleOnLoading="Stopping ..."
onClick={() => handleUpdateAndStop()} onClick={() => handleUpdateAndStop()}
> >
Update and Stop Update and stop
</ActionButton> </ActionButton>
)} )}

View File

@ -1,12 +1,12 @@
import React from 'react'; import React from 'react';
import { ExclamationTriangleIcon } from '@patternfly/react-icons'; import { ExclamationTriangleIcon } from '@patternfly/react-icons/dist/esm/icons/exclamation-triangle-icon';
import { Button } from '@patternfly/react-core/dist/esm/components/Button';
import { import {
Button,
EmptyState, EmptyState,
EmptyStateBody, EmptyStateBody,
EmptyStateFooter, EmptyStateFooter,
PageSection, } from '@patternfly/react-core/dist/esm/components/EmptyState';
} from '@patternfly/react-core'; import { PageSection } from '@patternfly/react-core/dist/esm/components/Page';
import { useTypedNavigate } from '~/app/routerHelper'; import { useTypedNavigate } from '~/app/routerHelper';
const NotFound: React.FunctionComponent = () => { const NotFound: React.FunctionComponent = () => {

View File

@ -6,6 +6,8 @@ import {
WorkspacePodSecretMount, WorkspacePodSecretMount,
Workspace, Workspace,
WorkspaceImageRef, WorkspaceImageRef,
WorkspacePodVolumeMounts,
WorkspaceKindPodMetadata,
} from '~/shared/api/backendApiTypes'; } from '~/shared/api/backendApiTypes';
export interface WorkspaceColumnDefinition { export interface WorkspaceColumnDefinition {
@ -54,9 +56,9 @@ export interface WorkspaceKindProperties {
} }
export interface WorkspaceKindImageConfigValue extends WorkspaceImageConfigValue { export interface WorkspaceKindImageConfigValue extends WorkspaceImageConfigValue {
imagePullPolicy: ImagePullPolicy.IfNotPresent | ImagePullPolicy.Always | ImagePullPolicy.Never; imagePullPolicy?: ImagePullPolicy.IfNotPresent | ImagePullPolicy.Always | ImagePullPolicy.Never;
ports: WorkspaceKindImagePort[]; ports?: WorkspaceKindImagePort[];
image: string; image?: string;
} }
export enum ImagePullPolicy { export enum ImagePullPolicy {
@ -92,9 +94,26 @@ export interface WorkspaceKindPodConfigData {
default: string; default: string;
values: WorkspaceKindPodConfigValue[]; values: WorkspaceKindPodConfigValue[];
} }
export interface WorkspaceKindPodCulling {
enabled: boolean;
maxInactiveSeconds: number;
activityProbe: {
jupyter: {
lastActivity: boolean;
};
};
}
export interface WorkspaceKindPodTemplateData {
podMetadata: WorkspaceKindPodMetadata;
volumeMounts: WorkspacePodVolumeMounts;
culling?: WorkspaceKindPodCulling;
extraVolumeMounts?: WorkspacePodVolumeMount[];
}
export interface WorkspaceKindFormData { export interface WorkspaceKindFormData {
properties: WorkspaceKindProperties; properties: WorkspaceKindProperties;
imageConfig: WorkspaceKindImageConfigData; imageConfig: WorkspaceKindImageConfigData;
podConfig: WorkspaceKindPodConfigData; podConfig: WorkspaceKindPodConfigData;
podTemplate: WorkspaceKindPodTemplateData;
} }

View File

@ -19,6 +19,7 @@ export const mergeRequestInit = (
type CallRestJSONOptions = { type CallRestJSONOptions = {
queryParams?: Record<string, unknown>; queryParams?: Record<string, unknown>;
parseJSON?: boolean; parseJSON?: boolean;
directYAML?: boolean;
} & EitherOrNone< } & EitherOrNone<
{ {
fileContents: string; fileContents: string;
@ -32,7 +33,7 @@ const callRestJSON = <T>(
host: string, host: string,
path: string, path: string,
requestInit: RequestInit, requestInit: RequestInit,
{ data, fileContents, queryParams, parseJSON = true }: CallRestJSONOptions, { data, fileContents, queryParams, parseJSON = true, directYAML = false }: CallRestJSONOptions,
): Promise<T> => { ): Promise<T> => {
const { method, ...otherOptions } = requestInit; const { method, ...otherOptions } = requestInit;
@ -54,12 +55,17 @@ const callRestJSON = <T>(
let contentType: string | undefined; let contentType: string | undefined;
let formData: FormData | undefined; let formData: FormData | undefined;
if (fileContents) { if (fileContents) {
formData = new FormData(); if (directYAML) {
formData.append( requestData = fileContents;
'uploadfile', contentType = 'application/yaml';
new Blob([fileContents], { type: 'application/x-yaml' }), } else {
'uploadedFile.yml', formData = new FormData();
); formData.append(
'uploadfile',
new Blob([fileContents], { type: 'application/x-yaml' }),
'uploadedFile.yml',
);
}
} else if (data) { } else if (data) {
// It's OK for contentType and requestData to BOTH be undefined for e.g. a GET request or POST with no body. // It's OK for contentType and requestData to BOTH be undefined for e.g. a GET request or POST with no body.
contentType = 'application/json;charset=UTF-8'; contentType = 'application/json;charset=UTF-8';
@ -122,6 +128,7 @@ export const restFILE = <T>(
fileContents, fileContents,
queryParams, queryParams,
parseJSON: options?.parseJSON, parseJSON: options?.parseJSON,
directYAML: options?.directYAML,
}); });
/** POST -- but no body data -- targets simple endpoints */ /** POST -- but no body data -- targets simple endpoints */
@ -173,48 +180,6 @@ export const restDELETE = <T>(
parseJSON: options?.parseJSON, parseJSON: options?.parseJSON,
}); });
/** POST -- but with YAML content directly in body */
export const restYAML = <T>(
host: string,
path: string,
yamlContent: string,
queryParams?: Record<string, unknown>,
options?: APIOptions,
): Promise<T> => {
const { method, ...otherOptions } = mergeRequestInit(options, { method: 'POST' });
const sanitizedQueryParams = queryParams
? Object.entries(queryParams).reduce((acc, [key, value]) => {
if (value) {
return { ...acc, [key]: value };
}
return acc;
}, {})
: null;
const searchParams = sanitizedQueryParams
? new URLSearchParams(sanitizedQueryParams).toString()
: null;
return fetch(`${host}${path}${searchParams ? `?${searchParams}` : ''}`, {
...otherOptions,
headers: {
...otherOptions.headers,
...(DEV_MODE && { [AUTH_HEADER]: localStorage.getItem(AUTH_HEADER) }),
'Content-Type': 'application/vnd.kubeflow-notebooks.manifest+yaml',
},
method,
body: yamlContent,
}).then((response) =>
response.text().then((fetchedData) => {
if (options?.parseJSON !== false) {
return JSON.parse(fetchedData);
}
return fetchedData;
}),
);
};
export const isNotebookResponse = <T>(response: unknown): response is ResponseBody<T> => { export const isNotebookResponse = <T>(response: unknown): response is ResponseBody<T> => {
if (typeof response === 'object' && response !== null) { if (typeof response === 'object' && response !== null) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
@ -233,6 +198,10 @@ export const isErrorEnvelope = (e: unknown): e is ErrorEnvelope =>
typeof (e as { error: { message: unknown } }).error.message === 'string'; typeof (e as { error: { message: unknown } }).error.message === 'string';
export function extractNotebookResponse<T>(response: unknown): T { export function extractNotebookResponse<T>(response: unknown): T {
// Check if this is an error envelope first
if (isErrorEnvelope(response)) {
throw new ErrorEnvelopeException(response);
}
if (isNotebookResponse<T>(response)) { if (isNotebookResponse<T>(response)) {
return response.data; return response.data;
} }
@ -260,6 +229,9 @@ export async function wrapRequest<T>(promise: Promise<T>, extractData = true): P
const res = await handleRestFailures<T>(promise); const res = await handleRestFailures<T>(promise);
return extractData ? extractNotebookResponse<T>(res) : res; return extractData ? extractNotebookResponse<T>(res) : res;
} catch (error) { } catch (error) {
if (error instanceof ErrorEnvelopeException) {
throw error;
}
throw new ErrorEnvelopeException(extractErrorEnvelope(error)); throw new ErrorEnvelopeException(extractErrorEnvelope(error));
} }
} }

View File

@ -1,10 +1,10 @@
import { import {
restCREATE, restCREATE,
restDELETE, restDELETE,
restFILE,
restGET, restGET,
restPATCH, restPATCH,
restUPDATE, restUPDATE,
restYAML,
wrapRequest, wrapRequest,
} from '~/shared/api/apiUtils'; } from '~/shared/api/apiUtils';
import { import {
@ -72,7 +72,7 @@ export const getWorkspaceKind: GetWorkspaceKindAPI = (hostPath) => (opts, kind)
wrapRequest(restGET(hostPath, `/workspacekinds/${kind}`, {}, opts)); wrapRequest(restGET(hostPath, `/workspacekinds/${kind}`, {}, opts));
export const createWorkspaceKind: CreateWorkspaceKindAPI = (hostPath) => (opts, data) => export const createWorkspaceKind: CreateWorkspaceKindAPI = (hostPath) => (opts, data) =>
wrapRequest(restYAML(hostPath, `/workspacekinds`, data, {}, opts)); wrapRequest(restFILE(hostPath, `/workspacekinds`, data, {}, opts));
export const updateWorkspaceKind: UpdateWorkspaceKindAPI = (hostPath) => (opts, kind, data) => export const updateWorkspaceKind: UpdateWorkspaceKindAPI = (hostPath) => (opts, kind, data) =>
wrapRequest(restUPDATE(hostPath, `/workspacekinds/${kind}`, data, {}, opts)); wrapRequest(restUPDATE(hostPath, `/workspacekinds/${kind}`, data, {}, opts));

View File

@ -3,6 +3,7 @@ export type APIOptions = {
signal?: AbortSignal; signal?: AbortSignal;
parseJSON?: boolean; parseJSON?: boolean;
headers?: Record<string, string>; headers?: Record<string, string>;
directYAML?: boolean;
}; };
export type APIState<T> = { export type APIState<T> = {

View File

@ -1,5 +1,5 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { Button } from '@patternfly/react-core'; import { Button } from '@patternfly/react-core/dist/esm/components/Button';
type ActionButtonProps = { type ActionButtonProps = {
action: string; action: string;

View File

@ -4,9 +4,9 @@ import {
EmptyStateBody, EmptyStateBody,
EmptyStateFooter, EmptyStateFooter,
EmptyStateActions, EmptyStateActions,
Button, } from '@patternfly/react-core/dist/esm/components/EmptyState';
} from '@patternfly/react-core'; import { Button } from '@patternfly/react-core/dist/esm/components/Button';
import { SearchIcon } from '@patternfly/react-icons'; import { SearchIcon } from '@patternfly/react-icons/dist/esm/icons/search-icon';
interface CustomEmptyStateProps { interface CustomEmptyStateProps {
onClearFilters: () => void; onClearFilters: () => void;

View File

@ -5,14 +5,12 @@ import {
ModalFooter, ModalFooter,
ModalHeader, ModalHeader,
ModalVariant, ModalVariant,
Button, } from '@patternfly/react-core/dist/esm/components/Modal';
TextInput, import { Button } from '@patternfly/react-core/dist/esm/components/Button';
Stack, import { TextInput } from '@patternfly/react-core/dist/esm/components/TextInput';
StackItem, import { Stack, StackItem } from '@patternfly/react-core/dist/esm/layouts/Stack';
FlexItem, import { FlexItem } from '@patternfly/react-core/dist/esm/layouts/Flex';
HelperText, import { HelperText, HelperTextItem } from '@patternfly/react-core/dist/esm/components/HelperText';
HelperTextItem,
} from '@patternfly/react-core';
import { default as ExclamationCircleIcon } from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon'; import { default as ExclamationCircleIcon } from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon';
import { ActionButton } from '~/shared/components/ActionButton'; import { ActionButton } from '~/shared/components/ActionButton';

View File

@ -11,17 +11,21 @@ import {
MenuContent, MenuContent,
MenuItem, MenuItem,
MenuList, MenuList,
} from '@patternfly/react-core/dist/esm/components/Menu';
import {
MenuToggle, MenuToggle,
MenuToggleElement, MenuToggleElement,
Popper, } from '@patternfly/react-core/dist/esm/components/MenuToggle';
import { Popper } from '@patternfly/react-core/helpers';
import {
Toolbar, Toolbar,
ToolbarContent, ToolbarContent,
ToolbarFilter,
ToolbarGroup, ToolbarGroup,
ToolbarItem, ToolbarItem,
ToolbarFilter,
ToolbarToggleGroup, ToolbarToggleGroup,
} from '@patternfly/react-core'; } from '@patternfly/react-core/dist/esm/components/Toolbar';
import { FilterIcon } from '@patternfly/react-icons'; import { FilterIcon } from '@patternfly/react-icons/dist/esm/icons/filter-icon';
import ThemeAwareSearchInput from '~/app/components/ThemeAwareSearchInput'; import ThemeAwareSearchInput from '~/app/components/ThemeAwareSearchInput';
export interface FilterProps { export interface FilterProps {
@ -61,6 +65,11 @@ const Filter = React.forwardRef<FilterRef, FilterProps>(
[activeFilter.columnKey, columnDefinition], [activeFilter.columnKey, columnDefinition],
); );
const textInputActiveFilterLabel = useMemo(
() => activeFilterLabel.toLowerCase(),
[activeFilterLabel],
);
const handleFilterMenuKeys = useCallback( const handleFilterMenuKeys = useCallback(
(event: KeyboardEvent) => { (event: KeyboardEvent) => {
if (!isFilterMenuOpen) { if (!isFilterMenuOpen) {
@ -170,7 +179,6 @@ const Filter = React.forwardRef<FilterRef, FilterProps>(
columnKey: Object.keys(columnDefinition)[0], columnKey: Object.keys(columnDefinition)[0],
value: '', value: '',
}); });
setFilters([]);
}, [columnDefinition, setFilters]); }, [columnDefinition, setFilters]);
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
@ -258,9 +266,9 @@ const Filter = React.forwardRef<FilterRef, FilterProps>(
data-testid={`${id}-search-input`} data-testid={`${id}-search-input`}
value={searchValue} value={searchValue}
onChange={onSearchChange} onChange={onSearchChange}
placeholder={`Filter by ${activeFilterLabel}`} placeholder={`Filter by ${textInputActiveFilterLabel}`}
fieldLabel={`Find by ${activeFilterLabel}`} fieldLabel={`Find by ${textInputActiveFilterLabel}`}
aria-label={`Filter by ${activeFilterLabel}`} aria-label={`Filter by ${textInputActiveFilterLabel}`}
/> />
</ToolbarItem> </ToolbarItem>
{filters.map( {filters.map(

View File

@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import { ExclamationCircleIcon } from '@patternfly/react-icons'; import { ExclamationCircleIcon } from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon';
import { Content, ContentVariants, Flex, FlexItem, Tooltip } from '@patternfly/react-core'; import { Content, ContentVariants } from '@patternfly/react-core/dist/esm/components/Content';
import { Flex, FlexItem } from '@patternfly/react-core/dist/esm/layouts/Flex';
import { Tooltip } from '@patternfly/react-core/dist/esm/components/Tooltip';
type ImageFallbackProps = { type ImageFallbackProps = {
extended?: boolean; extended?: boolean;

View File

@ -2,18 +2,15 @@ import React, { FC, useEffect, useMemo, useState } from 'react';
import { import {
Dropdown, Dropdown,
DropdownItem, DropdownItem,
MenuToggle,
DropdownList, DropdownList,
DropdownProps, DropdownProps,
MenuSearch, } from '@patternfly/react-core/dist/esm/components/Dropdown';
MenuSearchInput, import { MenuToggle } from '@patternfly/react-core/dist/esm/components/MenuToggle';
InputGroup, import { MenuSearch, MenuSearchInput } from '@patternfly/react-core/dist/esm/components/Menu';
InputGroupItem, import { InputGroup, InputGroupItem } from '@patternfly/react-core/dist/esm/components/InputGroup';
SearchInput, import { SearchInput } from '@patternfly/react-core/dist/esm/components/SearchInput';
Button, import { Button, ButtonVariant } from '@patternfly/react-core/dist/esm/components/Button';
ButtonVariant, import { Divider } from '@patternfly/react-core/dist/esm/components/Divider';
Divider,
} from '@patternfly/react-core';
import { SearchIcon } from '@patternfly/react-icons/dist/esm/icons/search-icon'; import { SearchIcon } from '@patternfly/react-icons/dist/esm/icons/search-icon';
import { useNamespaceContext } from '~/app/context/NamespaceContextProvider'; import { useNamespaceContext } from '~/app/context/NamespaceContextProvider';

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Skeleton, SkeletonProps } from '@patternfly/react-core'; import { Skeleton, SkeletonProps } from '@patternfly/react-core/dist/esm/components/Skeleton';
type WithValidImageProps = { type WithValidImageProps = {
imageSrc: string | undefined | null; imageSrc: string | undefined | null;

View File

@ -59,7 +59,7 @@
--mui-radio__input--Width: 10px; --mui-radio__input--Width: 10px;
--mui-radio__input--Height: 10px; --mui-radio__input--Height: 10px;
// Sidebar from https://github.com/kubeflow/kubeflow/blob/4275d99754ac91f6cf5654b03824a73825e9fe55/components/centraldashboard/public/components/main-page.css#L7C1-L13C51 // Sidebar from https://github.com/kubeflow/kubeflow/blob/4275d99754ac91f6cf5654b03824a73825e9fe55/components/centraldashboard/public/components/main-page.css#L7C1-L13C51
--kf-central-primary-background-color: #0a3b71; --kf-central-primary-background-color: #0a3b71;
--kf-central-sidebar-default-color: #ffffff90; --kf-central-sidebar-default-color: #ffffff90;
--kf-central-app-drawer-width: 240px; --kf-central-app-drawer-width: 240px;
@ -103,7 +103,7 @@
--mui-spacing-8px: var(--mui-spacing); --mui-spacing-8px: var(--mui-spacing);
--mui-spacing-16px: calc(2 * var(--mui-spacing)); --mui-spacing-16px: calc(2 * var(--mui-spacing));
--pf-t--global--spacer--gap--group-to-group--vertical--default: var(--pf-t--global--spacer--sm); --pf-t--global--spacer--gap--group-to-group--vertical--default: var(--pf-t--global--spacer--sm);
--pf-t--global--border--width--box--status--default: 1px; --pf-t--global--border--width--box--status--default: 1px;
--pf-t--global--border--radius--pill: var(--mui-shape-borderRadius); --pf-t--global--border--radius--pill: var(--mui-shape-borderRadius);
--pf-t--global--text--color--brand--default: var(--mui-palette-primary-main); --pf-t--global--text--color--brand--default: var(--mui-palette-primary-main);
@ -122,7 +122,9 @@
} }
.mui-theme .pf-v6-c-alert { .mui-theme .pf-v6-c-alert {
--pf-v6-c-alert--m-warning__title--Color: var(--pf-t--global--text--color--status--warning--default); --pf-v6-c-alert--m-warning__title--Color: var(
--pf-t--global--text--color--status--warning--default
);
--pf-v6-c-alert__icon--MarginInlineEnd: var(--mui-alert__icon--MarginInlineEnd); --pf-v6-c-alert__icon--MarginInlineEnd: var(--mui-alert__icon--MarginInlineEnd);
--pf-v6-c-alert__icon--MarginBlockStart: var(--mui-alert__icon--MarginBlockStart); --pf-v6-c-alert__icon--MarginBlockStart: var(--mui-alert__icon--MarginBlockStart);
--pf-v6-c-alert__icon--FontSize: var(--mui-alert__icon--FontSize); --pf-v6-c-alert__icon--FontSize: var(--mui-alert__icon--FontSize);
@ -152,8 +154,6 @@
--pf-v6-c-button--PaddingInlineStart: var(--mui-button--PaddingInlineStart); --pf-v6-c-button--PaddingInlineStart: var(--mui-button--PaddingInlineStart);
--pf-v6-c-button--PaddingInlineEnd: var(--mui-button--PaddingInlineEnd); --pf-v6-c-button--PaddingInlineEnd: var(--mui-button--PaddingInlineEnd);
--pf-v6-c-button--LineHeight: var(--mui-button--LineHeight); --pf-v6-c-button--LineHeight: var(--mui-button--LineHeight);
text-transform: var(--mui-text-transform);
letter-spacing: 0.02857em; letter-spacing: 0.02857em;
} }
@ -164,6 +164,10 @@
&:hover { &:hover {
text-decoration-color: initial; text-decoration-color: initial;
} }
&.workspace-kind-summary-button {
text-transform: none;
}
} }
.mui-theme .pf-v6-c-button.pf-m-link.pf-m-inline .pf-v6-c-button__icon { .mui-theme .pf-v6-c-button.pf-m-link.pf-m-inline .pf-v6-c-button__icon {
@ -182,7 +186,8 @@
--pf-v6-c-card--BorderColor: var(--mui-palette-divider); --pf-v6-c-card--BorderColor: var(--mui-palette-divider);
} }
.pf-v6-c-card__selectable-actions :is(.pf-v6-c-check__label, .pf-v6-c-radio__label, .pf-v6-c-card__clickable-action):hover { .pf-v6-c-card__selectable-actions
:is(.pf-v6-c-check__label, .pf-v6-c-radio__label, .pf-v6-c-card__clickable-action):hover {
--pf-v6-c-card--BorderColor: var(--mui-palette-grey-300); --pf-v6-c-card--BorderColor: var(--mui-palette-grey-300);
} }
@ -210,7 +215,9 @@
} }
.mui-theme .pf-v6-c-drawer { .mui-theme .pf-v6-c-drawer {
--pf-v6-c-drawer--m-inline--m-expanded__panel--after--BackgroundColor: var(--mui-drawer-BorderLeft); --pf-v6-c-drawer--m-inline--m-expanded__panel--after--BackgroundColor: var(
--mui-drawer-BorderLeft
);
} }
.mui-theme .pf-v6-c-form__group { .mui-theme .pf-v6-c-form__group {
@ -259,6 +266,28 @@
z-index: 1; z-index: 1;
} }
// Form controls with number inputs - specific styling overrides
.mui-theme .pf-v6-c-form__group:has(.pf-v6-c-number-input) {
.pf-v6-c-form__label {
top: 30%;
font-size: 16px;
left: 0;
}
.pf-v6-c-form-control {
// Override default form control padding to match button padding in this context
--pf-v6-c-form-control--PaddingBlockStart: var(--mui-spacing-8px);
--pf-v6-c-form-control--PaddingBlockEnd: var(--mui-spacing-8px);
&.workspace-kind-unit-select {
--pf-v6-c-form-control--PaddingInlineEnd: calc(
var(--pf-v6-c-form-control__select--PaddingInlineEnd) +
var(--pf-v6-c-form-control__icon--FontSize)
);
}
}
}
.mui-theme .pf-v6-c-form-control input::placeholder { .mui-theme .pf-v6-c-form-control input::placeholder {
--pf-v6-c-form-control--m-placeholder--Color: var(--mui-palette-grey-600); --pf-v6-c-form-control--m-placeholder--Color: var(--mui-palette-grey-600);
} }
@ -273,13 +302,13 @@
resize: none; resize: none;
--pf-v6-c-form-control--PaddingBlockStart: var(--mui-spacing-16px); --pf-v6-c-form-control--PaddingBlockStart: var(--mui-spacing-16px);
--pf-v6-c-form-control--PaddingBlockEnd: var(--mui-spacing-16px); --pf-v6-c-form-control--PaddingBlockEnd: var(--mui-spacing-16px);
#text-file-simple-filename { #text-file-simple-filename {
--pf-v6-c-form-control--PaddingBlockStart: var(--mui-spacing-8px); --pf-v6-c-form-control--PaddingBlockStart: var(--mui-spacing-8px);
--pf-v6-c-form-control--PaddingBlockEnd: var(--mui-spacing-8px);; --pf-v6-c-form-control--PaddingBlockEnd: var(--mui-spacing-8px);
} }
} }
.mui-theme .pf-v6-c-form__section { .mui-theme .pf-v6-c-form__section {
--pf-v6-c-form__section--Gap: 0px; --pf-v6-c-form__section--Gap: 0px;
} }
@ -316,7 +345,7 @@
box-sizing: border-box; box-sizing: border-box;
} }
.mui-theme .pf-v6-c-form-control> :is(input, select, textarea):focus { .mui-theme .pf-v6-c-form-control > :is(input, select, textarea):focus {
--pf-v6-c-form-control--OutlineOffset: none; --pf-v6-c-form-control--OutlineOffset: none;
outline: none; outline: none;
} }
@ -365,7 +394,7 @@
border-color: black; border-color: black;
} }
.form-fieldset-wrapper:hover span.pf-v6-c-form-control.pf-m-disabled~.form-fieldset { .form-fieldset-wrapper:hover span.pf-v6-c-form-control.pf-m-disabled ~ .form-fieldset {
border-color: rgba(0, 0, 0, 0.23); border-color: rgba(0, 0, 0, 0.23);
} }
@ -453,7 +482,6 @@
.tr-fieldset-wrapper .pf-v6-c-form-control, .tr-fieldset-wrapper .pf-v6-c-form-control,
.toolbar-fieldset-wrapper .pf-v6-c-form-control, .toolbar-fieldset-wrapper .pf-v6-c-form-control,
.form-fieldset-wrapper .pf-v6-c-form-control { .form-fieldset-wrapper .pf-v6-c-form-control {
--pf-v6-c-form-control--before--BorderColor: transparent !important; --pf-v6-c-form-control--before--BorderColor: transparent !important;
--pf-v6-c-form-control--after--BorderColor: transparent !important; --pf-v6-c-form-control--after--BorderColor: transparent !important;
} }
@ -462,8 +490,8 @@
--pf-v6-c-form__field-group-body--PaddingBlockStart: 8px; --pf-v6-c-form__field-group-body--PaddingBlockStart: 8px;
} }
.pf-v6-c-form__group .pf-v6-c-form-control:focus-within+.pf-v6-c-form__label, .pf-v6-c-form__group .pf-v6-c-form-control:focus-within + .pf-v6-c-form__label,
.pf-v6-c-form__group .pf-v6-c-form-control:not(:placeholder-shown)+.pf-v6-c-form__label { .pf-v6-c-form__group .pf-v6-c-form-control:not(:placeholder-shown) + .pf-v6-c-form__label {
color: var(--mui-palette-primary-main); color: var(--mui-palette-primary-main);
} }
@ -496,7 +524,9 @@
.mui-theme .pf-v6-c-menu-toggle { .mui-theme .pf-v6-c-menu-toggle {
--pf-v6-c-menu-toggle__toggle-icon--MinHeight: var(--mui-menu-toggle__toggle-icon--MinHeight); --pf-v6-c-menu-toggle__toggle-icon--MinHeight: var(--mui-menu-toggle__toggle-icon--MinHeight);
--pf-v6-c-menu-toggle__controls--MinWidth: var(--mui-menu-toggle__controls--MinWidth); --pf-v6-c-menu-toggle__controls--MinWidth: var(--mui-menu-toggle__controls--MinWidth);
--pf-v6-c-menu-toggle--expanded--BackgroundColor: var(--mui-menu-toggle--expanded--BackgroundColor); --pf-v6-c-menu-toggle--expanded--BackgroundColor: var(
--mui-menu-toggle--expanded--BackgroundColor
);
--pf-v6-c-menu-toggle--expanded--BorderColor: var(--mui-menu-toggle--expanded--BorderColor); --pf-v6-c-menu-toggle--expanded--BorderColor: var(--mui-menu-toggle--expanded--BorderColor);
--pf-v6-c-menu-toggle--PaddingInlineStart: var(--mui-menu-toggle--PaddingInlineStart); --pf-v6-c-menu-toggle--PaddingInlineStart: var(--mui-menu-toggle--PaddingInlineStart);
--pf-v6-c-menu-toggle--PaddingInlineEnd: var(--mui-menu-toggle--PaddingInlineEnd); --pf-v6-c-menu-toggle--PaddingInlineEnd: var(--mui-menu-toggle--PaddingInlineEnd);
@ -504,17 +534,21 @@
--pf-v6-c-menu-toggle--expanded--Color: var(--mui-palette-common-black); --pf-v6-c-menu-toggle--expanded--Color: var(--mui-palette-common-black);
--pf-v6-c-menu-toggle--hover--BorderColor: var(--mui-menu-toggle--hover--BorderColor); --pf-v6-c-menu-toggle--hover--BorderColor: var(--mui-menu-toggle--hover--BorderColor);
--pf-v6-c-menu-toggle--BorderColor: var(--mui-menu-toggle--BorderColor); --pf-v6-c-menu-toggle--BorderColor: var(--mui-menu-toggle--BorderColor);
--pf-v6-c-menu-toggle--hover--BackgroundColor: var(--pf-t--global--background--color--action--plain--hover); --pf-v6-c-menu-toggle--hover--BackgroundColor: var(
--pf-v6-c-menu-toggle--m-split-button--m-action--m-primary--expanded--child--BackgroundColor: var(--pf-t--global--color--brand--default); --pf-t--global--background--color--action--plain--hover
--pf-v6-c-menu-toggle--m-split-button--m-action--m-primary--child--BorderInlineStartColor: var(--mui-palette-primary-dark); );
--pf-v6-c-menu-toggle--m-split-button--m-action--m-primary--expanded--child--BackgroundColor: var(
--pf-t--global--color--brand--default
);
--pf-v6-c-menu-toggle--m-split-button--m-action--m-primary--child--BorderInlineStartColor: var(
--mui-palette-primary-dark
);
text-transform: var(--mui-text-transform);
font-weight: var(--mui-button-font-weight); font-weight: var(--mui-button-font-weight);
letter-spacing: 0.02857em; letter-spacing: 0.02857em;
} }
.mui-theme .pf-v6-c-menu-toggle__button { .mui-theme .pf-v6-c-menu-toggle__button {
text-transform: var(--mui-text-transform);
font-weight: var(--mui-button-font-weight); font-weight: var(--mui-button-font-weight);
letter-spacing: 0.02857em; letter-spacing: 0.02857em;
align-self: stretch; align-self: stretch;
@ -530,8 +564,12 @@
--pf-v6-c-menu-toggle--expanded--Color: var(--pf-t--global--text--color--on-brand--clicked); --pf-v6-c-menu-toggle--expanded--Color: var(--pf-t--global--text--color--on-brand--clicked);
} }
.mui-theme .pf-v6-c-menu-toggle.pf-m-primary.pf-m-split-button .pf-v6-c-menu-toggle__button[aria-expanded='true'] { .mui-theme
--pf-v6-c-menu-toggle--m-split-button--m-action--m-primary--child--BackgroundColor: var(--mui-palette-primary-dark); .pf-v6-c-menu-toggle.pf-m-primary.pf-m-split-button
.pf-v6-c-menu-toggle__button[aria-expanded='true'] {
--pf-v6-c-menu-toggle--m-split-button--m-action--m-primary--child--BackgroundColor: var(
--mui-palette-primary-dark
);
} }
.pf-v6-c-menu-toggle.pf-m-secondary.pf-m-split-button { .pf-v6-c-menu-toggle.pf-m-secondary.pf-m-split-button {
@ -595,7 +633,6 @@
row-gap: none; row-gap: none;
} }
.mui-theme .pf-v6-c-page__sidebar { .mui-theme .pf-v6-c-page__sidebar {
--pf-v6-c-page__sidebar--BackgroundColor: var(--kf-central-primary-background-color); --pf-v6-c-page__sidebar--BackgroundColor: var(--kf-central-primary-background-color);
} }
@ -603,18 +640,17 @@
.mui-theme .pf-v6-c-page__sidebar-body { .mui-theme .pf-v6-c-page__sidebar-body {
--pf-v6-c-page__sidebar-body--PaddingInlineStart: 0px; --pf-v6-c-page__sidebar-body--PaddingInlineStart: 0px;
--pf-v6-c-page__sidebar-body--PaddingInlineEnd: 0px; --pf-v6-c-page__sidebar-body--PaddingInlineEnd: 0px;
} }
.mui-theme .pf-v6-c-progress-stepper__step.pf-m-info { .mui-theme .pf-v6-c-progress-stepper__step.pf-m-info {
--pf-v6-c-progress-stepper__step-icon--Color: var(--mui-palette-primary-main); --pf-v6-c-progress-stepper__step-icon--Color: var(--mui-palette-primary-main);
} }
.mui-theme .pf-v6-c-progress-stepper__step.pf-m-success { .mui-theme .pf-v6-c-progress-stepper__step.pf-m-success {
--pf-v6-c-progress-stepper__step-icon--Color: var(--mui-palette-success-main); --pf-v6-c-progress-stepper__step-icon--Color: var(--mui-palette-success-main);
} }
.mui-theme .pf-v6-c-radio.pf-m-standalone .pf-v6-c-radio__input { .mui-theme .pf-v6-c-radio.pf-m-standalone:not(.workspace-kind-form-radio) .pf-v6-c-radio__input {
display: none; display: none;
} }
@ -634,11 +670,15 @@
--pf-v6-c-table--cell--PaddingInlineEnd: var(--mui-table--cell--PaddingInlineEnd); --pf-v6-c-table--cell--PaddingInlineEnd: var(--mui-table--cell--PaddingInlineEnd);
--pf-v6-c-table--cell--PaddingBlockStart: var(--mui-table--cell--PaddingBlockStart); --pf-v6-c-table--cell--PaddingBlockStart: var(--mui-table--cell--PaddingBlockStart);
--pf-v6-c-table--cell--PaddingBlockEnd: var(--mui-table--cell--PaddingBlockEnd); --pf-v6-c-table--cell--PaddingBlockEnd: var(--mui-table--cell--PaddingBlockEnd);
--pf-v6-c-table--cell--first-last-child--PaddingInline: var(--mui-table--cell--first-last-child--PaddingInline); --pf-v6-c-table--cell--first-last-child--PaddingInline: var(
--mui-table--cell--first-last-child--PaddingInline
);
--pf-v6-c-table__thead--cell--FontWeight: var(--mui-button-font-weight); --pf-v6-c-table__thead--cell--FontWeight: var(--mui-button-font-weight);
--pf-v6-c-table__thead--cell--FontSize: var(--mui-table__thead--cell--FontSize); --pf-v6-c-table__thead--cell--FontSize: var(--mui-table__thead--cell--FontSize);
--pf-v6-c-table__tr--BorderBlockEndColor: var(--mui-palette-grey-300); --pf-v6-c-table__tr--BorderBlockEndColor: var(--mui-palette-grey-300);
--pf-v6-c-table__sort-indicator--MarginInlineStart: var(--mui-table__sort-indicator--MarginInlineStart); --pf-v6-c-table__sort-indicator--MarginInlineStart: var(
--mui-table__sort-indicator--MarginInlineStart
);
letter-spacing: 0.01071em; letter-spacing: 0.01071em;
} }
@ -656,10 +696,12 @@
transform-origin: center center; transform-origin: center center;
align-self: start; align-self: start;
} }
/* CSS workaround for spacing in labels in Workspace Kind */ /* CSS workaround for spacing in labels in Workspace Kind */
.form-label-field-group .pf-v6-c-table tr:where(.pf-v6-c-table__tr) > :where(th, td) { .form-label-field-group .pf-v6-c-table tr:where(.pf-v6-c-table__tr) > :where(th, td) {
padding-block-start: 0px; padding-block-start: 0px;
} }
/* CSS workaround to use MUI icon for sort icon */ /* CSS workaround to use MUI icon for sort icon */
.mui-theme .pf-v6-c-table__sort-indicator::before { .mui-theme .pf-v6-c-table__sort-indicator::before {
display: block; display: block;
@ -712,12 +754,13 @@
--pf-v6-c-tabs__link--PaddingInlineStart: var(--mui-tabs__link--PaddingInlineStart); --pf-v6-c-tabs__link--PaddingInlineStart: var(--mui-tabs__link--PaddingInlineStart);
--pf-v6-c-tabs__link--PaddingInlineEnd: var(--mui-tabs__link--PaddingInlineEnd); --pf-v6-c-tabs__link--PaddingInlineEnd: var(--mui-tabs__link--PaddingInlineEnd);
--pf-v6-c-tabs__item--m-current__link--Color: var(--pf-t--global--text--color--brand--default); --pf-v6-c-tabs__item--m-current__link--Color: var(--pf-t--global--text--color--brand--default);
--pf-v6-c-tabs__item--m-current__link--after--BorderWidth: var(--mui-tabs__item--m-current__link--after--BorderWidth); --pf-v6-c-tabs__item--m-current__link--after--BorderWidth: var(
--mui-tabs__item--m-current__link--after--BorderWidth
);
--pf-v6-c-tabs__link--FontSize: 0.875rem; --pf-v6-c-tabs__link--FontSize: 0.875rem;
} }
.mui-theme .pf-v6-c-tabs__link { .mui-theme .pf-v6-c-tabs__link {
text-transform: var(--mui-text-transform);
font-weight: var(--mui-button-font-weight); font-weight: var(--mui-button-font-weight);
line-height: var(--mui-button-line-height); line-height: var(--mui-button-line-height);
letter-spacing: 0.02857em; letter-spacing: 0.02857em;
@ -862,12 +905,9 @@
} }
.mui-theme .pf-v6-c-toolbar__group.pf-m-filter-group .pf-v6-c-form-control { .mui-theme .pf-v6-c-toolbar__group.pf-m-filter-group .pf-v6-c-form-control {
// Override default form control padding to match button padding in this context // Override default form control padding to match button padding in this context
--pf-v6-c-form-control--PaddingBlockStart: var(--mui-spacing-8px); --pf-v6-c-form-control--PaddingBlockStart: var(--mui-spacing-8px);
--pf-v6-c-form-control--PaddingBlockEnd: var(--mui-spacing-8px); --pf-v6-c-form-control--PaddingBlockEnd: var(--mui-spacing-8px);
} }
// Fix hover state margin issue by removing problematic padding // Fix hover state margin issue by removing problematic padding
@ -881,6 +921,7 @@
.workspacekind-file-upload { .workspacekind-file-upload {
height: 100%; height: 100%;
.pf-v6-c-file-upload__file-details { .pf-v6-c-file-upload__file-details {
flex-grow: 1; flex-grow: 1;
} }
@ -888,6 +929,6 @@
/* Workaround for Toggle group header in Workspace Kind Form */ /* Workaround for Toggle group header in Workspace Kind Form */
.workspace-kind-form-header .pf-v6-c-toggle-group__button.pf-m-selected { .workspace-kind-form-header .pf-v6-c-toggle-group__button.pf-m-selected {
background-color: #E0F0FF; background-color: #e0f0ff;
color: var(--pf-t--color--black); color: var(--pf-t--color--black);
} }

View File

@ -40,6 +40,13 @@ export const MEMORY_UNITS_FOR_PARSING: UnitOption[] = [
{ name: 'KiB', unit: 'Ki', weight: 1024 }, { name: 'KiB', unit: 'Ki', weight: 1024 },
{ name: 'B', unit: '', weight: 1 }, { name: 'B', unit: '', weight: 1 },
]; ];
export const TIME_UNIT_FOR_SELECTION: UnitOption[] = [
{ name: 'Minutes', unit: 'Minutes', weight: 60 },
{ name: 'Hours', unit: 'Hours', weight: 60 * 60 },
{ name: 'Days', unit: 'Days', weight: 60 * 60 * 24 },
];
export const OTHER: UnitOption[] = [{ name: '', unit: '', weight: 1 }]; export const OTHER: UnitOption[] = [{ name: '', unit: '', weight: 1 }];
export const splitValueUnit = ( export const splitValueUnit = (