mirror of
https://github.com/portainer/portainer.git
synced 2025-07-22 14:59:41 +02:00
feat(docker/containers): migrate network tab to react [EE-5210] (#10344)
This commit is contained in:
parent
e92f067e42
commit
2b47b84e5e
13 changed files with 413 additions and 246 deletions
|
@ -0,0 +1,36 @@
|
|||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||
|
||||
import { Option, PortainerSelect } from '@@/form-components/PortainerSelect';
|
||||
|
||||
import { useContainers } from '../../queries/containers';
|
||||
import { ContainerStatus } from '../../types';
|
||||
|
||||
export function ContainerSelector({
|
||||
onChange,
|
||||
value,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}) {
|
||||
const environmentId = useEnvironmentId();
|
||||
|
||||
const containersQuery = useContainers<Array<Option<string>>>(environmentId, {
|
||||
filters: { status: [ContainerStatus.Running] },
|
||||
select(containers) {
|
||||
return containers.map((n) => {
|
||||
const name = n.Names[0];
|
||||
|
||||
return { label: name, value: name };
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<PortainerSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={containersQuery.data || []}
|
||||
isLoading={containersQuery.isLoading}
|
||||
/>
|
||||
);
|
||||
}
|
139
app/react/docker/containers/CreateView/NetworkTab/NetworkTab.tsx
Normal file
139
app/react/docker/containers/CreateView/NetworkTab/NetworkTab.tsx
Normal file
|
@ -0,0 +1,139 @@
|
|||
import { FormikErrors } from 'formik';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { FormControl } from '@@/form-components/FormControl';
|
||||
import { Input } from '@@/form-components/Input';
|
||||
import { InputList, ItemProps } from '@@/form-components/InputList';
|
||||
import { InputGroup } from '@@/form-components/InputGroup';
|
||||
import { FormError } from '@@/form-components/FormError';
|
||||
|
||||
import { NetworkSelector } from '../../components/NetworkSelector';
|
||||
|
||||
import { CONTAINER_MODE, Values } from './types';
|
||||
import { ContainerSelector } from './ContainerSelector';
|
||||
|
||||
export function NetworkTab({
|
||||
values: initialValues,
|
||||
onChange,
|
||||
errors,
|
||||
}: {
|
||||
values: Values;
|
||||
onChange(values: Values): void;
|
||||
errors?: FormikErrors<Values>;
|
||||
}) {
|
||||
const [values, setControlledValues] = useState(initialValues);
|
||||
|
||||
return (
|
||||
<div className="mt-3">
|
||||
<FormControl label="Network" errors={errors?.networkMode}>
|
||||
<NetworkSelector
|
||||
value={values.networkMode}
|
||||
additionalOptions={[{ label: 'Container', value: CONTAINER_MODE }]}
|
||||
onChange={(networkMode) => handleChange({ networkMode })}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
{values.networkMode === CONTAINER_MODE && (
|
||||
<FormControl label="Container" errors={errors?.container}>
|
||||
<ContainerSelector
|
||||
value={values.container}
|
||||
onChange={(container) => handleChange({ container })}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
<FormControl label="Hostname" errors={errors?.hostname}>
|
||||
<Input
|
||||
value={values.hostname}
|
||||
onChange={(e) => handleChange({ hostname: e.target.value })}
|
||||
placeholder="e.g. web01"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl label="Domain Name" errors={errors?.domain}>
|
||||
<Input
|
||||
value={values.domain}
|
||||
onChange={(e) => handleChange({ domain: e.target.value })}
|
||||
placeholder="e.g. example.com"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl label="MAC Address" errors={errors?.macAddress}>
|
||||
<Input
|
||||
value={values.macAddress}
|
||||
onChange={(e) => handleChange({ macAddress: e.target.value })}
|
||||
placeholder="e.g. 12-34-56-78-9a-bc"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl label="IPv4 Address" errors={errors?.ipv4Address}>
|
||||
<Input
|
||||
value={values.ipv4Address}
|
||||
onChange={(e) => handleChange({ ipv4Address: e.target.value })}
|
||||
placeholder="e.g. 172.20.0.7"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl label="IPv6 Address" errors={errors?.ipv6Address}>
|
||||
<Input
|
||||
value={values.ipv6Address}
|
||||
onChange={(e) => handleChange({ ipv6Address: e.target.value })}
|
||||
placeholder="e.g. a:b:c:d::1234"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl label="Primary DNS Server" errors={errors?.primaryDns}>
|
||||
<Input
|
||||
value={values.primaryDns}
|
||||
onChange={(e) => handleChange({ primaryDns: e.target.value })}
|
||||
placeholder="e.g. 1.1.1.1, 2606:4700:4700::1111"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl label="Secondary DNS Server" errors={errors?.secondaryDns}>
|
||||
<Input
|
||||
value={values.secondaryDns}
|
||||
onChange={(e) => handleChange({ secondaryDns: e.target.value })}
|
||||
placeholder="e.g. 1.0.0.1, 2606:4700:4700::1001"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<InputList
|
||||
label="Hosts file entries"
|
||||
value={values.hostsFileEntries}
|
||||
onChange={(hostsFileEntries) => handleChange({ hostsFileEntries })}
|
||||
errors={errors?.hostsFileEntries}
|
||||
item={HostsFileEntryItem}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
function handleChange(newValues: Partial<Values>) {
|
||||
onChange({ ...values, ...newValues });
|
||||
setControlledValues((values) => ({ ...values, ...newValues }));
|
||||
}
|
||||
}
|
||||
|
||||
function HostsFileEntryItem({
|
||||
item,
|
||||
onChange,
|
||||
disabled,
|
||||
error,
|
||||
readOnly,
|
||||
}: ItemProps<string>) {
|
||||
return (
|
||||
<div>
|
||||
<InputGroup>
|
||||
<InputGroup.Addon>value</InputGroup.Addon>
|
||||
<Input
|
||||
value={item}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
disabled={disabled}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</InputGroup>
|
||||
|
||||
{error && <FormError>{error}</FormError>}
|
||||
</div>
|
||||
);
|
||||
}
|
14
app/react/docker/containers/CreateView/NetworkTab/index.ts
Normal file
14
app/react/docker/containers/CreateView/NetworkTab/index.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { validation } from './validation';
|
||||
import { toRequest } from './toRequest';
|
||||
import { toViewModel, getDefaultViewModel } from './toViewModel';
|
||||
|
||||
export { NetworkTab } from './NetworkTab';
|
||||
|
||||
export { type Values as NetworkTabValues } from './types';
|
||||
|
||||
export const networkTabUtils = {
|
||||
toRequest,
|
||||
toViewModel,
|
||||
validation,
|
||||
getDefaultViewModel,
|
||||
};
|
|
@ -0,0 +1,42 @@
|
|||
import { CreateContainerRequest } from '../types';
|
||||
|
||||
import { CONTAINER_MODE, Values } from './types';
|
||||
|
||||
export function toRequest(
|
||||
oldConfig: CreateContainerRequest,
|
||||
values: Values,
|
||||
fromContainerId: string
|
||||
): CreateContainerRequest {
|
||||
let mode = values.networkMode;
|
||||
let hostName = values.hostname;
|
||||
if (mode === CONTAINER_MODE && values.container) {
|
||||
mode += `:${values.container}`;
|
||||
hostName = '';
|
||||
}
|
||||
|
||||
return {
|
||||
...oldConfig,
|
||||
Hostname: hostName,
|
||||
MacAddress: values.macAddress,
|
||||
HostConfig: {
|
||||
...oldConfig.HostConfig,
|
||||
NetworkMode: mode,
|
||||
Dns: [values.primaryDns, values.secondaryDns].filter((d) => d),
|
||||
ExtraHosts: values.hostsFileEntries,
|
||||
},
|
||||
NetworkingConfig: {
|
||||
...oldConfig.NetworkingConfig,
|
||||
EndpointsConfig: {
|
||||
[mode]: {
|
||||
IPAMConfig: {
|
||||
IPv4Address: values.ipv4Address,
|
||||
IPv6Address: values.ipv6Address,
|
||||
},
|
||||
Aliases: oldConfig.NetworkingConfig.EndpointsConfig?.[
|
||||
mode
|
||||
]?.Aliases?.filter((al) => !fromContainerId.startsWith(al)),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
102
app/react/docker/containers/CreateView/NetworkTab/toViewModel.ts
Normal file
102
app/react/docker/containers/CreateView/NetworkTab/toViewModel.ts
Normal file
|
@ -0,0 +1,102 @@
|
|||
import { DockerNetwork } from '@/react/docker/networks/types';
|
||||
|
||||
import { ContainerJSON } from '../../queries/container';
|
||||
import { DockerContainer } from '../../types';
|
||||
|
||||
import { CONTAINER_MODE, Values } from './types';
|
||||
|
||||
export function getDefaultViewModel(hasBridgeNetwork: boolean) {
|
||||
return {
|
||||
networkMode: hasBridgeNetwork ? 'bridge' : 'nat',
|
||||
hostname: '',
|
||||
domain: '',
|
||||
macAddress: '',
|
||||
ipv4Address: '',
|
||||
ipv6Address: '',
|
||||
primaryDns: '',
|
||||
secondaryDns: '',
|
||||
hostsFileEntries: [],
|
||||
container: '',
|
||||
};
|
||||
}
|
||||
|
||||
export function toViewModel(
|
||||
config: ContainerJSON,
|
||||
networks: Array<DockerNetwork>,
|
||||
runningContainers: Array<DockerContainer> = []
|
||||
): Values {
|
||||
const dns = config.HostConfig?.Dns;
|
||||
const [primaryDns = '', secondaryDns = ''] = dns || [];
|
||||
|
||||
const hostsFileEntries = config.HostConfig?.ExtraHosts || [];
|
||||
|
||||
const [networkMode, container = ''] = getNetworkMode(
|
||||
config,
|
||||
networks,
|
||||
runningContainers
|
||||
);
|
||||
|
||||
const networkSettings = config.NetworkSettings?.Networks?.[networkMode];
|
||||
let ipv4Address = '';
|
||||
let ipv6Address = '';
|
||||
if (networkSettings && networkSettings.IPAMConfig) {
|
||||
ipv4Address = networkSettings.IPAMConfig.IPv4Address || '';
|
||||
ipv6Address = networkSettings.IPAMConfig.IPv6Address || '';
|
||||
}
|
||||
|
||||
const macAddress = networkSettings?.MacAddress || '';
|
||||
|
||||
return {
|
||||
networkMode,
|
||||
hostname: config.Config?.Hostname || '',
|
||||
domain: config.Config?.Domainname || '',
|
||||
macAddress,
|
||||
ipv4Address,
|
||||
ipv6Address,
|
||||
primaryDns,
|
||||
secondaryDns,
|
||||
hostsFileEntries,
|
||||
container,
|
||||
};
|
||||
}
|
||||
|
||||
function getNetworkMode(
|
||||
config: ContainerJSON,
|
||||
networks: Array<DockerNetwork>,
|
||||
runningContainers: Array<DockerContainer> = []
|
||||
) {
|
||||
let networkMode = config.HostConfig?.NetworkMode || '';
|
||||
if (!networkMode) {
|
||||
const networks = Object.keys(config.NetworkSettings?.Networks || {});
|
||||
if (networks.length > 0) {
|
||||
[networkMode] = networks;
|
||||
}
|
||||
}
|
||||
|
||||
if (networkMode.startsWith('container:')) {
|
||||
const networkContainerId = networkMode.split(/^container:/)[1];
|
||||
const container =
|
||||
runningContainers.find((c) => c.Id === networkContainerId)?.Names[0] ||
|
||||
'';
|
||||
return [CONTAINER_MODE, container] as const;
|
||||
}
|
||||
|
||||
const networkNames = networks.map((n) => n.Name);
|
||||
|
||||
if (networkNames.includes(networkMode)) {
|
||||
return [networkMode] as const;
|
||||
}
|
||||
|
||||
if (
|
||||
networkNames.includes('bridge') &&
|
||||
(!networkMode || networkMode === 'default' || networkMode === 'bridge')
|
||||
) {
|
||||
return ['bridge'] as const;
|
||||
}
|
||||
|
||||
if (networkNames.includes('nat')) {
|
||||
return ['nat'] as const;
|
||||
}
|
||||
|
||||
return [networks[0].Name] as const;
|
||||
}
|
14
app/react/docker/containers/CreateView/NetworkTab/types.ts
Normal file
14
app/react/docker/containers/CreateView/NetworkTab/types.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
export const CONTAINER_MODE = 'container';
|
||||
|
||||
export interface Values {
|
||||
networkMode: string;
|
||||
hostname: string;
|
||||
domain: string;
|
||||
macAddress: string;
|
||||
ipv4Address: string;
|
||||
ipv6Address: string;
|
||||
primaryDns: string;
|
||||
secondaryDns: string;
|
||||
hostsFileEntries: Array<string>;
|
||||
container: string;
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { array, object, SchemaOf, string } from 'yup';
|
||||
|
||||
import { Values } from './types';
|
||||
|
||||
export function validation(): SchemaOf<Values> {
|
||||
return object({
|
||||
networkMode: string().default(''),
|
||||
hostname: string().default(''),
|
||||
domain: string().default(''),
|
||||
macAddress: string().default(''),
|
||||
ipv4Address: string().default(''),
|
||||
ipv6Address: string().default(''),
|
||||
primaryDns: string().default(''),
|
||||
secondaryDns: string().default(''),
|
||||
hostsFileEntries: array(string().required('Entry is required')).default([]),
|
||||
container: string()
|
||||
.default('')
|
||||
.when('network', {
|
||||
is: 'container',
|
||||
then: string().required('Container is required'),
|
||||
}),
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue