Browse Source

[frontend-next] bug fix

0xJacky 2 years ago
parent
commit
070c53b0b2

+ 0 - 1
frontend/.env.development

@@ -1,2 +1 @@
 VITE_API_ROOT = /api
-VITE_API_WSS_ROOT = wss://nginx.jackyu.cn/api

+ 1 - 2
frontend/.env.production

@@ -1,2 +1 @@
-VUE_APP_API_ROOT = /api
-VUE_APP_API_WSS_ROOT = /api
+VITE_API_ROOT = /api

+ 1 - 1
frontend/frontend.go

@@ -4,5 +4,5 @@ import (
 	"embed"
 )
 
-//go:embed dist
+//go:embed dist/* dist/*/*
 var DistFS embed.FS

+ 0 - 18
frontend/public/vite.svg

@@ -1,18 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img"
-     class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257">
-    <defs>
-        <linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%">
-            <stop offset="0%" stop-color="#41D1FF"></stop>
-            <stop offset="100%" stop-color="#BD34FE"></stop>
-        </linearGradient>
-        <linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%">
-            <stop offset="0%" stop-color="#FFEA83"></stop>
-            <stop offset="8.333%" stop-color="#FFDD35"></stop>
-            <stop offset="100%" stop-color="#FFA800"></stop>
-        </linearGradient>
-    </defs>
-    <path fill="url(#IconifyId1813088fe1fbc01fb466)"
-          d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path>
-    <path fill="url(#IconifyId1813088fe1fbc01fb467)"
-          d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path>
-</svg>

+ 13 - 20
frontend/src/components/StdDataDisplay/StdPagination.vue

@@ -1,8 +1,19 @@
+<script setup lang="ts">
+import {useGettext} from 'vue3-gettext'
+
+const {pagination, size} = defineProps(['pagination', 'size'])
+const emit = defineEmits(['changePage'])
+const {$gettext} = useGettext()
+
+function changePage(num: number) {
+    emit('changePage', num)
+}
+</script>
+
 <template>
-    <div v-if="Object.keys(pagination).length !== 0">
+    <div v-if="pagination.total>pagination.per_page">
         <a-pagination
             :current="pagination.current_page"
-            :hideOnSinglePage="true"
             :pageSize="pagination.per_page"
             :size="size"
             :total="pagination.total"
@@ -10,27 +21,9 @@
             class="pagination"
             @change="changePage"
         />
-        <div class="clear"></div>
     </div>
 </template>
 
-<script>
-export default {
-    name: 'StdPagination',
-    props: {
-        pagination: Object,
-        size: {
-            default: ''
-        }
-    },
-    methods: {
-        changePage(num) {
-            return this.$emit('changePage', num)
-        }
-    }
-}
-</script>
-
 <style lang="less">
 .ant-pagination-total-text {
     @media (max-width: 450px) {

+ 12 - 7
frontend/src/components/StdDataDisplay/StdTable.vue

@@ -5,7 +5,7 @@ const {$gettext, interpolate} = gettext
 
 import StdDataEntry from '@/components/StdDataEntry'
 import StdPagination from './StdPagination.vue'
-import {nextTick, reactive, ref} from 'vue'
+import {nextTick, reactive, ref, watch} from 'vue'
 import {useRoute, useRouter} from 'vue-router'
 import {message} from 'ant-design-vue'
 
@@ -61,9 +61,9 @@ const props = defineProps({
 })
 
 
-const data_source = reactive([])
+const data_source = ref([])
 const loading = ref(true)
-const pagination = ({
+const pagination = reactive({
     total: 1,
     per_page: 10,
     current_page: 1,
@@ -80,7 +80,6 @@ const rowSelection = reactive({})
 const searchColumns = getSearchColumns()
 const pithyColumns = getPithyColumns()
 
-
 get_list()
 
 defineExpose({
@@ -102,7 +101,7 @@ function get_list(page_num = null) {
         params['page'] = page_num
     }
     props.api!.get_list(params).then((r: any) => {
-        Object.assign(data_source, r.data)
+        data_source.value = r.data
 
         if (r.pagination !== undefined) {
             Object.assign(pagination, r.pagination)
@@ -161,10 +160,17 @@ function onSelect(record: any) {
 const router = useRouter()
 
 const reset_search = async () => {
-    params = reactive({})
+    Object.keys(params).forEach(v => {
+        delete params[v]
+    })
     router.push({query: {}}).catch(() => {
     })
 }
+
+watch(params, () => {
+    router.push({query: params})
+    get_list()
+})
 </script>
 
 <template>
@@ -212,7 +218,6 @@ const reset_search = async () => {
                     </template>
                 </template>
             </template>
-
         </a-table>
         <std-pagination :pagination="pagination" @changePage="get_list"/>
     </div>

+ 4 - 0
frontend/src/dark.less

@@ -1 +1,5 @@
 @import "ant-design-vue/dist/antd.dark";
+
+.directive-editor-extra {
+    background-color: rgba(0, 0, 0, 0.84) !important;
+}

+ 1 - 1
frontend/src/main.ts

@@ -3,7 +3,7 @@ import {createPinia} from 'pinia'
 import gettext from './gettext'
 import App from './App.vue'
 import router from './routes'
-import 'ant-design-vue/dist/antd.less'
+//import 'ant-design-vue/dist/antd.less'
 import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
 import {useSettingsStore} from '@/pinia'
 

+ 1 - 3
frontend/src/style.less

@@ -1,3 +1 @@
-@import "ant-design-vue/dist/antd.variable";
-
-@border-radius-base: 4px;
+@import 'ant-design-vue/dist/antd.less';

+ 28 - 30
frontend/src/views/domain/cert/Cert.vue

@@ -1,44 +1,42 @@
+<script setup lang="ts">
+import CertInfo from '@/views/domain/cert/CertInfo.vue'
+import IssueCert from '@/views/domain/cert/IssueCert.vue'
+import {computed, ref} from 'vue'
+
+const {directivesMap, current_server_directives, enabled} = defineProps<{
+    directivesMap: any
+    current_server_directives: Array<any>
+    enabled: boolean
+}>()
+
+const info = ref(null)
+
+interface Info {
+    get(): void
+}
+
+function callback() {
+    const t: Info | null = info.value
+    t!.get()
+}
+
+const name = computed(() => {
+    return directivesMap['server_name'][0].params.trim()
+})
+</script>
+
 <template>
     <div>
         <cert-info ref="info" :domain="name" v-if="name"/>
         <issue-cert
             :current_server_directives="current_server_directives"
             :directives-map="directivesMap"
-            v-model="auto_cert"
+            v-model:enabled="enabled"
             @callback="callback"
         />
     </div>
 </template>
 
-<script>
-import CertInfo from '@/views/domain/cert/CertInfo'
-import IssueCert from '@/views/domain/cert/IssueCert'
-
-export default {
-    name: 'Cert',
-    components: {IssueCert, CertInfo},
-    props: {
-        directivesMap: Object,
-        current_server_directives: Array,
-        auto_cert: Boolean
-    },
-    model: {
-        prop: 'auto_cert',
-        event: 'change_auto_cert'
-    },
-    methods: {
-        callback() {
-            this.$refs.info.get()
-        }
-    },
-    computed: {
-        name() {
-            return this.directivesMap['server_name'][0].params.trim()
-        }
-    }
-}
-</script>
-
 <style scoped>
 
 </style>

+ 4 - 0
frontend/src/views/domain/cert/CertInfo.vue

@@ -19,6 +19,10 @@ function get() {
         ok.value = false
     })
 }
+
+defineExpose({
+    get
+})
 </script>
 
 <template>

+ 116 - 119
frontend/src/views/domain/cert/IssueCert.vue

@@ -1,9 +1,123 @@
+<script setup lang="ts">
+import {issue_cert} from '../methods'
+import {useGettext} from 'vue3-gettext'
+import {computed, nextTick, ref, watch} from 'vue'
+import {message} from 'ant-design-vue'
+import domain from '@/api/domain'
+
+const {$gettext, interpolate} = useGettext()
+
+const {directivesMap, current_server_directives, enabled} = defineProps<{
+    directivesMap: any
+    current_server_directives: Array<any>
+    enabled: boolean
+}>()
+
+const emit = defineEmits(['changeEnabled', 'callback', 'update:enabled'])
+
+const issuing_cert = ref(false)
+
+function onchange(r: boolean) {
+    emit('changeEnabled', r)
+    change_auto_cert(r)
+    if (r) {
+        job()
+    }
+}
+
+function job() {
+    issuing_cert.value = true
+
+    if (no_server_name.value) {
+        message.error($gettext('server_name not found in directives'))
+        issuing_cert.value = false
+        return
+    }
+
+    if (server_name_more_than_one.value) {
+        message.error($gettext('server_name parameters more than one'))
+        issuing_cert.value = false
+        return
+    }
+
+    const server_name = directivesMap['server_name'][0]
+
+    if (!directivesMap['ssl_certificate']) {
+        current_server_directives.splice(server_name.idx + 1, 0, {
+            directive: 'ssl_certificate',
+            params: ''
+        })
+    }
+
+    nextTick(() => {
+        if (!directivesMap['ssl_certificate_key']) {
+            const ssl_certificate = directivesMap['ssl_certificate'][0]
+            current_server_directives.splice(ssl_certificate.idx + 1, 0, {
+                directive: 'ssl_certificate_key',
+                params: ''
+            })
+        }
+    })
+
+    setTimeout(() => {
+        issue_cert(name.value, callback)
+    }, 100)
+}
+
+function callback(ssl_certificate: string, ssl_certificate_key: string) {
+    directivesMap['ssl_certificate'][0]['params'] = ssl_certificate
+    directivesMap['ssl_certificate_key'][0]['params'] = ssl_certificate_key
+
+    issuing_cert.value = false
+    emit('callback')
+}
+
+function change_auto_cert(r: boolean) {
+    if (r) {
+        domain.add_auto_cert(name.value).then(() => {
+            message.success(interpolate($gettext('Auto-renewal enabled for %{name}'), {name: name.value}))
+        }).catch(e => {
+            message.error(e.message ?? interpolate($gettext('Enable auto-renewal failed for %{name}'), {name: name.value}))
+        })
+    } else {
+        domain.remove_auto_cert(name.value).then(() => {
+            message.success(interpolate($gettext('Auto-renewal disabled for %{name}'), {name: name.value}))
+        }).catch(e => {
+            message.error(e.message ?? interpolate($gettext('Disable auto-renewal failed for %{name}'), {name: name.value}))
+        })
+    }
+}
+
+const server_name_more_than_one = computed(() => {
+    return directivesMap['server_name'] && (directivesMap['server_name'].length > 1 ||
+        directivesMap['server_name'][0].params.trim().indexOf(' ') > 0)
+})
+
+const no_server_name = computed(() => {
+    return directivesMap['server_name'].length === 0
+})
+
+const name = computed(() => {
+    return directivesMap['server_name'][0].params.trim()
+})
+
+watch(server_name_more_than_one, () => {
+    emit('update:enabled', false)
+    onchange(false)
+})
+
+watch(no_server_name, () => {
+    emit('update:enabled', false)
+    onchange(false)
+})
+</script>
+
 <template>
     <div>
         <a-form-item :label="$gettext('Encrypt website with Let\'s Encrypt')">
             <a-switch
                 :loading="issuing_cert"
-                v-model="M_enabled"
+                v-model:checked="enabled"
                 @change="onchange"
                 :disabled="no_server_name||server_name_more_than_one"
             />
@@ -27,7 +141,7 @@
             Note: The server_name in the current configuration must be the domain name
             you need to get the certificate.
         </p>
-        <p v-if="enabled" v-translate>
+        <p v-translate>
             The certificate for the domain will be checked every hour,
             and will be renewed if it has been more than 1 month since it was last issued.
         </p>
@@ -38,123 +152,6 @@
     </div>
 </template>
 
-<script>
-import {issue_cert} from '@/views/domain/methods'
-import $gettext, {$interpolate} from '@/lib/translate/gettext'
-
-export default {
-    name: 'IssueCert',
-    props: {
-        directivesMap: Object,
-        current_server_directives: Array,
-        enabled: Boolean
-    },
-    model: {
-        prop: 'enabled',
-        event: 'changeEnabled'
-    },
-    data() {
-        return {
-            issuing_cert: false,
-            M_enabled: this.enabled,
-        }
-    },
-    methods: {
-        onchange(r) {
-            this.$emit('changeEnabled', r)
-            this.change_auto_cert(r)
-            if (r) {
-                this.job()
-            }
-        },
-        job() {
-            this.issuing_cert = true
-
-            if (this.no_server_name) {
-                this.$message.error($gettext('server_name not found in directives'))
-                this.issuing_cert = false
-                return
-            }
-
-            if (this.server_name_more_than_one) {
-                this.$message.error($gettext('server_name parameters more than one'))
-                this.issuing_cert = false
-                return
-            }
-
-            const server_name = this.directivesMap['server_name'][0]
-
-            if (!this.directivesMap['ssl_certificate']) {
-                this.current_server_directives.splice(server_name.idx + 1, 0, {
-                    directive: 'ssl_certificate',
-                    params: ''
-                })
-            }
-
-            this.$nextTick(() => {
-                if (!this.directivesMap['ssl_certificate_key']) {
-                    const ssl_certificate = this.directivesMap['ssl_certificate'][0]
-                    this.current_server_directives.splice(ssl_certificate.idx + 1, 0, {
-                        directive: 'ssl_certificate_key',
-                        params: ''
-                    })
-                }
-            })
-
-            setTimeout(() => {
-                issue_cert(this.name, this.callback)
-            }, 100)
-        },
-        callback(ssl_certificate, ssl_certificate_key) {
-            this.$set(this.directivesMap['ssl_certificate'][0], 'params', ssl_certificate)
-            this.$set(this.directivesMap['ssl_certificate_key'][0], 'params', ssl_certificate_key)
-            this.issuing_cert = false
-            this.$emit('callback')
-        },
-        change_auto_cert(r) {
-            if (r) {
-                this.$api.domain.add_auto_cert(this.name).then(() => {
-                    this.$message.success($interpolate($gettext('Auto-renewal enabled for %{name}'), {name: this.name}))
-                }).catch(e => {
-                    this.$message.error(e.message ?? $interpolate($gettext('Enable auto-renewal failed for %{name}'), {name: this.name}))
-                })
-            } else {
-                this.$api.domain.remove_auto_cert(this.name).then(() => {
-                    this.$message.success($interpolate($gettext('Auto-renewal disabled for %{name}'), {name: this.name}))
-                }).catch(e => {
-                    this.$message.error(e.message ?? $interpolate($gettext('Disable auto-renewal failed for %{name}'), {name: this.name}))
-                })
-            }
-        },
-    },
-    watch: {
-        server_name_more_than_one() {
-            this.M_enabled = false
-            this.onchange(false)
-        },
-        no_server_name() {
-            this.M_enabled = false
-            this.onchange(false)
-        }
-    },
-    computed: {
-        is_demo() {
-            return this.$store.getters.env.demo === true
-        },
-        server_name_more_than_one() {
-            return this.directivesMap['server_name'] && (this.directivesMap['server_name'].length > 1 ||
-                this.directivesMap['server_name'][0].params.trim().indexOf(' ') > 0)
-        },
-        no_server_name() {
-            return !this.directivesMap['server_name']
-        },
-        name() {
-            return this.directivesMap['server_name'][0].params.trim()
-        }
-    }
-}
-</script>
-
 <style lang="less" scoped>
 .switch-wrapper {
     position: relative;

+ 0 - 37
frontend/src/views/domain/methods.js

@@ -1,37 +0,0 @@
-import $gettext from '@/lib/translate/gettext'
-import store from '@/lib/store'
-import Vue from 'vue'
-
-const issue_cert = (server_name, callback) => {
-    Vue.prototype.$message.info($gettext('Getting the certificate, please wait...'), 15)
-    const ws = new WebSocket(Vue.prototype.getWebSocketRoot() + '/cert/issue/' + server_name
-        + '?token=' + btoa(store.state.user.token))
-
-    ws.onopen = () => {
-        ws.send('go')
-    }
-
-    ws.onmessage = m => {
-        const r = JSON.parse(m.data)
-        switch (r.status) {
-            case 'success':
-                Vue.prototype.$message.success(r.message, 10)
-                break
-            case 'info':
-                Vue.prototype.$message.info(r.message, 10)
-                break
-            case 'error':
-                Vue.prototype.$message.error(r.message, 10)
-                break
-        }
-
-        if (r.status === 'success' && r.ssl_certificate !== undefined && r.ssl_certificate_key !== undefined) {
-            callback(r.ssl_certificate, r.ssl_certificate_key)
-        }
-    }
-    // setTimeout(() => {
-    //     callback('a', 'b')
-    // }, 10000)
-}
-
-export {issue_cert}

+ 40 - 0
frontend/src/views/domain/methods.ts

@@ -0,0 +1,40 @@
+import gettext from '@/gettext'
+import websocket from '@/lib/websocket'
+import ReconnectingWebSocket from 'reconnecting-websocket'
+import {message} from 'ant-design-vue'
+
+const {$gettext} = gettext
+
+const issue_cert = async (server_name: string, callback: Function) => {
+    // message.info($gettext('Getting the certificate, please wait...'), 15)
+    //
+    // const ws: ReconnectingWebSocket = websocket('/api/cert/issue/' + server_name)
+    //
+    // ws.onopen = () => {
+    //     ws.send('go')
+    // }
+    //
+    // ws.onmessage = m => {
+    //     const r = JSON.parse(m.data)
+    //     switch (r.status) {
+    //         case 'success':
+    //             message.success(r.message, 10)
+    //             break
+    //         case 'info':
+    //             message.info(r.message, 10)
+    //             break
+    //         case 'error':
+    //             message.error(r.message, 10)
+    //             break
+    //     }
+    //
+    //     if (r.status === 'success' && r.ssl_certificate !== undefined && r.ssl_certificate_key !== undefined) {
+    //         callback(r.ssl_certificate, r.ssl_certificate_key)
+    //     }
+    // }
+    setTimeout(() => {
+        callback('a', 'b')
+    }, 10000)
+}
+
+export {issue_cert}

+ 4 - 4
frontend/src/views/domain/ngx_conf/LocationEditor.vue

@@ -28,11 +28,11 @@ function add() {
 
 function save() {
     adding.value = false
-    locations.push(this.location)
+    locations?.push(location)
 }
 
-function remove(index) {
-    locations.splice(index, 1)
+function remove(index: number) {
+    locations?.splice(index, 1)
 }
 </script>
 
@@ -43,7 +43,7 @@ function remove(index) {
             :title="$gettext('Location')" size="small">
         <a-form layout="vertical">
             <a-form-item :label="$gettext('Comments')">
-                <a-textarea v-model:value="v.comments"/>
+                <a-textarea v-model:value="v.comments" :bordered="false"/>
             </a-form-item>
             <a-form-item :label="$gettext('Path')">
                 <a-input addon-before="location" v-model:value="v.path"/>

+ 7 - 18
frontend/src/views/domain/ngx_conf/NgxConfigEditor.vue

@@ -1,11 +1,10 @@
 <script setup lang="ts">
-import CertInfo from '@/views/domain/cert/CertInfo'
-// import IssueCert from '@/views/domain/cert/IssueCert'
 import DirectiveEditor from '@/views/domain/ngx_conf/directive/DirectiveEditor'
 import LocationEditor from '@/views/domain/ngx_conf/LocationEditor'
 import {computed, ref} from 'vue'
 import {useRoute} from 'vue-router'
 import {useGettext} from 'vue3-gettext'
+import Cert from '@/views/domain/cert/Cert.vue'
 
 const {$gettext} = useGettext()
 
@@ -16,15 +15,6 @@ const route = useRoute()
 const current_server_index = ref(0)
 const name = ref(route.params.name)
 
-const init_ssl_status = ref(false)
-
-function update_cert_info() {
-    // TODO
-    // if (name.value && this.$refs['cert-info' + this.current_server_index]) {
-    //     this.$refs['cert-info' + this.current_server_index].get()
-    // }
-}
-
 function change_tls(r: any) {
     if (r) {
         // deep copy servers[0] to servers[1]
@@ -140,17 +130,16 @@ const current_support_ssl = computed(() => {
 
                 <div class="tab-content">
                     <template v-if="current_support_ssl&&enabled">
-                        <cert-info :domain="name" v-if="current_support_ssl"/>
-                        <!--                        <issue-cert-->
-                        <!--                            :current_server_directives="current_server_directives"-->
-                        <!--                            :directives-map="directivesMap"-->
-                        <!--                            v-model="auto_cert"-->
-                        <!--                        />-->
+                        <cert
+                            v-if="current_support_ssl"
+                            :current_server_directives="current_server_directives"
+                            :directives-map="directivesMap"
+                            v-model:enabled="auto_cert"/>
                     </template>
 
                     <template v-if="v.comments">
                         <h3 v-translate>Comments</h3>
-                        <p style="white-space: pre-wrap;">{{ v.comments }}</p>
+                        <a-textarea v-model:value="v.comments" :bordered="false"/>
                     </template>
 
                     <directive-editor :ngx_directives="v.directives"/>

+ 16 - 12
frontend/src/views/domain/ngx_conf/directive/DirectiveAdd.vue

@@ -3,7 +3,7 @@ import {If} from '@/views/domain/ngx_conf'
 import CodeEditor from '@/components/CodeEditor'
 import {reactive, ref} from 'vue'
 import {useGettext} from 'vue3-gettext'
-import {CloseOutlined} from '@ant-design/icons-vue'
+import {DeleteOutlined} from '@ant-design/icons-vue'
 
 const {$gettext} = useGettext()
 
@@ -52,17 +52,18 @@ function save() {
             </a-form-item>
             <a-form-item>
                 <code-editor v-if="mode===If" default-height="100px" v-model:content="directive.params"/>
-
-                <a-input-group compact v-else>
-
-                    <a-input style="width: 30%" :placeholder="$gettext('Directive')" v-model="directive.directive"/>
-
-                    <a-input style="width: 70%" :placeholder="$gettext('Params')" v-model="directive.params">
-                        <template #suffix>
-                            <CloseOutlined @click="adding=false" style="color: rgba(0,0,0,.45);font-size: 10px;"/>
+                <div class="input-wrapper" v-else>
+                    <a-input-group compact>
+                        <a-input style="width: 30%" :placeholder="$gettext('Directive')" v-model="directive.directive"/>
+                        <a-input style="width: 70%" :placeholder="$gettext('Params')" v-model="directive.params"/>
+                    </a-input-group>
+                    <a-button @click="adding=false">
+                        <template #icon>
+                            <DeleteOutlined style="font-size: 14px;"/>
                         </template>
-                    </a-input>
-                </a-input-group>
+                    </a-button>
+
+                </div>
             </a-form-item>
         </div>
         <a-button block v-if="!adding" @click="add">{{ $gettext('Add Directive Below') }}</a-button>
@@ -73,5 +74,8 @@ function save() {
 </template>
 
 <style lang="less" scoped>
-
+.input-wrapper {
+    display: flex;
+    gap: 10px;
+}
 </style>

+ 23 - 14
frontend/src/views/domain/ngx_conf/directive/DirectiveEditor.vue

@@ -4,7 +4,7 @@ import {If} from '@/views/domain/ngx_conf'
 import DirectiveAdd from '@/views/domain/ngx_conf/directive/DirectiveAdd'
 import {useGettext} from 'vue3-gettext'
 import {reactive, ref} from 'vue'
-import {CloseOutlined} from '@ant-design/icons-vue'
+import {DeleteOutlined} from '@ant-design/icons-vue'
 
 const {$gettext} = useGettext()
 
@@ -45,19 +45,23 @@ function onSave(idx: number) {
     <a-form-item v-for="(directive,index) in ngx_directives" @click="current_idx=index">
         <code-editor v-if="directive.directive === If" v-model:content="directive.params"
                      defaultHeight="100px"/>
-        <a-input :addon-before="directive.directive" v-model:value="directive.params" @click="current_idx=k"
-                 v-else>
-            <template #suffix>
-                <a-popconfirm @confirm="remove(index)"
-                              :title="$gettext('Are you sure you want to remove this directive?')"
-                              :ok-text="$gettext('Yes')"
-                              :cancel-text="$gettext('No')">
-                    <CloseOutlined style="color: rgba(0,0,0,.45);font-size: 10px;"/>
-                </a-popconfirm>
-            </template>
-        </a-input>
+        <div class="input-wrapper" v-else>
+            <a-input :addon-before="directive.directive" v-model:value="directive.params" @click="current_idx=k"
+            >
+            </a-input>
+            <a-popconfirm @confirm="remove(index)"
+                          :title="$gettext('Are you sure you want to remove this directive?')"
+                          :ok-text="$gettext('Yes')"
+                          :cancel-text="$gettext('No')">
+                <a-button>
+                    <template #icon>
+                        <DeleteOutlined style="font-size: 14px;"/>
+                    </template>
+                </a-button>
+            </a-popconfirm>
+        </div>
         <transition name="slide">
-            <div v-if="current_idx===index" class="extra">
+            <div v-if="current_idx===index" class="directive-editor-extra">
                 <div class="extra-content">
                     <a-form layout="vertical">
                         <a-form-item :label="$gettext('Comments')">
@@ -74,7 +78,7 @@ function onSave(idx: number) {
 </template>
 
 <style lang="less" scoped>
-.extra {
+.directive-editor-extra {
     background-color: #fafafa;
     padding: 10px 20px 20px;
     margin-bottom: 10px;
@@ -91,4 +95,9 @@ function onSave(idx: number) {
 .slide-enter-to, .slide-leave-from {
     max-height: 600px;
 }
+
+.input-wrapper {
+    display: flex;
+    gap: 10px;
+}
 </style>

+ 1 - 1
frontend/src/views/user/User.vue

@@ -45,7 +45,7 @@ const columns = [{
 </script>
 
 <template>
-    <std-curd :columns="columns" :api="user"/>
+    <std-curd :title="$gettext('Manage Users')" :columns="columns" :api="user"/>
 </template>
 
 <style scoped>

+ 1 - 1
frontend/version.json

@@ -1 +1 @@
-{"version":"1.5.0","build_id":11,"total_build":81}
+{"version":"1.5.0","build_id":14,"total_build":84}

+ 3 - 0
frontend/vite.config.ts

@@ -58,6 +58,9 @@ export default defineConfig({
     css: {
         preprocessorOptions: {
             less: {
+                modifyVars: {
+                    'border-radius-base': '4px',
+                },
                 javascriptEnabled: true,
             }
         },

+ 2 - 11
server/api/user.go

@@ -11,18 +11,9 @@ import (
 )
 
 func GetUsers(c *gin.Context) {
-	curd := model.NewCurd(&model.Auth{})
-
-	var list []model.Auth
-	err := curd.GetList(&list)
+	data := model.GetUserList(c, c.Query("name"))
 
-	if err != nil {
-		ErrHandler(c, err)
-		return
-	}
-	c.JSON(http.StatusOK, gin.H{
-		"data": list,
-	})
+	c.JSON(http.StatusOK, data)
 }
 
 func GetUser(c *gin.Context) {

+ 18 - 0
server/model/auth.go

@@ -2,6 +2,7 @@ package model
 
 import (
 	"github.com/0xJacky/Nginx-UI/server/settings"
+	"github.com/gin-gonic/gin"
 	"github.com/golang-jwt/jwt"
 	"time"
 )
@@ -30,6 +31,23 @@ func GetUser(name string) (user Auth, err error) {
 	return user, err
 }
 
+func GetUserList(c *gin.Context, username interface{}) (data DataList) {
+	var total int64
+	db.Model(&Auth{}).Count(&total)
+	var users []Auth
+
+	result := db.Model(&Auth{}).Scopes(orderAndPaginate(c))
+
+	if username != "" {
+		result = result.Where("name LIKE ?", "%"+username.(string)+"%")
+	}
+
+	result.Find(&users)
+
+	data = GetListWithPagination(&users, c, total)
+	return
+}
+
 func DeleteToken(token string) error {
 	return db.Where("token = ?", token).Delete(&AuthToken{}).Error
 }

+ 116 - 0
server/model/model.go

@@ -0,0 +1,116 @@
+package model
+
+import (
+	"fmt"
+	"github.com/0xJacky/Nginx-UI/server/settings"
+	"github.com/gin-gonic/gin"
+	"github.com/spf13/cast"
+	"gorm.io/driver/sqlite"
+	"gorm.io/gorm"
+	"gorm.io/gorm/logger"
+	"log"
+	"path"
+	"time"
+)
+
+var db *gorm.DB
+
+type Model struct {
+	ID        uint       `gorm:"primary_key" json:"id"`
+	CreatedAt time.Time  `json:"created_at"`
+	UpdatedAt time.Time  `json:"updated_at"`
+	DeletedAt *time.Time `gorm:"index" json:"deleted_at"`
+}
+
+func Init() {
+	dbPath := path.Join(path.Dir(settings.ConfPath), fmt.Sprintf("%s.db", settings.ServerSettings.Database))
+	var err error
+	db, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{
+		Logger:      logger.Default.LogMode(logger.Info),
+		PrepareStmt: true,
+	})
+	if err != nil {
+		log.Println(err)
+	}
+	// Migrate the schema
+	AutoMigrate(&ConfigBackup{})
+	AutoMigrate(&Auth{})
+	AutoMigrate(&AuthToken{})
+	AutoMigrate(&Cert{})
+}
+
+func AutoMigrate(model interface{}) {
+	err := db.AutoMigrate(model)
+	if err != nil {
+		log.Fatal(err)
+	}
+}
+
+func orderAndPaginate(c *gin.Context) func(db *gorm.DB) *gorm.DB {
+	return func(db *gorm.DB) *gorm.DB {
+		sort := c.DefaultQuery("sort", "desc")
+		order := c.DefaultQuery("order_by", "id") +
+			" " + sort
+
+		page := cast.ToInt(c.Query("page"))
+		if page == 0 {
+			page = 1
+		}
+		pageSize := settings.ServerSettings.PageSize
+		reqPageSize := c.Query("page_size")
+		if reqPageSize != "" {
+			pageSize = cast.ToInt(reqPageSize)
+		}
+		offset := (page - 1) * pageSize
+
+		return db.Order(order).Offset(offset).Limit(pageSize)
+	}
+}
+
+func totalPage(total int64, pageSize int) int64 {
+	n := total / int64(pageSize)
+	if total%int64(pageSize) > 0 {
+		n++
+	}
+	return n
+}
+
+type Pagination struct {
+	Total       int64 `json:"total"`
+	PerPage     int   `json:"per_page"`
+	CurrentPage int   `json:"current_page"`
+	TotalPages  int64 `json:"total_pages"`
+}
+
+type DataList struct {
+	Data       interface{} `json:"data"`
+	Pagination Pagination  `json:"pagination,omitempty"`
+}
+
+func GetListWithPagination(models interface{},
+	c *gin.Context, totalRecords int64) (result DataList) {
+
+	page := cast.ToInt(c.Query("page"))
+	if page == 0 {
+		page = 1
+	}
+
+	result = DataList{}
+
+	result.Data = models
+
+	pageSize := settings.ServerSettings.PageSize
+	reqPageSize := c.Query("page_size")
+	if reqPageSize != "" {
+		pageSize = cast.ToInt(reqPageSize)
+	}
+
+	result.Pagination = Pagination{
+		Total:       totalRecords,
+		PerPage:     pageSize,
+		CurrentPage: page,
+		TotalPages:  totalPage(totalRecords, pageSize),
+	}
+
+	return
+}

+ 0 - 45
server/model/models.go

@@ -1,45 +0,0 @@
-package model
-
-import (
-	"fmt"
-	"github.com/0xJacky/Nginx-UI/server/settings"
-	"gorm.io/driver/sqlite"
-	"gorm.io/gorm"
-	"gorm.io/gorm/logger"
-	"log"
-	"path"
-	"time"
-)
-
-var db *gorm.DB
-
-type Model struct {
-	ID        uint       `gorm:"primary_key" json:"id"`
-	CreatedAt time.Time  `json:"created_at"`
-	UpdatedAt time.Time  `json:"updated_at"`
-	DeletedAt *time.Time `gorm:"index" json:"deleted_at"`
-}
-
-func Init() {
-	dbPath := path.Join(path.Dir(settings.ConfPath), fmt.Sprintf("%s.db", settings.ServerSettings.Database))
-	var err error
-	db, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{
-		Logger:      logger.Default.LogMode(logger.Info),
-		PrepareStmt: true,
-	})
-	if err != nil {
-		log.Println(err)
-	}
-	// Migrate the schema
-	AutoMigrate(&ConfigBackup{})
-	AutoMigrate(&Auth{})
-	AutoMigrate(&AuthToken{})
-	AutoMigrate(&Cert{})
-}
-
-func AutoMigrate(model interface{}) {
-	err := db.AutoMigrate(model)
-	if err != nil {
-		log.Fatal(err)
-	}
-}

+ 2 - 0
server/settings/settings.go

@@ -25,6 +25,7 @@ type Server struct {
 	Database          string
 	StartCmd          string
 	Demo              bool
+	PageSize          int
 }
 
 var ServerSettings = &Server{
@@ -34,6 +35,7 @@ var ServerSettings = &Server{
 	Database:          "database",
 	StartCmd:          "login",
 	Demo:              false,
+	PageSize:          10,
 }
 
 var ConfPath string