2025-01-05 22:47:50 +01:00
|
|
|
<template>
|
2025-01-06 12:16:17 +01:00
|
|
|
<Dialog ref="dialogHandle" :show-x="true" @close="step = 0">
|
2025-01-05 23:35:01 +01:00
|
|
|
<div class="content">
|
2025-01-05 22:47:50 +01:00
|
|
|
<div class="header">
|
2025-01-05 23:35:01 +01:00
|
|
|
<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>
|
2025-01-05 23:35:01 +01:00
|
|
|
<img class="icon" :src="app.iconUrl" />
|
2025-01-05 22:47:50 +01:00
|
|
|
</div>
|
2025-01-05 23:35:01 +01:00
|
|
|
<Transition name="slide-left" mode="out-in">
|
2025-01-06 12:16:17 +01:00
|
|
|
<div v-if="step === STEP.DETAILS">
|
|
|
|
|
<Button @click="step = STEP.INSTALL" icon="fa-solid fa-circle-down">Install {{ manifest.title }}</Button>
|
2025-01-05 23:35:01 +01:00
|
|
|
<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>
|
2025-01-06 12:16:17 +01:00
|
|
|
<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" option-key="domain" />
|
|
|
|
|
</div>
|
2025-01-06 14:03:29 +01:00
|
|
|
</FormGroup>
|
|
|
|
|
|
2025-01-06 21:19:57 +01:00
|
|
|
<!-- TODO: <p class="text-small text-warning" ng-show="appInstall.domain.provider === 'noop' || appInstall.domain.provider === 'manual'" ng-bind-html="'appstore.installDialog.manualWarning' | tr:{ location: ((appInstall.subdomain ? appInstall.subdomain + '.' : '') + appInstall.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>
|
|
|
|
|
|
2025-01-06 20:23:39 +01:00
|
|
|
<!-- TODO upstreamUri for proxyapp -->
|
|
|
|
|
|
2025-01-07 12:49:19 +01:00
|
|
|
<PortBindings v-model:tcp-ports="tcpPorts" v-model:udp-ports="udpPorts" :error="formError"/>
|
2025-01-06 14:03:29 +01:00
|
|
|
<AccessControl v-model="accessRestriction"/>
|
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>
|
2025-01-05 23:35:01 +01:00
|
|
|
</div>
|
|
|
|
|
</Transition>
|
2025-01-05 22:47:50 +01:00
|
|
|
</div>
|
|
|
|
|
</Dialog>
|
|
|
|
|
</template>
|
|
|
|
|
|
2025-01-06 12:16:17 +01:00
|
|
|
<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';
|
2025-01-07 12:00:31 +01:00
|
|
|
import DashboardModel from '../models/DashboardModel.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
|
|
|
|
2025-01-06 12:16:17 +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);
|
2025-01-07 12:00:31 +01:00
|
|
|
const dashboardModel = DashboardModel.create(API_ORIGIN, localStorage.token);
|
2025-01-06 14:35:14 +01:00
|
|
|
|
2025-01-06 12:16:17 +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({});
|
2025-01-06 12:16:17 +01:00
|
|
|
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;
|
2025-01-06 14:03:29 +01:00
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// form data
|
|
|
|
|
const location = ref('');
|
|
|
|
|
const accessRestriction = ref(null);
|
2025-01-06 14:35:14 +01:00
|
|
|
const domain = ref('');
|
2025-01-06 20:23:39 +01:00
|
|
|
const tcpPorts = ref({});
|
|
|
|
|
const udpPorts = ref({});
|
2025-01-06 21:19:57 +01:00
|
|
|
const secondaryDomains = ref({});
|
2025-01-06 14:35:14 +01:00
|
|
|
|
|
|
|
|
async function submit() {
|
|
|
|
|
const config = {
|
|
|
|
|
subdomain: location.value,
|
|
|
|
|
domain: domain.value,
|
|
|
|
|
accessRestriction: accessRestriction.value,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
2025-01-06 21:19:57 +01:00
|
|
|
const finalSecondaryDomains = {};
|
|
|
|
|
for (var p in secondaryDomains.value) {
|
|
|
|
|
finalSecondaryDomains[p] = {
|
|
|
|
|
subdomain: secondaryDomains.value[p].value,
|
|
|
|
|
domain: secondaryDomains.value[p].domain
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
config.secondaryDomains = finalSecondaryDomains;
|
|
|
|
|
|
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-06 12:16:17 +01:00
|
|
|
|
2025-01-06 14:35:14 +01:00
|
|
|
onMounted(async () => {
|
|
|
|
|
domains.value = await domainsModel.list();
|
2025-01-07 12:00:31 +01:00
|
|
|
const config = await dashboardModel.getConfig();
|
2025-01-06 14:35:14 +01:00
|
|
|
|
2025-01-07 12:00:31 +01:00
|
|
|
// preselect with dashboard domain
|
|
|
|
|
domain.value = config.adminDomain || domains.value[0].domain;
|
2025-01-06 14:35:14 +01:00
|
|
|
});
|
|
|
|
|
|
2025-01-06 12:16:17 +01:00
|
|
|
defineExpose({
|
|
|
|
|
open(a) {
|
2025-01-06 21:03:29 +01:00
|
|
|
step.value = STEP.DETAILS;
|
2025-01-06 12:16:17 +01:00
|
|
|
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;
|
|
|
|
|
|
2025-01-06 21:19:57 +01:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-06 12:16:17 +01:00
|
|
|
dialog.value.open();
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-01-05 22:47:50 +01:00
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
2025-01-05 23:35:01 +01:00
|
|
|
.slide-left-enter-active,
|
|
|
|
|
.slide-left-leave-active {
|
|
|
|
|
transition: all 0.25s ease-out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.slide-left-enter-from {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transform: translateX(30px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.slide-left-leave-to {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transform: translateX(-30px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.content {
|
|
|
|
|
width: 1024px;
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
height: 1024px;
|
|
|
|
|
max-height: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-05 22:47:50 +01:00
|
|
|
.header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
2025-01-05 23:35:01 +01:00
|
|
|
justify-content: space-between;
|
|
|
|
|
margin: 4px;
|
2025-01-05 22:47:50 +01:00
|
|
|
}
|
|
|
|
|
|
2025-01-05 23:35:01 +01:00
|
|
|
.summary {
|
2025-01-05 22:47:50 +01:00
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
2025-01-05 23:35:01 +01:00
|
|
|
font-size: 14px;
|
2025-01-05 22:47:50 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.title {
|
2025-01-05 23:35:01 +01:00
|
|
|
font-size: 32px;
|
2025-01-05 22:47:50 +01:00
|
|
|
margin-bottom: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.icon {
|
2025-01-05 23:35:01 +01:00
|
|
|
width: 128px;
|
|
|
|
|
height: 128px;
|
|
|
|
|
object-fit: contain;
|
2025-01-05 22:47:50 +01:00
|
|
|
margin-right: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.description {
|
2025-01-05 23:35:01 +01:00
|
|
|
margin: 0 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.screenshots {
|
|
|
|
|
margin: 10px 4px;
|
2025-01-05 22:47:50 +01:00
|
|
|
display: flex;
|
2025-01-05 23:35:01 +01:00
|
|
|
gap: 20px;
|
|
|
|
|
width: 100%;
|
|
|
|
|
padding: 10px 0;
|
2025-01-06 12:16:17 +01:00
|
|
|
|
|
|
|
|
scroll-snap-type: x mandatory;
|
2025-01-05 23:35:01 +01:00
|
|
|
overflow: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.screenshot {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
border-radius: var(--pankow-border-radius);
|
|
|
|
|
height: 300px;
|
|
|
|
|
object-size: contain;
|
2025-01-06 12:16:17 +01:00
|
|
|
scroll-snap-align: center;
|
|
|
|
|
scroll-snap-stop: always;
|
2025-01-05 22:47:50 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
</style>
|