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 (
|
||||
"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/gin-gonic/gin"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
func AddConfig(c *gin.Context) {
|
||||
var request struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Content string `json:"content" binding:"required"`
|
||||
var json struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
NewFilepath string `json:"new_filepath" binding:"required"`
|
||||
Content string `json:"content"`
|
||||
Overwrite bool `json:"overwrite"`
|
||||
}
|
||||
|
||||
err := c.BindJSON(&request)
|
||||
if err != nil {
|
||||
api.ErrHandler(c, err)
|
||||
if !api.BindAndValid(c, &json) {
|
||||
return
|
||||
}
|
||||
|
||||
name := request.Name
|
||||
content := request.Content
|
||||
|
||||
path := nginx.GetConfPath("/", name)
|
||||
|
||||
if _, err = os.Stat(path); err == nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{
|
||||
"message": "config exist",
|
||||
name := json.Name
|
||||
content := json.Content
|
||||
path := json.NewFilepath
|
||||
if !helper.IsUnderDirectory(path, nginx.GetConfPath()) {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"message": "new filepath is not under the nginx conf path",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if content != "" {
|
||||
err = os.WriteFile(path, []byte(content), 0644)
|
||||
if err != nil {
|
||||
api.ErrHandler(c, err)
|
||||
return
|
||||
}
|
||||
if !json.Overwrite && helper.FileExists(path) {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{
|
||||
"message": "File exists",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
err := os.WriteFile(path, []byte(content), 0644)
|
||||
if err != nil {
|
||||
api.ErrHandler(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
output := nginx.Reload()
|
||||
|
@ -50,7 +56,10 @@ func AddConfig(c *gin.Context) {
|
|||
}
|
||||
|
||||
c.JSON(http.StatusOK, config.Config{
|
||||
Name: name,
|
||||
Content: content,
|
||||
Name: name,
|
||||
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 (
|
||||
"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/query"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
@ -15,16 +16,20 @@ func GetConfig(c *gin.Context) {
|
|||
name := c.Param("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)
|
||||
|
||||
if err != nil {
|
||||
api.ErrHandler(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(path)
|
||||
|
||||
if err != nil {
|
||||
api.ErrHandler(c, err)
|
||||
return
|
||||
|
@ -32,7 +37,6 @@ func GetConfig(c *gin.Context) {
|
|||
|
||||
g := query.ChatGPTLog
|
||||
chatgpt, err := g.Where(g.Name.Eq(path)).FirstOrCreate()
|
||||
|
||||
if err != nil {
|
||||
api.ErrHandler(c, err)
|
||||
return
|
||||
|
|
|
@ -2,10 +2,15 @@ package config
|
|||
|
||||
import (
|
||||
"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/query"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
type EditConfigJson struct {
|
||||
|
@ -14,15 +19,39 @@ type EditConfigJson struct {
|
|||
|
||||
func EditConfig(c *gin.Context) {
|
||||
name := c.Param("name")
|
||||
var request EditConfigJson
|
||||
err := c.BindJSON(&request)
|
||||
if err != nil {
|
||||
api.ErrHandler(c, err)
|
||||
var json struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Filepath string `json:"filepath" binding:"required"`
|
||||
NewFilepath string `json:"new_filepath" binding:"required"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
if !api.BindAndValid(c, &json) {
|
||||
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)
|
||||
if err != nil {
|
||||
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 {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"message": output,
|
||||
|
@ -46,5 +95,21 @@ func EditConfig(c *gin.Context) {
|
|||
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.POST("config", AddConfig)
|
||||
r.POST("config/*name", EditConfig)
|
||||
r.GET("config_base_path", GetBasePath)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Curd from '@/api/curd'
|
||||
import type { ChatComplicationMessage } from '@/api/openai'
|
||||
import http from '@/lib/http'
|
||||
|
||||
export interface Config {
|
||||
name: string
|
||||
|
@ -9,6 +10,16 @@ export interface Config {
|
|||
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
|
||||
|
|
|
@ -104,13 +104,24 @@ export const routes: RouteRecordRaw[] = [
|
|||
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',
|
||||
name: 'Edit Configuration',
|
||||
component: () => import('@/views/config/ConfigEdit.vue'),
|
||||
component: () => import('@/views/config/ConfigEditor.vue'),
|
||||
meta: {
|
||||
name: () => $gettext('Edit Configuration'),
|
||||
hiddenInSidebar: true,
|
||||
lastRouteName: 'Manage Configs',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -3,20 +3,18 @@ import StdTable from '@/components/StdDesign/StdDataDisplay/StdTable.vue'
|
|||
import config from '@/api/config'
|
||||
import configColumns from '@/views/config/config'
|
||||
import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
|
||||
import router from '@/routes'
|
||||
import InspectConfig from '@/views/config/InspectConfig.vue'
|
||||
|
||||
const api = config
|
||||
|
||||
const table = ref(null)
|
||||
const table = ref()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const basePath = computed(() => {
|
||||
let dir = route?.query?.dir ?? ''
|
||||
if (dir)
|
||||
dir += '/'
|
||||
|
||||
return dir
|
||||
return dir as string
|
||||
})
|
||||
|
||||
const getParams = computed(() => {
|
||||
|
@ -36,15 +34,32 @@ const refInspectConfig = ref()
|
|||
watch(route, () => {
|
||||
refInspectConfig.value?.test()
|
||||
})
|
||||
|
||||
function goBack() {
|
||||
router.push({
|
||||
path: '/config',
|
||||
query: {
|
||||
dir: `${basePath.value.split('/').slice(0, -2).join('/')}` || undefined,
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ACard :title="$gettext('Configurations')">
|
||||
<template #extra>
|
||||
<a
|
||||
@click="router.push({
|
||||
path: '/config/add',
|
||||
query: { basePath: basePath || undefined },
|
||||
})"
|
||||
>{{ $gettext('Add') }}</a>
|
||||
</template>
|
||||
<InspectConfig ref="refInspectConfig" />
|
||||
<StdTable
|
||||
:key="update"
|
||||
ref="table"
|
||||
:api="api"
|
||||
:api="config"
|
||||
:columns="configColumns"
|
||||
disable-delete
|
||||
disable-search
|
||||
|
@ -68,7 +83,7 @@ watch(route, () => {
|
|||
}"
|
||||
/>
|
||||
<FooterToolBar v-if="basePath">
|
||||
<AButton @click="router.go(-1)">
|
||||
<AButton @click="goBack">
|
||||
{{ $gettext('Back') }}
|
||||
</AButton>
|
||||
</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 {
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Content string `json:"content"`
|
||||
ChatGPTMessages []openai.ChatCompletionMessage `json:"chatgpt_messages,omitempty"`
|
||||
FilePath string `json:"filepath,omitempty"`
|
||||
ModifiedAt time.Time `json:"modified_at"`
|
||||
|
|
|
@ -8,4 +8,5 @@ import (
|
|||
func TestIsUnderDirectory(t *testing.T) {
|
||||
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("/etc/nginx/../../root/nginx.conf", "/etc/nginx"))
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue