ソースを参照

feat: duplicate site config to remote #70

0xJacky 2 年 前
コミット
6c38b11ab3

+ 3 - 0
frontend/components.d.ts

@@ -16,6 +16,8 @@ declare module '@vue/runtime-core' {
     ABreadcrumbItem: typeof import('ant-design-vue/es')['BreadcrumbItem']
     AButton: typeof import('ant-design-vue/es')['Button']
     ACard: typeof import('ant-design-vue/es')['Card']
+    ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
+    ACheckboxGroup: typeof import('ant-design-vue/es')['CheckboxGroup']
     ACol: typeof import('ant-design-vue/es')['Col']
     ACollapse: typeof import('ant-design-vue/es')['Collapse']
     ACollapsePanel: typeof import('ant-design-vue/es')['CollapsePanel']
@@ -70,6 +72,7 @@ declare module '@vue/runtime-core' {
     FooterToolbarFooterToolBar: typeof import('./src/components/FooterToolbar/FooterToolBar.vue')['default']
     LogoLogo: typeof import('./src/components/Logo/Logo.vue')['default']
     NginxControlNginxControl: typeof import('./src/components/NginxControl/NginxControl.vue')['default']
+    NodeSelectorNodeSelector: typeof import('./src/components/NodeSelector/NodeSelector.vue')['default']
     PageHeaderPageHeader: typeof import('./src/components/PageHeader/PageHeader.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']

+ 2 - 2
frontend/src/api/curd.ts

@@ -22,8 +22,8 @@ class Curd {
         return http.get(this.baseUrl + (id ? '/' + id : ''))
     }
 
-    _save(id: any = null, data: any) {
-        return http.post(this.baseUrl + (id ? '/' + id : ''), data)
+    _save(id: any = null, data: any, config: any = undefined) {
+        return http.post(this.baseUrl + (id ? '/' + id : ''), data, config)
     }
 
     _destroy(id: any = null) {

+ 4 - 0
frontend/src/components/EnvIndicator/EnvIndicator.vue

@@ -75,6 +75,10 @@ watch(node_id, () => {
         align-items: center;
         justify-content: space-between;
 
+        .env-name {
+            max-width: 50px;
+        }
+
         .ant-tag {
             cursor: pointer;
             margin-right: 0;

+ 51 - 0
frontend/src/components/NodeSelector/NodeSelector.vue

@@ -0,0 +1,51 @@
+<script setup lang="ts">
+import {computed, ref} from 'vue'
+import environment from '@/api/environment'
+import {useGettext} from 'vue3-gettext'
+
+const {$gettext} = useGettext()
+
+const props = defineProps(['target', 'map'])
+const emit = defineEmits(['update:target'])
+
+const data = ref([])
+const data_map = ref({})
+
+environment.get_list().then(r => {
+    data.value = r.data
+    r.data.forEach(node => {
+        data_map[node.id] = node
+    })
+})
+
+const value = computed({
+    get() {
+        return props.target
+    },
+    set(v) {
+        if (typeof props.map === 'object') {
+            v.forEach(id => {
+                if (id !== 0) props.map[id] = data_map[id].name
+            })
+        }
+        emit('update:target', v)
+    }
+})
+</script>
+
+<template>
+    <a-checkbox-group v-model:value="value" style="width: 100%">
+        <a-row>
+            <a-col :span="8">
+                <a-checkbox :value="0">{{ $gettext('Local') }}</a-checkbox>
+            </a-col>
+            <a-col :span="8" v-for="node in data">
+                <a-checkbox :value="node.id">{{ node.name }}</a-checkbox>
+            </a-col>
+        </a-row>
+    </a-checkbox-group>
+</template>
+
+<style scoped lang="less">
+
+</style>

+ 5 - 1
frontend/src/pinia/moudule/settings.ts

@@ -10,7 +10,11 @@ export const useSettingsStore = defineStore('settings', {
             name: 'Local'
         }
     }),
-    getters: {},
+    getters: {
+        is_remote(): boolean {
+            return this.environment.id !== 0
+        }
+    },
     actions: {
         set_language(lang: string) {
             this.language = lang

+ 49 - 9
frontend/src/views/domain/SiteDuplicate.vue

@@ -1,15 +1,19 @@
 <script setup lang="ts">
 import {computed, nextTick, reactive, ref, watch} from 'vue'
 import {useGettext} from 'vue3-gettext'
-import {Form, message} from 'ant-design-vue'
+import {Form, message, notification} from 'ant-design-vue'
 import gettext from '@/gettext'
 import domain from '@/api/domain'
+import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
+import {useSettingsStore} from '@/pinia'
 
 const {$gettext} = useGettext()
 
 const props = defineProps(['visible', 'name'])
 const emit = defineEmits(['update:visible', 'duplicated'])
 
+const settings = useSettingsStore()
+
 const show = computed({
     get() {
         return props.visible
@@ -19,7 +23,7 @@ const show = computed({
     }
 })
 
-const modelRef = reactive({name: ''})
+const modelRef = reactive({name: '', target: []})
 
 const rulesRef = reactive({
     name: [
@@ -28,6 +32,12 @@ const rulesRef = reactive({
             message: () => $gettext('Please input name, ' +
                 'this will be used as the filename of the new configuration!')
         }
+    ],
+    target: [
+        {
+            required: true,
+            message: () => $gettext('Please select at least one node!')
+        }
     ]
 })
 
@@ -35,16 +45,42 @@ const {validate, validateInfos, clearValidate} = Form.useForm(modelRef, rulesRef
 
 const loading = ref(false)
 
+const node_map = reactive({})
+
 function onSubmit() {
     validate().then(async () => {
         loading.value = true
 
-        domain.duplicate(props.name, {name: modelRef.name}).then(() => {
-            message.success($gettext('Duplicated successfully'))
-            show.value = false
-            emit('duplicated')
-        }).catch((e: any) => {
-            message.error($gettext(e?.message ?? 'Server error'))
+        modelRef.target.forEach(id => {
+            if (id === 0) {
+                domain.duplicate(props.name, {name: modelRef.name}).then(() => {
+                    message.success($gettext('Duplicate to local successfully'))
+                    show.value = false
+                    emit('duplicated')
+                }).catch((e: any) => {
+                    message.error($gettext(e?.message ?? 'Server error'))
+                })
+            } else {
+                // get source content
+                domain.get(props.name).then(r => {
+                    domain.save(modelRef.name, {
+                        name: modelRef.name,
+                        content: r.config
+                    }, {headers: {'X-Node-ID': id}}).then(() => {
+                        notification.success({
+                            message: $gettext('Duplicate successfully'),
+                            description:
+                                $gettext('Duplicate %{conf_name} to %{node_name} successfully',
+                                    {conf_name: props.name, node_name: node_map[id]})
+                        })
+                    }).catch(e => {
+                        notification.error({
+                            message: $gettext('Duplicate failed'),
+                            description: $gettext(e.message)
+                        })
+                    })
+                })
+            }
         })
 
         loading.value = false
@@ -54,6 +90,7 @@ function onSubmit() {
 watch(() => props.visible, (v) => {
     if (v) {
         modelRef.name = ''
+        modelRef.target = [0]
         nextTick(() => clearValidate())
     }
 })
@@ -65,11 +102,14 @@ watch(() => gettext.current, () => {
 
 <template>
     <a-modal :title="$gettext('Duplicate')" v-model:visible="show" @ok="onSubmit"
-             :confirm-loading="loading">
+             :confirm-loading="loading" :mask="null">
         <a-form layout="vertical">
             <a-form-item :label="$gettext('Name')" v-bind="validateInfos.name">
                 <a-input v-model:value="modelRef.name"/>
             </a-form-item>
+            <a-form-item v-if="!settings.is_remote" :label="$gettext('Target')" v-bind="validateInfos.target">
+                <node-selector v-model:target="modelRef.target" :map="node_map"/>
+            </a-form-item>
         </a-form>
     </a-modal>
 </template>