Compare commits

...

2 Commits

Author SHA1 Message Date
14a7d936d2 RoadSign Sideload now built-in web ui 2024-01-01 18:16:57 +08:00
86b65cd21f 🎉 Basic Web Sideload 2024-01-01 18:07:21 +08:00
38 changed files with 3867 additions and 19 deletions

59
.idea/codeStyles/Project.xml generated Normal file
View File

@ -0,0 +1,59 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<HTMLCodeStyleSettings>
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
</HTMLCodeStyleSettings>
<JSCodeStyleSettings version="0">
<option name="USE_SEMICOLON_AFTER_STATEMENT" value="false" />
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</JSCodeStyleSettings>
<TypeScriptCodeStyleSettings version="0">
<option name="USE_SEMICOLON_AFTER_STATEMENT" value="false" />
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</TypeScriptCodeStyleSettings>
<VueCodeStyleSettings>
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
</VueCodeStyleSettings>
<codeStyleSettings language="HTML">
<option name="SOFT_MARGINS" value="120" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JavaScript">
<option name="SOFT_MARGINS" value="120" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="TypeScript">
<option name="SOFT_MARGINS" value="120" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="Vue">
<option name="SOFT_MARGINS" value="120" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

View File

@ -1,8 +1,14 @@
# Building Backend
FROM golang:alpine as roadsign-server
RUN apk add nodejs npm
WORKDIR /source
COPY . .
WORKDIR /source/pkg/sideload/view
RUN npm install
RUN npm run build
WORKDIR /source
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -buildvcs -o /dist ./pkg/cmd/server/main.go
# Runtime

29
pkg/sideload/processes.go Normal file
View File

@ -0,0 +1,29 @@
package sideload
import (
"code.smartsheep.studio/goatworks/roadsign/pkg/sign"
"github.com/gofiber/fiber/v2"
"github.com/samber/lo"
)
func getProcesses(c *fiber.Ctx) error {
processes := lo.FlatMap(sign.App.Sites, func(item *sign.SiteConfig, idx int) []*sign.ProcessInstance {
return item.Processes
})
return c.JSON(processes)
}
func getProcessLog(c *fiber.Ctx) error {
processes := lo.FlatMap(sign.App.Sites, func(item *sign.SiteConfig, idx int) []*sign.ProcessInstance {
return item.Processes
})
if target, ok := lo.Find(processes, func(item *sign.ProcessInstance) bool {
return item.ID == c.Params("id")
}); !ok {
return fiber.NewError(fiber.StatusNotFound)
} else {
return c.SendString(target.GetLogs())
}
}

View File

@ -15,8 +15,8 @@ import (
func doPublish(c *fiber.Ctx) error {
var workdir string
var site *sign.SiteConfig
var upstream *sign.UpstreamConfig
var process *sign.ProcessConfig
var upstream *sign.UpstreamInstance
var process *sign.ProcessInstance
for _, item := range sign.App.Sites {
if item.ID == c.Params("site") {
site = item

View File

@ -1,8 +1,11 @@
package sideload
import (
"code.smartsheep.studio/goatworks/roadsign/pkg/sideload/view"
"fmt"
"github.com/gofiber/fiber/v2/middleware/filesystem"
jsoniter "github.com/json-iterator/go"
"net/http"
roadsign "code.smartsheep.studio/goatworks/roadsign/pkg"
"github.com/gofiber/fiber/v2"
@ -39,9 +42,21 @@ func InitSideload() *fiber.App {
},
}))
app.Use("/", filesystem.New(filesystem.Config{
Root: http.FS(view.FS),
PathPrefix: "dist",
Index: "index.html",
NotFoundFile: "index.html",
}))
cgi := app.Group("/cgi").Name("CGI")
{
cgi.All("/connectivity", responseConnectivity)
cgi.Get("/statistics", getStatistics)
cgi.Get("/sites", getSites)
cgi.Get("/sites/cfg/:id", getSiteConfig)
cgi.Get("/processes", getProcesses)
cgi.Get("/processes/logs/:id", getProcessLog)
}
webhooks := app.Group("/webhooks").Name("WebHooks")

View File

@ -12,6 +12,24 @@ import (
"gopkg.in/yaml.v2"
)
func getSites(c *fiber.Ctx) error {
return c.JSON(sign.App.Sites)
}
func getSiteConfig(c *fiber.Ctx) error {
fp := filepath.Join(viper.GetString("paths.configs"), c.Params("id"))
var err error
var data []byte
if data, err = os.ReadFile(fp + ".yml"); err != nil {
if data, err = os.ReadFile(fp + ".yaml"); err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
}
return c.Type("yaml").SendString(string(data))
}
func doSyncSite(c *fiber.Ctx) error {
var req sign.SiteConfig

View File

@ -0,0 +1,28 @@
package sideload
import (
"code.smartsheep.studio/goatworks/roadsign/pkg/sign"
"github.com/gofiber/fiber/v2"
"github.com/samber/lo"
)
func getStatistics(c *fiber.Ctx) error {
upstreams := lo.FlatMap(sign.App.Sites, func(item *sign.SiteConfig, idx int) []*sign.UpstreamInstance {
return item.Upstreams
})
processes := lo.FlatMap(sign.App.Sites, func(item *sign.SiteConfig, idx int) []*sign.ProcessInstance {
return item.Processes
})
unhealthy := lo.FlatMap(sign.App.Sites, func(item *sign.SiteConfig, idx int) []*sign.ProcessInstance {
return lo.Filter(item.Processes, func(item *sign.ProcessInstance, idx int) bool {
return item.Status != sign.ProcessStarted
})
})
return c.JSON(fiber.Map{
"sites": len(sign.App.Sites),
"upstreams": len(upstreams),
"processes": len(processes),
"status": len(unhealthy) == 0,
})
}

View File

@ -0,0 +1,2 @@
/dist
/node_modules

View File

@ -0,0 +1,15 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
'extends': [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier/skip-formatting'
],
parserOptions: {
ecmaVersion: 'latest'
}
}

30
pkg/sideload/view/.gitignore vendored Normal file
View File

@ -0,0 +1,30 @@
# 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?
*.tsbuildinfo

View File

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"tabWidth": 2,
"singleQuote": false,
"printWidth": 120,
"trailingComma": "none"
}

View File

@ -0,0 +1,8 @@
{
"recommendations": [
"Vue.volar",
"Vue.vscode-typescript-vue-plugin",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}

View File

@ -0,0 +1,46 @@
# @roadsign/sideload-ui
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
1. Disable the built-in TypeScript Extension
1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
yarn
```
### Compile and Hot-Reload for Development
```sh
yarn dev
```
### Type-Check, Compile and Minify for Production
```sh
yarn build
```
### Lint with [ESLint](https://eslint.org/)
```sh
yarn lint
```

View File

@ -0,0 +1,6 @@
package view
import "embed"
//go:embed all:dist
var FS embed.FS

1
pkg/sideload/view/env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

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>RoadSign</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@ -0,0 +1,44 @@
{
"name": "@roadsign/sideload-ui",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build --force",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/"
},
"dependencies": {
"@guolao/vue-monaco-editor": "^1.4.1",
"highlight.js": "^11.9.0",
"js-yaml": "^4.1.0",
"pinia": "^2.1.7",
"vue": "^3.3.11",
"vue-router": "^4.2.5"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.3.3",
"@tsconfig/node18": "^18.2.2",
"@types/js-yaml": "^4.0.9",
"@types/node": "^18.19.3",
"@vicons/carbon": "^0.12.0",
"@vitejs/plugin-vue": "^4.5.2",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.5.0",
"eslint": "^8.49.0",
"eslint-plugin-vue": "^9.17.0",
"naive-ui": "^2.36.0",
"npm-run-all2": "^6.1.1",
"prettier": "^3.0.3",
"typescript": "~5.3.0",
"unocss": "^0.58.2",
"vfonts": "^0.0.3",
"vite": "^5.0.10",
"vue-tsc": "^1.8.25"
}
}

View File

@ -0,0 +1,6 @@
@import "vfonts/IBMPlexSans.css";
@import "vfonts/IBMPlexMono.css";
a {
color: #3f7ee8;
}

View File

@ -0,0 +1,135 @@
<template>
<div class="flex gap-[4px]">
<n-button size="small" @click="publishing = true">
<template #icon>
<n-icon :component="CloudUpload" />
</template>
</n-button>
<n-button size="small" @click="editConfig()">
<template #icon>
<n-icon :component="Edit" />
</template>
</n-button>
<n-modal
v-model:show="publishing"
class="w-[720px]"
preset="card"
title="Publish Artifacts"
segmented
closable
>
We are sorry about this tool isn't completed yet. <br>
For now, you can use our <b>Wonderful Command Line Tool RDS</b> <br>
Learn more on our <a href="https://wiki.smartsheep.studio/roadsign/index.html" target="_blank">official wiki</a>.
<br>
<br>
Install it by this command below
<n-code code="go install code.smartsheep.studio/goatworks/roadsign/pkg/cmd/rds@latest" />
<br>
Then connect your rds client to this server
<n-code :code="`rds connect <name> ${host} <credentials>`" />
<br>
After that you can publish your stuff (You need to compress them to zip archive before publish)
<n-code :code="`rds deploy <name> ${props.id} <upstream id or process id>`" />
</n-modal>
<n-modal
v-model:show="editing"
class="w-[720px]"
content-style="padding: 0"
preset="card"
title="Edit Configuration"
segmented
closable
>
<div class="relative h-[540px]">
<vue-monaco-editor
v-model:value="config"
:options="{ automaticLayout: true, minimap: { enabled: false } }"
language="yaml"
/>
<div class="fab">
<n-tooltip placement="left">
<template #trigger>
<n-button
circle
type="primary"
size="large"
class="shadow-lg"
:loading="submitting"
@click="syncConfig()"
>
<template #icon>
<n-icon :component="Save" />
</template>
</n-button>
</template>
This operation will restart all processes related. Service may interrupted for some while.
</n-tooltip>
</div>
</div>
</n-modal>
</div>
</template>
<script setup lang="ts">
import { NButton, NCode, NIcon, NModal, NTooltip, useMessage } from "naive-ui"
import { CloudUpload, Edit, Save } from "@vicons/carbon"
import { ref } from "vue"
import { VueMonacoEditor } from "@guolao/vue-monaco-editor"
import * as yaml from "js-yaml"
const message = useMessage()
const props = defineProps<{ id: string, rules: any[], upstreams: any[], processes: any[] }>()
const emits = defineEmits(["reload"])
const host = location.protocol + "//" + location.host
const submitting = ref(false)
const publishing = ref(false)
const editing = ref(false)
const config = ref<string | undefined>(undefined)
async function editConfig() {
const resp = await fetch(`/cgi/sites/cfg/${props.id}`)
config.value = await resp.text()
editing.value = true
}
async function syncConfig() {
if (config.value == null) return
let content
try {
content = yaml.load(config.value)
} catch (e: any) {
message.warning(`Your configuration has some issue: ${e.message}`)
return
}
submitting.value = true
const resp = await fetch(`/webhooks/sync/${props.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(content)
})
if (resp.status != 200) {
message.error(`Something went wrong... ${await resp.text()}`)
} else {
emits("reload")
}
submitting.value = false
}
</script>
<style scoped>
.fab {
position: absolute;
bottom: 16px;
right: 24px;
}
</style>

View File

@ -0,0 +1,110 @@
<template>
<div>
<n-button circle size="small" type="primary" @click="creating = true">
<template #icon>
<n-icon :component="Add" />
</template>
</n-button>
<n-modal
v-model:show="creating"
class="w-[720px]"
content-style="padding: 0"
preset="card"
title="Create Site"
segmented
closable
>
<div class="py-4 px-5 border border-solid border-b border-[#eee]">
<n-input
v-model:value="data.id"
placeholder="Will be the file name of this file"
/>
</div>
<div class="relative mt-[4px] h-[540px]">
<vue-monaco-editor
v-model:value="data.content"
:options="{ automaticLayout: true, minimap: { enabled: false } }"
language="yaml"
/>
<div class="fab">
<n-tooltip placement="left">
<template #trigger>
<n-button
circle
type="primary"
size="large"
class="shadow-lg"
:loading="submitting"
@click="submit()"
>
<template #icon>
<n-icon :component="Checkmark" />
</template>
</n-button>
</template>
This operation will publish this site right away.
</n-tooltip>
</div>
</div>
</n-modal>
</div>
</template>
<script setup lang="ts">
import { NButton, NIcon, NInput, NModal, NTooltip, useMessage } from "naive-ui"
import { Add, Checkmark } from "@vicons/carbon"
import { VueMonacoEditor } from "@guolao/vue-monaco-editor"
import { ref } from "vue"
import * as yaml from "js-yaml"
const message = useMessage()
const emits = defineEmits(["reload"])
const submitting = ref(false)
const creating = ref(false)
const data = ref<any>({})
async function submit() {
let content
try {
content = yaml.load(data.value.content)
} catch (e: any) {
message.warning(`Your configuration has some issue: ${e.message}`)
return
}
submitting.value = true
const resp = await fetch(`/webhooks/sync/${data.value.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(content)
})
if (resp.status != 200) {
message.error(`Something went wrong... ${await resp.text()}`)
} else {
reset()
emits("reload")
message.success("Your site has been created! 🎉")
creating.value = false
}
submitting.value = false
}
function reset() {
data.value.id = ""
data.value.content = ""
}
</script>
<style scoped>
.fab {
position: absolute;
bottom: 16px;
right: 24px;
}
</style>

View File

@ -0,0 +1,36 @@
<template>
<div class="flex flex-col gap-1">
<div>
<div class="font-bold">Rules</div>
<n-code :hljs="hljs" :code="parseData(props.rules)" language="json" />
</div>
<div>
<div class="font-bold">Upstreams</div>
<n-code :hljs="hljs" :code="parseData(props.upstreams)" language="json" />
</div>
<div>
<div class="font-bold">Processes</div>
<n-code :hljs="hljs" :code="parseData(props.processes)" language="json" />
</div>
</div>
</template>
<script setup lang="ts">
import { NCode } from "naive-ui"
import hljs from "highlight.js/lib/core"
import json from "highlight.js/lib/languages/json"
hljs.registerLanguage("json", json)
const props = defineProps<{ rules: any[], upstreams: any[], processes: any[] }>()
function parseData(data: any): string {
return JSON.stringify(data, null, 1)
.replace(/ +/g, " ")
.replace(/\n/g, "")
}
</script>

View File

@ -0,0 +1,76 @@
<template>
<div>
<n-card title="Sites">
<template #header-extra>
<sites-table-add @reload="readSites()" />
</template>
<n-data-table
:columns="columns"
:data="data"
:row-key="(row: any) => row.id"
/>
</n-card>
</div>
</template>
<script setup lang="ts">
import { NCard, NDataTable, NTag } from "naive-ui"
import { h, ref } from "vue"
import SitesTableExpand from "@/components/data/sites-table-expand.vue"
import SitesTableAction from "@/components/data/sites-table-action.vue"
import SitesTableAdd from "@/components/data/sites-table-add.vue"
const columns: any[] = [
{
type: "expand",
renderExpand(row: any) {
return h(SitesTableExpand, { ...row, class: "pl-[38px]" })
}
},
{
title: "ID",
key: "id",
render(row: any) {
return h(NTag, { type: "info", bordered: false, size: "small" }, row?.id)
}
},
{
title: "Rules",
key: "rules",
render(row: any) {
return row?.rules?.length ?? 0
}
},
{
title: "Upstreams",
key: "upstreams",
render(row: any) {
return row?.upstreams?.length ?? 0
}
},
{
title: "Processes",
key: "processes",
render(row: any) {
return row?.processes?.length ?? 0
}
},
{
title: "Actions",
key: "actions",
render(row: any) {
return h(SitesTableAction, { ...row, onReload: () => readSites() })
}
}
]
const data = ref<any[]>([])
async function readSites() {
const resp = await fetch("/cgi/sites")
data.value = await resp.json()
}
readSites()
</script>

View File

@ -0,0 +1,60 @@
<template>
<n-layout>
<n-layout-header class="header py-[8px] px-[36px]" bordered>
<div class="flex items-center gap-2">
<router-link class="link" to="/">
RoadSign<i>!</i>
</router-link>
</div>
<div class="nav-menu">
<div class="h-full flex items-center header-nav">
<n-menu v-model:value="key" :options="options" mode="horizontal" />
</div>
</div>
</n-layout-header>
<n-layout-content class="h-[calc(100vh-70px)] container mx-auto" content-style="padding: 24px">
<router-view />
</n-layout-content>
</n-layout>
</template>
<script setup lang="ts">
import { type MenuOption, NIcon, NLayout, NLayoutContent, NLayoutHeader, NMenu } from "naive-ui"
import { type Component, h, ref } from "vue"
import { Dashboard } from "@vicons/carbon"
import { RouterLink, useRoute, useRouter } from "vue-router"
const route = useRoute()
const router = useRouter()
const key = ref(route.name?.toString())
router.afterEach((to) => {
key.value = to.name?.toString() ?? "index"
})
const options: MenuOption[] = [
{
label: () => h(RouterLink, { to: { name: "dashboard" } }, "Dashboard"),
icon: renderIcon(Dashboard),
key: "dashboard"
}
]
function renderIcon(icon: Component) {
return () => h(NIcon, null, { default: () => h(icon) })
}
</script>
<style scoped>
.header {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 40px;
}
.link {
all: unset;
cursor: pointer;
}
</style>

View File

@ -0,0 +1,16 @@
import "./assets/main.css"
import "virtual:uno.css"
import { createApp } from "vue"
import { createPinia } from "pinia"
import root from "./root.vue"
import router from "./router"
const app = createApp(root)
app.use(createPinia())
app.use(router)
app.mount("#app")

View File

@ -0,0 +1,9 @@
<template>
<n-message-provider>
<router-view />
</n-message-provider>
</template>
<script setup lang="ts">
import { NMessageProvider } from "naive-ui"
</script>

View File

@ -0,0 +1,21 @@
import { createRouter, createWebHistory } from "vue-router"
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/",
name: "layouts.main",
component: () => import("@/layouts/main.vue"),
children: [
{
path: "/",
name: "dashboard",
component: () => import("@/views/dashboard.vue")
},
]
},
]
})
export default router

View File

@ -0,0 +1,35 @@
<template>
<div class="flex flex-col gap-2">
<div class="grid gap-2 grid-cols-2 lg:grid-cols-4">
<n-card embedded>
<n-statistic label="Status">{{ data?.status ? "Operational" : "Incident" }}</n-statistic>
</n-card>
<n-card embedded>
<n-statistic label="Sites">{{ data?.sites }}</n-statistic>
</n-card>
<n-card embedded>
<n-statistic label="Upstreams">{{ data?.upstreams }}</n-statistic>
</n-card>
<n-card embedded>
<n-statistic label="Processes">{{ data?.processes }}</n-statistic>
</n-card>
</div>
<sites-table />
</div>
</template>
<script setup lang="ts">
import { NCard, NStatistic } from "naive-ui"
import { ref } from "vue"
import SitesTable from "@/components/data/sites-table.vue"
const data = ref<any>({})
async function readStatistics() {
const resp = await fetch("/cgi/statistics")
data.value = await resp.json()
}
readStatistics()
</script>

View File

@ -0,0 +1,13 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"noEmit": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

View File

@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}

View File

@ -0,0 +1,17 @@
{
"extends": "@tsconfig/node18/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*"
],
"compilerOptions": {
"composite": true,
"noEmit": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}

View File

@ -0,0 +1,5 @@
import { defineConfig, presetUno } from "unocss"
export default defineConfig({
presets: [presetUno({ preflight: false })]
})

View File

@ -0,0 +1,24 @@
import { fileURLToPath, URL } from "node:url"
import { defineConfig } from "vite"
import vue from "@vitejs/plugin-vue"
import unocss from "unocss/vite"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
unocss()
],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url))
}
},
server: {
proxy: {
"/webhooks": "http://127.0.0.1:81",
"/cgi": "http://127.0.0.1:81"
}
}
})

2904
pkg/sideload/view/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -2,12 +2,25 @@ package sign
import (
"fmt"
"github.com/samber/lo"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
type ProcessConfig struct {
type ProcessStatus = int8
const (
ProcessCreated = ProcessStatus(iota)
ProcessStarting
ProcessStarted
ProcessExited
ProcessFailure
)
type ProcessInstance struct {
ID string `json:"id" yaml:"id"`
Workdir string `json:"workdir" yaml:"workdir"`
Command []string `json:"command" yaml:"command"`
@ -15,10 +28,13 @@ type ProcessConfig struct {
Prepares [][]string `json:"prepares" yaml:"prepares"`
Preheat bool `json:"preheat" yaml:"preheat"`
Cmd *exec.Cmd `json:"-"`
Cmd *exec.Cmd `json:"-"`
Logger strings.Builder `json:"-"`
Status ProcessStatus `json:"status"`
}
func (v *ProcessConfig) BootProcess() error {
func (v *ProcessInstance) BootProcess() error {
if v.Cmd != nil {
return nil
}
@ -43,7 +59,7 @@ func (v *ProcessConfig) BootProcess() error {
}
}
func (v *ProcessConfig) PrepareProcess() error {
func (v *ProcessInstance) PrepareProcess() error {
for _, script := range v.Prepares {
if len(script) <= 0 {
continue
@ -57,7 +73,7 @@ func (v *ProcessConfig) PrepareProcess() error {
return nil
}
func (v *ProcessConfig) StartProcess() error {
func (v *ProcessInstance) StartProcess() error {
if len(v.Command) <= 0 {
return fmt.Errorf("you need set the command for %s to enable process manager", v.ID)
}
@ -65,11 +81,28 @@ func (v *ProcessConfig) StartProcess() error {
v.Cmd = exec.Command(v.Command[0], v.Command[1:]...)
v.Cmd.Dir = filepath.Join(v.Workdir)
v.Cmd.Env = append(v.Cmd.Env, v.Environment...)
v.Cmd.Stdout = &v.Logger
v.Cmd.Stderr = &v.Logger
// Monitor
go func() {
for {
if v.Cmd.Process == nil || v.Cmd.ProcessState == nil {
v.Status = ProcessStarting
} else if !v.Cmd.ProcessState.Exited() {
v.Status = ProcessStarted
} else {
v.Status = lo.Ternary(v.Cmd.ProcessState.Success(), ProcessExited, ProcessFailure)
return
}
time.Sleep(100 * time.Millisecond)
}
}()
return v.Cmd.Start()
}
func (v *ProcessConfig) StopProcess() error {
func (v *ProcessInstance) StopProcess() error {
if v.Cmd != nil && v.Cmd.Process != nil {
if err := v.Cmd.Process.Signal(os.Interrupt); err != nil {
v.Cmd.Process.Kill()
@ -82,8 +115,12 @@ func (v *ProcessConfig) StopProcess() error {
return nil
}
func (v *ProcessInstance) GetLogs() string {
return v.Logger.String()
}
func (v *RoadApp) PreheatProcesses(callbacks ...func(total int, success int)) {
var processes []*ProcessConfig
var processes []*ProcessInstance
for _, site := range v.Sites {
for _, process := range site.Processes {
if process.Preheat {

View File

@ -18,7 +18,7 @@ import (
"github.com/valyala/fasthttp"
)
func makeHypertextResponse(c *fiber.Ctx, upstream *UpstreamConfig) error {
func makeHypertextResponse(c *fiber.Ctx, upstream *UpstreamInstance) error {
timeout := time.Duration(viper.GetInt64("performance.network_timeout")) * time.Millisecond
return proxy.Do(c, upstream.MakeURI(c), &fasthttp.Client{
ReadTimeout: timeout,
@ -26,7 +26,7 @@ func makeHypertextResponse(c *fiber.Ctx, upstream *UpstreamConfig) error {
})
}
func makeFileResponse(c *fiber.Ctx, upstream *UpstreamConfig) error {
func makeFileResponse(c *fiber.Ctx, upstream *UpstreamInstance) error {
uri, queries := upstream.GetRawURI()
root := http.Dir(uri)

View File

@ -44,13 +44,13 @@ type RequestTransformerConfig = transformers.RequestTransformerConfig
type SiteConfig struct {
ID string `json:"id"`
Rules []*RouterRuleConfig `json:"rules" yaml:"rules"`
Rules []*RouterRule `json:"rules" yaml:"rules"`
Transformers []*RequestTransformerConfig `json:"transformers" yaml:"transformers"`
Upstreams []*UpstreamConfig `json:"upstreams" yaml:"upstreams"`
Processes []*ProcessConfig `json:"processes" yaml:"processes"`
Upstreams []*UpstreamInstance `json:"upstreams" yaml:"upstreams"`
Processes []*ProcessInstance `json:"processes" yaml:"processes"`
}
type RouterRuleConfig struct {
type RouterRule struct {
Host []string `json:"host" yaml:"host"`
Path []string `json:"path" yaml:"path"`
Queries map[string]string `json:"queries" yaml:"queries"`

View File

@ -15,12 +15,12 @@ const (
UpstreamTypeUnknown = "unknown"
)
type UpstreamConfig struct {
type UpstreamInstance struct {
ID string `json:"id" yaml:"id"`
URI string `json:"uri" yaml:"uri"`
}
func (v *UpstreamConfig) GetType() string {
func (v *UpstreamInstance) GetType() string {
protocol := strings.SplitN(v.URI, "://", 2)[0]
switch protocol {
case "file", "files":
@ -32,7 +32,7 @@ func (v *UpstreamConfig) GetType() string {
return UpstreamTypeUnknown
}
func (v *UpstreamConfig) GetRawURI() (string, url.Values) {
func (v *UpstreamInstance) GetRawURI() (string, url.Values) {
uri := strings.SplitN(v.URI, "://", 2)[1]
data := strings.SplitN(uri, "?", 2)
data = append(data, " ") // Make data array least have two element
@ -41,7 +41,7 @@ func (v *UpstreamConfig) GetRawURI() (string, url.Values) {
return data[0], qs
}
func (v *UpstreamConfig) MakeURI(ctx *fiber.Ctx) string {
func (v *UpstreamInstance) MakeURI(ctx *fiber.Ctx) string {
var queries []string
for k, v := range ctx.Queries() {
parsed, _ := url.QueryUnescape(v)