WIP: sieve filters

This commit is contained in:
Antoine Nguyen
2024-06-22 19:44:45 +02:00
committed by Spitfireap
parent 4020b2fe5c
commit d2ef09b7f2
70 changed files with 8939 additions and 27 deletions
+21
View File
@@ -50,4 +50,25 @@ export default {
setARMessage(data) {
return repository.put(`${resource}/armessage/`, data)
},
getFilterSets() {
return repository.get(`${resource}/filtersets/`)
},
createFilterSet(data) {
return repository.post(`${resource}/filtersets/`, data)
},
getFilterConditionTemplates() {
return repository.get(`${resource}/filtersets/condition_templates/`)
},
getFilterActionTemplates() {
return repository.get(`${resource}/filtersets/action_templates/`)
},
getFilters(filterSetName) {
return repository.get(`${resource}/filtersets/${filterSetName}/filters/`)
},
createFilter(filterSetName, data) {
return repository.post(
`${resource}/filtersets/${filterSetName}/filters/`,
data
)
},
}
@@ -0,0 +1,275 @@
<template>
<v-card
:title="title"
>
<v-card-text>
<v-form ref="formRef">
<v-text-field
v-model="form.name"
:label="$gettext('Name')"
variant="outlined"
density="compact"
:rules="[rules.required]"
/>
<div>
<h4>{{ $gettext('Conditions') }}</h4>
<v-radio-group
v-model="form.match_type"
color="primary"
inline
>
<v-radio
:label="$gettext('All of the following')"
value="allof"
/>
<v-radio
:label="$gettext('Any of the following')"
value="anyof"
/>
<v-radio
:label="$gettext('All messages')"
value="all"
/>
</v-radio-group>
<div v-for="(condition, index) in form.conditions" class="d-flex align-center">
<v-select
v-model="condition.name"
:label="$gettext('Choose a header')"
:items="conditionTemplates"
item-title="label"
item-value="name"
variant="outlined"
density="compact"
:rules="[rules.required]"
/>
<v-select
v-model="condition.operator"
:label="$gettext('Choose an operator')"
:items="getConditionOperators(condition.name)"
item-title="label"
item-value="name"
variant="outlined"
class="ml-2"
density="compact"
:rules="[rules.required]"
/>
<v-text-field
v-model="condition.value"
:label="$gettext('Value')"
variant="outlined"
class="ml-2"
density="compact"
:type="getConditionType(index)"
:rules="[rules.required]"
/>
<v-btn
v-if="index === 0"
icon="mdi-plus"
variant="text"
:title="$gettext('Add condition')"
color="primary"
@click="addCondition"
/>
<v-btn
v-else
icon="mdi-trash-can"
variant="text"
:title="$gettext('Remove condition')"
color="error"
@click="removeCondition(index)"
/>
</div>
</div>
<div>
<h4>{{ $gettext('Actions') }}</h4>
<div v-for="(action, index) in form.actions" class="d-flex align-start mt-2">
<v-select
v-model="action.name"
:label="$gettext('Choose an action')"
:items="actionTemplates"
item-title="label"
item-value="name"
variant="outlined"
density="compact"
class="flex-grow-0"
/>
<div class="mx-2 d-flex flex-column flex-grow-1">
<template v-for="arg in getActionArguments(action.name)">
<v-text-field
v-if="arg.type === 'string' || arg.type === 'list'"
v-model="action.args[arg.name]"
density="compact"
variant="outlined"
:rules="[rules.required]"
/>
<v-checkbox
v-if="arg.type === 'boolean'"
v-model="action.args[arg.name]"
:label="arg.label"
/>
</template>
</div>
<v-btn
v-if="index === 0"
icon="mdi-plus"
variant="text"
:title="$gettext('Add action')"
color="primary"
@click="addAction"
/>
<v-btn
v-else
icon="mdi-trash-can"
variant="text"
:title="$gettext('Remove action')"
color="error"
@click="removeAction(index)"
/>
</div>
</div>
</v-form>
</v-card-text>
<v-divider></v-divider>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
:text="$gettext('Cancel')"
variant="plain"
@click="close"
></v-btn>
<v-btn
color="primary"
:text="submitLabel"
@click="submit"
></v-btn>
</v-card-actions>
</v-card>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import { useGettext } from 'vue3-gettext'
import accountApi from '@/api/account'
import rules from '@/plugins/rules'
const props = defineProps({
filterSet: {
type: String,
default: null
},
filter: {
type: Object,
required: false,
default: null
}
})
const emit = defineEmits(['close'])
const { $gettext } = useGettext()
const conditionTemplates = ref([])
const actionTemplates = ref([])
const formRef = ref()
const form = ref(getInitialFormContent())
const title = computed(() => {
return (props.filter) ? $gettext('Edit filter') : $gettext('New filter')
})
const submitLabel = computed(() => {
return (props.filter) ? $gettext('Update') : $gettext('Create')
})
watch(() => props.filter, (value) => {
if (value) {
form.value = JSON.parse(JSON.stringify(props.filter))
} else {
form.value = getInitialFormContent()
}
}, { immediate: true })
function close() {
emit('close')
form.value = getInitialFormContent()
}
function getInitialFormContent() {
return {
match_type: 'anyof',
conditions: [{}],
actions: [{
args: {}
}]
}
}
function getConditionOperators(condition) {
for (const template of conditionTemplates.value) {
if (template.name === condition) {
return template.operators
}
}
return []
}
function getConditionType(index) {
for (const template of conditionTemplates.value) {
if (template.name === form.value.conditions[index].name) {
for (const operator of template.operators) {
if (operator.name === form.value.conditions[index].operator) {
return (operator.type === 'string') ? 'text' : 'number'
}
}
}
}
}
function getActionArguments(action) {
for (const template of actionTemplates.value) {
if (template.name === action) {
return template.args
}
}
return []
}
function addCondition() {
form.value.conditions.push({
name: conditionTemplates.value[0].name,
operator: conditionTemplates.value[0].operators[0]
})
}
function removeCondition(index) {
form.value.conditions.splice(index, 1)
}
function addAction() {
form.value.actions.push({
name: actionTemplates.value[0].name,
args: {}
})
}
function removeAction(index) {
form.value.actions.splice(index, 1)
}
async function submit() {
const { valid } = await formRef.value.validate()
if (!valid) {
return
}
accountApi.createFilter(props.filterSet, form.value).then(() => {
})
}
accountApi.getFilterConditionTemplates().then(resp => {
conditionTemplates.value = resp.data
form.value.conditions[0].name = conditionTemplates.value[0].name
form.value.conditions[0].operator = conditionTemplates.value[0].operators[0]
})
accountApi.getFilterActionTemplates().then(resp => {
actionTemplates.value = resp.data
form.value.actions[0].name = actionTemplates.value[0].name
})
</script>
@@ -0,0 +1,62 @@
<template>
<v-card
:title="$gettext('Create a new filter set')"
>
<v-card-text>
<v-form ref="formRef">
<v-text-field
v-model="form.name"
:label="$gettext('Name')"
variant="outlined"
:rules="[rules.required]"
/>
<v-switch
:label="$gettext('Active')"
v-model="form.active"
color="primary"
/>
</v-form>
</v-card-text>
<v-divider></v-divider>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
:text="$gettext('Cancel')"
variant="plain"
@click="close"
></v-btn>
<v-btn
color="primary"
:text="$gettext('Create')"
@click="submit"
></v-btn>
</v-card-actions>
</v-card>
</template>
<script setup>
import { ref } from 'vue'
import rules from '@/plugins/rules'
import accountApi from '@/api/account'
const emit = defineEmits(['close'])
const form = ref({})
const formRef = ref()
function close() {
formRef.value.reset()
form.value = {}
emit('close')
}
async function submit() {
const { valid } = await formRef.value.validate()
if (!valid) {
return
}
accountApi.createFilterSet(form.value).then(() => {
close()
})
}
</script>
@@ -257,6 +257,11 @@ const mainMenuItems = [
]
const userSettingsMenuItems = [
{
text: $gettext('Filters'),
to: { name: 'AccountFilters' },
icon: 'mdi-filter'
},
{
text: $gettext('Settings'),
to: { name: 'AccountSettings' },
+16 -10
View File
@@ -256,6 +256,15 @@ const routes = [
requiresAuth: true,
},
children: [
{
path: 'filters',
name: 'AccountFilters',
component: () => import('@/views/account/FiltersView.vue'),
meta: {
layout: 'account',
requiresAuth: true,
},
},
{
path: ':tab?',
name: 'AccountSettings',
@@ -283,18 +292,15 @@ router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth !== undefined) {
const authStore = useAuthStore()
authStore.initialize().then(() => {
if (!authStore.isAuthenticated) {
next({ name: 'Login' })
} else {
/*
authStore.validateAccess()
/*
if (to.meta.allowedRoles !== undefined) {
if (to.meta.allowedRoles.indexOf(authStore.authUser.role) === -1) {
next({ name: 'Dashboard' })
return
}
if (to.meta.allowedRoles.indexOf(authStore.authUser.role) === -1) {
next({ name: 'Dashboard' })
return
}
}*/
next()
}
next()
})
} else {
next()
+33 -15
View File
@@ -3,15 +3,28 @@ import Cookies from 'js-cookie'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import gettext from '@/plugins/gettext'
import { UserManager } from 'oidc-client-ts'
import repository from '@/api/repository'
import accountApi from '@/api/account'
import accountsApi from '@/api/accounts'
import authApi from '@/api/auth'
// import authApi from '@/api/auth'
export const useAuthStore = defineStore('auth', () => {
const authUser = ref({})
const isAuthenticated = ref(false)
const manager = new UserManager({
authority: '/api/o',
client_id: 'LVQbfIIX3khWR3nDvix1u9yEGHZUxcx53bhJ7FlD',
// Is client_secret necessary?
client_secret:
'fXzG7tq23eXrM7RK41pXHRseuRWnxe4gV0p4A4YXJlF9jva7QfsDft1g7ESYU9AXR35QE0lo2Ti696OXsqb9JHLjtrPxaYmgMSRmRwLCce5hVr3FB2QhfSOThFYhcuuR',
metadata: {
authorization_endpoint: '/api/o/authorize/',
token_endpoint: '/api/o/token/',
},
scope: 'read write',
})
const userHasMailbox = computed(() => {
return authUser.value.mailbox !== null
@@ -26,31 +39,35 @@ export const useAuthStore = defineStore('auth', () => {
}
async function initialize() {
const token = Cookies.get('token')
if (!token) {
if (isAuthenticated.value) {
return
}
repository.defaults.headers.common.Authorization = `Bearer ${token}`
const user = await manager.getUser()
if (!user) {
return
}
repository.defaults.headers.common.Authorization = `Bearer ${user.access_token}`
repository.defaults.headers.post['Content-Type'] = 'application/json'
return fetchUser()
}
async function login(payload) {
const resp = await authApi.requestToken(payload)
const cookiesAttributes = { sameSite: 'strict' }
if (payload.rememberMe) {
cookiesAttributes.expires = 90
async function validateAccess() {
const user = await manager.getUser()
if (!user || user.expired) {
manager.signinRedirect()
}
}
const cookie = Cookies.withAttributes(cookiesAttributes)
cookie.set('token', resp.data.access)
cookie.set('refreshToken', resp.data.refresh)
async function login(payload) {
await manager.signinResourceOwnerCredentials(payload)
return initialize()
// const cookiesAttributes = { sameSite: 'strict' }
// if (payload.rememberMe) {
// cookiesAttributes.expires = 90
// }
}
async function $reset() {
delete repository.defaults.headers.common.Authorization
Cookies.remove('token')
Cookies.remove('refreshToken')
authUser.value = {}
isAuthenticated.value = false
}
@@ -85,12 +102,13 @@ export const useAuthStore = defineStore('auth', () => {
return {
authUser,
isAuthenticated,
userHasMailbox,
validateAccess,
fetchUser,
initialize,
login,
$reset,
updateAccount,
finalizeTFASetup,
userHasMailbox,
}
})
+165
View File
@@ -0,0 +1,165 @@
<template>
<v-toolbar flat>
<v-toolbar-title>{{ $gettext('Message filters') }}</v-toolbar-title>
</v-toolbar>
<div class="d-flex bg-white align-center pa-4">
<v-autocomplete
v-model="currentFilterSet"
:label="$gettext('Select a filter set')"
:items="filterSets"
item-title="name"
return-object
variant="outlined"
hide-details
class="flex-grow-0"
style="width: 200px"
/>
<v-btn
color="primary"
icon="mdi-plus"
size="small"
class="ml-4"
@click="showCreationForm = true"
:title="$gettext('Add filter set')"
/>
</div>
<div
v-if="currentFilterSet"
class="bg-white mt-4 pa-2"
>
<div class="d-flex align-center">
<v-spacer />
<v-btn
color="primary"
:title="$gettext('Add new filter')"
@click="showFilterForm = true"
>
<v-icon icon="mdi-plus" />
{{ $gettext('Add') }}
</v-btn>
</div>
<v-data-table
:headers="headers"
:items="filters"
>
<template #[`item.enabled`]="{ item }">
{{ $yesno(item.enabled) }}
</template>
<template #[`item.filter_actions`]="{ item }">
<v-menu offset-y>
<template #activator="{ props }">
<v-btn
icon="mdi-dots-horizontal"
variant="text"
v-bind="props"
>
</v-btn>
</template>
<MenuItems :items="filterActions" :obj="item" />
</v-menu>
</template>
</v-data-table>
</div>
<v-dialog
v-model="showFilterSetForm"
max-width="600"
>
<FilterSetForm
@close="closeFilterSetForm"
/>
</v-dialog>
<v-dialog
v-model="showFilterForm"
max-width="800"
>
<FilterForm
v-if="currentFilterSet"
:filter-set="currentFilterSet.name"
:filter="selectedFilter"
@close="closeFilterForm"
/>
</v-dialog>
</template>
<script setup>
import { ref, watch } from 'vue'
import { useGettext } from 'vue3-gettext'
import accountApi from '@/api/account'
import FilterSetForm from '@/components/account/FilterSetForm.vue'
import FilterForm from '@/components/account/FilterForm.vue'
import MenuItems from '@/components/tools/MenuItems.vue'
const { $gettext } = useGettext()
const filterSets = ref([])
const filters = ref([])
const currentFilterSet = ref()
const selectedFilter = ref(null)
const showFilterSetForm = ref(false)
const showFilterForm = ref(false)
const headers = [
{ title: $gettext('Name'), key: 'name' },
{ title: $gettext('Active'), key: 'enabled' },
{ title: '', key: 'filter_actions', align: 'end' }
]
watch(currentFilterSet, (value) => {
accountApi.getFilters(value.name).then(resp => {
filters.value = resp.data
})
})
function closeFilterSetForm() {
showFilterSetForm.value = false
fetchFilterSets()
}
function closeFilterForm() {
showFilterForm.value = false
selectedFilter.value = null
}
function fetchFilterSets() {
accountApi.getFilterSets().then(resp => {
filterSets.value = resp.data
})
}
function editFilter(filter) {
selectedFilter.value = filter
showFilterForm.value = true
}
function deleteFilter(filter) {
}
const filterActions = [
{
label: $gettext('Edit'),
icon: 'mdi-circle-edit-outline',
onClick: editFilter
},
{
label: $gettext('Delete'),
icon: 'mdi-delete-outline',
onClick: deleteFilter,
color: 'red'
}
]
fetchFilterSets()
</script>
<style scoped>
.v-toolbar {
background-color: #f7f8fa !important;
}
.v-tabs {
background-color: #f7f8fa !important;
}
</style>