Rule and Ruleset modals

This commit is contained in:
Alireza Ahmadi
2024-05-12 23:40:23 +02:00
parent cb606828ff
commit 8272285fe5
5 changed files with 941 additions and 12 deletions
+382
View File
@@ -0,0 +1,382 @@
<template>
<v-card style="background-color: inherit;">
<v-row>
<v-col cols="12" v-if="optionInbound">
<v-combobox
v-model="rule.inbound"
:items="inTags"
:label="$t('pages.inbounds')"
multiple
chips
hide-details
></v-combobox>
</v-col>
<v-col cols="12" v-if="optionClient">
<v-combobox
v-model="rule.auth_user"
:items="clients"
:label="$t('pages.clients')"
multiple
chips
hide-details
></v-combobox>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="optionIPver">
<v-select
hide-details
label="IP Version"
:items="[4,6]"
v-model.number="rule.ip_version">
</v-select>
</v-col>
<v-col cols="12" sm="6" v-if="optionProtocol">
<v-combobox
v-model="rule.protocol"
:items="['http','tls', 'quic', 'stun', 'dns']"
label="Protocols"
multiple
chips
hide-details
></v-combobox>
</v-col>
</v-row>
<v-row v-if="optionDomain">
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
:items="domainKeys"
@update:model-value="updateDomainOption($event)"
v-model="domainOption">
</v-select>
</v-col>
<v-col cols="12" sm="6" v-if="rule.domain != undefined">
<v-text-field
label="Domains (comma separated)"
hide-details
v-model="domain"></v-text-field>
</v-col>
<v-col cols="12" sm="6" v-if="rule.domain_suffix != undefined">
<v-text-field
label="Domain Suffixes (comma separated)"
hide-details
v-model="domain_suffix"></v-text-field>
</v-col>
<v-col cols="12" sm="6" v-if="rule.domain_keyword != undefined">
<v-text-field
label="Domain Keywords (comma separated)"
hide-details
v-model="domain_keyword"></v-text-field>
</v-col>
<v-col cols="12" sm="6" v-if="rule.domain_regex != undefined">
<v-text-field
label="Domain Regexes (comma separated)"
hide-details
v-model="domain_regex"></v-text-field>
</v-col>
<v-col cols="12" sm="6" v-if="rule.ip_cidr != undefined">
<v-text-field
label="IP CIDRs (comma separated)"
hide-details
v-model="ip_cidr"></v-text-field>
</v-col>
<v-col cols="12" sm="6" v-if="rule.ip_is_private != undefined">
<v-switch v-model="rule.ip_is_private" color="primary" label="Private IP" hide-details></v-switch>
</v-col>
</v-row>
<v-row v-if="optionPort">
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
:items="portKeys"
@update:model-value="updatePortOption($event)"
v-model="portOption">
</v-select>
</v-col>
<v-col cols="12" sm="6" v-if="rule.port != undefined">
<v-text-field
label="Ports (comma separated)"
hide-details
v-model="port"></v-text-field>
</v-col>
<v-col cols="12" sm="6" v-if="rule.port_range != undefined">
<v-text-field
label="Port Ranges (comma separated)"
hide-details
v-model="port_range"></v-text-field>
</v-col>
</v-row>
<v-row v-if="optionSrcIP">
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
:items="srcIPKeys"
@update:model-value="updateSrcIPOption($event)"
v-model="srcIPOption">
</v-select>
</v-col>
<v-col cols="12" sm="6" v-if="rule.source_ip_cidr != undefined">
<v-text-field
label="Source IP CIDRs (comma separated)"
hide-details
v-model="source_ip_cidr"></v-text-field>
</v-col>
<v-col cols="12" sm="6" v-if="rule.source_ip_is_private != undefined">
<v-switch v-model="rule.source_ip_is_private" color="primary" label="Private Source IP" hide-details></v-switch>
</v-col>
</v-row>
<v-row v-if="optionSrcPort">
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
:items="srcPortKeys"
@update:model-value="updateSrcPortOption($event)"
v-model="srcPortOption">
</v-select>
</v-col>
<v-col cols="12" sm="6" v-if="rule.source_port != undefined">
<v-text-field
label="Source Ports (comma separated)"
hide-details
v-model="source_port"></v-text-field>
</v-col>
<v-col cols="12" sm="6" v-if="rule.source_port_range != undefined">
<v-text-field
label="Source Port Ranges (comma separated)"
hide-details
v-model="source_port_range"></v-text-field>
</v-col>
</v-row>
<v-row v-if="optionRuleSet">
<v-col cols="12" sm="6">
<v-combobox
v-model="rule.rule_set"
:items="rsTags"
label="Rulesets"
multiple
chips
hide-details
></v-combobox>
</v-col>
<v-col cols="12" sm="6">
<v-switch v-model="rule.rule_set_ipcidr_match_source" color="primary" label="Ruleset IPcidr Match Source" hide-details></v-switch>
</v-col>
</v-row>
<v-card-actions>
<v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details variant="tonal">Rule Options</v-btn>
</template>
<v-card>
<v-list>
<v-list-item>
<v-switch v-model="optionInbound" color="primary" :label="$t('pages.inbounds')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionClient" color="primary" :label="$t('pages.clients')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionIPver" color="primary" label="IP Version" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionProtocol" color="primary" label="Protocol" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionDomain" color="primary" label="Domain/IP" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionPort" color="primary" label="Port" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionSrcIP" color="primary" label="Source IP" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionSrcPort" color="primary" label="Source Port" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionRuleSet" color="primary" label="Rule Set" hide-details></v-switch>
</v-list-item>
</v-list>
</v-card>
</v-menu>
</v-card-actions>
</v-card>
</template>
<script lang="ts">
export default {
props: ['rule', 'clients', 'inTags', 'rsTags', 'deleteable'],
data() {
return {
menu: false,
domainKeys: ['domain', 'domain_suffix', 'domain_keyword', 'domain_regex', 'ip_cidr', 'ip_is_private'],
portKeys: ['port', 'port_range'],
srcIPKeys: ['source_ip_cidr', 'source_ip_is_private'],
srcPortKeys: ['source_port', 'source_port_range'],
domainOption: 'domain',
portOption: 'port',
srcIPOption: 'source_ip_cidr',
srcPortOption: 'source_port',
}
},
methods: {
updateDomainOption(option:string) {
this.domainKeys.forEach(k => delete this.$props.rule[k])
this.$props.rule[option] = option == 'ip_is_private' ? false : []
},
updatePortOption(option:string) {
this.portKeys.forEach(k => delete this.$props.rule[k])
this.$props.rule[option] = []
},
updateSrcIPOption(option:string) {
this.srcIPKeys.forEach(k => delete this.$props.rule[k])
this.$props.rule[option] = option == 'source_ip_is_private' ? false : []
},
updateSrcPortOption(option:string) {
this.srcPortKeys.forEach(k => delete this.$props.rule[k])
this.$props.rule[option] = []
},
},
computed: {
optionInbound: {
get() { return this.$props.rule.inbound != undefined },
set(v:boolean) { this.$props.rule.inbound = v ? [] : undefined }
},
optionClient: {
get() { return this.$props.rule.auth_user != undefined },
set(v:boolean) { this.$props.rule.auth_user = v ? [] : undefined }
},
optionIPver: {
get() { return this.$props.rule.ip_version != undefined },
set(v:boolean) { this.$props.rule.ip_version = v ? 4 : undefined }
},
optionProtocol: {
get() { return this.$props.rule.protocol != undefined },
set(v:boolean) { this.$props.rule.protocol = v ? ['http'] : undefined }
},
optionDomain: {
get() { return Object.keys(this.$props.rule).some(r => this.domainKeys.includes(r)) },
set(v:boolean) {
if (v) {
this.$props.rule.domain = []
} else {
this.domainKeys.forEach(k => delete this.$props.rule[k])
}
this.domainOption = 'domain'
}
},
optionPort: {
get() { return Object.keys(this.$props.rule).some(r => this.portKeys.includes(r)) },
set(v:boolean) {
if (v) {
this.$props.rule.port = []
} else {
this.portKeys.forEach(k => delete this.$props.rule[k])
}
this.portOption = 'port'
}
},
optionSrcIP: {
get() { return Object.keys(this.$props.rule).some(r => this.srcIPKeys.includes(r)) },
set(v:boolean) {
if (v) {
this.$props.rule.source_ip_cidr = []
} else {
this.srcIPKeys.forEach(k => delete this.$props.rule[k])
}
this.srcIPOption = 'source_ip_cidr'
}
},
optionSrcPort: {
get() { return Object.keys(this.$props.rule).some(r => this.srcPortKeys.includes(r)) },
set(v:boolean) {
if (v) {
this.$props.rule.source_port = []
} else {
this.srcPortKeys.forEach(k => delete this.$props.rule[k])
}
this.srcPortOption = 'source_port'
}
},
optionRuleSet: {
get() { return this.$props.rule.rule_set != undefined },
set(v:boolean) {
if (v) {
this.$props.rule.rule_set = []
this.$props.rule.rule_set_ipcidr_match_source = false
} else {
delete this.$props.rule.rule_set
delete this.$props.rule.rule_set_ipcidr_match_source
}
}
},
domain: {
get() { return this.$props.rule.domain?.join(',') },
set(v:string) { this.$props.rule.domain = v.length>0 ? v.split(',') : [] }
},
domain_suffix: {
get() { return this.$props.rule.domain_suffix?.join(',') },
set(v:string) { this.$props.rule.domain_suffix = v.length>0 ? v.split(',') : [] }
},
domain_keyword: {
get() { return this.$props.rule.domain_keyword?.join(',') },
set(v:string) { this.$props.rule.domain_keyword = v.length>0 ? v.split(',') : [] }
},
domain_regex: {
get() { return this.$props.rule.domain_regex?.join(',') },
set(v:string) { this.$props.rule.domain_regex = v.length>0 ? v.split(',') : [] }
},
ip_cidr: {
get() { return this.$props.rule.ip_cidr?.join(',') },
set(v:string) { this.$props.rule.ip_cidr = v.length>0 ? v.split(',') : [] }
},
port: {
get() { return this.$props.rule.port?.join(',') },
set(v:string) {
if(!v.endsWith(',')) {
this.$props.rule.port = v.length > 0 ? v.split(',').map(str => parseInt(str, 10)) : []
}
}
},
port_range: {
get() { return this.$props.rule.port_range?.join(',') },
set(v:string) { this.$props.rule.port_range = v.length>0 ? v.split(',') : [] }
},
source_ip_cidr: {
get() { return this.$props.rule.source_ip_cidr?.join(',') },
set(v:string) { this.$props.rule.source_ip_cidr = v.length>0 ? v.split(',') : [] }
},
source_port: {
get() { return this.$props.rule.source_port?.join(',') },
set(v:string) {
if(!v.endsWith(',')) {
this.$props.rule.source_port = v.length > 0 ? v.split(',').map(str => parseInt(str, 10)) : []
}
}
},
source_port_range: {
get() { return this.$props.rule.source_port_range?.join(',') },
set(v:string) { this.$props.rule.source_port_range = v.length>0 ? v.split(',') : [] }
},
},
mounted() {
const ruleKeys = Object.keys(this.$props.rule)
if (this.optionDomain) {
const enabledOption = this.domainKeys.filter(k => ruleKeys.includes(k))
this.domainOption = enabledOption.length>0 ? enabledOption[0] : 'domain'
}
if (this.optionPort) {
const enabledOption = this.portKeys.filter(k => ruleKeys.includes(k))
this.portOption = enabledOption.length>0 ? enabledOption[0] : 'port'
}
if (this.optionSrcIP) {
const enabledOption = this.srcIPKeys.filter(k => ruleKeys.includes(k))
this.srcIPOption = enabledOption.length>0 ? enabledOption[0] : 'source_ip_cidr'
}
if (this.optionSrcPort) {
const enabledOption = this.srcPortKeys.filter(k => ruleKeys.includes(k))
this.srcPortOption = enabledOption.length>0 ? enabledOption[0] : 'source_port'
}
}
}
</script>
+169
View File
@@ -0,0 +1,169 @@
<template>
<v-dialog transition="dialog-bottom-transition" width="800">
<v-card class="rounded-lg">
<v-card-title>
{{ $t('actions.' + title) + " " + $t('objects.rule') }}
</v-card-title>
<v-divider></v-divider>
<v-card-text style="padding: 0 16px;">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" v-model="logical" label="Logical Rule" hide-details></v-switch>
</v-col>
<v-spacer></v-spacer>
<v-col cols="auto" v-if="logical" justify="center" align="center">
<v-btn color="primary" @click="ruleData.rules.push({})" hide-details>{{ $t('actions.add') + " " + $t('objects.rule') }}</v-btn>
</v-col>
</v-row>
<v-card style="background-color: inherit; margin-bottom: 5px;" v-for="(r, index) in ruleData.rules" v-if="ruleData.type == 'logical'">
<v-card-subtitle>{{ $t('objects.rule') + ' ' + (index+1) }}
<v-icon @click="ruleData.rules.splice(index,1)" icon="mdi-delete" v-if="ruleData.rules.length>1" />
</v-card-subtitle>
<v-card-text style="padding: 0;">
<RuleOptions
:rule="r"
:clients="clients"
:inTags="inTags"
:rsTags="rsTags" />
</v-card-text>
</v-card>
<RuleOptions
v-else
:rule="ruleData.rules[0]"
:clients="clients"
:inTags="inTags"
:rsTags="rsTags" />
<v-row>
<v-col cols="12" sm="6" md="4">
<v-combobox
v-model="ruleData.outbound"
:items="outTags"
:label="$t('objects.outbound')"
hide-details
></v-combobox>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="logical">
<v-combobox
v-model="ruleData.mode"
:items="['and', 'or']"
label="Mode"
hide-details
></v-combobox>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" v-model="ruleData.invert" label="Invert" hide-details></v-switch>
</v-col>
</v-row>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="blue-darken-1"
variant="outlined"
@click="closeModal"
>
{{ $t('actions.close') }}
</v-btn>
<v-btn
color="blue-darken-1"
variant="tonal"
:loading="loading"
@click="saveChanges"
>
{{ $t('actions.save') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script lang="ts">
import { logicalRule, rule } from '@/types/rules'
import RuleOptions from '@/components/Rule.vue'
export default {
props: ['visible', 'data', 'index', 'clients', 'inTags', 'outTags', 'rsTags'],
emits: ['close', 'save'],
data() {
return {
title: 'add',
loading: false,
ruleData: <logicalRule>{
type: 'logical',
mode: 'and',
rules: <rule[]>[{}],
invert: false,
outbound: 'direct',
}
}
},
methods: {
updateData() {
if (this.$props.index != -1) {
const newData = JSON.parse(this.$props.data)
if (newData.type) {
this.ruleData = newData
} else {
this.ruleData = <logicalRule>{
type: 'simple',
mode: 'and',
rules: <rule[]>[{...newData}],
invert: newData.invert,
outbound: newData.outbound,
}
}
this.title = 'edit'
}
else {
this.ruleData = <logicalRule>{
type: 'simple',
mode: 'and',
rules: <rule[]>[{}],
invert: false,
outbound: this.$props.outTags[0]?? 'direct',
}
this.title = 'add'
}
},
closeModal() {
this.updateData() // reset
this.$emit('close')
},
saveChanges() {
this.loading = true
if (this.ruleData.type == 'simple'){
this.ruleData.rules[0].outbound = this.ruleData.outbound
this.ruleData.rules[0].invert = this.ruleData.invert
}
this.$emit('save', this.ruleData)
this.loading = false
},
deleteRule(index:number) {
this.ruleData.rules.splice(index,1)
}
},
computed: {
logical: {
get() { return this.ruleData.type == 'logical' },
set(v:boolean) {
if (v) {
this.ruleData.type = 'logical'
this.ruleData.outbound = this.ruleData.rules[0].outbound?? this.$props.outTags[0]
delete this.ruleData.rules[0].outbound
} else {
this.ruleData.type = 'simple'
this.ruleData.rules[0].outbound = this.ruleData.outbound
}
}
}
},
watch: {
visible(newValue) {
if (newValue) {
this.updateData()
}
},
},
components: { RuleOptions }
}
</script>
+133
View File
@@ -0,0 +1,133 @@
<template>
<v-dialog transition="dialog-bottom-transition" width="800">
<v-card class="rounded-lg">
<v-card-title>
{{ $t('actions.' + title) + " Ruleset" }}
</v-card-title>
<v-divider></v-divider>
<v-card-text style="padding: 0 16px;">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
label="Type"
:items="['local', 'remote']"
@update:model-value="updateType($event)"
v-model="rule_set.type">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field v-model="rule_set.tag" :label="$t('in.tag')" hide-details></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
label="Format"
:items="['source', 'binary']"
v-model="rule_set.format">
</v-select>
</v-col>
</v-row>
<v-row v-if="rule_set.type == 'local'">
<v-col cols="12">
<v-text-field v-model="rule_set.path" label="Path" hide-details></v-text-field>
</v-col>
</v-row>
<v-row v-else>
<v-col cols="12">
<v-text-field v-model="rule_set.url" label="URL" hide-details></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
:label="$t('objects.outbound')"
:items="outTags"
clearable
@click:clear="delete rule_set.download_detour"
v-model="rule_set.download_detour">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field v-model.number="update_intervals" :suffix="$t('date.d')" type="number" min="0" label="Update Intervals" hide-details></v-text-field>
</v-col>
</v-row>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="blue-darken-1"
variant="outlined"
@click="closeModal"
>
{{ $t('actions.close') }}
</v-btn>
<v-btn
color="blue-darken-1"
variant="tonal"
:loading="loading"
@click="saveChanges"
>
{{ $t('actions.save') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script lang="ts">
import RandomUtil from '@/plugins/randomUtil'
import { ruleset } from '@/types/rules'
export default {
props: ['visible', 'data', 'index', 'outTags'],
emits: ['close', 'save'],
data() {
return {
title: "add",
loading: false,
rule_set: <ruleset>{},
}
},
methods: {
updateData() {
if (this.$props.index != -1) {
this.title = "edit"
this.rule_set = <ruleset>JSON.parse(this.$props.data)
}
else {
this.title = "add"
this.rule_set = <ruleset>{type: 'local', tag: "rs-" + RandomUtil.randomSeq(3), format: 'binary'}
}
},
updateType(t:string) {
if (t == 'local') {
delete this.rule_set.url
delete this.rule_set.download_detour
delete this.rule_set.update_interval
} else {
delete this.rule_set.path
}
},
closeModal() {
this.$emit('close')
},
saveChanges() {
this.loading = true
this.$emit('save', this.rule_set)
this.loading = false
}
},
computed: {
update_intervals: {
get() { return this.rule_set.update_interval != undefined ? parseInt(this.rule_set.update_interval.replace('d','')) : 0 },
set(v:number) { this.rule_set.update_interval = v>0 ? v + 'd' : undefined }
},
},
watch: {
visible(newValue) {
if (newValue) {
this.updateData()
}
},
},
}
</script>
+47
View File
@@ -0,0 +1,47 @@
export interface logicalRule {
type: 'logical' | 'simple'
mode: 'and' | 'or'
rules: rule[]
invert: boolean
outbound: string
}
export interface rule {
inbound?: string[]
ip_version?: 4 | 6
network?: string[]
auth_user?: string[]
protocol?: string[]
domain?: string[]
domain_suffix?: string[]
domain_keyword?: string[]
domain_regex?: string[]
source_ip_cidr?: string[]
source_ip_is_private?: boolean
ip_cidr?: string[]
ip_is_private?: boolean
source_port?: number[]
source_port_range?: string[]
port?: number[]
port_range?: string[]
process_name?: string[]
process_path?: string[]
package_name?: string[]
user?: string[]
user_id?: number[]
clash_mode?: string
rule_set?: string[]
rule_set_ipcidr_match_source?: boolean
invert?: boolean
outbound?: string
}
export interface ruleset {
type: 'local' | 'remote'
tag: string
format: 'source' | 'binary'
path?: string
url?: string
download_detour?: string
update_interval?: string
}
+210 -12
View File
@@ -1,20 +1,98 @@
<template> <template>
<RuleVue
v-model="ruleModal.visible"
:visible="ruleModal.visible"
:index="ruleModal.index"
:data="ruleModal.data"
:clients="clients"
:inTags="inboundTags"
:outTags="outboundTags"
:rsTags="rulesetTags"
@close="closeRuleModal"
@save="saveRuleModal"
/>
<RulesetVue
v-model="rulesetModal.visible"
:visible="rulesetModal.visible"
:index="rulesetModal.index"
:data="rulesetModal.data"
:outTags="outboundTags"
@close="closeRulesetModal"
@save="saveRulesetModal"
/>
<v-row> <v-row>
<v-col cols="12" sm="4" md="3" lg="2" v-for="(item, index) in <any[]>rules" :key="item.name"> <v-col cols="12" justify="center" align="center">
<v-btn color="primary" @click="showRuleModal(-1)" style="margin: 0 5px;">{{ $t('actions.add') + " " + $t('objects.rule') }}</v-btn>
<v-btn color="primary" @click="showRulesetModal(-1)" style="margin: 0 5px;">{{ $t('actions.add') + " Ruleset" }}</v-btn>
</v-col>
</v-row>
<v-row>
<v-col cols="12">Rulesets</v-col>
<v-col cols="12" sm="4" md="3" lg="2" v-for="(item, index) in <any[]>rulesets" :key="item.tag">
<v-card rounded="xl" elevation="5" min-width="200" :title="index"> <v-card rounded="xl" elevation="5" min-width="200" :title="index">
<v-card-subtitle style="margin-top: -20px;">
<v-row>
<v-col>{{ item.type }}</v-col>
</v-row>
</v-card-subtitle>
<v-card-text> <v-card-text>
<v-row> <v-row>
<v-col>Type</v-col> <v-col>{{ $t('in.tag') }}</v-col>
<v-col dir="ltr"> <v-col dir="ltr">
{{ item.type }} {{ item.tag }}
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col>Mode</v-col> <v-col>Format</v-col>
<v-col dir="ltr"> <v-col dir="ltr">
{{ item.mode }} {{ item.format }}
</v-col> </v-col>
</v-row> </v-row>
<v-row>
<v-col>Update</v-col>
<v-col dir="ltr">
{{ item.update_interval?? '-' }}
</v-col>
</v-row>
</v-card-text>
<v-divider></v-divider>
<v-card-actions style="padding: 0;">
<v-btn icon="mdi-file-edit" @click="showRulesetModal(index)">
<v-icon />
<v-tooltip activator="parent" location="top" :text="$t('actions.edit')"></v-tooltip>
</v-btn>
<v-btn icon="mdi-file-remove" style="margin-inline-start:0;" color="warning" @click="delRulesetOverlay[index] = true">
<v-icon />
<v-tooltip activator="parent" location="top" :text="$t('actions.del')"></v-tooltip>
</v-btn>
<v-overlay
v-model="delRulesetOverlay[index]"
contained
class="align-center justify-center"
>
<v-card :title="$t('actions.del')" rounded="lg">
<v-divider></v-divider>
<v-card-text>{{ $t('confirm') }}</v-card-text>
<v-card-actions>
<v-btn color="error" variant="outlined" @click="delRuleset(index)">{{ $t('yes') }}</v-btn>
<v-btn color="success" variant="outlined" @click="delRulesetOverlay[index] = false">{{ $t('no') }}</v-btn>
</v-card-actions>
</v-card>
</v-overlay>
</v-card-actions>
</v-card>
</v-col>
</v-row>
<v-row>
<v-col cols="12">{{ $t('pages.rules') }}</v-col>
<v-col cols="12" sm="4" md="3" lg="2" v-for="(item, index) in <any[]>rules">
<v-card rounded="xl" elevation="5" min-width="200" :title="index">
<v-card-subtitle style="margin-top: -20px;">
<v-row>
<v-col>{{ item.type != undefined ? 'Logical (' + item.mode + ')' : 'Simple' }}</v-col>
</v-row>
</v-card-subtitle>
<v-card-text>
<v-row> <v-row>
<v-col>{{ $t('objects.outbound') }}</v-col> <v-col>{{ $t('objects.outbound') }}</v-col>
<v-col dir="ltr"> <v-col dir="ltr">
@@ -24,16 +102,41 @@
<v-row> <v-row>
<v-col>{{ $t('pages.rules') }}</v-col> <v-col>{{ $t('pages.rules') }}</v-col>
<v-col dir="ltr"> <v-col dir="ltr">
{{ item.rules ? item.rules.length : 0 }} {{ item.rules ? item.rules.length : Object.keys(item).filter(r => !["rule_set_ipcidr_match_source","invert","outbound"].includes(r)).length }}
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col>Invert</v-col> <v-col>Invert</v-col>
<v-col dir="ltr"> <v-col dir="ltr">
{{ item.invert }} {{ $t( (item.invert?? false)? 'yes' : 'no') }}
</v-col> </v-col>
</v-row> </v-row>
</v-card-text> </v-card-text>
<v-divider></v-divider>
<v-card-actions style="padding: 0;">
<v-btn icon="mdi-file-edit" @click="showRuleModal(index)">
<v-icon />
<v-tooltip activator="parent" location="top" :text="$t('actions.edit')"></v-tooltip>
</v-btn>
<v-btn icon="mdi-file-remove" style="margin-inline-start:0;" color="warning" @click="delRuleOverlay[index] = true">
<v-icon />
<v-tooltip activator="parent" location="top" :text="$t('actions.del')"></v-tooltip>
</v-btn>
<v-overlay
v-model="delRuleOverlay[index]"
contained
class="align-center justify-center"
>
<v-card :title="$t('actions.del')" rounded="lg">
<v-divider></v-divider>
<v-card-text>{{ $t('confirm') }}</v-card-text>
<v-card-actions>
<v-btn color="error" variant="outlined" @click="delRule(index)">{{ $t('yes') }}</v-btn>
<v-btn color="success" variant="outlined" @click="delRuleOverlay[index] = false">{{ $t('no') }}</v-btn>
</v-card-actions>
</v-card>
</v-overlay>
</v-card-actions>
</v-card> </v-card>
</v-col> </v-col>
</v-row> </v-row>
@@ -42,14 +145,21 @@
<script lang="ts" setup> <script lang="ts" setup>
import Data from '@/store/modules/data' import Data from '@/store/modules/data'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import RuleVue from '@/layouts/modals/Rule.vue'
import RulesetVue from '@/layouts/modals/Ruleset.vue'
import { Config } from '@/types/config'
import { logicalRule, ruleset } from '@/types/rules'
const appConfig = Data().config const appConfig = computed((): Config => {
return <Config> Data().config
})
const clients = computed((): string[] => {
return Data().clients.map((c:any) => c.name)
})
const route = computed((): any => { const route = computed((): any => {
if (!appConfig || !('route' in appConfig)) { return appConfig.value.route
return []
}
return appConfig.route
}) })
const rules = computed((): any[] => { const rules = computed((): any[] => {
@@ -59,4 +169,92 @@ const rules = computed((): any[] => {
} }
return data.rules return data.rules
}) })
const rulesets = computed((): any[] => {
const data = route.value
if (!route || !('rule_set' in data) || !Array.isArray(data.rule_set)){
return []
}
return data.rule_set
})
const rulesetTags = computed((): any[] => {
return rulesets.value.map((rs:any) => rs.tag)
})
const outboundTags = computed((): string[] => {
return appConfig.value.outbounds.map((o:any) => o.tag)
})
const inboundTags = computed((): string[] => {
return appConfig.value.inbounds.map((i:any) => i.tag)
})
let delRuleOverlay = ref(new Array<boolean>)
let delRulesetOverlay = ref(new Array<boolean>)
const ruleModal = ref({
visible: false,
index: -1,
data: "",
})
const showRuleModal = (index: number) => {
ruleModal.value.index = index
ruleModal.value.data = index == -1 ? '' : JSON.stringify(rules.value[index])
ruleModal.value.visible = true
}
const closeRuleModal = () => {
ruleModal.value.visible = false
}
const saveRuleModal = (data:logicalRule) => {
// Logical or simple
const ruleData = data.type == 'logical' ? data : data.rules[0]
// New or Edit
if (ruleModal.value.index == -1) {
rules.value.push(ruleData)
} else {
rules.value[ruleModal.value.index] = ruleData
}
ruleModal.value.visible = false
}
const delRule = (index: number) => {
rules.value.splice(index,1)
delRuleOverlay.value[index] = false
}
const rulesetModal = ref({
visible: false,
index: -1,
data: "",
})
const showRulesetModal = (index: number) => {
rulesetModal.value.index = index
rulesetModal.value.data = index == -1 ? '' : JSON.stringify(rulesets.value[index])
rulesetModal.value.visible = true
}
const closeRulesetModal = () => {
rulesetModal.value.visible = false
}
const saveRulesetModal = (data:ruleset) => {
// New or Edit
if (rulesetModal.value.index == -1) {
rulesets.value.push(data)
} else {
rulesets.value[rulesetModal.value.index] = data
}
rulesetModal.value.visible = false
}
const delRuleset = (index: number) => {
rulesets.value.splice(index,1)
delRulesetOverlay.value[index] = false
}
</script> </script>