Dynamic filter options, Vuex store to pass them, improved code structure

This commit is contained in:
Erki Aas 2022-11-07 14:57:50 +02:00
parent ad2daab97d
commit 4fb4c9670d
10 changed files with 238 additions and 109 deletions

View File

@ -19,6 +19,7 @@
"single-spa-vue": "^2.5.1",
"systemjs-webpack-interop": "^2.3.7",
"vue": "^3.2.39",
"vuex": "next",
"vue-select": "beta",
"vuetify": "^3.0.0-beta.0",
"webfontloader": "^1.0.0"

View File

@ -1,8 +1,8 @@
<template>
<v-select
v-model="filter"
v-model="filterValue"
:options="options"
class="ag-custom-component-popup"
@open="updateOptions"
></v-select>
</template>
@ -15,19 +15,31 @@ export default {
vSelect
},
props: {
options: {
field: {
},
changeValue: {
},
filter: {
}
},
data() {
return {
filter: null,
options: []
}
},
watch: {
filter(value) {
this.changeValue(value)
computed: {
filterValue: {
get() {
return this.filter
},
set(newValue) {
this.changeValue(newValue)
}
}
},
methods: {
updateOptions() {
this.options = this.$store.state.filterOptions[this.field] ?? []
}
}
}

View File

@ -7,7 +7,8 @@ export default {
Combobox
},
template: `<Combobox
:options="params.options"
:field="params.field"
:filter="filter"
:change-value="updateFilter"
/>`,
data: function () {

View File

@ -0,0 +1,60 @@
import ComboboxFilter from "../../Filter/ComboboxFilter";
const config = {
defaultColDef: {
width: 120,
initialPinned: true,
resizable: true,
enableCellChangeFlash: true
},
columnDefs: [
{
field: '@timestamp',
width: 70,
sortable: true
},
{
field: 'kubernetes.namespace',
headerName: 'namespace',
tooltipValueGetter: (params) => params.value,
filter: ComboboxFilter,
filterParams: {
options: [],
field: 'kubernetes.namespace',
parentColumn: null,
}
},
{
field: 'kubernetes.pod.name',
headerName: 'pod',
tooltipValueGetter: (params) => params.value,
filter: ComboboxFilter,
filterParams: {
options: [],
field: 'kubernetes.pod.name',
parentColumn: 'kubernetes.namespace',
}
},
{
field: 'kubernetes.container.name',
headerName: 'container',
tooltipValueGetter: (params) => params.value,
filter: ComboboxFilter,
filterParams: {
options: [],
field: 'kubernetes.container.name',
parentColumn: 'kubernetes.pod.name',
}
},
{
field: 'message',
tooltipValueGetter: (params) => params.value,
width: 500,
},
{
field: 'stream',
},
],
}
export default config

View File

@ -24,6 +24,10 @@ import "ag-grid-community/styles//ag-grid.css";
import "ag-grid-community/styles//ag-theme-material.css";
import ExamineLogModal from "./Modal/ExamineLogModal.vue";
import ComboboxFilter from "./Filter/ComboboxFilter.js";
import flattenObj from "../helpers/flattenObj";
import parseEventData from "../helpers/parseEventData";
import { mapActions } from 'vuex';
import config from "./Grid/Main/config";
export default {
components: {
@ -34,103 +38,123 @@ export default {
data() {
return {
examineLogContent: null,
...config,
gridApi: null,
gridColumnApi: null,
defaultColDef: {
width: 50,
initialPinned: true,
resizable: true,
enableCellChangeFlash: true
},
currentRowCount: 0,
comboBoxOptions: {},
viewRowCount: 20,
es: null,
}
},
computed: {
columnDefs() {
return [
{
field: '@timestamp',
width: 70,
sortable: true
},
{
field: 'kubernetes.namespace',
headerName: 'namespace',
tooltipValueGetter: (params) => params.value,
filter: ComboboxFilter,
filterParams: {
options: this.comboBoxOptions['kubernetes.namespace'],
field: 'kubernetes.namespace',
}
},
{
field: 'kubernetes.pod.name',
headerName: 'pod',
tooltipValueGetter: (params) => params.value,
filter: ComboboxFilter,
filterParams: {
options: this.comboBoxOptions['kubernetes.pod.name'],
field: 'kubernetes.pod.name',
}
},
{
field: 'kubernetes.container.name',
headerName: 'container',
tooltipValueGetter: (params) => params.value,
filter: ComboboxFilter,
filterParams: {
options: this.comboBoxOptions['kubernetes.container.name'],
field: 'kubernetes.container.name',
}
},
{
field: 'message',
tooltipValueGetter: (params) => params.value,
width: 500,
},
{
field: 'stream',
},
];
}
filterQuery() {
return this.$store.state.filterQuery
},
},
watch: {
filterQuery() {
this.setupStream()
},
},
created() {
this.setupStream()
// TODO: monitor actual URL
this.setFilterQuery([])
},
methods: {
...mapActions({
setFilterOptions: 'setFilterOptions',
setFilterQuery: 'setFilterQuery',
}),
setupStream() {
let es = new EventSource('/events');
this.es && this.es.close();
let url = new URL('/events', window.location.href);
this.filterQuery.map((e) => {
url.searchParams.append(e.key, e.value);
})
let es = new EventSource(url.toString());
es.onmessage = (e) => this.handleReceiveMessage(e)
es.addEventListener("filters", (e) => this.handleReceiveFilters(e))
this.es = es
},
onGridReady(params) {
this.gridApi = params.api;
this.gridColumnApi = params.columnApi;
this.gridColumnApi.applyColumnState({
state: [{
colId: '@timestamp',
sort: 'desc'
}]
});
this.gridApi.addGlobalListener((type, event) => {
if (type === 'filterChanged') {
let changedColumn = event.columns[0] ? (event.columns[0].colId) : null
let query = []
let gridColumns = event.columnApi.columnModel.gridColumns
gridColumns.map((column) => {
// Reset child column filter if parent changed
let parentColumn = column?.colDef?.filterParams?.parentColumn
if (parentColumn && changedColumn === parentColumn) {
let filterInstance = this.gridApi.getFilterInstance(column.colId);
column.filterActive = null
filterInstance.updateFilter(null)
this.gridApi.onFilterChanged();
}
if (column.filterActive) {
query.push({
key: column.colId,
value: column.filterActive
})
}
})
this.setFilterQuery(query)
}
});
},
handleReceiveMessage (event) {
const eventData = this.parseEventData(event.data);
const eventData = parseEventData(event.data);
const res = this.gridApi.applyTransaction({
add: [eventData]
});
const rowNode = res.add[0]
this.gridApi.flashCells({ rowNodes: [rowNode]});
this.gridApi.sizeColumnsToFit()
},
handleReceiveFilters (event) {
this.comboBoxOptions = this.parseEventData(event.data);
},
parseEventData (eventData) {
try {
let json = JSON.parse(eventData)
if (!json.message) {
json.message = JSON.stringify(json.json)
let data = parseEventData(event.data);
let opts = this.comboBoxOptions
for (let k in data) {
if (!(k in opts)) {
opts[k] = []
}
// TODO: proper merging
if ((data[k].parentKey) || (opts[k].length === 0)) {
opts[k].push(data[k])
}
return json
} catch (e) {
console.error(e, eventData)
}
this.comboBoxOptions = opts
let correctOptions = {};
for (let column in opts) {
correctOptions[column] = []
let columnDef = this.columnDefs.find((columnDef) => {
return columnDef.field === column
});
let parentColumnName = columnDef?.filterParams?.parentColumn;
let possibleColumnOptions = opts[column].filter((k) => {
return k.parentKey === parentColumnName
})
if (possibleColumnOptions.length === 1) {
correctOptions[column] = possibleColumnOptions[0].options
} else if (possibleColumnOptions.length > 1) {
let filterInstance = this.gridApi.getFilterInstance(parentColumnName)
possibleColumnOptions.forEach((opt) => {
if (filterInstance && (opt.parentValue === filterInstance.filter)) {
correctOptions[column] = opt.options
}
})
}
}
this.gridApi.sizeColumnsToFit()
this.setFilterOptions(correctOptions)
},
openExamineLog (row) {
const selectedRow = row.data
@ -152,19 +176,5 @@ export default {
},
}
const flattenObj = (ob) => {
let result = {};
for (const i in ob) {
if ((typeof ob[i]) === 'object' && !Array.isArray(ob[i])) {
const temp = flattenObj(ob[i]);
for (const j in temp) {
result[i + '.' + j] = temp[j];
}
}
else {
result[i] = ob[i];
}
}
return result;
};
</script>

17
src/helpers/flattenObj.js Normal file
View File

@ -0,0 +1,17 @@
const flattenObj = (ob) => {
let result = {};
for (const i in ob) {
if ((typeof ob[i]) === 'object' && !Array.isArray(ob[i])) {
const temp = flattenObj(ob[i]);
for (const j in temp) {
result[i + '.' + j] = temp[j];
}
}
else {
result[i] = ob[i];
}
}
return result;
};
export default flattenObj;

View File

@ -0,0 +1,13 @@
const parseEventData = (eventData) => {
try {
let json = JSON.parse(eventData)
if (!json.message && json.json) {
json.message = JSON.stringify(json.json)
}
return json
} catch (e) {
console.error(e, eventData)
}
};
export default parseEventData;

View File

@ -1,4 +1,5 @@
import { createApp } from 'vue'
import store from "./stores";
import App from './App.vue'
import vuetify from './plugins/vuetify'
import { loadFonts } from './plugins/webfontloader'
@ -6,6 +7,8 @@ import './assets/main.css'
import 'vue-select/dist/vue-select.css';
loadFonts()
createApp(App)
.use(vuetify)
.mount('#app')
const app = createApp(App);
app.use(store);
app.use(vuetify);
app.mount("#app");

View File

@ -1,14 +0,0 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})

26
src/stores/index.js Normal file
View File

@ -0,0 +1,26 @@
import { createStore } from "vuex"
const store = createStore({
state: {
filterOptions: {},
filterQuery: []
},
actions: {
setFilterOptions(context, payload) {
context.commit("SET_FILTER_OPTIONS", payload);
},
setFilterQuery(context, payload) {
context.commit("SET_FILTER_QUERY", payload);
},
},
mutations: {
SET_FILTER_OPTIONS(state, payload) {
state.filterOptions = payload
},
SET_FILTER_QUERY(state, payload) {
state.filterQuery = payload
},
},
});
export default store