foreman_fog_proxmox 0.15.1 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/app/helpers/proxmox_compute_resources_helper.rb +1 -1
  3. data/app/helpers/proxmox_form_helper.rb +2 -2
  4. data/app/helpers/proxmox_vm_attrs_helper.rb +131 -0
  5. data/app/helpers/proxmox_vm_interfaces_helper.rb +3 -3
  6. data/app/helpers/proxmox_vm_volumes_helper.rb +3 -3
  7. data/app/models/concerns/fog_extensions/proxmox/node.rb +1 -1
  8. data/app/models/foreman_fog_proxmox/proxmox_compute_attributes.rb +3 -3
  9. data/app/models/foreman_fog_proxmox/proxmox_images.rb +5 -0
  10. data/app/models/foreman_fog_proxmox/proxmox_version.rb +1 -1
  11. data/app/models/foreman_fog_proxmox/proxmox_vm_commands.rb +1 -1
  12. data/app/models/foreman_fog_proxmox/proxmox_vm_new.rb +1 -1
  13. data/app/overrides/compute_resources_vms/form/add_from_profile_to_compute_attributes_form.rb +8 -0
  14. data/app/overrides/compute_resources_vms/form/add_react_component_to_host.rb +25 -0
  15. data/app/overrides/compute_resources_vms/form/update_react_component_to_host_form.rb +25 -0
  16. data/app/views/compute_resources_vms/form/proxmox/_add_react_component_to_host_form.html.erb +5 -0
  17. data/app/views/compute_resources_vms/form/proxmox/_base.html.erb +21 -21
  18. data/app/views/compute_resources_vms/form/proxmox/_update_react_component_to_host_form.html.erb +26 -0
  19. data/config/routes.rb +3 -3
  20. data/lib/foreman_fog_proxmox/version.rb +1 -1
  21. data/package.json +42 -0
  22. data/test/factories/proxmox_factory.rb +7 -7
  23. data/test/unit/foreman_fog_proxmox/proxmox_compute_attributes_test.rb +1 -1
  24. data/test/unit/foreman_fog_proxmox/proxmox_interfaces_test.rb +6 -6
  25. data/webpack/components/GeneralTabContent.js +107 -0
  26. data/webpack/components/ProxmoxComputeSelectors.js +141 -0
  27. data/webpack/components/ProxmoxContainer/MountPoint.js +91 -0
  28. data/webpack/components/ProxmoxContainer/ProxmoxContainerHardware.js +85 -0
  29. data/webpack/components/ProxmoxContainer/ProxmoxContainerNetwork.js +179 -0
  30. data/webpack/components/ProxmoxContainer/ProxmoxContainerOptions.js +104 -0
  31. data/webpack/components/ProxmoxContainer/ProxmoxContainerStorage.js +194 -0
  32. data/webpack/components/ProxmoxContainer/components/NetworkInterface.js +193 -0
  33. data/webpack/components/ProxmoxServer/ProxmoxServerHardware.js +204 -0
  34. data/webpack/components/ProxmoxServer/ProxmoxServerNetwork.js +161 -0
  35. data/webpack/components/ProxmoxServer/ProxmoxServerOptions.js +105 -0
  36. data/webpack/components/ProxmoxServer/ProxmoxServerStorage.js +272 -0
  37. data/webpack/components/ProxmoxServer/components/CDRom.js +149 -0
  38. data/webpack/components/ProxmoxServer/components/CPUFlagsModal.js +88 -0
  39. data/webpack/components/ProxmoxServer/components/HardDisk.js +143 -0
  40. data/webpack/components/ProxmoxServer/components/NetworkInterface.js +150 -0
  41. data/webpack/components/ProxmoxStoragesUtils.js +50 -0
  42. data/webpack/components/ProxmoxVmType.js +256 -0
  43. data/webpack/components/ProxmoxVmUtils.js +62 -0
  44. data/webpack/components/common/FormInputs.js +143 -0
  45. data/webpack/global_index.js +15 -0
  46. data/webpack/index.js +7 -0
  47. metadata +49 -21
@@ -0,0 +1,107 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { PageSection, Divider } from '@patternfly/react-core';
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+ import InputField from './common/FormInputs';
6
+
7
+ const GeneralTabContent = ({
8
+ general,
9
+ fromProfile,
10
+ newVm,
11
+ nodesMap,
12
+ poolsMap,
13
+ imagesMap,
14
+ handleChange,
15
+ untemplatable,
16
+ }) => (
17
+ <PageSection padding={{ default: 'noPadding' }}>
18
+ <Divider component="li" style={{ marginBottom: '2rem' }} />
19
+ <InputField
20
+ name={general?.vmid?.name}
21
+ label={__('VM ID')}
22
+ required
23
+ type="number"
24
+ value={general?.vmid?.value}
25
+ disabled={fromProfile}
26
+ onChange={handleChange}
27
+ />
28
+ <InputField
29
+ name={general?.nodeId?.name}
30
+ label={__('Node')}
31
+ required
32
+ type="select"
33
+ value={general?.nodeId?.value}
34
+ options={nodesMap}
35
+ onChange={handleChange}
36
+ />
37
+ <InputField
38
+ name={general?.pool?.name}
39
+ label={__('Pool')}
40
+ type="select"
41
+ value={general?.pool?.value}
42
+ options={poolsMap}
43
+ onChange={handleChange}
44
+ />
45
+ {newVm && !fromProfile && (
46
+ <InputField
47
+ name={general?.startAfterCreate?.name}
48
+ label={__('Start after creation?')}
49
+ type="checkbox"
50
+ value={general?.startAfterCreate?.value}
51
+ checked={general?.startAfterCreate?.value === '1'}
52
+ onChange={handleChange}
53
+ />
54
+ )}
55
+ {!fromProfile && !untemplatable && (
56
+ <InputField
57
+ name={general?.templated?.name}
58
+ label={__('Create image?')}
59
+ type="checkbox"
60
+ value={general?.templated?.value}
61
+ checked={general?.templated?.value === '1'}
62
+ disabled={untemplatable}
63
+ onChange={handleChange}
64
+ />
65
+ )}
66
+ {fromProfile && (
67
+ <InputField
68
+ name={general?.imageId?.name}
69
+ label={__('Image')}
70
+ type="select"
71
+ value={general?.imageId?.value}
72
+ disabled={imagesMap.length === 0}
73
+ options={imagesMap}
74
+ onChange={handleChange}
75
+ />
76
+ )}
77
+ <InputField
78
+ name={general?.description?.name}
79
+ label={__('Description')}
80
+ type="textarea"
81
+ value={general?.description?.value}
82
+ onChange={handleChange}
83
+ />
84
+ </PageSection>
85
+ );
86
+
87
+ GeneralTabContent.propTypes = {
88
+ general: PropTypes.object.isRequired,
89
+ fromProfile: PropTypes.bool,
90
+ newVm: PropTypes.bool,
91
+ nodesMap: PropTypes.array,
92
+ poolsMap: PropTypes.array,
93
+ imagesMap: PropTypes.array,
94
+ handleChange: PropTypes.func.isRequired,
95
+ untemplatable: PropTypes.bool,
96
+ };
97
+
98
+ GeneralTabContent.defaultProps = {
99
+ fromProfile: false,
100
+ newVm: false,
101
+ nodesMap: [],
102
+ poolsMap: [],
103
+ imagesMap: [],
104
+ untemplatable: false,
105
+ };
106
+
107
+ export default GeneralTabContent;
@@ -0,0 +1,141 @@
1
+ const ProxmoxComputeSelectors = {
2
+ proxmoxTypesMap: [
3
+ { value: 'qemu', label: 'KVM/Qemu server' },
4
+ { value: 'lxc', label: 'LXC container' },
5
+ ],
6
+
7
+ proxmoxControllersCloudinitMap: [
8
+ { value: 'ide', label: 'IDE' },
9
+ { value: 'sata', label: 'SATA' },
10
+ { value: 'scsi', label: 'SCSI' },
11
+ ],
12
+
13
+ proxmoxScsiControllersMap: [
14
+ { value: 'lsi', label: 'LSI 53C895A (Default)' },
15
+ { value: 'lsi53c810', label: 'LSI 53C810' },
16
+ { value: 'virtio-scsi-pci', label: 'VirtIO SCSI' },
17
+ { value: 'virtio-scsi-single', label: 'VirtIO SCSI Single' },
18
+ { value: 'megasas', label: 'MegaRAID SAS 8708EM2' },
19
+ { value: 'pvscsi', label: 'VMware PVSCSI' },
20
+ ],
21
+
22
+ proxmoxArchsMap: [
23
+ { value: 'amd64', label: '64 bits' },
24
+ { value: 'i386', label: '32 bits' },
25
+ ],
26
+
27
+ proxmoxOstypesMap: [
28
+ { value: 'debian', label: 'Debian' },
29
+ { value: 'ubuntu', label: 'Ubuntu' },
30
+ { value: 'centos', label: 'CentOS' },
31
+ { value: 'fedora', label: 'Fedora' },
32
+ { value: 'opensuse', label: 'OpenSuse' },
33
+ { value: 'archlinux', label: 'ArchLinux' },
34
+ { value: 'gentoo', label: 'Gentoo' },
35
+ { value: 'alpine', label: 'Alpine' },
36
+ { value: 'unmanaged', label: 'Unmanaged' },
37
+ ],
38
+
39
+ proxmoxOperatingSystemsMap: [
40
+ { value: 'other', label: 'Unspecified OS' },
41
+ { value: 'wxp', label: 'Microsoft Windows XP' },
42
+ { value: 'w2k', label: 'Microsoft Windows 2000' },
43
+ { value: 'w2k3', label: 'Microsoft Windows 2003' },
44
+ { value: 'w2k8', label: 'Microsoft Windows 2008' },
45
+ { value: 'wvista', label: 'Microsoft Windows Vista' },
46
+ { value: 'win7', label: 'Microsoft Windows 7' },
47
+ { value: 'win8', label: 'Microsoft Windows 8/2012/2012r2' },
48
+ { value: 'win10', label: 'Microsoft Windows 10/2016' },
49
+ { value: 'l24', label: 'Linux 2.4 Kernel' },
50
+ { value: 'l26', label: 'Linux 2.6/3.X + Kernel' },
51
+ { value: 'solaris', label: 'Solaris/OpenSolaris/OpenIndiania kernel' },
52
+ ],
53
+
54
+ proxmoxVgasMap: [
55
+ { value: 'std', label: 'Standard VGA' },
56
+ { value: 'vmware', label: 'Vmware compatible' },
57
+ { value: 'qxl', label: 'SPICE' },
58
+ { value: 'qxl2', label: 'SPICE 2 monitors' },
59
+ { value: 'qxl3', label: 'SPICE 3 monitors' },
60
+ { value: 'qxl4', label: 'SPICE 4 monitors' },
61
+ { value: 'serial0', label: 'Serial terminal 0' },
62
+ { value: 'serial1', label: 'Serial terminal 1' },
63
+ { value: 'serial2', label: 'Serial terminal 2' },
64
+ { value: 'serial3', label: 'Serial terminal 3' },
65
+ ],
66
+
67
+ proxmoxCachesMap: [
68
+ { value: '', labal: '' },
69
+ { value: 'directsync', label: 'Direct sync' },
70
+ { value: 'writethrough', label: 'Write through' },
71
+ { value: 'writeback', label: 'Write back' },
72
+ { value: 'unsafe', label: 'Write back unsafe' },
73
+ { value: 'none', label: 'No cache' },
74
+ ],
75
+
76
+ proxmoxCpusMap: [
77
+ { value: '486', label: '486' },
78
+ { value: 'athlon', label: 'athlon' },
79
+ { value: 'core2duo', label: 'core2duo' },
80
+ { value: 'coreduo', label: 'coreduo' },
81
+ { value: 'kvm32', label: 'kvm32' },
82
+ { value: 'kvm64', label: '(Default) kvm64' },
83
+ { value: 'pentium', label: 'pentium' },
84
+ { value: 'pentium2', label: 'pentium2' },
85
+ { value: 'pentium3', label: 'pentium3' },
86
+ { value: 'phenom', label: 'phenom' },
87
+ { value: 'qemu32', label: 'qemu32' },
88
+ { value: 'qemu64', label: 'qemu64' },
89
+ { value: 'Conroe', label: 'Conroe' },
90
+ { value: 'Penryn', label: 'Penryn' },
91
+ { value: 'Nehalem', label: 'Nehalem' },
92
+ { value: 'Westmere', label: 'Westmere' },
93
+ { value: 'SandyBridge', label: 'SandyBridge' },
94
+ { value: 'IvyBridge', label: 'IvyBridge' },
95
+ { value: 'Haswell', label: 'Haswell' },
96
+ { value: 'Haswell-noTSX', label: 'Haswell-noTSX' },
97
+ { value: 'Broadwell', label: 'Broadwell' },
98
+ { value: 'Broadwell-noTSX', label: 'Broadwell-noTSX' },
99
+ { value: 'Skylake-Client', label: 'Skylake-Client' },
100
+ { value: 'Opteron_G1', label: 'Opteron_G1' },
101
+ { value: 'Opteron_G2', label: 'Opteron_G2' },
102
+ { value: 'Opteron_G3', label: 'Opteron_G3' },
103
+ { value: 'Opteron_G4', label: 'Opteron_G4' },
104
+ { value: 'Opteron_G5', label: 'Opteron_G5' },
105
+ { value: 'host', label: 'host' },
106
+ ],
107
+
108
+ proxmoxCpuFlagsMap: [
109
+ { value: '-1', label: 'Off' },
110
+ { value: '0', label: 'Default' },
111
+ { value: '+1', label: 'On' },
112
+ ],
113
+
114
+ proxmoxScsihwMap: [
115
+ { value: 'lsi', label: 'lsi' },
116
+ { value: 'lsi53c810', label: 'lsi53c810' },
117
+ { value: 'megasas', label: 'megasas' },
118
+ { value: 'virtio-scsi-pci', label: 'virtio-scsi-pci' },
119
+ { value: 'virtio-scsi-single', label: 'virtio-scsi-single' },
120
+ { value: 'pvscsi', label: 'pvscsi' },
121
+ ],
122
+
123
+ proxmoxNetworkcardsMap: [
124
+ { value: 'e1000', label: 'Intel E1000' },
125
+ { value: 'virtio', label: 'VirtIO (paravirtualized)' },
126
+ { value: 'rtl8139', label: 'Realtek RTL8139' },
127
+ { value: 'vmxnet3', label: 'VMware vmxnet3' },
128
+ ],
129
+
130
+ proxmoxBiosMap: [
131
+ { value: 'seabios', label: '(Default) Seabios' },
132
+ { value: 'ovmf', label: 'OVMF (UEFI)' },
133
+ ],
134
+ };
135
+
136
+ ProxmoxComputeSelectors.proxmoxControllersHDDMap = [
137
+ ...ProxmoxComputeSelectors.proxmoxControllersCloudinitMap,
138
+ { value: 'virtio', label: 'VirtIO Block' },
139
+ ];
140
+
141
+ export default ProxmoxComputeSelectors;
@@ -0,0 +1,91 @@
1
+ import React, { useState } from 'react';
2
+ import { Divider } from '@patternfly/react-core';
3
+ import { translate as __ } from 'foremanReact/common/I18n';
4
+ import PropTypes from 'prop-types';
5
+ import InputField from '../common/FormInputs';
6
+
7
+ const MountPoint = ({ id, data, storagesMap }) => {
8
+ const [mp, setMp] = useState(data);
9
+ const [error, setError] = useState('');
10
+
11
+ const handleChange = e => {
12
+ const { name, value } = e.target;
13
+ const updatedKey = Object.keys(mp).find(key => mp[key].name === name);
14
+ const updatedData = {
15
+ ...mp,
16
+ [updatedKey]: { ...mp[updatedKey], value },
17
+ };
18
+ setMp(updatedData);
19
+
20
+ if (updatedKey === 'mp' && value.trim() === '') {
21
+ setError(__('Path cannot be empty'));
22
+ } else {
23
+ setError('');
24
+ }
25
+ };
26
+
27
+ return (
28
+ <div>
29
+ <Divider component="li" style={{ marginBottom: '2rem' }} />
30
+ <InputField
31
+ name={mp?.storage?.name}
32
+ label={__('Storage')}
33
+ type="select"
34
+ options={storagesMap}
35
+ value={mp?.storage?.value}
36
+ onChange={handleChange}
37
+ />
38
+ <InputField
39
+ name={mp?.mp?.name}
40
+ label={__('Path')}
41
+ required
42
+ value={mp?.mp?.value}
43
+ onChange={handleChange}
44
+ error={error}
45
+ />
46
+ <InputField
47
+ name={mp?.size?.name}
48
+ label={__('Size')}
49
+ type="number"
50
+ value={mp?.size?.value}
51
+ onChange={handleChange}
52
+ />
53
+ <input
54
+ name={mp?.device?.name}
55
+ type="hidden"
56
+ value={mp?.device?.value}
57
+ onChange={handleChange}
58
+ />
59
+ <input
60
+ name={mp?.id?.name}
61
+ type="hidden"
62
+ value={mp?.id?.value}
63
+ onChange={handleChange}
64
+ />
65
+ <input
66
+ name={mp?.volid?.name}
67
+ type="hidden"
68
+ value={mp?.volid?.value}
69
+ onChange={handleChange}
70
+ />
71
+ <input
72
+ name={mp?.storageType?.name}
73
+ type="hidden"
74
+ value={mp?.storageType?.value}
75
+ />
76
+ </div>
77
+ );
78
+ };
79
+
80
+ MountPoint.propTypes = {
81
+ id: PropTypes.any.isRequired,
82
+ data: PropTypes.object,
83
+ storagesMap: PropTypes.array,
84
+ };
85
+
86
+ MountPoint.defaultProps = {
87
+ data: {},
88
+ storagesMap: [],
89
+ };
90
+
91
+ export default MountPoint;
@@ -0,0 +1,85 @@
1
+ import React, { useState } from 'react';
2
+ import { PageSection, Title, Divider } from '@patternfly/react-core';
3
+ import { translate as __ } from 'foremanReact/common/I18n';
4
+ import PropTypes from 'prop-types';
5
+ import InputField from '../common/FormInputs';
6
+ import ProxmoxComputeSelectors from '../ProxmoxComputeSelectors';
7
+
8
+ const ProxmoxContainerHardware = ({ hardware }) => {
9
+ const [hw, setHw] = useState(hardware);
10
+ const handleChange = e => {
11
+ const { name, value } = e.target;
12
+ const updatedKey = Object.keys(hw).find(key => hw[key].name === name);
13
+
14
+ setHw(prevHw => ({
15
+ ...prevHw,
16
+ [updatedKey]: { ...prevHw[updatedKey], value },
17
+ }));
18
+ };
19
+
20
+ return (
21
+ <div>
22
+ <PageSection padding={{ default: 'noPadding' }}>
23
+ <Title headingLevel="h3">{__('CPU')}</Title>
24
+ <Divider component="li" style={{ marginBottom: '2rem' }} />
25
+ <InputField
26
+ name={hw?.arch?.name}
27
+ label={__('Architecture')}
28
+ type="select"
29
+ options={ProxmoxComputeSelectors.proxmoxArchsMap}
30
+ value={hw?.arch?.value}
31
+ onChange={handleChange}
32
+ />
33
+ <InputField
34
+ name={hw?.cores?.name}
35
+ label={__('Cores')}
36
+ type="number"
37
+ value={hw?.cores?.value}
38
+ onChange={handleChange}
39
+ />
40
+ <InputField
41
+ name={hw?.cpulimit?.name}
42
+ label={__('CPU limit')}
43
+ type="number"
44
+ value={hw?.cpulimit?.value}
45
+ onChange={handleChange}
46
+ />
47
+ <InputField
48
+ name={hw?.cpuunits?.name}
49
+ label={__('CPU units')}
50
+ type="number"
51
+ value={hw?.cpuunits?.value}
52
+ onChange={handleChange}
53
+ />
54
+ </PageSection>
55
+ <PageSection padding={{ default: 'noPadding' }}>
56
+ <Title headingLevel="h3">{__('Memory')}</Title>
57
+ <Divider component="li" style={{ marginBottom: '2rem' }} />
58
+ <InputField
59
+ name={hw?.memory?.name}
60
+ label={__('Memory (MB)')}
61
+ type="text"
62
+ value={hw?.memory?.value}
63
+ onChange={handleChange}
64
+ />
65
+ <InputField
66
+ name={hw?.swap?.name}
67
+ label={__('Swap (MB)')}
68
+ type="text"
69
+ value={hw?.swap?.value}
70
+ onChange={handleChange}
71
+ />
72
+ </PageSection>
73
+ </div>
74
+ );
75
+ };
76
+
77
+ ProxmoxContainerHardware.propTypes = {
78
+ hardware: PropTypes.object,
79
+ };
80
+
81
+ ProxmoxContainerHardware.defaultProps = {
82
+ hardware: {},
83
+ };
84
+
85
+ export default ProxmoxContainerHardware;
@@ -0,0 +1,179 @@
1
+ import React, { useState, useEffect, useCallback } from 'react';
2
+ import { Title, PageSection, Button } from '@patternfly/react-core';
3
+ import { TimesIcon } from '@patternfly/react-icons';
4
+ import { sprintf, translate as __ } from 'foremanReact/common/I18n';
5
+ import PropTypes from 'prop-types';
6
+ import NetworkInterface from './components/NetworkInterface';
7
+
8
+ const ProxmoxContainerNetwork = ({ network, bridges, paramScope }) => {
9
+ const [interfaces, setInterfaces] = useState([]);
10
+ const [nextId, setNextId] = useState(0);
11
+ const [availableIds, setAvailableIds] = useState([]);
12
+ const [usedIds, setUsedIds] = useState(new Set());
13
+ useEffect(() => {
14
+ if (network?.length > 0) {
15
+ const existingIds = new Set();
16
+ network.forEach(net => {
17
+ if (!net.value.name.value) return;
18
+ const id = parseInt(net.value.id.value.replace('net', ''), 10);
19
+ existingIds.add(id);
20
+ addInterface(null, net.value);
21
+ });
22
+ setUsedIds(existingIds);
23
+ }
24
+ }, [network]);
25
+
26
+ const getLowestAvailableId = useCallback(() => {
27
+ let id = 0;
28
+ while (usedIds.has(id)) {
29
+ id += 1;
30
+ }
31
+ return id;
32
+ }, [usedIds]);
33
+
34
+ const addInterface = useCallback(
35
+ (event, initialData = null) => {
36
+ if (event) event.preventDefault();
37
+ const netId = getLowestAvailableId();
38
+ const initData = initialData || {
39
+ id: {
40
+ name: `${paramScope}[interfaces_attributes][${nextId}][id]`,
41
+ value: `net${netId}`,
42
+ },
43
+ name: {
44
+ name: `${paramScope}[interfaces_attributes][${nextId}][name]`,
45
+ value: `eth${netId}`,
46
+ },
47
+ bridge: {
48
+ name: `${paramScope}[interfaces_attributes][${nextId}][bridge]`,
49
+ value: bridges?.[0]?.iface || '',
50
+ },
51
+ dhcp: {
52
+ name: `${paramScope}[interfaces_attributes][${nextId}][dhcp]`,
53
+ value: '0',
54
+ },
55
+ cidr: {
56
+ name: `${paramScope}[interfaces_attributes][${nextId}][cidr]`,
57
+ value: '0',
58
+ },
59
+ gw: {
60
+ name: `${paramScope}[interfaces_attributes][${nextId}][gw]`,
61
+ value: '',
62
+ },
63
+ dhcp6: {
64
+ name: `${paramScope}[interfaces_attributes][${nextId}][dhcp6]`,
65
+ value: '0',
66
+ },
67
+ cidr6: {
68
+ name: `${paramScope}[interfaces_attributes][${nextId}][cidr6]`,
69
+ value: '0',
70
+ },
71
+ gw6: {
72
+ name: `${paramScope}[interfaces_attributes][${nextId}][gw6]`,
73
+ value: '',
74
+ },
75
+ tag: {
76
+ name: `${paramScope}[interfaces_attributes][${nextId}][tag]`,
77
+ value: '',
78
+ },
79
+ rate: {
80
+ name: `${paramScope}[interfaces_attributes][${nextId}][rate]`,
81
+ value: '',
82
+ },
83
+ firewall: {
84
+ name: `${paramScope}[interfaces_attributes][${nextId}][firewall]`,
85
+ value: '0',
86
+ },
87
+ };
88
+ setNextId(prevId => {
89
+ if (availableIds.length > 0) {
90
+ setAvailableIds(availableIds.slice(1));
91
+ } else {
92
+ prevId += 1;
93
+ }
94
+ setUsedIds(prevIds => new Set(prevIds).add(netId));
95
+ const newId = availableIds.length > 0 ? availableIds[0] : prevId;
96
+ const newInterface = {
97
+ id: newId,
98
+ bridges,
99
+ data: initData,
100
+ networks: network,
101
+ };
102
+
103
+ setInterfaces(prevInterfaces => [...prevInterfaces, newInterface]);
104
+ return prevId;
105
+ });
106
+ },
107
+ [availableIds, bridges, network, nextId, paramScope, getLowestAvailableId]
108
+ );
109
+
110
+ const removeInterface = idToRemove => {
111
+ const newInterfaces = interfaces.filter(nic => nic.id !== idToRemove);
112
+ setInterfaces(newInterfaces);
113
+ setAvailableIds([...availableIds, idToRemove].sort((a, b) => a - b));
114
+ setUsedIds(prevIds => {
115
+ const newIds = new Set(prevIds);
116
+ newIds.delete(idToRemove);
117
+ return newIds;
118
+ });
119
+ };
120
+
121
+ const updateNetworkData = (id, updatedData) => {
122
+ setInterfaces(
123
+ interfaces.map(net =>
124
+ net.id === id ? { ...net, data: updatedData } : net
125
+ )
126
+ );
127
+ };
128
+
129
+ return (
130
+ <div>
131
+ <PageSection padding={{ default: 'noPadding' }}>
132
+ <Button onClick={addInterface} variant="secondary">
133
+ {__('Add Interface')}
134
+ </Button>
135
+ {interfaces.map(nic => (
136
+ <div key={nic.id} style={{ position: 'relative' }}>
137
+ <div
138
+ style={{
139
+ marginTop: '10px',
140
+ display: 'flex',
141
+ justifyContent: 'space-between',
142
+ alignItems: 'center',
143
+ }}
144
+ >
145
+ <Title headingLevel="h4">
146
+ {sprintf(__('Nic %(nicId)s'), { nicId: nic.id })}
147
+ </Title>
148
+ <button onClick={() => removeInterface(nic.id)} type="button">
149
+ <TimesIcon />
150
+ </button>
151
+ </div>
152
+ <NetworkInterface
153
+ id={nic.id}
154
+ data={nic.data}
155
+ bridges={nic.bridges}
156
+ networks={nic.networks}
157
+ updateNetworkData={updateNetworkData}
158
+ existingInterfaces={interfaces}
159
+ />
160
+ </div>
161
+ ))}
162
+ </PageSection>
163
+ </div>
164
+ );
165
+ };
166
+
167
+ ProxmoxContainerNetwork.propTypes = {
168
+ network: PropTypes.object,
169
+ bridges: PropTypes.array,
170
+ paramScope: PropTypes.string,
171
+ };
172
+
173
+ ProxmoxContainerNetwork.defaultProps = {
174
+ network: {},
175
+ bridges: [],
176
+ paramScope: '',
177
+ };
178
+
179
+ export default ProxmoxContainerNetwork;