mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-09-10 22:14:48 -05:00
feat(xo6/traffic rule): edit a traffic rule (#10056)
This commit is contained in:
@@ -755,9 +755,9 @@ export type TrafficRuleDirection = 'from' | 'to' | 'from/to'
|
||||
|
||||
export type RawTrafficRule = {
|
||||
allow: boolean
|
||||
protocol: string
|
||||
protocol: TrafficRuleProtocol
|
||||
ipRange: string
|
||||
direction: string
|
||||
direction: TrafficRuleDirection
|
||||
port?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -153,3 +153,10 @@ If the translation in your language for this new key is the same as an already e
|
||||
```
|
||||
|
||||
This provides the option to override the `save-changes` translation in some locales if needed, while keeping the same translation as `save` by default.
|
||||
|
||||
Some translations use modifiers on linked translations, such as `@.lower:<key>` (lowercase) or `@.capitalize:<key>`.
|
||||
|
||||
These modifiers automatically transform the casing of the referenced translation.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> When translating a key that is referenced with a modifier, keep in mind that the output will be automatically transformed. Make sure the result is correct in your language, and override the referencing key with a direct translation if needed.
|
||||
|
||||
@@ -295,3 +295,21 @@ If you really need to **exceptionally** include a literal string, you can either
|
||||
For other locales added by contributors, some keys might be missing when we add new translations.
|
||||
|
||||
This can result in ESLint errors when some translation keys are missing. To avoid this, remember to add the locale to the list of excluded files in `.eslintrc.js` at the root of the project (rule is `'@intlify/vue-i18n/no-missing-keys-in-other-locales': ['error', { ignoreLocales: ['de', 'fa'] }]`), and update the `@xen-orchestra/web-core/lib/i18n.ts` file to include the new locale.
|
||||
|
||||
### Linked translations with modifiers
|
||||
|
||||
Vue-i18n provides built-in modifiers that can be applied to linked translations, such as @.lower:<key>, @.upper:<key>, or @.capitalize:<key>.
|
||||
|
||||
These modifiers transform the referenced translation (e.g., forcing it to lowercase).
|
||||
|
||||
> \[!IMPORTANT\]
|
||||
> Case transformation rules vary across languages (e.g., German capitalizes all nouns, Turkish has specific `I`/`ı` casing rules). When using a modifier, the referenced key SHOULD contain a simple, standalone word - not a sentence or phrase.
|
||||
|
||||
#### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"vif": "VIF",
|
||||
"on-vif": "on {'@.lower:vif'}"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
"action:duplicate-n-vms": "Duplicate 1 VM | Duplicate {n} VMs",
|
||||
"action:edit": "Edit",
|
||||
"action:edit-config": "Edit config",
|
||||
"action:edit-traffic-rule": "@:action:edit @.lower:traffic-rule",
|
||||
"action:enable": "Enable",
|
||||
"action:enable-host": "Enable host",
|
||||
"action:enable-light-mode": "Enable light mode",
|
||||
@@ -391,6 +392,7 @@
|
||||
"fast-clone-available": "Fast clone is available only for halted VMs.",
|
||||
"fetching-fresh-data": "Fetching fresh data",
|
||||
"field:exceeds-max-characters": "Field value must be {max} characters or less.",
|
||||
"field:required": "Field is required.",
|
||||
"file": "File",
|
||||
"filter-actions": "Filter actions",
|
||||
"following-hosts-unreachable": "The following hosts are unreachable",
|
||||
@@ -505,6 +507,7 @@
|
||||
"job:delete:in-progress": "Delete in progress…",
|
||||
"job:disable:in-progress": "Disable in progress…",
|
||||
"job:disconnect:in-progress": "Disconnect in progress…",
|
||||
"job:edit:in-progress": "Edit in progress…",
|
||||
"job:enable:in-progress": "Enable in progress…",
|
||||
"job:export:in-progress": "Export in progress…",
|
||||
"job:host-detach:in-progress": "Host detach in progress…",
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
"action:duplicate-n-vms": "Dupliquer 1 VM | Dupliquer {n} VMs",
|
||||
"action:edit": "Modifier",
|
||||
"action:edit-config": "Modifier config",
|
||||
"action:edit-traffic-rule": "@:action:edit la @.lower:traffic-rule",
|
||||
"action:enable": "Activer",
|
||||
"action:enable-host": "Activer l'hôte",
|
||||
"action:enable-light-mode": "Activer le mode clair",
|
||||
@@ -391,6 +392,7 @@
|
||||
"fast-clone-available": "Clonage rapide disponible uniquement pour les VMs arrêtées.",
|
||||
"fetching-fresh-data": "Récupération de données à jour",
|
||||
"field:exceeds-max-characters": "Le champ doit contenir {max} caractères ou moins.",
|
||||
"field:required": "Champ obligatoire.",
|
||||
"file": "Fichier",
|
||||
"filter-actions": "Actions du filtre",
|
||||
"following-hosts-unreachable": "Les hôtes suivants sont inaccessibles",
|
||||
@@ -504,6 +506,7 @@
|
||||
"job:delete:in-progress": "Suppression en cours…",
|
||||
"job:disable:in-progress": "Désactivation en cours…",
|
||||
"job:disconnect:in-progress": "Déconnexion en cours…",
|
||||
"job:edit:in-progress": "Modification en cours…",
|
||||
"job:enable:in-progress": "Activation en cours…",
|
||||
"job:export:in-progress": "Export en cours…",
|
||||
"job:host-detach:in-progress": "Détachement de l'hôte en cours…",
|
||||
|
||||
@@ -52,6 +52,7 @@ import type { FrontXoPool } from '@/modules/pool/remote-resources/use-xo-pool-co
|
||||
import { useDirectionLabels } from '@/modules/traffic-rules/composables/direction-labels.composable.ts'
|
||||
import { useTrafficRuleTarget } from '@/modules/traffic-rules/composables/traffic-rule-target.composable.ts'
|
||||
import { useTrafficRuleDelete } from '@/modules/traffic-rules/composables/use-traffic-rule-delete.composable.ts'
|
||||
import { useTrafficRuleEdit } from '@/modules/traffic-rules/composables/use-traffic-rule-edit.composable.ts'
|
||||
import type { EnrichedTrafficRule } from '@/modules/traffic-rules/types.ts'
|
||||
import { XO_LINKS } from '@/shared/constants.ts'
|
||||
import VtsQueryBuilder from '@core/components/query-builder/VtsQueryBuilder.vue'
|
||||
@@ -154,6 +155,8 @@ const { HeadCells, BodyCells } = useTrafficRulesColumns({
|
||||
const { deleteTrafficRules, canDeleteTrafficRules, isDeletingTrafficRules, deleteTrafficRulesErrorMessage } =
|
||||
useTrafficRuleDelete(() => [rule])
|
||||
|
||||
const { editTrafficRule, canEditTrafficRule, isEditingTrafficRule } = useTrafficRuleEdit(() => rule)
|
||||
|
||||
return {
|
||||
order: r => r(rule.order),
|
||||
policy: r => r(t(rule.allow ? 'allow' : 'drop'), rule.allow ? 'success' : 'danger'),
|
||||
@@ -166,6 +169,13 @@ const { HeadCells, BodyCells } = useTrafficRulesColumns({
|
||||
r({
|
||||
onClick: () => (selectedRuleId.value = rule.id),
|
||||
actions: [
|
||||
{
|
||||
label: t('action:edit'),
|
||||
icon: 'action:edit',
|
||||
onClick: () => editTrafficRule(),
|
||||
busy: isEditingTrafficRule.value,
|
||||
disabled: !canEditTrafficRule.value,
|
||||
},
|
||||
{
|
||||
label: t('action:delete'),
|
||||
icon: 'action:delete',
|
||||
|
||||
@@ -1,15 +1,33 @@
|
||||
<template>
|
||||
<UiButton
|
||||
size="medium"
|
||||
variant="tertiary"
|
||||
accent="brand"
|
||||
left-icon="action:edit"
|
||||
:disabled="!canEditTrafficRule"
|
||||
:busy="isEditingTrafficRule"
|
||||
@click="editTrafficRule()"
|
||||
>
|
||||
{{ t('action:edit') }}
|
||||
</UiButton>
|
||||
<VtsDeleteButton :disabled="!canDeleteTrafficRules" :busy="isDeletingTrafficRules" @click="deleteTrafficRules()" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useTrafficRuleDelete } from '@/modules/traffic-rules/composables/use-traffic-rule-delete.composable.ts'
|
||||
import { useTrafficRuleEdit } from '@/modules/traffic-rules/composables/use-traffic-rule-edit.composable.ts'
|
||||
import VtsDeleteButton from '@core/components/delete-button/VtsDeleteButton.vue'
|
||||
import UiButton from '@core/components/ui/button/UiButton.vue'
|
||||
import type { TrafficRule } from '@vates/types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { rule } = defineProps<{
|
||||
rule: TrafficRule
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { deleteTrafficRules, canDeleteTrafficRules, isDeletingTrafficRules } = useTrafficRuleDelete(() => [rule])
|
||||
|
||||
const { editTrafficRule, canEditTrafficRule, isEditingTrafficRule } = useTrafficRuleEdit(() => rule)
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<UiDrawer
|
||||
class="traffic-rule-edit-drawer"
|
||||
:on-dismiss="() => emit('cancel')"
|
||||
@dismiss="emit('cancel')"
|
||||
@confirm="onConfirm()"
|
||||
>
|
||||
<template #title>{{ t('action:edit-traffic-rule') }}</template>
|
||||
|
||||
<template #content>
|
||||
<UiTitle class="section-title">
|
||||
{{ t('general-information') }}
|
||||
</UiTitle>
|
||||
|
||||
<span class="typo-body-regular-small required-hint">{{ t('field:required') }}</span>
|
||||
|
||||
<EditTrafficRuleForm ref="form" class="form" :rule />
|
||||
</template>
|
||||
|
||||
<template #buttons>
|
||||
<VtsOverlayCancelButton @click="emit('cancel')" />
|
||||
<VtsOverlayConfirmButton>
|
||||
{{ t('action:save') }}
|
||||
</VtsOverlayConfirmButton>
|
||||
</template>
|
||||
</UiDrawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import EditTrafficRuleForm from '@/modules/traffic-rules/components/form/edit/EditTrafficRuleForm.vue'
|
||||
import type { TrafficRulePayload } from '@/modules/traffic-rules/jobs/xo-traffic-rule-create.job.ts'
|
||||
import VtsOverlayCancelButton from '@core/components/overlay/VtsOverlayCancelButton.vue'
|
||||
import VtsOverlayConfirmButton from '@core/components/overlay/VtsOverlayConfirmButton.vue'
|
||||
import UiDrawer from '@core/components/ui/drawer/UiDrawer.vue'
|
||||
import UiTitle from '@core/components/ui/title/UiTitle.vue'
|
||||
import type { TrafficRule } from '@vates/types'
|
||||
import { useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
defineProps<{
|
||||
rule: TrafficRule
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
cancel: []
|
||||
confirm: [payload: TrafficRulePayload | undefined]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const form = useTemplateRef('form')
|
||||
|
||||
async function onConfirm() {
|
||||
emit('confirm', await form.value?.validate())
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.traffic-rule-edit-drawer {
|
||||
.section-title {
|
||||
margin-block-end: 2.4rem;
|
||||
}
|
||||
|
||||
.form {
|
||||
margin-block-start: 2.4rem;
|
||||
}
|
||||
|
||||
.required-hint::before {
|
||||
content: '* ';
|
||||
color: var(--color-brand-txt-base);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<div class="edit-traffic-rule-form">
|
||||
<div class="row">
|
||||
<TrafficRuleFormSelect v-bind="allowSelectBindings">
|
||||
<template #option="{ option }">
|
||||
<VtsOption :option>
|
||||
<span class="option-content">
|
||||
<VtsStatus v-if="option.properties.status" :status="option.properties.status" icon-only />
|
||||
{{ option.properties.label }}
|
||||
</span>
|
||||
</VtsOption>
|
||||
</template>
|
||||
</TrafficRuleFormSelect>
|
||||
<TrafficRuleFormSelect v-bind="protocolSelectBindings" />
|
||||
<TrafficRuleFormNumberInput v-if="hasPort" v-bind="portInputBindings" />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<TrafficRuleFormSelect v-bind="directionSelectBindings" />
|
||||
<TrafficRuleFormTextInput v-bind="ipRangeInputBindings" />
|
||||
</div>
|
||||
|
||||
<div class="row target-row">
|
||||
<span class="prefix-wrapper">
|
||||
<span class="prefix">{{ t('on') }}</span>
|
||||
</span>
|
||||
<TrafficRuleFormSelect v-bind="targetTypeSelectBindings" />
|
||||
<TrafficRuleFormSelect v-if="isVifTarget" v-bind="vmSelectBindings">
|
||||
<template #option="{ option }">
|
||||
<VtsOption :option>
|
||||
<span class="option-content">
|
||||
<VtsIcon v-if="option.properties.icon" :name="option.properties.icon" size="medium" />
|
||||
{{ option.properties.label }}
|
||||
</span>
|
||||
</VtsOption>
|
||||
</template>
|
||||
</TrafficRuleFormSelect>
|
||||
<TrafficRuleFormSelect v-bind="targetSelectBindings">
|
||||
<template #option="{ option }">
|
||||
<VtsOption :option>
|
||||
<span class="option-content">
|
||||
<VtsIcon v-if="option.properties.icon" :name="option.properties.icon" size="medium" />
|
||||
{{ option.properties.label }}
|
||||
</span>
|
||||
</VtsOption>
|
||||
</template>
|
||||
</TrafficRuleFormSelect>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import TrafficRuleFormNumberInput from '@/modules/traffic-rules/components/form/inputs/TrafficRuleFormNumberInput.vue'
|
||||
import TrafficRuleFormSelect from '@/modules/traffic-rules/components/form/inputs/TrafficRuleFormSelect.vue'
|
||||
import TrafficRuleFormTextInput from '@/modules/traffic-rules/components/form/inputs/TrafficRuleFormTextInput.vue'
|
||||
import { useEditTrafficRuleForm } from '@/modules/traffic-rules/form/edit/use-edit-traffic-rule-form.ts'
|
||||
import VtsIcon from '@core/components/icon/VtsIcon.vue'
|
||||
import VtsOption from '@core/components/select/VtsOption.vue'
|
||||
import VtsStatus from '@core/components/status/VtsStatus.vue'
|
||||
import type { TrafficRule } from '@vates/types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { rule } = defineProps<{
|
||||
rule: TrafficRule
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const {
|
||||
hasPort,
|
||||
isVifTarget,
|
||||
allowSelectBindings,
|
||||
protocolSelectBindings,
|
||||
portInputBindings,
|
||||
directionSelectBindings,
|
||||
ipRangeInputBindings,
|
||||
targetTypeSelectBindings,
|
||||
vmSelectBindings,
|
||||
targetSelectBindings,
|
||||
validateAndBuildPayload,
|
||||
} = useEditTrafficRuleForm(() => rule)
|
||||
|
||||
defineExpose({
|
||||
validate: validateAndBuildPayload,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.option-content {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.edit-traffic-rule-form {
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2.4rem;
|
||||
|
||||
& > * {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-block-start: 2.4rem;
|
||||
}
|
||||
|
||||
@media (--medium-or-large) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 2.4rem;
|
||||
max-width: 78rem;
|
||||
|
||||
& > * {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (--medium-or-large) {
|
||||
.row:nth-child(2) > :nth-child(2) {
|
||||
grid-column: 2 / 4;
|
||||
}
|
||||
|
||||
.target-row:has(> :nth-child(3):last-child) > :nth-child(3) {
|
||||
grid-column: 3 / -1;
|
||||
}
|
||||
|
||||
.target-row {
|
||||
align-items: start;
|
||||
|
||||
.prefix-wrapper {
|
||||
place-self: end;
|
||||
|
||||
.prefix {
|
||||
height: 4rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--color-neutral-txt-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -66,12 +66,12 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FrontXoPool } from '@/modules/pool/remote-resources/use-xo-pool-collection.ts'
|
||||
import TrafficRuleFormNumberInput from '@/modules/traffic-rules/components/form/new/inputs/TrafficRuleFormNumberInput.vue'
|
||||
import TrafficRuleFormSelect from '@/modules/traffic-rules/components/form/new/inputs/TrafficRuleFormSelect.vue'
|
||||
import TrafficRuleFormTextInput from '@/modules/traffic-rules/components/form/new/inputs/TrafficRuleFormTextInput.vue'
|
||||
import TrafficRuleFormNumberInput from '@/modules/traffic-rules/components/form/inputs/TrafficRuleFormNumberInput.vue'
|
||||
import TrafficRuleFormSelect from '@/modules/traffic-rules/components/form/inputs/TrafficRuleFormSelect.vue'
|
||||
import TrafficRuleFormTextInput from '@/modules/traffic-rules/components/form/inputs/TrafficRuleFormTextInput.vue'
|
||||
import NewTrafficRuleButtonsSection from '@/modules/traffic-rules/components/form/new/NewTrafficRuleButtonsSection.vue'
|
||||
import { useNewTrafficRuleForm } from '@/modules/traffic-rules/form/new/use-new-traffic-rule-form.ts'
|
||||
import type { NewTrafficRulePayload } from '@/modules/traffic-rules/jobs/xo-traffic-rule-create.job.ts'
|
||||
import type { TrafficRulePayload } from '@/modules/traffic-rules/jobs/xo-traffic-rule-create.job.ts'
|
||||
import type { FrontXoVif } from '@/modules/vif/remote-resources/use-xo-vif-collection.ts'
|
||||
import VtsForm from '@core/components/form/VtsForm.vue'
|
||||
import VtsIcon from '@core/components/icon/VtsIcon.vue'
|
||||
@@ -87,7 +87,7 @@ const { poolId, vifId } = defineProps<{
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
create: [data: NewTrafficRulePayload]
|
||||
create: [data: TrafficRulePayload]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<VtsSidePanel :has-selection="!!rule" @close="emit('close')">
|
||||
<template v-if="rule" #actions>
|
||||
<TrafficRuleActions :rule class="delete-button" />
|
||||
<TrafficRuleActions :rule />
|
||||
</template>
|
||||
|
||||
<template v-if="rule" #default>
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { TrafficRulePayload } from '@/modules/traffic-rules/jobs/xo-traffic-rule-create.job.ts'
|
||||
import { useXoTrafficRuleEditJob } from '@/modules/traffic-rules/jobs/xo-traffic-rule-edit.job.ts'
|
||||
import { KEEP_OVERLAY_OPEN } from '@core/packages/overlay/symbols.ts'
|
||||
import { useOverlay } from '@core/packages/overlay/use-overlay.ts'
|
||||
import { toComputed } from '@core/utils/to-computed.util.ts'
|
||||
import type { TrafficRule } from '@vates/types'
|
||||
import { reactiveComputed } from '@vueuse/core'
|
||||
import { type MaybeRefOrGetter, ref } from 'vue'
|
||||
|
||||
export function useTrafficRuleEdit(rawTrafficRule: MaybeRefOrGetter<TrafficRule>) {
|
||||
const trafficRule = toComputed(rawTrafficRule)
|
||||
|
||||
const newRulePayload = ref<TrafficRulePayload | undefined>(undefined)
|
||||
|
||||
const {
|
||||
run,
|
||||
canRun: canEditTrafficRule,
|
||||
isRunning: isEditingTrafficRule,
|
||||
errorMessage: editTrafficRuleErrorMessage,
|
||||
} = useXoTrafficRuleEditJob(() => [trafficRule.value], newRulePayload)
|
||||
|
||||
const { open } = useOverlay({
|
||||
component: () => import('@/modules/traffic-rules/components/drawer/TrafficRuleEditDrawer.vue'),
|
||||
events: {
|
||||
onConfirm: async (payload: TrafficRulePayload | undefined) => {
|
||||
if (payload === undefined) {
|
||||
return KEEP_OVERLAY_OPEN
|
||||
}
|
||||
|
||||
newRulePayload.value = payload
|
||||
|
||||
try {
|
||||
await run()
|
||||
} catch (error) {
|
||||
console.error('Error when updating traffic rule:', error)
|
||||
}
|
||||
},
|
||||
onCancel: true,
|
||||
},
|
||||
})
|
||||
|
||||
const props = reactiveComputed(() => ({
|
||||
rule: trafficRule.value,
|
||||
}))
|
||||
|
||||
function editTrafficRule() {
|
||||
return open({ props })
|
||||
}
|
||||
|
||||
return { editTrafficRule, canEditTrafficRule, isEditingTrafficRule, editTrafficRuleErrorMessage }
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import {
|
||||
type FrontXoNetwork,
|
||||
useXoNetworkCollection,
|
||||
} from '@/modules/network/remote-resources/use-xo-network-collection.ts'
|
||||
import type { TargetOption } from '@/modules/traffic-rules/form/target-option.util.ts'
|
||||
import {
|
||||
networkToTargetOption,
|
||||
vifToTargetOption,
|
||||
vmToTargetOption,
|
||||
} from '@/modules/traffic-rules/form/target-option.util.ts'
|
||||
import {
|
||||
type BaseTrafficRuleFormData,
|
||||
useTrafficRuleFormBase,
|
||||
} from '@/modules/traffic-rules/form/use-traffic-rule-form-base.ts'
|
||||
import type { TrafficRulePayload } from '@/modules/traffic-rules/jobs/xo-traffic-rule-create.job.ts'
|
||||
import { type FrontXoVif, useXoVifCollection } from '@/modules/vif/remote-resources/use-xo-vif-collection.ts'
|
||||
import { type FrontXoVm, useXoVmCollection } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'
|
||||
import { toComputed } from '@core/utils/to-computed.util.ts'
|
||||
import type { TrafficRule, TrafficRuleTargetType } from '@vates/types'
|
||||
import { computed, type MaybeRefOrGetter, reactive, watchEffect } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export type EditTrafficRuleFormData = BaseTrafficRuleFormData & {
|
||||
targetType: TrafficRuleTargetType
|
||||
targetId: FrontXoNetwork['id'] | FrontXoVif['id'] | undefined
|
||||
vmId: FrontXoVm['id'] | undefined
|
||||
}
|
||||
|
||||
export function useEditTrafficRuleForm(rawRule: MaybeRefOrGetter<TrafficRule>) {
|
||||
const { t } = useI18n()
|
||||
|
||||
const rule = toComputed(rawRule)
|
||||
|
||||
const { useGetNetworkById } = useXoNetworkCollection()
|
||||
const { useGetVifById } = useXoVifCollection()
|
||||
const { useGetVmById } = useXoVmCollection()
|
||||
|
||||
const isVifTarget = computed(() => rule.value.type === 'VIF')
|
||||
|
||||
const ruleNetwork = useGetNetworkById(() => (rule.value.type === 'network' ? rule.value.sourceId : undefined))
|
||||
const ruleVif = useGetVifById(() => (rule.value.type === 'VIF' ? rule.value.sourceId : undefined))
|
||||
const ruleVm = useGetVmById(() => ruleVif.value?.$VM)
|
||||
|
||||
const formData = reactive<EditTrafficRuleFormData>({
|
||||
allow: rule.value.allow,
|
||||
direction: rule.value.direction,
|
||||
protocol: rule.value.protocol,
|
||||
port: rule.value.port !== undefined ? Number(rule.value.port) : undefined,
|
||||
ipRange: rule.value.ipRange,
|
||||
targetType: rule.value.type,
|
||||
targetId: rule.value.sourceId,
|
||||
vmId: undefined,
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (ruleVif.value && !formData.vmId) {
|
||||
formData.vmId = ruleVif.value.$VM
|
||||
}
|
||||
})
|
||||
|
||||
const targetTypeOptions = [
|
||||
{ id: 'network', label: t('network'), value: 'network' },
|
||||
{ id: 'VIF', label: t('vif'), value: 'VIF' },
|
||||
]
|
||||
|
||||
const vmOptions = computed(() => {
|
||||
if (ruleVm.value) {
|
||||
const hasVifs = ruleVm.value.VIFs.length > 0
|
||||
return [vmToTargetOption(ruleVm.value, hasVifs)]
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
const targetOptions = computed<TargetOption[]>(() => {
|
||||
if (rule.value.type === 'VIF') {
|
||||
return ruleVif.value ? [vifToTargetOption(ruleVif.value, `${t('vif')}${ruleVif.value.device}`)] : []
|
||||
}
|
||||
return ruleNetwork.value ? [networkToTargetOption(ruleNetwork.value)] : []
|
||||
})
|
||||
|
||||
const {
|
||||
validate,
|
||||
useFormSelect,
|
||||
useSelect,
|
||||
hasPort,
|
||||
allowSelectBindings,
|
||||
directionSelectBindings,
|
||||
protocolSelectBindings,
|
||||
portInputBindings,
|
||||
ipRangeInputBindings,
|
||||
buildBaseRulePayload,
|
||||
} = useTrafficRuleFormBase(formData)
|
||||
|
||||
const { id: targetTypeSelectId } = useFormSelect('targetType', targetTypeOptions, {
|
||||
required: true,
|
||||
disabled: () => true,
|
||||
option: { label: 'label', value: 'value' },
|
||||
})
|
||||
|
||||
const { id: vmSelectId } = useFormSelect('vmId', vmOptions, {
|
||||
required: () => isVifTarget.value,
|
||||
disabled: () => true,
|
||||
option: { label: 'label', value: 'value', properties: source => ({ icon: source.icon }) },
|
||||
})
|
||||
|
||||
const { id: targetSelectId } = useFormSelect('targetId', targetOptions, {
|
||||
required: true,
|
||||
disabled: () => true,
|
||||
option: {
|
||||
label: 'label',
|
||||
value: 'value',
|
||||
properties: source => ({ icon: source.icon }),
|
||||
},
|
||||
})
|
||||
|
||||
async function validateAndBuildPayload(): Promise<TrafficRulePayload | undefined> {
|
||||
const valid = await validate()
|
||||
|
||||
if (!valid) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
...buildBaseRulePayload(),
|
||||
targetType: rule.value.type,
|
||||
targetId: rule.value.sourceId,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isVifTarget,
|
||||
hasPort,
|
||||
allowSelectBindings,
|
||||
directionSelectBindings,
|
||||
protocolSelectBindings,
|
||||
portInputBindings,
|
||||
ipRangeInputBindings,
|
||||
targetTypeSelectBindings: useSelect(targetTypeSelectId, () => ({ label: t('object') })),
|
||||
vmSelectBindings: useSelect(vmSelectId, () => ({ label: t('from-vm') })),
|
||||
targetSelectBindings: useSelect(targetSelectId, () => ({
|
||||
label: formData.targetType === 'network' ? t('network') : t('vif'),
|
||||
})),
|
||||
validateAndBuildPayload,
|
||||
}
|
||||
}
|
||||
@@ -6,19 +6,19 @@ import { getNetworkIcon } from '@/modules/network/utils/xo-network.util.ts'
|
||||
import { useXoPifCollection } from '@/modules/pif/remote-resources/use-xo-pif-collection.ts'
|
||||
import { type FrontXoPool, useXoPoolCollection } from '@/modules/pool/remote-resources/use-xo-pool-collection.ts'
|
||||
import { useDirectionLabels } from '@/modules/traffic-rules/composables/direction-labels.composable.ts'
|
||||
import { vmToTargetOption } from '@/modules/traffic-rules/form/target-option.util.ts'
|
||||
import {
|
||||
type BaseTrafficRuleFormData,
|
||||
useTrafficRuleFormBase,
|
||||
} from '@/modules/traffic-rules/form/use-traffic-rule-form-base.ts'
|
||||
import type { NewTrafficRulePayload } from '@/modules/traffic-rules/jobs/xo-traffic-rule-create.job.ts'
|
||||
import type { TrafficRulePayload } from '@/modules/traffic-rules/jobs/xo-traffic-rule-create.job.ts'
|
||||
import { isNetworkRuleSupported } from '@/modules/traffic-rules/utils/xo-traffic-rule.util.ts'
|
||||
import { type FrontXoVif, useXoVifCollection } from '@/modules/vif/remote-resources/use-xo-vif-collection.ts'
|
||||
import { type FrontXoVm, useXoVmCollection } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'
|
||||
import { type IconName, objectIcon } from '@core/icons'
|
||||
import type { IconName } from '@core/icons'
|
||||
import { type FormValidationConfig, required, requiredIf, withMessage } from '@core/packages/form-validation'
|
||||
import { toComputed } from '@core/utils/to-computed.util.ts'
|
||||
import type { TrafficRuleTargetType } from '@vates/types'
|
||||
import { toLower } from 'lodash-es'
|
||||
import { computed, type MaybeRefOrGetter, reactive, watch, watchEffect } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -121,13 +121,7 @@ export function useNewTrafficRuleForm(
|
||||
poolVms.value.map(vm => {
|
||||
const hasVifs = vm.VIFs.length > 0
|
||||
|
||||
return {
|
||||
id: vm.id,
|
||||
label: vm.name_label,
|
||||
value: vm.id,
|
||||
icon: objectIcon('vm', toLower(vm.power_state)),
|
||||
disabled: !hasVifs,
|
||||
}
|
||||
return vmToTargetOption(vm, hasVifs)
|
||||
})
|
||||
)
|
||||
|
||||
@@ -236,7 +230,7 @@ export function useNewTrafficRuleForm(
|
||||
}
|
||||
})
|
||||
|
||||
async function validateAndBuildPayload(): Promise<NewTrafficRulePayload | undefined> {
|
||||
async function validateAndBuildPayload(): Promise<TrafficRulePayload | undefined> {
|
||||
if (isUnsupportedNetworkTarget.value) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { FrontXoNetwork } from '@/modules/network/remote-resources/use-xo-network-collection.ts'
|
||||
import type { FrontXoVif } from '@/modules/vif/remote-resources/use-xo-vif-collection.ts'
|
||||
import type { FrontXoVm } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'
|
||||
import { type IconName, objectIcon } from '@core/icons'
|
||||
import { toLower } from 'lodash-es'
|
||||
|
||||
export type TargetOption = {
|
||||
id: FrontXoVif['id'] | FrontXoNetwork['id']
|
||||
label: string
|
||||
value: FrontXoVif['id'] | FrontXoNetwork['id']
|
||||
icon: 'object:network' | 'object:vif'
|
||||
}
|
||||
|
||||
export type VmOption = {
|
||||
id: FrontXoVm['id']
|
||||
label: string
|
||||
value: FrontXoVm['id']
|
||||
icon: IconName
|
||||
disabled: boolean
|
||||
}
|
||||
|
||||
export function vifToTargetOption(vif: FrontXoVif, vifLabel: string): TargetOption {
|
||||
return {
|
||||
id: vif.id,
|
||||
label: vifLabel,
|
||||
value: vif.id,
|
||||
icon: 'object:vif',
|
||||
}
|
||||
}
|
||||
|
||||
export function networkToTargetOption(network: FrontXoNetwork): TargetOption {
|
||||
return {
|
||||
id: network.id,
|
||||
label: network.name_label,
|
||||
value: network.id,
|
||||
icon: 'object:network',
|
||||
}
|
||||
}
|
||||
|
||||
export function vmToTargetOption(vm: FrontXoVm, hasVifs: boolean): VmOption {
|
||||
return {
|
||||
id: vm.id,
|
||||
label: vm.name_label,
|
||||
value: vm.id,
|
||||
icon: objectIcon('vm', toLower(vm.power_state)),
|
||||
disabled: !hasVifs,
|
||||
}
|
||||
}
|
||||
@@ -28,11 +28,11 @@ export type BaseTrafficRuleFormData = {
|
||||
ipRange: string
|
||||
}
|
||||
|
||||
export type BaseNewTrafficRulePayload = {
|
||||
export type BaseTrafficRulePayload = {
|
||||
allow: boolean
|
||||
direction: TrafficRuleDirection
|
||||
protocol: TrafficRuleProtocol
|
||||
port?: number
|
||||
port: number | null
|
||||
ipRange: string
|
||||
}
|
||||
|
||||
@@ -101,13 +101,13 @@ export function useTrafficRuleFormBase<T extends BaseTrafficRuleFormData>(
|
||||
|
||||
const hasPort = computed(() => protocolHasPort(formData.protocol))
|
||||
|
||||
function buildBaseRulePayload(): BaseNewTrafficRulePayload {
|
||||
function buildBaseRulePayload(): BaseTrafficRulePayload {
|
||||
return {
|
||||
allow: formData.allow,
|
||||
direction: formData.direction,
|
||||
protocol: formData.protocol,
|
||||
ipRange: formData.ipRange,
|
||||
...(protocolHasPort(formData.protocol) && typeof formData.port === 'number' && { port: formData.port }),
|
||||
port: protocolHasPort(formData.protocol) && typeof formData.port === 'number' ? formData.port : null,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { NewTrafficRulePayload } from '@/modules/traffic-rules/jobs/xo-traffic-rule-create.job.ts'
|
||||
import type { TrafficRulePayload } from '@/modules/traffic-rules/jobs/xo-traffic-rule-create.job.ts'
|
||||
import { defineJobArg } from '@core/packages/job'
|
||||
|
||||
export const payloadsArg = defineJobArg<NewTrafficRulePayload>({
|
||||
export const payloadsArg = defineJobArg<TrafficRulePayload>({
|
||||
identify: payload => payload.targetId,
|
||||
toArray: true,
|
||||
})
|
||||
|
||||
@@ -13,14 +13,14 @@ import {
|
||||
} from '@vates/types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export type NewTrafficRulePayload = {
|
||||
export type TrafficRulePayload = {
|
||||
targetType: TrafficRuleTargetType
|
||||
targetId: XoNetwork['id'] | XoVif['id']
|
||||
allow: boolean
|
||||
direction: TrafficRuleDirection
|
||||
protocol: TrafficRuleProtocol
|
||||
ipRange: string
|
||||
port?: number
|
||||
port: number | null
|
||||
}
|
||||
|
||||
export const useXoTrafficRuleCreateJob = defineJob('traffic-rule.create', [payloadsArg], () => {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { BaseTrafficRulePayload } from '@/modules/traffic-rules/form/use-traffic-rule-form-base.ts'
|
||||
import { defineJobArg } from '@core/packages/job'
|
||||
|
||||
export const editTrafficRulePayloadArg = defineJobArg<BaseTrafficRulePayload>({
|
||||
identify: false,
|
||||
toArray: false,
|
||||
})
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { FrontXoTask } from '@/modules/task/remote-resources/use-xo-task-collection.ts'
|
||||
import type { BaseTrafficRulePayload } from '@/modules/traffic-rules/form/use-traffic-rule-form-base.ts'
|
||||
import { xoTrafficRulesArg } from '@/modules/traffic-rules/jobs/xo-traffic-rule-args.ts'
|
||||
import { editTrafficRulePayloadArg } from '@/modules/traffic-rules/jobs/xo-traffic-rule-edit-args.ts'
|
||||
import { useXoTaskUtils } from '@/shared/composables/xo-task-utils.composable.ts'
|
||||
import { fetchPost } from '@/shared/utils/fetch.util.ts'
|
||||
import { defineJob, JobError, JobRunningError } from '@core/packages/job'
|
||||
import type { TrafficRule } from '@vates/types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export const useXoTrafficRuleEditJob = defineJob(
|
||||
'traffic-rule.edit',
|
||||
[xoTrafficRulesArg, editTrafficRulePayloadArg],
|
||||
() => {
|
||||
const { monitorTask } = useXoTaskUtils()
|
||||
const { t } = useI18n()
|
||||
|
||||
return {
|
||||
async run(oldRules: TrafficRule[], newRule: BaseTrafficRulePayload): Promise<PromiseSettledResult<void>[]> {
|
||||
const results = await Promise.allSettled(
|
||||
oldRules.map(async oldRule => {
|
||||
const scope = oldRule.type === 'network' ? 'networks' : 'vifs'
|
||||
const endpoint = `plugins/sdn-controller/${scope}/${oldRule.sourceId}/actions/update_traffic_rule`
|
||||
|
||||
const { taskId } = await fetchPost<{ taskId: FrontXoTask['id'] }>(endpoint, {
|
||||
oldRule: {
|
||||
allow: oldRule.allow,
|
||||
direction: oldRule.direction,
|
||||
ipRange: oldRule.ipRange,
|
||||
protocol: oldRule.protocol,
|
||||
port: oldRule.port !== undefined ? Number(oldRule.port) : undefined,
|
||||
},
|
||||
newRule,
|
||||
})
|
||||
|
||||
await monitorTask(taskId)
|
||||
})
|
||||
)
|
||||
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === 'rejected') {
|
||||
console.error(`Failed to edit traffic rule ${oldRules[index].id}:`, result.reason)
|
||||
}
|
||||
})
|
||||
return results
|
||||
},
|
||||
|
||||
validate(isRunning, oldRules) {
|
||||
if (isRunning) {
|
||||
throw new JobRunningError(t('job:edit:in-progress'))
|
||||
}
|
||||
|
||||
if (oldRules.length === 0) {
|
||||
throw new JobError(t('job:arg:missing-payload'))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -28,7 +28,7 @@
|
||||
import type { FrontXoPool } from '@/modules/pool/remote-resources/use-xo-pool-collection.ts'
|
||||
import NewTrafficRuleForm from '@/modules/traffic-rules/components/form/new/NewTrafficRuleForm.vue'
|
||||
import {
|
||||
type NewTrafficRulePayload,
|
||||
type TrafficRulePayload,
|
||||
useXoTrafficRuleCreateJob,
|
||||
} from '@/modules/traffic-rules/jobs/xo-traffic-rule-create.job.ts'
|
||||
import { type FrontXoVif, useXoVifCollection } from '@/modules/vif/remote-resources/use-xo-vif-collection.ts'
|
||||
@@ -59,7 +59,7 @@ const vif = useGetVifById(() => vifId.value)
|
||||
|
||||
const poolId = computed<FrontXoPool['id'] | undefined>(() => queryPoolId.value ?? vif.value?.$pool)
|
||||
|
||||
const formPayload = ref<NewTrafficRulePayload>()
|
||||
const formPayload = ref<TrafficRulePayload>()
|
||||
|
||||
const error = ref<ApiError | Error | undefined>()
|
||||
const hasTrafficRuleCreationError = computed(() => error.value !== undefined)
|
||||
@@ -72,7 +72,7 @@ function handleGoBack() {
|
||||
error.value = undefined
|
||||
}
|
||||
|
||||
async function createTrafficRule(newPayload: NewTrafficRulePayload) {
|
||||
async function createTrafficRule(newPayload: TrafficRulePayload) {
|
||||
formPayload.value = newPayload
|
||||
|
||||
if (!canRun.value) {
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
|
||||
> Users must be able to say: "Nice enhancement, I'm eager to test it"
|
||||
|
||||
- [XO6/Traffic rules] Add possibility of editing a traffic rule (PR [#10056](https://github.com/vatesfr/xen-orchestra/pull/10056))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
> Users must be able to say: "I had this issue, happy to know it's fixed"
|
||||
|
||||
Reference in New Issue
Block a user