Files
cloudron-box/dashboard/src/components/AppInstallDialog.vue

289 lines
8.4 KiB
Vue
Raw Normal View History

2025-01-05 22:47:50 +01:00
<template>
2025-01-10 16:03:52 +01:00
<Dialog ref="dialogHandle" @close="onClose()">
<div class="content" :class="{ 'step-detail': step === STEP.DETAILS, 'step-install': step === STEP.INSTALL }">
2025-01-05 22:47:50 +01:00
<div class="header">
<div class="summary">
2025-01-05 22:47:50 +01:00
<div class="title">{{ manifest.title }}</div>
<div class="lastUpdated">{{ $t('appstore.installDialog.lastUpdated', { date: prettyDate(app.creationDate) }) }}</div>
<div class="memoryRequirement">{{ $t('appstore.installDialog.memoryRequirement', { size: prettyFileSize(manifest.memoryLimit) }) }}</div>
<div class="author"><a :href="manifest.website" target="_blank">Website</a></div>
</div>
<img class="icon" :src="app.iconUrl" />
2025-01-05 22:47:50 +01:00
</div>
<Transition name="slide-left" mode="out-in">
<div v-if="step === STEP.DETAILS">
<Button @click="step = STEP.INSTALL" icon="fa-solid fa-circle-down">Install {{ manifest.title }}</Button>
<div class="screenshots">
<img class="screenshot" v-for="image in manifest.mediaLinks" :key="image" :src="image"/>
</div>
<div class="description" v-html="description"></div>
</div>
<div v-else-if="step === STEP.INSTALL">
2025-01-07 12:49:19 +01:00
<form @submit.prevent="submit()" autocomplete="off">
2025-01-06 14:03:29 +01:00
<fieldset :disabled="busy">
<input style="display: none;" type="submit" :disabled="!formValid" />
2025-01-07 12:49:19 +01:00
<FormGroup :class="{ 'has-error': formError.location }">
2025-01-06 14:03:29 +01:00
<label for="location">{{ $t('appstore.installDialog.location') }}</label>
2025-01-06 14:35:14 +01:00
<div>
<TextInput id="location" v-model="location" />
<Dropdown v-model="domain" :options="domains" option-label="domain" />
2025-01-06 14:35:14 +01:00
</div>
2025-01-06 14:03:29 +01:00
</FormGroup>
<p class="text-small text-warning" v-show="domain.provider === 'noop' || domain.provider === 'manual'" v-html="$t('appstore.installDialog.manualWarning', { location: ((location ? location + '.' : '') + domain.domain) })"></p>
<FormGroup v-for="(port, key) in secondaryDomains" :key="key">
<label :for="'secondaryDomainInput' + key">{{ port.title }}</label>
<small>{{ port.description }}</small>
<div>
<TextInput :id="'secondaryDomainInput' + key" v-model="port.value" :placeholder="$t('appstore.installDialog.locationPlaceholder')"/>
<Dropdown v-model="port.domain" :options="domains" option-label="domain" option-key="domain" />
</div>
</FormGroup>
<FormGroup :class="{ 'has-error': formError.upstreamUri }" v-show="manifest.id === PROXY_APP_ID">
<label for="upstreamUri">Upstream URI</label>
<TextInput id="upstreamUri" v-model="upstreamUri" />
</FormGroup>
2025-01-06 20:23:39 +01:00
2025-01-07 12:49:19 +01:00
<PortBindings v-model:tcp-ports="tcpPorts" v-model:udp-ports="udpPorts" :error="formError"/>
2025-01-09 18:28:51 +01:00
<AccessControl v-model="accessRestriction" :manifest="manifest"/>
2025-01-06 14:35:14 +01:00
<Button @click="submit" icon="fa-solid fa-circle-down" :disabled="!formValid" :loading="busy">Install {{ manifest.title }}</Button>
2025-01-06 14:03:29 +01:00
</fieldset>
</form>
</div>
</Transition>
2025-01-05 22:47:50 +01:00
</div>
</Dialog>
</template>
<script setup>
2025-01-05 22:47:50 +01:00
2025-01-06 14:35:14 +01:00
import { ref, computed, useTemplateRef, onMounted } from 'vue';
2025-01-05 22:47:50 +01:00
import { marked } from 'marked';
2025-01-06 14:35:14 +01:00
import { Button, Dialog, Dropdown, FormGroup, TextInput } from 'pankow';
2025-01-06 14:03:29 +01:00
import { prettyDate, prettyFileSize } from 'pankow/utils';
import AccessControl from './AccessControl.vue';
2025-01-06 20:23:39 +01:00
import PortBindings from './PortBindings.vue';
2025-01-06 14:35:14 +01:00
import DomainsModel from '../models/DomainsModel.js';
import AppsModel from '../models/AppsModel.js';
import DashboardModel from '../models/DashboardModel.js';
import { PROXY_APP_ID } from '../constants.js';
2025-01-06 14:35:14 +01:00
const API_ORIGIN = import.meta.env.VITE_API_ORIGIN ? import.meta.env.VITE_API_ORIGIN : window.location.origin;
2025-01-05 22:47:50 +01:00
const STEP = Object.freeze({
DETAILS: Symbol('details'),
INSTALL: Symbol('install'),
});
2025-01-06 14:35:14 +01:00
const domainsModel = DomainsModel.create(API_ORIGIN, localStorage.token);
const appsModel = AppsModel.create(API_ORIGIN, localStorage.token);
const dashboardModel = DashboardModel.create(API_ORIGIN, localStorage.token);
2025-01-06 14:35:14 +01:00
// reactive
2025-01-06 14:35:14 +01:00
const busy = ref(false);
2025-01-07 12:49:19 +01:00
const formError = ref({});
const app = ref({});
const manifest = ref({});
const step = ref(STEP.DETAILS);
const dialog = useTemplateRef('dialogHandle');
const description = computed(() => marked.parse(manifest.value.description || ''));
2025-01-06 14:35:14 +01:00
const domains = ref([]);
2025-01-06 14:03:29 +01:00
const formValid = computed(() => {
2025-01-06 14:35:14 +01:00
if (!domain.value) return false;
if (manifest.value.id === PROXY_APP_ID) {
try {
new URL(upstreamUri.value);
// eslint-disable-next-line no-unused-vars
} catch (e) {
return false;
}
}
2025-01-06 14:03:29 +01:00
return true;
});
// form data
const location = ref('');
2025-01-09 18:28:51 +01:00
// accessRestriction:
// object = SSO with user groups
// true = SSO all
// false = NOSSO
const accessRestriction = ref(true);
const domain = ref({});
2025-01-06 20:23:39 +01:00
const tcpPorts = ref({});
const udpPorts = ref({});
const secondaryDomains = ref({});
const upstreamUri = ref('');
2025-01-06 14:35:14 +01:00
async function submit() {
const config = {
subdomain: location.value,
domain: domain.value.domain,
2025-01-09 18:28:51 +01:00
accessRestriction: typeof accessRestriction.value === 'object' ? accessRestriction.value : null,
2025-01-06 14:35:14 +01:00
};
2025-01-09 18:28:51 +01:00
if (manifest.value.optionalSso) config.sso = accessRestriction.value;
2025-01-06 14:03:29 +01:00
2025-01-06 20:23:39 +01:00
const finalPorts = {};
for (const p in tcpPorts.value) {
const port = tcpPorts.value[p];
if (port.enabled) finalPorts[p] = port.value;
}
for (const p in udpPorts.value) {
const port = udpPorts.value[p];
if (port.enabled) finalPorts[p] = port.value;
}
config.ports = finalPorts;
const finalSecondaryDomains = {};
for (var p in secondaryDomains.value) {
finalSecondaryDomains[p] = {
subdomain: secondaryDomains.value[p].value,
domain: secondaryDomains.value[p].domain
};
}
config.secondaryDomains = finalSecondaryDomains;
if (manifest.value.id === PROXY_APP_ID) {
config.upstreamUri = upstreamUri.value;
}
2025-01-06 14:35:14 +01:00
busy.value = true;
const error = await appsModel.install(manifest.value, config);
busy.value = false;
2025-01-06 14:03:29 +01:00
2025-01-06 14:35:14 +01:00
if (!error) {
dialog.value.close();
return window.location.href = '#/apps';
}
2025-01-07 12:49:19 +01:00
formError.value = {};
if (error.status === 'Conflict' && error.message.indexOf('port') !== -1) {
const match = error.message.match(/.*port.(.*)/);
formError.value.port = match ? parseInt(match[1]) : null;
} else if (error.status === 'Conflict' && error.message.indexOf('primary location') !== -1) {
formError.value.location = true;
} else {
console.error('Failed to install:', error);
}
2025-01-06 14:03:29 +01:00
}
2025-01-10 16:03:52 +01:00
function onClose() {
setTimeout(() => step.value = STEP.DETAILS, 2000);
}
2025-01-06 14:35:14 +01:00
onMounted(async () => {
domains.value = await domainsModel.list();
const config = await dashboardModel.getConfig();
2025-01-06 14:35:14 +01:00
// preselect with dashboard domain
domain.value = domains.value.find(d => d.domain === config.adminDomain) || domains.value[0];
2025-01-06 14:35:14 +01:00
});
defineExpose({
open(a) {
2025-01-06 21:03:29 +01:00
step.value = STEP.DETAILS;
app.value = a;
manifest.value = a.manifest;
2025-01-06 20:23:39 +01:00
tcpPorts.value = a.manifest.tcpPorts;
udpPorts.value = a.manifest.udpPorts;
secondaryDomains.value = a.manifest.httpPorts;
for (const p in secondaryDomains.value) {
const port = secondaryDomains.value[p];
port.value = port.defaultValue;
port.domain = domains.value[0].domain;
}
dialog.value.open();
}
});
2025-01-05 22:47:50 +01:00
</script>
<style scoped>
.slide-left-enter-active,
.slide-left-leave-active {
transition: all 0.25s ease-out;
}
.slide-left-enter-from {
opacity: 0;
transform: translateY(30px);
}
.slide-left-leave-to {
opacity: 0;
}
.content {
max-width: 100%;
max-height: 100%;
transition: all 0.25s;
2025-01-10 16:03:52 +01:00
}
.step-detail {
width: 1024px;
}
.step-install {
width: 480px;
}
2025-01-05 22:47:50 +01:00
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin: 4px;
2025-01-05 22:47:50 +01:00
}
.summary {
2025-01-05 22:47:50 +01:00
display: flex;
flex-direction: column;
font-size: 14px;
2025-01-05 22:47:50 +01:00
}
.title {
font-size: 32px;
2025-01-05 22:47:50 +01:00
margin-bottom: 10px;
}
.icon {
width: 128px;
height: 128px;
object-fit: contain;
2025-01-05 22:47:50 +01:00
margin-right: 20px;
}
.description {
margin: 0 4px;
}
.screenshots {
margin: 10px 4px;
2025-01-05 22:47:50 +01:00
display: flex;
gap: 20px;
width: 100%;
padding: 10px 0;
scroll-snap-type: x mandatory;
overflow: auto;
}
.screenshot {
display: inline-block;
border-radius: var(--pankow-border-radius);
height: 300px;
object-size: contain;
scroll-snap-align: center;
scroll-snap-stop: always;
2025-01-05 22:47:50 +01:00
}
</style>