diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..7604d3d --- /dev/null +++ b/frontend/.gitignore @@ -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 diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..5103d0f --- /dev/null +++ b/frontend/README.md @@ -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. + diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..38c9324 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Log Viewer + + +
+ + + diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..616dffe --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,15 @@ + + + + + + diff --git a/frontend/src/assets/base.css b/frontend/src/assets/base.css new file mode 100644 index 0000000..71dc55a --- /dev/null +++ b/frontend/src/assets/base.css @@ -0,0 +1,74 @@ +/* color palette from */ +: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; +} diff --git a/frontend/src/assets/logo.svg b/frontend/src/assets/logo.svg new file mode 100644 index 0000000..bc826fe --- /dev/null +++ b/frontend/src/assets/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/main.css b/frontend/src/assets/main.css new file mode 100644 index 0000000..81dca27 --- /dev/null +++ b/frontend/src/assets/main.css @@ -0,0 +1,12 @@ +@import "./base.css"; + +div#app { + width: 100%; + height: 100vh; +} + +.screenshots-drawer { + position: fixed; + display: flex; + flex-direction: column; +} diff --git a/frontend/src/components/LogViewer.vue b/frontend/src/components/LogViewer.vue new file mode 100644 index 0000000..b81c57e --- /dev/null +++ b/frontend/src/components/LogViewer.vue @@ -0,0 +1,118 @@ + + + diff --git a/frontend/src/components/ScreenshotCell.js b/frontend/src/components/ScreenshotCell.js new file mode 100644 index 0000000..3653f5e --- /dev/null +++ b/frontend/src/components/ScreenshotCell.js @@ -0,0 +1,30 @@ +export default { + template: `
+ View screenshots +
+ +
+
`, + 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 + } + }, +}; \ No newline at end of file diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..8b9d201 --- /dev/null +++ b/frontend/src/main.js @@ -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') diff --git a/frontend/src/set-public-path.js b/frontend/src/set-public-path.js new file mode 100644 index 0000000..5170684 --- /dev/null +++ b/frontend/src/set-public-path.js @@ -0,0 +1,3 @@ +import { setPublicPath } from "systemjs-webpack-interop"; + +setPublicPath("app1"); diff --git a/frontend/src/stores/counter.js b/frontend/src/stores/counter.js new file mode 100644 index 0000000..cfaade1 --- /dev/null +++ b/frontend/src/stores/counter.js @@ -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 } +}) diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..b810ea4 --- /dev/null +++ b/frontend/vite.config.js @@ -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' + } + } + })], +} diff --git a/frontend/vue.config.js b/frontend/vue.config.js new file mode 100644 index 0000000..4b7b239 --- /dev/null +++ b/frontend/vue.config.js @@ -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`); + } + }, + }), + ], + }, +};