feat: manage config support dir

This commit is contained in:
0xJacky 2023-01-02 18:38:40 +08:00
parent 6bcb575442
commit 65b192c8be
No known key found for this signature in database
GPG key ID: B6E4A6E4A561BAF0
5 changed files with 231 additions and 162 deletions

View file

@ -74,7 +74,17 @@ export const routes = [
} }
}, },
{ {
path: 'config/:name', path: 'config/:dir*',
name: () => $gettext('Manage Configs'),
component: () => import('@/views/config/Config.vue'),
meta: {
icon: FileOutlined,
hideChildren: true,
hiddenInSidebar: true
}
},
{
path: 'config/:name+/edit',
name: () => $gettext('Edit Configuration'), name: () => $gettext('Edit Configuration'),
component: () => import('@/views/config/ConfigEdit.vue'), component: () => import('@/views/config/ConfigEdit.vue'),
meta: { meta: {

View file

@ -2,43 +2,66 @@
import StdTable from '@/components/StdDataDisplay/StdTable.vue' import StdTable from '@/components/StdDataDisplay/StdTable.vue'
import gettext from '@/gettext' import gettext from '@/gettext'
import config from '@/api/config' import config from '@/api/config'
import {datetime} from '@/components/StdDataDisplay/StdTableTransformer' import {customRender, datetime} from '@/components/StdDataDisplay/StdTableTransformer'
import {computed, h, nextTick, ref, watch} from 'vue'
const {$gettext} = gettext const {$gettext} = gettext
const api = config const api = config
const columns = [{ import configColumns from '@/views/config/config'
title: () => $gettext('Name'), import {useRoute} from 'vue-router'
dataIndex: 'name', import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
sorter: true, import router from '@/routes'
pithy: true
}, { const table = ref(null)
title: () => $gettext('Updated at'), const route = useRoute()
dataIndex: 'modify',
customRender: datetime, const basePath = computed(() => {
datetime: true, let dir = route?.params?.dir ? (route?.params?.dir as string[])?.join('/') : ''
sorter: true, if (dir) dir += '/'
pithy: true return dir
}, { })
title: () => $gettext('Action'),
dataIndex: 'action' const get_params = computed(() => {
}] return {
dir: basePath.value
}
})
const update = ref(1)
watch(get_params, () => {
update.value++
})
</script> </script>
<template> <template>
<a-card :title="$gettext('Configurations')"> <a-card :title="$gettext('Configurations')">
<std-table <std-table
:key="update"
ref="table"
:api="api" :api="api"
:columns="columns" :columns="configColumns"
:deletable="false" :deletable="false"
:disable_search="true" :disable_search="true"
row-key="name" row-key="name"
@clickEdit="r => { :get_params="get_params"
$router.push({ @clickEdit="(r, row) => {
path: '/config/' + r if (!row.is_dir) {
}) $router.push({
path: '/config/' + basePath + r + '/edit'
})
} else {
$router.push({
path: '/config/' + basePath + r
})
}
}" }"
/> />
<footer-tool-bar v-if="basePath">
<a-button @click="router.go(-1)">{{ $gettext('Back') }}</a-button>
</footer-tool-bar>
</a-card> </a-card>
</template> </template>

View file

@ -2,7 +2,7 @@
import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue' import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
import gettext from '@/gettext' import gettext from '@/gettext'
import {useRoute} from 'vue-router' import {useRoute} from 'vue-router'
import {ref} from 'vue' import {computed, ref} from 'vue'
import config from '@/api/config' import config from '@/api/config'
import {message} from 'ant-design-vue' import {message} from 'ant-design-vue'
import CodeEditor from '@/components/CodeEditor/CodeEditor.vue' import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
@ -10,7 +10,13 @@ import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
const {$gettext, interpolate} = gettext const {$gettext, interpolate} = gettext
const route = useRoute() const route = useRoute()
const name = ref(route.params.name) const name = computed(() => {
const n = route.params.name
if (typeof n === 'string') {
return n
}
return n?.join('/')
})
const configText = ref('') const configText = ref('')
@ -46,7 +52,7 @@ function save() {
<footer-tool-bar> <footer-tool-bar>
<a-space> <a-space>
<a-button @click="$router.go(-1)"> <a-button @click="$router.go(-1)">
<translate>Cancel</translate> <translate>Back</translate>
</a-button> </a-button>
<a-button type="primary" @click="save"> <a-button type="primary" @click="save">
<translate>Save</translate> <translate>Save</translate>

View file

@ -0,0 +1,41 @@
import {customRender, datetime} from '@/components/StdDataDisplay/StdTableTransformer'
import gettext from '@/gettext'
const {$gettext} = gettext
import {Badge} from 'ant-design-vue'
import {h} from 'vue'
const configColumns = [{
title: () => $gettext('Name'),
dataIndex: 'name',
sorter: true,
pithy: true
}, {
title: () => $gettext('Type'),
dataIndex: 'is_dir',
customRender: (args: customRender) => {
const template: any = []
const {text, column} = args
if (text === true || text > 0) {
template.push($gettext('Dir'))
} else {
template.push($gettext('File'))
}
return h('div', template)
},
sorter: true,
pithy: true
}, {
title: () => $gettext('Updated at'),
dataIndex: 'modify',
customRender: datetime,
datetime: true,
sorter: true,
pithy: true
}, {
title: () => $gettext('Action'),
dataIndex: 'action'
}]
export default configColumns

View file

@ -1,184 +1,173 @@
package api package api
import ( import (
"github.com/0xJacky/Nginx-UI/server/pkg/config_list" "github.com/0xJacky/Nginx-UI/server/pkg/config_list"
"github.com/0xJacky/Nginx-UI/server/pkg/nginx" "github.com/0xJacky/Nginx-UI/server/pkg/nginx"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"log" "log"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
) )
func GetConfigs(c *gin.Context) { func GetConfigs(c *gin.Context) {
orderBy := c.Query("order_by") orderBy := c.Query("order_by")
sort := c.DefaultQuery("sort", "desc") sort := c.DefaultQuery("sort", "desc")
dir := c.DefaultQuery("dir", "/")
mySort := map[string]string{ mySort := map[string]string{
"name": "string", "name": "string",
"modify": "time", "modify": "time",
} "is_dir": "bool",
}
configFiles, err := os.ReadDir(nginx.GetNginxConfPath("/")) configFiles, err := os.ReadDir(nginx.GetNginxConfPath(dir))
if err != nil { if err != nil {
ErrHandler(c, err) ErrHandler(c, err)
return return
} }
var configs []gin.H var configs []gin.H
for i := range configFiles { for i := range configFiles {
file := configFiles[i] file := configFiles[i]
fileInfo, _ := file.Info() fileInfo, _ := file.Info()
switch mode := fileInfo.Mode(); { switch mode := fileInfo.Mode(); {
case mode.IsRegular(): // regular file, not a hidden file case mode.IsRegular(): // regular file, not a hidden file
if "." == file.Name()[0:1] { if "." == file.Name()[0:1] {
continue continue
} }
case mode&os.ModeSymlink != 0: // is a symbol case mode&os.ModeSymlink != 0: // is a symbol
var targetPath string var targetPath string
targetPath, err = os.Readlink(nginx.GetNginxConfPath(file.Name())) targetPath, err = os.Readlink(nginx.GetNginxConfPath(file.Name()))
if err != nil { if err != nil {
log.Println("GetConfigs Read Symlink Error", targetPath, err) log.Println("GetConfigs Read Symlink Error", targetPath, err)
continue continue
} }
}
var targetInfo os.FileInfo configs = append(configs, gin.H{
targetInfo, err = os.Stat(targetPath) "name": file.Name(),
if err != nil { "size": fileInfo.Size(),
log.Println("GetConfigs Stat Error", targetPath, err) "modify": fileInfo.ModTime(),
continue "is_dir": file.IsDir(),
} })
// but target file is not a dir }
if targetInfo.IsDir() {
continue
}
default:
continue
}
configs = append(configs, gin.H{ configs = config_list.Sort(orderBy, sort, mySort[orderBy], configs)
"name": file.Name(),
"size": fileInfo.Size(),
"modify": fileInfo.ModTime(),
})
}
configs = config_list.Sort(orderBy, sort, mySort[orderBy], configs) c.JSON(http.StatusOK, gin.H{
"data": configs,
c.JSON(http.StatusOK, gin.H{ })
"data": configs,
})
} }
func GetConfig(c *gin.Context) { func GetConfig(c *gin.Context) {
name := c.Param("name") name := c.Param("name")
path := filepath.Join(nginx.GetNginxConfPath("/"), name) path := filepath.Join(nginx.GetNginxConfPath("/"), name)
content, err := os.ReadFile(path) content, err := os.ReadFile(path)
if err != nil { if err != nil {
ErrHandler(c, err) ErrHandler(c, err)
return return
} }
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"config": string(content), "config": string(content),
}) })
} }
type AddConfigJson struct { type AddConfigJson struct {
Name string `json:"name" binding:"required"` Name string `json:"name" binding:"required"`
Content string `json:"content" binding:"required"` Content string `json:"content" binding:"required"`
} }
func AddConfig(c *gin.Context) { func AddConfig(c *gin.Context) {
var request AddConfigJson var request AddConfigJson
err := c.BindJSON(&request) err := c.BindJSON(&request)
if err != nil { if err != nil {
ErrHandler(c, err) ErrHandler(c, err)
return return
} }
name := request.Name name := request.Name
content := request.Content content := request.Content
path := filepath.Join(nginx.GetNginxConfPath("/"), name) path := filepath.Join(nginx.GetNginxConfPath("/"), name)
log.Println(path) if _, err = os.Stat(path); err == nil {
if _, err = os.Stat(path); err == nil { c.JSON(http.StatusNotAcceptable, gin.H{
c.JSON(http.StatusNotAcceptable, gin.H{ "message": "config exist",
"message": "config exist", })
}) return
return }
}
if content != "" { if content != "" {
err = os.WriteFile(path, []byte(content), 0644) err = os.WriteFile(path, []byte(content), 0644)
if err != nil { if err != nil {
ErrHandler(c, err) ErrHandler(c, err)
return return
} }
} }
output := nginx.ReloadNginx() output := nginx.ReloadNginx()
if output != "" && strings.Contains(output, "error") { if output != "" && strings.Contains(output, "error") {
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
"message": output, "message": output,
}) })
return return
} }
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"name": name, "name": name,
"content": content, "content": content,
}) })
} }
type EditConfigJson struct { type EditConfigJson struct {
Content string `json:"content" binding:"required"` Content string `json:"content" binding:"required"`
} }
func EditConfig(c *gin.Context) { func EditConfig(c *gin.Context) {
name := c.Param("name") name := c.Param("name")
var request EditConfigJson var request EditConfigJson
err := c.BindJSON(&request) err := c.BindJSON(&request)
if err != nil { if err != nil {
ErrHandler(c, err) ErrHandler(c, err)
return return
} }
path := filepath.Join(nginx.GetNginxConfPath("/"), name) path := filepath.Join(nginx.GetNginxConfPath("/"), name)
content := request.Content content := request.Content
origContent, err := os.ReadFile(path) origContent, err := os.ReadFile(path)
if err != nil { if err != nil {
ErrHandler(c, err) ErrHandler(c, err)
return return
} }
if content != "" && content != string(origContent) { if content != "" && content != string(origContent) {
// model.CreateBackup(path) // model.CreateBackup(path)
err = os.WriteFile(path, []byte(content), 0644) err = os.WriteFile(path, []byte(content), 0644)
if err != nil { if err != nil {
ErrHandler(c, err) ErrHandler(c, err)
return return
} }
} }
output := nginx.ReloadNginx() output := nginx.ReloadNginx()
if output != "" && strings.Contains(output, "error") { if output != "" && strings.Contains(output, "error") {
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
"message": output, "message": output,
}) })
return return
} }
GetConfig(c) GetConfig(c)
} }