feat(ws): add initial workspace creation wizard frontend (#227)
* feat(ws): add initial workspace creation wizard frontend Signed-off-by: paulovmr <832830+paulovmr@users.noreply.github.com> * feat(ws): add initial workspace creation wizard frontend Signed-off-by: paulovmr <832830+paulovmr@users.noreply.github.com> * card view style fixes (#2) Signed-off-by: Jenny <32821331+jenny-s51@users.noreply.github.com> * fix(ws): fix scroll behavior with PageGroup Signed-off-by: Jenny <32821331+jenny-s51@users.noreply.github.com> * feat(ws): add initial workspace creation wizard frontend Signed-off-by: paulovmr <832830+paulovmr@users.noreply.github.com> * fix(ws): Apply flex-grow: 0 to page section Signed-off-by: Jenny <32821331+jenny-s51@users.noreply.github.com> --------- Signed-off-by: paulovmr <832830+paulovmr@users.noreply.github.com> Signed-off-by: Jenny <32821331+jenny-s51@users.noreply.github.com> Co-authored-by: Jenny <32821331+jenny-s51@users.noreply.github.com>
This commit is contained in:
parent
19eca50561
commit
2bc10ecc20
|
@ -138,6 +138,16 @@ module.exports = (env) => {
|
||||||
'sass-loader',
|
'sass-loader',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
test: /\.css$/i,
|
||||||
|
use: [
|
||||||
|
'style-loader',
|
||||||
|
'css-loader',
|
||||||
|
],
|
||||||
|
include: [
|
||||||
|
path.resolve(relativeDir, 'node_modules/@patternfly/react-catalog-view-extension/dist/css/react-catalog-view-extension.css'),
|
||||||
|
]
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@patternfly/react-catalog-view-extension": "^6.0.0",
|
||||||
"@patternfly/react-code-editor": "^6.0.0",
|
"@patternfly/react-code-editor": "^6.0.0",
|
||||||
"@patternfly/react-core": "^6.0.0",
|
"@patternfly/react-core": "^6.0.0",
|
||||||
"@patternfly/react-icons": "^6.0.0",
|
"@patternfly/react-icons": "^6.0.0",
|
||||||
|
@ -4039,6 +4040,20 @@
|
||||||
"url": "https://opencollective.com/parcel"
|
"url": "https://opencollective.com/parcel"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@patternfly/react-catalog-view-extension": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@patternfly/react-catalog-view-extension/-/react-catalog-view-extension-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-AfvfJXjADXX1YZnWNZLe0mLgBepSnn4Xn9SQsvTeS+PLFFGgJQEsbhRyFXdGVJLVU/19+a/YOpHgZZeI2dus0A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@patternfly/react-core": "^6.0.0",
|
||||||
|
"@patternfly/react-styles": "^6.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^17 || ^18",
|
||||||
|
"react-dom": "^17 || ^18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@patternfly/react-code-editor": {
|
"node_modules/@patternfly/react-code-editor": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@patternfly/react-code-editor/-/react-code-editor-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@patternfly/react-code-editor/-/react-code-editor-6.1.0.tgz",
|
||||||
|
|
|
@ -95,6 +95,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@patternfly/react-code-editor": "^6.0.0",
|
"@patternfly/react-code-editor": "^6.0.0",
|
||||||
|
"@patternfly/react-catalog-view-extension": "^6.0.0",
|
||||||
"@patternfly/react-core": "^6.0.0",
|
"@patternfly/react-core": "^6.0.0",
|
||||||
"@patternfly/react-icons": "^6.0.0",
|
"@patternfly/react-icons": "^6.0.0",
|
||||||
"@patternfly/react-styles": "^6.0.0",
|
"@patternfly/react-styles": "^6.0.0",
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { Route, Routes } from 'react-router-dom';
|
||||||
import { NotFound } from './pages/notFound/NotFound';
|
import { NotFound } from './pages/notFound/NotFound';
|
||||||
import { Debug } from './pages/Debug/Debug';
|
import { Debug } from './pages/Debug/Debug';
|
||||||
import { Workspaces } from './pages/Workspaces/Workspaces';
|
import { Workspaces } from './pages/Workspaces/Workspaces';
|
||||||
|
import { WorkspaceCreation } from './pages/Workspaces/Creation/WorkspaceCreation';
|
||||||
import '~/shared/style/MUI-theme.scss';
|
import '~/shared/style/MUI-theme.scss';
|
||||||
|
|
||||||
export const isNavDataGroup = (navItem: NavDataItem): navItem is NavDataGroup =>
|
export const isNavDataGroup = (navItem: NavDataItem): navItem is NavDataGroup =>
|
||||||
|
@ -43,7 +44,7 @@ export const useAdminDebugSettings = (): NavDataItem[] => {
|
||||||
export const useNavData = (): NavDataItem[] => [
|
export const useNavData = (): NavDataItem[] => [
|
||||||
{
|
{
|
||||||
label: 'Notebooks',
|
label: 'Notebooks',
|
||||||
path: '/',
|
path: '/workspaces',
|
||||||
},
|
},
|
||||||
...useAdminDebugSettings(),
|
...useAdminDebugSettings(),
|
||||||
];
|
];
|
||||||
|
@ -53,6 +54,8 @@ const AppRoutes: React.FC = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
|
<Route path="/workspaces/create" element={<WorkspaceCreation />} />
|
||||||
|
<Route path="/workspaces" element={<Workspaces />} />
|
||||||
<Route path="/" element={<Workspaces />} />
|
<Route path="/" element={<Workspaces />} />
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,132 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Content,
|
||||||
|
Flex,
|
||||||
|
FlexItem,
|
||||||
|
PageGroup,
|
||||||
|
PageSection,
|
||||||
|
ProgressStep,
|
||||||
|
ProgressStepper,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { WorkspaceCreationImageSelection } from '~/app/pages/Workspaces/Creation/WorkspaceCreationImageSelection';
|
||||||
|
import { WorkspaceCreationKindSelection } from '~/app/pages/Workspaces/Creation/WorkspaceCreationKindSelection';
|
||||||
|
import { WorkspaceCreationPropertiesSelection } from '~/app/pages/Workspaces/Creation/WorkspaceCreationPropertiesSelection';
|
||||||
|
|
||||||
|
enum WorkspaceCreationSteps {
|
||||||
|
KindSelection,
|
||||||
|
ImageSelection,
|
||||||
|
Properties,
|
||||||
|
}
|
||||||
|
|
||||||
|
const WorkspaceCreation: React.FunctionComponent = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [currentStep, setCurrentStep] = useState(WorkspaceCreationSteps.KindSelection);
|
||||||
|
|
||||||
|
const getStepVariant = useCallback(
|
||||||
|
(step: WorkspaceCreationSteps) => {
|
||||||
|
if (step > currentStep) {
|
||||||
|
return 'pending';
|
||||||
|
}
|
||||||
|
if (step < currentStep) {
|
||||||
|
return 'success';
|
||||||
|
}
|
||||||
|
return 'info';
|
||||||
|
},
|
||||||
|
[currentStep],
|
||||||
|
);
|
||||||
|
|
||||||
|
const previousStep = useCallback(() => {
|
||||||
|
setCurrentStep(currentStep - 1);
|
||||||
|
}, [currentStep]);
|
||||||
|
|
||||||
|
const nextStep = useCallback(() => {
|
||||||
|
setCurrentStep(currentStep + 1);
|
||||||
|
}, [currentStep]);
|
||||||
|
|
||||||
|
const cancel = useCallback(() => {
|
||||||
|
navigate('/workspaces');
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageGroup stickyOnBreakpoint={{ default: 'top' }}>
|
||||||
|
<PageSection isFilled={false}>
|
||||||
|
<Content>
|
||||||
|
<h1>Create workspace</h1>
|
||||||
|
</Content>
|
||||||
|
<ProgressStepper aria-label="Workspace creation stepper">
|
||||||
|
<ProgressStep
|
||||||
|
variant={getStepVariant(WorkspaceCreationSteps.KindSelection)}
|
||||||
|
id="kind-selection-step"
|
||||||
|
titleId="kind-selection-step-title"
|
||||||
|
aria-label="Kind selection step"
|
||||||
|
>
|
||||||
|
Kind selection
|
||||||
|
</ProgressStep>
|
||||||
|
<ProgressStep
|
||||||
|
variant={getStepVariant(WorkspaceCreationSteps.ImageSelection)}
|
||||||
|
isCurrent
|
||||||
|
id="image-selection-step"
|
||||||
|
titleId="image-selection-step-title"
|
||||||
|
aria-label="Image selection step"
|
||||||
|
>
|
||||||
|
Image selection
|
||||||
|
</ProgressStep>
|
||||||
|
<ProgressStep
|
||||||
|
variant={getStepVariant(WorkspaceCreationSteps.Properties)}
|
||||||
|
id="properties-step"
|
||||||
|
titleId="properties-step-title"
|
||||||
|
aria-label="Properties step"
|
||||||
|
>
|
||||||
|
Properties
|
||||||
|
</ProgressStep>
|
||||||
|
</ProgressStepper>
|
||||||
|
</PageSection>
|
||||||
|
</PageGroup>
|
||||||
|
<PageSection isFilled>
|
||||||
|
{currentStep === WorkspaceCreationSteps.KindSelection && <WorkspaceCreationKindSelection />}
|
||||||
|
{currentStep === WorkspaceCreationSteps.ImageSelection && (
|
||||||
|
<WorkspaceCreationImageSelection />
|
||||||
|
)}
|
||||||
|
{currentStep === WorkspaceCreationSteps.Properties && (
|
||||||
|
<WorkspaceCreationPropertiesSelection />
|
||||||
|
)}
|
||||||
|
</PageSection>
|
||||||
|
<PageSection isFilled={false} stickyOnBreakpoint={{ default: 'bottom' }}>
|
||||||
|
<Flex>
|
||||||
|
<FlexItem>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
ouiaId="Primary"
|
||||||
|
onClick={previousStep}
|
||||||
|
isDisabled={currentStep === 0}
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</Button>
|
||||||
|
</FlexItem>
|
||||||
|
<FlexItem>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
ouiaId="Primary"
|
||||||
|
onClick={nextStep}
|
||||||
|
isDisabled={currentStep === Object.keys(WorkspaceCreationSteps).length / 2 - 1}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</FlexItem>
|
||||||
|
<FlexItem>
|
||||||
|
<Button variant="link" isInline onClick={cancel}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</FlexItem>
|
||||||
|
</Flex>
|
||||||
|
</PageSection>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { WorkspaceCreation };
|
|
@ -0,0 +1,10 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { Content } from '@patternfly/react-core';
|
||||||
|
|
||||||
|
const WorkspaceCreationImageSelection: React.FunctionComponent = () => (
|
||||||
|
<Content>
|
||||||
|
<p>Select a workspace image and image version to use for the workspace.</p>
|
||||||
|
</Content>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { WorkspaceCreationImageSelection };
|
|
@ -0,0 +1,23 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Title } from '@patternfly/react-core';
|
||||||
|
import { WorkspaceKind } from '~/shared/types';
|
||||||
|
|
||||||
|
type WorkspaceCreationKindDetailsProps = {
|
||||||
|
workspaceKind?: WorkspaceKind;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WorkspaceCreationKindDetails: React.FunctionComponent<
|
||||||
|
WorkspaceCreationKindDetailsProps
|
||||||
|
> = ({ workspaceKind }) => (
|
||||||
|
<>
|
||||||
|
{!workspaceKind && <p>Select a workspace kind to view its details here.</p>}
|
||||||
|
|
||||||
|
{workspaceKind && (
|
||||||
|
<>
|
||||||
|
<Title headingLevel="h6">Workspace kind</Title>
|
||||||
|
<Title headingLevel="h3">{workspaceKind.name}</Title>
|
||||||
|
<p>{workspaceKind.description}</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
|
@ -0,0 +1,131 @@
|
||||||
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
import {
|
||||||
|
CardBody,
|
||||||
|
CardTitle,
|
||||||
|
Gallery,
|
||||||
|
PageSection,
|
||||||
|
Toolbar,
|
||||||
|
ToolbarContent,
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
EmptyState,
|
||||||
|
EmptyStateBody,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
import { SearchIcon } from '@patternfly/react-icons/dist/esm/icons/search-icon';
|
||||||
|
import { WorkspaceKind } from '~/shared/types';
|
||||||
|
import Filter, { FilteredColumn } from '~/shared/components/Filter';
|
||||||
|
|
||||||
|
type WorkspaceCreationKindListProps = {
|
||||||
|
allWorkspaceKinds: WorkspaceKind[];
|
||||||
|
onSelect: (workspaceKind: WorkspaceKind) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WorkspaceCreationKindList: React.FunctionComponent<WorkspaceCreationKindListProps> = ({
|
||||||
|
allWorkspaceKinds,
|
||||||
|
onSelect,
|
||||||
|
}) => {
|
||||||
|
const [workspaceKinds, setWorkspaceKinds] = useState<WorkspaceKind[]>(allWorkspaceKinds);
|
||||||
|
const [selectedWorkspaceKind, setSelectedWorkspaceKind] = useState<WorkspaceKind>();
|
||||||
|
|
||||||
|
const filterableColumns = useMemo(
|
||||||
|
() => ({
|
||||||
|
name: 'Name',
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onFilter = useCallback(
|
||||||
|
(filters: FilteredColumn[]) => {
|
||||||
|
// Search name with search value
|
||||||
|
let filteredWorkspaceKinds = allWorkspaceKinds;
|
||||||
|
filters.forEach((filter) => {
|
||||||
|
let searchValueInput: RegExp;
|
||||||
|
try {
|
||||||
|
searchValueInput = new RegExp(filter.value, 'i');
|
||||||
|
} catch {
|
||||||
|
searchValueInput = new RegExp(filter.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredWorkspaceKinds = filteredWorkspaceKinds.filter((kind) => {
|
||||||
|
if (filter.value === '') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
switch (filter.columnName) {
|
||||||
|
case filterableColumns.name:
|
||||||
|
return (
|
||||||
|
kind.name.search(searchValueInput) >= 0 ||
|
||||||
|
kind.displayName.search(searchValueInput) >= 0
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
setWorkspaceKinds(filteredWorkspaceKinds);
|
||||||
|
},
|
||||||
|
[filterableColumns, allWorkspaceKinds],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onChange = useCallback(
|
||||||
|
(event: React.FormEvent<HTMLInputElement>) => {
|
||||||
|
const newSelectedWorkspaceKind = workspaceKinds.find(
|
||||||
|
(kind) => kind.name === event.currentTarget.name,
|
||||||
|
);
|
||||||
|
setSelectedWorkspaceKind(newSelectedWorkspaceKind);
|
||||||
|
onSelect(newSelectedWorkspaceKind);
|
||||||
|
},
|
||||||
|
[workspaceKinds, onSelect],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageSection>
|
||||||
|
<Toolbar id="toolbar-group-types">
|
||||||
|
<ToolbarContent>
|
||||||
|
<Filter
|
||||||
|
id="filter-workspace-kinds"
|
||||||
|
onFilter={onFilter}
|
||||||
|
columnNames={filterableColumns}
|
||||||
|
/>
|
||||||
|
</ToolbarContent>
|
||||||
|
</Toolbar>
|
||||||
|
</PageSection>
|
||||||
|
<PageSection isFilled>
|
||||||
|
{workspaceKinds.length === 0 && (
|
||||||
|
<EmptyState titleText="No results found" headingLevel="h4" icon={SearchIcon}>
|
||||||
|
<EmptyStateBody>
|
||||||
|
No results match the filter criteria. Clear all filters and try again.
|
||||||
|
</EmptyStateBody>
|
||||||
|
</EmptyState>
|
||||||
|
)}
|
||||||
|
{workspaceKinds.length > 0 && (
|
||||||
|
<Gallery hasGutter aria-label="Selectable card container">
|
||||||
|
{workspaceKinds.map((kind) => (
|
||||||
|
<Card
|
||||||
|
isCompact
|
||||||
|
isSelectable
|
||||||
|
key={kind.name}
|
||||||
|
id={kind.name.replace(/ /g, '-')}
|
||||||
|
isSelected={kind.name === selectedWorkspaceKind?.name}
|
||||||
|
>
|
||||||
|
<CardHeader
|
||||||
|
selectableActions={{
|
||||||
|
selectableActionId: `selectable-actions-item-${kind.name}`,
|
||||||
|
selectableActionAriaLabelledby: kind.name.replace(/ /g, '-'),
|
||||||
|
name: kind.name,
|
||||||
|
variant: 'single',
|
||||||
|
onChange,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img src={kind.icon.url} alt={`${kind.name} icon`} style={{ maxWidth: '60px' }} />
|
||||||
|
<CardTitle>{kind.displayName}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody>{kind.description}</CardBody>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Gallery>
|
||||||
|
)}
|
||||||
|
</PageSection>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,131 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { Content, Divider, Split, SplitItem } from '@patternfly/react-core';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import { WorkspaceKind } from '~/shared/types';
|
||||||
|
import { WorkspaceCreationKindDetails } from '~/app/pages/Workspaces/Creation/WorkspaceCreationKindDetails';
|
||||||
|
import { WorkspaceCreationKindList } from '~/app/pages/Workspaces/Creation/WorkspaceCreationKindList';
|
||||||
|
|
||||||
|
const WorkspaceCreationKindSelection: React.FunctionComponent = () => {
|
||||||
|
/* Replace mocks below for BFF call */
|
||||||
|
const mockedWorkspaceKind: WorkspaceKind = useMemo(
|
||||||
|
() => ({
|
||||||
|
name: 'jupyter-lab1',
|
||||||
|
displayName: 'JupyterLab Notebook',
|
||||||
|
description: 'A Workspace which runs JupyterLab in a Pod',
|
||||||
|
deprecated: false,
|
||||||
|
deprecationMessage: '',
|
||||||
|
hidden: false,
|
||||||
|
icon: {
|
||||||
|
url: 'https://jupyter.org/assets/favicons/apple-touch-icon-152x152.png',
|
||||||
|
},
|
||||||
|
logo: {
|
||||||
|
url: 'https://upload.wikimedia.org/wikipedia/commons/3/38/Jupyter_logo.svg',
|
||||||
|
},
|
||||||
|
podTemplate: {
|
||||||
|
podMetadata: {
|
||||||
|
labels: { myWorkspaceKindLabel: 'my-value' },
|
||||||
|
annotations: { myWorkspaceKindAnnotation: 'my-value' },
|
||||||
|
},
|
||||||
|
volumeMounts: { home: '/home/jovyan' },
|
||||||
|
options: {
|
||||||
|
imageConfig: {
|
||||||
|
default: 'jupyterlab_scipy_190',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
id: 'jupyterlab_scipy_180',
|
||||||
|
displayName: 'jupyter-scipy:v1.8.0',
|
||||||
|
labels: { pythonVersion: '3.11' },
|
||||||
|
hidden: true,
|
||||||
|
redirect: {
|
||||||
|
to: 'jupyterlab_scipy_190',
|
||||||
|
message: {
|
||||||
|
text: 'This update will change...',
|
||||||
|
level: 'Info',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'jupyterlab_scipy_190',
|
||||||
|
displayName: 'jupyter-scipy:v1.9.0',
|
||||||
|
labels: { pythonVersion: '3.11' },
|
||||||
|
hidden: true,
|
||||||
|
redirect: {
|
||||||
|
to: 'jupyterlab_scipy_200',
|
||||||
|
message: {
|
||||||
|
text: 'This update will change...',
|
||||||
|
level: 'Warning',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
podConfig: {
|
||||||
|
default: 'tiny_cpu',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
id: 'tiny_cpu',
|
||||||
|
displayName: 'Tiny CPU',
|
||||||
|
description: 'Pod with 0.1 CPU, 128 Mb RAM',
|
||||||
|
labels: { cpu: '100m', memory: '128Mi' },
|
||||||
|
redirect: {
|
||||||
|
to: 'small_cpu',
|
||||||
|
message: {
|
||||||
|
text: 'This update will change...',
|
||||||
|
level: 'Danger',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
/* Replace mocks below for BFF call */
|
||||||
|
const allWorkspaceKinds = useMemo(() => {
|
||||||
|
const kinds: WorkspaceKind[] = [];
|
||||||
|
|
||||||
|
for (let i = 1; i <= 15; i++) {
|
||||||
|
const kind = { ...mockedWorkspaceKind };
|
||||||
|
kind.name += i;
|
||||||
|
kind.displayName += ` ${i}`;
|
||||||
|
kind.podTemplate = { ...mockedWorkspaceKind.podTemplate };
|
||||||
|
kind.podTemplate.podMetadata = { ...mockedWorkspaceKind.podTemplate.podMetadata };
|
||||||
|
kind.podTemplate.podMetadata.labels = {
|
||||||
|
...mockedWorkspaceKind.podTemplate.podMetadata.labels,
|
||||||
|
};
|
||||||
|
kind.podTemplate.podMetadata.labels[`my-label-key-${Math.ceil(i / 4)}`] =
|
||||||
|
`my-label-value-${Math.ceil(i)}`;
|
||||||
|
kinds.push(kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
return kinds;
|
||||||
|
}, [mockedWorkspaceKind]);
|
||||||
|
|
||||||
|
const [selectedKind, setSelectedKind] = useState<WorkspaceKind>();
|
||||||
|
|
||||||
|
const kindDetailsContent = useMemo(
|
||||||
|
() => <WorkspaceCreationKindDetails workspaceKind={selectedKind} />,
|
||||||
|
[selectedKind],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Content style={{ height: '100%' }}>
|
||||||
|
<p>Select a workspace kind to use for the workspace.</p>
|
||||||
|
<Divider />
|
||||||
|
<Split hasGutter>
|
||||||
|
<SplitItem isFilled>
|
||||||
|
<WorkspaceCreationKindList
|
||||||
|
allWorkspaceKinds={allWorkspaceKinds}
|
||||||
|
onSelect={(workspaceKind) => setSelectedKind(workspaceKind)}
|
||||||
|
/>
|
||||||
|
</SplitItem>
|
||||||
|
<SplitItem style={{ minWidth: '200px' }}>{kindDetailsContent}</SplitItem>
|
||||||
|
</Split>
|
||||||
|
</Content>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { WorkspaceCreationKindSelection };
|
|
@ -0,0 +1,10 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { Content } from '@patternfly/react-core';
|
||||||
|
|
||||||
|
const WorkspaceCreationPropertiesSelection: React.FunctionComponent = () => (
|
||||||
|
<Content>
|
||||||
|
<p>Configure properties for your workspace.</p>
|
||||||
|
</Content>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { WorkspaceCreationPropertiesSelection };
|
|
@ -32,7 +32,8 @@ import {
|
||||||
QuestionCircleIcon,
|
QuestionCircleIcon,
|
||||||
CodeIcon,
|
CodeIcon,
|
||||||
} from '@patternfly/react-icons';
|
} from '@patternfly/react-icons';
|
||||||
import { useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Workspace, WorkspacesColumnNames, WorkspaceState } from '~/shared/types';
|
import { Workspace, WorkspacesColumnNames, WorkspaceState } from '~/shared/types';
|
||||||
import { WorkspaceDetails } from '~/app/pages/Workspaces/Details/WorkspaceDetails';
|
import { WorkspaceDetails } from '~/app/pages/Workspaces/Details/WorkspaceDetails';
|
||||||
import { ExpandedWorkspaceRow } from '~/app/pages/Workspaces/ExpandedWorkspaceRow';
|
import { ExpandedWorkspaceRow } from '~/app/pages/Workspaces/ExpandedWorkspaceRow';
|
||||||
|
@ -182,6 +183,11 @@ export const Workspaces: React.FunctionComponent = () => {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const createWorkspace = useCallback(() => {
|
||||||
|
navigate('/workspaces/create');
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
const [workspaceKinds] = useWorkspaceKinds();
|
const [workspaceKinds] = useWorkspaceKinds();
|
||||||
let kindLogoDict: Record<string, string> = {};
|
let kindLogoDict: Record<string, string> = {};
|
||||||
kindLogoDict = buildKindLogoDictionary(workspaceKinds);
|
kindLogoDict = buildKindLogoDictionary(workspaceKinds);
|
||||||
|
@ -554,7 +560,7 @@ export const Workspaces: React.FunctionComponent = () => {
|
||||||
<br />
|
<br />
|
||||||
<Content style={{ display: 'flex', alignItems: 'flex-start', columnGap: '20px' }}>
|
<Content style={{ display: 'flex', alignItems: 'flex-start', columnGap: '20px' }}>
|
||||||
<Filter id="filter-workspaces" onFilter={onFilter} columnNames={filterableColumns} />
|
<Filter id="filter-workspaces" onFilter={onFilter} columnNames={filterableColumns} />
|
||||||
<Button variant="primary" ouiaId="Primary">
|
<Button variant="primary" ouiaId="Primary" onClick={createWorkspace}>
|
||||||
Create Workspace
|
Create Workspace
|
||||||
</Button>
|
</Button>
|
||||||
</Content>
|
</Content>
|
||||||
|
|
|
@ -104,7 +104,6 @@
|
||||||
|
|
||||||
--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--border--radius--medium: 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);
|
||||||
|
|
||||||
--pf-t--global--color--nonstatus--blue--default: var(--mui-palette-primary-main);
|
--pf-t--global--color--nonstatus--blue--default: var(--mui-palette-primary-main);
|
||||||
|
@ -545,48 +544,6 @@
|
||||||
row-gap: none;
|
row-gap: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mui-theme .pf-v6-radio {
|
|
||||||
--pf-v6-c-radio--AccentColor: var(--mui-palette-primary-main);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mui-theme .pf-v6-c-radio {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin: var(--mui-radio--margin);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mui-theme .pf-v6-c-radio__input {
|
|
||||||
/* Hide default radio button */
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mui-theme .pf-v6-c-radio__label {
|
|
||||||
--pf-v6-c-radio__label--FontSize: 16px;
|
|
||||||
padding-left: var(--mui-radio-PaddingLeft);
|
|
||||||
position: relative;
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom radio circle */
|
|
||||||
.mui-theme .pf-v6-c-radio__label::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
width: var(--mui-radio__label--Width);
|
|
||||||
height: var(--mui-radio__label--Height);
|
|
||||||
border: 2px solid var(--mui-palette-primary-main);
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--mui-palette-common-white);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* When radio is checked */
|
|
||||||
.mui-theme .pf-v6-c-radio__input:checked+.pf-v6-c-radio__label::before {
|
|
||||||
background: var(--mui-palette-common-white);
|
|
||||||
border-color: var(--mui-palette-primary-main);
|
|
||||||
}
|
|
||||||
|
|
||||||
.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);
|
||||||
|
@ -598,20 +555,6 @@
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Inner dot for checked state */
|
|
||||||
.mui-theme .pf-v6-c-radio__input:checked+.pf-v6-c-radio__label::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 5px;
|
|
||||||
/* Center the dot */
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
width: var(--mui-radio__input--Width);
|
|
||||||
/* Size of inner dot */
|
|
||||||
height: var(--mui-radio__input--Height);
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--mui-palette-primary-main);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mui-theme .pf-v6-c-table {
|
.mui-theme .pf-v6-c-table {
|
||||||
--pf-v6-c-table__sort--m-selected__button--Color: var(--mui-palette-text-primary);
|
--pf-v6-c-table__sort--m-selected__button--Color: var(--mui-palette-text-primary);
|
||||||
|
@ -789,6 +732,10 @@
|
||||||
box-shadow: var(--mui-shadows-1);
|
box-shadow: var(--mui-shadows-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mui-theme .pf-v6-c-page__main-group.pf-m-sticky-top {
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.mui-theme .pf-v6-c-pagination {
|
.mui-theme .pf-v6-c-pagination {
|
||||||
--pf-v6-c-pagination__total-items--Display: block;
|
--pf-v6-c-pagination__total-items--Display: block;
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,14 +30,13 @@ export interface WorkspaceKind {
|
||||||
options: {
|
options: {
|
||||||
imageConfig: {
|
imageConfig: {
|
||||||
default: string;
|
default: string;
|
||||||
values: [
|
values: {
|
||||||
{
|
|
||||||
id: string;
|
id: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
labels: {
|
labels: {
|
||||||
pythonVersion: string;
|
pythonVersion: string;
|
||||||
};
|
};
|
||||||
hidden: true;
|
hidden: boolean;
|
||||||
redirect?: {
|
redirect?: {
|
||||||
to: string;
|
to: string;
|
||||||
message: {
|
message: {
|
||||||
|
@ -45,13 +44,11 @@ export interface WorkspaceKind {
|
||||||
level: string;
|
level: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
},
|
}[];
|
||||||
];
|
|
||||||
};
|
};
|
||||||
podConfig: {
|
podConfig: {
|
||||||
default: string;
|
default: string;
|
||||||
values: [
|
values: {
|
||||||
{
|
|
||||||
id: string;
|
id: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
@ -59,8 +56,14 @@ export interface WorkspaceKind {
|
||||||
cpu: string;
|
cpu: string;
|
||||||
memory: string;
|
memory: string;
|
||||||
};
|
};
|
||||||
},
|
redirect?: {
|
||||||
];
|
to: string;
|
||||||
|
message: {
|
||||||
|
text: string;
|
||||||
|
level: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue