diff --git a/app/src/api/api.js b/app/src/api/api.js index d199dccb..0d9f4426 100644 --- a/app/src/api/api.js +++ b/app/src/api/api.js @@ -5,7 +5,6 @@ import store from '@/store' import { openWebSocket, getResponseData, handleError } from './handlers' -import { objectToParams } from '@/helpers/commons' /** @@ -31,6 +30,31 @@ import { objectToParams } from '@/helpers/commons' */ +/** + * Converts an object literal into an `URLSearchParams` that can be turned into a + * query string or used as a body in a `fetch` call. + * + * @param {Object} obj - An object literal to convert. + * @param {Object} options + * @param {Boolean} [options.addLocale=false] - Option to append the locale to the query string. + * @return {URLSearchParams} + */ +export function objectToParams (obj, { addLocale = false } = {}) { + const urlParams = new URLSearchParams() + for (const [key, value] of Object.entries(obj)) { + if (Array.isArray(value)) { + value.forEach(v => urlParams.append(key, v)) + } else { + urlParams.append(key, value) + } + } + if (addLocale) { + urlParams.append('locale', store.getters.locale) + } + return urlParams +} + + export default { options: { credentials: 'include', @@ -55,9 +79,15 @@ export default { * @param {Options} [options={ wait = true, websocket = true, initial = false, asFormData = false }] * @return {Promise} Promise that resolve the api response data or an error. */ - async fetch (method, uri, data = {}, { wait = true, websocket = true, initial = false, asFormData = false } = {}) { + async fetch ( + method, + uri, + data = {}, + humanKey = null, + { wait = true, websocket = true, initial = false, asFormData = false } = {} + ) { // `await` because Vuex actions returns promises by default. - const request = await store.dispatch('INIT_REQUEST', { method, uri, initial, wait, websocket }) + const request = await store.dispatch('INIT_REQUEST', { method, uri, humanKey, initial, wait, websocket }) if (websocket) { await openWebSocket(request) @@ -70,6 +100,10 @@ export default { options = { ...options, method, body: objectToParams(data, { addLocale: true }) } } + if (['upgrade', 'postinstall', 'reboot', 'shutdown', 'diagnsosis'].some(action => uri.includes(action))) { + store.dispatch('END_REQUEST', { request, success: true, wait }) + return + } const response = await fetch('/yunohost/api/' + uri, options) const responseData = await getResponseData(response) store.dispatch('END_REQUEST', { request, success: response.ok, wait }) @@ -92,10 +126,10 @@ export default { const results = [] if (wait) store.commit('SET_WAITING', true) try { - for (const [method, uri, data, options = {}] of queries) { + for (const [method, uri, data, humanKey, options = {}] of queries) { if (wait) options.wait = false if (initial) options.initial = true - results.push(await this[method.toLowerCase()](uri, data, options)) + results.push(await this[method.toLowerCase()](uri, data, humanKey, options)) } } finally { // Stop waiting even if there is an error. @@ -114,10 +148,10 @@ export default { * @param {Options} [options={}] - options to apply to the call (default is `{ websocket: false, wait: false }`) * @return {Promise} Promise that resolve the api response data or an error. */ - get (uri, data = null, options = {}) { + get (uri, data = null, humanKey = null, options = {}) { options = { websocket: false, wait: false, ...options } - if (typeof uri === 'string') return this.fetch('GET', uri, null, options) - return store.dispatch('GET', { ...uri, options }) + if (typeof uri === 'string') return this.fetch('GET', uri, null, humanKey, options) + return store.dispatch('GET', { ...uri, humanKey, options }) }, @@ -129,9 +163,9 @@ export default { * @param {Options} [options={}] - options to apply to the call * @return {Promise} Promise that resolve the api response data or an error. */ - post (uri, data = {}, options = {}) { - if (typeof uri === 'string') return this.fetch('POST', uri, data, options) - return store.dispatch('POST', { ...uri, data, options }) + post (uri, data = {}, humanKey = null, options = {}) { + if (typeof uri === 'string') return this.fetch('POST', uri, data, humanKey, options) + return store.dispatch('POST', { ...uri, data, humanKey, options }) }, @@ -143,9 +177,9 @@ export default { * @param {Options} [options={}] - options to apply to the call * @return {Promise} Promise that resolve the api response data or an error. */ - put (uri, data = {}, options = {}) { - if (typeof uri === 'string') return this.fetch('PUT', uri, data, options) - return store.dispatch('PUT', { ...uri, data, options }) + put (uri, data = {}, humanKey = null, options = {}) { + if (typeof uri === 'string') return this.fetch('PUT', uri, data, humanKey, options) + return store.dispatch('PUT', { ...uri, data, humanKey, options }) }, @@ -157,8 +191,8 @@ export default { * @param {Options} [options={}] - options to apply to the call (default is `{ websocket: false, wait: false }`) * @return {Promise} Promise that resolve the api response data or an error. */ - delete (uri, data = {}, options = {}) { - if (typeof uri === 'string') return this.fetch('DELETE', uri, data, options) - return store.dispatch('DELETE', { ...uri, data, options }) + delete (uri, data = {}, humanKey = null, options = {}) { + if (typeof uri === 'string') return this.fetch('DELETE', uri, data, humanKey, options) + return store.dispatch('DELETE', { ...uri, data, humanKey, options }) } } diff --git a/app/src/api/index.js b/app/src/api/index.js index 4c919aa1..c3f1ae7f 100644 --- a/app/src/api/index.js +++ b/app/src/api/index.js @@ -1,2 +1,2 @@ -export { default } from './api' +export { default, objectToParams } from './api' export { handleError, registerGlobalErrorHandlers } from './handlers' diff --git a/app/src/components/QueryHeader.vue b/app/src/components/QueryHeader.vue index 0109979f..4f6d8cff 100644 --- a/app/src/components/QueryHeader.vue +++ b/app/src/components/QueryHeader.vue @@ -5,8 +5,7 @@ - {{ request.uri | readableUri }} - ({{ $t('history.methods.' + request.method) }}) + {{ request.humanRoute }}
diff --git a/app/src/helpers/commons.js b/app/src/helpers/commons.js index 7a94f386..b9510510 100644 --- a/app/src/helpers/commons.js +++ b/app/src/helpers/commons.js @@ -1,6 +1,3 @@ -import store from '@/store' - - /** * Allow to set a timeout on a `Promise` expected response. * The returned Promise will be rejected if the original Promise is not resolved or @@ -19,31 +16,6 @@ export function timeout (promise, delay) { } -/** - * Converts an object literal into an `URLSearchParams` that can be turned into a - * query string or used as a body in a `fetch` call. - * - * @param {Object} obj - An object literal to convert. - * @param {Object} options - * @param {Boolean} [options.addLocale=false] - Option to append the locale to the query string. - * @return {URLSearchParams} - */ -export function objectToParams (obj, { addLocale = false } = {}) { - const urlParams = new URLSearchParams() - for (const [key, value] of Object.entries(obj)) { - if (Array.isArray(value)) { - value.forEach(v => urlParams.append(key, v)) - } else { - urlParams.append(key, value) - } - } - if (addLocale) { - urlParams.append('locale', store.getters.locale) - } - return urlParams -} - - /** * Check if passed value is an object literal. * diff --git a/app/src/i18n/locales/en.json b/app/src/i18n/locales/en.json index 4e192826..e7c4a57a 100644 --- a/app/src/i18n/locales/en.json +++ b/app/src/i18n/locales/en.json @@ -91,8 +91,8 @@ "confirm_app_default": "Are you sure you want to make this app default?", "confirm_change_maindomain": "Are you sure you want to change the main domain?", "confirm_delete": "Are you sure you want to delete {name}?", - "confirm_firewall_open": "Are you sure you want to open port {port} (protocol: {protocol}, connection: {connection})", - "confirm_firewall_close": "Are you sure you want to close port {port} (protocol: {protocol}, connection: {connection})", + "confirm_firewall_allow": "Are you sure you want to open port {port} (protocol: {protocol}, connection: {connection})", + "confirm_firewall_disallow": "Are you sure you want to close port {port} (protocol: {protocol}, connection: {connection})", "confirm_install_custom_app": "WARNING! Installing 3rd party applications may compromise the integrity and security of your system. You should probably NOT install it unless you know what you are doing. Are you willing to take that risk?", "confirm_install_domain_root": "Are you sure you want to install this application on '/'? You will not be able to install any other app on {domain}", "confirm_app_install": "Are you sure you want to install this application?", @@ -201,6 +201,7 @@ "groups_and_permissions_manage": "Manage groups and permissions", "permissions": "Permissions", "history": { + "is_empty": "Nothing in history for now.", "title": "History", "last_action": "Last action:", "methods": { @@ -330,6 +331,82 @@ "rerun_diagnosis": "Rerun diagnosis", "restore": "Restore", "restart": "Restart", + "human_routes": { + "adminpw": "Change admin password", + "apps": { + "change_label": "Change label of '{prevName}' for '{nextName}'", + "change_url": "Change access url of '{name}'", + "install": "Install app '{name}'", + "set_default": "Redirect '{domain}' domain root to '{name}'", + "perform_action": "Perform action '{action}' of app '{name}'", + "uninstall": "Uninstall app '{name}'", + "update_config": "Update app '{name}' configuration" + }, + "backups": { + "create": "Create a backup", + "delete": "Delete backup '{name}'", + "restore": "Restore backup '{name}'" + }, + "diagnosis": { + "ignore": { + "error": "Ignore an error", + "warning": "Ignore a warning" + }, + "run": "Run the diagnosis", + "run_specific": "Run '{description}' diagnosis", + "unignore": { + "error": "Unignore an error", + "warning": "Unignore a warning" + } + }, + "domains": { + "add": "Add domain '{name}'", + "delete": "Delete domain '{name}'", + "install_LE": "Install certificate for '{name}'", + "manual_renew_LE": "Renew certificate for '{name}'", + "regen_selfsigned": "Renew self-signed certificate for '{name}'", + "revert_to_selfsigned": "Revert to self-signed certificate for '{name}'", + "set_default": "Set '{name}' as default domain" + }, + "firewall": { + "ports": "{action} port {port} ({protocol}, {connection})", + "upnp": "{action} UPnP" + }, + "groups": { + "create": "Create group '{name}'", + "delete": "Delete group '{name}'", + "add": "Add '{user}' to group '{name}'", + "remove": "Remove '{user}' from group '{name}'" + }, + "migrations": { + "run": "Run migrations", + "skip": "Skip migrations" + }, + "permissions": { + "add": "Allow '{name}' to access '{perm}'", + "remove": "Remove '{name}' access to '{perm}'" + }, + "postinstall": "Run the post-install", + "reboot": "Reboot the server", + "services": { + "restart": "Restart the service '{name}'", + "start": "Start the service '{name}'", + "stop": "Stop the service '{name}'" + }, + "share_logs": "Generate link for log '{name}'", + "shutdown": "Shutdown the server", + "update": "Check for updates", + "upgrade": { + "system": "Upgrade the system", + "apps": "Upgrade all apps", + "app": "Upgrade '{app}' app" + }, + "users": { + "create": "Create user '{name}'", + "delete": "Delete user '{name}'", + "update": "Update user '{name}'" + } + }, "run": "Run", "running": "Running", "save": "Save", diff --git a/app/src/scss/main.scss b/app/src/scss/main.scss index c33992bf..83096b46 100644 --- a/app/src/scss/main.scss +++ b/app/src/scss/main.scss @@ -33,12 +33,10 @@ body { min-height: 100vh } -.menu-list { - .list-group-item { - padding: $list-group-item-padding-y 0; - display: flex; - align-items: center; - } +.menu-list .list-group-item { + padding: $list-group-item-padding-y 0; + display: flex; + align-items: center; } @@ -114,10 +112,6 @@ body { top: 2px; } -.list-group-item .icon { - margin-left: 0.3rem; -} - // Fork-awesome overrides .fa-fw { width: 1.25em !important; diff --git a/app/src/store/data.js b/app/src/store/data.js index 73747784..fc56801d 100644 --- a/app/src/store/data.js +++ b/app/src/store/data.js @@ -90,21 +90,21 @@ export default { }, actions: { - 'GET' ({ state, commit, rootState }, { uri, param, storeKey = uri, options = {} }) { + 'GET' ({ state, commit, rootState }, { uri, param, humanKey, storeKey = uri, options = {} }) { const noCache = !rootState.cache || options.noCache || false const currentState = param ? state[storeKey][param] : state[storeKey] // if data has already been queried, simply return if (currentState !== undefined && !noCache) return currentState - return api.fetch('GET', param ? `${uri}/${param}` : uri, null, options).then(responseData => { + return api.fetch('GET', param ? `${uri}/${param}` : uri, null, humanKey, options).then(responseData => { const data = responseData[storeKey] ? responseData[storeKey] : responseData commit('SET_' + storeKey.toUpperCase(), param ? [param, data] : data) return param ? state[storeKey][param] : state[storeKey] }) }, - 'POST' ({ state, commit }, { uri, storeKey = uri, data, options }) { - return api.fetch('POST', uri, data, options).then(responseData => { + 'POST' ({ state, commit }, { uri, storeKey = uri, data, humanKey, options }) { + return api.fetch('POST', uri, data, humanKey, options).then(responseData => { // FIXME api/domains returns null if (responseData === null) responseData = data responseData = responseData[storeKey] ? responseData[storeKey] : responseData @@ -113,16 +113,16 @@ export default { }) }, - 'PUT' ({ state, commit }, { uri, param, storeKey = uri, data, options }) { - return api.fetch('PUT', param ? `${uri}/${param}` : uri, data, options).then(responseData => { + 'PUT' ({ state, commit }, { uri, param, storeKey = uri, data, humanKey, options }) { + return api.fetch('PUT', param ? `${uri}/${param}` : uri, data, humanKey, options).then(responseData => { const data = responseData[storeKey] ? responseData[storeKey] : responseData commit('UPDATE_' + storeKey.toUpperCase(), param ? [param, data] : data) return param ? state[storeKey][param] : state[storeKey] }) }, - 'DELETE' ({ commit }, { uri, param, storeKey = uri, data, options }) { - return api.fetch('DELETE', param ? `${uri}/${param}` : uri, data, options).then(() => { + 'DELETE' ({ commit }, { uri, param, storeKey = uri, data, humanKey, options }) { + return api.fetch('DELETE', param ? `${uri}/${param}` : uri, data, humanKey, options).then(() => { commit('DEL_' + storeKey.toUpperCase(), param) }) } diff --git a/app/src/store/info.js b/app/src/store/info.js index 89d1a603..de7f4647 100644 --- a/app/src/store/info.js +++ b/app/src/store/info.js @@ -1,7 +1,8 @@ import Vue from 'vue' import api from '@/api' import router from '@/router' -import { timeout } from '@/helpers/commons' +import i18n from '@/i18n' +import { timeout, isObjectLiteral } from '@/helpers/commons' export default { state: { @@ -107,7 +108,7 @@ export default { }, 'LOGIN' ({ dispatch }, password) { - return api.post('login', { password }, { websocket: false }).then(() => { + return api.post('login', { password }, null, { websocket: false }).then(() => { dispatch('CONNECT') }) }, @@ -123,8 +124,12 @@ export default { }) }, - 'INIT_REQUEST' ({ commit }, { method, uri, initial, wait, websocket }) { - let request = { method, uri, initial, status: 'pending' } + 'INIT_REQUEST' ({ commit }, { method, uri, humanKey, initial, wait, websocket }) { + // Try to find a description for an API route to display in history and modals + const { key, ...args } = isObjectLiteral(humanKey) ? humanKey : { key: humanKey } + const humanRoute = key ? i18n.t('human_routes.' + key, args) : `[${method}] /${uri}` + + let request = { method, uri, humanRoute, initial, status: 'pending' } if (websocket) { request = { ...request, messages: [], date: Date.now(), warnings: 0, errors: 0 } commit('ADD_HISTORY_ACTION', request) diff --git a/app/src/views/Home.vue b/app/src/views/Home.vue index f784fa13..8d28d1e6 100644 --- a/app/src/views/Home.vue +++ b/app/src/views/Home.vue @@ -6,7 +6,7 @@ :key="item.routeName" :to="{ name: item.routeName }" > - +

{{ $t(item.translation) }}

diff --git a/app/src/views/_partials/HistoryConsole.vue b/app/src/views/_partials/HistoryConsole.vue index 6d0a18ac..6b004a54 100644 --- a/app/src/views/_partials/HistoryConsole.vue +++ b/app/src/views/_partials/HistoryConsole.vue @@ -33,6 +33,10 @@ class="accordion" role="tablist" id="history" ref="history" > +

+ {{ $t('history.is_empty') }} +

+