Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: nat helpers UI #352

Merged
merged 1 commit into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>)
Tbaile marked this conversation as resolved.
Show resolved Hide resolved
}

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