1
0
Fork 0
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:
Chaim Lev-Ari 2023-09-21 14:02:02 +03:00 committed by GitHub
parent e92f067e42
commit 2b47b84e5e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 413 additions and 246 deletions

View file

@ -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}
/>
);
}

View 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>
);
}

View 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,
};

View file

@ -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)),
},
},
},
};
}

View 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;
}

View 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;
}

View file

@ -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'),
}),
});
}