mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2025-05-11 02:15:48 +02:00
refactor: config management
This commit is contained in:
parent
eb9ede5a4e
commit
53ae1a1ef9
12 changed files with 405 additions and 203 deletions
|
@ -3,42 +3,48 @@ package config
|
||||||
import (
|
import (
|
||||||
"github.com/0xJacky/Nginx-UI/api"
|
"github.com/0xJacky/Nginx-UI/api"
|
||||||
"github.com/0xJacky/Nginx-UI/internal/config"
|
"github.com/0xJacky/Nginx-UI/internal/config"
|
||||||
|
"github.com/0xJacky/Nginx-UI/internal/helper"
|
||||||
"github.com/0xJacky/Nginx-UI/internal/nginx"
|
"github.com/0xJacky/Nginx-UI/internal/nginx"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/sashabaranov/go-openai"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func AddConfig(c *gin.Context) {
|
func AddConfig(c *gin.Context) {
|
||||||
var request struct {
|
var json struct {
|
||||||
Name string `json:"name" binding:"required"`
|
Name string `json:"name" binding:"required"`
|
||||||
Content string `json:"content" binding:"required"`
|
NewFilepath string `json:"new_filepath" binding:"required"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
Overwrite bool `json:"overwrite"`
|
||||||
}
|
}
|
||||||
|
|
||||||
err := c.BindJSON(&request)
|
if !api.BindAndValid(c, &json) {
|
||||||
if err != nil {
|
|
||||||
api.ErrHandler(c, err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
name := request.Name
|
name := json.Name
|
||||||
content := request.Content
|
content := json.Content
|
||||||
|
path := json.NewFilepath
|
||||||
path := nginx.GetConfPath("/", name)
|
if !helper.IsUnderDirectory(path, nginx.GetConfPath()) {
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{
|
||||||
if _, err = os.Stat(path); err == nil {
|
"message": "new filepath is not under the nginx conf path",
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{
|
|
||||||
"message": "config exist",
|
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if content != "" {
|
if !json.Overwrite && helper.FileExists(path) {
|
||||||
err = os.WriteFile(path, []byte(content), 0644)
|
c.JSON(http.StatusNotAcceptable, gin.H{
|
||||||
if err != nil {
|
"message": "File exists",
|
||||||
api.ErrHandler(c, err)
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err := os.WriteFile(path, []byte(content), 0644)
|
||||||
|
if err != nil {
|
||||||
|
api.ErrHandler(c, err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
output := nginx.Reload()
|
output := nginx.Reload()
|
||||||
|
@ -50,7 +56,10 @@ func AddConfig(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, config.Config{
|
c.JSON(http.StatusOK, config.Config{
|
||||||
Name: name,
|
Name: name,
|
||||||
Content: content,
|
Content: content,
|
||||||
|
ChatGPTMessages: make([]openai.ChatCompletionMessage, 0),
|
||||||
|
FilePath: path,
|
||||||
|
ModifiedAt: time.Now(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
13
api/config/base_path.go
Normal file
13
api/config/base_path.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/0xJacky/Nginx-UI/internal/nginx"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetBasePath(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"base_path": nginx.GetConfPath(),
|
||||||
|
})
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ package config
|
||||||
import (
|
import (
|
||||||
"github.com/0xJacky/Nginx-UI/api"
|
"github.com/0xJacky/Nginx-UI/api"
|
||||||
"github.com/0xJacky/Nginx-UI/internal/config"
|
"github.com/0xJacky/Nginx-UI/internal/config"
|
||||||
|
"github.com/0xJacky/Nginx-UI/internal/helper"
|
||||||
"github.com/0xJacky/Nginx-UI/internal/nginx"
|
"github.com/0xJacky/Nginx-UI/internal/nginx"
|
||||||
"github.com/0xJacky/Nginx-UI/query"
|
"github.com/0xJacky/Nginx-UI/query"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
@ -15,16 +16,20 @@ func GetConfig(c *gin.Context) {
|
||||||
name := c.Param("name")
|
name := c.Param("name")
|
||||||
|
|
||||||
path := nginx.GetConfPath("/", name)
|
path := nginx.GetConfPath("/", name)
|
||||||
|
if !helper.IsUnderDirectory(path, nginx.GetConfPath()) {
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{
|
||||||
|
"message": "path is not under the nginx conf path",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
stat, err := os.Stat(path)
|
stat, err := os.Stat(path)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.ErrHandler(c, err)
|
api.ErrHandler(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
content, err := os.ReadFile(path)
|
content, err := os.ReadFile(path)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.ErrHandler(c, err)
|
api.ErrHandler(c, err)
|
||||||
return
|
return
|
||||||
|
@ -32,7 +37,6 @@ func GetConfig(c *gin.Context) {
|
||||||
|
|
||||||
g := query.ChatGPTLog
|
g := query.ChatGPTLog
|
||||||
chatgpt, err := g.Where(g.Name.Eq(path)).FirstOrCreate()
|
chatgpt, err := g.Where(g.Name.Eq(path)).FirstOrCreate()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.ErrHandler(c, err)
|
api.ErrHandler(c, err)
|
||||||
return
|
return
|
||||||
|
|
|
@ -2,10 +2,15 @@ package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/0xJacky/Nginx-UI/api"
|
"github.com/0xJacky/Nginx-UI/api"
|
||||||
|
"github.com/0xJacky/Nginx-UI/internal/config"
|
||||||
|
"github.com/0xJacky/Nginx-UI/internal/helper"
|
||||||
"github.com/0xJacky/Nginx-UI/internal/nginx"
|
"github.com/0xJacky/Nginx-UI/internal/nginx"
|
||||||
|
"github.com/0xJacky/Nginx-UI/query"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/sashabaranov/go-openai"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type EditConfigJson struct {
|
type EditConfigJson struct {
|
||||||
|
@ -14,15 +19,39 @@ type EditConfigJson struct {
|
||||||
|
|
||||||
func EditConfig(c *gin.Context) {
|
func EditConfig(c *gin.Context) {
|
||||||
name := c.Param("name")
|
name := c.Param("name")
|
||||||
var request EditConfigJson
|
var json struct {
|
||||||
err := c.BindJSON(&request)
|
Name string `json:"name" binding:"required"`
|
||||||
if err != nil {
|
Filepath string `json:"filepath" binding:"required"`
|
||||||
api.ErrHandler(c, err)
|
NewFilepath string `json:"new_filepath" binding:"required"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
}
|
||||||
|
if !api.BindAndValid(c, &json) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
path := nginx.GetConfPath("/", name)
|
|
||||||
content := request.Content
|
|
||||||
|
|
||||||
|
path := json.Filepath
|
||||||
|
if !helper.IsUnderDirectory(path, nginx.GetConfPath()) {
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{
|
||||||
|
"message": "filepath is not under the nginx conf path",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !helper.IsUnderDirectory(json.NewFilepath, nginx.GetConfPath()) {
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{
|
||||||
|
"message": "new filepath is not under the nginx conf path",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{
|
||||||
|
"message": "file not found",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
content := json.Content
|
||||||
origContent, err := os.ReadFile(path)
|
origContent, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.ErrHandler(c, err)
|
api.ErrHandler(c, err)
|
||||||
|
@ -37,8 +66,28 @@ func EditConfig(c *gin.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
output := nginx.Reload()
|
g := query.ChatGPTLog
|
||||||
|
|
||||||
|
// handle rename
|
||||||
|
if path != json.NewFilepath {
|
||||||
|
if helper.FileExists(json.NewFilepath) {
|
||||||
|
c.JSON(http.StatusNotAcceptable, gin.H{
|
||||||
|
"message": "File exists",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err := os.Rename(json.Filepath, json.NewFilepath)
|
||||||
|
if err != nil {
|
||||||
|
api.ErrHandler(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// update ChatGPT record
|
||||||
|
_, _ = g.Where(g.Name.Eq(json.NewFilepath)).Delete()
|
||||||
|
_, _ = g.Where(g.Name.Eq(path)).Update(g.Name, json.NewFilepath)
|
||||||
|
}
|
||||||
|
|
||||||
|
output := nginx.Reload()
|
||||||
if nginx.GetLogLevel(output) >= nginx.Warn {
|
if nginx.GetLogLevel(output) >= nginx.Warn {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
"message": output,
|
"message": output,
|
||||||
|
@ -46,5 +95,21 @@ func EditConfig(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
GetConfig(c)
|
chatgpt, err := g.Where(g.Name.Eq(json.NewFilepath)).FirstOrCreate()
|
||||||
|
if err != nil {
|
||||||
|
api.ErrHandler(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if chatgpt.Content == nil {
|
||||||
|
chatgpt.Content = make([]openai.ChatCompletionMessage, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, config.Config{
|
||||||
|
Name: name,
|
||||||
|
Content: content,
|
||||||
|
ChatGPTMessages: chatgpt.Content,
|
||||||
|
FilePath: json.NewFilepath,
|
||||||
|
ModifiedAt: time.Now(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,4 +7,5 @@ func InitRouter(r *gin.RouterGroup) {
|
||||||
r.GET("config/*name", GetConfig)
|
r.GET("config/*name", GetConfig)
|
||||||
r.POST("config", AddConfig)
|
r.POST("config", AddConfig)
|
||||||
r.POST("config/*name", EditConfig)
|
r.POST("config/*name", EditConfig)
|
||||||
|
r.GET("config_base_path", GetBasePath)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import Curd from '@/api/curd'
|
import Curd from '@/api/curd'
|
||||||
import type { ChatComplicationMessage } from '@/api/openai'
|
import type { ChatComplicationMessage } from '@/api/openai'
|
||||||
|
import http from '@/lib/http'
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
name: string
|
name: string
|
||||||
|
@ -9,6 +10,16 @@ export interface Config {
|
||||||
modified_at: string
|
modified_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const config: Curd<Config> = new Curd('/config')
|
class ConfigCurd extends Curd<Config> {
|
||||||
|
constructor() {
|
||||||
|
super('/config')
|
||||||
|
}
|
||||||
|
|
||||||
|
get_base_path() {
|
||||||
|
return http.get('/config_base_path')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const config: ConfigCurd = new ConfigCurd()
|
||||||
|
|
||||||
export default config
|
export default config
|
||||||
|
|
|
@ -104,13 +104,24 @@ export const routes: RouteRecordRaw[] = [
|
||||||
hideChildren: true,
|
hideChildren: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'config/add',
|
||||||
|
name: 'Add Configuration',
|
||||||
|
component: () => import('@/views/config/ConfigEditor.vue'),
|
||||||
|
meta: {
|
||||||
|
name: () => $gettext('Add Configuration'),
|
||||||
|
hiddenInSidebar: true,
|
||||||
|
lastRouteName: 'Manage Configs',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'config/:name+/edit',
|
path: 'config/:name+/edit',
|
||||||
name: 'Edit Configuration',
|
name: 'Edit Configuration',
|
||||||
component: () => import('@/views/config/ConfigEdit.vue'),
|
component: () => import('@/views/config/ConfigEditor.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
name: () => $gettext('Edit Configuration'),
|
name: () => $gettext('Edit Configuration'),
|
||||||
hiddenInSidebar: true,
|
hiddenInSidebar: true,
|
||||||
|
lastRouteName: 'Manage Configs',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -3,20 +3,18 @@ import StdTable from '@/components/StdDesign/StdDataDisplay/StdTable.vue'
|
||||||
import config from '@/api/config'
|
import config from '@/api/config'
|
||||||
import configColumns from '@/views/config/config'
|
import configColumns from '@/views/config/config'
|
||||||
import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
|
import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
|
||||||
import router from '@/routes'
|
|
||||||
import InspectConfig from '@/views/config/InspectConfig.vue'
|
import InspectConfig from '@/views/config/InspectConfig.vue'
|
||||||
|
|
||||||
const api = config
|
const table = ref()
|
||||||
|
|
||||||
const table = ref(null)
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
const basePath = computed(() => {
|
const basePath = computed(() => {
|
||||||
let dir = route?.query?.dir ?? ''
|
let dir = route?.query?.dir ?? ''
|
||||||
if (dir)
|
if (dir)
|
||||||
dir += '/'
|
dir += '/'
|
||||||
|
|
||||||
return dir
|
return dir as string
|
||||||
})
|
})
|
||||||
|
|
||||||
const getParams = computed(() => {
|
const getParams = computed(() => {
|
||||||
|
@ -36,15 +34,32 @@ const refInspectConfig = ref()
|
||||||
watch(route, () => {
|
watch(route, () => {
|
||||||
refInspectConfig.value?.test()
|
refInspectConfig.value?.test()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
router.push({
|
||||||
|
path: '/config',
|
||||||
|
query: {
|
||||||
|
dir: `${basePath.value.split('/').slice(0, -2).join('/')}` || undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ACard :title="$gettext('Configurations')">
|
<ACard :title="$gettext('Configurations')">
|
||||||
|
<template #extra>
|
||||||
|
<a
|
||||||
|
@click="router.push({
|
||||||
|
path: '/config/add',
|
||||||
|
query: { basePath: basePath || undefined },
|
||||||
|
})"
|
||||||
|
>{{ $gettext('Add') }}</a>
|
||||||
|
</template>
|
||||||
<InspectConfig ref="refInspectConfig" />
|
<InspectConfig ref="refInspectConfig" />
|
||||||
<StdTable
|
<StdTable
|
||||||
:key="update"
|
:key="update"
|
||||||
ref="table"
|
ref="table"
|
||||||
:api="api"
|
:api="config"
|
||||||
:columns="configColumns"
|
:columns="configColumns"
|
||||||
disable-delete
|
disable-delete
|
||||||
disable-search
|
disable-search
|
||||||
|
@ -68,7 +83,7 @@ watch(route, () => {
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
<FooterToolBar v-if="basePath">
|
<FooterToolBar v-if="basePath">
|
||||||
<AButton @click="router.go(-1)">
|
<AButton @click="goBack">
|
||||||
{{ $gettext('Back') }}
|
{{ $gettext('Back') }}
|
||||||
</AButton>
|
</AButton>
|
||||||
</FooterToolBar>
|
</FooterToolBar>
|
||||||
|
|
|
@ -1,160 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { useRoute } from 'vue-router'
|
|
||||||
import { message } from 'ant-design-vue'
|
|
||||||
import type { Ref } from 'vue'
|
|
||||||
import { formatDateTime } from '@/lib/helper'
|
|
||||||
import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
|
|
||||||
import config from '@/api/config'
|
|
||||||
import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
|
|
||||||
import ngx from '@/api/ngx'
|
|
||||||
import InspectConfig from '@/views/config/InspectConfig.vue'
|
|
||||||
import ChatGPT from '@/components/ChatGPT/ChatGPT.vue'
|
|
||||||
import type { ChatComplicationMessage } from '@/api/openai'
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
|
|
||||||
const inspect_config = ref()
|
|
||||||
|
|
||||||
const name = computed(() => {
|
|
||||||
const n = route.params.name
|
|
||||||
if (typeof n === 'string')
|
|
||||||
return n
|
|
||||||
|
|
||||||
return n?.join('/')
|
|
||||||
})
|
|
||||||
|
|
||||||
const configText = ref('')
|
|
||||||
const history_chatgpt_record = ref([]) as Ref<ChatComplicationMessage[]>
|
|
||||||
const filepath = ref('')
|
|
||||||
const active_key = ref(['1', '2'])
|
|
||||||
const modified_at = ref('')
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
if (name.value) {
|
|
||||||
config.get(name.value).then(r => {
|
|
||||||
configText.value = r.content
|
|
||||||
history_chatgpt_record.value = r.chatgpt_messages
|
|
||||||
filepath.value = r.filepath
|
|
||||||
modified_at.value = r.modified_at
|
|
||||||
}).catch(r => {
|
|
||||||
message.error(r.message ?? $gettext('Server error'))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
configText.value = ''
|
|
||||||
history_chatgpt_record.value = []
|
|
||||||
filepath.value = ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init()
|
|
||||||
|
|
||||||
function save() {
|
|
||||||
config.save(name.value, { content: configText.value }).then(r => {
|
|
||||||
configText.value = r.content
|
|
||||||
message.success($gettext('Saved successfully'))
|
|
||||||
}).catch(r => {
|
|
||||||
message.error($gettext('Save error %{msg}', { msg: r.message ?? '' }))
|
|
||||||
}).finally(() => {
|
|
||||||
inspect_config.value.test()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function format_code() {
|
|
||||||
ngx.format_code(configText.value).then(r => {
|
|
||||||
configText.value = r.content
|
|
||||||
message.success($gettext('Format successfully'))
|
|
||||||
}).catch(r => {
|
|
||||||
message.error($gettext('Format error %{msg}', { msg: r.message ?? '' }))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<ARow :gutter="16">
|
|
||||||
<ACol
|
|
||||||
:xs="24"
|
|
||||||
:sm="24"
|
|
||||||
:md="18"
|
|
||||||
>
|
|
||||||
<ACard :title="$gettext('Edit Configuration')">
|
|
||||||
<InspectConfig ref="inspect_config" />
|
|
||||||
<CodeEditor v-model:content="configText" />
|
|
||||||
<FooterToolBar>
|
|
||||||
<ASpace>
|
|
||||||
<AButton @click="$router.go(-1)">
|
|
||||||
{{ $gettext('Back') }}
|
|
||||||
</AButton>
|
|
||||||
<AButton @click="format_code">
|
|
||||||
{{ $gettext('Format Code') }}
|
|
||||||
</AButton>
|
|
||||||
<AButton
|
|
||||||
type="primary"
|
|
||||||
@click="save"
|
|
||||||
>
|
|
||||||
{{ $gettext('Save') }}
|
|
||||||
</AButton>
|
|
||||||
</ASpace>
|
|
||||||
</FooterToolBar>
|
|
||||||
</ACard>
|
|
||||||
</ACol>
|
|
||||||
|
|
||||||
<ACol
|
|
||||||
:xs="24"
|
|
||||||
:sm="24"
|
|
||||||
:md="6"
|
|
||||||
>
|
|
||||||
<ACard class="col-right">
|
|
||||||
<ACollapse
|
|
||||||
v-model:activeKey="active_key"
|
|
||||||
ghost
|
|
||||||
>
|
|
||||||
<ACollapsePanel
|
|
||||||
key="1"
|
|
||||||
:header="$gettext('Basic')"
|
|
||||||
>
|
|
||||||
<AForm layout="vertical">
|
|
||||||
<AFormItem :label="$gettext('Path')">
|
|
||||||
{{ filepath }}
|
|
||||||
</AFormItem>
|
|
||||||
<AFormItem :label="$gettext('Updated at')">
|
|
||||||
{{ formatDateTime(modified_at) }}
|
|
||||||
</AFormItem>
|
|
||||||
</AForm>
|
|
||||||
</ACollapsePanel>
|
|
||||||
<ACollapsePanel
|
|
||||||
key="2"
|
|
||||||
header="ChatGPT"
|
|
||||||
>
|
|
||||||
<ChatGPT
|
|
||||||
v-model:history-messages="history_chatgpt_record"
|
|
||||||
:content="configText"
|
|
||||||
:path="filepath"
|
|
||||||
/>
|
|
||||||
</ACollapsePanel>
|
|
||||||
</ACollapse>
|
|
||||||
</ACard>
|
|
||||||
</ACol>
|
|
||||||
</ARow>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
.col-right {
|
|
||||||
position: sticky;
|
|
||||||
top: 78px;
|
|
||||||
|
|
||||||
:deep(.ant-card-body) {
|
|
||||||
max-height: 100vh;
|
|
||||||
overflow-y: scroll;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.ant-collapse-ghost > .ant-collapse-item > .ant-collapse-content > .ant-collapse-content-box) {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.ant-collapse > .ant-collapse-item > .ant-collapse-header) {
|
|
||||||
padding: 0 0 10px 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
232
app/src/views/config/ConfigEditor.vue
Normal file
232
app/src/views/config/ConfigEditor.vue
Normal file
|
@ -0,0 +1,232 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import type { Ref } from 'vue'
|
||||||
|
import { formatDateTime } from '@/lib/helper'
|
||||||
|
import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
|
||||||
|
import config from '@/api/config'
|
||||||
|
import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
|
||||||
|
import ngx from '@/api/ngx'
|
||||||
|
import InspectConfig from '@/views/config/InspectConfig.vue'
|
||||||
|
import ChatGPT from '@/components/ChatGPT/ChatGPT.vue'
|
||||||
|
import type { ChatComplicationMessage } from '@/api/openai'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const refForm = ref()
|
||||||
|
const refInspectConfig = ref()
|
||||||
|
const origName = ref('')
|
||||||
|
const addMode = computed(() => !route.params.name)
|
||||||
|
const errors = ref({})
|
||||||
|
|
||||||
|
const basePath = computed(() => {
|
||||||
|
if (route.query.basePath)
|
||||||
|
return route?.query?.basePath?.toString().replaceAll('/', '')
|
||||||
|
else if (typeof route.params.name === 'object')
|
||||||
|
return (route.params.name as string[]).slice(0, -1).join('/')
|
||||||
|
else
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = ref({
|
||||||
|
name: '',
|
||||||
|
content: '',
|
||||||
|
filepath: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const historyChatgptRecord = ref([]) as Ref<ChatComplicationMessage[]>
|
||||||
|
const activeKey = ref(['basic', 'chatgpt'])
|
||||||
|
const modifiedAt = ref('')
|
||||||
|
const nginxConfigBase = ref('')
|
||||||
|
|
||||||
|
const newPath = computed(() => [nginxConfigBase.value, basePath.value, data.value.name]
|
||||||
|
.filter(v => v)
|
||||||
|
.join('/'))
|
||||||
|
|
||||||
|
const relativePath = computed(() => (route.params.name as string[]).join('/'))
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
const { name } = route.params
|
||||||
|
|
||||||
|
data.value.name = name?.[name?.length - 1] ?? ''
|
||||||
|
origName.value = data.value.name
|
||||||
|
if (data.value.name) {
|
||||||
|
config.get(relativePath.value).then(r => {
|
||||||
|
data.value = r
|
||||||
|
historyChatgptRecord.value = r.chatgpt_messages
|
||||||
|
modifiedAt.value = r.modified_at
|
||||||
|
}).catch(r => {
|
||||||
|
message.error(r.message ?? $gettext('Server error'))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
data.value.content = ''
|
||||||
|
historyChatgptRecord.value = []
|
||||||
|
data.value.filepath = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await config.get_base_path().then(r => {
|
||||||
|
nginxConfigBase.value = r.base_path
|
||||||
|
})
|
||||||
|
await init()
|
||||||
|
})
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
refForm.value.validate().then(() => {
|
||||||
|
config.save(addMode.value ? null : relativePath.value, {
|
||||||
|
name: data.value.name,
|
||||||
|
filepath: data.value.filepath,
|
||||||
|
new_filepath: newPath.value,
|
||||||
|
content: data.value.content,
|
||||||
|
}).then(r => {
|
||||||
|
data.value.content = r.content
|
||||||
|
message.success($gettext('Saved successfully'))
|
||||||
|
router.push(`/config/${r.filepath.replaceAll(`${nginxConfigBase.value}/`, '')}/edit`)
|
||||||
|
}).catch(e => {
|
||||||
|
errors.value = e.errors
|
||||||
|
message.error($gettext('Save error %{msg}', { msg: e.message ?? '' }))
|
||||||
|
}).finally(() => {
|
||||||
|
refInspectConfig.value.test()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCode() {
|
||||||
|
ngx.format_code(data.value.content).then(r => {
|
||||||
|
data.value.content = r.content
|
||||||
|
message.success($gettext('Format successfully'))
|
||||||
|
}).catch(r => {
|
||||||
|
message.error($gettext('Format error %{msg}', { msg: r.message ?? '' }))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
router.push({
|
||||||
|
path: '/config',
|
||||||
|
query: {
|
||||||
|
dir: basePath.value || undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ARow :gutter="16">
|
||||||
|
<ACol
|
||||||
|
:xs="24"
|
||||||
|
:sm="24"
|
||||||
|
:md="18"
|
||||||
|
>
|
||||||
|
<ACard :title="addMode ? $gettext('Add Configuration') : $gettext('Edit Configuration')">
|
||||||
|
<InspectConfig
|
||||||
|
v-show="!addMode"
|
||||||
|
ref="refInspectConfig"
|
||||||
|
/>
|
||||||
|
<CodeEditor v-model:content="data.content" />
|
||||||
|
<FooterToolBar>
|
||||||
|
<ASpace>
|
||||||
|
<AButton @click="goBack">
|
||||||
|
{{ $gettext('Back') }}
|
||||||
|
</AButton>
|
||||||
|
<AButton @click="formatCode">
|
||||||
|
{{ $gettext('Format Code') }}
|
||||||
|
</AButton>
|
||||||
|
<AButton
|
||||||
|
type="primary"
|
||||||
|
@click="save"
|
||||||
|
>
|
||||||
|
{{ $gettext('Save') }}
|
||||||
|
</AButton>
|
||||||
|
</ASpace>
|
||||||
|
</FooterToolBar>
|
||||||
|
</ACard>
|
||||||
|
</ACol>
|
||||||
|
|
||||||
|
<ACol
|
||||||
|
:xs="24"
|
||||||
|
:sm="24"
|
||||||
|
:md="6"
|
||||||
|
>
|
||||||
|
<ACard class="col-right">
|
||||||
|
<ACollapse
|
||||||
|
v-model:activeKey="activeKey"
|
||||||
|
ghost
|
||||||
|
>
|
||||||
|
<ACollapsePanel
|
||||||
|
key="basic"
|
||||||
|
:header="$gettext('Basic')"
|
||||||
|
>
|
||||||
|
<AForm
|
||||||
|
ref="refForm"
|
||||||
|
layout="vertical"
|
||||||
|
:model="data"
|
||||||
|
:rules="{
|
||||||
|
name: [
|
||||||
|
{ required: true, message: $gettext('Please input a filename') },
|
||||||
|
{ pattern: /^[^\\/]+$/, message: $gettext('Invalid filename') },
|
||||||
|
],
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<AFormItem
|
||||||
|
name="name"
|
||||||
|
:label="$gettext('Name')"
|
||||||
|
>
|
||||||
|
<AInput v-model:value="data.name" />
|
||||||
|
</AFormItem>
|
||||||
|
<AFormItem
|
||||||
|
v-if="!addMode"
|
||||||
|
:label="$gettext('Path')"
|
||||||
|
>
|
||||||
|
{{ data.filepath }}
|
||||||
|
</AFormItem>
|
||||||
|
<AFormItem
|
||||||
|
v-show="data.name !== origName"
|
||||||
|
:label="addMode ? $gettext('New Path') : $gettext('Changed Path')"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
{{ newPath }}
|
||||||
|
</AFormItem>
|
||||||
|
<AFormItem
|
||||||
|
v-if="!addMode"
|
||||||
|
:label="$gettext('Updated at')"
|
||||||
|
>
|
||||||
|
{{ formatDateTime(modifiedAt) }}
|
||||||
|
</AFormItem>
|
||||||
|
</AForm>
|
||||||
|
</ACollapsePanel>
|
||||||
|
<ACollapsePanel
|
||||||
|
key="chatgpt"
|
||||||
|
header="ChatGPT"
|
||||||
|
>
|
||||||
|
<ChatGPT
|
||||||
|
v-model:history-messages="historyChatgptRecord"
|
||||||
|
:content="data.content"
|
||||||
|
:path="data.filepath"
|
||||||
|
/>
|
||||||
|
</ACollapsePanel>
|
||||||
|
</ACollapse>
|
||||||
|
</ACard>
|
||||||
|
</ACol>
|
||||||
|
</ARow>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.col-right {
|
||||||
|
position: sticky;
|
||||||
|
top: 78px;
|
||||||
|
|
||||||
|
:deep(.ant-card-body) {
|
||||||
|
max-height: 100vh;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-collapse-ghost > .ant-collapse-item > .ant-collapse-content > .ant-collapse-content-box) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-collapse > .ant-collapse-item > .ant-collapse-header) {
|
||||||
|
padding: 0 0 10px 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -7,7 +7,7 @@ import (
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Content string `json:"content,omitempty"`
|
Content string `json:"content"`
|
||||||
ChatGPTMessages []openai.ChatCompletionMessage `json:"chatgpt_messages,omitempty"`
|
ChatGPTMessages []openai.ChatCompletionMessage `json:"chatgpt_messages,omitempty"`
|
||||||
FilePath string `json:"filepath,omitempty"`
|
FilePath string `json:"filepath,omitempty"`
|
||||||
ModifiedAt time.Time `json:"modified_at"`
|
ModifiedAt time.Time `json:"modified_at"`
|
||||||
|
|
|
@ -8,4 +8,5 @@ import (
|
||||||
func TestIsUnderDirectory(t *testing.T) {
|
func TestIsUnderDirectory(t *testing.T) {
|
||||||
assert.Equal(t, true, IsUnderDirectory("/etc/nginx/nginx.conf", "/etc/nginx"))
|
assert.Equal(t, true, IsUnderDirectory("/etc/nginx/nginx.conf", "/etc/nginx"))
|
||||||
assert.Equal(t, false, IsUnderDirectory("../../root/nginx.conf", "/etc/nginx"))
|
assert.Equal(t, false, IsUnderDirectory("../../root/nginx.conf", "/etc/nginx"))
|
||||||
|
assert.Equal(t, false, IsUnderDirectory("/etc/nginx/../../root/nginx.conf", "/etc/nginx"))
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue