diff --git a/back/webmin/graphql/resolvers/BigResolver.ts b/back/webmin/graphql/resolvers/BigResolver.ts index 5e88ec7726bb1f19278b7255984298244aaf246d..1ccc04b85bab54d12531f7542b9a30b9980c5120 100644 --- a/back/webmin/graphql/resolvers/BigResolver.ts +++ b/back/webmin/graphql/resolvers/BigResolver.ts @@ -10,6 +10,7 @@ import {gqlHeads} from '../../queries/gql-heads' import {gqlWs2pInfos} from '../../queries/gql-ws2p' import {gqlIsSyncStarted, gqlSynchronize} from '../../queries/gql-synchronize' import {gqlUid} from '../../queries/gql-uid' +import {getConf, testAndSaveConf} from '../../queries/gql-conf' import {Arg} from 'type-graphql/dist/decorators/Arg' import {getForks, getMainChain} from '../../queries/gql-forks' import {ChainType} from '../types/ChainType' @@ -67,6 +68,16 @@ export class BigResolver { return gqlUid(ApplicationContext.server)(null, { pub }) } + @Query(type => String) + getConf(): Promise<string> { + return getConf(ApplicationContext.server) + } + + @Query(type => String) + testAndSaveConf(@Arg("conf") conf: string): Promise<string> { + return testAndSaveConf(ApplicationContext.server, ApplicationContext.startServices, ApplicationContext.stopServices, conf) + } + @Query(type => ChainType) getMainChain(@Arg("start", type => Int) start: number, @Arg("end", type => Int) end: number): Promise<ChainType> { return getMainChain(ApplicationContext.server, start, end) diff --git a/back/webmin/queries/gql-conf.ts b/back/webmin/queries/gql-conf.ts new file mode 100644 index 0000000000000000000000000000000000000000..fee3fc207acf9f33eb4c485c65c0871d160a5e7b --- /dev/null +++ b/back/webmin/queries/gql-conf.ts @@ -0,0 +1,26 @@ +import {Server} from 'duniter/server' + +export async function getConf(server: Server): Promise<string> { + const raw = (await server.dal.confDAL.readRawConfFile()) || '' + if (!raw) { + return '' + } + return JSON.stringify(JSON.parse(raw), null, '\t') +} + +export async function testAndSaveConf( + server: Server, + startServices: () => Promise<void>, + stopServices: () => Promise<void>, + conf: string +): Promise<string> { + // Test the syntax + const jsonConf = JSON.parse(conf) + // OK: stop services and reload the server + await stopServices() + await server.dal.confDAL.saveConf(jsonConf) + await server.reloadConf() + await startServices() + // We reuse the getConf() function + return getConf(server) +} diff --git a/package.json b/package.json index e700a73a51c61d5e34c3e1536d57aee289726184..087e6ee61fe71cea1a445f261fa7cc1a7d886093 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,6 @@ "express": "^4.16.4", "express-http-proxy": "^1.5.1", "graphql-tag": "^2.9.0", - "js-yaml": "^3.12.1", "mocha": "^6.1.4", "moment": "^2.23.0", "nodemon": "^1.18.9", @@ -68,6 +67,7 @@ "ts-node": "^8.1.0", "typescript": "^3.4.3", "vue-cli-plugin-apollo": "^0.20.0", + "vue-codemirror": "^4.0.6", "vue-template-compiler": "^2.5.21" }, "postcss": { diff --git a/src/lib/i18n/en.ts b/src/lib/i18n/en.ts index 35390b070686f2ba7a5f6c9fde6208ac4141a71a..d563eb752bdc9f3146c667221ba5275adbde5687 100644 --- a/src/lib/i18n/en.ts +++ b/src/lib/i18n/en.ts @@ -25,4 +25,7 @@ export const i18nLangEn: lang = { 'parameters.conf.file.explanation': 'This is your configuration file. You can directly edit it from here if you wish.', 'fork.view.too.much': 'Too much blocks requested: max is {0}.', 'parameters.menu.lang': 'Language', + 'parameters.conf.file.save': 'Test and save file', + 'parameters.conf.file.saved.and.applied': 'Configuration saved and server restarted.', + 'parameters.lang.changed': 'Language switched to « English »' } \ No newline at end of file diff --git a/src/lib/i18n/fr.ts b/src/lib/i18n/fr.ts index d40c24e732ecd7d63efc6082bae83e37708304d8..9bb5dae7a9238fdb2b4077098b514e40966bc95c 100644 --- a/src/lib/i18n/fr.ts +++ b/src/lib/i18n/fr.ts @@ -25,4 +25,7 @@ export const i18nLangFr: lang = { 'parameters.conf.file.explanation': 'Ceci est votre fichier de configuration. Vous pouvez le modifier directement ici si vous le souhaitez.', 'fork.view.too.much': 'Trop de blocs demandés : le maximum est de {0}.', 'parameters.menu.lang': 'Langage', + 'parameters.conf.file.save': 'Tester et sauvegarder le fichier', + 'parameters.conf.file.saved.and.applied': 'Fichier sauvegardé et serveur redémarré.', + 'parameters.lang.changed': 'Langue changée en « Français »', } \ No newline at end of file diff --git a/src/lib/i18n/lang.ts b/src/lib/i18n/lang.ts index de0c9a05ee8d69381c65fc435028b7d682d6a589..4bbc40eed03905568f87eead093d7966f8fdccd7 100644 --- a/src/lib/i18n/lang.ts +++ b/src/lib/i18n/lang.ts @@ -23,4 +23,7 @@ export interface lang { 'parameters.conf.file.explanation': string 'fork.view.too.much': string 'parameters.menu.lang': string + 'parameters.conf.file.save': string + 'parameters.conf.file.saved.and.applied': string + 'parameters.lang.changed': string } diff --git a/src/lib/services/webmin.service.ts b/src/lib/services/webmin.service.ts index 44b717b00952c3cbdc58b805ee2d5bf372a01194..149fb98dc7570c74ef72beb4c060e17b66f22b24 100644 --- a/src/lib/services/webmin.service.ts +++ b/src/lib/services/webmin.service.ts @@ -277,6 +277,41 @@ export class WebminService { .result() } + async getConf(): Promise<string> { + const res = await this.getApollo() + .watchQuery<{ getConf: string }>({ + query: gql` + query { + getConf + } + `, + }) + .result() + return res.data.getConf + } + + async testAndSaveConf(conf: string): Promise<string> { + const res = await this.getApollo() + .watchQuery<{ testAndSaveConf: string }>({ + query: gql` + query ($conf: String!) { + testAndSaveConf(conf: $conf) + } + `, + variables: { + conf + } + }) + // if (await res.getLastError()) { + // throw Error(res.getLastError().stack) + // } + const result = (await res.result()) + if (result.errors && result.errors.length) { + throw result.errors[0] + } + return result.data.testAndSaveConf + } + async uid(pub: string) { const res = await this.getApollo() .watchQuery({ diff --git a/src/views/home/Parameters.vue b/src/views/home/Parameters.vue index d64a0804eb6d488aa2f778b3a4ccc4bc6a97f052..66afcdaf186a38e64cb9c1e1cf9d9d62c825ff1e 100644 --- a/src/views/home/Parameters.vue +++ b/src/views/home/Parameters.vue @@ -36,6 +36,8 @@ export default class extends Vue { menus = [ + { route: '/home/parameters', name: 'parameters.menu.conf' }, + { route: '/home/parameters/network', name: 'parameters.menu.network' }, { route: '/home/parameters/lang', name: 'parameters.menu.lang' }, ] diff --git a/src/views/home/parameters/Conf.vue b/src/views/home/parameters/Conf.vue new file mode 100644 index 0000000000000000000000000000000000000000..6da1fa846ba4a5779a4bf645a89074306f9164f1 --- /dev/null +++ b/src/views/home/parameters/Conf.vue @@ -0,0 +1,136 @@ +<style lang="css"> + .CodeMirror, + .CodeMirror-merge, + .vue-codemirror.merge, + .vue-codemirror.merge > div, + .CodeMirror-merge.CodeMirror-merge-2pane > div { + height: 100%; + } + + .CodeMirror-merge .CodeMirror, + .CodeMirror-merge.CodeMirror-merge-2pane { + height: 100%; + } + + .CodeMirror-merge-l-inserted { + background-color: #aeffbc; + border-color: #aeffbc; + } + + .CodeMirror-merge-l-deleted { + background-color: #ffa799; + border-color: #ffa799; + } + + .CodeMirror-merge-left > .CodeMirror.cm-s-default { + background-color: #f1f1f1; + } +</style> + +<template> + + <div class="container-fluid"> + + <p>{{ $t('parameters.conf.file.explanation') }}</p> + + <p class="d-flex align-items-center"> + <b-spinner variant="success" label="Spinning" v-if="saving" class="mr-2"/> + <b-button + :disabled="saving || code === newCode" + :variant="saving || code === newCode ? 'outline-success' : 'success'" + @click="testAndSaveConf">{{ $t('parameters.conf.file.save') }}</b-button> + </p> + + <div class="row"> + <div class="col-12"> + <codemirror :merge="true" :options="cmOption" :key="cmOption.origLeft" @input="changeCode" ></codemirror> + </div> + </div> + + </div> + +</template> + +<script lang="ts"> + import {Component, Vue} from 'vue-property-decorator'; + import {codemirror} from 'vue-codemirror' + // require styles + import 'codemirror/lib/codemirror.css' + import 'codemirror/mode/javascript/javascript' + + // merge js + import 'codemirror/addon/merge/merge.js' + // merge css + import 'codemirror/addon/merge/merge.css' + // google DiffMatchPatch + import DiffMatchPatch from 'diff-match-patch' + // DiffMatchPatch config with global + (window as any).diff_match_patch = DiffMatchPatch; + (window as any).DIFF_DELETE = -1; + (window as any).DIFF_INSERT = 1; + (window as any).DIFF_EQUAL = 0; + + @Component({ + components: { + codemirror + }, + }) + export default class extends Vue { + + saving = false + newCode = '' + cmOption = { + value: '', + origLeft: '', + connect: 'align', + mode: 'text/javascript', + lineNumbers: true, + collapseIdentical: false, + highlightDifferences: true + } + + changeCode(newCode) { + this.newCode = newCode + } + + get code() { + return this.cmOption.value + } + + async beforeMount() { + await this.refresh() + } + + async refresh() { + const conf = await this.$webmin.getConf() + this.newCode = conf + this.cmOption.value = conf + this.cmOption.origLeft = conf + } + + async testAndSaveConf() { + this.saving = true + try { + const conf = await this.$webmin.testAndSaveConf(this.newCode) + this.cmOption.value = conf + this.cmOption.origLeft = conf + this.$bvToast.toast(this.$t('parameters.conf.file.saved.and.applied'), { + title: 'Success', + variant: 'success', + autoHideDelay: 8000, + appendToast: true, + }) + } catch (e) { + if (e.message.match(/Unexpected/)) { + this.$bvToast.toast(this.$t('parameters.conf.file.incorrect'), { + title: 'Error', + variant: 'danger', + autoHideDelay: 8000, + appendToast: true, + }) + } + } + this.saving = false + } + } +</script> diff --git a/src/views/home/parameters/Language.vue b/src/views/home/parameters/Language.vue index c70a8aa9d731d0038ecaf47eef55c56c4fd71dbb..18b0db02befb9027106efe7159487934aa1e5286 100644 --- a/src/views/home/parameters/Language.vue +++ b/src/views/home/parameters/Language.vue @@ -3,12 +3,7 @@ <template> - <div> - <select v-model="lang" @change="changeLocale"> - <option value="en">English</option> - <option value="fr">Français</option> - </select> - </div> + <b-form-select class="col-6" v-model="lang" :options="options" @change="changeLocale"/> </template> @@ -30,6 +25,19 @@ changeLocale() { this.$i18n.locale = this.lang localStorage.setItem(Constants.STORAGE_LANGUAGE, this.lang) + this.$bvToast.toast(this.$t('parameters.lang.changed'), { + title: 'Success', + variant: 'success', + autoHideDelay: 8000, + appendToast: true, + }) + } + + get options() { + return { + 'en': 'English', + 'fr': 'Français', + } } } </script> diff --git a/src/views/home/parameters/Network.vue b/src/views/home/parameters/Network.vue new file mode 100644 index 0000000000000000000000000000000000000000..15f73b1d6dbc60747fb015641cedc8023a206c14 --- /dev/null +++ b/src/views/home/parameters/Network.vue @@ -0,0 +1,23 @@ +<style lang="css" scoped> +</style> + +<template> + + <div> + <font-awesome-icon class="mr-3" icon="tools" style="font-size: 40px"/> + <font-awesome-icon class="mr-3" icon="hard-hat" style="font-size: 40px"/> + <font-awesome-icon class="mr-3" icon="exclamation-triangle" style="font-size: 40px"/> + </div> + +</template> + +<script lang="ts"> + import {Component, Vue} from 'vue-property-decorator'; + + @Component({ + components: {}, + }) + export default class extends Vue { + + } +</script> diff --git a/src/vue-modules/register-router.ts b/src/vue-modules/register-router.ts index af58b7c1fe010d0195a46b96bf44f0ea20b42d72..cb13b316575ba92891ea039fb25efa86d13c1baf 100644 --- a/src/vue-modules/register-router.ts +++ b/src/vue-modules/register-router.ts @@ -6,6 +6,8 @@ import Loader from '@/views/Loader.vue' import Sync from '@/views/Sync.vue' import Overview from '@/views/home/Overview.vue' import Parameters from '@/views/home/Parameters.vue' +import Conf from '@/views/home/parameters/Conf.vue' +import Network from '@/views/home/parameters/Network.vue' import Forkview from '@/views/home/Forkview.vue' import Language from '@/views/home/parameters/Language.vue' @@ -42,14 +44,6 @@ export default new Router({ name: 'parameters', component: Parameters, children: [{ - path: 'lang', - name: 'lang', - component: Language - }] - }] - }, - /* - { path: '', name: 'conf', component: Conf @@ -57,8 +51,13 @@ export default new Router({ path: 'network', name: 'network', component: Network - }, - */ + },{ + path: 'lang', + name: 'lang', + component: Language + }] + }] + }, { path: '/about', name: 'about', diff --git a/src/vue-modules/register-vue-apollo.ts b/src/vue-modules/register-vue-apollo.ts index c8c3defd9048a32801840a63f85a68a3405eb66f..72a0b14ac3e1ae3cb8d3210132cd5fa46f221a30 100644 --- a/src/vue-modules/register-vue-apollo.ts +++ b/src/vue-modules/register-vue-apollo.ts @@ -38,7 +38,7 @@ const apolloClient = new ApolloClient({ defaultOptions: { watchQuery: { fetchPolicy: 'network-only', - errorPolicy: 'ignore', + errorPolicy: 'all', }, query: { fetchPolicy: 'network-only', diff --git a/yarn.lock b/yarn.lock index ba951c9025bdd5383a345d68b0b56237d7c3a97c..41ae0106adf9d6920a473a3d2abc5f42a1f3362f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2304,6 +2304,11 @@ code-point-at@^1.0.0: resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= +codemirror@^5.41.0: + version "5.46.0" + resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.46.0.tgz#be3591572f88911e0105a007c324856a9ece0fb7" + integrity sha512-3QpMge0vg4QEhHW3hBAtCipJEWjTJrqLLXdIaWptJOblf1vHFeXLNtFhPai/uX2lnFCehWNk4yOdaMR853Z02w== + collection-visit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" @@ -3164,6 +3169,11 @@ dicer@0.3.0: dependencies: streamsearch "0.1.2" +diff-match-patch@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.4.tgz#6ac4b55237463761c4daf0dc603eb869124744b1" + integrity sha512-Uv3SW8bmH9nAtHKaKSanOQmj2DnlH65fUpcrMdfdaOxUG02QQ4YGZ8AE7kKOMisF7UqvOlGKVYWRvezdncW9lg== + diff@3.5.0, diff@^3.1.0, diff@^3.2.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" @@ -5504,7 +5514,7 @@ js-tokens@^4.0.0: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@3.13.1, js-yaml@^3.12.1, js-yaml@^3.13.0, js-yaml@^3.13.1, js-yaml@^3.9.0: +js-yaml@3.13.1, js-yaml@^3.13.0, js-yaml@^3.13.1, js-yaml@^3.9.0: version "3.13.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== @@ -10037,6 +10047,14 @@ vue-cli-plugin-apollo@^0.20.0: subscriptions-transport-ws "^0.9.16" ts-node "^8.0.3" +vue-codemirror@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/vue-codemirror/-/vue-codemirror-4.0.6.tgz#b786bb80d8d762a93aab8e46f79a81006f0437c4" + integrity sha512-ilU7Uf0mqBNSSV3KT7FNEeRIxH4s1fmpG4TfHlzvXn0QiQAbkXS9lLfwuZpaBVEnpP5CSE62iGJjoliTuA8poQ== + dependencies: + codemirror "^5.41.0" + diff-match-patch "^1.0.0" + vue-functional-data-merge@^2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/vue-functional-data-merge/-/vue-functional-data-merge-2.0.7.tgz#bdee655181eacdcb1f96ce95a4cc14e75313d1da"