Vue.js 3 based frontend

This commit is contained in:
Erki Aas 2022-10-05 17:44:28 +03:00
parent b6f21b8192
commit 6cb78c657f
14 changed files with 445 additions and 0 deletions

29
frontend/.gitignore vendored Normal file
View 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
View 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
View 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
View 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>

View 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;
}

View 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

View File

@ -0,0 +1,12 @@
@import "./base.css";
div#app {
width: 100%;
height: 100vh;
}
.screenshots-drawer {
position: fixed;
display: flex;
flex-direction: column;
}

View 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>

View 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
View 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')

View File

@ -0,0 +1,3 @@
import { setPublicPath } from "systemjs-webpack-interop";
setPublicPath("app1");

View 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
View 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
View 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`);
}
},
}),
],
},
};