mirror of
https://github.com/YunoHost/yunohost-admin.git
synced 2024-09-03 20:06:15 +02:00
refactor: rework async AppInfo
This commit is contained in:
parent
38859f5afc
commit
a5714e56fc
5 changed files with 347 additions and 330 deletions
|
@ -88,6 +88,15 @@ export function arrayDiff<T extends string>(
|
|||
return arr1.filter((item) => !arr2.includes(item))
|
||||
}
|
||||
|
||||
export function joinOrNull(
|
||||
value: any[] | string | null | undefined,
|
||||
): string | null {
|
||||
if (Array.isArray(value) && value.length) {
|
||||
return value.join(i18n.global.t('words.separator'))
|
||||
}
|
||||
return typeof value === 'string' ? value : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new string with escaped HTML (`&<>"'` replaced by entities).
|
||||
*
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import type { Obj, Translation } from '@/types/commons'
|
||||
import type { Permission } from '@/types/core/data'
|
||||
|
||||
// APPS
|
||||
|
||||
|
@ -71,3 +72,48 @@ export type Catalog = {
|
|||
title: string
|
||||
}[]
|
||||
}
|
||||
|
||||
export type AppInfo = {
|
||||
id: string
|
||||
description: string
|
||||
label: string
|
||||
name: string
|
||||
version: string
|
||||
domain_path: string
|
||||
logo: string | null
|
||||
screenshot?: string
|
||||
upgradable: string
|
||||
settings: { domain?: string; path?: string } & Obj
|
||||
setting_path: string
|
||||
permissions: Obj<Permission & { sublabel: string }>
|
||||
manifest: AppMinManifest & {
|
||||
install: Obj<AnyOption>
|
||||
upstream: {
|
||||
license: string | null
|
||||
website: string | null
|
||||
demo: string | null
|
||||
admindoc: string | null
|
||||
userdoc: string | null
|
||||
code: string | null
|
||||
}
|
||||
resources: Obj
|
||||
doc: {
|
||||
DESCRIPTION?: Translation
|
||||
ADMIN?: Translation
|
||||
} & Obj<Translation>
|
||||
notifications: {
|
||||
PRE_INSTALL: Obj<Translation> | null
|
||||
POST_INSTALL: Obj<Translation> | null
|
||||
PRE_UPGRADE: Obj<Translation> | null
|
||||
POST_UPGRADE: Obj<Translation> | null
|
||||
}
|
||||
}
|
||||
from_catalog: CatalogApp
|
||||
is_webapp: boolean
|
||||
is_default: boolean
|
||||
supports_change_url: boolean
|
||||
supports_backup_restore: boolean
|
||||
supports_multi_instance: boolean
|
||||
supports_config_panel: boolean
|
||||
supports_purge: boolean
|
||||
}
|
||||
|
|
|
@ -1,25 +1,26 @@
|
|||
<script setup lang="ts">
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { computed, reactive, ref, shallowRef } from 'vue'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import api, { objectToParams } from '@/api'
|
||||
import { type APIError } from '@/api/errors'
|
||||
import ConfigPanelsComponent from '@/components/ConfigPanels.vue'
|
||||
import type {
|
||||
ConfigPanelsProps,
|
||||
OnPanelApply,
|
||||
} from '@/composables/configPanels'
|
||||
import { formatConfigPanels, useConfigPanels } from '@/composables/configPanels'
|
||||
import { useDomains } from '@/composables/data'
|
||||
import { useArrayRule, useForm } from '@/composables/form'
|
||||
import { useAutoModal } from '@/composables/useAutoModal'
|
||||
import { useInitialQueries } from '@/composables/useInitialQueries'
|
||||
import { isEmptyValue } from '@/helpers/commons'
|
||||
import { isEmptyValue, joinOrNull, pick, toEntries } from '@/helpers/commons'
|
||||
import { humanPermissionName } from '@/helpers/filters/human'
|
||||
import { helpers, required } from '@/helpers/validators'
|
||||
import { required } from '@/helpers/validators'
|
||||
import { formatI18nField } from '@/helpers/yunohostArguments'
|
||||
import type { Obj } from '@/types/commons'
|
||||
import type { AppInfo } from '@/types/core/api'
|
||||
import type { Permission } from '@/types/core/data'
|
||||
import type { CoreConfigPanels } from '@/types/core/options'
|
||||
import type { BaseItemComputedProps } from '@/types/form'
|
||||
import AppIntegrationAndLinks from './_AppIntegrationAndLinks.vue'
|
||||
import { formatAppIntegration, formatAppLinks } from './appData'
|
||||
|
||||
const props = defineProps<{
|
||||
id: string
|
||||
|
@ -27,144 +28,80 @@ const props = defineProps<{
|
|||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const modalConfirm = useAutoModal()
|
||||
|
||||
const { domainsAsChoices } = useDomains()
|
||||
|
||||
// FIXME
|
||||
type AppForm = {
|
||||
labels: { label: string; show_tile: boolean }[]
|
||||
url: { domain: string; path: string }
|
||||
}
|
||||
const form: AppForm = reactive({
|
||||
labels: [],
|
||||
url: { domain: '', path: '' },
|
||||
})
|
||||
const rules = computed(() => ({
|
||||
labels: {
|
||||
$each: helpers.forEach({
|
||||
label: { required },
|
||||
}),
|
||||
},
|
||||
url: { path: { required } },
|
||||
}))
|
||||
const externalResults = reactive({})
|
||||
const v$ = useVuelidate(rules, form, { $externalResults: externalResults })
|
||||
const { loading, refetch } = useInitialQueries(
|
||||
[
|
||||
const [app, form, coreConfig, configPanelErr] = await api
|
||||
.fetchAll<[AppInfo, Obj<Permission>]>([
|
||||
{ uri: `apps/${props.id}?full` },
|
||||
// FIXME permissions needed?
|
||||
{ uri: 'users/permissions?full', cachePath: 'permissions' },
|
||||
{ uri: 'domains', cachePath: 'domains' },
|
||||
],
|
||||
{ onQueriesResponse },
|
||||
)
|
||||
|
||||
const app = ref()
|
||||
const purge = ref(false)
|
||||
const configPanelErr = ref('')
|
||||
const config = shallowRef<ConfigPanelsProps | undefined>()
|
||||
const doc = ref()
|
||||
|
||||
const currentTab = computed(() => {
|
||||
return route.params.tabId
|
||||
})
|
||||
|
||||
const allowedGroups = computed(() => {
|
||||
if (!app.value) return
|
||||
return app.value.permissions[0].allowed
|
||||
})
|
||||
|
||||
function appLinksIcons(linkType) {
|
||||
const linksIcons = {
|
||||
license: 'institution',
|
||||
website: 'globe',
|
||||
admindoc: 'book',
|
||||
userdoc: 'book',
|
||||
code: 'code',
|
||||
package: 'code',
|
||||
package_license: 'institution',
|
||||
forum: 'comments',
|
||||
}
|
||||
return linksIcons[linkType]
|
||||
}
|
||||
|
||||
async function onQueriesResponse(app_: any) {
|
||||
// const form = { labels: [] }
|
||||
|
||||
const mainPermission = app_.permissions[props.id + '.main']
|
||||
mainPermission.name = props.id + '.main'
|
||||
mainPermission.title = t('permission_main')
|
||||
mainPermission.tileAvailable =
|
||||
mainPermission.url !== null && !mainPermission.url.startsWith('re:')
|
||||
form.labels.push({
|
||||
label: mainPermission.label,
|
||||
show_tile: mainPermission.show_tile,
|
||||
])
|
||||
.then(async ([app_]) => {
|
||||
// Query config panels if app supports it
|
||||
let config: CoreConfigPanels | undefined
|
||||
let configPanelErr: string | undefined
|
||||
if (app_.supports_config_panel) {
|
||||
await api
|
||||
.get<CoreConfigPanels>(`apps/${props.id}/config?full`)
|
||||
.then((coreConfig) => {
|
||||
// Fake integration of operations in config panels
|
||||
coreConfig.panels.unshift({
|
||||
id: 'operations',
|
||||
name: t('operations'),
|
||||
})
|
||||
config = coreConfig
|
||||
})
|
||||
.catch((err: APIError) => {
|
||||
configPanelErr = err.message
|
||||
})
|
||||
}
|
||||
|
||||
const permissions = [mainPermission]
|
||||
for (const [name, perm] of Object.entries(app_.permissions)) {
|
||||
if (!name.endsWith('.main')) {
|
||||
permissions.push({
|
||||
const { domain, path } = app_.settings
|
||||
const form = ref({
|
||||
labels: [] as { label: string; show_tile: boolean }[],
|
||||
url: domain && path ? { domain, path: path.slice(1) } : undefined,
|
||||
})
|
||||
const permissions = []
|
||||
for (const [name, perm] of toEntries(app_.permissions)) {
|
||||
const isMain = name.endsWith('.main')
|
||||
const permission = {
|
||||
...perm,
|
||||
name,
|
||||
label: perm.sublabel,
|
||||
title: humanPermissionName(name),
|
||||
tileAvailable: perm.url !== null && !perm.url.startsWith('re:'),
|
||||
})
|
||||
form.labels.push({ label: perm.sublabel, show_tile: perm.show_tile })
|
||||
label: isMain ? perm.label : perm.sublabel,
|
||||
title: isMain ? t('permission_main') : humanPermissionName(name),
|
||||
tileAvailable: !!perm.url && !perm.url.startsWith('re:'),
|
||||
}
|
||||
permissions.push(permission)
|
||||
form.value.labels.push(pick(permission, ['label', 'show_tile']))
|
||||
}
|
||||
// this.form = form
|
||||
|
||||
const { DESCRIPTION, ADMIN, ...doc } = app_.manifest.doc
|
||||
const notifs = app_.manifest.notifications
|
||||
const {
|
||||
ldap,
|
||||
sso,
|
||||
multi_instance,
|
||||
ram,
|
||||
disk,
|
||||
architectures: archs,
|
||||
} = app_.manifest.integration
|
||||
app.value = {
|
||||
const { label, allowed } = app_.permissions[props.id + '.main']
|
||||
const app = {
|
||||
id: props.id,
|
||||
version: app_.version,
|
||||
label: mainPermission.label,
|
||||
domain: app_.settings.domain,
|
||||
alternativeTo: app_.from_catalog.potential_alternative_to?.length
|
||||
? app_.from_catalog.potential_alternative_to.join(t('words.separator'))
|
||||
: null,
|
||||
description: DESCRIPTION ? formatI18nField(DESCRIPTION) : app_.description,
|
||||
integration:
|
||||
app_.manifest.packaging_format >= 2
|
||||
? {
|
||||
archs: Array.isArray(archs)
|
||||
? archs.join(t('words.separator'))
|
||||
: archs,
|
||||
ldap: ldap === 'not_relevant' ? null : ldap,
|
||||
sso: sso === 'not_relevant' ? null : sso,
|
||||
multi_instance,
|
||||
resources: { ram: ram.runtime, disk },
|
||||
}
|
||||
: null,
|
||||
links: [
|
||||
[
|
||||
'license',
|
||||
`https://spdx.org/licenses/${app_.manifest.upstream.license}`,
|
||||
],
|
||||
...['website', 'admindoc', 'userdoc', 'code'].map((key) => {
|
||||
return [key, app_.manifest.upstream[key]]
|
||||
label,
|
||||
domain,
|
||||
url: domain && path ? `https://${domain}${path}` : null,
|
||||
allowedGroups: allowed.length ? allowed.join(', ') : t('nobody'),
|
||||
alternativeTo: joinOrNull(app_.from_catalog.potential_alternative_to),
|
||||
description: formatI18nField(DESCRIPTION) || app_.description,
|
||||
integration: formatAppIntegration(
|
||||
app_.manifest.integration,
|
||||
app_.manifest.packaging_format,
|
||||
),
|
||||
// TODO: could return `remote` key of manifest to pass only manifest and id?
|
||||
links: formatAppLinks({
|
||||
...app_.manifest,
|
||||
// @ts-expect-error
|
||||
remote: app_.from_catalog.git ?? { url: null },
|
||||
}),
|
||||
['package', app_.from_catalog.git?.url],
|
||||
['package_license', app_.from_catalog.git?.url + '/blob/master/LICENSE'],
|
||||
['forum', `https://forum.yunohost.org/tag/${app_.manifest.id}`],
|
||||
].filter(([key, val]) => !!val),
|
||||
doc: {
|
||||
notifications: {
|
||||
postInstall:
|
||||
notifs.POST_INSTALL && notifs.POST_INSTALL.main
|
||||
postInstall: notifs.POST_INSTALL?.main
|
||||
? [['main', formatI18nField(notifs.POST_INSTALL.main)]]
|
||||
: [],
|
||||
postUpgrade: notifs.POST_UPGRADE
|
||||
|
@ -183,50 +120,22 @@ async function onQueriesResponse(app_: any) {
|
|||
]),
|
||||
].filter((doc) => doc[1]),
|
||||
},
|
||||
is_webapp: app_.is_webapp,
|
||||
is_default: app_.is_default,
|
||||
supports_change_url: app_.supports_change_url,
|
||||
supports_config_panel: app_.supports_config_panel,
|
||||
supports_purge: app_.supports_purge,
|
||||
isWebapp: app_.is_webapp,
|
||||
isDefault: ref(app_.is_default),
|
||||
supportsChangeUrl: app_.supports_change_url,
|
||||
supportsPurge: app_.supports_purge,
|
||||
permissions,
|
||||
}
|
||||
if (app_.settings.domain && app_.settings.path) {
|
||||
app.value.url = 'https://' + app_.settings.domain + app_.settings.path
|
||||
form.url = {
|
||||
domain: app_.settings.domain,
|
||||
path: app_.settings.path.slice(1),
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!Object.values(app.value.doc.notifications).some((notif) => notif.length)
|
||||
) {
|
||||
app.value.doc.notifications = null
|
||||
}
|
||||
|
||||
if (app_.supports_config_panel) {
|
||||
await api
|
||||
.get(`apps/${props.id}/config?full`)
|
||||
.then((cp) => {
|
||||
const config_ = cp as CoreConfigPanels
|
||||
// Fake integration of operations in config panels
|
||||
config_.panels.unshift({
|
||||
id: 'operations',
|
||||
name: t('operations'),
|
||||
return [app, form, config, configPanelErr] as const
|
||||
})
|
||||
config.value = useConfigPanels(
|
||||
formatConfigPanels(config_),
|
||||
const { domainsAsChoices } = useDomains()
|
||||
|
||||
const config = coreConfig
|
||||
? useConfigPanels(
|
||||
formatConfigPanels(coreConfig),
|
||||
() => props.tabId,
|
||||
onPanelApply,
|
||||
)
|
||||
})
|
||||
.catch((err: APIError) => {
|
||||
configPanelErr.value = err.message
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const onPanelApply: OnPanelApply = ({ panelId, data, action }, onError) => {
|
||||
({ panelId, data, action }, onError) => {
|
||||
api
|
||||
.put({
|
||||
uri: action
|
||||
|
@ -239,37 +148,57 @@ const onPanelApply: OnPanelApply = ({ panelId, data, action }, onError) => {
|
|||
name: props.id,
|
||||
},
|
||||
})
|
||||
.then(() => refetch())
|
||||
.then(() => api.refetch())
|
||||
.catch(onError)
|
||||
}
|
||||
},
|
||||
)
|
||||
: undefined
|
||||
|
||||
function changeLabel(permName, data) {
|
||||
data.show_tile = data.show_tile ? 'True' : 'False'
|
||||
const fields = {
|
||||
labels: reactive({
|
||||
rules: useArrayRule(() => form.value.labels, { label: { required } }),
|
||||
}),
|
||||
url: {
|
||||
rules: { path: { required } },
|
||||
},
|
||||
}
|
||||
const { v } = useForm(form, fields)
|
||||
const purge = ref(false)
|
||||
|
||||
async function changeLabel(permName: string, i: number) {
|
||||
if (!(await v.value.form.labels[i].$validate())) return
|
||||
const data = form.value.labels[i]
|
||||
api
|
||||
.put({
|
||||
uri: 'users/permissions/' + permName,
|
||||
data,
|
||||
data: {
|
||||
label: data.label,
|
||||
show_tile: data.show_tile ? 'True' : 'False',
|
||||
},
|
||||
humanKey: {
|
||||
key: 'apps.change_label',
|
||||
prevName: app.value.label,
|
||||
prevName: app.label,
|
||||
nextName: data.label,
|
||||
},
|
||||
})
|
||||
.then(() => refetch(false))
|
||||
// FIXME really need to refetch? permissions store update should be ok
|
||||
.then(() => api.refetch())
|
||||
}
|
||||
|
||||
async function changeUrl() {
|
||||
if (!(await v.value.form.url.$validate())) return
|
||||
const confirmed = await modalConfirm(t('confirm_app_change_url'))
|
||||
if (!confirmed) return
|
||||
|
||||
const { domain, path } = form.url
|
||||
const { domain, path } = form.value.url!
|
||||
api
|
||||
.put({
|
||||
uri: `apps/${props.id}/changeurl`,
|
||||
data: { domain, path: '/' + path },
|
||||
humanKey: { key: 'apps.change_url', name: app.value.label },
|
||||
humanKey: { key: 'apps.change_url', name: app.label },
|
||||
})
|
||||
.then(() => refetch(false))
|
||||
// Refetch because some content of this page relies on the url
|
||||
.then(() => api.refetch())
|
||||
}
|
||||
|
||||
async function setAsDefaultDomain(undo = false) {
|
||||
|
@ -281,20 +210,21 @@ async function setAsDefaultDomain(undo = false) {
|
|||
uri: `apps/${props.id}/default${undo ? '?undo' : ''}`,
|
||||
humanKey: {
|
||||
key: 'apps.set_default',
|
||||
name: app.value.label,
|
||||
domain: app.value.domain,
|
||||
name: app.label,
|
||||
domain: app.domain,
|
||||
},
|
||||
})
|
||||
.then(() => refetch(false))
|
||||
.then(() => (app.isDefault.value = true))
|
||||
}
|
||||
|
||||
async function dismissNotification(name: string) {
|
||||
api
|
||||
.put({
|
||||
uri: `apps/${props.id}/dismiss_notification/${name}`,
|
||||
humanKey: { key: 'apps.dismiss_notification', name: app.value.label },
|
||||
humanKey: { key: 'apps.dismiss_notification', name: app.label },
|
||||
})
|
||||
.then(() => refetch(false))
|
||||
// FIXME no need to refetch i guess, filter the reactive notifs?
|
||||
.then(() => api.refetch)
|
||||
}
|
||||
|
||||
async function uninstall() {
|
||||
|
@ -303,7 +233,7 @@ async function uninstall() {
|
|||
.delete({
|
||||
uri: 'apps/' + props.id,
|
||||
data,
|
||||
humanKey: { key: 'apps.uninstall', name: app.value.label },
|
||||
humanKey: { key: 'apps.uninstall', name: app.label },
|
||||
})
|
||||
.then(() => {
|
||||
router.push({ name: 'app-list' })
|
||||
|
@ -312,14 +242,9 @@ async function uninstall() {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<ViewBase :loading="loading">
|
||||
<div>
|
||||
<YAlert
|
||||
v-if="
|
||||
app &&
|
||||
app.doc &&
|
||||
app.doc.notifications &&
|
||||
app.doc.notifications.postInstall.length
|
||||
"
|
||||
v-if="app.doc.notifications.postInstall.length"
|
||||
variant="info"
|
||||
class="my-4"
|
||||
>
|
||||
|
@ -345,12 +270,7 @@ async function uninstall() {
|
|||
</YAlert>
|
||||
|
||||
<YAlert
|
||||
v-if="
|
||||
app &&
|
||||
app.doc &&
|
||||
app.doc.notifications &&
|
||||
app.doc.notifications.postUpgrade.length
|
||||
"
|
||||
v-if="app.doc.notifications.postUpgrade.length"
|
||||
variant="info"
|
||||
class="my-4"
|
||||
>
|
||||
|
@ -375,7 +295,7 @@ async function uninstall() {
|
|||
/>
|
||||
</YAlert>
|
||||
|
||||
<section v-if="app" class="border rounded p-3 mb-4">
|
||||
<section class="border rounded p-3 mb-4">
|
||||
<div class="d-md-flex align-items-center mb-4">
|
||||
<h1 class="mb-3 mb-md-0">
|
||||
<YIcon iname="cube" />
|
||||
|
@ -436,14 +356,14 @@ async function uninstall() {
|
|||
<!-- BASIC INFOS -->
|
||||
<ConfigPanelsComponent
|
||||
v-if="config"
|
||||
v-model="config.form"
|
||||
v-model="config.form.value"
|
||||
:panel="config.panel.value"
|
||||
:validations="config.v.value"
|
||||
:routes="config.routes"
|
||||
@apply="config.onPanelApply"
|
||||
>
|
||||
<!-- OPERATIONS TAB -->
|
||||
<template v-if="currentTab === 'operations'" #default>
|
||||
<template v-if="tabId === 'operations'" #default>
|
||||
<!-- CHANGE PERMISSIONS LABEL -->
|
||||
<BFormGroup
|
||||
:label="$t('app_manage_label_and_tiles')"
|
||||
|
@ -457,15 +377,14 @@ async function uninstall() {
|
|||
label-cols="0"
|
||||
label-class=""
|
||||
class="m-0"
|
||||
:validation="v$.form.labels.$each[i]"
|
||||
:validation="v.form.labels[i]"
|
||||
>
|
||||
<template #default="{ self }">
|
||||
<template #default="componentProps">
|
||||
<BInputGroup>
|
||||
<InputItem
|
||||
:state="self.state"
|
||||
v-model="form.labels[i].label"
|
||||
:id="'perm' + i"
|
||||
:aria-describedby="'perm-' + i + '_group__BV_description_'"
|
||||
v-bind="componentProps as BaseItemComputedProps"
|
||||
v-model="form.labels[i].label"
|
||||
/>
|
||||
|
||||
<BInputGroupText v-if="perm.tileAvailable">
|
||||
|
@ -476,9 +395,9 @@ async function uninstall() {
|
|||
</BInputGroupText>
|
||||
|
||||
<BButton
|
||||
variant="info"
|
||||
v-t="'save'"
|
||||
@click="changeLabel(perm.name, form.labels[i])"
|
||||
variant="info"
|
||||
@click="changeLabel(perm.name, i)"
|
||||
/>
|
||||
</BInputGroup>
|
||||
</template>
|
||||
|
@ -500,9 +419,7 @@ async function uninstall() {
|
|||
label-class="fw-bold"
|
||||
label-cols-lg="0"
|
||||
>
|
||||
{{
|
||||
allowedGroups.length > 0 ? allowedGroups.join(', ') : $t('nobody')
|
||||
}}
|
||||
{{ app.allowedGroups }}
|
||||
<BButton
|
||||
size="sm"
|
||||
:to="{ name: 'group-list' }"
|
||||
|
@ -516,14 +433,13 @@ async function uninstall() {
|
|||
<hr />
|
||||
|
||||
<!-- CHANGE URL -->
|
||||
<BFormGroup
|
||||
<FormField
|
||||
v-if="app.isWebapp"
|
||||
:label="$t('app_info_changeurl_desc')"
|
||||
label-for="input-url"
|
||||
:label-cols-lg="app.supports_change_url ? 0 : 0"
|
||||
:label-cols="0"
|
||||
label-class="fw-bold"
|
||||
v-if="app.is_webapp"
|
||||
>
|
||||
<BInputGroup v-if="app.supports_change_url">
|
||||
<BInputGroup v-if="app.supportsChangeUrl && form.url">
|
||||
<BInputGroupText>https://</BInputGroupText>
|
||||
|
||||
<BFormSelect
|
||||
|
@ -533,11 +449,7 @@ async function uninstall() {
|
|||
|
||||
<BInputGroupText>/</BInputGroupText>
|
||||
|
||||
<BFormInput
|
||||
id="input-url"
|
||||
v-model="form.url.path"
|
||||
class="flex-grow-3"
|
||||
/>
|
||||
<BFormInput v-model="form.url.path" class="flex-grow-3" />
|
||||
|
||||
<BButton @click="changeUrl" variant="info" v-t="'save'" />
|
||||
</BInputGroup>
|
||||
|
@ -546,18 +458,17 @@ async function uninstall() {
|
|||
<YIcon iname="exclamation" />
|
||||
{{ $t('app_info_change_url_disabled_tooltip') }}
|
||||
</div>
|
||||
</BFormGroup>
|
||||
<hr v-if="app.is_webapp" />
|
||||
</FormField>
|
||||
<hr v-if="app.isWebapp" />
|
||||
|
||||
<!-- MAKE DEFAULT -->
|
||||
<BFormGroup
|
||||
<FormField
|
||||
v-if="app.isWebapp"
|
||||
:label="$t('app_info_default_desc', { domain: app.domain })"
|
||||
label-for="main-domain"
|
||||
label-class="fw-bold"
|
||||
label-cols-md="4"
|
||||
v-if="app.is_webapp"
|
||||
label-cols="0"
|
||||
>
|
||||
<template v-if="!app.is_default">
|
||||
<template v-if="!app.isDefault.value">
|
||||
<BButton
|
||||
@click="setAsDefaultDomain(false)"
|
||||
id="main-domain"
|
||||
|
@ -576,11 +487,11 @@ async function uninstall() {
|
|||
<YIcon iname="star" /> {{ $t('app_make_not_default') }}
|
||||
</BButton>
|
||||
</template>
|
||||
</BFormGroup>
|
||||
</FormField>
|
||||
</template>
|
||||
</ConfigPanelsComponent>
|
||||
|
||||
<BCard v-if="app && app.doc.admin.length" no-body>
|
||||
<BCard v-if="app.doc.admin.length" no-body>
|
||||
<BTabs card fill pills>
|
||||
<BTab v-for="[name, content] in app.doc.admin" :key="name">
|
||||
<template #title>
|
||||
|
@ -592,83 +503,23 @@ async function uninstall() {
|
|||
</BTabs>
|
||||
</BCard>
|
||||
|
||||
<YCard
|
||||
v-if="app && app.integration"
|
||||
id="app-integration"
|
||||
:title="$t('app.integration.title')"
|
||||
collapsable
|
||||
collapsed
|
||||
no-body
|
||||
>
|
||||
<BListGroup flush>
|
||||
<YListGroupItem variant="info">
|
||||
{{ $t('app.integration.archs') }} {{ app.integration.archs }}
|
||||
</YListGroupItem>
|
||||
<YListGroupItem
|
||||
v-if="app.integration.ldap"
|
||||
:variant="app.integration.ldap === true ? 'success' : 'warning'"
|
||||
>
|
||||
{{ $t(`app.integration.ldap.${app.integration.ldap}`) }}
|
||||
</YListGroupItem>
|
||||
<YListGroupItem
|
||||
v-if="app.integration.sso"
|
||||
:variant="app.integration.sso === true ? 'success' : 'warning'"
|
||||
>
|
||||
{{ $t(`app.integration.sso.${app.integration.sso}`) }}
|
||||
</YListGroupItem>
|
||||
<YListGroupItem variant="info">
|
||||
{{
|
||||
$t(
|
||||
`app.integration.multi_instance.${app.integration.multi_instance}`,
|
||||
)
|
||||
}}
|
||||
</YListGroupItem>
|
||||
<YListGroupItem variant="info">
|
||||
{{ $t('app.integration.resources', app.integration.resources) }}
|
||||
</YListGroupItem>
|
||||
</BListGroup>
|
||||
</YCard>
|
||||
|
||||
<YCard
|
||||
v-if="app"
|
||||
id="app-links"
|
||||
icon="link"
|
||||
:title="$t('app.links.title')"
|
||||
collapsable
|
||||
collapsed
|
||||
no-body
|
||||
>
|
||||
<BListGroup flush>
|
||||
<YListGroupItem v-for="[key, link] in app.links" :key="key" no-status>
|
||||
<BLink :href="link" target="_blank">
|
||||
<YIcon :iname="appLinksIcons(key)" class="me-1" />
|
||||
{{ $t('app.links.' + key) }}
|
||||
</BLink>
|
||||
</YListGroupItem>
|
||||
</BListGroup>
|
||||
</YCard>
|
||||
<AppIntegrationAndLinks :integration="app.integration" :links="app.links" />
|
||||
|
||||
<BModal
|
||||
v-if="app"
|
||||
id="uninstall-modal"
|
||||
:title="$t('confirm_uninstall', { name: id })"
|
||||
header-bg-variant="warning"
|
||||
header-class="text-black"
|
||||
:body-class="{ 'd-none': !app.supports_purge }"
|
||||
:body-class="{ 'd-none': !app.supportsPurge }"
|
||||
@ok="uninstall"
|
||||
>
|
||||
<BFormGroup v-if="app.supports_purge">
|
||||
<BFormGroup v-if="app.supportsPurge">
|
||||
<BFormCheckbox v-model="purge">
|
||||
{{ $t('app.uninstall.purge_desc', { name: id }) }}
|
||||
</BFormCheckbox>
|
||||
</BFormGroup>
|
||||
</BModal>
|
||||
|
||||
<template #skeleton>
|
||||
<CardInfoSkeleton :item-count="8" />
|
||||
<CardFormSkeleton />
|
||||
</template>
|
||||
</ViewBase>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -689,4 +540,9 @@ select {
|
|||
.yuno-alert div div:not(:last-child) {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
// FIXME bootstrap-vue-next bug
|
||||
:deep(.card-header-tabs) {
|
||||
margin-bottom: unset;
|
||||
}
|
||||
</style>
|
||||
|
|
67
app/src/views/app/_AppIntegrationAndLinks.vue
Normal file
67
app/src/views/app/_AppIntegrationAndLinks.vue
Normal file
|
@ -0,0 +1,67 @@
|
|||
<script setup lang="ts">
|
||||
import type { AppLinks, AppIntegration } from './appData'
|
||||
|
||||
defineProps<{
|
||||
links: AppLinks
|
||||
integration?: AppIntegration
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<YCard
|
||||
v-if="integration"
|
||||
id="app-integration"
|
||||
:title="$t('app.integration.title')"
|
||||
collapsable
|
||||
collapsed
|
||||
no-body
|
||||
>
|
||||
<BListGroup flush>
|
||||
<YListGroupItem variant="info">
|
||||
{{ $t('app.integration.archs') }} {{ integration.archs }}
|
||||
</YListGroupItem>
|
||||
<YListGroupItem
|
||||
v-if="integration.ldap"
|
||||
:variant="integration.ldap === true ? 'success' : 'warning'"
|
||||
>
|
||||
{{ $t(`app.integration.ldap.${integration.ldap}`) }}
|
||||
</YListGroupItem>
|
||||
<YListGroupItem
|
||||
v-if="integration.sso"
|
||||
:variant="integration.sso === true ? 'success' : 'warning'"
|
||||
>
|
||||
{{ $t(`app.integration.sso.${integration.sso}`) }}
|
||||
</YListGroupItem>
|
||||
<YListGroupItem variant="info">
|
||||
{{ $t(`app.integration.multi_instance.${integration.multiInstance}`) }}
|
||||
</YListGroupItem>
|
||||
<YListGroupItem variant="info">
|
||||
{{ $t('app.integration.resources', integration.resources) }}
|
||||
</YListGroupItem>
|
||||
</BListGroup>
|
||||
</YCard>
|
||||
|
||||
<YCard
|
||||
id="app-links"
|
||||
icon="link"
|
||||
:title="$t('app.links.title')"
|
||||
collapsable
|
||||
collapsed
|
||||
no-body
|
||||
>
|
||||
<template #header>
|
||||
<h2><YIcon iname="link" /> {{ $t('app.links.title') }}</h2>
|
||||
</template>
|
||||
|
||||
<BListGroup flush>
|
||||
<template v-for="([icon, link], key) in links" :key="key">
|
||||
<YListGroupItem v-if="link" no-status>
|
||||
<YIcon :iname="icon" class="me-3" />
|
||||
<BLink :href="link" target="_blank">
|
||||
{{ $t('app.links.' + key) }}
|
||||
</BLink>
|
||||
</YListGroupItem>
|
||||
</template>
|
||||
</BListGroup>
|
||||
</YCard>
|
||||
</template>
|
|
@ -1,4 +1,5 @@
|
|||
import type { AppLevel, AppState } from '@/types/core/api'
|
||||
import { joinOrNull } from '@/helpers/commons'
|
||||
import type { AppLevel, AppManifest, AppState } from '@/types/core/api'
|
||||
|
||||
export function formatAppQuality(app: { state: AppState; level: AppLevel }) {
|
||||
const variants = {
|
||||
|
@ -17,3 +18,41 @@ export function formatAppQuality(app: { state: AppState; level: AppLevel }) {
|
|||
: app.state
|
||||
return { state, variant: variants[state] }
|
||||
}
|
||||
|
||||
export function formatAppIntegration(
|
||||
{
|
||||
architectures,
|
||||
ldap,
|
||||
sso,
|
||||
multi_instance,
|
||||
ram,
|
||||
disk,
|
||||
}: AppManifest['integration'],
|
||||
packagingFormat: number,
|
||||
) {
|
||||
if (packagingFormat < 2) return undefined // LEGACY
|
||||
return {
|
||||
archs: joinOrNull(architectures),
|
||||
ldap: ldap === 'not_relevant' ? null : ldap,
|
||||
sso: sso === 'not_relevant' ? null : sso,
|
||||
multiInstance: multi_instance,
|
||||
resources: { ram: ram.runtime, disk },
|
||||
}
|
||||
}
|
||||
|
||||
export function formatAppLinks({ upstream, id, remote }: AppManifest) {
|
||||
const url = remote.url
|
||||
return {
|
||||
license: ['institution', `https://spdx.org/licenses/${upstream.license}`],
|
||||
website: ['globe', upstream.website],
|
||||
admindoc: ['book', upstream.admindoc],
|
||||
userdoc: ['book', upstream.userdoc],
|
||||
code: ['code', upstream.code],
|
||||
package: ['code', url],
|
||||
package_license: ['institution', url ? `${url}/blob/master/LICENSE` : null],
|
||||
forum: ['comments', `https://forum.yunohost.org/tag/${id}`],
|
||||
} as const
|
||||
}
|
||||
|
||||
export type AppIntegration = ReturnType<typeof formatAppIntegration>
|
||||
export type AppLinks = ReturnType<typeof formatAppLinks>
|
||||
|
|
Loading…
Add table
Reference in a new issue