Vue.js 3 based frontend
This commit is contained in:
		
							
								
								
									
										29
									
								
								frontend/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								frontend/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| # Logs | ||||
| logs | ||||
| *.log | ||||
| npm-debug.log* | ||||
| yarn-debug.log* | ||||
| yarn-error.log* | ||||
| pnpm-debug.log* | ||||
| lerna-debug.log* | ||||
|  | ||||
| node_modules | ||||
| .DS_Store | ||||
| dist | ||||
| dist-ssr | ||||
| coverage | ||||
| *.local | ||||
|  | ||||
| /cypress/videos/ | ||||
| /cypress/screenshots/ | ||||
|  | ||||
| # Editor directories and files | ||||
| .vscode/* | ||||
| !.vscode/extensions.json | ||||
| .idea | ||||
| *.suo | ||||
| *.ntvs* | ||||
| *.njsproj | ||||
| *.sln | ||||
| *.sw? | ||||
| *.kpt-pipeline | ||||
							
								
								
									
										67
									
								
								frontend/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								frontend/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| # log-viewer microfrontend (WIP) | ||||
| This is an experimental microfrontend to view logs coming from camtiler, testing microfrontends concept and developing | ||||
| Javascript/Node application directly on Kubernetes cluster using Skaffold (hot-reloading source files works!) | ||||
|  | ||||
| Currently the application itself is a really minimalistic frontend based on Vue.js 3 framework. | ||||
|  | ||||
| ## Getting started | ||||
| 0. Have access with kubectl to desired Kubernetes cluster  | ||||
| 0. Have local Docker daemon ready along with access to your image registry (building in-cluster with Kaniko is currently not tested)  | ||||
| 1. Get Skaffold 2.0.0-beta*: https://github.com/GoogleContainerTools/skaffold/releases | ||||
| 2. Run `skaffold dev --namespace {desired K8S namespace}` | ||||
|  | ||||
| And you should be good to go. Skaffold would create a deployment, build the container and upload it to your registry | ||||
| and deploy it. | ||||
|  | ||||
| ## Good to know | ||||
| ### Skaffold | ||||
| All Skaffold configuration is in skaffold.yml. There we have: | ||||
| 1. App/deployment name (metadata) | ||||
| 2. Build artifacts - artifact that Skaffold would build and use, either in case of development or also actual deploy | ||||
| 3. Profiles - similar to `npm` commands/profiles, you can have many different actions (run `skaffold {action}`). | ||||
|    We only have `dev` which will build and deploy the `log-viewer-frontend` container | ||||
|    (in manner how it's defined in Kubernetes manifest), and then enable `manual sync`, which defines which  | ||||
|    changed files will be copied to the container when it's running - change in everything else will trigger | ||||
|    rebuilding the container. | ||||
|  | ||||
| ### Hot-reload vs rebuilding the container | ||||
| Currently the files that will be copied are fine-tuned as they are known to work well with Vue.js hot-reload feature | ||||
| (eg actual frontend code). Other files, such as framework configuration files (`vite.config.js`, `vue.config.js`) | ||||
| will trigger rebuilding and more importantly, restarting the container, to provide stability. It might not be necessary | ||||
| to restart the container, this is just testing at this point. You can try it by introducing less explicit `sync`  | ||||
| config in `skaffold.yml`. | ||||
|  | ||||
| Change in `packages*` files will also trigger rebuilding and restarting the container, with the difference of | ||||
| caching the result of the `npm install` command - `node_modules` directory. So in case of changing framework config, | ||||
| the container will be quickly rebuilt and restarted, but when changing packages in `package.json`, `npm install` | ||||
| will also be ran when re-building the container. | ||||
|  | ||||
| ### Dockerfile | ||||
| This is a really simple Dockerfile based on the official Node Docker image. | ||||
| Only extra stuff it does is: | ||||
| 1. exposing the port for reference | ||||
| 2. changing workdir(?) | ||||
| 3. running `npm install` on-demand | ||||
| 4. copying other code into the container | ||||
| 5. defining the run command (important!) | ||||
|  | ||||
| We also have the `.dockerignore` to filter out files that never need to make into the container, such as IDE | ||||
| metadata directories, Git, readme etc. | ||||
|  | ||||
| With the current config, your local Docker daemon will be used to build the image and upload it to your image registry. | ||||
| Building in-cluster with Kaniko should be supported (https://skaffold.dev/docs/pipeline-stages/builders/docker/) | ||||
| but currently not tested.  | ||||
|  | ||||
| ### Kubernetes | ||||
| The Kubernetes manifest(s) are supposed to be in the `k8s/` directory. | ||||
| Skaffold will pick them up automatically from there, apply them (also cleaning up after itself), | ||||
| and match the container images from it's manifest and Kubernetes manifest, so it knows when to make the Kubernetes | ||||
| redeploy the image. | ||||
|  | ||||
| Here we have a simple deployment, where `https://playground.k-space.ee/` takes us to the (development) frontend | ||||
| container and `https://playground.k-space.ee/events` gets data from the `camtiler log-backend`. | ||||
|  | ||||
| The actual production usage may vary a lot in case of frontend - one would usually not run a  | ||||
| Node.js server to serve frontend assets, unless it's a SSR frontend (Nuxt, Next frameworks). | ||||
| But for Node.js backends, it's pretty close to the standard. | ||||
|  | ||||
							
								
								
									
										13
									
								
								frontend/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								frontend/index.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
|   <head> | ||||
|     <meta charset="UTF-8" /> | ||||
|     <link rel="icon" href="/favicon.ico" /> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||
|     <title>Log Viewer</title> | ||||
|   </head> | ||||
|   <body> | ||||
|     <div id="app"></div> | ||||
|     <script type="module" src="/src/main.js"></script> | ||||
|   </body> | ||||
| </html> | ||||
							
								
								
									
										15
									
								
								frontend/src/App.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								frontend/src/App.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| <script setup> | ||||
| import LogViewer from './components/LogViewer.vue' | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|     <LogViewer /> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|   name: 'app1', | ||||
| } | ||||
| </script> | ||||
| <style scoped> | ||||
| </style> | ||||
							
								
								
									
										74
									
								
								frontend/src/assets/base.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								frontend/src/assets/base.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | ||||
| /* color palette from <https://github.com/vuejs/theme> */ | ||||
| :root { | ||||
|   --vt-c-white: #ffffff; | ||||
|   --vt-c-white-soft: #f8f8f8; | ||||
|   --vt-c-white-mute: #f2f2f2; | ||||
|  | ||||
|   --vt-c-black: #181818; | ||||
|   --vt-c-black-soft: #222222; | ||||
|   --vt-c-black-mute: #282828; | ||||
|  | ||||
|   --vt-c-indigo: #2c3e50; | ||||
|  | ||||
|   --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); | ||||
|   --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); | ||||
|   --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); | ||||
|   --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); | ||||
|  | ||||
|   --vt-c-text-light-1: var(--vt-c-indigo); | ||||
|   --vt-c-text-light-2: rgba(60, 60, 60, 0.66); | ||||
|   --vt-c-text-dark-1: var(--vt-c-white); | ||||
|   --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); | ||||
| } | ||||
|  | ||||
| /* semantic color variables for this project */ | ||||
| :root { | ||||
|   --color-background: var(--vt-c-white); | ||||
|   --color-background-soft: var(--vt-c-white-soft); | ||||
|   --color-background-mute: var(--vt-c-white-mute); | ||||
|  | ||||
|   --color-border: var(--vt-c-divider-light-2); | ||||
|   --color-border-hover: var(--vt-c-divider-light-1); | ||||
|  | ||||
|   --color-heading: var(--vt-c-text-light-1); | ||||
|   --color-text: var(--vt-c-text-light-1); | ||||
|  | ||||
|   --section-gap: 160px; | ||||
| } | ||||
|  | ||||
| @media (prefers-color-scheme: dark) { | ||||
|   :root { | ||||
|     --color-background: var(--vt-c-black); | ||||
|     --color-background-soft: var(--vt-c-black-soft); | ||||
|     --color-background-mute: var(--vt-c-black-mute); | ||||
|  | ||||
|     --color-border: var(--vt-c-divider-dark-2); | ||||
|     --color-border-hover: var(--vt-c-divider-dark-1); | ||||
|  | ||||
|     --color-heading: var(--vt-c-text-dark-1); | ||||
|     --color-text: var(--vt-c-text-dark-2); | ||||
|   } | ||||
| } | ||||
|  | ||||
| *, | ||||
| *::before, | ||||
| *::after { | ||||
|   box-sizing: border-box; | ||||
|   margin: 0; | ||||
|   position: relative; | ||||
|   font-weight: normal; | ||||
| } | ||||
|  | ||||
| body { | ||||
|   min-height: 100vh; | ||||
|   color: var(--color-text); | ||||
|   background: var(--color-background); | ||||
|   transition: color 0.5s, background-color 0.5s; | ||||
|   line-height: 1.6; | ||||
|   font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, | ||||
|     Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; | ||||
|   font-size: 15px; | ||||
|   text-rendering: optimizeLegibility; | ||||
|   -webkit-font-smoothing: antialiased; | ||||
|   -moz-osx-font-smoothing: grayscale; | ||||
| } | ||||
							
								
								
									
										1
									
								
								frontend/src/assets/logo.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/src/assets/logo.svg
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"  xmlns:v="https://vecta.io/nano"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg> | ||||
| After Width: | Height: | Size: 308 B | 
							
								
								
									
										12
									
								
								frontend/src/assets/main.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								frontend/src/assets/main.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| @import "./base.css"; | ||||
|  | ||||
| div#app { | ||||
|   width: 100%; | ||||
|   height: 100vh; | ||||
| } | ||||
|  | ||||
| .screenshots-drawer { | ||||
|   position: fixed; | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
| } | ||||
							
								
								
									
										118
									
								
								frontend/src/components/LogViewer.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								frontend/src/components/LogViewer.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,118 @@ | ||||
| <template> | ||||
|   <div style="height: 100%; width: 100%"> | ||||
|   <ag-grid-vue | ||||
|       style="width: 100%; height: 100%;" | ||||
|       class="ag-theme-alpine" | ||||
|       @grid-ready="onGridReady" | ||||
|       :defaultColDef="defaultColDef" | ||||
|       :columnDefs="columnDefs" | ||||
|       :row-data="null" | ||||
|       :supress-horisontal-scroll="true" | ||||
|       :enable-scrolling="true" | ||||
|   ></ag-grid-vue> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { AgGridVue } from "ag-grid-vue3"; | ||||
| import "ag-grid-community/styles//ag-grid.css"; | ||||
| import "ag-grid-community/styles//ag-theme-alpine.css"; | ||||
| import ScreenshotCell from "./ScreenshotCell.js"; | ||||
| import {watchEffect} from "vue"; | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|     AgGridVue, | ||||
|     ScreenshotCell: ScreenshotCell, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       gridApi: null, | ||||
|       gridColumnApi: null, | ||||
|       rowData: [], | ||||
|       defaultColDef: { | ||||
|         width: 50, | ||||
|         initialPinned: true, | ||||
|         filter: true, | ||||
|         floatingFilter: true, | ||||
|         resizable: true, | ||||
|       }, | ||||
|       columnDefs:  [ | ||||
|         { | ||||
|           field: '_id', | ||||
|         }, | ||||
|         { | ||||
|           field: 'kubernetes.namespace', | ||||
|           headerName: 'namespace', | ||||
|         }, | ||||
|         { | ||||
|           field: 'kubernetes.pod.name', | ||||
|           headerName: 'pod', | ||||
|         }, | ||||
|         { | ||||
|           field: 'kubernetes.container.name', | ||||
|           headerName: 'container', | ||||
|         }, | ||||
|         { | ||||
|           field: 'message', | ||||
|           tooltipValueGetter: (params) => 'Address: ' + params.value, | ||||
|           width: 500, | ||||
|         }, | ||||
|         { | ||||
|           field: 'stream', | ||||
|         }, | ||||
|         { | ||||
|           field: '@timestamp', | ||||
|           width: 70 | ||||
|         } | ||||
|       ], | ||||
|     } | ||||
|   }, | ||||
|   created() { | ||||
|     this.setupStream() | ||||
|   }, | ||||
|   methods: { | ||||
|     setupStream() { | ||||
|       let es = new EventSource('/events'); | ||||
|       es.onmessage = (e) => this.handleReceiveMessage(e) | ||||
|     }, | ||||
|     onGridReady(params) { | ||||
|       this.gridApi = params.api; | ||||
|       this.gridColumnApi = params.columnApi; | ||||
|     }, | ||||
|     handleReceiveMessage (event) { | ||||
|       const eventData = this.parseEventData(event.data); | ||||
|       this.rowData.unshift(eventData) | ||||
|       this.gridApi.setRowData(this.rowData) | ||||
|       this.gridApi.sizeColumnsToFit() | ||||
|     }, | ||||
|     parseEventData (eventData) { | ||||
|       try { | ||||
|         let json = JSON.parse(eventData) | ||||
|         if (!json.message) { | ||||
|           json.message = JSON.stringify(json.json) | ||||
|         } | ||||
|         return json | ||||
|       } catch (e) { | ||||
|         console.error(e, eventData) | ||||
|       } | ||||
|     }, | ||||
|     setColumnDefs (json) { | ||||
|       const keys = Object.keys(json) | ||||
|       const defs = [] | ||||
|       keys.map((key) => { | ||||
|         const definition = { | ||||
|           field: key, | ||||
|           initialPinned: true, | ||||
|           filter: true, | ||||
|           floatingFilter: true, | ||||
|           minWidth: 80 | ||||
|         } | ||||
|         defs.push(definition) | ||||
|       }) | ||||
|       this.columnDefs = defs | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| </script> | ||||
							
								
								
									
										30
									
								
								frontend/src/components/ScreenshotCell.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								frontend/src/components/ScreenshotCell.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| export default { | ||||
|     template: `<div> | ||||
|       <a @click="openDrawer">View screenshots</a> | ||||
|       <div v-if="drawerOpen" class="screenshots-drawer"> | ||||
|         <img v-for="screenshot in screenshots" :src="screenshot.orig"/> | ||||
|       </div> | ||||
|     </div>`, | ||||
|     data: function () { | ||||
|         return { | ||||
|             screenshots: [], | ||||
|             drawerOpen: false, | ||||
|         }; | ||||
|     }, | ||||
|     beforeMount() { | ||||
|         this.updateImage(this.params); | ||||
|         this.updateImage(this.params); | ||||
|     }, | ||||
|     methods: { | ||||
|         updateImage(params) { | ||||
|             this.screenshots = params.value | ||||
|             this.value = params.value; | ||||
|         }, | ||||
|         refresh(params) { | ||||
|             this.updateImage(params); | ||||
|         }, | ||||
|         openDrawer () { | ||||
|             this.drawerOpen = true | ||||
|         } | ||||
|     }, | ||||
| }; | ||||
							
								
								
									
										11
									
								
								frontend/src/main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								frontend/src/main.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| import { createApp } from 'vue' | ||||
| import { createPinia } from 'pinia' | ||||
| import App from './App.vue' | ||||
|  | ||||
| import './assets/main.css' | ||||
|  | ||||
| const app = createApp(App) | ||||
|  | ||||
| app.use(createPinia()) | ||||
|  | ||||
| app.mount('#app') | ||||
							
								
								
									
										3
									
								
								frontend/src/set-public-path.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								frontend/src/set-public-path.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| import { setPublicPath } from "systemjs-webpack-interop"; | ||||
|  | ||||
| setPublicPath("app1"); | ||||
							
								
								
									
										14
									
								
								frontend/src/stores/counter.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								frontend/src/stores/counter.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| 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 } | ||||
| }) | ||||
							
								
								
									
										21
									
								
								frontend/vite.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								frontend/vite.config.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| import vue from '@vitejs/plugin-vue' | ||||
|  | ||||
| export default { | ||||
|   resolve: { | ||||
|     alias: { | ||||
|       vue: 'vue/dist/vue.esm-bundler.js' | ||||
|     } | ||||
|   }, | ||||
|   rollupOptions: { | ||||
|     input: 'src/main.js', | ||||
|     format: 'system', | ||||
|     preserveEntrySignatures: true | ||||
|   }, | ||||
|   plugins: [vue({ | ||||
|     template: { | ||||
|       transformAssetUrls: { | ||||
|         base: '/src' | ||||
|       } | ||||
|     } | ||||
|   })], | ||||
| } | ||||
							
								
								
									
										37
									
								
								frontend/vue.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								frontend/vue.config.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
|  | ||||
| const path = require('path'); | ||||
| const fs = require('fs'); | ||||
| const EventHooksPlugin = require('event-hooks-webpack-plugin'); | ||||
|  | ||||
| module.exports = { | ||||
|     publicPath: '/logs', | ||||
|     chainWebpack: (config) => { | ||||
|         config.devServer.headers({ | ||||
|             'Access-Control-Allow-Origin': '*', | ||||
|         }); | ||||
|         config.devServer.set('port', 8080); | ||||
|         config.devServer.set('hot', true); | ||||
|  | ||||
|         config.output.filename('[name].js'); | ||||
|         config.output.publicPath('/logs'); | ||||
|  | ||||
|         config.externals([ | ||||
|             'vue', | ||||
|             'vue-router' | ||||
|         ]); | ||||
|     }, | ||||
|     lintOnSave: true, | ||||
|     filenameHashing: false, | ||||
|     configureWebpack: { | ||||
|         plugins: [ | ||||
|             new EventHooksPlugin({ | ||||
|                 done: () => { | ||||
|                     if (process.env.NODE_ENV !== 'development') { | ||||
|                         const buildDir = path.join(__dirname, '/dist'); | ||||
|                         fs.unlinkSync(`${buildDir}/index.html`); | ||||
|                     } | ||||
|                 }, | ||||
|             }), | ||||
|         ], | ||||
|     }, | ||||
| }; | ||||
		Reference in New Issue
	
	Block a user