Merge pull request #8 from PRO-Robotech/feature/Design

serving with express
This commit is contained in:
typescreep
2025-06-09 14:57:45 +03:00
14 changed files with 4132 additions and 43 deletions

View File

@@ -1 +1,8 @@
KUBE_API_URL=
CUSTOMIZATION_API_GROUP=
CUSTOMIZATION_API_VERSION=
RPROJECTS_VERSION=
PROJECTS_RESOURCE_NAME=
MARKETPLACE_RESOURCE_NAME=
MARKETPLACE_KIND=
INSTANCES_VERSION=

View File

@@ -1,5 +1,6 @@
**/build/*
**/public/*
**/server/*
**/node_modules/*
!src/
vite.config.ts

4
.gitignore vendored
View File

@@ -2,6 +2,7 @@
# dependencies
/node_modules
server/node_modules
# testing
/coverage
@@ -11,10 +12,11 @@
# misc
.DS_Store
.env.options
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.env.options
.env.local
public/env.js

View File

@@ -9,6 +9,7 @@
href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&display=swap"
rel="stylesheet"
/>
<script src="/env.js"></script>
<title>OpenAPI UI</title>
</head>
<body>

1695
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,5 @@
{
"name": "openapi-ui",
"type": "module",
"version": "0.0.1",
"private": true,
"scripts": {
@@ -8,6 +7,10 @@
"build": "tsc && vite build",
"serve": "vite preview --port 4001 --strictPort",
"preview": "vite preview --port 4001 --strictPort",
"server:prod": "node build/index.js",
"server:prod:local": "cross-env LOCAL=true BASEPREFIX=/openapi-ui node build/index.js",
"server:dev": "cross-env LOCAL=true BASEPREFIX=/openapi-ui tsx ./server/index.ts",
"server:build": "tsc --project tsconfig.server.json --noEmit false",
"tsc": "tsc --project tsconfig.json",
"lint:css": "stylelint ./src/**/styled.{js,ts} --fix",
"lint:js": "eslint .",
@@ -17,7 +20,7 @@
"@ant-design/icons": "5.6.0",
"@monaco-editor/react": "4.6.0",
"@originjs/vite-plugin-federation": "1.3.6",
"@prorobotech/openapi-k8s-toolkit": "0.0.1-alpha.23",
"@prorobotech/openapi-k8s-toolkit": "0.0.1-alpha.24",
"@readme/openapi-parser": "4.0.0",
"@reduxjs/toolkit": "2.2.5",
"@tanstack/react-query": "5.62.2",
@@ -25,10 +28,16 @@
"antd": "5.20.0",
"axios": "1.4.0",
"cross-env": "7.0.3",
"dotenv": "16.4.7",
"dotenv": "16.4.5",
"express": "4.19.2",
"express-healthcheck": "0.1.0",
"express-prom-bundle": "8.0.0",
"express-winston": "4.2.0",
"http-proxy-middleware": "2.0.6",
"jsonpath": "1.1.1",
"lodash": "4.17.21",
"openapi-types": "12.1.3",
"prom-client": "15.1.3",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-redux": "9.1.2",
@@ -63,6 +72,7 @@
"prettier": "3.0.2",
"stylelint": "16.14.1",
"stylelint-config-standard": "37.0.0",
"tsx": "4.19.2",
"vite": "5.4.6",
"vite-plugin-node-polyfills": "0.22.0",
"vite-tsconfig-paths": "5.0.1"

31
server/getDynamicIndex.ts Normal file
View File

@@ -0,0 +1,31 @@
export const getDynamicIndex = (baseprefix: string): string => {
try {
const mainJs = 'index-react.js'
const mainCss = 'style.css'
return `<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&display=swap"
rel="stylesheet"
/>
<title>OpenAPI UI</title>
<script src="${baseprefix}/env.js"></script>
<script type="module" crossorigin src="${baseprefix}/${mainJs}"></script>
<link rel="stylesheet" crossorigin href="${baseprefix}/${mainCss}">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
`
} catch {
return 'Error while trying'
}
}

158
server/index.ts Normal file
View File

@@ -0,0 +1,158 @@
const path = require('path')
const fs = require('fs').promises
import express, { Express } from 'express'
const { createProxyMiddleware } = require('http-proxy-middleware')
import dotenv from 'dotenv'
import { getDynamicIndex } from './getDynamicIndex'
dotenv.config()
const basePrefix = process.env.BASEPREFIX
let options: dotenv.DotenvParseOutput | undefined
if (process.env.LOCAL === 'true') {
const { parsed } = dotenv.config({ path: './.env.options' })
options = parsed
}
const KUBE_API_URL = process.env.LOCAL === 'true' ? options?.KUBE_API_URL : process.env.KUBE_API_URL
const CUSTOMIZATION_API_GROUP =
process.env.LOCAL === 'true' ? options?.CUSTOMIZATION_API_GROUP : process.env.CUSTOMIZATION_API_GROUP
const CUSTOMIZATION_API_VERSION =
process.env.LOCAL === 'true' ? options?.CUSTOMIZATION_API_VERSION : process.env.CUSTOMIZATION_API_VERSION
const RPROJECTS_VERSION = process.env.LOCAL === 'true' ? options?.RPROJECTS_VERSION : process.env.RPROJECTS_VERSION
const PROJECTS_RESOURCE_NAME =
process.env.LOCAL === 'true' ? options?.PROJECTS_RESOURCE_NAME : process.env.PROJECTS_RESOURCE_NAME
const MARKETPLACE_RESOURCE_NAME =
process.env.LOCAL === 'true' ? options?.MARKETPLACE_RESOURCE_NAME : process.env.MARKETPLACE_RESOURCE_NAME
const MARKETPLACE_KIND = process.env.LOCAL === 'true' ? options?.MARKETPLACE_KIND : process.env.MARKETPLACE_KIND
const INSTANCES_VERSION = process.env.LOCAL === 'true' ? options?.INSTANCES_VERSION : process.env.INSTANCES_VERSION
const healthcheck = require('express-healthcheck')
const promBundle = require('express-prom-bundle')
const metricsMiddleware = promBundle({ includeMethod: true, metricsPath: `${basePrefix ? basePrefix : ''}/metrics` })
const winston = require('winston')
const expressWinston = require('express-winston')
const app: Express = express()
const port = process.env.PORT || 8080
app.use(`${basePrefix ? basePrefix : ''}/healthcheck`, healthcheck())
app.use(metricsMiddleware)
if (process.env.LOGGER === 'true') {
app.use(
expressWinston.logger({
transports: [new winston.transports.Console()],
timeStamp: true,
format: winston.format.combine(
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss',
}),
winston.format.json(),
),
expressFormat: true,
colorize: false,
requestWhitelist: ['body'],
responseWhitelist: ['body'],
}),
)
}
// Only add proxies if LOCAL=true
if (process.env.LOCAL === 'true') {
console.log('✅ Proxies are enabled.')
// Proxy: /api/clusters/.*/k8s/
app.use(
'/api/clusters/:clusterId/k8s',
createProxyMiddleware({
target: `${KUBE_API_URL}/api/clusters`,
changeOrigin: true,
secure: false,
ws: true,
pathRewrite: (path, req) => path.replace(/^\/api\/clusters\//, '/'),
// logLevel: 'debug',
// onProxyReq: (proxyReq, req, res) => {
// console.debug(`[PROXY] ${req.method} ${req.originalUrl} -> ${proxyReq.getHeader('host')}${proxyReq.path}`)
// },
}),
)
// Proxy: /clusterlist
app.use(
'/clusterlist',
createProxyMiddleware({
target: `${KUBE_API_URL}/clusterlist`,
changeOrigin: true,
secure: false,
pathRewrite: (path, req) => path.replace(/^\/clusterlist/, ''),
// logLevel: 'debug',
// onProxyReq: (proxyReq, req, res) => {
// console.debug(`[PROXY] ${req.method} ${req.originalUrl} -> ${proxyReq.getHeader('host')}${proxyReq.path}`)
// },
}),
)
} else {
console.log('🚫 Proxies are disabled.')
}
app.get(`${basePrefix ? basePrefix : ''}/env.js`, (_, res) => {
res.set('Content-Type', 'text/javascript')
res.send(
`
window._env_ = {
${basePrefix ? ` BASEPREFIX: "${basePrefix}",` : ''}
CUSTOMIZATION_API_GROUP: ${JSON.stringify(CUSTOMIZATION_API_GROUP) || '"check envs"'},
CUSTOMIZATION_API_VERSION: ${JSON.stringify(CUSTOMIZATION_API_VERSION) || '"check envs"'},
RPROJECTS_VERSION: ${JSON.stringify(RPROJECTS_VERSION) || '"check envs"'},
PROJECTS_RESOURCE_NAME: ${JSON.stringify(PROJECTS_RESOURCE_NAME) || '"check envs"'},
MARKETPLACE_RESOURCE_NAME: ${JSON.stringify(MARKETPLACE_RESOURCE_NAME) || '"check envs"'},
MARKETPLACE_KIND: ${JSON.stringify(MARKETPLACE_KIND) || '"check envs"'},
INSTANCES_VERSION: ${JSON.stringify(INSTANCES_VERSION) || '"check envs"'}
}
`,
)
})
app.get(`${basePrefix ? basePrefix : ''}/docs`, (_, res) => {
res.redirect(process.env.DOCUMENTATION_URI || '/')
})
const tryFiles = async (req, res, next) => {
try {
const unsafeReqPath = basePrefix ? req.path.replace(basePrefix, '') : req.path
const safeReqPath = path.normalize(unsafeReqPath).replace(/^(\.\.(\/|\\|$))+/, '')
const filePath = path.join(__dirname, safeReqPath.replace(/^\//, ''))
await fs.access(filePath)
return res.sendFile(filePath)
} catch (error: any) {
if (basePrefix) {
const indexText = getDynamicIndex(basePrefix)
res.set('Content-Type', 'text/html')
return res.send(indexText)
}
return res.sendFile('/index.html', {
root: path.join(__dirname),
})
}
}
app.get(`${basePrefix ? basePrefix : ''}/`, (_, res) => {
if (basePrefix) {
const indexText = getDynamicIndex(basePrefix)
res.set('Content-Type', 'text/html')
return res.send(indexText)
}
res.sendFile('/index.html', {
root: path.join(__dirname),
})
})
app.get('*', (req, res, next) => {
tryFiles(req, res, next)
})
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`)
})

2184
server/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

22
server/package.json Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "openapi-ui",
"version": "0.0.1",
"private": true,
"scripts": {
"server:build": "tsc --project tsconfig.server.json --noEmit false"
},
"dependencies": {
"cross-env": "7.0.3",
"dotenv": "16.4.5",
"express": "4.19.2",
"express-healthcheck": "0.1.0",
"express-prom-bundle": "8.0.0",
"express-winston": "4.2.0",
"http-proxy-middleware": "2.0.6",
"prom-client": "15.1.3",
"typescript": "4.9.5"
},
"devDependencies": {
"tsx": "4.19.2"
}
}

View File

@@ -1,7 +1,10 @@
export const BASE_API_GROUP = import.meta.env.VITE_CUSTOMIZATION_API_GROUP
export const BASE_API_VERSION = import.meta.env.VITE_CUSTOMIZATION_API_VERSION
export const BASE_PROJECTS_VERSION = import.meta.env.VITE_PROJECTS_VERSION
export const BASE_PROJECTS_RESOURCE_NAME = import.meta.env.VITE_PROJECTS_RESOURCE_NAME
export const BASE_MARKETPLACE_RESOURCE_NAME = import.meta.env.VITE_MARKETPLACE_RESOURCE_NAME
export const BASE_MARKETPLACE_KIND = import.meta.env.VITE_MARKETPLACE_KIND
export const BASE_INSTANCES_VERSION = import.meta.env.VITE_INSTANCES_VERSION
/* eslint-disable no-underscore-dangle */
export const BASE_API_GROUP = window._env_.CUSTOMIZATION_API_GROUP || import.meta.env.VITE_CUSTOMIZATION_API_GROUP
export const BASE_API_VERSION = window._env_.CUSTOMIZATION_API_VERSION || import.meta.env.VITE_CUSTOMIZATION_API_VERSION
export const BASE_PROJECTS_VERSION = window._env_.PROJECTS_VERSION || import.meta.env.VITE_PROJECTS_VERSION
export const BASE_PROJECTS_RESOURCE_NAME =
window._env_.PROJECTS_RESOURCE_NAME || import.meta.env.VITE_PROJECTS_RESOURCE_NAME
export const BASE_MARKETPLACE_RESOURCE_NAME =
window._env_.MARKETPLACE_RESOURCE_NAME || import.meta.env.VITE_MARKETPLACE_RESOURCE_NAME
export const BASE_MARKETPLACE_KIND = window._env_.MARKETPLACE_KIND || import.meta.env.VITE_MARKETPLACE_KIND
export const BASE_INSTANCES_VERSION = window._env_.INSTANCES_VERSION || import.meta.env.VITE_INSTANCES_VERSION

View File

@@ -1,6 +1,7 @@
/* eslint-disable no-underscore-dangle */
export const getBasePrefix = (isFederation?: boolean) => {
if (isFederation) {
return '/openapi-ui-federation'
}
return import.meta.env.BASE_URL || '/openapi-ui'
return window._env_.BASEPREFIX || import.meta.env.BASE_URL || '/openapi-ui'
}

23
tsconfig.server.json Normal file
View File

@@ -0,0 +1,23 @@
{
"compilerOptions": {
"baseUrl": "server",
"target": "esnext",
"lib": ["esnext"],
"module": "CommonJS",
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"outDir": "./build",
"noImplicitAny": false
},
"include": ["server", "server/**/*.ts"],
"exclude": ["node_modules"]
}

View File

@@ -5,19 +5,26 @@ import react from '@vitejs/plugin-react-swc'
import federation from '@originjs/vite-plugin-federation'
import { nodePolyfills } from 'vite-plugin-node-polyfills'
const { VITE_BASEPREFIX } = process.env
const { parsed: options } = await dotenv.config({ path: './.env.options' })
// const { VITE_BASEPREFIX } = process.env
const { parsed: options } = dotenv.config({ path: './.env.options' })
// https://vitejs.dev/config/
export default defineConfig({
root: './',
base: VITE_BASEPREFIX || '/openapi-ui',
// base: VITE_BASEPREFIX || '/openapi-ui',
build: {
outDir: 'build',
modulePreload: false,
target: 'esnext',
minify: false,
cssCodeSplit: false,
rollupOptions: {
output: {
entryFileNames: `[name]-react.js`,
chunkFileNames: `[name]-react.js`,
assetFileNames: `[name].[ext]`,
},
},
},
publicDir: 'public',
plugins: [