diff --git a/package-lock.json b/package-lock.json index 6ed8d1dec..a9f3469c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,10 +13,12 @@ "@fortawesome/free-solid-svg-icons": "github:nethesis/Font-Awesome#solid", "@fortawesome/vue-fontawesome": "^3.0.3", "@headlessui/vue": "^1.7.13", + "@nethesis/nethesis-brands-svg-icons": "github:nethesis/Font-Awesome#ns-brands", "@nethesis/nethesis-light-svg-icons": "github:nethesis/Font-Awesome#ns-light", "@nethesis/nethesis-solid-svg-icons": "github:nethesis/Font-Awesome#ns-solid", - "@nethserver/vue-tailwind-lib": "^0.0.81", + "@nethserver/vue-tailwind-lib": "^0.0.86", "@types/lodash": "^4.14.195", + "@vueuse/core": "^10.5.0", "axios": "^1.4.0", "chart.js": "^4.4.0", "chartjs-adapter-date-fns": "^3.0.0", @@ -1232,6 +1234,27 @@ "node": ">=8" } }, + "node_modules/@nethesis/nethesis-brands-svg-icons": { + "version": "6.2.1", + "resolved": "git+ssh://git@github.com/nethesis/Font-Awesome.git#faa2de88183b701fb19fc7d356883ccdb14f7be3", + "hasInstallScript": true, + "license": "UNLICENSED", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.2.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@nethesis/nethesis-brands-svg-icons/node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.2.1.tgz", + "integrity": "sha512-Sz07mnQrTekFWLz5BMjOzHl/+NooTdW8F8kDQxjWwbpOJcnoSg4vUDng8d/WR1wOxM0O+CY9Zw0nR054riNYtQ==", + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, "node_modules/@nethesis/nethesis-light-svg-icons": { "version": "6.2.1", "resolved": "git+ssh://git@github.com/nethesis/Font-Awesome.git#8004a40f286bd9ec7f36439b7f8ab42c59351ecf", @@ -1255,7 +1278,7 @@ }, "node_modules/@nethesis/nethesis-solid-svg-icons": { "version": "6.2.1", - "resolved": "git+ssh://git@github.com/nethesis/Font-Awesome.git#1dae9c8106f38bea00b035b57e127b33177a9ff6", + "resolved": "git+ssh://git@github.com/nethesis/Font-Awesome.git#f629ffc3e6ed0a877e8ae6a835d2efbbf43ee73a", "hasInstallScript": true, "license": "UNLICENSED", "dependencies": { @@ -1275,9 +1298,9 @@ } }, "node_modules/@nethserver/vue-tailwind-lib": { - "version": "0.0.81", - "resolved": "https://registry.npmjs.org/@nethserver/vue-tailwind-lib/-/vue-tailwind-lib-0.0.81.tgz", - "integrity": "sha512-Y7cpGT6N5tKtsAZpxD+1Z2XCKkRPTrX5POGa3d/APbXKvdGSD1CXCpM4G5+nRsv940F/W7mxc2T6pssQIc2HCw==", + "version": "0.0.86", + "resolved": "https://registry.npmjs.org/@nethserver/vue-tailwind-lib/-/vue-tailwind-lib-0.0.86.tgz", + "integrity": "sha512-Unsq7Qo+/PTu/TDv4P1s317+5DzeNnOtlO3ACGjp0eCqlqvA1x83Xfqdnmxp3ojZvDFmHO30MsGSA74PdM16sg==", "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.4.0", "@fortawesome/free-solid-svg-icons": "github:nethesis/Font-Awesome#solid", @@ -1421,9 +1444,9 @@ "dev": true }, "node_modules/@types/web-bluetooth": { - "version": "0.0.17", - "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.17.tgz", - "integrity": "sha512-4p9vcSmxAayx72yn70joFoL44c9MO/0+iVEBIQXe3v2h2SiAsEIo/G5v6ObFWvNKRFjbrVadNf9LqEEZeQPzdA==" + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.18.tgz", + "integrity": "sha512-v/ZHEj9xh82usl8LMR3GarzFY1IrbXJw5L4QfQhokjRV91q+SelFqxQWSep1ucXEZ22+dSTwLFkXeur25sPIbw==" }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.59.6", @@ -1862,23 +1885,23 @@ "dev": true }, "node_modules/@vueuse/core": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.3.0.tgz", - "integrity": "sha512-BEM5yxcFKb5btFjTSAFjTu5jmwoW66fyV9uJIP4wUXXU8aR5Hl44gndaaXp7dC5HSObmgbnR2RN+Un1p68Mf5Q==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.5.0.tgz", + "integrity": "sha512-z/tI2eSvxwLRjOhDm0h/SXAjNm8N5ld6/SC/JQs6o6kpJ6Ya50LnEL8g5hoYu005i28L0zqB5L5yAl8Jl26K3A==", "dependencies": { - "@types/web-bluetooth": "^0.0.17", - "@vueuse/metadata": "10.3.0", - "@vueuse/shared": "10.3.0", - "vue-demi": ">=0.14.5" + "@types/web-bluetooth": "^0.0.18", + "@vueuse/metadata": "10.5.0", + "@vueuse/shared": "10.5.0", + "vue-demi": ">=0.14.6" }, "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/@vueuse/core/node_modules/vue-demi": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.5.tgz", - "integrity": "sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==", + "version": "0.14.6", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz", + "integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==", "hasInstallScript": true, "bin": { "vue-demi-fix": "bin/vue-demi-fix.js", @@ -1901,28 +1924,28 @@ } }, "node_modules/@vueuse/metadata": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.3.0.tgz", - "integrity": "sha512-Ema3YhNOa4swDsV0V7CEY5JXvK19JI/o1szFO1iWxdFg3vhdFtCtSTP26PCvbUpnUtNHBY2wx5y3WDXND5Pvnw==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.5.0.tgz", + "integrity": "sha512-fEbElR+MaIYyCkeM0SzWkdoMtOpIwO72x8WsZHRE7IggiOlILttqttM69AS13nrDxosnDBYdyy3C5mR1LCxHsw==", "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/@vueuse/shared": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.3.0.tgz", - "integrity": "sha512-kGqCTEuFPMK4+fNWy6dUOiYmxGcUbtznMwBZLC1PubidF4VZY05B+Oht7Jh7/6x4VOWGpvu3R37WHi81cKpiqg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.5.0.tgz", + "integrity": "sha512-18iyxbbHYLst9MqU1X1QNdMHIjks6wC7XTVf0KNOv5es/Ms6gjVFCAAWTVP2JStuGqydg3DT+ExpFORUEi9yhg==", "dependencies": { - "vue-demi": ">=0.14.5" + "vue-demi": ">=0.14.6" }, "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/@vueuse/shared/node_modules/vue-demi": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.5.tgz", - "integrity": "sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==", + "version": "0.14.6", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz", + "integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==", "hasInstallScript": true, "bin": { "vue-demi-fix": "bin/vue-demi-fix.js", diff --git a/package.json b/package.json index c785d435d..e34d8b5fe 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,12 @@ "@fortawesome/free-solid-svg-icons": "github:nethesis/Font-Awesome#solid", "@fortawesome/vue-fontawesome": "^3.0.3", "@headlessui/vue": "^1.7.13", + "@nethesis/nethesis-brands-svg-icons": "github:nethesis/Font-Awesome#ns-brands", "@nethesis/nethesis-light-svg-icons": "github:nethesis/Font-Awesome#ns-light", "@nethesis/nethesis-solid-svg-icons": "github:nethesis/Font-Awesome#ns-solid", - "@nethserver/vue-tailwind-lib": "^0.0.81", + "@nethserver/vue-tailwind-lib": "^0.0.86", "@types/lodash": "^4.14.195", + "@vueuse/core": "^10.5.0", "axios": "^1.4.0", "chart.js": "^4.4.0", "chartjs-adapter-date-fns": "^3.0.0", diff --git a/public/i18n/en/translation.json b/public/i18n/en/translation.json index 0541b64e2..b1160bc95 100644 --- a/public/i18n/en/translation.json +++ b/public/i18n/en/translation.json @@ -9,7 +9,12 @@ "edit": "Edit", "delete": "Delete", "filter": "Filter", - "advanced_settings": "Advanced settings" + "advanced_settings": "Advanced settings", + "enable": "Enable", + "disable": "Disable", + "enabled": "Enabled", + "disabled": "Disabled", + "page_under_construction": "Page under construction" }, "error": { "generic_error": "Something went wrong", @@ -94,7 +99,15 @@ "cannot_retrieve_dhcp_interfaces": "Cannot retrieve DHCP interfaces", "cannot_retrieve_storage_configuration": "Cannot retrieve storage configuration", "cannot_configure_storage": "Cannot configure storage", - "cannot_remove_storage": "Cannot remove storage" + "cannot_remove_storage": "Cannot remove storage", + "cannot_retrieve_dpi_rules": "Cannot retrieve DPI rules", + "cannot_create_dpi_rule": "Cannot create DPI rule", + "cannot_edit_dpi_rule": "Cannot edit DPI rule", + "cannot_retrieve_popular_apps": "Cannot retrieve popular apps", + "cannot_retrieve_devices": "Cannot retrieve devices", + "cannot_retrieve_applications": "Cannot retrieve applications", + "zone_already_exists": "Zone already exists", + "cannot_delete_rule": "Cannote delete rule" }, "ne_text_input": { "show_password": "Show password", @@ -411,7 +424,8 @@ "bond_balance_alb": "Adaptive load balancing", "bond_primary_device": "Primary device", "primary_device_not_included": "Primary device must be included in selected devices", - "no_devices_available": "No devices available" + "no_devices_available": "No devices available", + "custom_zone": "Custom" }, "multi_wan": { "title": "MultiWAN", @@ -627,8 +641,7 @@ "limit_log_messages": "Limit log messages", "enable_logging": "Enable logging on this zone", "delete_zone": "Delete \"{0}\"?", - "edit_zone_name": "Edit zone {name}", - "zone_already_exists": "Zone already exists" + "edit_zone_name": "Edit zone {name}" }, "port_forward": { "title": "Port forward", @@ -675,6 +688,38 @@ "security": { "title": "Security" }, + "dpi": { + "title": "DPI filter", + "rules": "Rules", + "exceptions": "Exceptions", + "rules_description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + "no_rules_found": "No rules found", + "create_rule": "Create rule", + "edit_rule": "Edit rule", + "save_rule": "Save rule", + "enable_rule": "Enable rule", + "source": "Source", + "choose_interface": "Choose interface", + "source_tooltip": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + "no_interfaces_available": "No interfaces available", + "apps_and_protocols": "Apps and protocols to block", + "apps_and_protocols_placeholder": "Search {num} apps, protocols and categories", + "no_apps": "No apps or protocols", + "clear_search": "Clear search", + "num_apps_and_protocols_selected": "{num} apps and protocols selected", + "search_results": "Search results", + "no_apps_or_protocols_found": "No apps or protocols found", + "try_changing_search_filter": "Try changing your search filter", + "available_with_subscription": "Available with subscription", + "continue_typing_to_show_other_search_results": "Continue typing your search query to show other search results", + "select_at_least_one_app_or_protocol": "Select at least one app or protocol", + "type_application": "App", + "type_protocol": "Protocol", + "confirm_delete_rule": "You are about to delete DPI rule on interface '{iface}'", + "delete_rule": "Delete rule", + "blocked_apps_and_protocols": "Blocked apps and protocols", + "plus_num_others": "+{num} others" + }, "vpn": { "title": "VPN" }, diff --git a/src/components/standalone/SideMenu.vue b/src/components/standalone/SideMenu.vue index cc067dea2..0e3c3ef83 100644 --- a/src/components/standalone/SideMenu.vue +++ b/src/components/standalone/SideMenu.vue @@ -86,7 +86,17 @@ const navigation: Ref = ref([ } ] }, - { name: t('standalone.security.title'), to: 'security', icon: 'shield-halved' }, + { + name: t('standalone.security.title'), + to: 'security', + icon: 'shield-halved', + children: [ + { + name: t('standalone.dpi.title'), + to: 'security/dpi' + } + ] + }, { name: t('standalone.vpn.title'), to: 'vpn', icon: 'globe' }, { name: t('standalone.logs.title'), to: 'logs', icon: 'list' }, { name: t('standalone.report.title'), to: 'report', icon: 'chart-line' } diff --git a/src/components/standalone/dpi/DeleteDpiRuleModal.vue b/src/components/standalone/dpi/DeleteDpiRuleModal.vue new file mode 100644 index 000000000..1302c8930 --- /dev/null +++ b/src/components/standalone/dpi/DeleteDpiRuleModal.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/src/components/standalone/dpi/DpiRuleCard.vue b/src/components/standalone/dpi/DpiRuleCard.vue new file mode 100644 index 000000000..c85b295d1 --- /dev/null +++ b/src/components/standalone/dpi/DpiRuleCard.vue @@ -0,0 +1,171 @@ + + + + + diff --git a/src/components/standalone/dpi/DpiRules.vue b/src/components/standalone/dpi/DpiRules.vue new file mode 100644 index 000000000..217ef29e0 --- /dev/null +++ b/src/components/standalone/dpi/DpiRules.vue @@ -0,0 +1,152 @@ + + + + + diff --git a/src/components/standalone/dpi/ManageDpiRuleModal.vue b/src/components/standalone/dpi/ManageDpiRuleModal.vue new file mode 100644 index 000000000..d99ad8991 --- /dev/null +++ b/src/components/standalone/dpi/ManageDpiRuleModal.vue @@ -0,0 +1,608 @@ + + + + + diff --git a/src/components/standalone/firewall/CreateOrEditZoneDrawer.vue b/src/components/standalone/firewall/CreateOrEditZoneDrawer.vue index 50169dae1..867480f4f 100644 --- a/src/components/standalone/firewall/CreateOrEditZoneDrawer.vue +++ b/src/components/standalone/firewall/CreateOrEditZoneDrawer.vue @@ -250,7 +250,7 @@ function validate(): boolean { diff --git a/src/lib/fontawesome.ts b/src/lib/fontawesome.ts index 355cc427d..03fdde313 100644 --- a/src/lib/fontawesome.ts +++ b/src/lib/fontawesome.ts @@ -6,7 +6,9 @@ import { faPowerOff, faCircleStop, faHouse as fasHouse, - faUserGear + faUserGear, + faDiagramProject as fasDiagramProject, + faArrowsLeftRight as fasArrowsLeftRight } from '@fortawesome/free-solid-svg-icons' import { faHouse as falHouse } from '@nethesis/nethesis-light-svg-icons' import { faServer as fasServer } from '@fortawesome/free-solid-svg-icons' @@ -68,6 +70,24 @@ import { faCircleChevronDown } from '@fortawesome/free-solid-svg-icons/faCircleC import { faCircleChevronUp } from '@fortawesome/free-solid-svg-icons/faCircleChevronUp' import { faDownLeftAndUpRightToCenter } from '@fortawesome/free-solid-svg-icons/faDownLeftAndUpRightToCenter' import { faUpRightAndDownLeftFromCenter } from '@fortawesome/free-solid-svg-icons/faUpRightAndDownLeftFromCenter' +import { faShapes as fasShapes } from '@fortawesome/free-solid-svg-icons' +import { faTrafficCone as fasTrafficCone } from '@nethesis/nethesis-solid-svg-icons' +import { faStar as fasStar } from '@fortawesome/free-solid-svg-icons' + +import { + faAmazon, + faFacebook, + faFacebookMessenger, + faInstagram, + faPinterest, + faSnapchat, + faTelegram, + faTiktok, + faTwitch, + faVimeo, + faWhatsapp, + faYoutube +} from '@nethesis/nethesis-brands-svg-icons' export async function loadFontAwesome(app: any) { app.component('FontAwesomeIcon', FontAwesomeIcon) @@ -137,4 +157,24 @@ export async function loadFontAwesome(app: any) { library.add(faCircleChevronUp) library.add(faDownLeftAndUpRightToCenter) library.add(faUpRightAndDownLeftFromCenter) + library.add(fasShapes) + library.add(fasDiagramProject) + library.add(fasArrowsLeftRight) + library.add(faFacebook) + library.add(faAmazon) + library.add(faWhatsapp) + library.add(faInstagram) + // library.add(faNetflix) //// + // library.add(faXTwitter) //// + library.add(faTelegram) + library.add(faTiktok) + library.add(faYoutube) + library.add(faFacebookMessenger) + library.add(faVimeo) + library.add(faSnapchat) + library.add(faPinterest) + // library.add(faNordVpn) //// + library.add(faTwitch) + library.add(fasTrafficCone) + library.add(fasStar) } diff --git a/src/lib/standalone/dpi.ts b/src/lib/standalone/dpi.ts new file mode 100644 index 000000000..376dea4b2 --- /dev/null +++ b/src/lib/standalone/dpi.ts @@ -0,0 +1,76 @@ +// Copyright (C) 2023 Nethesis S.r.l. +// SPDX-License-Identifier: GPL-3.0-or-later + +import { upperFirst } from 'lodash' + +export interface DpiAppOrProtocol { + id: number + name: string + type: 'application' | 'protocol' + missing?: boolean + category?: { + id: number + name: string + } +} + +export interface DpiRule { + 'config-name': string + enabled: boolean + device: string + interface: string + criteria: DpiAppOrProtocol[] +} + +export function getHumanizedAppName(appName: string) { + // capitalize, remove 'netify.' prefix and replace dashes with spaces + return upperFirst(appName.replace(/-/g, ' ').replace(/netify./g, '')) +} + +export function getHumanizedCategoryName(categoryName: string) { + // capitalize and replace dashes with spaces + return upperFirst(categoryName.replace(/-/g, ' ')) +} + +export function getAppIcon(app: DpiAppOrProtocol) { + switch (app.name) { + case 'netify.facebook': + return ['fab', 'facebook'] + case 'netify.amazon-prime': + return ['fab', 'amazon'] + case 'netify.whatsapp': + return ['fab', 'whatsapp'] + case 'netify.instagram': + return ['fab', 'instagram'] + // case 'netify.netflix': //// + // return ['fab', ''] + case 'netify.telegram': + return ['fab', 'telegram'] + case 'netify.tiktok': + return ['fab', 'tiktok'] + case 'netify.youtube': + return ['fab', 'youtube'] + case 'netify.facebook-messenger': + return ['fab', 'facebook-messenger'] + // case 'netify.twitter': //// + // return ['fab', ''] + case 'netify.vimeo': + return ['fab', 'vimeo'] + case 'netify.snapchat': + return ['fab', 'snapchat'] + case 'netify.pinterest': + return ['fab', 'pinterest'] + // case 'netify.nordvpn': //// + // return ['fab', ''] + case 'netify.twitch': + return ['fab', 'twitch'] + case 'netify.teamviewer': + return ['fas', 'arrows-left-right'] + default: + if (app.type === 'application') { + return ['fas', 'shapes'] + } else { + return ['fas', 'diagram-project'] + } + } +} diff --git a/src/lib/standalone/network.ts b/src/lib/standalone/network.ts index eb0758b4a..273638706 100644 --- a/src/lib/standalone/network.ts +++ b/src/lib/standalone/network.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later import { SpecialZones, type Forwarding, type Zone } from '@/stores/standalone/useFirewallStore' +import { useI18n } from 'vue-i18n' export function getInterface(deviceOrIface: any, networkConfig: any) { // if deviceOrIface is an interface, just return it as it is @@ -51,6 +52,8 @@ export function getZoneLabel(zoneName: string) { } export function getZoneColor(zoneName: string) { + const { t } = useI18n() + switch (zoneName) { case 'lan': return 'Green' @@ -60,7 +63,8 @@ export function getZoneColor(zoneName: string) { return 'Blue' //// dmz default: - return '' + // custom zone + return t('standalone.interfaces_and_devices.custom_zone') } } @@ -88,7 +92,8 @@ export function getZoneBorderColorClasses(zoneName: string) { case 'openvpnrw': return 'border-teal-700 dark:border-teal-700' default: - return 'border-gray-500 dark:border-gray-500' + // custom zone + return 'border-indigo-700 dark:border-indigo-700' } } @@ -102,7 +107,7 @@ export function getZoneIcon(zoneName: string) { return 'user-group' //// dmz default: - return '' + return 'star' } } diff --git a/src/lib/validation.ts b/src/lib/validation.ts index bda4caf97..68fe62727 100644 --- a/src/lib/validation.ts +++ b/src/lib/validation.ts @@ -292,9 +292,17 @@ export class MessageBag extends Map> { * @param key key of the messageBag, e.g. 'zoneName' * @param prefix string prefix to build the i18n key, e.g. 'standalone.zones_and_policies' */ - getFirstI18nKeyFor(key: string, prefix: string): string { + getFirstI18nKeyFor(key: string): string { if (this.has(key)) { - return `${prefix}.${this.getFirstFor(key)}` + const i18nKey = this.getFirstFor(key) + + if (i18nKey.includes('.')) { + // assume the i18nKey already contains the prefix, e.g. 'error.xyz' + return i18nKey + } else { + // add 'error.' prefix to build the i18n key + return `error.${i18nKey}` + } } return '' } diff --git a/src/router/index.ts b/src/router/index.ts index 985e65e44..2d7179dd0 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -79,6 +79,11 @@ const standaloneRoutes = [ path: 'logs', name: 'Logs', component: () => import('../views/standalone/LogsView.vue') + }, + { + path: 'security/dpi', + name: 'Dpi', + component: () => import('../views/standalone/security/DpiFilterView.vue') } ] diff --git a/src/views/standalone/security/DpiFilterView.vue b/src/views/standalone/security/DpiFilterView.vue new file mode 100644 index 000000000..4d7b77aa4 --- /dev/null +++ b/src/views/standalone/security/DpiFilterView.vue @@ -0,0 +1,66 @@ + + + + +