mirror of
https://github.com/portainer/portainer.git
synced 2025-08-02 20:35:25 +02:00
feat(gpu): rework docker GPU for UI performance [EE-4918] (#8518)
This commit is contained in:
parent
769c8372fb
commit
fd916bc8a2
52 changed files with 692 additions and 285 deletions
18
app/react/components/InsightsBox/InsightsBox.stories.tsx
Normal file
18
app/react/components/InsightsBox/InsightsBox.stories.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { Meta, Story } from '@storybook/react';
|
||||
|
||||
import { InsightsBox, Props } from './InsightsBox';
|
||||
|
||||
export default {
|
||||
component: InsightsBox,
|
||||
header: 'Components/InsightsBox',
|
||||
} as Meta;
|
||||
|
||||
function Template({ header, content }: Props) {
|
||||
return <InsightsBox header={header} content={content} />;
|
||||
}
|
||||
|
||||
export const Primary: Story<Props> = Template.bind({});
|
||||
Primary.args = {
|
||||
header: 'Insights box header',
|
||||
content: 'This is the content of the insights box',
|
||||
};
|
16
app/react/components/InsightsBox/InsightsBox.test.tsx
Normal file
16
app/react/components/InsightsBox/InsightsBox.test.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { render } from '@testing-library/react';
|
||||
|
||||
import { InsightsBox } from './InsightsBox';
|
||||
|
||||
test('should display a InsightsBox with a header and content', async () => {
|
||||
const header = 'test header';
|
||||
const content = 'test content';
|
||||
const { findByText } = render(
|
||||
<InsightsBox header={header} content={content} />
|
||||
);
|
||||
|
||||
const headerFound = await findByText(header);
|
||||
expect(headerFound).toBeTruthy();
|
||||
const contentFound = await findByText(content);
|
||||
expect(contentFound).toBeTruthy();
|
||||
});
|
63
app/react/components/InsightsBox/InsightsBox.tsx
Normal file
63
app/react/components/InsightsBox/InsightsBox.tsx
Normal file
|
@ -0,0 +1,63 @@
|
|||
import clsx from 'clsx';
|
||||
import { Lightbulb, X } from 'lucide-react';
|
||||
import { ReactNode, useMemo } from 'react';
|
||||
import sanitize from 'sanitize-html';
|
||||
import { useStore } from 'zustand';
|
||||
|
||||
import { Button } from '@@/buttons';
|
||||
|
||||
import { insightStore } from './insights-store';
|
||||
|
||||
export type Props = {
|
||||
header: string;
|
||||
content: ReactNode;
|
||||
setHtmlContent?: boolean;
|
||||
insightCloseId?: string; // set if you want to be able to close the box and not show it again
|
||||
};
|
||||
|
||||
export function InsightsBox({
|
||||
header,
|
||||
content,
|
||||
setHtmlContent,
|
||||
insightCloseId,
|
||||
}: Props) {
|
||||
// allow to close the box and not show it again in local storage with zustand
|
||||
const { addInsightIDClosed, isClosed } = useStore(insightStore);
|
||||
const isInsightClosed = isClosed(insightCloseId);
|
||||
|
||||
// allow angular views to set html messages for the insights box
|
||||
const htmlContent = useMemo(() => {
|
||||
if (setHtmlContent && typeof content === 'string') {
|
||||
// eslint-disable-next-line react/no-danger
|
||||
return <div dangerouslySetInnerHTML={{ __html: sanitize(content) }} />;
|
||||
}
|
||||
return null;
|
||||
}, [setHtmlContent, content]);
|
||||
|
||||
if (isInsightClosed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative flex w-full gap-1 rounded-lg bg-gray-modern-3 p-4 text-sm th-highcontrast:bg-legacy-grey-3 th-dark:bg-legacy-grey-3">
|
||||
<div className="shrink-0">
|
||||
<Lightbulb className="h-4 text-warning-7 th-highcontrast:text-warning-6 th-dark:text-warning-6" />
|
||||
</div>
|
||||
<div>
|
||||
<p className={clsx('mb-2 font-bold', insightCloseId && 'pr-4')}>
|
||||
{header}
|
||||
</p>
|
||||
<div>{htmlContent || content}</div>
|
||||
</div>
|
||||
{insightCloseId && (
|
||||
<Button
|
||||
icon={X}
|
||||
className="absolute top-2 right-2 flex !text-gray-7 hover:!text-gray-8 th-highcontrast:!text-gray-6 th-highcontrast:hover:!text-gray-5 th-dark:!text-gray-6 th-dark:hover:!text-gray-5"
|
||||
color="link"
|
||||
size="medium"
|
||||
onClick={() => addInsightIDClosed(insightCloseId)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
1
app/react/components/InsightsBox/index.ts
Normal file
1
app/react/components/InsightsBox/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { InsightsBox } from './InsightsBox';
|
35
app/react/components/InsightsBox/insights-store.ts
Normal file
35
app/react/components/InsightsBox/insights-store.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { createStore } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
import { keyBuilder } from '@/react/hooks/useLocalStorage';
|
||||
|
||||
interface InsightsStore {
|
||||
insightIDsClosed: string[];
|
||||
addInsightIDClosed: (insightIDClosed: string) => void;
|
||||
isClosed: (insightID?: string) => boolean;
|
||||
}
|
||||
|
||||
export const insightStore = createStore<InsightsStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
insightIDsClosed: [],
|
||||
addInsightIDClosed: (insightIDClosed: string) => {
|
||||
set((state) => {
|
||||
const currentIDsClosed = state.insightIDsClosed || [];
|
||||
return { insightIDsClosed: [...currentIDsClosed, insightIDClosed] };
|
||||
});
|
||||
},
|
||||
isClosed: (insightID?: string) => {
|
||||
if (!insightID) {
|
||||
return false;
|
||||
}
|
||||
const currentIDsClosed = get().insightIDsClosed || [];
|
||||
return currentIDsClosed.includes(insightID);
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: keyBuilder('insightIDsClosed'),
|
||||
getStorage: () => localStorage,
|
||||
}
|
||||
)
|
||||
);
|
|
@ -1,7 +1,3 @@
|
|||
.items {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.items > * + * {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
@ -24,11 +20,6 @@
|
|||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
display: flex;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.default-item {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { ComponentType } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { FormikErrors } from 'formik';
|
||||
import { ArrowDown, ArrowUp, Trash2 } from 'lucide-react';
|
||||
import { ArrowDown, ArrowUp, Plus, Trash2 } from 'lucide-react';
|
||||
|
||||
import { AddButton, Button } from '@@/buttons';
|
||||
import { Button } from '@@/buttons';
|
||||
import { Tooltip } from '@@/Tip/Tooltip';
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
|
||||
|
@ -79,18 +79,10 @@ export function InputList<T = DefaultType>({
|
|||
return (
|
||||
<div className={clsx('form-group', styles.root)}>
|
||||
<div className={clsx('col-sm-12', styles.header)}>
|
||||
<div className={clsx('control-label text-left', styles.label)}>
|
||||
<span className="control-label space-right pt-2 text-left !font-bold">
|
||||
{label}
|
||||
{tooltip && <Tooltip message={tooltip} />}
|
||||
</div>
|
||||
{!(isAddButtonHidden || readOnly) && (
|
||||
<AddButton
|
||||
label={addLabel}
|
||||
className="space-left"
|
||||
onClick={handleAdd}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{textTip && (
|
||||
|
@ -99,68 +91,86 @@ export function InputList<T = DefaultType>({
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div className={clsx('col-sm-12', styles.items, 'space-y-4')}>
|
||||
{value.map((item, index) => {
|
||||
const key = itemKeyGetter(item, index);
|
||||
const error = typeof errors === 'object' ? errors[index] : undefined;
|
||||
{value.length > 0 && (
|
||||
<div className="col-sm-12 mt-5 flex flex-col gap-y-5">
|
||||
{value.map((item, index) => {
|
||||
const key = itemKeyGetter(item, index);
|
||||
const error =
|
||||
typeof errors === 'object' ? errors[index] : undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className={clsx(
|
||||
styles.itemLine,
|
||||
{ [styles.hasError]: !!error },
|
||||
'vertical-center'
|
||||
)}
|
||||
>
|
||||
{Item ? (
|
||||
<Item
|
||||
item={item}
|
||||
onChange={(value: T) => handleChangeItem(key, value)}
|
||||
error={error}
|
||||
disabled={disabled}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
) : (
|
||||
renderItem(
|
||||
item,
|
||||
(value: T) => handleChangeItem(key, value),
|
||||
error
|
||||
)
|
||||
)}
|
||||
<div className={clsx(styles.itemActions, 'items-start')}>
|
||||
{!readOnly && movable && (
|
||||
<>
|
||||
<Button
|
||||
size="medium"
|
||||
disabled={disabled || index === 0}
|
||||
onClick={() => handleMoveUp(index)}
|
||||
className="vertical-center btn-only-icon"
|
||||
icon={ArrowUp}
|
||||
/>
|
||||
<Button
|
||||
size="medium"
|
||||
type="button"
|
||||
disabled={disabled || index === value.length - 1}
|
||||
onClick={() => handleMoveDown(index)}
|
||||
className="vertical-center btn-only-icon"
|
||||
icon={ArrowDown}
|
||||
/>
|
||||
</>
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className={clsx(
|
||||
styles.itemLine,
|
||||
{ [styles.hasError]: !!error },
|
||||
'vertical-center'
|
||||
)}
|
||||
{!readOnly && (
|
||||
<Button
|
||||
color="dangerlight"
|
||||
size="medium"
|
||||
onClick={() => handleRemoveItem(key, item)}
|
||||
className="vertical-center btn-only-icon"
|
||||
icon={Trash2}
|
||||
>
|
||||
{Item ? (
|
||||
<Item
|
||||
item={item}
|
||||
onChange={(value: T) => handleChangeItem(key, value)}
|
||||
error={error}
|
||||
disabled={disabled}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
) : (
|
||||
renderItem(
|
||||
item,
|
||||
(value: T) => handleChangeItem(key, value),
|
||||
error
|
||||
)
|
||||
)}
|
||||
<div className="items-start">
|
||||
{!readOnly && movable && (
|
||||
<>
|
||||
<Button
|
||||
size="medium"
|
||||
disabled={disabled || index === 0}
|
||||
onClick={() => handleMoveUp(index)}
|
||||
className="vertical-center btn-only-icon"
|
||||
icon={ArrowUp}
|
||||
/>
|
||||
<Button
|
||||
size="medium"
|
||||
type="button"
|
||||
disabled={disabled || index === value.length - 1}
|
||||
onClick={() => handleMoveDown(index)}
|
||||
className="vertical-center btn-only-icon"
|
||||
icon={ArrowDown}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{!readOnly && (
|
||||
<Button
|
||||
color="dangerlight"
|
||||
size="medium"
|
||||
onClick={() => handleRemoveItem(key, item)}
|
||||
className="vertical-center btn-only-icon"
|
||||
icon={Trash2}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<div className="col-sm-12 mt-5">
|
||||
{!(isAddButtonHidden || readOnly) && (
|
||||
<Button
|
||||
onClick={handleAdd}
|
||||
disabled={disabled}
|
||||
type="button"
|
||||
color="default"
|
||||
className="!ml-0"
|
||||
size="small"
|
||||
icon={Plus}
|
||||
>
|
||||
{addLabel}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -10,6 +10,7 @@ import { OptionProps } from 'react-select/dist/declarations/src/components/Optio
|
|||
import { Select } from '@@/form-components/ReactSelect';
|
||||
import { Switch } from '@@/form-components/SwitchField/Switch';
|
||||
import { Tooltip } from '@@/Tip/Tooltip';
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
|
||||
interface Values {
|
||||
enabled: boolean;
|
||||
|
@ -35,6 +36,7 @@ export interface Props {
|
|||
gpus: GPU[];
|
||||
usedGpus: string[];
|
||||
usedAllGpus: boolean;
|
||||
enableGpuManagement?: boolean;
|
||||
}
|
||||
|
||||
const NvidiaCapabilitiesOptions = [
|
||||
|
@ -103,6 +105,7 @@ export function Gpu({
|
|||
gpus = [],
|
||||
usedGpus = [],
|
||||
usedAllGpus,
|
||||
enableGpuManagement,
|
||||
}: Props) {
|
||||
const options = useMemo(() => {
|
||||
const options = (gpus || []).map((gpu) => ({
|
||||
|
@ -181,30 +184,38 @@ export function Gpu({
|
|||
|
||||
return (
|
||||
<div>
|
||||
{!enableGpuManagement && (
|
||||
<TextTip color="blue">
|
||||
GPU in the UI is not currently enabled for this environment.
|
||||
</TextTip>
|
||||
)}
|
||||
<div className="form-group">
|
||||
<div className="col-sm-3 col-lg-2 control-label text-left">
|
||||
Enable GPU
|
||||
<Switch
|
||||
id="enabled"
|
||||
name="enabled"
|
||||
checked={values.enabled}
|
||||
checked={values.enabled && !!enableGpuManagement}
|
||||
onChange={toggleEnableGpu}
|
||||
className="ml-2"
|
||||
disabled={enableGpuManagement === false}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-sm-9 col-lg-10 text-left">
|
||||
<Select<GpuOption, true>
|
||||
isMulti
|
||||
closeMenuOnSelect
|
||||
value={gpuValue}
|
||||
isClearable={false}
|
||||
backspaceRemovesValue={false}
|
||||
isDisabled={!values.enabled}
|
||||
onChange={onChangeSelectedGpus}
|
||||
options={options}
|
||||
components={{ MultiValueRemove }}
|
||||
/>
|
||||
</div>
|
||||
{enableGpuManagement && values.enabled && (
|
||||
<div className="col-sm-9 col-lg-10 text-left">
|
||||
<Select<GpuOption, true>
|
||||
isMulti
|
||||
closeMenuOnSelect
|
||||
value={gpuValue}
|
||||
isClearable={false}
|
||||
backspaceRemovesValue={false}
|
||||
isDisabled={!values.enabled}
|
||||
onChange={onChangeSelectedGpus}
|
||||
options={options}
|
||||
components={{ MultiValueRemove }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{values.enabled && (
|
||||
|
|
|
@ -15,7 +15,7 @@ import { gpus } from './gpus';
|
|||
|
||||
export function useColumns(
|
||||
isHostColumnVisible: boolean,
|
||||
isGPUsColumnVisible: boolean
|
||||
isGPUsColumnVisible?: boolean
|
||||
) {
|
||||
return useMemo(
|
||||
() =>
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
import { useInfo } from '@/docker/services/system.service';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
import { useInfo } from '@/docker/services/system.service';
|
||||
import { useEnvironment } from '@/react/portainer/environments/queries';
|
||||
|
||||
import { DockerContainer, ContainerStatus } from './types';
|
||||
import { DockerContainerResponse } from './types/response';
|
||||
|
@ -95,10 +96,13 @@ function createStatus(statusText = ''): ContainerStatus {
|
|||
}
|
||||
|
||||
export function useShowGPUsColumn(environmentID: EnvironmentId) {
|
||||
const envInfoQuery = useInfo(
|
||||
const isDockerStandaloneQuery = useInfo(
|
||||
environmentID,
|
||||
(info) => !!info.Swarm?.NodeID && !!info.Swarm?.ControlAvailable
|
||||
(info) => !(!!info.Swarm?.NodeID && !!info.Swarm?.ControlAvailable) // is not a swarm environment, therefore docker standalone
|
||||
);
|
||||
|
||||
return envInfoQuery.data !== true && !envInfoQuery.isLoading;
|
||||
const enableGPUManagementQuery = useEnvironment(
|
||||
environmentID,
|
||||
(env) => env?.EnableGPUManagement
|
||||
);
|
||||
return isDockerStandaloneQuery.data && enableGPUManagementQuery.data;
|
||||
}
|
||||
|
|
|
@ -48,11 +48,12 @@ function Item({ item, onChange }: ItemProps<Gpu>) {
|
|||
export function GpusList({ value, onChange }: Props) {
|
||||
return (
|
||||
<InputList<Gpu>
|
||||
label="GPU"
|
||||
label="GPUs"
|
||||
tooltip="You may optionally set up the GPUs that will be selectable against containers, although 'All GPUs' will always be available."
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
itemBuilder={() => ({ value: '', name: '' })}
|
||||
addLabel="add"
|
||||
addLabel="Add GPU"
|
||||
item={Item}
|
||||
/>
|
||||
);
|
|
@ -1,4 +1,3 @@
|
|||
import { Gpu } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList';
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { type EnvironmentGroupId } from '@/react/portainer/environments/environment-groups/types';
|
||||
import { type TagId } from '@/portainer/tags/types';
|
||||
|
@ -18,7 +17,6 @@ interface CreateLocalDockerEnvironment {
|
|||
socketPath?: string;
|
||||
publicUrl?: string;
|
||||
meta?: EnvironmentMetadata;
|
||||
gpus?: Gpu[];
|
||||
}
|
||||
|
||||
export async function createLocalDockerEnvironment({
|
||||
|
@ -26,7 +24,6 @@ export async function createLocalDockerEnvironment({
|
|||
socketPath = '',
|
||||
publicUrl = '',
|
||||
meta = { tagIds: [] },
|
||||
gpus = [],
|
||||
}: CreateLocalDockerEnvironment) {
|
||||
const url = prefixPath(socketPath);
|
||||
|
||||
|
@ -37,7 +34,6 @@ export async function createLocalDockerEnvironment({
|
|||
url,
|
||||
publicUrl,
|
||||
meta,
|
||||
gpus,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -113,7 +109,6 @@ export interface EnvironmentOptions {
|
|||
azure?: AzureSettings;
|
||||
tls?: TLSSettings;
|
||||
isEdgeDevice?: boolean;
|
||||
gpus?: Gpu[];
|
||||
pollFrequency?: number;
|
||||
edge?: EdgeSettings;
|
||||
tunnelServerAddr?: string;
|
||||
|
@ -145,7 +140,6 @@ export interface CreateAgentEnvironmentValues {
|
|||
name: string;
|
||||
environmentUrl: string;
|
||||
meta: EnvironmentMetadata;
|
||||
gpus: Gpu[];
|
||||
}
|
||||
|
||||
export function createAgentEnvironment({
|
||||
|
@ -173,7 +167,6 @@ interface CreateEdgeAgentEnvironment {
|
|||
tunnelServerAddr?: string;
|
||||
meta?: EnvironmentMetadata;
|
||||
pollFrequency: number;
|
||||
gpus?: Gpu[];
|
||||
isEdgeDevice?: boolean;
|
||||
edge: EdgeSettings;
|
||||
}
|
||||
|
@ -182,7 +175,6 @@ export function createEdgeAgentEnvironment({
|
|||
name,
|
||||
portainerUrl,
|
||||
meta = { tagIds: [] },
|
||||
gpus = [],
|
||||
isEdgeDevice,
|
||||
pollFrequency,
|
||||
edge,
|
||||
|
@ -196,7 +188,6 @@ export function createEdgeAgentEnvironment({
|
|||
skipVerify: true,
|
||||
skipClientVerify: true,
|
||||
},
|
||||
gpus,
|
||||
isEdgeDevice,
|
||||
pollFrequency,
|
||||
edge,
|
||||
|
@ -226,7 +217,6 @@ async function createEnvironment(
|
|||
TagIds: arrayToJson(tagIds),
|
||||
CheckinInterval: options.pollFrequency,
|
||||
IsEdgeDevice: options.isEdgeDevice,
|
||||
Gpus: arrayToJson(options.gpus),
|
||||
};
|
||||
|
||||
const { tls, azure } = options;
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
import { useQuery } from 'react-query';
|
||||
|
||||
import { getEndpoint } from '@/react/portainer/environments/environment.service';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
import {
|
||||
Environment,
|
||||
EnvironmentId,
|
||||
} from '@/react/portainer/environments/types';
|
||||
import { withError } from '@/react-tools/react-query';
|
||||
|
||||
export function useEnvironment(id?: EnvironmentId) {
|
||||
export function useEnvironment<T = Environment | null>(
|
||||
id?: EnvironmentId,
|
||||
select?: (environment: Environment | null) => T
|
||||
) {
|
||||
return useQuery(['environments', id], () => (id ? getEndpoint(id) : null), {
|
||||
select,
|
||||
...withError('Failed loading environment'),
|
||||
staleTime: 50,
|
||||
enabled: !!id,
|
||||
|
|
|
@ -131,6 +131,7 @@ export type Environment = {
|
|||
TagIds: TagId[];
|
||||
GroupId: EnvironmentGroupId;
|
||||
DeploymentOptions: DeploymentOptions | null;
|
||||
EnableGPUManagement: boolean;
|
||||
EdgeID?: string;
|
||||
EdgeKey: string;
|
||||
EdgeCheckinInterval?: number;
|
||||
|
|
|
@ -3,7 +3,6 @@ import { useReducer } from 'react';
|
|||
import { Plug2 } from 'lucide-react';
|
||||
|
||||
import { useCreateRemoteEnvironmentMutation } from '@/react/portainer/environments/queries/useCreateEnvironmentMutation';
|
||||
import { Hardware } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/Hardware';
|
||||
import { notifySuccess } from '@/portainer/services/notifications';
|
||||
import {
|
||||
Environment,
|
||||
|
@ -13,6 +12,7 @@ import {
|
|||
import { LoadingButton } from '@@/buttons/LoadingButton';
|
||||
import { FormControl } from '@@/form-components/FormControl';
|
||||
import { Input } from '@@/form-components/Input';
|
||||
import { InsightsBox } from '@@/InsightsBox';
|
||||
|
||||
import { NameField } from '../../shared/NameField';
|
||||
import { MoreSettingsSection } from '../../shared/MoreSettingsSection';
|
||||
|
@ -23,9 +23,10 @@ import { TLSFieldset } from './TLSFieldset';
|
|||
|
||||
interface Props {
|
||||
onCreate(environment: Environment): void;
|
||||
isDockerStandalone?: boolean;
|
||||
}
|
||||
|
||||
export function APIForm({ onCreate }: Props) {
|
||||
export function APIForm({ onCreate, isDockerStandalone }: Props) {
|
||||
const [formKey, clearForm] = useReducer((state) => state + 1, 0);
|
||||
const initialValues: FormValues = {
|
||||
url: '',
|
||||
|
@ -35,7 +36,6 @@ export function APIForm({ onCreate }: Props) {
|
|||
groupId: 1,
|
||||
tagIds: [],
|
||||
},
|
||||
gpus: [],
|
||||
};
|
||||
|
||||
const mutation = useCreateRemoteEnvironmentMutation(
|
||||
|
@ -73,7 +73,33 @@ export function APIForm({ onCreate }: Props) {
|
|||
<TLSFieldset />
|
||||
|
||||
<MoreSettingsSection>
|
||||
<Hardware />
|
||||
{isDockerStandalone && (
|
||||
<InsightsBox
|
||||
content={
|
||||
<>
|
||||
<p>
|
||||
From 2.18 on, the set-up of available GPUs for a Docker
|
||||
Standalone environment has been shifted from Add
|
||||
environment and Environment details to Host -> Setup,
|
||||
so as to align with other settings.
|
||||
</p>
|
||||
<p>
|
||||
A toggle has been introduced for enabling/disabling
|
||||
management of GPU settings in the Portainer UI - to
|
||||
alleviate the performance impact of showing those
|
||||
settings.
|
||||
</p>
|
||||
<p>
|
||||
The UI has been updated to clarify that GPU settings
|
||||
support is only for Docker Standalone (and not Docker
|
||||
Swarm, which was never supported in the UI).
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
header="GPU settings update"
|
||||
insightCloseId="gpu-settings-update-closed"
|
||||
/>
|
||||
)}
|
||||
</MoreSettingsSection>
|
||||
|
||||
<div className="form-group">
|
||||
|
@ -104,7 +130,6 @@ export function APIForm({ onCreate }: Props) {
|
|||
options: {
|
||||
tls,
|
||||
meta: values.meta,
|
||||
gpus: values.gpus,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import { boolean, object, SchemaOf, string } from 'yup';
|
||||
|
||||
import { gpusListValidation } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList';
|
||||
|
||||
import { metadataValidation } from '../../shared/MetadataFieldset/validation';
|
||||
import { useNameValidation } from '../../shared/NameField';
|
||||
|
||||
|
@ -16,6 +14,5 @@ export function useValidation(): SchemaOf<FormValues> {
|
|||
skipVerify: boolean(),
|
||||
meta: metadataValidation(),
|
||||
...certsValidation(),
|
||||
gpus: gpusListValidation(),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -4,12 +4,13 @@ import { APIForm } from './APIForm';
|
|||
|
||||
interface Props {
|
||||
onCreate(environment: Environment): void;
|
||||
isDockerStandalone?: boolean;
|
||||
}
|
||||
|
||||
export function APITab({ onCreate }: Props) {
|
||||
export function APITab({ onCreate, isDockerStandalone }: Props) {
|
||||
return (
|
||||
<div className="mt-5">
|
||||
<APIForm onCreate={onCreate} />
|
||||
<APIForm onCreate={onCreate} isDockerStandalone={isDockerStandalone} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { Gpu } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList';
|
||||
import { EnvironmentMetadata } from '@/react/portainer/environments/environment.service/create';
|
||||
|
||||
export interface FormValues {
|
||||
|
@ -10,5 +9,4 @@ export interface FormValues {
|
|||
certFile?: File;
|
||||
keyFile?: File;
|
||||
meta: EnvironmentMetadata;
|
||||
gpus?: Gpu[];
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ import { useReducer } from 'react';
|
|||
import { Plug2 } from 'lucide-react';
|
||||
|
||||
import { useCreateLocalDockerEnvironmentMutation } from '@/react/portainer/environments/queries/useCreateEnvironmentMutation';
|
||||
import { Hardware } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/Hardware';
|
||||
import { notifySuccess } from '@/portainer/services/notifications';
|
||||
import { Environment } from '@/react/portainer/environments/types';
|
||||
|
||||
|
@ -11,6 +10,7 @@ import { LoadingButton } from '@@/buttons/LoadingButton';
|
|||
import { FormControl } from '@@/form-components/FormControl';
|
||||
import { Input } from '@@/form-components/Input';
|
||||
import { SwitchField } from '@@/form-components/SwitchField';
|
||||
import { InsightsBox } from '@@/InsightsBox';
|
||||
|
||||
import { NameField } from '../../shared/NameField';
|
||||
import { MoreSettingsSection } from '../../shared/MoreSettingsSection';
|
||||
|
@ -20,16 +20,16 @@ import { FormValues } from './types';
|
|||
|
||||
interface Props {
|
||||
onCreate(environment: Environment): void;
|
||||
isDockerStandalone?: boolean;
|
||||
}
|
||||
|
||||
export function SocketForm({ onCreate }: Props) {
|
||||
export function SocketForm({ onCreate, isDockerStandalone }: Props) {
|
||||
const [formKey, clearForm] = useReducer((state) => state + 1, 0);
|
||||
const initialValues: FormValues = {
|
||||
name: '',
|
||||
socketPath: '',
|
||||
overridePath: false,
|
||||
meta: { groupId: 1, tagIds: [] },
|
||||
gpus: [],
|
||||
};
|
||||
|
||||
const mutation = useCreateLocalDockerEnvironmentMutation();
|
||||
|
@ -50,7 +50,33 @@ export function SocketForm({ onCreate }: Props) {
|
|||
<OverrideSocketFieldset />
|
||||
|
||||
<MoreSettingsSection>
|
||||
<Hardware />
|
||||
{isDockerStandalone && (
|
||||
<InsightsBox
|
||||
content={
|
||||
<>
|
||||
<p>
|
||||
From 2.18 on, the set-up of available GPUs for a Docker
|
||||
Standalone environment has been shifted from Add
|
||||
environment and Environment details to Host -> Setup,
|
||||
so as to align with other settings.
|
||||
</p>
|
||||
<p>
|
||||
A toggle has been introduced for enabling/disabling
|
||||
management of GPU settings in the Portainer UI - to
|
||||
alleviate the performance impact of showing those
|
||||
settings.
|
||||
</p>
|
||||
<p>
|
||||
The UI has been updated to clarify that GPU settings
|
||||
support is only for Docker Standalone (and not Docker
|
||||
Swarm, which was never supported in the UI).
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
header="GPU settings update"
|
||||
insightCloseId="gpu-settings-update-closed"
|
||||
/>
|
||||
)}
|
||||
</MoreSettingsSection>
|
||||
|
||||
<div className="form-group">
|
||||
|
@ -76,7 +102,6 @@ export function SocketForm({ onCreate }: Props) {
|
|||
{
|
||||
name: values.name,
|
||||
socketPath: values.overridePath ? values.socketPath : '',
|
||||
gpus: values.gpus,
|
||||
meta: values.meta,
|
||||
},
|
||||
{
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import { boolean, object, SchemaOf, string } from 'yup';
|
||||
|
||||
import { gpusListValidation } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList';
|
||||
|
||||
import { metadataValidation } from '../../shared/MetadataFieldset/validation';
|
||||
import { useNameValidation } from '../../shared/NameField';
|
||||
|
||||
|
@ -21,6 +19,5 @@ export function useValidation(): SchemaOf<FormValues> {
|
|||
)
|
||||
: schema
|
||||
),
|
||||
gpus: gpusListValidation(),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -6,15 +6,19 @@ import { SocketForm } from './SocketForm';
|
|||
|
||||
interface Props {
|
||||
onCreate(environment: Environment): void;
|
||||
isDockerStandalone?: boolean;
|
||||
}
|
||||
|
||||
export function SocketTab({ onCreate }: Props) {
|
||||
export function SocketTab({ onCreate, isDockerStandalone }: Props) {
|
||||
return (
|
||||
<>
|
||||
<DeploymentScripts />
|
||||
|
||||
<div className="mt-5">
|
||||
<SocketForm onCreate={onCreate} />
|
||||
<SocketForm
|
||||
onCreate={onCreate}
|
||||
isDockerStandalone={isDockerStandalone}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { Gpu } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList';
|
||||
import { EnvironmentMetadata } from '@/react/portainer/environments/environment.service/create';
|
||||
|
||||
export interface FormValues {
|
||||
|
@ -6,5 +5,4 @@ export interface FormValues {
|
|||
socketPath: string;
|
||||
overridePath: boolean;
|
||||
meta: EnvironmentMetadata;
|
||||
gpus: Gpu[];
|
||||
}
|
||||
|
|
|
@ -109,12 +109,14 @@ export function WizardDocker({ onCreate, isDockerStandalone }: Props) {
|
|||
return (
|
||||
<APITab
|
||||
onCreate={(environment) => onCreate(environment, 'dockerApi')}
|
||||
isDockerStandalone={isDockerStandalone}
|
||||
/>
|
||||
);
|
||||
case 'socket':
|
||||
return (
|
||||
<SocketTab
|
||||
onCreate={(environment) => onCreate(environment, 'localEndpoint')}
|
||||
isDockerStandalone={isDockerStandalone}
|
||||
/>
|
||||
);
|
||||
case 'edgeAgentStandard':
|
||||
|
|
|
@ -11,14 +11,12 @@ import { LoadingButton } from '@@/buttons/LoadingButton';
|
|||
|
||||
import { NameField } from '../NameField';
|
||||
import { MoreSettingsSection } from '../MoreSettingsSection';
|
||||
import { Hardware } from '../Hardware/Hardware';
|
||||
|
||||
import { EnvironmentUrlField } from './EnvironmentUrlField';
|
||||
import { useValidation } from './AgentForm.validation';
|
||||
|
||||
interface Props {
|
||||
onCreate(environment: Environment): void;
|
||||
showGpus?: boolean;
|
||||
}
|
||||
|
||||
const initialValues: CreateAgentEnvironmentValues = {
|
||||
|
@ -28,10 +26,9 @@ const initialValues: CreateAgentEnvironmentValues = {
|
|||
groupId: 1,
|
||||
tagIds: [],
|
||||
},
|
||||
gpus: [],
|
||||
};
|
||||
|
||||
export function AgentForm({ onCreate, showGpus = false }: Props) {
|
||||
export function AgentForm({ onCreate }: Props) {
|
||||
const [formKey, clearForm] = useReducer((state) => state + 1, 0);
|
||||
|
||||
const mutation = useCreateAgentEnvironmentMutation();
|
||||
|
@ -50,7 +47,7 @@ export function AgentForm({ onCreate, showGpus = false }: Props) {
|
|||
<NameField />
|
||||
<EnvironmentUrlField />
|
||||
|
||||
<MoreSettingsSection>{showGpus && <Hardware />}</MoreSettingsSection>
|
||||
<MoreSettingsSection />
|
||||
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { object, SchemaOf, string } from 'yup';
|
||||
|
||||
import { gpusListValidation } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList';
|
||||
import { CreateAgentEnvironmentValues } from '@/react/portainer/environments/environment.service/create';
|
||||
|
||||
import { metadataValidation } from '../MetadataFieldset/validation';
|
||||
|
@ -11,7 +10,6 @@ export function useValidation(): SchemaOf<CreateAgentEnvironmentValues> {
|
|||
name: useNameValidation(),
|
||||
environmentUrl: environmentValidation(),
|
||||
meta: metadataValidation(),
|
||||
gpus: gpusListValidation(),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,6 @@ import { FormSection } from '@@/form-components/FormSection';
|
|||
import { LoadingButton } from '@@/buttons/LoadingButton';
|
||||
|
||||
import { MoreSettingsSection } from '../../MoreSettingsSection';
|
||||
import { Hardware } from '../../Hardware/Hardware';
|
||||
|
||||
import { EdgeAgentFieldset } from './EdgeAgentFieldset';
|
||||
import { useValidationSchema } from './EdgeAgentForm.validation';
|
||||
|
@ -26,16 +25,10 @@ import { FormValues } from './types';
|
|||
interface Props {
|
||||
onCreate(environment: Environment): void;
|
||||
readonly: boolean;
|
||||
showGpus?: boolean;
|
||||
asyncMode: boolean;
|
||||
}
|
||||
|
||||
export function EdgeAgentForm({
|
||||
onCreate,
|
||||
readonly,
|
||||
asyncMode,
|
||||
showGpus = false,
|
||||
}: Props) {
|
||||
export function EdgeAgentForm({ onCreate, readonly, asyncMode }: Props) {
|
||||
const settingsQuery = useSettings();
|
||||
|
||||
const createMutation = useCreateEdgeAgentEnvironmentMutation();
|
||||
|
@ -76,7 +69,6 @@ export function EdgeAgentForm({
|
|||
/>
|
||||
)}
|
||||
</FormSection>
|
||||
{showGpus && <Hardware />}
|
||||
</MoreSettingsSection>
|
||||
|
||||
{!readonly && (
|
||||
|
@ -133,6 +125,5 @@ export function buildInitialValues(settings: Settings): FormValues {
|
|||
PingInterval: EDGE_ASYNC_INTERVAL_USE_DEFAULT,
|
||||
SnapshotInterval: EDGE_ASYNC_INTERVAL_USE_DEFAULT,
|
||||
},
|
||||
gpus: [],
|
||||
};
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import {
|
|||
edgeAsyncIntervalsValidation,
|
||||
EdgeAsyncIntervalsValues,
|
||||
} from '@/react/edge/components/EdgeAsyncIntervalsForm';
|
||||
import { gpusListValidation } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList';
|
||||
import { validation as urlValidation } from '@/react/portainer/common/PortainerTunnelAddrField';
|
||||
import { validation as addressValidation } from '@/react/portainer/common/PortainerUrlField';
|
||||
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||
|
@ -23,7 +22,6 @@ export function useValidationSchema(asyncMode: boolean): SchemaOf<FormValues> {
|
|||
tunnelServerAddr: asyncMode ? string() : addressValidation(),
|
||||
pollFrequency: number().required(),
|
||||
meta: metadataValidation(),
|
||||
gpus: gpusListValidation(),
|
||||
edge: isBE
|
||||
? edgeAsyncIntervalsValidation()
|
||||
: (null as unknown as SchemaOf<EdgeAsyncIntervalsValues>),
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { EdgeAsyncIntervalsValues } from '@/react/edge/components/EdgeAsyncIntervalsForm';
|
||||
import { Gpu } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList';
|
||||
import { EnvironmentMetadata } from '@/react/portainer/environments/environment.service/create';
|
||||
|
||||
export interface FormValues {
|
||||
|
@ -9,7 +8,6 @@ export interface FormValues {
|
|||
tunnelServerAddr?: string;
|
||||
pollFrequency: number;
|
||||
meta: EnvironmentMetadata;
|
||||
gpus: Gpu[];
|
||||
|
||||
edge: EdgeAsyncIntervalsValues;
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@ interface Props {
|
|||
onCreate: (environment: Environment) => void;
|
||||
commands: CommandTab[] | Partial<Record<OS, CommandTab[]>>;
|
||||
isNomadTokenVisible?: boolean;
|
||||
showGpus?: boolean;
|
||||
asyncMode?: boolean;
|
||||
}
|
||||
|
||||
|
@ -23,7 +22,6 @@ export function EdgeAgentTab({
|
|||
onCreate,
|
||||
commands,
|
||||
isNomadTokenVisible,
|
||||
showGpus = false,
|
||||
asyncMode = false,
|
||||
}: Props) {
|
||||
const [edgeInfo, setEdgeInfo] = useState<EdgeInfo>();
|
||||
|
@ -35,7 +33,6 @@ export function EdgeAgentTab({
|
|||
onCreate={handleCreate}
|
||||
readonly={!!edgeInfo}
|
||||
key={formKey}
|
||||
showGpus={showGpus}
|
||||
asyncMode={asyncMode}
|
||||
/>
|
||||
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
import { useField } from 'formik';
|
||||
|
||||
import {
|
||||
Gpu,
|
||||
GpusList,
|
||||
} from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList';
|
||||
|
||||
import { FormSection } from '@@/form-components/FormSection';
|
||||
|
||||
export function Hardware() {
|
||||
const [field, , helpers] = useField('gpus');
|
||||
|
||||
function onChange(value: Gpu[]) {
|
||||
helpers.setValue(value);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormSection title="Hardware acceleration">
|
||||
<GpusList value={field.value} onChange={onChange} />
|
||||
</FormSection>
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue