From e7e9cbcfa26e5a4522baa720e21cb47b8492889f Mon Sep 17 00:00:00 2001
From: MattIPv4 <me@mattcowley.co.uk>
Date: Mon, 4 May 2020 20:55:23 +0100
Subject: [PATCH] Add global tools section & export data generator

---
 src/nginxconfig/scss/style.scss               |  24 +++
 src/nginxconfig/templates/app.vue             |   6 +-
 .../templates/global_sections/index.js        |   1 +
 .../templates/global_sections/tools.vue       | 192 ++++++++++++++++++
 src/nginxconfig/util/export_data.js           |  42 ++++
 5 files changed, 264 insertions(+), 1 deletion(-)
 create mode 100644 src/nginxconfig/templates/global_sections/tools.vue
 create mode 100644 src/nginxconfig/util/export_data.js

diff --git a/src/nginxconfig/scss/style.scss b/src/nginxconfig/scss/style.scss
index e31c5f2..4281af0 100644
--- a/src/nginxconfig/scss/style.scss
+++ b/src/nginxconfig/scss/style.scss
@@ -144,6 +144,18 @@ $highlight: #f2c94c;
       }
     }
 
+    &.is-grouped {
+      > .control {
+        &:last-child {
+          margin: .25rem 0 0;
+        }
+
+        &:not(:last-child) {
+          margin: .25rem .75rem 0 0;
+        }
+      }
+    }
+
     .is-changed {
       input {
         &:not(.vs__search) {
@@ -208,6 +220,18 @@ $highlight: #f2c94c;
     }
   }
 
+  .field-body {
+    &.is-vertical {
+      flex-direction: column;
+
+      > .field {
+        &:not(:last-child) {
+          margin-bottom: .75rem;
+        }
+      }
+    }
+  }
+
   .checkbox,
   .radio {
     border-radius: $border-radius;
diff --git a/src/nginxconfig/templates/app.vue b/src/nginxconfig/templates/app.vue
index 71cfae0..8299f80 100644
--- a/src/nginxconfig/templates/app.vue
+++ b/src/nginxconfig/templates/app.vue
@@ -53,7 +53,7 @@ limitations under the License.
             <h2>Global config</h2>
             <Global :data="global"></Global>
 
-            <pre><code>{{ JSON.stringify({ domains: activeDomains, global }, null, 2) }}</code></pre>
+            <pre><code>{{ exportData }}</code></pre>
         </div>
 
         <Footer :text="i18n.templates.app.oss"></Footer>
@@ -65,6 +65,7 @@ limitations under the License.
     import Header from 'do-vue/src/templates/header';
     import Footer from 'do-vue/src/templates/footer';
     import isChanged from '../util/is_changed';
+    import exportData from '../util/export_data';
     import i18n from '../i18n';
     import Domain from './domain';
     import Global from './global';
@@ -91,6 +92,9 @@ limitations under the License.
             activeDomains() {
                 return this.$data.domains.map((domain, index) => [domain, index]).filter(d => d[0] !== null);
             },
+            exportData() {
+                return JSON.stringify(exportData(this.activeDomains, this.$data.global), null, 2);
+            },
         },
         methods: {
             changes(index) {
diff --git a/src/nginxconfig/templates/global_sections/index.js b/src/nginxconfig/templates/global_sections/index.js
index 9cf6c4f..1a58387 100644
--- a/src/nginxconfig/templates/global_sections/index.js
+++ b/src/nginxconfig/templates/global_sections/index.js
@@ -5,3 +5,4 @@ export { default as Python } from './python';
 export { default as Performance } from './performance';
 export { default as Logging } from './logging';
 export { default as NGINX } from './nginx';
+export { default as Tools } from './tools';
diff --git a/src/nginxconfig/templates/global_sections/tools.vue b/src/nginxconfig/templates/global_sections/tools.vue
new file mode 100644
index 0000000..2497989
--- /dev/null
+++ b/src/nginxconfig/templates/global_sections/tools.vue
@@ -0,0 +1,192 @@
+<template>
+    <div>
+        <div class="field is-horizontal">
+            <div class="field-label">
+                <label class="label">Modularized structure</label>
+            </div>
+            <div class="field-body">
+                <div class="field">
+                    <div :class="`control${modularizedStructureChanged ? ' is-changed' : ''}`">
+                        <div class="checkbox">
+                            <PrettyCheck v-model="modularizedStructure" class="p-default p-curve p-fill p-icon">
+                                <i slot="extra" class="icon fas fa-check"></i>
+                                enable modularized config files
+                            </PrettyCheck>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <div class="field is-horizontal">
+            <div class="field-label">
+                <label class="label">Symlink vhost</label>
+            </div>
+            <div class="field-body">
+                <div class="field">
+                    <div :class="`control${symlinkVhostChanged ? ' is-changed' : ''}`">
+                        <div class="checkbox">
+                            <PrettyCheck v-model="symlinkVhost" class="p-default p-curve p-fill p-icon">
+                                <i slot="extra" class="icon fas fa-check"></i>
+                                enable symlink from sites-available/ to sites-enabled/
+                            </PrettyCheck>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <div class="field is-horizontal">
+            <div class="field-label">
+                <label class="label">Share configuration</label>
+            </div>
+            <div class="field-body">
+                <div class="field">
+                    <div class="control">
+                        <input v-model="shareLink"
+                               class="input"
+                               type="text"
+                               disabled="disabled"
+                        />
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <div class="field is-horizontal">
+            <div class="field-label">
+                <label class="label">Reset configuration</label>
+            </div>
+            <div class="field-body">
+                <div class="field is-grouped">
+                    <div class="control">
+                        <a class="button is-danger is-mini" @click="resetGlobal">
+                            Reset global config
+                        </a>
+                    </div>
+                    <div v-if="hasDomain" class="control">
+                        <a class="button is-danger is-mini" @click="resetDomains">
+                            Reset all domains
+                        </a>
+                    </div>
+                    <div v-if="hasDomain" class="control">
+                        <a class="button is-danger is-mini" @click="removeDomains">
+                            Remove all domains
+                        </a>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <div class="field is-horizontal">
+            <div class="field-label">
+            </div>
+            <div class="field-body is-vertical">
+                <div v-for="domainData in $parent.$parent.activeDomains" class="field is-horizontal">
+                    <div class="field-label">
+                        <label class="label">{{ domainData[0].server.domain.computed }}</label>
+                    </div>
+                    <div class="field-body">
+                        <div class="field is-grouped">
+                            <div class="control">
+                                <a class="button is-danger is-mini" @click="resetDomain(domainData[1])">
+                                    Reset domain config
+                                </a>
+                            </div>
+                            <div class="control">
+                                <a class="button is-danger is-mini" @click="removeDomain(domainData[1])">
+                                    Remove domain
+                                </a>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+    import PrettyCheck from 'pretty-checkbox-vue/check';
+    import i18n from '../../i18n';
+    import delegatedFromDefaults from '../../util/delegated_from_defaults';
+    import computedFromDefaults from '../../util/computed_from_defaults';
+    import exportData from '../../util/export_data';
+
+    const defaults = {
+        modularizedStructure: {
+            default: true,
+            enabled: true,
+        },
+        symlinkVhost: {
+            default: true,
+            enabled: true,
+        },
+    };
+
+    export default {
+        name: 'GlobalTools',                            // Component name
+        display: 'Tools',                               // Display name for tab
+        key: 'tools',                                   // Key for data in parent
+        delegated: delegatedFromDefaults(defaults),     // Data the parent will present here
+        components: {
+            PrettyCheck,
+        },
+        props: {
+            data: Object,                               // Data delegated back to us from parent
+        },
+        data () {
+            return {
+                i18n,
+            };
+        },
+        computed: {
+            ...computedFromDefaults(defaults, 'tools'), // Getters & setters for the delegated data
+            hasDomain() {
+                return this.$parent.$parent.activeDomains.length > 0;
+            },
+            shareLink() {
+                return JSON.stringify(exportData(this.$parent.$parent.activeDomains, this.$parent.$props.data));
+            },
+        },
+        methods: {
+            // TODO: These all need confirmation prompts!
+            resetGlobal() {
+                Object.values(this.$parent.$props.data).forEach(category => {
+                    Object.values(category).forEach(property => {
+                        property.value = property.default;
+                        property.computed = property.default;
+                    });
+                });
+            },
+            resetDomain(index) {
+                if (index >= this.$parent.$parent.$data.domains.length) return;
+
+                const domain = this.$parent.$parent.$data.domains[index];
+                if (!domain) return;
+
+                Object.values(domain).forEach(category => {
+                    Object.values(category).forEach(property => {
+                        property.value = property.default;
+                        property.computed = property.default;
+                    });
+                });
+            },
+            removeDomain(index) {
+                if (index >= this.$parent.$parent.$data.domains.length) return;
+
+                this.$set(this.$parent.$parent.$data.domains, index, null);
+            },
+            resetDomains() {
+                for (let i = 0; i < this.$parent.$parent.$data.domains.length; i++) {
+                    this.resetDomain(i);
+                }
+            },
+            removeDomains() {
+                for (let i = 0; i < this.$parent.$parent.$data.domains.length; i++) {
+                    this.removeDomain(i);
+                }
+            },
+        },
+    };
+</script>
diff --git a/src/nginxconfig/util/export_data.js b/src/nginxconfig/util/export_data.js
new file mode 100644
index 0000000..d4243b2
--- /dev/null
+++ b/src/nginxconfig/util/export_data.js
@@ -0,0 +1,42 @@
+const categoriesExport = (categories) => {
+    const categoriesData = {};
+
+    // Work through each category
+    for (const category in categories) {
+        const categoryData = {};
+
+        // Go over each property in the category
+        for (const property in categories[category]) {
+
+            // If the user input differs from the default, they changed it, so we save it
+            const propertyData = categories[category][property];
+            if (propertyData.value !== propertyData.default) {
+                categoryData[property] = propertyData.value;
+            }
+        }
+
+        // If there were any property changes, save the category
+        if (Object.keys(categoryData).length) {
+            categoriesData[category] = categoryData;
+        }
+    }
+
+    return categoriesData;
+};
+
+export default (domains, global) => {
+    const exportData = {};
+
+    // Handle domains
+    // Always save changes, even if none, so we can replicate the correct number of domains
+    exportData.domains = domains.map(domain => categoriesExport(domain[0]));
+
+    // Handle global
+    // If there were any category changes, save global changes
+    const globalData = categoriesExport(global);
+    if (Object.keys(globalData).length) {
+        exportData.global = globalData;
+    }
+
+    return exportData;
+};