mirror of
https://github.com/YunoHost/yunohost-admin.git
synced 2024-09-03 20:06:15 +02:00
add AppInstall view/route
This commit is contained in:
parent
84374cdd53
commit
b1b9e65b7b
5 changed files with 304 additions and 17 deletions
64
app/src/helpers/yunohostArguments.js
Normal file
64
app/src/helpers/yunohostArguments.js
Normal file
|
@ -0,0 +1,64 @@
|
|||
import i18n from '@/i18n'
|
||||
import store from '@/store'
|
||||
|
||||
function formatI18nField (field) {
|
||||
if (typeof field === 'string') return field
|
||||
const { locale, fallbackLocale } = store.state
|
||||
return field[locale] || field[fallbackLocale] || field.en
|
||||
}
|
||||
|
||||
export function formatYunoHostArgument (_arg) {
|
||||
const arg = {
|
||||
component: undefined,
|
||||
label: formatI18nField(_arg.ask),
|
||||
props: { id: _arg.name }
|
||||
}
|
||||
|
||||
// Some apps has `string` as type but expect a select since it has `choices`
|
||||
if (_arg.choices !== undefined) {
|
||||
arg.component = 'SelectItem'
|
||||
arg.props.choices = _arg.choices
|
||||
// Input
|
||||
} else if ([undefined, 'string', 'number', 'password', 'email'].includes(_arg.type)) {
|
||||
arg.component = 'InputItem'
|
||||
if (![undefined, 'string'].includes(_arg.type)) {
|
||||
arg.props.type = _arg.type
|
||||
if (_arg.type === 'password') {
|
||||
arg.description = i18n.t('good_practices_about_admin_password')
|
||||
}
|
||||
}
|
||||
// Checkbox
|
||||
} else if (_arg.type === 'boolean') {
|
||||
arg.component = 'CheckboxItem'
|
||||
arg.props.value = _arg.default || false
|
||||
// Special (store related)
|
||||
} else if (['user', 'domain'].includes(_arg.type)) {
|
||||
arg.component = 'SelectItem'
|
||||
arg.link = { name: _arg.type + '-list', text: i18n.t(`manage_${_arg.type}s`) }
|
||||
arg.props = { ...arg.props, ...store.getters[_arg.type + 'sAsOptions'] }
|
||||
// Unknown from the specs, try to display it as an input[text]
|
||||
// FIXME throw an error instead ?
|
||||
} else {
|
||||
arg.component = 'InputItem'
|
||||
}
|
||||
|
||||
// Required
|
||||
arg.props.required = _arg.optional !== true
|
||||
// Default value
|
||||
if (_arg.default) {
|
||||
arg.props.value = _arg.default
|
||||
}
|
||||
// Help message
|
||||
if (_arg.help) {
|
||||
arg.description = formatI18nField(_arg.help)
|
||||
}
|
||||
// Example
|
||||
if (_arg.example) {
|
||||
arg.example = _arg.example
|
||||
if (arg.component === 'InputItem') {
|
||||
arg.props.placeholder = arg.example
|
||||
}
|
||||
}
|
||||
|
||||
return arg
|
||||
}
|
|
@ -64,7 +64,7 @@
|
|||
"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_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": "You will not be able to install any other app on %s. Continue?",
|
||||
"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_install_app_lowquality": "Warning: this application may work but is not well-integrated in YunoHost. Some features such as single sign-on and backup/restore might not be available.",
|
||||
"confirm_install_app_inprogress": "WARNING! This application is still experimental (if not explicitly not working) and it is likely to break your system! You should probably NOT install it unless you know what you are doing. Are you willing to take that risk?",
|
||||
|
@ -140,7 +140,7 @@
|
|||
"firewall_port_already": "Port {port} is already {state} (protocol: {protocol}; connection: {connection})",
|
||||
"not_github_link": "Url must be a valid Github link to a repository"
|
||||
},
|
||||
"form_input_example": "Example: %s",
|
||||
"form_input_example": "Example: {example}",
|
||||
"from_to": "from {0} to {1}",
|
||||
"good_practices_about_admin_password": "You are now about to define a new admin password. The password should be at least 8 characters - though it is good practice to use longer password (i.e. a passphrase) and/or to use various kind of characters (uppercase, lowercase, digits and special characters).",
|
||||
"good_practices_about_user_password": "You are now about to define a new user password. The password should be at least 8 characters - though it is good practice to use longer password (i.e. a passphrase) and/or to use various kind of characters (uppercase, lowercase, digits and special characters).",
|
||||
|
@ -191,7 +191,7 @@
|
|||
"ipv6": "IPv6",
|
||||
"issues": "{count} issues",
|
||||
"label": "Label",
|
||||
"label_for_manifestname": "Label for %s",
|
||||
"label_for_manifestname": "Label for {name}",
|
||||
"last_ran": "Last time ran:",
|
||||
"license": "License",
|
||||
"loading": "Loading …",
|
||||
|
|
|
@ -188,6 +188,32 @@ const routes = [
|
|||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'app-install',
|
||||
path: '/apps/install/:id',
|
||||
component: () => import(/* webpackChunkName: "views/apps" */ '@/views/app/AppInstall'),
|
||||
props: true,
|
||||
meta: {
|
||||
breadcrumb: [
|
||||
{ name: 'app-list', trad: 'applications' },
|
||||
{ name: 'app-catalog', trad: 'catalog' },
|
||||
{ name: 'app-catalog', trad: 'install_name', param: 'id' }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'app-install-custom',
|
||||
path: '/apps/install-custom/:id',
|
||||
component: () => import(/* webpackChunkName: "views/apps" */ '@/views/app/AppInstall'),
|
||||
props: true,
|
||||
meta: {
|
||||
breadcrumb: [
|
||||
{ name: 'app-list', trad: 'applications' },
|
||||
{ name: 'app-catalog', trad: 'catalog' },
|
||||
{ name: 'app-catalog', trad: 'install_name', param: 'id' }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
/* ────────────────╮
|
||||
│ SYSTEM UPDATE │
|
||||
|
|
|
@ -49,21 +49,27 @@ body {
|
|||
display: block;
|
||||
}
|
||||
|
||||
.col-form-label {
|
||||
@include media-breakpoint-up(xs) {
|
||||
width: 100%;
|
||||
// <b-row /> elems with <b-col> inside and eventually .sep inside
|
||||
.row-line {
|
||||
&:hover > div :first-child {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 0.2rem;
|
||||
padding: 0 .5rem;
|
||||
margin-right: -.5rem;
|
||||
margin-left: -.5rem;
|
||||
}
|
||||
@include media-breakpoint-up(sm) {
|
||||
flex-basis: 40%;
|
||||
}
|
||||
@include media-breakpoint-up(md) {
|
||||
flex-basis: 30%;
|
||||
}
|
||||
@include media-breakpoint-up(lg) {
|
||||
flex-basis: 25%;
|
||||
}
|
||||
@include media-breakpoint-up(xl) {
|
||||
flex-basis: 20%;
|
||||
|
||||
& > div {
|
||||
align-self: flex-start;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// display a dashed line between col items
|
||||
.sep {
|
||||
height: 0;
|
||||
border-top: 1px dashed rgba(0, 0, 0, 0.25);
|
||||
flex-grow: 1;
|
||||
margin: 0 .75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
191
app/src/views/app/AppInstall.vue
Normal file
191
app/src/views/app/AppInstall.vue
Normal file
|
@ -0,0 +1,191 @@
|
|||
<template>
|
||||
<div class="app-install">
|
||||
<div v-if="infos">
|
||||
<!-- BASIC INFOS -->
|
||||
<b-card>
|
||||
<template v-slot:header>
|
||||
<h2><icon iname="info-circle" /> {{ $t('infos') }} — {{ name }}</h2>
|
||||
</template>
|
||||
|
||||
<b-row
|
||||
v-for="key in infosKeys" :key="key"
|
||||
no-gutters class="row-line"
|
||||
>
|
||||
<b-col cols="5" md="3" xl="3">
|
||||
<strong>{{ $t(key) }}</strong>
|
||||
<span class="sep" />
|
||||
</b-col>
|
||||
<b-col>
|
||||
<span>{{ infos[key] }}</span>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-card>
|
||||
|
||||
<!-- INSTALL FORM -->
|
||||
<b-card>
|
||||
<template v-slot:header>
|
||||
<h2><icon iname="wrench" /> {{ $t('operations') }}</h2>
|
||||
</template>
|
||||
|
||||
<b-form id="install-form" @submit.prevent="beforeInstall">
|
||||
<form-item-helper v-bind="form.label" />
|
||||
<form-item-helper v-for="arg in form.args" :key="arg.name" v-bind="arg" />
|
||||
|
||||
<b-form-invalid-feedback id="global-feedback" :state="server.isValid">
|
||||
{{ this.server.error }}
|
||||
</b-form-invalid-feedback>
|
||||
</b-form>
|
||||
|
||||
<template v-slot:footer>
|
||||
<b-button
|
||||
type="submit" form="install-form"
|
||||
variant="success" v-t="'install'"
|
||||
/>
|
||||
</template>
|
||||
</b-card>
|
||||
|
||||
<!-- CONFIRM INSTALL DOMAIN ROOT MODAL -->
|
||||
<b-modal
|
||||
id="confirm-domain-root-modal" ref="confirm-domain-root-modal" centered
|
||||
body-bg-variant="danger" body-text-variant="light"
|
||||
@ok="runInstall" hide-header
|
||||
:ok-title="$t('install')"
|
||||
>
|
||||
{{ $t('confirm_install_domain_root', { domain: confirmDomain }) }}
|
||||
</b-modal>
|
||||
</div>
|
||||
|
||||
<!-- In case of a custom url with no manifest found -->
|
||||
<b-alert v-else-if="infos === null" variant="warning" show>
|
||||
<icon iname="exclamation-triangle" /> {{ $t('app_install_custom_no_manifest') }}
|
||||
</b-alert>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api, { objectToParams } from '@/helpers/api'
|
||||
import { formatYunoHostArgument } from '@/helpers/yunohostArguments'
|
||||
|
||||
export default {
|
||||
name: 'AppInstall',
|
||||
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
name: undefined,
|
||||
infosKeys: ['id', 'description', 'license', 'version', 'multi_instance'],
|
||||
infos: undefined,
|
||||
form: undefined,
|
||||
server: {
|
||||
isValid: null,
|
||||
error: ''
|
||||
},
|
||||
confirmDomain: null
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
getExternalManifest () {
|
||||
const url = this.id.replace('github.com', 'raw.githubusercontent.com') + 'master/manifest.json'
|
||||
return fetch(url).then(response => {
|
||||
if (response.ok) return response.json()
|
||||
else {
|
||||
throw Error('No manifest found at ' + url)
|
||||
}
|
||||
}).catch(() => {
|
||||
this.infos = null
|
||||
})
|
||||
},
|
||||
|
||||
getApiManifest () {
|
||||
return api.get('appscatalog?full').then(response => response.apps[this.id].manifest)
|
||||
},
|
||||
|
||||
fetchData () {
|
||||
const isCustom = this.$route.name === 'app-install-custom'
|
||||
Promise.all([
|
||||
isCustom ? this.getExternalManifest() : this.getApiManifest(),
|
||||
this.$store.dispatch('FETCH_ALL', [
|
||||
{ uri: 'domains' },
|
||||
{ uri: 'domains/main', storeKey: 'main_domain' },
|
||||
{ uri: 'users' }
|
||||
])
|
||||
]).then((responses) => this.setupForm(responses[0]))
|
||||
},
|
||||
|
||||
setupForm (manifest) {
|
||||
if (manifest.license === undefined || manifest.license === 'free') {
|
||||
this.infosKeys.splice(2, 1)
|
||||
}
|
||||
const desc = manifest.description
|
||||
if (typeof desc !== 'string') {
|
||||
manifest.description = desc[this.$i18n.locale] || desc.en
|
||||
}
|
||||
manifest.multi_instance = this.$i18n.t(manifest.multi_instance ? 'yes' : 'no')
|
||||
|
||||
const infos = {}
|
||||
for (const key of this.infosKeys) {
|
||||
infos[key] = manifest[key]
|
||||
}
|
||||
this.infos = infos
|
||||
this.name = manifest.name
|
||||
|
||||
this.form = {
|
||||
label: formatYunoHostArgument({
|
||||
ask: this.$i18n.t('label_for_manifestname', { name: manifest.name }),
|
||||
default: manifest.name,
|
||||
name: 'label'
|
||||
}),
|
||||
args: manifest.arguments.install.map(arg => formatYunoHostArgument(arg))
|
||||
}
|
||||
},
|
||||
|
||||
beforeInstall () {
|
||||
const path = this.form.args.find(arg => arg.props.id === 'path').props.value
|
||||
if (path === '/') {
|
||||
this.confirmDomain = this.form.args.find(arg => arg.props.id === 'domain').props.value
|
||||
this.$refs['confirm-domain-root-modal'].show()
|
||||
} else {
|
||||
this.runInstall()
|
||||
}
|
||||
},
|
||||
|
||||
runInstall () {
|
||||
const args = {}
|
||||
for (const arg of this.form.args) {
|
||||
if (arg.component === 'CheckboxItem') {
|
||||
args[arg.props.id] = arg.props.value ? 1 : 0
|
||||
} else {
|
||||
args[arg.props.id] = arg.props.value
|
||||
}
|
||||
}
|
||||
const data = {
|
||||
app: this.id,
|
||||
label: this.form.label.props.value,
|
||||
args: objectToParams(args)
|
||||
}
|
||||
|
||||
api.post('apps', data).then(response => {
|
||||
this.$router.push({ name: 'app-list' })
|
||||
}).catch(err => {
|
||||
this.server.isValid = false
|
||||
this.server.error = err.message
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
this.fetchData()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
Loading…
Add table
Reference in a new issue