1<template>
2  <b-container fluid="xl">
3    <page-title :description="$t('pageNetwork.pageDescription')" />
4    <page-section :section-title="$t('pageNetwork.interface')">
5      <b-row>
6        <b-col lg="3">
7          <b-form-group
8            label-for="interface-select"
9            :label="$t('pageNetwork.form.networkInterface')"
10          >
11            <b-form-select
12              id="interface-select"
13              v-model="selectedInterfaceIndex"
14              :disabled="loading"
15              data-test-id="network-select-interface"
16              :options="interfaceSelectOptions"
17              @change="selectInterface"
18            >
19            </b-form-select>
20          </b-form-group>
21        </b-col>
22      </b-row>
23    </page-section>
24    <b-form novalidate @submit.prevent="submitForm">
25      <b-form-group :disabled="loading">
26        <page-section :section-title="$t('pageNetwork.system')">
27          <b-row>
28            <b-col lg="3">
29              <b-form-group
30                :label="$t('pageNetwork.form.defaultGateway')"
31                label-for="default-gateway"
32              >
33                <b-form-input
34                  id="default-gateway"
35                  v-model.trim="form.gateway"
36                  data-test-id="network-input-gateway"
37                  type="text"
38                  :state="getValidationState($v.form.gateway)"
39                  @change="$v.form.gateway.$touch()"
40                />
41                <b-form-invalid-feedback role="alert">
42                  <div v-if="!$v.form.gateway.required">
43                    {{ $t('global.form.fieldRequired') }}
44                  </div>
45                  <div v-if="!$v.form.gateway.ipAddress">
46                    {{ $t('global.form.invalidFormat') }}
47                  </div>
48                </b-form-invalid-feedback>
49              </b-form-group>
50            </b-col>
51            <b-col lg="3">
52              <b-form-group
53                :label="$t('pageNetwork.form.hostname')"
54                label-for="hostname-field"
55              >
56                <b-form-input
57                  id="hostname-field"
58                  v-model.trim="form.hostname"
59                  data-test-id="network-input-hostname"
60                  type="text"
61                  :state="getValidationState($v.form.hostname)"
62                  @change="$v.form.hostname.$touch()"
63                />
64                <b-form-invalid-feedback role="alert">
65                  <div v-if="!$v.form.hostname.required">
66                    {{ $t('global.form.fieldRequired') }}
67                  </div>
68                  <div v-if="!$v.form.hostname.validateHostname">
69                    {{
70                      $t('global.form.lengthMustBeBetween', { min: 1, max: 64 })
71                    }}
72                  </div>
73                </b-form-invalid-feedback>
74              </b-form-group>
75            </b-col>
76            <b-col lg="3">
77              <b-form-group
78                :label="$t('pageNetwork.form.macAddress')"
79                label-for="mac-address"
80              >
81                <b-form-input
82                  id="mac-address"
83                  v-model.trim="form.macAddress"
84                  data-test-id="network-input-macAddress"
85                  type="text"
86                  :state="getValidationState($v.form.macAddress)"
87                  @change="$v.form.macAddress.$touch()"
88                />
89                <b-form-invalid-feedback role="alert">
90                  <div v-if="!$v.form.macAddress.required">
91                    {{ $t('global.form.fieldRequired') }}
92                  </div>
93                  <div v-if="!$v.form.macAddress.macAddress">
94                    {{ $t('global.form.invalidFormat') }}
95                  </div>
96                </b-form-invalid-feedback>
97              </b-form-group>
98            </b-col>
99          </b-row>
100        </page-section>
101        <page-section :section-title="$t('pageNetwork.ipv4')">
102          <b-form-group :label="$t('pageNetwork.ipv4Configuration')">
103            <b-form-text id="enable-secure-help-block">
104              {{ $t('pageNetwork.ipv4Helper') }}
105            </b-form-text>
106            <b-form-radio
107              v-model="form.dhcpEnabled"
108              name="dhcp-radio"
109              :value="true"
110              @change="onChangeIpv4Config"
111            >
112              {{ $t('pageNetwork.dhcp') }}
113            </b-form-radio>
114            <b-form-radio
115              v-model="form.dhcpEnabled"
116              name="static-radio"
117              :value="false"
118              @change="onChangeIpv4Config"
119            >
120              {{ $t('pageNetwork.static') }}
121            </b-form-radio>
122          </b-form-group>
123          <b-row>
124            <b-col lg="9" class="mb-3">
125              <h3 class="h4">
126                {{ $t('pageNetwork.dhcp') }}
127              </h3>
128              <b-table
129                responsive="md"
130                hover
131                :fields="ipv4DhcpTableFields"
132                :items="form.ipv4DhcpTableItems"
133                :empty-text="$t('global.table.emptyMessage')"
134                class="mb-0"
135                show-empty
136              >
137                <template #cell(Address)="{ item, index }">
138                  <b-form-input
139                    v-model.trim="item.Address"
140                    :data-test-id="`network-input-dhcpIpv4-${index}`"
141                    :aria-label="
142                      $t('pageNetwork.table.dhcpIpv4AddressRow') +
143                      ' ' +
144                      (index + 1)
145                    "
146                    readonly
147                  />
148                </template>
149                <template #cell(SubnetMask)="{ item, index }">
150                  <b-form-input
151                    v-model.trim="item.SubnetMask"
152                    :data-test-id="`network-input-subnetMask-${index}`"
153                    :aria-label="
154                      $t('pageNetwork.table.dhcpIpv4SubnetRow') +
155                      ' ' +
156                      (index + 1)
157                    "
158                    readonly
159                  />
160                </template>
161                <template #cell(actions)="{ item, index }">
162                  <table-row-action
163                    v-for="(action, actionIndex) in item.actions"
164                    :key="actionIndex"
165                    :value="action.value"
166                    :title="action.title"
167                    :enabled="false"
168                    @click-table-action="
169                      onDeleteIpv4StaticTableRow($event, index)
170                    "
171                  >
172                    <template #icon>
173                      <icon-trashcan v-if="action.value === 'delete'" />
174                    </template>
175                  </table-row-action>
176                </template>
177              </b-table>
178            </b-col>
179            <b-col lg="9" class="mb-3">
180              <h3 class="h4">
181                {{ $t('pageNetwork.static') }}
182              </h3>
183              <b-table
184                responsive="md"
185                hover
186                :fields="ipv4StaticTableFields"
187                :items="form.ipv4StaticTableItems"
188                :empty-text="$t('global.table.emptyMessage')"
189                class="mb-0"
190                show-empty
191              >
192                <template #cell(Address)="{ item, index }">
193                  <b-form-input
194                    v-model.trim="item.Address"
195                    :data-test-id="`network-input-staticIpv4-${index}`"
196                    :aria-label="
197                      $t('pageNetwork.table.staticIpv4AddressRow') +
198                      ' ' +
199                      (index + 1)
200                    "
201                    :state="
202                      getValidationState(
203                        $v.form.ipv4StaticTableItems.$each.$iter[index].Address
204                      )
205                    "
206                    @change="
207                      $v.form.ipv4StaticTableItems.$each.$iter[
208                        index
209                      ].Address.$touch()
210                    "
211                  />
212                  <b-form-invalid-feedback role="alert">
213                    <div
214                      v-if="
215                        !$v.form.ipv4StaticTableItems.$each.$iter[index].Address
216                          .required
217                      "
218                    >
219                      {{ $t('global.form.fieldRequired') }}
220                    </div>
221                    <div
222                      v-if="
223                        !$v.form.ipv4StaticTableItems.$each.$iter[index].Address
224                          .ipAddress
225                      "
226                    >
227                      {{ $t('global.form.invalidFormat') }}
228                    </div>
229                  </b-form-invalid-feedback>
230                </template>
231                <template #cell(SubnetMask)="{ item, index }">
232                  <b-form-input
233                    v-model.trim="item.SubnetMask"
234                    :data-test-id="`network-input-subnetMask-${index}`"
235                    :aria-label="
236                      $t('pageNetwork.table.staticIpv4SubnetRow') +
237                      ' ' +
238                      (index + 1)
239                    "
240                    :state="
241                      getValidationState(
242                        $v.form.ipv4StaticTableItems.$each.$iter[index]
243                          .SubnetMask
244                      )
245                    "
246                    @change="
247                      $v.form.ipv4StaticTableItems.$each.$iter[
248                        index
249                      ].SubnetMask.$touch()
250                    "
251                  />
252                  <b-form-invalid-feedback role="alert">
253                    <div
254                      v-if="
255                        !$v.form.ipv4StaticTableItems.$each.$iter[index]
256                          .SubnetMask.required
257                      "
258                    >
259                      {{ $t('global.form.fieldRequired') }}
260                    </div>
261                    <div
262                      v-if="
263                        !$v.form.ipv4StaticTableItems.$each.$iter[index]
264                          .SubnetMask.ipAddress
265                      "
266                    >
267                      {{ $t('global.form.invalidFormat') }}
268                    </div>
269                  </b-form-invalid-feedback>
270                </template>
271                <template #cell(actions)="{ item, index }">
272                  <table-row-action
273                    v-for="(action, actionIndex) in item.actions"
274                    :key="actionIndex"
275                    :value="action.value"
276                    :title="action.title"
277                    @click-table-action="
278                      onDeleteIpv4StaticTableRow($event, index)
279                    "
280                  >
281                    <template #icon>
282                      <icon-trashcan v-if="action.value === 'delete'" />
283                    </template>
284                  </table-row-action>
285                </template>
286              </b-table>
287              <b-button variant="link" @click="addIpv4StaticTableRow">
288                <icon-add />
289                {{ $t('pageNetwork.table.addStaticIpv4Address') }}
290              </b-button>
291            </b-col>
292          </b-row>
293        </page-section>
294        <page-section :section-title="$t('pageNetwork.staticDns')">
295          <b-row>
296            <b-col lg="4" class="mb-3">
297              <b-table
298                responsive
299                hover
300                :fields="dnsTableFields"
301                :items="form.dnsStaticTableItems"
302                :empty-text="$t('global.table.emptyMessage')"
303                class="mb-0"
304                show-empty
305              >
306                <template #cell(address)="{ item, index }">
307                  <b-form-input
308                    v-model.trim="item.address"
309                    :data-test-id="`network-input-dnsAddress-${index}`"
310                    :aria-label="
311                      $t('pageNetwork.table.staticDnsRow') + ' ' + (index + 1)
312                    "
313                    :state="
314                      getValidationState(
315                        $v.form.dnsStaticTableItems.$each.$iter[index].address
316                      )
317                    "
318                    @change="
319                      $v.form.dnsStaticTableItems.$each.$iter[
320                        index
321                      ].address.$touch()
322                    "
323                  />
324                  <b-form-invalid-feedback role="alert">
325                    <div
326                      v-if="
327                        !$v.form.dnsStaticTableItems.$each.$iter[index].address
328                          .required
329                      "
330                    >
331                      {{ $t('global.form.fieldRequired') }}
332                    </div>
333                    <div
334                      v-if="
335                        !$v.form.dnsStaticTableItems.$each.$iter[index].address
336                          .ipAddress
337                      "
338                    >
339                      {{ $t('global.form.invalidFormat') }}
340                    </div>
341                  </b-form-invalid-feedback>
342                </template>
343                <template #cell(actions)="{ item, index }">
344                  <table-row-action
345                    v-for="(action, actionIndex) in item.actions"
346                    :key="actionIndex"
347                    :value="action.value"
348                    :title="action.title"
349                    @click-table-action="onDeleteDnsTableRow($event, index)"
350                  >
351                    <template #icon>
352                      <icon-trashcan v-if="action.value === 'delete'" />
353                    </template>
354                  </table-row-action>
355                </template>
356              </b-table>
357              <b-button variant="link" @click="addDnsTableRow">
358                <icon-add /> {{ $t('pageNetwork.table.addDns') }}
359              </b-button>
360            </b-col>
361          </b-row>
362        </page-section>
363        <b-button
364          variant="primary"
365          type="submit"
366          data-test-id="network-button-saveNetworkSettings"
367        >
368          {{ $t('global.action.saveSettings') }}
369        </b-button>
370      </b-form-group>
371    </b-form>
372  </b-container>
373</template>
374
375<script>
376import IconTrashcan from '@carbon/icons-vue/es/trash-can/20';
377import IconAdd from '@carbon/icons-vue/es/add--alt/20';
378import BVToastMixin from '@/components/Mixins/BVToastMixin';
379import LoadingBarMixin, { loading } from '@/components/Mixins/LoadingBarMixin';
380import PageSection from '@/components/Global/PageSection';
381import PageTitle from '@/components/Global/PageTitle';
382import TableRowAction from '@/components/Global/TableRowAction';
383import VuelidateMixin from '@/components/Mixins/VuelidateMixin';
384import { mapState } from 'vuex';
385import {
386  required,
387  helpers,
388  ipAddress,
389  macAddress,
390} from 'vuelidate/lib/validators';
391
392// Hostname pattern
393const validateHostname = helpers.regex('validateHostname', /^\S{0,64}$/);
394
395export default {
396  name: 'Network',
397  components: {
398    PageTitle,
399    PageSection,
400    TableRowAction,
401    IconTrashcan,
402    IconAdd,
403  },
404  mixins: [BVToastMixin, VuelidateMixin, LoadingBarMixin],
405  beforeRouteLeave(to, from, next) {
406    this.hideLoader();
407    next();
408  },
409  data() {
410    return {
411      ipv4DhcpTableFields: [
412        {
413          key: 'Address',
414          label: this.$t('pageNetwork.table.ipAddress'),
415        },
416        {
417          key: 'SubnetMask',
418          label: this.$t('pageNetwork.table.subnet'),
419        },
420        { key: 'actions', label: '', tdClass: 'text-right' },
421      ],
422      ipv4StaticTableFields: [
423        {
424          key: 'Address',
425          label: this.$t('pageNetwork.table.ipAddress'),
426        },
427        {
428          key: 'SubnetMask',
429          label: this.$t('pageNetwork.table.subnet'),
430        },
431        { key: 'actions', label: '', tdClass: 'text-right' },
432      ],
433      dnsTableFields: [
434        {
435          key: 'address',
436          label: this.$t('pageNetwork.table.ipAddress'),
437        },
438        { key: 'actions', label: '', tdClass: 'text-right' },
439      ],
440      selectedInterfaceIndex: 0,
441      selectedInterface: {},
442      form: {
443        dhcpEnabled: null,
444        gateway: '',
445        hostname: '',
446        macAddress: '',
447        ipv4StaticTableItems: [],
448        ipv4DhcpTableItems: [],
449        dnsStaticTableItems: [],
450      },
451      loading,
452    };
453  },
454  validations() {
455    return {
456      form: {
457        gateway: { required, ipAddress },
458        hostname: { required, validateHostname },
459        ipv4StaticTableItems: {
460          $each: {
461            Address: {
462              required,
463              ipAddress,
464            },
465            SubnetMask: {
466              required,
467              ipAddress,
468            },
469          },
470        },
471        macAddress: { required, macAddress: macAddress() },
472        dnsStaticTableItems: {
473          $each: {
474            address: {
475              required,
476              ipAddress,
477            },
478          },
479        },
480      },
481    };
482  },
483  computed: {
484    ...mapState('network', [
485      'ethernetData',
486      'interfaceOptions',
487      'defaultGateway',
488    ]),
489    interfaceSelectOptions() {
490      return this.interfaceOptions.map((option, index) => {
491        return {
492          text: option,
493          value: index,
494        };
495      });
496    },
497  },
498  watch: {
499    ethernetData: function () {
500      this.selectInterface();
501    },
502  },
503  created() {
504    this.startLoader();
505    this.$store
506      .dispatch('network/getEthernetData')
507      .finally(() => this.endLoader());
508  },
509  methods: {
510    selectInterface() {
511      this.selectedInterface = this.ethernetData[this.selectedInterfaceIndex];
512      this.getIpv4DhcpTableItems();
513      this.getIpv4StaticTableItems();
514      this.getDnsStaticTableItems();
515      this.getInterfaceSettings();
516    },
517    getInterfaceSettings() {
518      this.form.gateway = this.defaultGateway;
519      this.form.hostname = this.selectedInterface.HostName;
520      this.form.macAddress = this.selectedInterface.MACAddress;
521      this.form.dhcpEnabled = this.selectedInterface.DHCPv4.DHCPEnabled;
522    },
523    onChangeIpv4Config(value) {
524      this.form.dhcpEnabled = value;
525    },
526    getDnsStaticTableItems() {
527      const dns = this.selectedInterface.StaticNameServers || [];
528      this.form.dnsStaticTableItems = dns.map((server) => {
529        return {
530          address: server,
531          actions: [
532            {
533              value: 'delete',
534              enabled: this.form.dhcpEnabled,
535              title: this.$t('pageNetwork.table.deleteDns'),
536            },
537          ],
538        };
539      });
540    },
541    addDnsTableRow() {
542      this.$v.form.dnsStaticTableItems.$touch();
543      this.form.dnsStaticTableItems.push({
544        address: '',
545        actions: [
546          {
547            value: 'delete',
548            enabled: this.form.dhcpEnabled,
549            title: this.$t('pageNetwork.table.deleteDns'),
550          },
551        ],
552      });
553    },
554    deleteDnsTableRow(index) {
555      this.$v.form.dnsStaticTableItems.$touch();
556      this.form.dnsStaticTableItems.splice(index, 1);
557    },
558    onDeleteDnsTableRow(action, row) {
559      this.deleteDnsTableRow(row);
560    },
561    getIpv4DhcpTableItems() {
562      const addresses = this.selectedInterface.IPv4Addresses || [];
563      this.form.ipv4DhcpTableItems = addresses
564        .filter((ipv4) => ipv4.AddressOrigin === 'DHCP')
565        .map((ipv4) => {
566          return {
567            Address: ipv4.Address,
568            SubnetMask: ipv4.SubnetMask,
569            actions: [
570              {
571                value: 'delete',
572                enabled: false,
573                title: this.$t('pageNetwork.table.deleteDhcpIpv4'),
574              },
575            ],
576          };
577        });
578    },
579    getIpv4StaticTableItems() {
580      const addresses = this.selectedInterface.IPv4StaticAddresses || [];
581      this.form.ipv4StaticTableItems = addresses.map((ipv4) => {
582        return {
583          Address: ipv4.Address,
584          SubnetMask: ipv4.SubnetMask,
585          actions: [
586            {
587              value: 'delete',
588              enabled: this.form.dhcpEnabled,
589              title: this.$t('pageNetwork.table.deleteStaticIpv4'),
590            },
591          ],
592        };
593      });
594    },
595    addIpv4StaticTableRow() {
596      this.$v.form.ipv4StaticTableItems.$touch();
597      this.form.ipv4StaticTableItems.push({
598        Address: '',
599        SubnetMask: '',
600        actions: [
601          {
602            value: 'delete',
603            enabled: this.form.dhcpEnabled,
604            title: this.$t('pageNetwork.table.deleteStaticIpv4'),
605          },
606        ],
607      });
608    },
609    deleteIpv4StaticTableRow(index) {
610      this.$v.form.ipv4StaticTableItems.$touch();
611      this.form.ipv4StaticTableItems.splice(index, 1);
612    },
613    onDeleteIpv4StaticTableRow(action, row) {
614      this.deleteIpv4StaticTableRow(row);
615    },
616    submitForm() {
617      this.$v.$touch();
618      if (this.$v.$invalid) return;
619      this.startLoader();
620      let networkInterfaceSelected = this.selectedInterface;
621      let selectedInterfaceIndex = this.selectedInterfaceIndex;
622      let interfaceId = networkInterfaceSelected.Id;
623      let isDhcpEnabled = this.form.dhcpEnabled;
624      let macAddress = this.form.macAddress;
625      let hostname = this.form.hostname;
626      let networkSettingsForm = {
627        interfaceId,
628        hostname,
629        macAddress,
630        selectedInterfaceIndex,
631      };
632      // Enabling DHCP without any available IP addresses will bring network down
633      if (this.form.ipv4DhcpTableItems.length) {
634        networkSettingsForm.isDhcpEnabled = isDhcpEnabled;
635      } else {
636        networkSettingsForm.isDhcpEnabled = false;
637        this.errorToast(this.$t('pageNetwork.toast.errorSaveDhcpSettings'));
638      }
639      networkSettingsForm.staticIpv4 = this.form.ipv4StaticTableItems.map(
640        (updateIpv4) => {
641          delete updateIpv4.actions;
642          updateIpv4.Gateway = this.form.gateway;
643          return updateIpv4;
644        }
645      );
646      networkSettingsForm.staticNameServers = this.form.dnsStaticTableItems.map(
647        (updateDns) => {
648          return updateDns.address;
649        }
650      );
651      this.$store
652        .dispatch('network/updateInterfaceSettings', networkSettingsForm)
653        .then((success) => {
654          this.successToast(success);
655        })
656        .catch(({ message }) => this.errorToast(message))
657        .finally(() => {
658          this.$v.form.$reset();
659          this.endLoader();
660        });
661    },
662  },
663};
664</script>
665