Merge branch 'notebooks-v2' into workspaceKindSummary
Signed-off-by: Dominik Kawka <31955648+dominikkawka@users.noreply.github.com>
This commit is contained in:
commit
aa3f270e54
|
|
@ -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',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ class Home {
|
||||||
}
|
}
|
||||||
|
|
||||||
findButton() {
|
findButton() {
|
||||||
return cy.get('button:contains("Create Workspace")');
|
return cy.get('button:contains("Create workspace")');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 }) => {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 }>([
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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' && (
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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'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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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', {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -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 />
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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}>
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}>
|
||||||
|
|
|
||||||
|
|
@ -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) => (
|
||||||
|
|
|
||||||
|
|
@ -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)}
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 = () => {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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> = {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 = (
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue