Merge pull request #339 from YunoHost/uniformize-actionmap-api

[enh] Uniformize actionmap api + human readable actions
This commit is contained in:
Alexandre Aubin 2021-04-12 17:55:08 +02:00 committed by GitHub
commit c3d86065fa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 291 additions and 176 deletions

View file

@ -5,7 +5,6 @@
import store from '@/store' import store from '@/store'
import { openWebSocket, getResponseData, handleError } from './handlers' 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 { export default {
options: { options: {
credentials: 'include', credentials: 'include',
@ -55,9 +79,15 @@ export default {
* @param {Options} [options={ wait = true, websocket = true, initial = false, asFormData = false }] * @param {Options} [options={ wait = true, websocket = true, initial = false, asFormData = false }]
* @return {Promise<Object|Error>} Promise that resolve the api response data or an error. * @return {Promise<Object|Error>} 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. // `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) { if (websocket) {
await openWebSocket(request) await openWebSocket(request)
@ -70,6 +100,10 @@ export default {
options = { ...options, method, body: objectToParams(data, { addLocale: true }) } 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 response = await fetch('/yunohost/api/' + uri, options)
const responseData = await getResponseData(response) const responseData = await getResponseData(response)
store.dispatch('END_REQUEST', { request, success: response.ok, wait }) store.dispatch('END_REQUEST', { request, success: response.ok, wait })
@ -92,10 +126,10 @@ export default {
const results = [] const results = []
if (wait) store.commit('SET_WAITING', true) if (wait) store.commit('SET_WAITING', true)
try { try {
for (const [method, uri, data, options = {}] of queries) { for (const [method, uri, data, humanKey, options = {}] of queries) {
if (wait) options.wait = false if (wait) options.wait = false
if (initial) options.initial = true 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 { } finally {
// Stop waiting even if there is an error. // 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 }`) * @param {Options} [options={}] - options to apply to the call (default is `{ websocket: false, wait: false }`)
* @return {Promise<Object|Error>} Promise that resolve the api response data or an error. * @return {Promise<Object|Error>} 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 } options = { websocket: false, wait: false, ...options }
if (typeof uri === 'string') return this.fetch('GET', uri, null, options) if (typeof uri === 'string') return this.fetch('GET', uri, null, humanKey, options)
return store.dispatch('GET', { ...uri, options }) return store.dispatch('GET', { ...uri, humanKey, options })
}, },
@ -129,9 +163,9 @@ export default {
* @param {Options} [options={}] - options to apply to the call * @param {Options} [options={}] - options to apply to the call
* @return {Promise<Object|Error>} Promise that resolve the api response data or an error. * @return {Promise<Object|Error>} Promise that resolve the api response data or an error.
*/ */
post (uri, data = {}, options = {}) { post (uri, data = {}, humanKey = null, options = {}) {
if (typeof uri === 'string') return this.fetch('POST', uri, data, options) if (typeof uri === 'string') return this.fetch('POST', uri, data, humanKey, options)
return store.dispatch('POST', { ...uri, data, options }) return store.dispatch('POST', { ...uri, data, humanKey, options })
}, },
@ -143,9 +177,9 @@ export default {
* @param {Options} [options={}] - options to apply to the call * @param {Options} [options={}] - options to apply to the call
* @return {Promise<Object|Error>} Promise that resolve the api response data or an error. * @return {Promise<Object|Error>} Promise that resolve the api response data or an error.
*/ */
put (uri, data = {}, options = {}) { put (uri, data = {}, humanKey = null, options = {}) {
if (typeof uri === 'string') return this.fetch('PUT', uri, data, options) if (typeof uri === 'string') return this.fetch('PUT', uri, data, humanKey, options)
return store.dispatch('PUT', { ...uri, data, 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 }`) * @param {Options} [options={}] - options to apply to the call (default is `{ websocket: false, wait: false }`)
* @return {Promise<Object|Error>} Promise that resolve the api response data or an error. * @return {Promise<Object|Error>} Promise that resolve the api response data or an error.
*/ */
delete (uri, data = {}, options = {}) { delete (uri, data = {}, humanKey = null, options = {}) {
if (typeof uri === 'string') return this.fetch('DELETE', uri, data, options) if (typeof uri === 'string') return this.fetch('DELETE', uri, data, humanKey, options)
return store.dispatch('DELETE', { ...uri, data, options }) return store.dispatch('DELETE', { ...uri, data, humanKey, options })
} }
} }

View file

@ -1,2 +1,2 @@
export { default } from './api' export { default, objectToParams } from './api'
export { handleError, registerGlobalErrorHandlers } from './handlers' export { handleError, registerGlobalErrorHandlers } from './handlers'

View file

@ -5,8 +5,7 @@
<!-- REQUEST DESCRIPTION --> <!-- REQUEST DESCRIPTION -->
<strong class="request-desc"> <strong class="request-desc">
{{ request.uri | readableUri }} {{ request.humanRoute }}
<small>({{ $t('history.methods.' + request.method) }})</small>
</strong> </strong>
<div v-if="request.errors || request.warnings"> <div v-if="request.errors || request.warnings">

View file

@ -1,6 +1,3 @@
import store from '@/store'
/** /**
* Allow to set a timeout on a `Promise` expected response. * Allow to set a timeout on a `Promise` expected response.
* The returned Promise will be rejected if the original Promise is not resolved or * 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. * Check if passed value is an object literal.
* *

View file

@ -91,8 +91,8 @@
"confirm_app_default": "Are you sure you want to make this app default?", "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_change_maindomain": "Are you sure you want to change the main domain?",
"confirm_delete": "Are you sure you want to delete {name}?", "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_allow": "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_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_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_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?", "confirm_app_install": "Are you sure you want to install this application?",
@ -201,6 +201,7 @@
"groups_and_permissions_manage": "Manage groups and permissions", "groups_and_permissions_manage": "Manage groups and permissions",
"permissions": "Permissions", "permissions": "Permissions",
"history": { "history": {
"is_empty": "Nothing in history for now.",
"title": "History", "title": "History",
"last_action": "Last action:", "last_action": "Last action:",
"methods": { "methods": {
@ -330,6 +331,82 @@
"rerun_diagnosis": "Rerun diagnosis", "rerun_diagnosis": "Rerun diagnosis",
"restore": "Restore", "restore": "Restore",
"restart": "Restart", "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", "run": "Run",
"running": "Running", "running": "Running",
"save": "Save", "save": "Save",

View file

@ -33,12 +33,10 @@ body {
min-height: 100vh min-height: 100vh
} }
.menu-list { .menu-list .list-group-item {
.list-group-item {
padding: $list-group-item-padding-y 0; padding: $list-group-item-padding-y 0;
display: flex; display: flex;
align-items: center; align-items: center;
}
} }
@ -114,10 +112,6 @@ body {
top: 2px; top: 2px;
} }
.list-group-item .icon {
margin-left: 0.3rem;
}
// Fork-awesome overrides // Fork-awesome overrides
.fa-fw { .fa-fw {
width: 1.25em !important; width: 1.25em !important;

View file

@ -90,21 +90,21 @@ export default {
}, },
actions: { 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 noCache = !rootState.cache || options.noCache || false
const currentState = param ? state[storeKey][param] : state[storeKey] const currentState = param ? state[storeKey][param] : state[storeKey]
// if data has already been queried, simply return // if data has already been queried, simply return
if (currentState !== undefined && !noCache) return currentState 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 const data = responseData[storeKey] ? responseData[storeKey] : responseData
commit('SET_' + storeKey.toUpperCase(), param ? [param, data] : data) commit('SET_' + storeKey.toUpperCase(), param ? [param, data] : data)
return param ? state[storeKey][param] : state[storeKey] return param ? state[storeKey][param] : state[storeKey]
}) })
}, },
'POST' ({ state, commit }, { uri, storeKey = uri, data, options }) { 'POST' ({ state, commit }, { uri, storeKey = uri, data, humanKey, options }) {
return api.fetch('POST', uri, data, options).then(responseData => { return api.fetch('POST', uri, data, humanKey, options).then(responseData => {
// FIXME api/domains returns null // FIXME api/domains returns null
if (responseData === null) responseData = data if (responseData === null) responseData = data
responseData = responseData[storeKey] ? responseData[storeKey] : responseData responseData = responseData[storeKey] ? responseData[storeKey] : responseData
@ -113,16 +113,16 @@ export default {
}) })
}, },
'PUT' ({ state, commit }, { uri, param, storeKey = uri, data, options }) { 'PUT' ({ state, commit }, { uri, param, storeKey = uri, data, humanKey, options }) {
return api.fetch('PUT', param ? `${uri}/${param}` : uri, data, options).then(responseData => { return api.fetch('PUT', param ? `${uri}/${param}` : uri, data, humanKey, options).then(responseData => {
const data = responseData[storeKey] ? responseData[storeKey] : responseData const data = responseData[storeKey] ? responseData[storeKey] : responseData
commit('UPDATE_' + storeKey.toUpperCase(), param ? [param, data] : data) commit('UPDATE_' + storeKey.toUpperCase(), param ? [param, data] : data)
return param ? state[storeKey][param] : state[storeKey] return param ? state[storeKey][param] : state[storeKey]
}) })
}, },
'DELETE' ({ commit }, { uri, param, storeKey = uri, data, options }) { 'DELETE' ({ commit }, { uri, param, storeKey = uri, data, humanKey, options }) {
return api.fetch('DELETE', param ? `${uri}/${param}` : uri, data, options).then(() => { return api.fetch('DELETE', param ? `${uri}/${param}` : uri, data, humanKey, options).then(() => {
commit('DEL_' + storeKey.toUpperCase(), param) commit('DEL_' + storeKey.toUpperCase(), param)
}) })
} }

View file

@ -1,7 +1,8 @@
import Vue from 'vue' import Vue from 'vue'
import api from '@/api' import api from '@/api'
import router from '@/router' import router from '@/router'
import { timeout } from '@/helpers/commons' import i18n from '@/i18n'
import { timeout, isObjectLiteral } from '@/helpers/commons'
export default { export default {
state: { state: {
@ -107,7 +108,7 @@ export default {
}, },
'LOGIN' ({ dispatch }, password) { 'LOGIN' ({ dispatch }, password) {
return api.post('login', { password }, { websocket: false }).then(() => { return api.post('login', { password }, null, { websocket: false }).then(() => {
dispatch('CONNECT') dispatch('CONNECT')
}) })
}, },
@ -123,8 +124,12 @@ export default {
}) })
}, },
'INIT_REQUEST' ({ commit }, { method, uri, initial, wait, websocket }) { 'INIT_REQUEST' ({ commit }, { method, uri, humanKey, initial, wait, websocket }) {
let request = { method, uri, initial, status: 'pending' } // 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) { if (websocket) {
request = { ...request, messages: [], date: Date.now(), warnings: 0, errors: 0 } request = { ...request, messages: [], date: Date.now(), warnings: 0, errors: 0 }
commit('ADD_HISTORY_ACTION', request) commit('ADD_HISTORY_ACTION', request)

View file

@ -6,7 +6,7 @@
:key="item.routeName" :key="item.routeName"
:to="{ name: item.routeName }" :to="{ name: item.routeName }"
> >
<icon :iname="item.icon" class="lg" /> <icon :iname="item.icon" class="lg ml-1" />
<h4>{{ $t(item.translation) }}</h4> <h4>{{ $t(item.translation) }}</h4>
<icon iname="chevron-right" class="lg fs-sm ml-auto" /> <icon iname="chevron-right" class="lg fs-sm ml-auto" />
</b-list-group-item> </b-list-group-item>

View file

@ -33,6 +33,10 @@
class="accordion" role="tablist" class="accordion" role="tablist"
id="history" ref="history" id="history" ref="history"
> >
<p v-if="history.length === 0" class="alert m-0 px-2 py-1">
{{ $t('history.is_empty') }}
</p>
<!-- ACTION LIST --> <!-- ACTION LIST -->
<b-card <b-card
v-for="(action, i) in history" :key="i" v-for="(action, i) in history" :key="i"

View file

@ -38,11 +38,10 @@
</template> </template>
<script> <script>
import api from '@/api' import api, { objectToParams } from '@/api'
import { validationMixin } from 'vuelidate' import { validationMixin } from 'vuelidate'
import { formatI18nField, formatYunoHostArguments, formatFormData } from '@/helpers/yunohostArguments' import { formatI18nField, formatYunoHostArguments, formatFormData } from '@/helpers/yunohostArguments'
import { objectToParams } from '@/helpers/commons'
export default { export default {
name: 'AppActions', name: 'AppActions',
@ -100,7 +99,11 @@ export default {
// FIXME api expects at least one argument ?! (fake one given with { dontmindthis } ) // FIXME api expects at least one argument ?! (fake one given with { dontmindthis } )
const args = objectToParams(action.form ? formatFormData(action.form) : { dontmindthis: undefined }) const args = objectToParams(action.form ? formatFormData(action.form) : { dontmindthis: undefined })
api.put(`apps/${this.id}/actions/${action.id}`, { args }).then(response => { api.put(
`apps/${this.id}/actions/${action.id}`,
{ args },
{ key: 'apps.perform_action', action: action.id, name: this.id }
).then(() => {
this.$refs.view.fetchQueries() this.$refs.view.fetchQueries()
}).catch(err => { }).catch(err => {
if (err.name !== 'APIBadRequestError') throw err if (err.name !== 'APIBadRequestError') throw err

View file

@ -164,7 +164,7 @@ export default {
data () { data () {
return { return {
queries: [ queries: [
['GET', 'appscatalog?full&with_categories'] ['GET', 'apps/catalog?full&with_categories']
], ],
// Data // Data

View file

@ -38,9 +38,8 @@
import { validationMixin } from 'vuelidate' import { validationMixin } from 'vuelidate'
// FIXME needs test and rework // FIXME needs test and rework
import api from '@/api' import api, { objectToParams } from '@/api'
import { formatI18nField, formatYunoHostArguments, formatFormData } from '@/helpers/yunohostArguments' import { formatI18nField, formatYunoHostArguments, formatFormData } from '@/helpers/yunohostArguments'
import { objectToParams } from '@/helpers/commons'
export default { export default {
name: 'AppConfigPanel', name: 'AppConfigPanel',
@ -103,7 +102,9 @@ export default {
applyConfig (id_) { applyConfig (id_) {
const args = objectToParams(formatFormData(this.forms[id_])) const args = objectToParams(formatFormData(this.forms[id_]))
api.post(`apps/${this.id}/config`, { args }).then(response => { api.put(
`apps/${this.id}/config`, { args }, { key: 'apps.update_config', name: this.id }
).then(response => {
console.log('SUCCESS', response) console.log('SUCCESS', response)
}).catch(err => { }).catch(err => {
if (err.name !== 'APIBadRequestError') throw err if (err.name !== 'APIBadRequestError') throw err

View file

@ -252,7 +252,11 @@ export default {
changeLabel (permName, data) { changeLabel (permName, data) {
data.show_tile = data.show_tile ? 'True' : 'False' data.show_tile = data.show_tile ? 'True' : 'False'
api.put('users/permissions/' + permName, data).then(this.$refs.view.fetchQueries) api.put(
'users/permissions/' + permName,
data,
{ key: 'apps.change_label', prevName: this.infos.label, nextName: data.label }
).then(this.$refs.view.fetchQueries)
}, },
async changeUrl () { async changeUrl () {
@ -262,7 +266,8 @@ export default {
const { domain, path } = this.form.url const { domain, path } = this.form.url
api.put( api.put(
`apps/${this.id}/changeurl`, `apps/${this.id}/changeurl`,
{ domain, path: '/' + path } { domain, path: '/' + path },
{ key: 'apps.change_url', name: this.infos.label }
).then(this.$refs.view.fetchQueries) ).then(this.$refs.view.fetchQueries)
}, },
@ -270,7 +275,11 @@ export default {
const confirmed = await this.$askConfirmation(this.$i18n.t('confirm_app_default')) const confirmed = await this.$askConfirmation(this.$i18n.t('confirm_app_default'))
if (!confirmed) return if (!confirmed) return
api.put(`apps/${this.id}/default`).then(this.$refs.view.fetchQueries) api.put(
`apps/${this.id}/default`,
{},
{ key: 'apps.set_default', name: this.infos.label, domain: this.app.domain }
).then(this.$refs.view.fetchQueries)
}, },
async uninstall () { async uninstall () {
@ -279,7 +288,7 @@ export default {
) )
if (!confirmed) return if (!confirmed) return
api.delete('apps/' + this.id).then(() => { api.delete('apps/' + this.id, {}, { key: 'apps.uninstall', name: this.infos.label }).then(() => {
this.$router.push({ name: 'app-list' }) this.$router.push({ name: 'app-list' })
}) })
} }

View file

@ -49,8 +49,7 @@
<script> <script>
import { validationMixin } from 'vuelidate' import { validationMixin } from 'vuelidate'
import api from '@/api' import api, { objectToParams } from '@/api'
import { objectToParams } from '@/helpers/commons'
import { formatYunoHostArguments, formatI18nField, formatFormData } from '@/helpers/yunohostArguments' import { formatYunoHostArguments, formatI18nField, formatFormData } from '@/helpers/yunohostArguments'
export default { export default {
@ -93,7 +92,7 @@ export default {
}, },
getApiManifest () { getApiManifest () {
return api.get('appscatalog?full').then(response => response.apps[this.id].manifest) return api.get('apps/catalog?full').then(response => response.apps[this.id].manifest)
}, },
formatManifestData (manifest) { formatManifestData (manifest) {
@ -129,7 +128,7 @@ export default {
const { data: args, label } = formatFormData(this.form, { extract: ['label'] }) const { data: args, label } = formatFormData(this.form, { extract: ['label'] })
const data = { app: this.id, label, args: objectToParams(args) } const data = { app: this.id, label, args: objectToParams(args) }
api.post('apps', data).then(response => { api.post('apps', data, { key: 'apps.install', name: this.name }).then(() => {
this.$router.push({ name: 'app-list' }) this.$router.push({ name: 'app-list' })
}).catch(err => { }).catch(err => {
if (err.name !== 'APIBadRequestError') throw err if (err.name !== 'APIBadRequestError') throw err

View file

@ -164,7 +164,7 @@ export default {
} }
} }
api.post('backup', data).then(response => { api.post('backups', data, 'backups.create').then(() => {
this.$router.push({ name: 'backup-list', params: { id: this.id } }) this.$router.push({ name: 'backup-list', params: { id: this.id } })
}) })
} }

View file

@ -132,7 +132,7 @@ export default {
data () { data () {
return { return {
queries: [ queries: [
['GET', `backup/archives/${this.name}?with_details`] ['GET', `backups/${this.name}?with_details`]
], ],
selected: [], selected: [],
error: '', error: '',
@ -210,7 +210,9 @@ export default {
} }
} }
api.post('backup/restore/' + this.name, data).then(response => { api.put(
`backups/${this.name}/restore`, data, { key: 'backups.restore', name: this.name }
).then(() => {
this.isValid = null this.isValid = null
}).catch(err => { }).catch(err => {
if (err.name !== 'APIBadRequestError') throw err if (err.name !== 'APIBadRequestError') throw err
@ -223,14 +225,16 @@ export default {
const confirmed = await this.$askConfirmation(this.$i18n.t('confirm_delete', { name: this.name })) const confirmed = await this.$askConfirmation(this.$i18n.t('confirm_delete', { name: this.name }))
if (!confirmed) return if (!confirmed) return
api.delete('backup/archives/' + this.name).then(() => { api.delete(
'backups/' + this.name, {}, { key: 'backups.delete', name: this.name }
).then(() => {
this.$router.push({ name: 'backup-list', params: { id: this.id } }) this.$router.push({ name: 'backup-list', params: { id: this.id } })
}) })
}, },
downloadBackup () { downloadBackup () {
const host = this.$store.getters.host const host = this.$store.getters.host
window.open(`https://${host}/yunohost/api/backup/download/${this.name}`, '_blank') window.open(`https://${host}/yunohost/api/backups/${this.name}/download`, '_blank')
} }
}, },

View file

@ -45,7 +45,7 @@ export default {
data () { data () {
return { return {
queries: [ queries: [
['GET', 'backup/archives?with_info'] ['GET', 'backups?with_info']
], ],
archives: undefined archives: undefined
} }

View file

@ -42,7 +42,7 @@
</template> </template>
<template #header-buttons> <template #header-buttons>
<b-button size="sm" :variant="report.items ? 'info' : 'success'" @click="runDiagnosis(report.id)"> <b-button size="sm" :variant="report.items ? 'info' : 'success'" @click="runDiagnosis(report)">
<icon iname="refresh" /> {{ $t('rerun_diagnosis') }} <icon iname="refresh" /> {{ $t('rerun_diagnosis') }}
</b-button> </b-button>
</template> </template>
@ -64,13 +64,13 @@
<div class="d-flex flex-column flex-lg-row ml-auto"> <div class="d-flex flex-column flex-lg-row ml-auto">
<b-button <b-button
v-if="item.ignored" size="sm" v-if="item.ignored" size="sm"
@click="toggleIgnoreIssue(false, report, item)" @click="toggleIgnoreIssue('unignore', report, item)"
> >
<icon iname="bell" /> {{ $t('unignore') }} <icon iname="bell" /> {{ $t('unignore') }}
</b-button> </b-button>
<b-button <b-button
v-else-if="item.issue" variant="warning" size="sm" v-else-if="item.issue" variant="warning" size="sm"
@click="toggleIgnoreIssue(true, report, item)" @click="toggleIgnoreIssue('ignore', report, item)"
> >
<icon iname="bell-slash" /> {{ $t('ignore') }} <icon iname="bell-slash" /> {{ $t('ignore') }}
</b-button> </b-button>
@ -115,8 +115,8 @@ export default {
data () { data () {
return { return {
queries: [ queries: [
['POST', 'diagnosis/run?except_if_never_ran_yet'], ['PUT', 'diagnosis/run?except_if_never_ran_yet', {}, 'diagnosis.run'],
['GET', 'diagnosis/show?full'] ['GET', 'diagnosis?full']
], ],
reports: undefined reports: undefined
} }
@ -171,22 +171,30 @@ export default {
this.reports = reports this.reports = reports
}, },
runDiagnosis (id = null) { runDiagnosis ({ id = null, description } = {}) {
const param = id !== null ? '?force' : '' const param = id !== null ? '?force' : ''
const data = id !== null ? { categories: [id] } : {} const data = id !== null ? { categories: [id] } : {}
api.post('diagnosis/run' + param, data).then(this.$refs.view.fetchQueries)
api.put(
'diagnosis/run' + param,
data,
{ key: 'diagnosis.run' + (id !== null ? '_specific' : ''), description }
).then(this.$refs.view.fetchQueries)
}, },
toggleIgnoreIssue (ignore, report, item) { toggleIgnoreIssue (action, report, item) {
const key = (ignore ? 'add' : 'remove') + '_filter'
const filterArgs = Object.entries(item.meta).reduce((filterArgs, entries) => { const filterArgs = Object.entries(item.meta).reduce((filterArgs, entries) => {
filterArgs.push(entries.join('=')) filterArgs.push(entries.join('='))
return filterArgs return filterArgs
}, [report.id]) }, [report.id])
api.post('diagnosis/ignore', { [key]: filterArgs }).then(() => { api.put(
item.ignored = ignore 'diagnosis/' + action,
if (ignore) { { filter: filterArgs },
`diagnosis.${action}.${item.status.toLowerCase()}`
).then(() => {
item.ignored = action === 'ignore'
if (item.ignored) {
report[item.status.toLowerCase() + 's']-- report[item.status.toLowerCase() + 's']--
} else { } else {
report.ignoreds-- report.ignoreds--

View file

@ -27,8 +27,7 @@ export default {
onSubmit ({ domain, domainType }) { onSubmit ({ domain, domainType }) {
const uri = 'domains' + (domainType === 'dynDomain' ? '?dyndns' : '') const uri = 'domains' + (domainType === 'dynDomain' ? '?dyndns' : '')
api.post( api.post(
{ uri, storeKey: 'domains' }, { uri, storeKey: 'domains' }, { domain }, { key: 'domains.add', name: domain }
{ domain }
).then(() => { ).then(() => {
this.$router.push({ name: 'domain-list' }) this.$router.push({ name: 'domain-list' })
}).catch(err => { }).catch(err => {

View file

@ -84,7 +84,7 @@ export default {
data () { data () {
return { return {
queries: [ queries: [
['GET', `domains/cert-status/${this.name}?full`] ['GET', `domains/${this.name}/cert?full`]
], ],
cert: undefined, cert: undefined,
actionsEnabled: undefined actionsEnabled: undefined
@ -147,13 +147,13 @@ export default {
const confirmed = await this.$askConfirmation(this.$i18n.t(`confirm_cert_${action}`)) const confirmed = await this.$askConfirmation(this.$i18n.t(`confirm_cert_${action}`))
if (!confirmed) return if (!confirmed) return
let uri = 'domains/cert-install/' + this.name let uri = `domains/${this.name}/cert`
if (action === 'regen_selfsigned') uri += '?self_signed' if (action === 'regen_selfsigned') uri += '?self_signed'
else if (action === 'manual_renew_LE') uri += '?force' else if (action === 'manual_renew_LE') uri += '?force'
else if (action === 'revert_to_selfsigned') uri += '?self_signed&force' else if (action === 'revert_to_selfsigned') uri += '?self_signed&force'
// FIXME trigger loading ? while posting ? while getting ? api.put(
// this.$refs.view.fallback_loading = true uri, {}, { key: 'domains.' + action, name: this.name }
api.post(uri).then(this.$refs.view.fetchQueries) ).then(this.$refs.view.fetchQueries)
} }
} }
} }

View file

@ -83,7 +83,7 @@ export default {
if (!confirmed) return if (!confirmed) return
api.delete( api.delete(
{ uri: 'domains', param: this.name } { uri: 'domains', param: this.name }, {}, { key: 'domains.delete', name: this.name }
).then(() => { ).then(() => {
this.$router.push({ name: 'domain-list' }) this.$router.push({ name: 'domain-list' })
}) })
@ -94,8 +94,9 @@ export default {
if (!confirmed) return if (!confirmed) return
api.put( api.put(
{ uri: 'domains/main', storeKey: 'main_domain' }, { uri: `domains/${this.name}/main`, storeKey: 'main_domain' },
{ new_main_domain: this.name } {},
{ key: 'domains.set_default', name: this.name }
).then(() => { ).then(() => {
// FIXME Have to commit by hand here since the response is empty (should return the given name) // FIXME Have to commit by hand here since the response is empty (should return the given name)
this.$store.commit('UPDATE_MAIN_DOMAIN', this.name) this.$store.commit('UPDATE_MAIN_DOMAIN', this.name)

View file

@ -45,7 +45,8 @@ export default {
onSubmit () { onSubmit () {
api.post( api.post(
{ uri: 'users/groups', storeKey: 'groups' }, { uri: 'users/groups', storeKey: 'groups' },
this.form this.form,
{ key: 'groups.create', name: this.form.groupname }
).then(() => { ).then(() => {
this.$router.push({ name: 'group-list' }) this.$router.push({ name: 'group-list' })
}).catch(err => { }).catch(err => {

View file

@ -216,22 +216,28 @@ export default {
}, },
onPermissionChanged ({ item, index, name, groupType, action }) { onPermissionChanged ({ item, index, name, groupType, action }) {
const uri = 'users/permissions/' + item // const uri = 'users/permissions/' + item
const data = { [action]: name } // const data = { [action]: name }
const from = action === 'add' ? 'availablePermissions' : 'permissions' const from = action === 'add' ? 'availablePermissions' : 'permissions'
const to = action === 'add' ? 'permissions' : 'availablePermissions' const to = action === 'add' ? 'permissions' : 'availablePermissions'
api.put(uri, data).then(() => { api.put(
`users/permissions/${item}/${action}/${name}`,
{},
{ key: 'permissions.' + action, perm: item.replace('.main', ''), name }
).then(() => {
this[groupType + 'Groups'][name][from].splice(index, 1) this[groupType + 'Groups'][name][from].splice(index, 1)
this[groupType + 'Groups'][name][to].push(item) this[groupType + 'Groups'][name][to].push(item)
}) })
}, },
onUserChanged ({ item, index, name, action }) { onUserChanged ({ item, index, name, action }) {
const uri = 'users/groups/' + name
const data = { [action]: item }
const from = action === 'add' ? 'availableMembers' : 'members' const from = action === 'add' ? 'availableMembers' : 'members'
const to = action === 'add' ? 'members' : 'availableMembers' const to = action === 'add' ? 'members' : 'availableMembers'
api.put(uri, data).then(() => { api.put(
`users/groups/${name}/${action}/${item}`,
{},
{ key: 'groups.' + action, user: item, name }
).then(() => {
this.normalGroups[name][from].splice(index, 1) this.normalGroups[name][from].splice(index, 1)
this.normalGroups[name][to].push(item) this.normalGroups[name][to].push(item)
}) })
@ -255,7 +261,7 @@ export default {
if (!confirmed) return if (!confirmed) return
api.delete( api.delete(
{ uri: 'users/groups', param: name, storeKey: 'groups' } { uri: 'users/groups', param: name, storeKey: 'groups' }, {}, { key: 'groups.delete', name }
).then(() => { ).then(() => {
Vue.delete(this.normalGroups, name) Vue.delete(this.normalGroups, name)
}) })

View file

@ -120,13 +120,11 @@ export default {
) )
if (!confirmed) return if (!confirmed) return
if (!['start', 'restart', 'stop'].includes(action)) return api.put(
const method = action === 'stop' ? 'delete' : 'put' `services/${this.name}/${action}`,
const uri = action === 'restart' {},
? `services/${this.name}/restart` { key: 'services.' + action, name: this.name }
: 'services/' + this.name ).then(this.$refs.view.fetchQueries)
api[method](uri).then(this.$refs.view.fetchQueries)
}, },
shareLogs () { shareLogs () {

View file

@ -44,8 +44,8 @@ export default {
this.serverError = '' this.serverError = ''
api.fetchAll( api.fetchAll(
[['POST', 'login', { password: currentPassword }, { websocket: false }], [['POST', 'login', { password: currentPassword }, null, { websocket: false }],
['PUT', 'admisnpw', { new_password: password }]], ['PUT', 'adminpw', { new_password: password }, 'adminpw']],
{ wait: true } { wait: true }
).then(() => { ).then(() => {
this.$store.dispatch('DISCONNECT') this.$store.dispatch('DISCONNECT')

View file

@ -115,8 +115,8 @@ export default {
// Ports form data // Ports form data
actionChoices: [ actionChoices: [
{ value: 'open', text: this.$i18n.t('open') }, { value: 'allow', text: this.$i18n.t('open') },
{ value: 'close', text: this.$i18n.t('close') } { value: 'disallow', text: this.$i18n.t('close') }
], ],
connectionChoices: [ connectionChoices: [
{ value: 'ipv4', text: this.$i18n.t('ipv4') }, { value: 'ipv4', text: this.$i18n.t('ipv4') },
@ -128,7 +128,7 @@ export default {
{ value: 'Both', text: this.$i18n.t('both') } { value: 'Both', text: this.$i18n.t('both') }
], ],
form: { form: {
action: 'open', action: 'allow',
port: undefined, port: undefined,
connection: 'ipv4', connection: 'ipv4',
protocol: 'TCP' protocol: 'TCP'
@ -176,27 +176,21 @@ export default {
this.upnpEnabled = data.uPnP.enabled this.upnpEnabled = data.uPnP.enabled
}, },
togglePort ({ action, port, protocol, connection }) { async togglePort ({ action, port, protocol, connection }) {
return new Promise((resolve, reject) => { const confirmed = await this.$askConfirmation(
this.$askConfirmation(
this.$i18n.t('confirm_firewall_' + action, { port, protocol, connection }) this.$i18n.t('confirm_firewall_' + action, { port, protocol, connection })
).then(confirmed => { )
if (confirmed) { if (!confirmed) {
const method = action === 'open' ? 'post' : 'delete' return Promise.resolve(confirmed)
api[method](
`/firewall/port?${connection}_only`,
{ port, protocol },
{ wait: false }
).then(() => {
resolve(confirmed)
}).catch(error => {
reject(error)
})
} else {
resolve(confirmed)
} }
})
}) const actionTrad = this.$i18n.t({ allow: 'open', disallow: 'close' }[action])
return api.put(
`firewall/${protocol}/${action}/${port}?${connection}_only`,
{},
{ key: 'firewall.ports', protocol, action: actionTrad, port, connection },
{ wait: false }
).then(() => confirmed)
}, },
async toggleUpnp (value) { async toggleUpnp (value) {
@ -204,7 +198,11 @@ export default {
const confirmed = await this.$askConfirmation(this.$i18n.t('confirm_upnp_' + action)) const confirmed = await this.$askConfirmation(this.$i18n.t('confirm_upnp_' + action))
if (!confirmed) return if (!confirmed) return
api.get('firewall/upnp?action=' + action, null, { websocket: true, wait: true }).then(() => { api.put(
'firewall/upnp/' + action,
{},
{ key: 'firewall.upnp', action: this.$i18n.t(action) }
).then(() => {
// FIXME Couldn't test when it works. // FIXME Couldn't test when it works.
this.$refs.view.fetchQueries() this.$refs.view.fetchQueries()
}).catch(err => { }).catch(err => {
@ -215,7 +213,7 @@ export default {
onTablePortToggling (port, protocol, connection, index, value) { onTablePortToggling (port, protocol, connection, index, value) {
this.$set(this.protocols[protocol][index], connection, value) this.$set(this.protocols[protocol][index], connection, value)
const action = value ? 'open' : 'close' const action = value ? 'allow' : 'disallow'
this.togglePort({ action, port, protocol, connection }).then(toggled => { this.togglePort({ action, port, protocol, connection }).then(toggled => {
// Revert change on cancel // Revert change on cancel
if (!toggled) { if (!toggled) {

View file

@ -6,7 +6,7 @@
:key="item.routeName" :key="item.routeName"
:to="{name: item.routeName}" :to="{name: item.routeName}"
> >
<icon :iname="item.icon" class="lg" /> <icon :iname="item.icon" class="lg ml-1" />
<h4>{{ $t(item.translation) }}</h4> <h4>{{ $t(item.translation) }}</h4>
<icon iname="chevron-right" class="lg fs-sm ml-auto" /> <icon iname="chevron-right" class="lg fs-sm ml-auto" />
</b-list-group-item> </b-list-group-item>

View file

@ -60,8 +60,8 @@
</template> </template>
<script> <script>
import api from '@/api' import api, { objectToParams } from '@/api'
import { objectToParams, escapeHtml } from '@/helpers/commons' import { escapeHtml } from '@/helpers/commons'
import { readableDate } from '@/helpers/filters/date' import { readableDate } from '@/helpers/filters/date'
export default { export default {
@ -127,7 +127,12 @@ export default {
}, },
shareLogs () { shareLogs () {
api.get(`logs/${this.name}?share`, null, { websocket: true }).then(({ url }) => { api.get(
`logs/${this.name}/share`,
null,
{ key: 'share_logs', name: this.name },
{ websocket: true }
).then(({ url }) => {
window.open(url, '_blank') window.open(url, '_blank')
}) })
} }

View file

@ -121,7 +121,7 @@ export default {
} }
// Check that every migration's disclaimer has been checked. // Check that every migration's disclaimer has been checked.
if (Object.values(this.checked).every(value => value === true)) { if (Object.values(this.checked).every(value => value === true)) {
api.post('migrations/run?accept_disclaimer').then(() => { api.post('migrations/run?accept_disclaimer', 'migrations.run').then(() => {
this.$refs.view.fetchQueries() this.$refs.view.fetchQueries()
}) })
} }
@ -130,8 +130,7 @@ export default {
async skipMigration (id) { async skipMigration (id) {
const confirmed = await this.$askConfirmation(this.$i18n.t('confirm_migrations_skip')) const confirmed = await this.$askConfirmation(this.$i18n.t('confirm_migrations_skip'))
if (!confirmed) return if (!confirmed) return
api.post('/migrations/run', { skip: '', targets: id }, 'migration.skip').then(() => {
api.post('/migrations/run', { skip: '', targets: id }).then(() => {
this.$refs.view.fetchQueries() this.$refs.view.fetchQueries()
}) })
} }

View file

@ -66,7 +66,7 @@ export default {
if (!confirmed) return if (!confirmed) return
this.action = action this.action = action
api.put(action + '?force').then(() => { api.put(action + '?force', {}, action).then(() => {
// Use 'RESET_CONNECTED' and not 'DISCONNECT' else user will be redirect to login // Use 'RESET_CONNECTED' and not 'DISCONNECT' else user will be redirect to login
this.$store.dispatch('RESET_CONNECTED') this.$store.dispatch('RESET_CONNECTED')
this.inProcess = true this.inProcess = true

View file

@ -74,7 +74,7 @@ export default {
return { return {
queries: [ queries: [
['GET', 'migrations?pending'], ['GET', 'migrations?pending'],
['PUT', 'update'] ['PUT', 'update/all', {}, 'update']
], ],
// API data // API data
migrationsNotDone: undefined, migrationsNotDone: undefined,
@ -95,11 +95,8 @@ export default {
const confirmed = await this.$askConfirmation(confirmMsg) const confirmed = await this.$askConfirmation(confirmMsg)
if (!confirmed) return if (!confirmed) return
const uri = type === 'specific_app' const uri = id !== null ? `apps/${id}/upgrade` : 'upgrade/' + type
? 'upgrade/apps?app=' + id api.put(uri, {}, { key: 'upgrade.' + (id ? 'app' : type), app: id }).then(() => {
: 'upgrade?' + type
api.put(uri).then(() => {
this.$router.push({ name: 'tool-logs' }) this.$router.push({ name: 'tool-logs' })
}) })
} }

View file

@ -175,7 +175,7 @@ export default {
onSubmit () { onSubmit () {
const data = formatFormData(this.form, { flatten: true }) const data = formatFormData(this.form, { flatten: true })
api.post({ uri: 'users' }, data).then(() => { api.post({ uri: 'users' }, data, { key: 'users.create', name: this.form.username }).then(() => {
this.$router.push({ name: 'user-list' }) this.$router.push({ name: 'user-list' })
}).catch(err => { }).catch(err => {
if (err.name !== 'APIBadRequestError') throw err if (err.name !== 'APIBadRequestError') throw err

View file

@ -296,7 +296,8 @@ export default {
api.put( api.put(
{ uri: 'users', param: this.name, storeKey: 'users_details' }, { uri: 'users', param: this.name, storeKey: 'users_details' },
data data,
{ key: 'users.update', name: this.name }
).then(() => { ).then(() => {
this.$router.push({ name: 'user-info', param: { name: this.name } }) this.$router.push({ name: 'user-info', param: { name: this.name } })
}).catch(err => { }).catch(err => {

View file

@ -108,7 +108,8 @@ export default {
const data = this.purge ? { purge: '' } : {} const data = this.purge ? { purge: '' } : {}
api.delete( api.delete(
{ uri: 'users', param: this.name, storeKey: 'users_details' }, { uri: 'users', param: this.name, storeKey: 'users_details' },
data data,
{ key: 'users.delete', name: this.name }
).then(() => { ).then(() => {
this.$router.push({ name: 'user-list' }) this.$router.push({ name: 'user-list' })
}) })