Skip to content

Commit

Permalink
feat: netmap ui (#352)
Browse files Browse the repository at this point in the history
  • Loading branch information
andre8244 authored Sep 11, 2024
1 parent 6ec1700 commit 2117b27
Show file tree
Hide file tree
Showing 7 changed files with 632 additions and 7 deletions.
24 changes: 22 additions & 2 deletions public/i18n/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,9 @@
"cannot_delete_backup": "Cannot delete remote backup",
"end_must_be_greater_then_start": "IP end must be greater than start",
"start_reserved": "First IP of network is reserved",
"cannot_retrieve_object_suggestions": "Cannot retrieve object suggestions"
"cannot_retrieve_object_suggestions": "Cannot retrieve object suggestions",
"cannot_retrieve_nat_helpers": "Cannot retrieve NAT helpers",
"cannot_save_nat_helper": "Cannot save NAT helper"
},
"ne_text_input": {
"show_password": "Show password",
Expand Down Expand Up @@ -1864,7 +1866,8 @@
"rewrite_ip": "Rewrite IP",
"delete_nat_rule": "Delete NAT rule",
"confirm_delete_rule": "You are about to delete NAT rule '{name}'",
"any_address": "Any address"
"any_address": "Any address",
"rules_and_netmap": "Rules and NETMAP"
},
"netmap": {
"title": "NETMAP",
Expand All @@ -1889,6 +1892,23 @@
"delete_netmap": "Delete NETMAP",
"confirm_delete_rule": "You are about to delete NETMAP '{name}'"
},
"nat_helpers": {
"title": "NAT helpers",
"nat_helpers_description": "NAT helpers are used to automatically modify the payload of specific protocols to allow them to pass through NAT.",
"module": "Module",
"status": "Status",
"loaded_on_kernel": "Loaded on kernel",
"loaded": "Loaded",
"not_loaded": "Not loaded",
"enabled_but_not_loaded_tooltip": "The module is enabled but not loaded on the kernel.",
"disabled_but_loaded_tooltip": "This module is disabled but currently loaded on the kernel: you might need to reboot the unit to unload it.",
"filter_nat_helpers": "Filter NAT helpers",
"no_nat_helpers_found": "No NAT helpers found",
"edit_nat_helper": "Edit NAT helper",
"reboot_to_apply_changes": "Reboot to apply NAT helper changes",
"reboot_to_apply_changes_description": "Reboot the unit to apply the changes to module '{module}'",
"nat_helper_name_saved": "NAT helper '{module}' saved"
},
"conntrack": {
"title": "Connections",
"short_title": "Connections",
Expand Down
265 changes: 265 additions & 0 deletions src/components/standalone/firewall/nat/EditNatHelperDrawer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
<!--
Copyright (C) 2024 Nethesis S.r.l.
SPDX-License-Identifier: GPL-3.0-or-later
-->

<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { ref } from 'vue'
import { MessageBag, validateRequired, type validationOutput } from '@/lib/validation'
import { watch } from 'vue'
import { ubusCall, ValidationError } from '@/lib/standalone/ubus'
import {
NeInlineNotification,
NeSideDrawer,
NeButton,
NeTextInput,
getAxiosErrorMessage
} from '@nethesis/vue-components'
import { NeToggle } from '@nethesis/vue-components'
import type { NatHelper } from '@/stores/standalone/firewall'
import { upperFirst } from 'lodash-es'
import { useNotificationsStore } from '@/stores/notifications'
import { useRouter } from 'vue-router'
import { getStandaloneRoutePrefix } from '@/lib/router'

type Param = {
name: string
value: string
}

const props = withDefaults(
defineProps<{
isShown: boolean
natHelper?: NatHelper
}>(),
{ isShown: false }
)

const emit = defineEmits(['close', 'reloadData'])

const { t } = useI18n()
const notificationsStore = useNotificationsStore()
const router = useRouter()

const module = ref('')
const enabled = ref(true)
const params = ref<Param[]>([])
const errorBag = ref(new MessageBag())

const loading = ref({
editNatHelper: false
})

const error = ref({
editNatHelper: '',
editNatHelperDetails: ''
})

watch(
() => props.isShown,
() => {
if (props.isShown) {
clearErrors()

if (props.natHelper) {
module.value = props.natHelper.name
enabled.value = props.natHelper.enabled
const moduleParams = []

if (props.natHelper.params) {
for (let [paramName, paramValue] of Object.entries(props.natHelper.params)) {
moduleParams.push({
name: paramName,
value: paramValue
})
}
params.value = moduleParams
}
}
}
}
)

function clearErrors() {
errorBag.value.clear()
error.value.editNatHelper = ''
error.value.editNatHelperDetails = ''
}

function runValidators(validators: validationOutput[], label: string): boolean {
for (let validator of validators) {
if (!validator.valid) {
errorBag.value.set(label, [validator.errMessage as string])
}
}
return validators.every((validator) => validator.valid)
}

function validate() {
clearErrors()

if (!enabled.value) {
return true
}

// all parameters are required

const paramValidators: [validationOutput[], string][] = []

for (let param of params.value) {
paramValidators.push([[validateRequired(param.value)], param.name])
}

return paramValidators
.map(([validator, label]) => runValidators(validator, label))
.every((result) => result)
}

async function editNatHelper() {
const isValidationOk = validate()
if (!isValidationOk) {
return
}

loading.value.editNatHelper = true
const natHelperName = props.natHelper?.name

const payload = {
name: natHelperName,
enabled: enabled.value,
// convert params array to object
params: params.value.reduce((acc, param) => {
acc[param.name] = param.value
return acc
}, {} as Record<string, string>)
}

try {
const res = await ubusCall('ns.nathelpers', 'edit-nat-helper', payload)
const isRebootNeeded = res.data.reboot_needed

// show toast notification

let toastTitle = ''
let toastDescription = ''
let toastKind = ''
let toastAction: Function | undefined = undefined
let toastActionLabel = ''

if (isRebootNeeded) {
// show warning toast
toastTitle = t('standalone.nat_helpers.reboot_to_apply_changes')
toastDescription = t('standalone.nat_helpers.reboot_to_apply_changes_description', {
module: natHelperName
})
toastKind = 'warning'
toastAction = () => {
router.push(`${getStandaloneRoutePrefix()}/system/reboot-and-shutdown`)
}
toastActionLabel = t('common.go_to_page', { page: t('standalone.reboot_and_shutdown.title') })
} else {
// show success toast
toastTitle = t('standalone.nat_helpers.nat_helper_name_saved', {
module: natHelperName
})
toastKind = 'success'
}

setTimeout(() => {
notificationsStore.createNotification({
title: toastTitle,
description: toastDescription,
kind: toastKind,
secondaryAction: toastAction,
secondaryLabel: toastActionLabel
})
}, 500)

emit('reloadData')
closeDrawer()
} catch (err: any) {
console.error(err)

if (err instanceof ValidationError) {
errorBag.value = err.errorBag
} else {
error.value.editNatHelper = t(getAxiosErrorMessage(err))
error.value.editNatHelperDetails = err.toString()
}
} finally {
loading.value.editNatHelper = false
}
}

function closeDrawer() {
emit('close')
}

function getParamLabel(paramName: string) {
return upperFirst(paramName.replace(/_/g, ' '))
}
</script>

<template>
<NeSideDrawer
:isShown="isShown"
:title="t('standalone.nat_helpers.edit_nat_helper')"
:closeAriaLabel="t('common.shell.close_side_drawer')"
@close="closeDrawer()"
>
<form @submit.prevent>
<div class="space-y-6">
<NeTextInput v-model="module" :label="t('standalone.nat_helpers.module')" disabled />
<NeToggle
v-model="enabled"
:topLabel="t('common.status')"
:label="enabled ? t('common.enabled') : t('common.disabled')"
/>
<template v-if="enabled">
<!-- params -->
<NeTextInput
v-for="param in params"
:key="param.name"
v-model.trim="param.value"
:label="getParamLabel(param.name)"
:invalidMessage="t(errorBag.getFirstI18nKeyFor(param.name))"
/>
</template>
<!-- editNatHelper error notification -->
<NeInlineNotification
v-if="error.editNatHelper"
kind="error"
:title="t('error.cannot_save_nat_helper')"
:description="error.editNatHelper"
>
<template #details v-if="error.editNatHelperDetails">
{{ error.editNatHelperDetails }}
</template>
</NeInlineNotification>
<!-- footer -->
<hr class="my-8 border-gray-200 dark:border-gray-700" />
<div class="flex justify-end">
<NeButton
kind="tertiary"
size="lg"
@click.prevent="closeDrawer"
:disabled="loading.editNatHelper"
class="mr-3"
>
{{ t('common.cancel') }}
</NeButton>
<NeButton
kind="primary"
size="lg"
@click.prevent="editNatHelper"
:disabled="loading.editNatHelper"
:loading="loading.editNatHelper"
>
{{ t('common.save') }}
</NeButton>
</div>
</div>
</form>
</NeSideDrawer>
</template>
Loading

0 comments on commit 2117b27

Please sign in to comment.