diff --git a/.dockerignore b/.dockerignore
index 63aeb86..eb963c1 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,10 +1,12 @@
**/*
!index.html
+!mocks/
!nginx.conf
!package.json
!public/
!src/
!tsconfig.json
!tsconfig.node.json
+!vite-plugin-serve-handler/
!vite.config.ts
!yarn.lock
\ No newline at end of file
diff --git a/.env.development b/.env.development
deleted file mode 100644
index 4b00efa..0000000
--- a/.env.development
+++ /dev/null
@@ -1 +0,0 @@
-BANDWHICHD_API_SERVER=http://localhost:8080
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index f9f5cb5..0557990 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -11,8 +11,10 @@ COPY --chown=node:node \
tsconfig.node.json \
vite.config.ts \
./
+COPY --chown=node:node mocks ./mocks
COPY --chown=node:node public ./public
COPY --chown=node:node src ./src
+COPY --chown=node:node vite-plugin-serve-handler ./vite-plugin-serve-handler
RUN yarn build
FROM nginxinc/nginx-unprivileged:alpine
COPY nginx.conf /etc/nginx/templates/default.conf.template
@@ -24,5 +26,5 @@ LABEL org.opencontainers.image.vendor="neuland – Büro für Informatik GmbH"
LABEL org.opencontainers.image.licenses="MIT"
LABEL org.opencontainers.image.title="bandwhichd-ui"
LABEL org.opencontainers.image.description="bandwhichd ui displaying network topology and statistics"
-LABEL org.opencontainers.image.version="0.3.0"
+LABEL org.opencontainers.image.version="0.4.0"
COPY --from=build --chown=root:root /home/node/dist /usr/share/nginx/html
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
index 2a14e43..dda5d9f 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -2,7 +2,6 @@ services:
main:
build:
context: .
- env_file:
- - .env.development
- ports:
- - 3000:8080
\ No newline at end of file
+ environment:
+ BANDWHICHD_API_SERVER: http://127.0.0.1:8080
+ network_mode: host
\ No newline at end of file
diff --git a/index.html b/index.html
index ff514ae..7a2bdd4 100644
--- a/index.html
+++ b/index.html
@@ -4,7 +4,6 @@
bandwhichd-ui
-
diff --git a/mock-server.mjs b/mock-server.mjs
deleted file mode 100644
index a00a881..0000000
--- a/mock-server.mjs
+++ /dev/null
@@ -1,47 +0,0 @@
-import fs from "fs"
-import http from "http"
-import path from "path"
-import process from "process"
-
-const port = 8080
-
-const server = http.createServer((request, response) => {
- const chunks = []
-
- request.on("data", chunk => {
- chunks.push(chunk)
- })
-
- request.on("end", () => {
- if (request.method !== "GET"
- || request.url !== "/v1/stats") {
- response.writeHead(404)
- response.end()
- return
- }
-
- if (chunks.length > 0) {
- response.writeHead(400)
- response.end()
- return
- }
-
- const format =
- request.headers.accept === "text/vnd.graphviz; q=1.0"
- ? "dot"
- : "json"
-
- const filePath = path.join(process.cwd(), `stats.${format}`)
- const fileStat = fs.statSync(filePath)
-
- response.writeHead(200, {
- "Access-Control-Allow-Origin": "*",
- "Content-Length": fileStat.size
- })
- fs.createReadStream(filePath).pipe(response)
- })
-})
-
-server.listen(port, () => {
- console.log(`bandwhichd mock server listening on port ${port}`)
-})
\ No newline at end of file
diff --git a/mocks/apiHandler.ts b/mocks/apiHandler.ts
new file mode 100644
index 0000000..b766840
--- /dev/null
+++ b/mocks/apiHandler.ts
@@ -0,0 +1,105 @@
+import fs from "fs"
+import http from "http";
+import path from "path"
+import process from "process"
+import type { Connect, ResolvedConfig } from "vite";
+
+import { ServeHandler } from "../vite-plugin-serve-handler";
+
+const isApiRoute: (url: string) => boolean =
+ (url) => url === "/api" || url.startsWith("/api?") || url.startsWith("/api/");
+
+const bandwhichdApiServerFromEnv: (viteConfig: ResolvedConfig) => URL | null =
+ (viteConfig) => {
+ const bandwhichdApiServer = viteConfig.env["BANDWHICHD_API_SERVER"];
+ try {
+ return new URL(bandwhichdApiServer);
+ } catch (_) {
+ return null;
+ }
+ };
+
+const handleWithMocks =
+ (request: Connect.IncomingMessage, response: http.ServerResponse) => {
+ const chunks = [];
+ request.on("data", chunk => {
+ chunks.push(chunk);
+ });
+
+ request.on("end", () => {
+ if (request.method !== "GET"
+ || request.url !== "/api/v1/stats") {
+ response.writeHead(404);
+ response.end();
+ return;
+ }
+
+ if (chunks.length > 0) {
+ response.writeHead(400);
+ response.end();
+ return;
+ }
+
+ const format =
+ request.headers.accept === "text/vnd.graphviz; q=1.0"
+ ? "dot"
+ : "json";
+
+ const filePath = path.join(process.cwd(), 'mocks', `stats.${format}`);
+ const fileStat = fs.statSync(filePath);
+
+ response.writeHead(200, {
+ "Access-Control-Allow-Origin": "*",
+ "Content-Length": fileStat.size,
+ });
+ fs.createReadStream(filePath).pipe(response);
+ });
+ };
+
+const handleWithServer =
+ (request: Connect.IncomingMessage, response: http.ServerResponse, viteConfig: ResolvedConfig, bandwhichdApiServer: URL) => {
+ const upstreamRequestOptions: http.RequestOptions = {
+ protocol: bandwhichdApiServer.protocol,
+ host: bandwhichdApiServer.hostname,
+ port: bandwhichdApiServer.port,
+ method: request.method,
+ path: request.url.substring("/api".length),
+ headers: {
+ ...request.headers,
+ host: `${bandwhichdApiServer.host}`,
+ },
+ };
+
+ request.pipe(http.request(upstreamRequestOptions, (upstreamResponse) => {
+ upstreamResponse.pipe(response).on("error", (error) => {
+ viteConfig.logger.error("mocks/apiHandler: Error proxying response", {
+ error,
+ timestamp: true
+ });
+ response.end();
+ });
+ })).on("error", (error) => {
+ viteConfig.logger.error("mocks/apiHandler: Error proxying request", {
+ error,
+ timestamp: true
+ });
+ response.end();
+ });
+ };
+
+export const apiHandler: ServeHandler.Handler =
+ (request, response, viteConfig) => {
+ if (!isApiRoute(request.url)) {
+ return false;
+ }
+
+ const bandwhichdApiServer = bandwhichdApiServerFromEnv(viteConfig);
+
+ if (bandwhichdApiServer === null) {
+ handleWithMocks(request, response);
+ } else {
+ handleWithServer(request, response, viteConfig, bandwhichdApiServer);
+ }
+
+ return true;
+ };
\ No newline at end of file
diff --git a/stats.dot b/mocks/stats.dot
similarity index 100%
rename from stats.dot
rename to mocks/stats.dot
diff --git a/mocks/stats.json b/mocks/stats.json
new file mode 100644
index 0000000..9754ac6
--- /dev/null
+++ b/mocks/stats.json
@@ -0,0 +1 @@
+{"hosts":{"28e9f9f4-c9ec-7448-d591-f8fce34086ce":{"hostname":"spring-db1","additional_hostnames":[],"connections":{}},"d416fa7d-edef-ded2-5074-3244110a5a3d":{"hostname":"spring-mongodb","additional_hostnames":[],"connections":{}},"fda7ecc9-eb5f-002d-2d11-eb4ab88ef9e4":{"hostname":"spring-logging","additional_hostnames":[],"connections":{}},"b110d228-d4c9-667b-c4da-913425b25175":{"hostname":"ecom-nginx-services","additional_hostnames":[],"connections":{}},"3509c6bc-b535-a72a-2bc1-f5b585e03063":{"hostname":"ecom-cache2","additional_hostnames":[],"connections":{}},"dda70a41-0ee2-8e2c-dc15-1a12cd46b7e4":{"hostname":"ecom-app-live1","additional_hostnames":[],"connections":{}},"f6541cc6-8eea-8e2c-d40b-959e18660923":{"hostname":"ecom-cache","additional_hostnames":[],"connections":{}},"55faca59-16af-34b8-9f0d-882b6531e82d":{"hostname":"spring-staging","additional_hostnames":[],"connections":{}},"2b09a772-335d-d36b-1cfb-eb75f6c9bea1":{"hostname":"spring-app-live2","additional_hostnames":[],"connections":{}},"fd4760d6-baa4-3b27-3bf0-a83174eb5014":{"hostname":"spring-web-live6","additional_hostnames":[],"connections":{}},"35b2b94f-41f9-c120-8852-af0665d5b628":{"hostname":"spring-cache","additional_hostnames":[],"connections":{}},"6075cc26-5b1b-5f01-f0d8-2a78e139a0d2":{"hostname":"spring-cache2","additional_hostnames":[],"connections":{}},"1c9684d1-cb83-501e-63da-9077e9868a98":{"hostname":"spring-services1","additional_hostnames":[],"connections":{}},"2000044d-80c2-fb2f-80a7-30345cea90ff":{"hostname":"ecom-services1","additional_hostnames":[],"connections":{}},"eebbdbb0-1fdc-7802-8b35-6a5f84699eb5":{"hostname":"ecom-services2","additional_hostnames":[],"connections":{}},"045c74d2-e4f6-6c5d-76fa-a55a3b23be6e":{"hostname":"ecom-logging","additional_hostnames":[],"connections":{}},"0ead0515-5d04-9c5a-9a8d-9724d34882ee":{"hostname":"ecom-app-live2","additional_hostnames":[],"connections":{}},"78334682-0244-2adb-6cf2-c243717a3f58":{"hostname":"ecom-web-live3","additional_hostnames":[],"connections":{}},"71d66f34-2bc1-7853-bc9d-0ee87a963264":{"hostname":"ecom-web-live6","additional_hostnames":[],"connections":{}},"a5a0f816-8cb2-a634-9dc5-6d28dbbef6e3":{"hostname":"spring-staging2","additional_hostnames":[],"connections":{}},"40b13e86-8d85-816d-bbe4-09d223eaf94c":{"hostname":"spring-nginx-services","additional_hostnames":[],"connections":{}},"0add42fd-d09b-13ad-62d0-9d7edf87e7b1":{"hostname":"spring-app-live1","additional_hostnames":[],"connections":{}},"98bc86ae-6dd9-05a0-4119-851e82b84dc2":{"hostname":"spring-services2","additional_hostnames":[],"connections":{}},"2c22b189-ed70-9ad1-b758-54bd5b1aef4b":{"hostname":"spring-web-live3","additional_hostnames":[],"connections":{}}},"unmonitoredHosts":{}}
\ No newline at end of file
diff --git a/nginx.conf b/nginx.conf
index cb0a87f..749aa62 100644
--- a/nginx.conf
+++ b/nginx.conf
@@ -1,10 +1,16 @@
server {
- listen 8080;
- listen [::]:8080;
+ listen 3000;
+ listen [::]:3000;
+
+ location ~ ^/api(/.*)?$ {
+ proxy_pass ${BANDWHICHD_API_SERVER}$1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Real-IP $remote_addr;
+ }
location / {
root /usr/share/nginx/html;
index index.html;
- sub_filter 'PLACEHOLDER_API_SERVER' '${BANDWHICHD_API_SERVER}';
}
}
\ No newline at end of file
diff --git a/package.json b/package.json
index 1674f66..e8667aa 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "bandwhichd-ui",
- "version": "0.3.0",
+ "version": "0.4.0",
"description": "bandwhichd ui displaying network topology and statistics",
"license": "MIT",
"private": true,
@@ -9,8 +9,7 @@
"dev": "vite --port=3000",
"build": "tsc && vite build",
"preview": "vite preview",
- "test": "jest",
- "mock-server": "node mock-server.mjs"
+ "test": "jest"
},
"dependencies": {
"fp-ts": "^2.12.2",
diff --git a/src/Configuration.ts b/src/Configuration.ts
deleted file mode 100644
index 9840f46..0000000
--- a/src/Configuration.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-export interface Configuration {
- apiServer: string;
-}
-
-if (import.meta.env.DEV) {
- // @ts-ignore
- document.configuration = {
- apiServer: import.meta.env.BANDWHICHD_API_SERVER,
- };
-}
-
-// @ts-ignore
-export const configuration: Configuration = document.configuration
\ No newline at end of file
diff --git a/src/Graph.tsx b/src/Graph.tsx
index df4c815..ddc114d 100644
--- a/src/Graph.tsx
+++ b/src/Graph.tsx
@@ -1,12 +1,11 @@
import React, { useEffect, useRef, useState } from "react";
import * as VisNetwork from "vis-network";
-import { Configuration, configuration } from "./Configuration";
import { HostId } from "./Stats";
import styles from "./Graph.module.css";
-const fetchData = async (configuration: Configuration): Promise => {
- const response = await window.fetch(`${configuration.apiServer}/v1/stats`, {
+const fetchData = async (): Promise => {
+ const response = await window.fetch("/api/v1/stats", {
method: "GET",
headers: {
"Accept": "text/vnd.graphviz; q=1.0"
@@ -33,7 +32,7 @@ export const Graph: React.FC =
const container = containerRef.current;
setIsLoading(true);
- fetchData(configuration).then(data => {
+ fetchData().then(data => {
// @ts-ignore
const parsedData = VisNetwork.parseDOTNetwork(data);
parsedData.options.physics = {
diff --git a/src/Header.tsx b/src/Header.tsx
index eb1d420..2f0a7a4 100644
--- a/src/Header.tsx
+++ b/src/Header.tsx
@@ -1,7 +1,6 @@
import React from "react";
-import { configuration } from "./Configuration";
import styles from "./Header.module.css"
export const Header: React.FC =
- () => Connected to {configuration.apiServer}
;
\ No newline at end of file
+ () => ;
\ No newline at end of file
diff --git a/src/Main.tsx b/src/Main.tsx
index b106d0d..22b8d20 100644
--- a/src/Main.tsx
+++ b/src/Main.tsx
@@ -1,5 +1,4 @@
import React, { useEffect, useState } from "react";
-import { configuration } from "./Configuration";
import { Graph } from "./Graph";
import { HostDetails } from "./HostDetails";
@@ -23,7 +22,7 @@ export const Main: React.FC =
: { hostId: maybeSelectedHostId, ...maybeSelectedHostWithoutId };
useEffect(() => {
- fetchStats(configuration).then(stats => {
+ fetchStats().then(stats => {
setMaybeStats(stats);
}).catch(console.error);
}, []);
diff --git a/src/Stats.ts b/src/Stats.ts
index 6f3049d..bb89da8 100644
--- a/src/Stats.ts
+++ b/src/Stats.ts
@@ -1,4 +1,3 @@
-import { Configuration } from "./Configuration";
import { Map } from "immutable";
import * as Decoder from "io-ts/lib/Decoder"
import { mapDecoder } from "./lib/immutable/io-ts/mapDecoder";
@@ -45,8 +44,8 @@ const statsDecoder = Decoder.struct({
});
export const fetchStats =
- async (configuration: Configuration): Promise =>
- await window.fetch(`${configuration.apiServer}/v1/stats`, {
+ async (): Promise =>
+ await window.fetch("/api/v1/stats", {
method: "GET",
headers: {
"Accept": "application/json; q=1.0"
diff --git a/stats.json b/stats.json
deleted file mode 100644
index 505ac36..0000000
--- a/stats.json
+++ /dev/null
@@ -1 +0,0 @@
-{"hosts":{"28e9f9f4-c9ec-7448-d591-f8fce34086ce":{"hostname":"spring-db1","additional_hostnames":[]},"d416fa7d-edef-ded2-5074-3244110a5a3d":{"hostname":"spring-mongodb","additional_hostnames":[]},"fda7ecc9-eb5f-002d-2d11-eb4ab88ef9e4":{"hostname":"spring-logging","additional_hostnames":[]},"b110d228-d4c9-667b-c4da-913425b25175":{"hostname":"ecom-nginx-services","additional_hostnames":[]},"3509c6bc-b535-a72a-2bc1-f5b585e03063":{"hostname":"ecom-cache2","additional_hostnames":[]},"dda70a41-0ee2-8e2c-dc15-1a12cd46b7e4":{"hostname":"ecom-app-live1","additional_hostnames":[]},"f6541cc6-8eea-8e2c-d40b-959e18660923":{"hostname":"ecom-cache","additional_hostnames":[]},"55faca59-16af-34b8-9f0d-882b6531e82d":{"hostname":"spring-staging","additional_hostnames":[]},"2b09a772-335d-d36b-1cfb-eb75f6c9bea1":{"hostname":"spring-app-live2","additional_hostnames":[]},"fd4760d6-baa4-3b27-3bf0-a83174eb5014":{"hostname":"spring-web-live6","additional_hostnames":[]},"35b2b94f-41f9-c120-8852-af0665d5b628":{"hostname":"spring-cache","additional_hostnames":[]},"6075cc26-5b1b-5f01-f0d8-2a78e139a0d2":{"hostname":"spring-cache2","additional_hostnames":[]},"1c9684d1-cb83-501e-63da-9077e9868a98":{"hostname":"spring-services1","additional_hostnames":[]},"2000044d-80c2-fb2f-80a7-30345cea90ff":{"hostname":"ecom-services1","additional_hostnames":[]},"eebbdbb0-1fdc-7802-8b35-6a5f84699eb5":{"hostname":"ecom-services2","additional_hostnames":[]},"045c74d2-e4f6-6c5d-76fa-a55a3b23be6e":{"hostname":"ecom-logging","additional_hostnames":[]},"0ead0515-5d04-9c5a-9a8d-9724d34882ee":{"hostname":"ecom-app-live2","additional_hostnames":[]},"78334682-0244-2adb-6cf2-c243717a3f58":{"hostname":"ecom-web-live3","additional_hostnames":[]},"71d66f34-2bc1-7853-bc9d-0ee87a963264":{"hostname":"ecom-web-live6","additional_hostnames":[]},"a5a0f816-8cb2-a634-9dc5-6d28dbbef6e3":{"hostname":"spring-staging2","additional_hostnames":[]},"40b13e86-8d85-816d-bbe4-09d223eaf94c":{"hostname":"spring-nginx-services","additional_hostnames":[]},"0add42fd-d09b-13ad-62d0-9d7edf87e7b1":{"hostname":"spring-app-live1","additional_hostnames":[]},"98bc86ae-6dd9-05a0-4119-851e82b84dc2":{"hostname":"spring-services2","additional_hostnames":[]},"2c22b189-ed70-9ad1-b758-54bd5b1aef4b":{"hostname":"spring-web-live3","additional_hostnames":[]}}}
\ No newline at end of file
diff --git a/tsconfig.node.json b/tsconfig.node.json
index 9d31e2a..da0fbcd 100644
--- a/tsconfig.node.json
+++ b/tsconfig.node.json
@@ -5,5 +5,9 @@
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
- "include": ["vite.config.ts"]
+ "include": [
+ "mocks/",
+ "vite.config.ts",
+ "vite-plugin-serve-handler/",
+ ]
}
diff --git a/vite-plugin-serve-handler/index.ts b/vite-plugin-serve-handler/index.ts
new file mode 100644
index 0000000..877ff96
--- /dev/null
+++ b/vite-plugin-serve-handler/index.ts
@@ -0,0 +1,52 @@
+import http from "http";
+import type { Connect, Plugin, ResolvedConfig, ViteDevServer } from "vite";
+
+export namespace ServeHandler {
+ export type Handler = (request: Connect.IncomingMessage, response: http.ServerResponse, viteConfig: ResolvedConfig) => boolean;
+}
+
+const serverHandlerMiddleware: (config: ResolvedConfig, options: FinalServerHandlerOptions) => Connect.NextHandleFunction =
+ (config, options) => (request, response, next) => {
+ try {
+ if (!options.handler(request, response, config)) {
+ next();
+ }
+ } catch (error) {
+ config.logger.error("vite-plugin-serve-handler: handler error", {
+ error,
+ timestamp: true
+ });
+ next();
+ }
+ };
+
+export interface ServeHandlerOptions {
+ handler?: ServeHandler.Handler;
+}
+
+interface FinalServerHandlerOptions {
+ handler: ServeHandler.Handler;
+}
+
+const defaultServeHandlerOptions: FinalServerHandlerOptions = {
+ handler: (_, __) => false,
+};
+
+export const ServeHandler: (options?: ServeHandlerOptions) => Plugin =
+ (givenOptions) => {
+ let config: ResolvedConfig | null = null;
+ const options: FinalServerHandlerOptions =
+ typeof givenOptions !== "object"
+ ? defaultServeHandlerOptions
+ : { ...defaultServeHandlerOptions, ...givenOptions };
+ return {
+ name: "vite-plugin-serve-handler",
+ apply: "serve",
+ configResolved: (resolvedConfig) => {
+ config = resolvedConfig;
+ },
+ configureServer: (server: ViteDevServer) => {
+ server.middlewares.use(serverHandlerMiddleware(config, options));
+ },
+ };
+ };
\ No newline at end of file
diff --git a/vite.config.ts b/vite.config.ts
index e02e6a7..6fe3dc1 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,8 +1,15 @@
-import { defineConfig } from 'vite'
-import react from '@vitejs/plugin-react'
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+import { ServeHandler } from "./vite-plugin-serve-handler";
+import { apiHandler } from "./mocks/apiHandler";
// https://vitejs.dev/config/
export default defineConfig({
- envPrefix: 'BANDWHICHD_',
- plugins: [react()]
-})
\ No newline at end of file
+ envPrefix: "BANDWHICHD_",
+ plugins: [
+ react(),
+ ServeHandler({
+ handler: apiHandler,
+ }),
+ ]
+});
\ No newline at end of file