Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(settings): migrate AppAPI ExApps management to settings #48665

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 12 additions & 19 deletions apps/settings/lib/Controller/AppSettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\L10N\IFactory;
use OCP\Server;
use OCP\Util;
use Psr\Log\LoggerInterface;

Expand Down Expand Up @@ -78,6 +79,8 @@ public function __construct(
}

/**
* @psalm-suppress UndefinedClass AppAPI is shipped since 30.0.1
*
* @return TemplateResponse
*/
#[NoCSRFRequired]
Expand All @@ -89,6 +92,13 @@ public function viewApps(): TemplateResponse {
$this->initialState->provideInitialState('appstoreDeveloperDocs', $this->urlGenerator->linkToDocs('developer-manual'));
$this->initialState->provideInitialState('appstoreUpdateCount', count($this->getAppsWithUpdates()));

if ($this->appManager->isInstalled('app_api')) {
try {
Server::get(\OCA\AppAPI\Service\ExAppsPageService::class)->provideAppApiState($this->initialState);
} catch (\Psr\Container\NotFoundExceptionInterface|\Psr\Container\ContainerExceptionInterface $e) {
}
}

$policy = new ContentSecurityPolicy();
$policy->addAllowedImageDomain('https://usercontent.apps.nextcloud.com');

Expand Down Expand Up @@ -230,25 +240,11 @@ private function getAllCategories() {
], $categories);
}

/**
* Convert URL to proxied URL so CSP is no problem
*/
private function createProxyPreviewUrl(string $url): string {
if ($url === '') {
return '';
}
return 'https://usercontent.apps.nextcloud.com/' . base64_encode($url);
}

private function fetchApps() {
$appClass = new \OC_App();
$apps = $appClass->listAllApps();
foreach ($apps as $app) {
$app['installed'] = true;
// locally installed apps have a flatted screenshot property
if (isset($app['screenshot'][0])) {
$app['screenshot'] = $this->createProxyPreviewUrl($app['screenshot'][0]);
}
$this->allApps[$app['id']] = $app;
}

Expand Down Expand Up @@ -307,7 +303,7 @@ public function listApps(): JSONResponse {
$apps = array_map(function (array $appData) use ($dependencyAnalyzer, $ignoreMaxApps) {
if (isset($appData['appstoreData'])) {
$appstoreData = $appData['appstoreData'];
$appData['screenshot'] = $this->createProxyPreviewUrl($appstoreData['screenshots'][0]['url'] ?? '');
$appData['screenshot'] = isset($appstoreData['screenshots'][0]['url']) ? 'https://usercontent.apps.nextcloud.com/' . base64_encode($appstoreData['screenshots'][0]['url']) : '';
$appData['category'] = $appstoreData['categories'];
$appData['releases'] = $appstoreData['releases'];
}
Expand All @@ -321,10 +317,6 @@ public function listApps(): JSONResponse {
$groups = [];
if (is_string($appData['groups'])) {
$groups = json_decode($appData['groups']);
// ensure 'groups' is an array
if (!is_array($groups)) {
$groups = [$groups];
}
}
$appData['groups'] = $groups;
$appData['canUnInstall'] = !$appData['active'] && $appData['removable'];
Expand Down Expand Up @@ -432,6 +424,7 @@ private function getAppsForCategory($requestedCategory = ''): array {

$formattedApps[] = [
'id' => $app['id'],
'app_api' => false,
'name' => $app['translations'][$currentLanguage]['name'] ?? $app['translations']['en']['name'],
'description' => $app['translations'][$currentLanguage]['description'] ?? $app['translations']['en']['description'],
'summary' => $app['translations'][$currentLanguage]['summary'] ?? $app['translations']['en']['summary'],
Expand Down
44 changes: 42 additions & 2 deletions apps/settings/src/app-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,54 @@ export interface IAppstoreApp {
preview?: string
screenshot?: string

app_api: boolean
active: boolean
internal: boolean
removeable: boolean
removable: boolean
installed: boolean
canInstall: boolean
canUninstall: boolean
canUnInstall: boolean
isCompatible: boolean
needsDownload: boolean
update?: string

appstoreData: Record<string, never>
releases?: IAppstoreAppRelease[]
}

export interface IComputeDevice {
id: string,
label: string,
}

export interface IDeployConfig {
computeDevice: IComputeDevice,
net: string,
nextcloud_url: string,
}

export interface IDeployDaemon {
accepts_deploy_id: string,
deploy_config: IDeployConfig,
display_name: string,
host: string,
id: number,
name: string,
protocol: string,
}

export interface IExAppStatus {
action: string
deploy: number
deploy_start_time: number
error: string
init: number
init_start_time: number
type: string
}

export interface IAppstoreExApp extends IAppstoreApp {
daemon: IDeployDaemon | null | undefined
status: IExAppStatus | Record<string, never>
error: string
}
26 changes: 22 additions & 4 deletions apps/settings/src/components/AppList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ import { subscribe, unsubscribe } from '@nextcloud/event-bus'
import AppItem from './AppList/AppItem.vue'
import pLimit from 'p-limit'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import { useAppApiStore } from '../store/app-api-store'

export default {
name: 'AppList',
Expand All @@ -158,6 +159,13 @@ export default {
},
},

setup() {
const appApiStore = useAppApiStore()
return {
appApiStore,
}
},

data() {
return {
search: '',
Expand All @@ -168,7 +176,10 @@ export default {
return this.apps.filter(app => app.update).length
},
loading() {
return this.$store.getters.loading('list')
if (!this.$store.getters['appApiApps/isAppApiEnabled']) {
return this.$store.getters.loading('list')
}
return this.$store.getters.loading('list') || this.appApiStore.getLoading('list')
},
hasPendingUpdate() {
return this.apps.filter(app => app.update).length > 0
Expand All @@ -177,7 +188,9 @@ export default {
return this.hasPendingUpdate && this.useListView
},
apps() {
const apps = this.$store.getters.getAllApps
// Exclude ExApps from the list if AppAPI is disabled
const exApps = this.$store.getters.isAppApiEnabled ? this.appApiStore.getAllApps : []
const apps = [...this.$store.getters.getAllApps, ...exApps]
.filter(app => app.name.toLowerCase().search(this.search.toLowerCase()) !== -1)
.sort(function(a, b) {
const sortStringA = '' + (a.active ? 0 : 1) + (a.update ? 0 : 1) + a.name
Expand Down Expand Up @@ -304,8 +317,13 @@ export default {
const limit = pLimit(1)
this.apps
.filter(app => app.update)
.map(app => limit(() => this.$store.dispatch('updateApp', { appId: app.id })),
)
.map(app => limit(() => {
let type = 'updateApp'
if (app?.app_api) {
type = 'appApiApps/updateApp'
}
this.$store.dispatch(type, { appId: app.id })
}))
},
},
}
Expand Down
37 changes: 37 additions & 0 deletions apps/settings/src/components/AppList/AppDaemonBadge.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<!--
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<span v-if="daemon"
class="app-daemon-badge"
:title="daemon.name">
<NcIconSvgWrapper :path="mdiFileChart" :size="20" inline />
{{ daemon.display_name }}
</span>
</template>

<script setup lang="ts">
import type { IDeployDaemon } from '../../app-types.ts'
import { mdiFileChart } from '@mdi/js'
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'

defineProps<{
daemon?: IDeployDaemon
}>()
</script>

<style scoped lang="scss">
.app-daemon-badge {
color: var(--color-text-maxcontrast);
background-color: transparent;
border: 1px solid var(--color-text-maxcontrast);
border-radius: var(--border-radius);

display: flex;
flex-direction: row;
gap: 6px;
padding: 3px 6px;
width: fit-content;
}
</style>
36 changes: 28 additions & 8 deletions apps/settings/src/components/AppList/AppItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@
<component :is="dataItemTag"
class="app-image app-image-icon"
:headers="getDataItemHeaders(`app-table-col-icon`)">
<div v-if="(listView && !app.preview) || (!listView && !screenshotLoaded)" class="icon-settings-dark" />
<div v-if="!app?.app_api && shouldDisplayDefaultIcon" class="icon-settings-dark" />
<NcIconSvgWrapper v-else-if="app.app_api && shouldDisplayDefaultIcon"
:path="mdiCogOutline"
:size="listView ? 24 : 48"
style="min-width: auto; min-height: auto; height: 100%;" />

<svg v-else-if="listView && app.preview"
<svg v-else-if="listView && app.preview && !app.app_api"
width="32"
height="32"
viewBox="0 0 32 32">
Expand Down Expand Up @@ -71,10 +75,11 @@
<div v-if="app.error" class="warning">
{{ app.error }}
</div>
<div v-if="isLoading" class="icon icon-loading-small" />
<div v-if="isLoading || isInitializing" class="icon icon-loading-small" />
<NcButton v-if="app.update"
type="primary"
:disabled="installing || isLoading"
:disabled="installing || isLoading || !defaultDeployDaemonAccessible || isManualInstall"
:title="updateButtonText"
@click.stop="update(app.id)">
{{ t('settings', 'Update to {update}', {update:app.update}) }}
</NcButton>
Expand All @@ -86,23 +91,23 @@
{{ t('settings', 'Remove') }}
</NcButton>
<NcButton v-if="app.active"
:disabled="installing || isLoading"
:disabled="installing || isLoading || isInitializing || isDeploying"
@click.stop="disable(app.id)">
{{ t('settings','Disable') }}
{{ disableButtonText }}
</NcButton>
<NcButton v-if="!app.active && (app.canInstall || app.isCompatible)"
:title="enableButtonTooltip"
:aria-label="enableButtonTooltip"
type="primary"
:disabled="!app.canInstall || installing || isLoading"
:disabled="!app.canInstall || installing || isLoading || !defaultDeployDaemonAccessible || isInitializing || isDeploying"
@click.stop="enable(app.id)">
{{ enableButtonText }}
</NcButton>
<NcButton v-else-if="!app.active"
:title="forceEnableButtonTooltip"
:aria-label="forceEnableButtonTooltip"
type="secondary"
:disabled="installing || isLoading"
:disabled="installing || isLoading || !defaultDeployDaemonAccessible"
@click.stop="forceEnable(app.id)">
{{ forceEnableButtonText }}
</NcButton>
Expand All @@ -116,13 +121,17 @@ import AppLevelBadge from './AppLevelBadge.vue'
import AppManagement from '../../mixins/AppManagement.js'
import SvgFilterMixin from '../SvgFilterMixin.vue'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
import { mdiCogOutline } from '@mdi/js'
import { useAppApiStore } from '../../store/app-api-store.ts'

export default {
name: 'AppItem',
components: {
AppLevelBadge,
AppScore,
NcButton,
NcIconSvgWrapper,
},
mixins: [AppManagement, SvgFilterMixin],
props: {
Expand Down Expand Up @@ -151,6 +160,14 @@ export default {
default: false,
},
},
setup() {
const appApiStore = useAppApiStore()

return {
appApiStore,
mdiCogOutline,
}
},
data() {
return {
isSelected: false,
Expand All @@ -168,6 +185,9 @@ export default {
withSidebar() {
return !!this.$route.params.id
},
shouldDisplayDefaultIcon() {
return (this.listView && !this.app.preview) || (!this.listView && !this.screenshotLoaded)
},
},
watch: {
'$route.params.id'(id) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<!--
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->

<template>
<NcAppSidebarTab v-if="app?.daemon"
id="daemon"
:name="t('settings', 'Daemon')"
:order="3">
<template #icon>
<NcIconSvgWrapper :path="mdiFileChart" :size="24" />
</template>
<div class="daemon">
<h4>{{ t('settings', 'Deploy Daemon') }}</h4>
<p><b>{{ t('settings', 'Type') }}</b>: {{ app?.daemon.accepts_deploy_id }}</p>
<p><b>{{ t('settings', 'Name') }}</b>: {{ app?.daemon.name }}</p>
<p><b>{{ t('settings', 'Display Name') }}</b>: {{ app?.daemon.display_name }}</p>
<p><b>{{ t('settings', 'GPUs support') }}</b>: {{ gpuSupport }}</p>
<p><b>{{ t('settings', 'Compute device') }}</b>: {{ app?.daemon?.deploy_config?.computeDevice?.label }}</p>
</div>
</NcAppSidebarTab>
</template>

<script setup lang="ts">
import type { IAppstoreExApp } from '../../app-types'

import NcAppSidebarTab from '@nextcloud/vue/dist/Components/NcAppSidebarTab.js'
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'

import { mdiFileChart } from '@mdi/js'
import { ref } from 'vue'

const props = defineProps<{
app: IAppstoreExApp,
}>()

const gpuSupport = ref(props.app?.daemon?.deploy_config?.computeDevice?.id !== 'cpu' || false)
</script>

<style scoped lang="scss">
.daemon {
padding: 20px;

h4 {
font-weight: bold;
margin: 10px auto;
}
}
</style>
Loading
Loading