community: store versionsUrl in the database
This commit is contained in:
@@ -7,6 +7,8 @@ import { prettyDate, prettyBinarySize } from '@cloudron/pankow/utils';
|
||||
import AccessControl from './AccessControl.vue';
|
||||
import PortBindings from './PortBindings.vue';
|
||||
import AppsModel from '../models/AppsModel.js';
|
||||
import CommunityModel from '../models/CommunityModel.js';
|
||||
import AppstoreModel from '../models/AppstoreModel.js';
|
||||
import DomainsModel from '../models/DomainsModel.js';
|
||||
import UsersModel from '../models/UsersModel.js';
|
||||
import GroupsModel from '../models/GroupsModel.js';
|
||||
@@ -18,7 +20,9 @@ const STEP = Object.freeze({
|
||||
INSTALL: Symbol('install'),
|
||||
});
|
||||
|
||||
const appstoreModel = AppstoreModel.create();
|
||||
const appsModel = AppsModel.create();
|
||||
const communityModel = CommunityModel.create();
|
||||
const domainsModel = DomainsModel.create();
|
||||
const usersModel = UsersModel.create();
|
||||
const groupsModel = GroupsModel.create();
|
||||
@@ -29,7 +33,7 @@ const dashboardDomain = inject('dashboardDomain');
|
||||
// reactive
|
||||
const busy = ref(false);
|
||||
const formError = ref({});
|
||||
const app = ref({});
|
||||
const appData = ref({});
|
||||
const manifest = ref({});
|
||||
const step = ref(STEP.DETAILS);
|
||||
const dialog = useTemplateRef('dialogHandle');
|
||||
@@ -155,7 +159,7 @@ async function onSubmit(overwriteDns) {
|
||||
|
||||
if (manifest.value.id === PROXY_APP_ID) config.upstreamUri = upstreamUri.value;
|
||||
|
||||
const [error, result] = await appsModel.install(manifest.value, config);
|
||||
const [error, result] = await appsModel.install(appData.value, config);
|
||||
|
||||
if (!error) {
|
||||
dialog.value.close();
|
||||
@@ -209,14 +213,36 @@ function onScreenshotNext() {
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
open: function(appData, appCountExceeded, domainList) {
|
||||
open: async function(upstreamRef, appCountExceeded, domainList) {
|
||||
busy.value = false;
|
||||
step.value = STEP.DETAILS;
|
||||
step.value = STEP.LOADING;
|
||||
formError.value = {};
|
||||
|
||||
app.value = appData;
|
||||
// give it some time to fetch before showing loading
|
||||
const openTimer = setTimeout(dialog.value.open, 200);
|
||||
|
||||
if (upstreamRef.appStoreId) {
|
||||
const [id, version] = upstreamRef.appStoreId.split('@');
|
||||
const [error, result] = await appstoreModel.get(id, version);
|
||||
if (error) {
|
||||
clearTimeout(openTimer);
|
||||
dialog.value.close();
|
||||
throw new Error('App not found');
|
||||
}
|
||||
appData.value = { ...result, ...upstreamRef };
|
||||
} else if (upstreamRef.versionsUrl) {
|
||||
const [url, version] = upstreamRef.versionsUrl.split('@');
|
||||
const [error, result] = await communityModel.getApp(url, version);
|
||||
if (error) {
|
||||
clearTimeout(openTimer);
|
||||
dialog.value.close();
|
||||
throw new Error(error.body?.message || 'Failed to fetch community app');
|
||||
}
|
||||
appData.value = { ...result, ...upstreamRef };
|
||||
}
|
||||
|
||||
appMaxCountExceeded.value = appCountExceeded;
|
||||
manifest.value = appData.manifest;
|
||||
manifest.value = appData.value.manifest;
|
||||
location.value = '';
|
||||
accessRestrictionOption.value = ACL_OPTIONS.ANY;
|
||||
accessRestrictionAcl.value = { users: [], groups: [] };
|
||||
@@ -233,8 +259,8 @@ defineExpose({
|
||||
// preselect with dashboard domain
|
||||
domain.value = domains.value.find(d => d.domain === dashboardDomain.value).domain;
|
||||
|
||||
tcpPorts.value = appData.manifest.tcpPorts;
|
||||
udpPorts.value = appData.manifest.udpPorts;
|
||||
tcpPorts.value = manifest.value.tcpPorts;
|
||||
udpPorts.value = manifest.value.udpPorts;
|
||||
|
||||
// ensure we have value property
|
||||
for (const p in tcpPorts.value) {
|
||||
@@ -246,7 +272,7 @@ defineExpose({
|
||||
udpPorts.value[p].enabled = udpPorts.value[p].enabledByDefault ?? true;
|
||||
}
|
||||
|
||||
secondaryDomains.value = appData.manifest.httpPorts;
|
||||
secondaryDomains.value = manifest.value.httpPorts;
|
||||
for (const p in secondaryDomains.value) {
|
||||
const port = secondaryDomains.value[p];
|
||||
port.value = port.defaultValue;
|
||||
@@ -254,7 +280,7 @@ defineExpose({
|
||||
}
|
||||
|
||||
currentScreenshotPos = 0;
|
||||
dialog.value.open();
|
||||
step.value = STEP.DETAILS;
|
||||
},
|
||||
close() {
|
||||
dialog.value.close();
|
||||
@@ -270,13 +296,13 @@ defineExpose({
|
||||
</div>
|
||||
<div v-else class="app-install-dialog-body" :class="{ 'step-detail': step === STEP.DETAILS, 'step-install': step === STEP.INSTALL }">
|
||||
<div class="app-install-header">
|
||||
<div class="summary" v-if="app.manifest">
|
||||
<div class="title"><img class="icon pankow-no-desktop" style="width: 32px; height: 32px; margin-right: 10px" :src="app.iconUrl" />{{ manifest.title }}</div>
|
||||
<div><a :href="manifest.website" target="_blank">{{ manifest.title }}</a> {{ app.manifest.upstreamVersion }} - {{ $t('app.updates.info.packageVersion') }} {{ app.manifest.version }}</div>
|
||||
<div>{{ $t('appstore.installDialog.lastUpdated', { date: prettyDate(app.creationDate) }) }}</div>
|
||||
<div class="summary" v-if="appData.manifest">
|
||||
<div class="title"><img class="icon pankow-no-desktop" style="width: 32px; height: 32px; margin-right: 10px" :src="appData.iconUrl" />{{ manifest.title }}</div>
|
||||
<div><a :href="manifest.website" target="_blank">{{ manifest.title }}</a> {{ appData.manifest.upstreamVersion }} - {{ $t('app.updates.info.packageVersion') }} {{ appData.manifest.version }}</div>
|
||||
<div>{{ $t('appstore.installDialog.lastUpdated', { date: prettyDate(appData.creationDate) }) }}</div>
|
||||
<div>{{ $t('appstore.installDialog.memoryRequirement', { size: prettyBinarySize(manifest.memoryLimit || (256 * 1024 * 1024)) }) }}</div>
|
||||
</div>
|
||||
<img class="icon pankow-no-mobile" :src="app.iconUrl" />
|
||||
<img class="icon pankow-no-mobile" :src="appData.iconUrl" />
|
||||
</div>
|
||||
<Transition name="slide-left" mode="out-in">
|
||||
<div v-if="step === STEP.DETAILS">
|
||||
|
||||
@@ -2,33 +2,25 @@
|
||||
|
||||
import { ref, useTemplateRef } from 'vue';
|
||||
import { Dialog, TextInput, FormGroup } from '@cloudron/pankow';
|
||||
import CommunityModel from '../models/CommunityModel.js';
|
||||
|
||||
const communityModel = CommunityModel.create();
|
||||
|
||||
const emit = defineEmits([ 'success' ]);
|
||||
|
||||
const dialog = useTemplateRef('dialog');
|
||||
|
||||
const busy = ref(false);
|
||||
const formError = ref({});
|
||||
const url = ref('');
|
||||
const version = ref('latest');
|
||||
|
||||
async function onSubmit() {
|
||||
busy.value = true;
|
||||
function onSubmit() {
|
||||
formError.value = {};
|
||||
|
||||
const [error, result] = await communityModel.getApp(url.value, version.value);
|
||||
if (error) {
|
||||
formError.value.generic = error.body?.message || 'Failed to fetch community app';
|
||||
busy.value = false;
|
||||
return console.error(error);
|
||||
if (!url.value || !version.value) {
|
||||
formError.value.generic = 'URL and version are required';
|
||||
return;
|
||||
}
|
||||
|
||||
emit('success', result);
|
||||
emit('success', { url: url.value, version: version.value });
|
||||
dialog.value.close();
|
||||
busy.value = false;
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
@@ -36,7 +28,6 @@ defineExpose({
|
||||
url.value = '';
|
||||
version.value = 'latest';
|
||||
formError.value = {};
|
||||
busy.value = false;
|
||||
dialog.value.open();
|
||||
}
|
||||
});
|
||||
@@ -47,29 +38,25 @@ defineExpose({
|
||||
<Dialog ref="dialog"
|
||||
title="Install Community App"
|
||||
confirm-label="Continue"
|
||||
:confirm-busy="busy"
|
||||
:confirm-active="!busy && url !== ''"
|
||||
:confirm-active="url !== ''"
|
||||
reject-style="secondary"
|
||||
reject-label="Cancel"
|
||||
:reject-active="!busy"
|
||||
@confirm="onSubmit()"
|
||||
>
|
||||
<form @submit.prevent="onSubmit()" autocomplete="off">
|
||||
<fieldset :disabled="busy">
|
||||
<input type="submit" style="display: none;" />
|
||||
<input type="submit" style="display: none;" />
|
||||
|
||||
<div class="error-label" v-if="formError.generic">{{ formError.generic }}</div>
|
||||
<div class="error-label" v-if="formError.generic">{{ formError.generic }}</div>
|
||||
|
||||
<FormGroup>
|
||||
<label for="urlInput">CloudronVersions.json URL</label>
|
||||
<TextInput id="urlInput" v-model="url" required placeholder="https://example.com/CloudronVersions.json"/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<label for="urlInput">CloudronVersions.json URL</label>
|
||||
<TextInput id="urlInput" v-model="url" required placeholder="https://example.com/CloudronVersions.json"/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<label for="versionInput">Version</label>
|
||||
<TextInput id="versionInput" v-model="version" required/>
|
||||
</FormGroup>
|
||||
</fieldset>
|
||||
<FormGroup>
|
||||
<label for="versionInput">Version</label>
|
||||
<TextInput id="versionInput" v-model="version" required/>
|
||||
</FormGroup>
|
||||
</form>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
@@ -31,7 +31,6 @@ async function onAckChecklistItem(item, key) {
|
||||
hasOldChecklist.value = true;
|
||||
}
|
||||
|
||||
// Notes
|
||||
async function onSubmit() {
|
||||
busy.value = true;
|
||||
|
||||
@@ -81,6 +80,7 @@ onMounted(() => {
|
||||
<div class="info-row">
|
||||
<div class="info-label">{{ $t('app.updates.info.description') }}</div>
|
||||
<div class="info-value" v-if="app.appStoreId">{{ app.manifest.title }} {{ app.manifest.upstreamVersion }}</div>
|
||||
<div class="info-value" v-if="app.versionsUrl">{{ app.versionsUrl }} {{ app.manifest.upstreamVersion }}</div>
|
||||
<div class="info-value" v-else>{{ app.manifest.dockerImage }}</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -66,7 +66,11 @@ async function onUpdate() {
|
||||
busyUpdate.value = true;
|
||||
updateError.value = '';
|
||||
|
||||
const [error, result] = await appsModel.update(props.app.id, props.app.updateInfo.manifest, skipBackup.value);
|
||||
let appData = '';
|
||||
if (props.app.appStoreId) appData = { appStoreId: `${props.app.appStoreId}@${props.app.updateInfo.manifest.version}` };
|
||||
if (props.app.versionsUrl) appData = { versionsUrl: `${props.app.versionsUrl}@${props.app.updateInfo.manifest.version}` };
|
||||
|
||||
const [error, result] = await appsModel.update(props.app.id, appData, skipBackup.value);
|
||||
if (error) {
|
||||
busyUpdate.value = false;
|
||||
if (error.status === 400) updateError.value = error.body ? error.body.message : 'Internal error';
|
||||
@@ -118,20 +122,20 @@ onMounted(async () => {
|
||||
<SettingsItem>
|
||||
<div>
|
||||
<label>{{ $t('app.updates.auto.title') }}</label>
|
||||
<div v-if="!app.appStoreId">{{ $t('app.updates.info.customAppUpdateInfo') }}</div>
|
||||
<div v-else v-html="$t('app.updates.auto.description')"></div>
|
||||
</div>
|
||||
<Switch v-if="app.appStoreId" v-model="autoUpdatesEnabled" :disabled="autoUpdatesEnabledBusy" @change="onAutoUpdatesEnabledChange"/>
|
||||
<div v-if="app.appStoreId || app.versionsUrl" v-html="$t('app.updates.auto.description')"></div>
|
||||
<div v-else>{{ $t('app.updates.info.customAppUpdateInfo') }}</div>
|
||||
</div>
|
||||
<Switch v-if="app.appStoreId || app.versionsUrl" v-model="autoUpdatesEnabled" :disabled="autoUpdatesEnabledBusy" @change="onAutoUpdatesEnabledChange"/>
|
||||
</SettingsItem>
|
||||
|
||||
<hr style="margin-top: 20px"/>
|
||||
|
||||
<div v-if="app.appStoreId">
|
||||
<div v-if="app.appStoreId || app.versionsUrl">
|
||||
<label>{{ $t('app.updatesTabTitle') }}</label>
|
||||
<div v-html="$t('app.updates.updates.description', { appStoreLink: 'https://www.cloudron.io/store/index.html' })"></div>
|
||||
</div>
|
||||
<br/>
|
||||
<Button v-if="app.appStoreId" @click="onCheck()" :disabled="busyCheck" :loading="busyCheck">{{ $t('settings.updates.checkForUpdatesAction') }}</Button>
|
||||
<Button v-if="app.appStoreId || app.versionsUrl" @click="onCheck()" :disabled="busyCheck" :loading="busyCheck">{{ $t('settings.updates.checkForUpdatesAction') }}</Button>
|
||||
|
||||
<hr v-if="app.updateInfo" style="margin-top: 20px"/>
|
||||
|
||||
|
||||
@@ -172,9 +172,8 @@ function create() {
|
||||
return {
|
||||
name: 'AppsModel',
|
||||
getTask,
|
||||
async install(manifest, config) {
|
||||
async install(appData, config) {
|
||||
const data = {
|
||||
appStoreId: manifest.id + '@' + manifest.version,
|
||||
subdomain: config.subdomain,
|
||||
domain: config.domain,
|
||||
secondaryDomains: config.secondaryDomains,
|
||||
@@ -188,6 +187,13 @@ function create() {
|
||||
backupId: config.backupId // when restoring from archive
|
||||
};
|
||||
|
||||
// Support both appstore apps (manifest) and community apps (versionsUrl)
|
||||
if (appData.versionsUrl) {
|
||||
data.versionsUrl = appData.versionsUrl;
|
||||
} else if (appData.manifest) {
|
||||
data.appStoreId = appData.appStoreId;
|
||||
}
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = await fetcher.post(`${API_ORIGIN}/api/v1/apps`, data, { access_token: accessToken });
|
||||
@@ -329,12 +335,18 @@ function create() {
|
||||
if (result.status !== 200) return [result];
|
||||
return [null, result.body.update];
|
||||
},
|
||||
async update(id, manifest, skipBackup = false) {
|
||||
async update(id, appData, skipBackup = false) {
|
||||
const data = {
|
||||
appStoreId: `${manifest.id}@${manifest.version}`,
|
||||
skipBackup: !!skipBackup,
|
||||
};
|
||||
|
||||
// Support both appstore apps (manifest) and community apps (versionsUrl)
|
||||
if (appData.versionsUrl) {
|
||||
data.versionsUrl = appData.versionsUrl;
|
||||
} else if (appData.manifest) {
|
||||
data.appStoreId = appData.appStoreId;
|
||||
}
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = await fetcher.post(`${API_ORIGIN}/api/v1/apps/${id}/update`, data, { access_token: accessToken });
|
||||
|
||||
@@ -3,23 +3,22 @@ import { fetcher } from '@cloudron/pankow';
|
||||
import { API_ORIGIN } from '../constants.js';
|
||||
|
||||
function create() {
|
||||
const accessToken = localStorage.token;
|
||||
const accessToken = localStorage.token;
|
||||
|
||||
return {
|
||||
async getApp(url, version) {
|
||||
let result;
|
||||
try {
|
||||
result = await fetcher.get(`${API_ORIGIN}/api/v1/community/app`, { access_token: accessToken, url, version });
|
||||
} catch (e) {
|
||||
return [e];
|
||||
}
|
||||
|
||||
if (result.status !== 200) return [result];
|
||||
return [null, result.body];
|
||||
}
|
||||
};
|
||||
return {
|
||||
async getApp(url, version) {
|
||||
let result;
|
||||
try {
|
||||
result = await fetcher.get(`${API_ORIGIN}/api/v1/community/app`, { access_token: accessToken, url, version });
|
||||
} catch (e) {
|
||||
return [e];
|
||||
}
|
||||
if (result.status !== 200) return [result];
|
||||
return [null, result.body];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
create,
|
||||
create,
|
||||
};
|
||||
|
||||
@@ -144,17 +144,17 @@ async function onHashChange() {
|
||||
const params = new URLSearchParams(window.location.hash.slice(window.location.hash.indexOf('?')));
|
||||
const version = params.get('version') || 'latest';
|
||||
|
||||
const [error, appData] = await appstoreModel.get(appId, version);
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return inputDialog.value.info({
|
||||
title: t('appstore.appNotFoundDialog.title'),
|
||||
message: t('appstore.appNotFoundDialog.description', { appId, version }),
|
||||
confirmLabel: t('main.dialog.close'),
|
||||
});
|
||||
}
|
||||
// const [error, appData] = await appstoreModel.get(appId, version);
|
||||
// if (error) {
|
||||
// console.error(error);
|
||||
// return inputDialog.value.info({
|
||||
// title: t('appstore.appNotFoundDialog.title'),
|
||||
// message: t('appstore.appNotFoundDialog.description', { appId, version }),
|
||||
// confirmLabel: t('main.dialog.close'),
|
||||
// });
|
||||
// }
|
||||
|
||||
appInstallDialog.value.open(appData, installedApps.value.length >= features.value.appMaxCount, domains.value);
|
||||
appInstallDialog.value.open({ appStoreId: `${appId}@${version}` }, installedApps.value.length >= features.value.appMaxCount, domains.value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,8 +194,10 @@ function onInstallCommunityApp() {
|
||||
communityAppDialog.value.open();
|
||||
}
|
||||
|
||||
function onCommunityAppSuccess(appData) {
|
||||
appInstallDialog.value.open(appData, installedApps.value.length >= features.value.appMaxCount, domains.value);
|
||||
function onCommunityAppSuccess({ url, version }) {
|
||||
// Construct versionsUrl in url@version format
|
||||
const versionsUrl = `${url}@${version}`;
|
||||
appInstallDialog.value.open({ versionsUrl }, installedApps.value.length >= features.value.appMaxCount, domains.value);
|
||||
}
|
||||
|
||||
onActivated(async () => {
|
||||
|
||||
Reference in New Issue
Block a user