feat(ws): add workspace creation image step frontend (#241)
Signed-off-by: paulovmr <832830+paulovmr@users.noreply.github.com>
This commit is contained in:
parent
f0638441d6
commit
657ac9f56f
|
@ -16,6 +16,7 @@ import { WorkspaceCreationImageSelection } from '~/app/pages/Workspaces/Creation
|
|||
import { WorkspaceCreationKindSelection } from '~/app/pages/Workspaces/Creation/WorkspaceCreationKindSelection';
|
||||
import { WorkspaceCreationPropertiesSelection } from '~/app/pages/Workspaces/Creation/WorkspaceCreationPropertiesSelection';
|
||||
import { WorkspaceCreationPodConfigSelection } from '~/app/pages/Workspaces/Creation/WorkspaceCreationPodConfigSelection';
|
||||
import { WorkspaceImage, WorkspaceKind } from '~/shared/types';
|
||||
|
||||
enum WorkspaceCreationSteps {
|
||||
KindSelection,
|
||||
|
@ -28,6 +29,8 @@ const WorkspaceCreation: React.FunctionComponent = () => {
|
|||
const navigate = useNavigate();
|
||||
|
||||
const [currentStep, setCurrentStep] = useState(WorkspaceCreationSteps.KindSelection);
|
||||
const [selectedKind, setSelectedKind] = useState<WorkspaceKind>();
|
||||
const [selectedImage, setSelectedImage] = useState<WorkspaceImage>();
|
||||
|
||||
const getStepVariant = useCallback(
|
||||
(step: WorkspaceCreationSteps) => {
|
||||
|
@ -54,6 +57,11 @@ const WorkspaceCreation: React.FunctionComponent = () => {
|
|||
navigate('/workspaces');
|
||||
}, [navigate]);
|
||||
|
||||
const onSelectWorkspaceKind = useCallback((newWorkspaceKind: WorkspaceKind) => {
|
||||
setSelectedKind(newWorkspaceKind);
|
||||
setSelectedImage(undefined);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageGroup stickyOnBreakpoint={{ default: 'top' }}>
|
||||
|
@ -124,9 +132,18 @@ const WorkspaceCreation: React.FunctionComponent = () => {
|
|||
</PageSection>
|
||||
</PageGroup>
|
||||
<PageSection isFilled>
|
||||
{currentStep === WorkspaceCreationSteps.KindSelection && <WorkspaceCreationKindSelection />}
|
||||
{currentStep === WorkspaceCreationSteps.KindSelection && (
|
||||
<WorkspaceCreationKindSelection
|
||||
selectedKind={selectedKind}
|
||||
onSelect={onSelectWorkspaceKind}
|
||||
/>
|
||||
)}
|
||||
{currentStep === WorkspaceCreationSteps.ImageSelection && (
|
||||
<WorkspaceCreationImageSelection />
|
||||
<WorkspaceCreationImageSelection
|
||||
selectedImage={selectedImage}
|
||||
images={selectedKind?.podTemplate.options.imageConfig.values ?? []}
|
||||
onSelect={setSelectedImage}
|
||||
/>
|
||||
)}
|
||||
{currentStep === WorkspaceCreationSteps.PodConfigSelection && (
|
||||
<WorkspaceCreationPodConfigSelection />
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import React from 'react';
|
||||
import { Title } from '@patternfly/react-core';
|
||||
import { WorkspaceImage } from '~/shared/types';
|
||||
|
||||
type WorkspaceCreationImageDetailsProps = {
|
||||
workspaceImage?: WorkspaceImage;
|
||||
};
|
||||
|
||||
export const WorkspaceCreationImageDetails: React.FunctionComponent<
|
||||
WorkspaceCreationImageDetailsProps
|
||||
> = ({ workspaceImage }) => (
|
||||
<>
|
||||
{!workspaceImage && <p>Select an image to view its details here.</p>}
|
||||
|
||||
{workspaceImage && (
|
||||
<>
|
||||
<Title headingLevel="h6">Image</Title>
|
||||
<Title headingLevel="h3">{workspaceImage.displayName}</Title>
|
||||
{Object.keys(workspaceImage.labels).map((labelKey) => (
|
||||
<p key={labelKey}>
|
||||
{labelKey}={workspaceImage.labels[labelKey]}
|
||||
</p>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
|
@ -0,0 +1,79 @@
|
|||
import React, { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
FilterSidePanel,
|
||||
FilterSidePanelCategory,
|
||||
FilterSidePanelCategoryItem,
|
||||
} from '@patternfly/react-catalog-view-extension';
|
||||
import { WorkspaceImage } from '~/shared/types';
|
||||
import '@patternfly/react-catalog-view-extension/dist/css/react-catalog-view-extension.css';
|
||||
|
||||
type WorkspaceCreationImageFilterProps = {
|
||||
images: WorkspaceImage[];
|
||||
selectedLabels: Map<string, Set<string>>;
|
||||
onSelect: (labels: Map<string, Set<string>>) => void;
|
||||
};
|
||||
|
||||
export const WorkspaceCreationImageFilter: React.FunctionComponent<
|
||||
WorkspaceCreationImageFilterProps
|
||||
> = ({ images, selectedLabels, onSelect }) => {
|
||||
const filterMap = useMemo(() => {
|
||||
const labelsMap = new Map<string, Set<string>>();
|
||||
images.forEach((image) => {
|
||||
Object.keys(image.labels).forEach((labelKey) => {
|
||||
const labelValue = image.labels[labelKey];
|
||||
if (!labelsMap.has(labelKey)) {
|
||||
labelsMap.set(labelKey, new Set<string>());
|
||||
}
|
||||
labelsMap.get(labelKey).add(labelValue);
|
||||
});
|
||||
});
|
||||
return labelsMap;
|
||||
}, [images]);
|
||||
|
||||
const isChecked = useCallback(
|
||||
(label, labelValue) => selectedLabels.get(label)?.has(labelValue),
|
||||
[selectedLabels],
|
||||
);
|
||||
|
||||
const onChange = useCallback(
|
||||
(labelKey, labelValue, event) => {
|
||||
const { checked } = event.currentTarget;
|
||||
const newSelectedLabels: Map<string, Set<string>> = new Map(selectedLabels);
|
||||
|
||||
if (checked) {
|
||||
if (!newSelectedLabels.has(labelKey)) {
|
||||
newSelectedLabels.set(labelKey, new Set<string>());
|
||||
}
|
||||
newSelectedLabels.get(labelKey).add(labelValue);
|
||||
} else {
|
||||
const labelValues = newSelectedLabels.get(labelKey);
|
||||
labelValues.delete(labelValue);
|
||||
if (labelValues.size === 0) {
|
||||
newSelectedLabels.delete(labelKey);
|
||||
}
|
||||
}
|
||||
|
||||
onSelect(newSelectedLabels);
|
||||
console.error(newSelectedLabels);
|
||||
},
|
||||
[selectedLabels, onSelect],
|
||||
);
|
||||
|
||||
return (
|
||||
<FilterSidePanel id="filter-panel">
|
||||
{[...filterMap.keys()].map((label) => (
|
||||
<FilterSidePanelCategory key={label} title={label}>
|
||||
{Array.from(filterMap.get(label).values()).map((labelValue) => (
|
||||
<FilterSidePanelCategoryItem
|
||||
key={`${label}|||${labelValue}`}
|
||||
checked={isChecked(label, labelValue)}
|
||||
onClick={(e) => onChange(label, labelValue, e)}
|
||||
>
|
||||
{labelValue}
|
||||
</FilterSidePanelCategoryItem>
|
||||
))}
|
||||
</FilterSidePanelCategory>
|
||||
))}
|
||||
</FilterSidePanel>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,143 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
CardTitle,
|
||||
Gallery,
|
||||
PageSection,
|
||||
Toolbar,
|
||||
ToolbarContent,
|
||||
Card,
|
||||
CardHeader,
|
||||
EmptyState,
|
||||
EmptyStateBody,
|
||||
CardBody,
|
||||
} from '@patternfly/react-core';
|
||||
import { SearchIcon } from '@patternfly/react-icons/dist/esm/icons/search-icon';
|
||||
import { WorkspaceImage } from '~/shared/types';
|
||||
import Filter, { FilteredColumn } from '~/shared/components/Filter';
|
||||
|
||||
type WorkspaceCreationImageListProps = {
|
||||
images: WorkspaceImage[];
|
||||
selectedLabels: Map<string, Set<string>>;
|
||||
selectedImage: WorkspaceImage | undefined;
|
||||
onSelect: (workspaceImage: WorkspaceImage) => void;
|
||||
};
|
||||
|
||||
export const WorkspaceCreationImageList: React.FunctionComponent<
|
||||
WorkspaceCreationImageListProps
|
||||
> = ({ images, selectedLabels, selectedImage, onSelect }) => {
|
||||
const [workspaceImages, setWorkspaceImages] = useState<WorkspaceImage[]>(images);
|
||||
const [filters, setFilters] = useState<FilteredColumn[]>([]);
|
||||
|
||||
const filterableColumns = useMemo(
|
||||
() => ({
|
||||
name: 'Name',
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const getFilteredWorkspaceImagesByLabels = useCallback(
|
||||
(unfilteredImages: WorkspaceImage[]) =>
|
||||
unfilteredImages.filter((image) =>
|
||||
Object.keys(image.labels).reduce((accumulator, labelKey) => {
|
||||
const labelValue = image.labels[labelKey];
|
||||
if (selectedLabels.has(labelKey)) {
|
||||
return accumulator && selectedLabels.get(labelKey).has(labelValue);
|
||||
}
|
||||
return accumulator;
|
||||
}, true),
|
||||
),
|
||||
[selectedLabels],
|
||||
);
|
||||
|
||||
const onChange = useCallback(
|
||||
(event: React.FormEvent<HTMLInputElement>) => {
|
||||
const newSelectedWorkspaceImage = workspaceImages.find(
|
||||
(image) => image.displayName === event.currentTarget.name,
|
||||
);
|
||||
onSelect(newSelectedWorkspaceImage);
|
||||
},
|
||||
[workspaceImages, onSelect],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Search name with search value
|
||||
let filteredWorkspaceImages = images;
|
||||
|
||||
filters.forEach((filter) => {
|
||||
let searchValueInput: RegExp;
|
||||
try {
|
||||
searchValueInput = new RegExp(filter.value, 'i');
|
||||
} catch {
|
||||
searchValueInput = new RegExp(filter.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
|
||||
}
|
||||
|
||||
filteredWorkspaceImages = filteredWorkspaceImages.filter((image) => {
|
||||
if (filter.value === '') {
|
||||
return true;
|
||||
}
|
||||
switch (filter.columnName) {
|
||||
case filterableColumns.name:
|
||||
return (
|
||||
image.id.search(searchValueInput) >= 0 ||
|
||||
image.displayName.search(searchValueInput) >= 0
|
||||
);
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
setWorkspaceImages(getFilteredWorkspaceImagesByLabels(filteredWorkspaceImages));
|
||||
}, [filterableColumns, filters, images, selectedLabels, getFilteredWorkspaceImagesByLabels]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageSection>
|
||||
<Toolbar id="toolbar-group-types">
|
||||
<ToolbarContent>
|
||||
<Filter
|
||||
id="filter-workspace-images"
|
||||
onFilter={setFilters}
|
||||
columnNames={filterableColumns}
|
||||
/>
|
||||
</ToolbarContent>
|
||||
</Toolbar>
|
||||
</PageSection>
|
||||
<PageSection isFilled>
|
||||
{workspaceImages.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>
|
||||
)}
|
||||
{workspaceImages.length > 0 && (
|
||||
<Gallery hasGutter aria-label="Selectable card container">
|
||||
{workspaceImages.map((image) => (
|
||||
<Card
|
||||
isCompact
|
||||
isSelectable
|
||||
key={image.id}
|
||||
id={image.id.replace(/ /g, '-')}
|
||||
isSelected={image.id === selectedImage?.id}
|
||||
>
|
||||
<CardHeader
|
||||
selectableActions={{
|
||||
selectableActionId: `selectable-actions-item-${image.id.replace(/ /g, '-')}`,
|
||||
selectableActionAriaLabelledby: image.displayName.replace(/ /g, '-'),
|
||||
name: image.displayName,
|
||||
variant: 'single',
|
||||
onChange,
|
||||
}}
|
||||
>
|
||||
<CardTitle>{image.displayName}</CardTitle>
|
||||
<CardBody>{image.id}</CardBody>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
))}
|
||||
</Gallery>
|
||||
)}
|
||||
</PageSection>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,10 +1,56 @@
|
|||
import * as React from 'react';
|
||||
import { Content } from '@patternfly/react-core';
|
||||
import { Content, Divider, Split, SplitItem } from '@patternfly/react-core';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { WorkspaceImage } from '~/shared/types';
|
||||
import { WorkspaceCreationImageDetails } from '~/app/pages/Workspaces/Creation/WorkspaceCreationImageDetails';
|
||||
import { WorkspaceCreationImageList } from '~/app/pages/Workspaces/Creation/WorkspaceCreationImageList';
|
||||
import { WorkspaceCreationImageFilter } from '~/app/pages/Workspaces/Creation/WorkspaceCreationImageFilter';
|
||||
|
||||
const WorkspaceCreationImageSelection: React.FunctionComponent = () => (
|
||||
<Content>
|
||||
<p>Select a workspace image and image version to use for the workspace.</p>
|
||||
</Content>
|
||||
);
|
||||
interface WorkspaceCreationImageSelectionProps {
|
||||
images: WorkspaceImage[];
|
||||
selectedImage: WorkspaceImage | undefined;
|
||||
onSelect: (image: WorkspaceImage) => void;
|
||||
}
|
||||
|
||||
const WorkspaceCreationImageSelection: React.FunctionComponent<
|
||||
WorkspaceCreationImageSelectionProps
|
||||
> = ({ images, selectedImage, onSelect }) => {
|
||||
const [selectedLabels, setSelectedLabels] = useState<Map<string, Set<string>>>(new Map());
|
||||
|
||||
const imageFilterContent = useMemo(
|
||||
() => (
|
||||
<WorkspaceCreationImageFilter
|
||||
images={images}
|
||||
selectedLabels={selectedLabels}
|
||||
onSelect={setSelectedLabels}
|
||||
/>
|
||||
),
|
||||
[images, selectedLabels, setSelectedLabels],
|
||||
);
|
||||
|
||||
const imageDetailsContent = useMemo(
|
||||
() => <WorkspaceCreationImageDetails workspaceImage={selectedImage} />,
|
||||
[selectedImage],
|
||||
);
|
||||
|
||||
return (
|
||||
<Content style={{ height: '100%' }}>
|
||||
<p>Select a workspace image and image version to use for the workspace.</p>
|
||||
<Divider />
|
||||
<Split hasGutter>
|
||||
<SplitItem style={{ minWidth: '200px' }}>{imageFilterContent}</SplitItem>
|
||||
<SplitItem isFilled>
|
||||
<WorkspaceCreationImageList
|
||||
images={images}
|
||||
selectedLabels={selectedLabels}
|
||||
selectedImage={selectedImage}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
</SplitItem>
|
||||
<SplitItem style={{ minWidth: '200px' }}>{imageDetailsContent}</SplitItem>
|
||||
</Split>
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
|
||||
export { WorkspaceCreationImageSelection };
|
||||
|
|
|
@ -17,15 +17,16 @@ import Filter, { FilteredColumn } from '~/shared/components/Filter';
|
|||
|
||||
type WorkspaceCreationKindListProps = {
|
||||
allWorkspaceKinds: WorkspaceKind[];
|
||||
selectedKind: WorkspaceKind | undefined;
|
||||
onSelect: (workspaceKind: WorkspaceKind) => void;
|
||||
};
|
||||
|
||||
export const WorkspaceCreationKindList: React.FunctionComponent<WorkspaceCreationKindListProps> = ({
|
||||
allWorkspaceKinds,
|
||||
selectedKind,
|
||||
onSelect,
|
||||
}) => {
|
||||
const [workspaceKinds, setWorkspaceKinds] = useState<WorkspaceKind[]>(allWorkspaceKinds);
|
||||
const [selectedWorkspaceKind, setSelectedWorkspaceKind] = useState<WorkspaceKind>();
|
||||
|
||||
const filterableColumns = useMemo(
|
||||
() => ({
|
||||
|
@ -71,7 +72,6 @@ export const WorkspaceCreationKindList: React.FunctionComponent<WorkspaceCreatio
|
|||
const newSelectedWorkspaceKind = workspaceKinds.find(
|
||||
(kind) => kind.name === event.currentTarget.name,
|
||||
);
|
||||
setSelectedWorkspaceKind(newSelectedWorkspaceKind);
|
||||
onSelect(newSelectedWorkspaceKind);
|
||||
},
|
||||
[workspaceKinds, onSelect],
|
||||
|
@ -106,11 +106,11 @@ export const WorkspaceCreationKindList: React.FunctionComponent<WorkspaceCreatio
|
|||
isSelectable
|
||||
key={kind.name}
|
||||
id={kind.name.replace(/ /g, '-')}
|
||||
isSelected={kind.name === selectedWorkspaceKind?.name}
|
||||
isSelected={kind.name === selectedKind?.name}
|
||||
>
|
||||
<CardHeader
|
||||
selectableActions={{
|
||||
selectableActionId: `selectable-actions-item-${kind.name}`,
|
||||
selectableActionId: `selectable-actions-item-${kind.name.replace(/ /g, '-')}`,
|
||||
selectableActionAriaLabelledby: kind.name.replace(/ /g, '-'),
|
||||
name: kind.name,
|
||||
variant: 'single',
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
import * as React from 'react';
|
||||
import { Content, Divider, Split, SplitItem } from '@patternfly/react-core';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useMemo } 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 = () => {
|
||||
interface WorkspaceCreationKindSelectionProps {
|
||||
selectedKind: WorkspaceKind | undefined;
|
||||
onSelect: (kind: WorkspaceKind) => void;
|
||||
}
|
||||
|
||||
const WorkspaceCreationKindSelection: React.FunctionComponent<
|
||||
WorkspaceCreationKindSelectionProps
|
||||
> = ({ selectedKind, onSelect }) => {
|
||||
/* Replace mocks below for BFF call */
|
||||
const mockedWorkspaceKind: WorkspaceKind = useMemo(
|
||||
() => ({
|
||||
|
@ -47,7 +54,7 @@ const WorkspaceCreationKindSelection: React.FunctionComponent = () => {
|
|||
{
|
||||
id: 'jupyterlab_scipy_190',
|
||||
displayName: 'jupyter-scipy:v1.9.0',
|
||||
labels: { pythonVersion: '3.11' },
|
||||
labels: { pythonVersion: '3.12' },
|
||||
hidden: true,
|
||||
redirect: {
|
||||
to: 'jupyterlab_scipy_200',
|
||||
|
@ -57,6 +64,32 @@ const WorkspaceCreationKindSelection: React.FunctionComponent = () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'jupyterlab_scipy_200',
|
||||
displayName: 'jupyter-scipy:v2.0.0',
|
||||
labels: { pythonVersion: '3.12' },
|
||||
hidden: true,
|
||||
redirect: {
|
||||
to: 'jupyterlab_scipy_210',
|
||||
message: {
|
||||
text: 'This update will change...',
|
||||
level: 'Warning',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'jupyterlab_scipy_210',
|
||||
displayName: 'jupyter-scipy:v2.1.0',
|
||||
labels: { pythonVersion: '3.13' },
|
||||
hidden: true,
|
||||
redirect: {
|
||||
to: 'jupyterlab_scipy_220',
|
||||
message: {
|
||||
text: 'This update will change...',
|
||||
level: 'Warning',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
podConfig: {
|
||||
|
@ -104,8 +137,6 @@ const WorkspaceCreationKindSelection: React.FunctionComponent = () => {
|
|||
return kinds;
|
||||
}, [mockedWorkspaceKind]);
|
||||
|
||||
const [selectedKind, setSelectedKind] = useState<WorkspaceKind>();
|
||||
|
||||
const kindDetailsContent = useMemo(
|
||||
() => <WorkspaceCreationKindDetails workspaceKind={selectedKind} />,
|
||||
[selectedKind],
|
||||
|
@ -119,7 +150,8 @@ const WorkspaceCreationKindSelection: React.FunctionComponent = () => {
|
|||
<SplitItem isFilled>
|
||||
<WorkspaceCreationKindList
|
||||
allWorkspaceKinds={allWorkspaceKinds}
|
||||
onSelect={(workspaceKind) => setSelectedKind(workspaceKind)}
|
||||
selectedKind={selectedKind}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
</SplitItem>
|
||||
<SplitItem style={{ minWidth: '200px' }}>{kindDetailsContent}</SplitItem>
|
||||
|
|
|
@ -6,6 +6,39 @@ export interface WorkspaceLogo {
|
|||
url: string;
|
||||
}
|
||||
|
||||
export interface WorkspaceImage {
|
||||
id: string;
|
||||
displayName: string;
|
||||
labels: {
|
||||
pythonVersion: string;
|
||||
};
|
||||
hidden: boolean;
|
||||
redirect?: {
|
||||
to: string;
|
||||
message: {
|
||||
text: string;
|
||||
level: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface WorkspacePodConfig {
|
||||
id: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
labels: {
|
||||
cpu: string;
|
||||
memory: string;
|
||||
};
|
||||
redirect?: {
|
||||
to: string;
|
||||
message: {
|
||||
text: string;
|
||||
level: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface WorkspaceKind {
|
||||
name: string;
|
||||
displayName: string;
|
||||
|
@ -30,40 +63,11 @@ export interface WorkspaceKind {
|
|||
options: {
|
||||
imageConfig: {
|
||||
default: string;
|
||||
values: {
|
||||
id: string;
|
||||
displayName: string;
|
||||
labels: {
|
||||
pythonVersion: string;
|
||||
};
|
||||
hidden: boolean;
|
||||
redirect?: {
|
||||
to: string;
|
||||
message: {
|
||||
text: string;
|
||||
level: string;
|
||||
};
|
||||
};
|
||||
}[];
|
||||
values: WorkspaceImage[];
|
||||
};
|
||||
podConfig: {
|
||||
default: string;
|
||||
values: {
|
||||
id: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
labels: {
|
||||
cpu: string;
|
||||
memory: string;
|
||||
};
|
||||
redirect?: {
|
||||
to: string;
|
||||
message: {
|
||||
text: string;
|
||||
level: string;
|
||||
};
|
||||
};
|
||||
}[];
|
||||
values: WorkspacePodConfig[];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue