diff --git a/package.json b/package.json index 836330e..86b8887 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/components/Filter/Combobox.vue b/src/components/Filter/Combobox.vue index 4ddef6c..3685343 100644 --- a/src/components/Filter/Combobox.vue +++ b/src/components/Filter/Combobox.vue @@ -1,8 +1,8 @@ @@ -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] ?? [] } } } diff --git a/src/components/Filter/ComboboxFilter.js b/src/components/Filter/ComboboxFilter.js index 7b49d00..586a2c8 100644 --- a/src/components/Filter/ComboboxFilter.js +++ b/src/components/Filter/ComboboxFilter.js @@ -7,7 +7,8 @@ export default { Combobox }, template: ``, data: function () { diff --git a/src/components/Grid/Main/config.js b/src/components/Grid/Main/config.js new file mode 100644 index 0000000..2daeddc --- /dev/null +++ b/src/components/Grid/Main/config.js @@ -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 \ No newline at end of file diff --git a/src/components/LogViewer.vue b/src/components/LogViewer.vue index 637fc94..c367c74 100644 --- a/src/components/LogViewer.vue +++ b/src/components/LogViewer.vue @@ -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; -}; + diff --git a/src/helpers/flattenObj.js b/src/helpers/flattenObj.js new file mode 100644 index 0000000..e2c7765 --- /dev/null +++ b/src/helpers/flattenObj.js @@ -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; \ No newline at end of file diff --git a/src/helpers/parseEventData.js b/src/helpers/parseEventData.js new file mode 100644 index 0000000..c7fe398 --- /dev/null +++ b/src/helpers/parseEventData.js @@ -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; \ No newline at end of file diff --git a/src/main.js b/src/main.js index 24dac50..14f0d63 100644 --- a/src/main.js +++ b/src/main.js @@ -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"); + diff --git a/src/stores/counter.js b/src/stores/counter.js deleted file mode 100644 index cfaade1..0000000 --- a/src/stores/counter.js +++ /dev/null @@ -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 } -}) diff --git a/src/stores/index.js b/src/stores/index.js new file mode 100644 index 0000000..50c5422 --- /dev/null +++ b/src/stores/index.js @@ -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 \ No newline at end of file