mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-11-09 17:06:13 -06:00
feat(xo6): add edit order action within network for traffic rules
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<UiTableCell class="vts-button-icon-list-cell" align="center">
|
||||
<div class="buttons">
|
||||
<UiButtonIcon
|
||||
v-for="(button, index) of buttons"
|
||||
:key="index"
|
||||
v-tooltip="button.label"
|
||||
:aria-label="button.label"
|
||||
:icon="button.icon"
|
||||
:disabled="button.disabled"
|
||||
:target-scale="1.5"
|
||||
accent="brand"
|
||||
size="small"
|
||||
@click="button.onClick()"
|
||||
/>
|
||||
</div>
|
||||
</UiTableCell>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
|
||||
import UiTableCell from '@core/components/ui/table-cell/UiTableCell.vue'
|
||||
import { vTooltip } from '@core/directives/tooltip.directive.ts'
|
||||
import type { IconName } from '@core/icons'
|
||||
|
||||
export type ButtonIconItem = {
|
||||
icon: IconName
|
||||
label: string
|
||||
onClick: () => void
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
buttons: ButtonIconItem[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.vts-button-icon-list-cell {
|
||||
width: 8rem;
|
||||
padding: 0.8rem 1.6rem 0.8rem 0.8rem;
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<UiTableCell class="vts-rank-cell">
|
||||
<div class="content">
|
||||
<span>{{ rank }}</span>
|
||||
<span v-if="shiftIcon" class="shift">
|
||||
<VtsIcon :name="shiftIcon" size="small" />
|
||||
{{ shiftCount }}
|
||||
</span>
|
||||
</div>
|
||||
</UiTableCell>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import VtsIcon from '@core/components/icon/VtsIcon.vue'
|
||||
import UiTableCell from '@core/components/ui/table-cell/UiTableCell.vue'
|
||||
import type { IconName } from '@core/icons'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const { shift = 0 } = defineProps<{
|
||||
rank: number
|
||||
shift?: number
|
||||
}>()
|
||||
|
||||
const shiftIcon = computed<IconName | undefined>(() => {
|
||||
if (shift > 0) {
|
||||
return 'action:move-up'
|
||||
}
|
||||
|
||||
if (shift < 0) {
|
||||
return 'action:move-down'
|
||||
}
|
||||
|
||||
return undefined
|
||||
})
|
||||
|
||||
const shiftCount = computed(() => Math.abs(shift))
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.vts-rank-cell {
|
||||
width: 7rem;
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.shift {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
color: var(--color-neutral-txt-secondary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -4,10 +4,12 @@ import { faCopy, faSquare as checkboxEmpty, type IconDefinition } from '@fortawe
|
||||
import {
|
||||
faAdd,
|
||||
faArrowCircleRight,
|
||||
faArrowDown,
|
||||
faArrowDownLong,
|
||||
faArrowRight,
|
||||
faArrowRotateLeft,
|
||||
faArrowRotateRight,
|
||||
faArrowUp,
|
||||
faArrowUpLong,
|
||||
faArrowUpRightFromSquare,
|
||||
faBan,
|
||||
@@ -153,6 +155,8 @@ export const actionIcons = defineIconPack({
|
||||
color: 'var(--color-neutral-txt-primary)',
|
||||
rotate: 90,
|
||||
},
|
||||
'move-down': constructIcon(faArrowDown),
|
||||
'move-up': constructIcon(faArrowUp),
|
||||
'import-export': [
|
||||
{
|
||||
icon: faArrowUpLong,
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
"action:edit-br": "@:action:edit BR",
|
||||
"action:edit-config": "Edit config",
|
||||
"action:edit-n-brs": "@:action:edit {n} BRs",
|
||||
"action:edit-rules-order": "@:action:edit rules order within network",
|
||||
"action:edit-traffic-rule": "@:action:edit @.lower:traffic-rule",
|
||||
"action:emergency-shutdown": "Emergency shutdown",
|
||||
"action:emergency-shutdown-host": "@:action:emergency-shutdown host",
|
||||
@@ -123,7 +124,9 @@
|
||||
"action:migrate": "Migrate",
|
||||
"action:migrate-n-vms": "Migrate 1 VM | Migrate {n} VMs",
|
||||
"action:migrate-vdi-on-sr": "Migrate on SR",
|
||||
"action:move-down": "Move down",
|
||||
"action:move-filters-to-parent-group": "Move filters to parent group",
|
||||
"action:move-up": "Move up",
|
||||
"action:open": "Open",
|
||||
"action:open-app": "@:action:open {name}",
|
||||
"action:open-console-in-new-tab": "Open console in new tab",
|
||||
@@ -574,6 +577,7 @@
|
||||
"job:arg:missing-payload": "Payload is missing",
|
||||
"job:arg:name-required": "@:name-required",
|
||||
"job:arg:nbd-and-insecure-nbd-exclusive": "NBD and insecure NBD cannot be enabled together",
|
||||
"job:arg:network-required": "@:network-required",
|
||||
"job:arg:password-required": "Password is required",
|
||||
"job:arg:pif-id-required": "PIF ID is required",
|
||||
"job:arg:pif-ids-required": "At least one PIF ID is required",
|
||||
@@ -690,6 +694,8 @@
|
||||
"job:sr-delete:missing-sr": "No SRs to delete",
|
||||
"job:sr-disconnect:missing-sr": "No SRs to disconnect",
|
||||
"job:traffic-rule-delete:missing-rule": "No traffic rule selected",
|
||||
"job:traffic-rule-edit-order:no-free-priority": "No free priority left on this network to apply this order",
|
||||
"job:traffic-rule-edit-order:not-enough-rules": "Requires at least {n} rules",
|
||||
"job:vbd-connect:missing-vbd": "No VBDs to connect",
|
||||
"job:vbd-connect:vbd-already-connected": "VBD is already connected",
|
||||
"job:vbd-delete:missing-vbd": "No VBDs to delete",
|
||||
@@ -1395,6 +1401,8 @@
|
||||
"traffic-rule": "Traffic rule",
|
||||
"traffic-rule-delete-warning": "This action is irreversible. The selected traffic rule will be permanently deleted.",
|
||||
"traffic-rules": "Traffic rules",
|
||||
"traffic-rules:edit-order-info": "Traffic rules are evaluated by priority, starting with rule 1, then rule 2, and so on. The first matching rule is applied, and subsequent rules are skipped. Priority only matters within the same network.",
|
||||
"traffic-rules:edit-order-save-info": "Changes will only take effect when you save.",
|
||||
"traffic-rules:error-plugin": "You are using the direct OpenFlow protocol (legacy mode), which does not support network traffic rules. To create them, switch to the XAPI plugin mode (beta) — see {check-doc}.",
|
||||
"traffic-rules:error-plugin:check-doc": "the SDN controller documentation",
|
||||
"traffic-rules:format-warning": "Configuration error: Traffic rules are not in the expected format. Please {check-doc} on how to migrate them.",
|
||||
@@ -1403,7 +1411,8 @@
|
||||
"traffic-rules:position-on-network": "{position} rule on this network.",
|
||||
"traffic-rules:rule-ordinal-suffixes": "one:st, two:nd, few:rd, other:th",
|
||||
"traffic-rules:target-on": "@.capitalize:direction-on",
|
||||
"traffic-rules:xapi-plugin-required": "Requires XAPI plugin mode",
|
||||
"traffic-rules:xapi-plugin-required": "Requires @:xapi-plugin mode",
|
||||
"traffic-rules:xapi-plugin-required-with-link": "Requires {xapi-plugin} mode",
|
||||
"translation-tool": "Translation tool",
|
||||
"treeview": "Treeview",
|
||||
"trigger": "Trigger",
|
||||
@@ -1555,6 +1564,7 @@
|
||||
"with-memory": "With memory",
|
||||
"write": "Write",
|
||||
"writing-speed": "Writing speed",
|
||||
"xapi-plugin": "XAPI plugin",
|
||||
"xcp-ng": "XCP-ng",
|
||||
"xen-orchestra": "Xen Orchestra",
|
||||
"xo": "XO",
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
"action:edit-br": "@:action:edit le BR",
|
||||
"action:edit-config": "Modifier config",
|
||||
"action:edit-n-brs": "@:action:edit {n} BRs",
|
||||
"action:edit-rules-order": "@:action:edit l'ordre des règles dans le réseau",
|
||||
"action:edit-traffic-rule": "@:action:edit la @.lower:traffic-rule",
|
||||
"action:emergency-shutdown": "Arrêt d'urgence",
|
||||
"action:emergency-shutdown-host": "@:action:emergency-shutdown de l'hôte",
|
||||
@@ -123,7 +124,9 @@
|
||||
"action:migrate": "Migrer",
|
||||
"action:migrate-n-vms": "Migrer 1 VM | Migrer {n} VMs",
|
||||
"action:migrate-vdi-on-sr": "Migrer sur le SR",
|
||||
"action:move-down": "Descendre",
|
||||
"action:move-filters-to-parent-group": "Déplacer les filtres vers le groupe parent",
|
||||
"action:move-up": "Monter",
|
||||
"action:open": "Ouvrir",
|
||||
"action:open-app": "@:action:open {name}",
|
||||
"action:open-console-in-new-tab": "Ouvrir la console dans un nouvel onglet",
|
||||
@@ -574,6 +577,7 @@
|
||||
"job:arg:missing-payload": "Aucune donnée fournie pour l'execution du job",
|
||||
"job:arg:name-required": "@:name-required",
|
||||
"job:arg:nbd-and-insecure-nbd-exclusive": "NBD et NBD non sécurisé ne peuvent pas être activés ensemble",
|
||||
"job:arg:network-required": "@:network-required",
|
||||
"job:arg:password-required": "Le mot de passe est obligatoire",
|
||||
"job:arg:pif-id-required": "L'ID de PIF est obligatoire",
|
||||
"job:arg:pif-ids-required": "Au moins un ID de PIF est obligatoire",
|
||||
@@ -690,6 +694,8 @@
|
||||
"job:sr-delete:missing-sr": "Aucun SR à supprimer",
|
||||
"job:sr-disconnect:missing-sr": "Aucun SR à déconnecter",
|
||||
"job:traffic-rule-delete:missing-rule": "Aucune règle de trafic sélectionnée",
|
||||
"job:traffic-rule-edit-order:no-free-priority": "Plus aucune priorité libre sur ce réseau pour appliquer cet ordre",
|
||||
"job:traffic-rule-edit-order:not-enough-rules": "Nécessite au moins {n} règles",
|
||||
"job:vbd-connect:missing-vbd": "Aucun VBD à connecter",
|
||||
"job:vbd-connect:vbd-already-connected": "Le VBD est déjà connecté",
|
||||
"job:vbd-delete:missing-vbd": "Aucun VBD à supprimer",
|
||||
@@ -1395,6 +1401,8 @@
|
||||
"traffic-rule": "Règle de trafic",
|
||||
"traffic-rule-delete-warning": "Cette action est irréversible. La règle de trafic sélectionnée sera définitivement supprimée.",
|
||||
"traffic-rules": "Règles de trafic",
|
||||
"traffic-rules:edit-order-info": "Les règles de trafic sont évaluées par priorité, en commençant par la règle 1, puis la règle 2, et ainsi de suite. La première règle correspondante est appliquée et les suivantes sont ignorées. La priorité ne compte qu'au sein d'un même réseau.",
|
||||
"traffic-rules:edit-order-save-info": "Les modifications ne prendront effet qu'à l'enregistrement.",
|
||||
"traffic-rules:error-plugin": "Vous utilisez le protocole OpenFlow direct (mode historique), qui ne prend pas en charge les règles de trafic réseau. Pour en créer, passez en mode plugin XAPI (bêta) — voir {check-doc}.",
|
||||
"traffic-rules:error-plugin:check-doc": "la documentation du contrôleur SDN",
|
||||
"traffic-rules:format-warning": "Erreur de configuration : les règles de trafic ne sont pas au format attendu. Veuillez {check-doc} la plus récente pour savoir comment les migrer.",
|
||||
@@ -1403,7 +1411,8 @@
|
||||
"traffic-rules:position-on-network": "{position} règle sur ce réseau.",
|
||||
"traffic-rules:rule-ordinal-suffixes": "one:re, other:e",
|
||||
"traffic-rules:target-on": "@.capitalize:direction-on",
|
||||
"traffic-rules:xapi-plugin-required": "Nécessite le mode plugin XAPI",
|
||||
"traffic-rules:xapi-plugin-required": "Nécessite le mode @:xapi-plugin",
|
||||
"traffic-rules:xapi-plugin-required-with-link": "Nécessite le mode {xapi-plugin}",
|
||||
"translation-tool": "Outil de traduction",
|
||||
"treeview": "Arborescence",
|
||||
"trigger": "Déclencheur",
|
||||
@@ -1555,6 +1564,7 @@
|
||||
"with-memory": "Avec mémoire",
|
||||
"write": "Écriture",
|
||||
"writing-speed": "Vitesse d'écriture",
|
||||
"xapi-plugin": "plugin XAPI",
|
||||
"xcp-ng": "XCP-ng",
|
||||
"xen-orchestra": "Xen Orchestra",
|
||||
"xo": "XO",
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import VtsButtonIconListCell, { type ButtonIconItem } from '@core/components/table/cells/VtsButtonIconListCell.vue'
|
||||
import { defineColumn } from '@core/packages/table/define-column.ts'
|
||||
import { renderHeadCell } from '@core/tables/helpers/render-head-cell.ts'
|
||||
import type { HeaderConfig } from '@core/tables/types.ts'
|
||||
import { h } from 'vue'
|
||||
|
||||
export type { ButtonIconItem }
|
||||
|
||||
export const useButtonIconListColumn = defineColumn((config?: HeaderConfig) => ({
|
||||
renderHead: () => renderHeadCell(config?.headerLabel),
|
||||
renderBody: (buttons: ButtonIconItem[]) => h(VtsButtonIconListCell, { buttons }),
|
||||
}))
|
||||
@@ -0,0 +1,10 @@
|
||||
import VtsRankCell from '@core/components/table/cells/VtsRankCell.vue'
|
||||
import { defineColumn } from '@core/packages/table/define-column.ts'
|
||||
import { renderHeadCell } from '@core/tables/helpers/render-head-cell.ts'
|
||||
import type { HeaderConfig } from '@core/tables/types.ts'
|
||||
import { h } from 'vue'
|
||||
|
||||
export const useRankColumn = defineColumn((config?: HeaderConfig) => ({
|
||||
renderHead: () => renderHeadCell(config?.headerLabel),
|
||||
renderBody: (rank: number, shift?: number) => h(VtsRankCell, { rank, shift }),
|
||||
}))
|
||||
@@ -1,7 +1,9 @@
|
||||
import { defineColumns } from '@core/packages/table/define-columns.ts'
|
||||
import { useActionColumn } from '@core/tables/column-definitions/action-column.ts'
|
||||
import { useButtonIconListColumn } from '@core/tables/column-definitions/button-icon-list-column.ts'
|
||||
import { useDoubleLinkColumn } from '@core/tables/column-definitions/double-link-column.ts'
|
||||
import { useLinkColumn } from '@core/tables/column-definitions/link-column.ts'
|
||||
import { useRankColumn } from '@core/tables/column-definitions/rank-column.ts'
|
||||
import { useTagColumn } from '@core/tables/column-definitions/tag-column.ts'
|
||||
import { useTextColumn } from '@core/tables/column-definitions/text-column.ts'
|
||||
import { useTruncatedTextColumn } from '@core/tables/column-definitions/truncated-text-column.ts'
|
||||
@@ -13,6 +15,7 @@ export const useTrafficRulesColumns = defineColumns(() => {
|
||||
return {
|
||||
network: useLinkColumn({ headerLabel: () => t('network') }),
|
||||
priority: useTextColumn({ headerLabel: () => t('priority') }),
|
||||
rank: useRankColumn({ headerLabel: () => t('priority') }),
|
||||
policy: useTagColumn({ headerLabel: () => t('policy') }),
|
||||
protocol: useTextColumn({
|
||||
headerLabel: () => t('protocol:port'),
|
||||
@@ -24,5 +27,6 @@ export const useTrafficRulesColumns = defineColumns(() => {
|
||||
}),
|
||||
object: useDoubleLinkColumn({ headerLabel: () => t('object') }),
|
||||
actions: useActionColumn({}),
|
||||
moveButtons: useButtonIconListColumn(),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { FrontXoNetwork } from '@/modules/network/remote-resources/use-xo-network-collection.ts'
|
||||
import { defineJobArg } from '@core/packages/job'
|
||||
|
||||
export const xoNetworkArg = defineJobArg({
|
||||
identify: (network: FrontXoNetwork | undefined) => network?.id,
|
||||
toArray: false,
|
||||
})
|
||||
@@ -46,10 +46,9 @@
|
||||
import { useXoNetworkCollection } from '@/modules/network/remote-resources/use-xo-network-collection.ts'
|
||||
import { getPoolNetworkRoute } from '@/modules/network/utils/xo-network.util.ts'
|
||||
import type { FrontXoPool } from '@/modules/pool/remote-resources/use-xo-pool-collection.ts'
|
||||
import TrafficRuleTableActions from '@/modules/traffic-rules/components/actions/TrafficRuleTableActions.vue'
|
||||
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, RankedTrafficRule } from '@/modules/traffic-rules/types.ts'
|
||||
import { getNetworkRules, getTrafficRuleProtocolLabel } from '@/modules/traffic-rules/utils/xo-traffic-rule.util.ts'
|
||||
import { XO_LINKS } from '@/shared/constants.ts'
|
||||
@@ -158,7 +157,7 @@ const state = useTableState({
|
||||
})
|
||||
|
||||
const { HeadCells, BodyCells } = useTrafficRulesColumns({
|
||||
exclude: showPriority ? [] : ['priority'],
|
||||
exclude: showPriority ? ['rank', 'moveButtons'] : ['priority', 'rank', 'moveButtons'],
|
||||
body: (rule: EnrichedTrafficRule) => {
|
||||
const network = useGetNetworkById(() => rule.networkId)
|
||||
|
||||
@@ -166,11 +165,6 @@ const { HeadCells, BodyCells } = useTrafficRulesColumns({
|
||||
network.value ? getPoolNetworkRoute(network.value.$pool, network.value.id) : undefined
|
||||
)
|
||||
|
||||
const { deleteTrafficRules, canDeleteTrafficRules, isDeletingTrafficRules, deleteTrafficRulesErrorMessage } =
|
||||
useTrafficRuleDelete(() => [rule])
|
||||
|
||||
const { editTrafficRule, canEditTrafficRule, isEditingTrafficRule } = useTrafficRuleEdit(() => rule)
|
||||
|
||||
return {
|
||||
network: r =>
|
||||
r({
|
||||
@@ -188,24 +182,8 @@ const { HeadCells, BodyCells } = useTrafficRulesColumns({
|
||||
actions: r =>
|
||||
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',
|
||||
onClick: () => deleteTrafficRules(),
|
||||
disabled: !canDeleteTrafficRules.value,
|
||||
busy: isDeletingTrafficRules.value,
|
||||
hint: deleteTrafficRulesErrorMessage.value,
|
||||
accent: 'danger',
|
||||
},
|
||||
],
|
||||
component: TrafficRuleTableActions,
|
||||
props: { rule, rules: rawRules, showEditOrder: showPriority },
|
||||
}),
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,33 +1,15 @@
|
||||
<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,16 @@
|
||||
<template>
|
||||
<TrafficRuleEditButton :rule />
|
||||
<TrafficRuleEditOrderButton v-if="showEditOrder" :rule :rules />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import TrafficRuleEditButton from '@/modules/traffic-rules/components/actions/edit/TrafficRuleEditButton.vue'
|
||||
import TrafficRuleEditOrderButton from '@/modules/traffic-rules/components/actions/edit-order/TrafficRuleEditOrderButton.vue'
|
||||
import type { RankedTrafficRule } from '@/modules/traffic-rules/types.ts'
|
||||
|
||||
defineProps<{
|
||||
rule: RankedTrafficRule
|
||||
rules: RankedTrafficRule[]
|
||||
showEditOrder?: boolean
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<TrafficRuleMoreActions :rule :rules :show-edit-order />
|
||||
<TrafficRuleDeleteButton :rule />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import TrafficRuleDeleteButton from '@/modules/traffic-rules/components/actions/delete/TrafficRuleDeleteButton.vue'
|
||||
import TrafficRuleMoreActions from '@/modules/traffic-rules/components/actions/TrafficRuleMoreActions.vue'
|
||||
import type { RankedTrafficRule } from '@/modules/traffic-rules/types.ts'
|
||||
|
||||
defineProps<{
|
||||
rule: RankedTrafficRule
|
||||
rules: RankedTrafficRule[]
|
||||
showEditOrder?: boolean
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<MenuItem
|
||||
:busy="isDeletingTrafficRules"
|
||||
:disabled="!canDeleteTrafficRules"
|
||||
accent="danger"
|
||||
icon="action:delete"
|
||||
@click="deleteTrafficRules()"
|
||||
>
|
||||
{{ t('action:delete') }}
|
||||
<i v-if="deleteTrafficRulesErrorMessage" class="em-dash-prefix">{{ deleteTrafficRulesErrorMessage }}</i>
|
||||
</MenuItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useTrafficRuleDelete } from '@/modules/traffic-rules/composables/use-traffic-rule-delete.composable.ts'
|
||||
import MenuItem from '@core/components/menu/MenuItem.vue'
|
||||
import type { TrafficRule } from '@vates/types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { rule } = defineProps<{
|
||||
rule: TrafficRule
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { deleteTrafficRules, canDeleteTrafficRules, isDeletingTrafficRules, deleteTrafficRulesErrorMessage } =
|
||||
useTrafficRuleDelete(() => [rule])
|
||||
</script>
|
||||
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<MenuItem
|
||||
:busy="isEditingTrafficRulesOrder"
|
||||
:disabled="!canEditTrafficRulesOrder"
|
||||
accent="neutral"
|
||||
icon="action:edit"
|
||||
@click="editTrafficRulesOrder()"
|
||||
>
|
||||
{{ t('action:edit-rules-order') }}
|
||||
<i v-if="isXapiPluginRequired" class="em-dash-prefix">
|
||||
<I18nT keypath="traffic-rules:xapi-plugin-required-with-link" scope="global">
|
||||
<template #xapi-plugin>
|
||||
<UiLink :href="XO_LINKS.DOC_SDN_CONTROLLER_XAPI_PLUGIN" size="small">
|
||||
{{ t('xapi-plugin') }}
|
||||
</UiLink>
|
||||
</template>
|
||||
</I18nT>
|
||||
</i>
|
||||
<i v-else-if="editTrafficRulesOrderErrorMessage" class="em-dash-prefix">{{ editTrafficRulesOrderErrorMessage }}</i>
|
||||
</MenuItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useTrafficRuleEditOrder } from '@/modules/traffic-rules/composables/use-traffic-rule-edit-order.composable.ts'
|
||||
import type { RankedTrafficRule } from '@/modules/traffic-rules/types.ts'
|
||||
import { XO_LINKS } from '@/shared/constants.ts'
|
||||
import MenuItem from '@core/components/menu/MenuItem.vue'
|
||||
import UiLink from '@core/components/ui/link/UiLink.vue'
|
||||
import { I18nT, useI18n } from 'vue-i18n'
|
||||
|
||||
const { rule, rules } = defineProps<{
|
||||
rule: RankedTrafficRule
|
||||
rules: RankedTrafficRule[]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const {
|
||||
editTrafficRulesOrder,
|
||||
canEditTrafficRulesOrder,
|
||||
isEditingTrafficRulesOrder,
|
||||
editTrafficRulesOrderErrorMessage,
|
||||
isXapiPluginRequired,
|
||||
} = useTrafficRuleEditOrder(
|
||||
() => rule.networkId,
|
||||
() => rules
|
||||
)
|
||||
</script>
|
||||
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<MenuItem
|
||||
:busy="isEditingTrafficRule"
|
||||
:disabled="!canEditTrafficRule"
|
||||
accent="neutral"
|
||||
icon="action:edit"
|
||||
@click="editTrafficRule()"
|
||||
>
|
||||
{{ t('action:edit') }}
|
||||
</MenuItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useTrafficRuleEdit } from '@/modules/traffic-rules/composables/use-traffic-rule-edit.composable.ts'
|
||||
import MenuItem from '@core/components/menu/MenuItem.vue'
|
||||
import type { TrafficRule } from '@vates/types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { rule } = defineProps<{
|
||||
rule: TrafficRule
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { editTrafficRule, canEditTrafficRule, isEditingTrafficRule } = useTrafficRuleEdit(() => rule)
|
||||
</script>
|
||||
@@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<UiDrawer
|
||||
:on-dismiss="() => emit('cancel')"
|
||||
class="traffic-rule-edit-order-drawer"
|
||||
@confirm="emit('confirm', orderedRules)"
|
||||
@dismiss="emit('cancel')"
|
||||
>
|
||||
<template #title>{{ t('action:edit-rules-order') }}</template>
|
||||
|
||||
<template #content>
|
||||
<div class="container">
|
||||
<div class="network typo-body-regular-small">
|
||||
<span class="label">{{ t('network') }}</span>
|
||||
<UiLink v-if="network" :to="networkTo" icon="object:network" size="small">
|
||||
{{ network.name_label }}
|
||||
</UiLink>
|
||||
</div>
|
||||
|
||||
<UiAlert accent="info">
|
||||
<p>{{ t('traffic-rules:edit-order-info') }}</p>
|
||||
<p>{{ t('traffic-rules:edit-order-save-info') }}</p>
|
||||
</UiAlert>
|
||||
|
||||
<VtsTable :pagination-bindings sticky="right">
|
||||
<thead>
|
||||
<tr>
|
||||
<HeadCells />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<VtsRow v-for="rule of paginatedRules" :key="rule.id">
|
||||
<BodyCells :item="rule" />
|
||||
</VtsRow>
|
||||
</tbody>
|
||||
</VtsTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #buttons>
|
||||
<VtsOverlayCancelButton @click="emit('cancel')" />
|
||||
<VtsOverlayConfirmButton>
|
||||
{{ t('action:save') }}
|
||||
</VtsOverlayConfirmButton>
|
||||
</template>
|
||||
</UiDrawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FrontXoNetwork } from '@/modules/network/remote-resources/use-xo-network-collection.ts'
|
||||
import { getPoolNetworkRoute } from '@/modules/network/utils/xo-network.util.ts'
|
||||
import { useDirectionLabels } from '@/modules/traffic-rules/composables/direction-labels.composable.ts'
|
||||
import { useTrafficRuleTarget } from '@/modules/traffic-rules/composables/traffic-rule-target.composable.ts'
|
||||
import type { RankedTrafficRule } from '@/modules/traffic-rules/types.ts'
|
||||
import { getTrafficRuleProtocolLabel } from '@/modules/traffic-rules/utils/xo-traffic-rule.util.ts'
|
||||
import VtsOverlayCancelButton from '@core/components/overlay/VtsOverlayCancelButton.vue'
|
||||
import VtsOverlayConfirmButton from '@core/components/overlay/VtsOverlayConfirmButton.vue'
|
||||
import VtsRow from '@core/components/table/VtsRow.vue'
|
||||
import VtsTable from '@core/components/table/VtsTable.vue'
|
||||
import UiAlert from '@core/components/ui/alert/UiAlert.vue'
|
||||
import UiDrawer from '@core/components/ui/drawer/UiDrawer.vue'
|
||||
import UiLink from '@core/components/ui/link/UiLink.vue'
|
||||
import { usePagination } from '@core/composables/pagination.composable.ts'
|
||||
import { useTrafficRulesColumns } from '@core/tables/column-sets/traffic-rules-columns.ts'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { network, rules } = defineProps<{
|
||||
network: FrontXoNetwork | undefined
|
||||
rules: RankedTrafficRule[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
cancel: []
|
||||
confirm: [orderedRules: RankedTrafficRule[]]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const getTarget = useTrafficRuleTarget()
|
||||
|
||||
const getDirectionLabels = useDirectionLabels()
|
||||
|
||||
const networkTo = computed(() => {
|
||||
if (network === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return getPoolNetworkRoute(network.$pool, network.id)
|
||||
})
|
||||
|
||||
const orderedRules = ref([...rules])
|
||||
|
||||
const {
|
||||
pageRecords: paginatedRules,
|
||||
paginationBindings,
|
||||
seek,
|
||||
} = usePagination('traffic-rules-order', orderedRules, {
|
||||
ignoreRoute: true,
|
||||
})
|
||||
|
||||
function getRuleIndex(rule: RankedTrafficRule) {
|
||||
return orderedRules.value.findIndex(orderedRule => orderedRule.id === rule.id)
|
||||
}
|
||||
|
||||
function moveRule(rule: RankedTrafficRule, offset: -1 | 1) {
|
||||
const index = getRuleIndex(rule)
|
||||
const newIndex = index + offset
|
||||
|
||||
if (index === -1 || newIndex < 0 || newIndex >= orderedRules.value.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const newOrderedRules = [...orderedRules.value]
|
||||
const [movedRule] = newOrderedRules.splice(index, 1)
|
||||
newOrderedRules.splice(newIndex, 0, movedRule)
|
||||
|
||||
orderedRules.value = newOrderedRules
|
||||
|
||||
seek(orderedRule => orderedRule.id === rule.id)
|
||||
}
|
||||
|
||||
const { HeadCells, BodyCells } = useTrafficRulesColumns({
|
||||
exclude: ['network', 'priority', 'actions'],
|
||||
body: (rule: RankedTrafficRule) => {
|
||||
const [directionA, directionB] = getDirectionLabels(rule)
|
||||
|
||||
return {
|
||||
rank: r => {
|
||||
const newRank = getRuleIndex(rule) + 1
|
||||
|
||||
return r(newRank, rule.rank - newRank)
|
||||
},
|
||||
policy: r => r(t(rule.allow ? 'allow' : 'drop'), rule.allow ? 'success' : 'danger'),
|
||||
protocol: r => r(getTrafficRuleProtocolLabel(rule)),
|
||||
directionA: r => r(directionA),
|
||||
target: r => r(rule.ipRange),
|
||||
directionB: r => r(directionB),
|
||||
object: r => r(getTarget(rule)),
|
||||
moveButtons: r => {
|
||||
const index = getRuleIndex(rule)
|
||||
|
||||
return r([
|
||||
{
|
||||
icon: 'action:move-up',
|
||||
label: t('action:move-up'),
|
||||
onClick: () => moveRule(rule, -1),
|
||||
disabled: index === 0,
|
||||
},
|
||||
{
|
||||
icon: 'action:move-down',
|
||||
label: t('action:move-down'),
|
||||
onClick: () => moveRule(rule, 1),
|
||||
disabled: index === orderedRules.value.length - 1,
|
||||
},
|
||||
])
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.traffic-rule-edit-order-drawer {
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2.4rem;
|
||||
}
|
||||
|
||||
.network {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2.4rem;
|
||||
|
||||
.label {
|
||||
color: var(--color-neutral-txt-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -4,6 +4,10 @@
|
||||
<TrafficRuleActions :rule />
|
||||
</template>
|
||||
|
||||
<template v-if="rule" #more-actions>
|
||||
<TrafficRuleMoreActions :rule :rules :show-edit-order="showPriority" />
|
||||
</template>
|
||||
|
||||
<template v-if="rule" #default>
|
||||
<VtsStateHero v-if="!isReady" format="panel" size="medium" type="busy" />
|
||||
<template v-else>
|
||||
@@ -28,6 +32,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { useXoNetworkCollection } from '@/modules/network/remote-resources/use-xo-network-collection.ts'
|
||||
import TrafficRuleActions from '@/modules/traffic-rules/components/actions/TrafficRuleActions.vue'
|
||||
import TrafficRuleMoreActions from '@/modules/traffic-rules/components/actions/TrafficRuleMoreActions.vue'
|
||||
import TrafficRuleNetworkInfosCard from '@/modules/traffic-rules/components/list/panel/cards/TrafficRuleNetworkInfosCard.vue'
|
||||
import TrafficRuleNetworkPifsCard from '@/modules/traffic-rules/components/list/panel/cards/TrafficRuleNetworkPifsCard.vue'
|
||||
import TrafficRulePriorityCard from '@/modules/traffic-rules/components/list/panel/cards/TrafficRulePriorityCard.vue'
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import {
|
||||
type FrontXoNetwork,
|
||||
useXoNetworkCollection,
|
||||
} from '@/modules/network/remote-resources/use-xo-network-collection.ts'
|
||||
import { useXoPoolCollection } from '@/modules/pool/remote-resources/use-xo-pool-collection.ts'
|
||||
import { useXoTrafficRuleEditOrderJob } from '@/modules/traffic-rules/jobs/xo-traffic-rule-edit-order.job.ts'
|
||||
import type { RankedTrafficRule } from '@/modules/traffic-rules/types.ts'
|
||||
import { getNetworkRules, isNetworkRuleSupported } from '@/modules/traffic-rules/utils/xo-traffic-rule.util.ts'
|
||||
import { useOverlay } from '@core/packages/overlay/use-overlay.ts'
|
||||
import { toComputed } from '@core/utils/to-computed.util.ts'
|
||||
import { reactiveComputed } from '@vueuse/core'
|
||||
import { computed, type MaybeRefOrGetter, ref } from 'vue'
|
||||
|
||||
export function useTrafficRuleEditOrder(
|
||||
rawNetworkId: MaybeRefOrGetter<FrontXoNetwork['id']>,
|
||||
rawTrafficRules: MaybeRefOrGetter<RankedTrafficRule[]>
|
||||
) {
|
||||
const { getNetworkById } = useXoNetworkCollection()
|
||||
|
||||
const { getPoolById } = useXoPoolCollection()
|
||||
|
||||
const networkId = toComputed(rawNetworkId)
|
||||
|
||||
const trafficRules = toComputed(rawTrafficRules)
|
||||
|
||||
const network = computed(() => getNetworkById(networkId.value))
|
||||
|
||||
const networkRules = computed(() => getNetworkRules(trafficRules.value, networkId.value))
|
||||
|
||||
const isXapiPluginRequired = computed(() => {
|
||||
if (network.value === undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
const pool = getPoolById(network.value.$pool)
|
||||
|
||||
return !isNetworkRuleSupported(pool)
|
||||
})
|
||||
|
||||
const orderedRules = ref<RankedTrafficRule[] | undefined>(undefined)
|
||||
|
||||
const {
|
||||
run,
|
||||
canRun: canEditTrafficRulesOrder,
|
||||
isRunning: isEditingTrafficRulesOrder,
|
||||
errorMessage: editTrafficRulesOrderErrorMessage,
|
||||
} = useXoTrafficRuleEditOrderJob(networkRules, orderedRules, network)
|
||||
|
||||
const { open } = useOverlay({
|
||||
component: () => import('@/modules/traffic-rules/components/drawer/TrafficRuleEditOrderDrawer.vue'),
|
||||
events: {
|
||||
onConfirm: (newOrderedRules: RankedTrafficRule[]) => {
|
||||
orderedRules.value = newOrderedRules
|
||||
|
||||
run({ detached: true })
|
||||
},
|
||||
onCancel: true,
|
||||
},
|
||||
})
|
||||
|
||||
const props = reactiveComputed(() => ({
|
||||
network: network.value,
|
||||
rules: networkRules.value,
|
||||
}))
|
||||
|
||||
function editTrafficRulesOrder() {
|
||||
return open({ props })
|
||||
}
|
||||
|
||||
return {
|
||||
editTrafficRulesOrder,
|
||||
canEditTrafficRulesOrder,
|
||||
isEditingTrafficRulesOrder,
|
||||
editTrafficRulesOrderErrorMessage,
|
||||
isXapiPluginRequired,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineJobArg } from '@core/packages/job'
|
||||
import type { TrafficRule } from '@vates/types'
|
||||
|
||||
export const orderedTrafficRulesArg = defineJobArg<TrafficRule[]>({
|
||||
identify: false,
|
||||
toArray: false,
|
||||
})
|
||||
@@ -0,0 +1,76 @@
|
||||
import { xoNetworkArg } from '@/modules/network/jobs/xo-network-args.ts'
|
||||
import type { FrontXoNetwork } from '@/modules/network/remote-resources/use-xo-network-collection.ts'
|
||||
import { useXoPoolCollection } from '@/modules/pool/remote-resources/use-xo-pool-collection.ts'
|
||||
import type { FrontXoTask } from '@/modules/task/remote-resources/use-xo-task-collection.ts'
|
||||
import { xoTrafficRulesArg } from '@/modules/traffic-rules/jobs/xo-traffic-rule-args.ts'
|
||||
import { orderedTrafficRulesArg } from '@/modules/traffic-rules/jobs/xo-traffic-rule-edit-order-args.ts'
|
||||
import {
|
||||
getTrafficRulePriorityUpdates,
|
||||
isNetworkRuleSupported,
|
||||
} from '@/modules/traffic-rules/utils/xo-traffic-rule.util.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'
|
||||
|
||||
const MIN_RULES_TO_EDIT_ORDER = 2
|
||||
|
||||
export const useXoTrafficRuleEditOrderJob = defineJob(
|
||||
'traffic-rule.edit-order',
|
||||
[xoTrafficRulesArg, orderedTrafficRulesArg, xoNetworkArg],
|
||||
() => {
|
||||
const { monitorTask } = useXoTaskUtils()
|
||||
const { t } = useI18n()
|
||||
const { getPoolById } = useXoPoolCollection()
|
||||
|
||||
return {
|
||||
async run(networkRules: TrafficRule[], orderedRules: TrafficRule[]) {
|
||||
const updates = getTrafficRulePriorityUpdates(orderedRules, networkRules)
|
||||
|
||||
if (updates === undefined) {
|
||||
throw new JobError(t('job:traffic-rule-edit-order:no-free-priority'))
|
||||
}
|
||||
|
||||
for (const { rule, priority } of updates) {
|
||||
const scope = rule.type === 'network' ? 'networks' : 'vifs'
|
||||
const endpoint = `plugins/sdn-controller/${scope}/${rule.sourceId}/actions/update_traffic_rule`
|
||||
|
||||
const { taskId } = await fetchPost<{ taskId: FrontXoTask['id'] }>(endpoint, {
|
||||
oldRule: {
|
||||
allow: rule.allow,
|
||||
direction: rule.direction,
|
||||
ipRange: rule.ipRange,
|
||||
protocol: rule.protocol,
|
||||
port: rule.port !== undefined ? Number(rule.port) : undefined,
|
||||
priority: rule.priority,
|
||||
},
|
||||
newRule: { priority },
|
||||
})
|
||||
|
||||
await monitorTask(taskId)
|
||||
}
|
||||
},
|
||||
|
||||
validate(isRunning, networkRules, _orderedRules, network: FrontXoNetwork | undefined) {
|
||||
if (network === undefined) {
|
||||
throw new JobError(t('job:arg:network-required'))
|
||||
}
|
||||
|
||||
if (isRunning) {
|
||||
throw new JobRunningError(t('job:edit:in-progress'))
|
||||
}
|
||||
|
||||
if (networkRules.length < MIN_RULES_TO_EDIT_ORDER) {
|
||||
throw new JobError(t('job:traffic-rule-edit-order:not-enough-rules', { n: MIN_RULES_TO_EDIT_ORDER }))
|
||||
}
|
||||
|
||||
const pool = getPoolById(network.$pool)
|
||||
|
||||
if (!isNetworkRuleSupported(pool)) {
|
||||
throw new JobError(t('traffic-rules:xapi-plugin-required'))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,204 @@
|
||||
import type { FrontXoNetwork } from '@/modules/network/remote-resources/use-xo-network-collection.ts'
|
||||
import type { useXoPoolCollection } from '@/modules/pool/remote-resources/use-xo-pool-collection.ts'
|
||||
import { useXoTrafficRuleEditOrderJob } from '@/modules/traffic-rules/jobs/xo-traffic-rule-edit-order.job.ts'
|
||||
import type { useXoTaskUtils } from '@/shared/composables/xo-task-utils.composable.ts'
|
||||
import type { fetchPost } from '@/shared/utils/fetch.util.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import { mountComposable } from '@/test/mount-composable.ts'
|
||||
import { SDN_CONTROLLER_OF_METHOD_KEY, type TrafficRule, type XoNetwork } from '@vates/types'
|
||||
|
||||
const { post, monitorTask, pool } = vi.hoisted(() => ({
|
||||
post: vi.fn(),
|
||||
monitorTask: vi.fn(),
|
||||
pool: { otherConfig: {} as Record<string, string> },
|
||||
}))
|
||||
|
||||
vi.mock(import('@/shared/utils/fetch.util.ts'), () => ({
|
||||
fetchPost: post as unknown as typeof fetchPost,
|
||||
}))
|
||||
|
||||
// The task collection would open an SSE subscription
|
||||
vi.mock(import('@/shared/composables/xo-task-utils.composable.ts'), () => ({
|
||||
useXoTaskUtils: (() => ({ monitorTask })) as unknown as typeof useXoTaskUtils,
|
||||
}))
|
||||
|
||||
vi.mock(import('@/modules/pool/remote-resources/use-xo-pool-collection.ts'), () => ({
|
||||
useXoPoolCollection: (() => ({
|
||||
getPoolById: () => pool,
|
||||
})) as unknown as typeof useXoPoolCollection,
|
||||
}))
|
||||
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
post.mockReset()
|
||||
monitorTask.mockReset()
|
||||
|
||||
post.mockImplementation(async (url: string) => ({ taskId: `task-of-${url}` }))
|
||||
monitorTask.mockResolvedValue(undefined)
|
||||
pool.otherConfig = { [SDN_CONTROLLER_OF_METHOD_KEY]: 'xapi-plugin' }
|
||||
})
|
||||
|
||||
const NETWORK_ID = 'network-1' as XoNetwork['id']
|
||||
|
||||
const NETWORK = { id: NETWORK_ID, $pool: 'pool-1' } as FrontXoNetwork
|
||||
|
||||
const ENDPOINT = `plugins/sdn-controller/networks/${NETWORK_ID}/actions/update_traffic_rule`
|
||||
|
||||
function createRule(id: string, priority: number | undefined): TrafficRule {
|
||||
return {
|
||||
id,
|
||||
allow: true,
|
||||
protocol: 'TCP',
|
||||
ipRange: '10.0.0.0/8',
|
||||
direction: 'to',
|
||||
priority,
|
||||
networkId: NETWORK_ID,
|
||||
type: 'network',
|
||||
sourceId: NETWORK_ID,
|
||||
}
|
||||
}
|
||||
|
||||
function mountEditOrderJob(
|
||||
networkRules: TrafficRule[],
|
||||
orderedRules: TrafficRule[] | undefined,
|
||||
network: FrontXoNetwork | undefined = NETWORK
|
||||
) {
|
||||
return mountComposable(() =>
|
||||
useXoTrafficRuleEditOrderJob(
|
||||
() => networkRules,
|
||||
() => orderedRules,
|
||||
() => network
|
||||
)
|
||||
).wrapper.vm
|
||||
}
|
||||
|
||||
function getNewPriorities() {
|
||||
return post.mock.calls.map(([, body]) => body.newRule.priority)
|
||||
}
|
||||
|
||||
describe('run', () => {
|
||||
it('updates nothing when the order is unchanged', async () => {
|
||||
const rules = [createRule('a', 300), createRule('b', 200), createRule('c', 100)]
|
||||
|
||||
await mountEditOrderJob(rules, rules).run()
|
||||
|
||||
expect(post).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('only updates the moved rule, with a priority above the rules it now precedes', async () => {
|
||||
const [firstRule, secondRule, thirdRule] = [createRule('a', 300), createRule('b', 200), createRule('c', 100)]
|
||||
|
||||
await mountEditOrderJob([firstRule, secondRule, thirdRule], [thirdRule, firstRule, secondRule]).run()
|
||||
|
||||
expect(post.mock.calls).toEqual([
|
||||
[ENDPOINT, { oldRule: expect.objectContaining({ priority: 100 }), newRule: { priority: expect.any(Number) } }],
|
||||
])
|
||||
expect(getNewPriorities()[0]).toBeGreaterThan(300)
|
||||
})
|
||||
|
||||
it('never gives a priority a rule of the network already has', async () => {
|
||||
const [firstRule, secondRule, thirdRule, fourthRule] = [
|
||||
createRule('a', 400),
|
||||
createRule('b', 300),
|
||||
createRule('c', 200),
|
||||
createRule('d', 100),
|
||||
]
|
||||
|
||||
await mountEditOrderJob(
|
||||
[firstRule, secondRule, thirdRule, fourthRule],
|
||||
[secondRule, fourthRule, firstRule, thirdRule]
|
||||
).run()
|
||||
|
||||
const newPriorities = getNewPriorities()
|
||||
|
||||
newPriorities.forEach(priority => expect([400, 300, 200, 100]).not.toContain(priority))
|
||||
})
|
||||
|
||||
it('never gives a priority a rule of the network missing from the order already has', async () => {
|
||||
const [firstRule, secondRule, thirdRule, fourthRule] = [
|
||||
createRule('a', 5),
|
||||
createRule('b', 2),
|
||||
createRule('c', 0),
|
||||
createRule('d', 1),
|
||||
]
|
||||
// added on the network while the order was being edited
|
||||
const addedRule = createRule('x', 3)
|
||||
|
||||
await mountEditOrderJob(
|
||||
[firstRule, secondRule, thirdRule, fourthRule, addedRule],
|
||||
[firstRule, thirdRule, secondRule, fourthRule]
|
||||
).run()
|
||||
|
||||
getNewPriorities().forEach(priority => expect(priority).not.toBe(3))
|
||||
})
|
||||
|
||||
it('renumbers all the rules when there is no room between the kept ones', async () => {
|
||||
const [firstRule, secondRule] = [createRule('a', 1), createRule('b', 0)]
|
||||
|
||||
await mountEditOrderJob([firstRule, secondRule], [secondRule, firstRule]).run()
|
||||
|
||||
const [firstPriority, secondPriority] = getNewPriorities()
|
||||
|
||||
expect(post.mock.calls.map(([, body]) => body.oldRule.priority)).toEqual([0, 1])
|
||||
expect(firstPriority).toBeGreaterThan(secondPriority)
|
||||
})
|
||||
|
||||
it('gives a priority to the rules that have none', async () => {
|
||||
const [firstRule, secondRule] = [createRule('a', undefined), createRule('b', undefined)]
|
||||
|
||||
await mountEditOrderJob([firstRule, secondRule], [secondRule, firstRule]).run()
|
||||
|
||||
const [firstPriority, secondPriority] = getNewPriorities()
|
||||
|
||||
expect(firstPriority).toBeGreaterThan(secondPriority)
|
||||
})
|
||||
|
||||
it('applies the updates one at a time and stops at the first failure', async () => {
|
||||
const [firstRule, secondRule] = [createRule('a', 1), createRule('b', 0)]
|
||||
const error = new Error('Update failed')
|
||||
monitorTask.mockRejectedValueOnce(error)
|
||||
|
||||
await expect(mountEditOrderJob([firstRule, secondRule], [secondRule, firstRule]).run()).rejects.toBe(error)
|
||||
|
||||
expect(post).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('validate', () => {
|
||||
it('can run with at least 2 rules on a network using the XAPI plugin', () => {
|
||||
const rules = [createRule('a', 200), createRule('b', 100)]
|
||||
|
||||
expect(mountEditOrderJob(rules, undefined).canRun).toBe(true)
|
||||
})
|
||||
|
||||
it('cannot run without a network', () => {
|
||||
const rules = [createRule('a', 200), createRule('b', 100)]
|
||||
|
||||
const job = mountComposable(() =>
|
||||
useXoTrafficRuleEditOrderJob(
|
||||
() => rules,
|
||||
() => undefined,
|
||||
() => undefined
|
||||
)
|
||||
).wrapper.vm
|
||||
|
||||
expect(job.canRun).toBe(false)
|
||||
expect(job.errorMessage).toBe(t('job:arg:network-required'))
|
||||
})
|
||||
|
||||
it('cannot run with less than 2 rules', () => {
|
||||
const job = mountEditOrderJob([createRule('a', 100)], undefined)
|
||||
|
||||
expect(job.canRun).toBe(false)
|
||||
expect(job.errorMessage).toBe(t('job:traffic-rule-edit-order:not-enough-rules', { n: 2 }))
|
||||
})
|
||||
|
||||
it('cannot run without the XAPI plugin', () => {
|
||||
pool.otherConfig = {}
|
||||
|
||||
const job = mountEditOrderJob([createRule('a', 200), createRule('b', 100)], undefined)
|
||||
|
||||
expect(job.canRun).toBe(false)
|
||||
expect(job.errorMessage).toBe(t('traffic-rules:xapi-plugin-required'))
|
||||
})
|
||||
})
|
||||
@@ -6,6 +6,15 @@ import { SDN_CONTROLLER_OF_METHOD_KEY, type TrafficRule } from '@vates/types'
|
||||
// OpenFlow default, applied to rules stored without a priority
|
||||
const DEFAULT_TRAFFIC_RULE_PRIORITY = 32768
|
||||
|
||||
const MIN_TRAFFIC_RULE_PRIORITY = 0
|
||||
|
||||
const MAX_TRAFFIC_RULE_PRIORITY = 65535
|
||||
|
||||
type TrafficRulePriorityUpdate = {
|
||||
rule: TrafficRule
|
||||
priority: number
|
||||
}
|
||||
|
||||
export function isNetworkRuleSupported(pool: FrontXoPool | undefined): boolean {
|
||||
const ofMethod = pool?.otherConfig[SDN_CONTROLLER_OF_METHOD_KEY]
|
||||
|
||||
@@ -67,6 +76,118 @@ export function rankTrafficRules(
|
||||
})
|
||||
}
|
||||
|
||||
function getKeptRuleIndexes(orderedRules: TrafficRule[]): number[] {
|
||||
const chainLengths: number[] = []
|
||||
const chainPreviousIndexes: (number | undefined)[] = []
|
||||
let longestChainEndIndex: number | undefined
|
||||
|
||||
orderedRules.forEach((rule, index) => {
|
||||
chainLengths[index] = 0
|
||||
chainPreviousIndexes[index] = undefined
|
||||
|
||||
if (rule.priority === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
chainLengths[index] = 1
|
||||
|
||||
for (let previousIndex = 0; previousIndex < index; previousIndex++) {
|
||||
const previousRule = orderedRules[previousIndex]
|
||||
|
||||
if (
|
||||
previousRule.priority !== undefined &&
|
||||
previousRule.priority > rule.priority &&
|
||||
chainLengths[previousIndex] + 1 > chainLengths[index]
|
||||
) {
|
||||
chainLengths[index] = chainLengths[previousIndex] + 1
|
||||
chainPreviousIndexes[index] = previousIndex
|
||||
}
|
||||
}
|
||||
|
||||
if (longestChainEndIndex === undefined || chainLengths[index] > chainLengths[longestChainEndIndex]) {
|
||||
longestChainEndIndex = index
|
||||
}
|
||||
})
|
||||
|
||||
const keptIndexes: number[] = []
|
||||
let index = longestChainEndIndex
|
||||
|
||||
while (index !== undefined) {
|
||||
keptIndexes.unshift(index)
|
||||
index = chainPreviousIndexes[index]
|
||||
}
|
||||
|
||||
return keptIndexes
|
||||
}
|
||||
|
||||
// The other rules get a priority between those of their kept neighbours, spread out to leave room for later moves.
|
||||
// No new priority is one a rule of the network already has, so the updates never conflict with the other rules.
|
||||
function getPriorityUpdatesBetweenKeptRules(
|
||||
orderedRules: TrafficRule[],
|
||||
keptIndexes: number[],
|
||||
takenPriorities: Set<number | undefined>
|
||||
): TrafficRulePriorityUpdate[] | undefined {
|
||||
const boundIndexes = [-1, ...keptIndexes, orderedRules.length]
|
||||
const updates: TrafficRulePriorityUpdate[] = []
|
||||
|
||||
for (let intervalIndex = 0; intervalIndex < boundIndexes.length - 1; intervalIndex++) {
|
||||
const upperIndex = boundIndexes[intervalIndex]
|
||||
const lowerIndex = boundIndexes[intervalIndex + 1]
|
||||
const movedRules = orderedRules.slice(upperIndex + 1, lowerIndex)
|
||||
|
||||
if (movedRules.length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
let upperPriority = MAX_TRAFFIC_RULE_PRIORITY + 1
|
||||
|
||||
if (upperIndex !== -1) {
|
||||
upperPriority = getTrafficRulePriority(orderedRules[upperIndex])
|
||||
}
|
||||
|
||||
let lowerPriority = MIN_TRAFFIC_RULE_PRIORITY - 1
|
||||
|
||||
if (lowerIndex !== orderedRules.length) {
|
||||
lowerPriority = getTrafficRulePriority(orderedRules[lowerIndex])
|
||||
}
|
||||
|
||||
const freePriorities: number[] = []
|
||||
|
||||
for (let priority = upperPriority - 1; priority > lowerPriority; priority--) {
|
||||
if (!takenPriorities.has(priority)) {
|
||||
freePriorities.push(priority)
|
||||
}
|
||||
}
|
||||
|
||||
if (freePriorities.length <= movedRules.length) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const step = freePriorities.length / (movedRules.length + 1)
|
||||
|
||||
movedRules.forEach((rule, index) => {
|
||||
updates.push({ rule, priority: freePriorities[Math.floor((index + 1) * step)] })
|
||||
})
|
||||
}
|
||||
|
||||
return updates
|
||||
}
|
||||
|
||||
export function getTrafficRulePriorityUpdates(
|
||||
orderedRules: TrafficRule[],
|
||||
networkRules: TrafficRule[]
|
||||
): TrafficRulePriorityUpdate[] | undefined {
|
||||
const takenPriorities = new Set(networkRules.map(rule => rule.priority))
|
||||
|
||||
const updates = getPriorityUpdatesBetweenKeptRules(orderedRules, getKeptRuleIndexes(orderedRules), takenPriorities)
|
||||
|
||||
if (updates !== undefined) {
|
||||
return updates
|
||||
}
|
||||
|
||||
return getPriorityUpdatesBetweenKeptRules(orderedRules, [], takenPriorities)
|
||||
}
|
||||
|
||||
export function getOrdinalSuffix(suffixesMessage: string, locale: string, position: number): string | undefined {
|
||||
const suffixes = new Map<string, string>()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user