Files
cloudron-box/dashboard/src/components/AppsView.vue
T

347 lines
9.6 KiB
Vue
Raw Normal View History

2024-12-29 00:36:48 +01:00
<template>
<div class="content">
2024-12-29 19:19:03 +01:00
<h1 class="section-header">
{{ $t('apps.title') }}
2025-01-02 13:21:59 +01:00
<div>
<TextInput v-model="filter" placeholder="Filter ..." />
<Button tool @click="toggleView()" :icon="viewType === VIEW_TYPE.GRID ? 'fas fa-list' : 'fas fa-grip'"></Button>
</div>
2024-12-29 19:19:03 +01:00
</h1>
2024-12-29 00:36:48 +01:00
2025-01-02 13:21:59 +01:00
<div v-show="ready">
<TransitionGroup name="grid-animation" tag="div" class="grid" v-if="viewType === VIEW_TYPE.GRID">
<a v-for="app in filteredApps" :key="app.id" class="grid-item" @click="onOpenApp(app, $event)" :href="'https://' + app.fqdn" target="_blank" v-tooltip="app.fqdn">
2025-01-02 19:04:07 +01:00
<a class="config" v-show="isOperator(app)" :href="`#/app/${app.id}/info`" @click="openAppInfo(app)"><Icon icon="fa-solid fa-cog" /></a>
2025-01-02 13:21:59 +01:00
<img :src="API_ORIGIN + app.iconUrl"/>
2025-01-02 19:04:07 +01:00
<div class="grid-item-label">{{ app.label || app.subdomain || app.fqdn }}</div>
<div class="apps-progress" v-show="isOperator(app)">
<div class="apps-progress-filled" :style="{ width: app.progress+'%' }"></div>
<div class="apps-progress-label">{{ installationStateLabel(app) }}</div>
</div>
2025-01-02 13:21:59 +01:00
</a>
</TransitionGroup>
<div class="list" v-if="viewType === VIEW_TYPE.LIST">
<TableView :columns="listColumns" :model="filteredApps">
<template #icon="slotProps">
<img class="list-icon" :src="API_ORIGIN + slotProps.iconUrl"/>
</template>
<template #label="slotProps">
<a :href="'https://' + slotProps.fqdn" target="_blank" v-tooltip="slotProps.fqdn">
{{ slotProps.label || slotProps.subdomain || slotProps.fqdn }}
</a>
</template>
<template #appTitle="slotProps">
{{ slotProps.manifest.title }}
</template>
<template #domain="slotProps">
{{ slotProps.fqdn }}
</template>
<template #sso="slotProps">
<div v-show="slotProps.type !== APP_TYPES.LINK">
<Icon icon="fa-brands fa-openid" v-show="slotProps.ssoAuth && slotProps.manifest.addons.oidc" v-tooltip="$t('apps.auth.openid')" />
<Icon icon="fas fa-user" v-show="slotProps.ssoAuth && (!slotProps.manifest.addons.oidc && !slotProps.manifest.addons.email)" v-tooltip="$t('apps.auth.sso')" />
<Icon icon="far fa-user" v-show="!slotProps.ssoAuth && !slotProps.manifest.addons.email" v-tooltip="$t('apps.auth.nosso')" />
<Icon icon="fas fa-envelope" v-show="slotProps.manifest.addons.email" v-tooltip="$t('apps.auth.email')" />
</div>
</template>
<template #actions="slotProps">
<div class="actions">
<ButtonGroup>
<Button tool v-if="slotProps.type !== APP_TYPES.LINK" :href="'/logs.html?appId=' + slotProps.id" target="_blank" v-tooltip="$t('app.logsActionTooltip')" icon="fas fa-align-left"></Button>
<Button tool v-if="slotProps.type !== APP_TYPES.PROXIED && slotProps.type !== APP_TYPES.LINK" :href="'/terminal.html?id=' + slotProps.id" target="_blank" v-tooltip="$t('app.terminalActionTooltip')" icon="fa fa-terminal"></Button>
<Button tool v-if="slotProps.manifest.addons.localstorage" :href="'/filemanager.html#/home/app/' + slotProps.id" target="_blank" v-tooltip="$t('app.filemanagerActionTooltip')" icon="fas fa-folder"></Button>
</ButtonGroup>
<Button tool :href="`#/app/${slotProps.id}/info`" icon="fa-solid fa-cog"></Button>
</div>
</template>
</TableView>
2024-12-29 19:25:35 +01:00
</div>
2025-01-02 13:21:59 +01:00
<!-- TODO check user permissions -->
<div class="empty-placeholder" v-if="filteredApps.length === 0">
<!-- for admins -->
<div v-if="false">
<h4><i class="fa fa-cloud-download fa-fw"></i> {{ $t('apps.noApps.title') }}</h4>
<h5 v-html="$t('apps.noApps.description', { appStoreLink: '#/appstore' })"></h5>
</div>
<!-- for non-admins -->
<div v-if="true">
<h4>{{ $t('apps.noAccess.title') }}</h4>
<h5>{{ $t('apps.noAccess.description') }}</h5>
</div>
2024-12-29 19:25:35 +01:00
</div>
</div>
2024-12-29 00:36:48 +01:00
</div>
</template>
<script>
2025-01-02 19:04:07 +01:00
import { Button, ButtonGroup, Icon, ProgressBar, TableView, TextInput } from 'pankow';
2024-12-29 00:36:48 +01:00
2025-01-02 13:21:59 +01:00
import { APP_TYPES, HSTATES, ISTATES, RSTATES } from '../constants.js';
2024-12-29 00:36:48 +01:00
import AppsModel from '../models/AppsModel.js';
const API_ORIGIN = import.meta.env.VITE_API_ORIGIN ? import.meta.env.VITE_API_ORIGIN : window.location.origin;
const accessToken = localStorage.token;
const appsModel = AppsModel.create(API_ORIGIN, accessToken);
2025-01-02 13:21:59 +01:00
const VIEW_TYPE = {
LIST: 'list',
GRID: 'grid',
};
let refreshInterval;
2024-12-29 00:36:48 +01:00
export default {
name: 'AppsView',
components: {
Button,
2025-01-02 13:21:59 +01:00
ButtonGroup,
2024-12-29 00:36:48 +01:00
Icon,
2025-01-02 19:04:07 +01:00
ProgressBar,
2025-01-02 13:21:59 +01:00
TableView,
2024-12-29 19:19:03 +01:00
TextInput,
2024-12-29 00:36:48 +01:00
},
data() {
return {
API_ORIGIN,
2025-01-02 13:21:59 +01:00
APP_TYPES,
VIEW_TYPE,
2024-12-29 00:36:48 +01:00
ready: false,
2024-12-29 19:19:03 +01:00
filter: '',
2024-12-29 00:36:48 +01:00
apps: [],
2025-01-02 13:21:59 +01:00
viewType: (localStorage.appsView && (localStorage.appsView === VIEW_TYPE.GRID || localStorage.appsView === VIEW_TYPE.LIST)) ? localStorage.appsView : VIEW_TYPE.GRID,
listColumns: {
icon: {
width: '32px'
},
label: {
label: 'Label',
sort: true
},
domain: {
label: 'Location',
sort: true
},
appTitle: {
label: 'App Title',
sort: true
},
sso: {
label: 'Login',
sort: true
},
2025-01-02 19:04:07 +01:00
actions: {}
2025-01-02 13:21:59 +01:00
}
2024-12-29 00:36:48 +01:00
};
},
2024-12-29 19:19:03 +01:00
computed: {
filteredApps() {
return this.apps.filter(a => {
return a.fqdn.indexOf(this.filter) !== -1;
});
}
},
2024-12-29 00:36:48 +01:00
methods: {
2025-01-02 19:04:07 +01:00
installationStateLabel: AppsModel.installationStateLabel,
installationActive: AppsModel.installationActive,
appProgressMessage: AppsModel.appProgressMessage,
openAppInfo(app) {
window.location.href = `#/app/${app.id}/info`;
},
2025-01-02 13:21:59 +01:00
onOpenApp(app, event) {
function stopEvent() {
event.stopPropagation();
2025-01-02 19:04:07 +01:00
event.preventDefault();
2025-01-02 13:21:59 +01:00
}
if (app.installationState !== ISTATES.INSTALLED) {
if (app.installationState === ISTATES.ERROR && this.isOperator(app)) window.location.href = `#/app/${app.id}/repair`;
return stopEvent();
}
// app.health can also be null to indicate insufficient data
if (!app.health) return stopEvent();
if (app.runState === RSTATES.STOPPED) return stopEvent();
if (app.health === HSTATES.UNHEALTHY || app.health === HSTATES.ERROR || app.health === HSTATES.DEAD) {
if (this.isOperator(app)) window.location.href = `#/app/${app.id}/repair`;
return stopEvent();
}
// if (app.pendingPostInstallConfirmation && $scope.appPostInstallConfirm) {
// $scope.appPostInstallConfirm.show(app);
// return stopEvent();
// }
},
isOperator(app) {
return app.accessLevel === 'operator' || app.accessLevel === 'admin';
},
async refreshApps() {
this.apps = await appsModel.list();
},
toggleView() {
this.viewType = this.viewType === VIEW_TYPE.LIST ? VIEW_TYPE.GRID : VIEW_TYPE.LIST;
localStorage.appsView = this.viewType;
},
2024-12-29 00:36:48 +01:00
},
async mounted() {
this.apps = await appsModel.list();
this.ready = true;
2025-01-02 13:21:59 +01:00
refreshInterval = setInterval(this.refreshApps, 5000);
},
async unmounted() {
clearInterval(refreshInterval);
2024-12-29 00:36:48 +01:00
}
};
</script>
<style scoped>
2025-01-02 13:21:59 +01:00
.grid-animation-move,
2024-12-29 19:19:03 +01:00
.grid-animation-enter-active,
.grid-animation-leave-active {
transition: all 0.2s ease;
}
2025-01-02 13:21:59 +01:00
.grid-animation-enter-from {
opacity: 0;
transform: translateY(30px);
}
2024-12-29 19:19:03 +01:00
.grid-animation-leave-to {
opacity: 0;
2025-01-02 13:21:59 +01:00
transform: translateY(-30px);
}
.list-icon {
width: 32px;
height: 32px;
}
2025-01-02 19:04:07 +01:00
.apps-progress {
position: relative;
width: 90%;
text-align: center;
border-radius: 10px;
color: var(--pankow-text-color);
}
.apps-progress-filled {
background-color: var(--pankow-color-primary);
position: absolute;
top: 0;
left: 0;
height: 100%;
border-radius: 10px;
z-index: 1;
}
.apps-progress-label {
position: relative;
z-index: 2;
opacity: 0.7;
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
2025-01-02 13:21:59 +01:00
.actions {
text-align: right;
visibility: hidden;
}
tr:hover .actions {
visibility: visible;
2024-12-29 19:19:03 +01:00
}
2024-12-29 00:36:48 +01:00
.grid {
display: flex;
height: 100%;
width: 100%;
transition: 300ms;
flex-wrap: wrap;
justify-content: start;
align-content: start;
}
2025-01-02 13:21:59 +01:00
.grid-item {
2024-12-29 00:36:48 +01:00
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 190px;
height: 180px;
margin: 10px;
overflow: hidden;
border-radius: 10px;
background-color: var(--card-background);
}
2025-01-02 13:21:59 +01:00
.grid-item:focus,
.grid-item:hover {
2024-12-29 00:36:48 +01:00
box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.1);
background-color: var(--pankow-color-background-hover);
text-decoration: none;
}
2025-01-02 13:21:59 +01:00
.grid-item img {
2024-12-29 00:36:48 +01:00
width: 80px;
height: 80px;
}
2025-01-02 19:04:07 +01:00
.grid-item-label {
2024-12-29 00:36:48 +01:00
font-size: 18px;
font-weight: 100;
2025-01-02 19:04:07 +01:00
margin: 5px 0 5px 0;
2024-12-29 00:36:48 +01:00
color: var(--pankow-text-color);
}
2025-01-02 19:04:07 +01:00
.grid-item:focus .grid-item-label,
.grid-item:hover .grid-item-label {
2024-12-29 00:36:48 +01:00
text-decoration: none;
color: var(--accent-color);;
}
.config {
position: absolute;
color: var(--pankow-text-color);
font-size: 18px;
cursor: pointer;
width: 50px;
height: 50px;
border-top-right-radius: 10px;
right: 0;
top: 0;
opacity: 0;
display: flex;
justify-content: center;
align-items: center;
}
.config:focus,
.config:hover {
text-decoration: none;
color: var(--accent-color);;
opacity: 1;
}
2025-01-02 13:21:59 +01:00
.grid-item:focus .config,
.grid-item:hover .config {
2024-12-29 00:36:48 +01:00
opacity: 1;
}
2024-12-29 19:25:35 +01:00
.empty-placeholder {
margin-top: 20px;
}
2024-12-29 00:36:48 +01:00
</style>