Skip to content
Snippets Groups Projects
Commit 23f6fa1e authored by Petar Hristov's avatar Petar Hristov :speech_balloon: Committed by Benedikt Heinrichs
Browse files

Update: Fixes, Refactoring and API Error Handling

parent f2b90c92
Branches
Tags
3 merge requests!54Chore: 1.7.0,!51Release: Sprint/2022 08 :robot:,!49Update: Fixes, Refactoring and API Error Handling
Showing
with 289 additions and 75 deletions
......@@ -6,7 +6,7 @@ module.exports = {
ignorePatterns: ["node_modules", "build", "coverage"],
plugins: ["eslint-comments", "functional"],
extends: [
"plugin:vue/essential",
"plugin:vue/recommended",
"eslint:recommended",
"@vue/typescript/recommended",
"@vue/prettier",
......
......@@ -9,6 +9,7 @@
"cSpell.words": [
"Coscine",
"pinia",
"RWTH",
"vite"
]
}
......@@ -14,6 +14,7 @@
</div>
</main>
<ExpiryToast />
<NotificationToast />
</div>
</template>
......@@ -34,10 +35,6 @@ export default defineComponent({
return { mainStore, projectStore, userStore };
},
created() {
this.initialize();
},
computed: {
loggedIn(): boolean {
return this.mainStore.loggedIn;
......@@ -49,6 +46,11 @@ export default defineComponent({
},
watch: {
// Listen for token changes and reload the page to fetch data again
"mainStore.coscine.authorization.bearer"() {
window.location.reload();
},
loggedIn() {
this.userStore.retrieveUser(this.loggedIn);
},
......@@ -58,6 +60,10 @@ export default defineComponent({
},
},
created() {
this.initialize();
},
methods: {
async initialize() {
await this.userStore.retrieveUser(this.loggedIn);
......
......@@ -4,19 +4,20 @@
declare module "vue" {
export interface GlobalComponents {
BreadCrumbs: typeof import("./components/BreadCrumbs.vue")["default"];
CoscineCard: typeof import("./components/CoscineCard.vue")["default"];
CoscineFormGroup: typeof import("./components/CoscineFormGroup.vue")["default"];
CoscineHeadline: typeof import("./components/CoscineHeadline.vue")["default"];
CoscineModal: typeof import("./components/CoscineModal.vue")["default"];
ExpiryToast: typeof import("./components/ExpiryToast.vue")["default"];
LoadingIndicator: typeof import("./components/LoadingIndicator.vue")["default"];
LoadingSpinner: typeof import("./components/LoadingSpinner.vue")["default"];
BreadCrumbs: typeof import("./components/elements/BreadCrumbs.vue")["default"];
CoscineCard: typeof import("./components/coscine/CoscineCard.vue")["default"];
CoscineFormGroup: typeof import("./components/coscine/CoscineFormGroup.vue")["default"];
CoscineHeadline: typeof import("./components/coscine/CoscineHeadline.vue")["default"];
CoscineModal: typeof import("./components/coscine/CoscineModal.vue")["default"];
ExpiryToast: typeof import("./components/toasts/ExpiryToast.vue")["default"];
LoadingIndicator: typeof import("./components/elements/LoadingIndicator.vue")["default"];
LoadingSpinner: typeof import("./components/coscine/LoadingSpinner.vue")["default"];
Maintenance: typeof import("./components/banner/Maintenance.vue")["default"];
MultiSelect: typeof import("./components/MultiSelect.vue")["default"];
Navbar: typeof import("./components/Navbar.vue")["default"];
MultiSelect: typeof import("./components/coscine/MultiSelect.vue")["default"];
Navbar: typeof import("./components/elements/Navbar.vue")["default"];
NotificationToast: typeof import("./components/toasts/NotificationToast.vue")["default"];
Pilot: typeof import("./components/banner/Pilot.vue")["default"];
SidebarMenu: typeof import("./components/SidebarMenu.vue")["default"];
SidebarMenu: typeof import("./components/elements/SidebarMenu.vue")["default"];
}
}
......
......@@ -2,12 +2,12 @@
<b-alert
v-if="visibility"
:show="show"
@dismissed="saveVisibility"
dismissible
variant="warning"
@dismissed="saveVisibility"
>
<i18n :path="messagePath" tag="p">
<template v-slot:link>
<template #link>
<a :href="maintenance.url" target="_blank">{{
$t("banner.maintenance.moreInformation")
}}</a>
......
......@@ -2,9 +2,9 @@
<b-alert
v-if="pilotVisibility"
:show="pilotVisibility"
@dismissed="saveVisibility"
dismissible
variant="warning"
@dismissed="saveVisibility"
>
<i18n path="banner.pilot.pilotBannerText" tag="p">
<template #link>
......
......@@ -2,25 +2,26 @@
<b-card
no-body
class="coscine_card m-2 text-center"
:class="isLoading ? 'bg-light' : ''"
@click.prevent="openCard()"
>
<!-- Stretched Link (Card) -->
<a :href="hrefFromRouter(to)" class="stretched-link" />
<a v-if="!isLoading" :href="hrefFromRouter(to)" class="stretched-link" />
<!-- Badge -->
<template #header>
<b-badge pill :variant="badge_type" v-if="badge_visibility">
{{ badge_text }}
<b-badge v-if="badgeVisibility" pill :variant="badgeType">
{{ badgeText }}
</b-badge>
</template>
<!-- Settings Button -->
<b-button
v-if="toSettings"
@click.stop.prevent="settingsCard()"
size="sm"
variant="outline-primary"
class="settings_button"
@click.stop.prevent="settingsCard"
>
<b-icon icon="pencil-fill" />
<!-- Stretched Link (Settings) -->
......@@ -29,8 +30,10 @@
<b-card-body class="pt-0 pb-2 px-2">
<div>
<!-- Loading Spinner -->
<b-spinner v-if="isLoading" variant="primary" class="card_icon" />
<!-- Icon -->
<b-icon :icon="cardIcon" variant="primary" class="card_icon" />
<b-icon v-else :icon="cardIcon" variant="primary" class="card_icon" />
</div>
<!-- Title -->
<b-card-text>
......@@ -47,35 +50,27 @@ import { RawLocation } from "vue-router";
export default defineComponent({
components: {},
data() {
return {
createIcon: "plus-circle-fill",
resourceIcon: "archive",
projectIcon: "folder2-open",
placeHolderIcon: "circle-fill",
};
},
props: {
title: {
type: String,
required: true,
},
badge_text: {
badgeText: {
default: "",
type: String,
},
badge_visibility: {
badgeVisibility: {
default: false,
type: Boolean,
},
badge_type: {
badgeType: {
default: "warning",
type: String,
},
type: {
default: "",
type: String,
required: true,
required: false,
},
toSettings: {
default: null,
......@@ -85,6 +80,19 @@ export default defineComponent({
type: Object as PropType<RawLocation>,
required: true,
},
isLoading: {
default: false,
type: Boolean,
},
},
data() {
return {
createIcon: "plus-circle-fill",
resourceIcon: "archive",
projectIcon: "folder2-open",
placeHolderIcon: "circle-fill",
};
},
computed: {
......
......@@ -18,9 +18,9 @@
<!-- Label -->
<span id="label" class="text-break">{{ label }}</span>
<div class="d-inline ml-1" v-if="info">
<div v-if="info" class="d-inline ml-1">
<!-- Information Circle Icon -->
<b-icon icon="info-circle" :id="labelFor" />
<b-icon :id="labelFor" icon="info-circle" />
<!-- Popover -->
<b-popover
......
<template>
<b-modal
:visible="value"
@change="$emit('input', value)"
:title="title"
@hidden="hideModal"
:hide-footer="true"
@change="$emit('input', value)"
@hidden="hideModal"
>
<div>
{{ body }}
</div>
<slot />
<div class="mt-4">
<slot name="buttons" />
</div>
</b-modal>
</template>
......
......@@ -6,9 +6,8 @@
:show="isWaitingForResponse"
:variant="'white'"
:opacity="0.9"
:z-index="2000"
:z-index="1000"
:blur="''"
rounded="sm"
>
<template #overlay>
<div class="text-center">
......
......@@ -11,14 +11,14 @@
no-outer-focus
class="mb-2"
>
<template v-slot="{ tags, disabled, addTag, removeTag }">
<template #default="{ tags, disabled, addTag, removeTag }">
<ul v-if="tags.length > 0" class="list-inline d-inline-block mb-2">
<li v-for="tag in tags" :key="tag" class="list-inline-item">
<b-form-tag
@remove="removeTag(tag)"
:title="tag"
:disabled="disabled"
variant="info"
@remove="removeTag(tag)"
>{{ tag }}</b-form-tag
>
</li>
......@@ -44,8 +44,8 @@
:disabled="disabled"
>
<b-form-input
v-model="search"
id="tag-search-input"
v-model="search"
type="search"
size="sm"
autocomplete="off"
......
<template>
<header id="header">
<b-navbar toggleable="md" type="dark" variant="dark">
<!-- Sidebar Toggle -->
<b-navbar-nav>
<b-nav-item @click="toggleSidebar">
<b-icon icon="justify" />
</b-nav-item>
</b-navbar-nav>
<!-- Coscine Logo -->
<b-navbar-brand id="coscineLogo">
<RouterLink :to="{ name: 'list-projects' }">
<img alt="Coscine logo" src="@/assets/svg/coscine_white.svg" />
<img
alt="Coscine Logo"
src="@/assets/svg/coscine_white.svg"
class="mx-3"
/>
</RouterLink>
</b-navbar-brand>
......@@ -28,8 +25,8 @@
</template>
<b-form-input
id="search"
type="search"
v-model="searchTerm"
type="search"
:placeholder="$t('nav.search')"
/>
</b-input-group>
......@@ -45,8 +42,8 @@
<!-- Language Options -->
<b-dropdown-item
v-for="(locale, index) in locales"
:disabled="$root.$i18n.locale === locale"
:key="index"
:disabled="$root.$i18n.locale === locale"
@click="changeLocale(locale)"
>{{ $t("nav.lang" + locale.toUpperCase()) }}</b-dropdown-item
>
......@@ -71,11 +68,11 @@
>
</b-nav-item-dropdown>
<b-nav-item-dropdown right v-if="loggedIn">
<template #button-content v-if="user"
<b-nav-item-dropdown v-if="loggedIn" right>
<template v-if="user" #button-content
>{{ $t("nav.user", { displayName: user.displayName }) }}
</template>
<template #button-content v-else
<template v-else #button-content
><b-skeleton
animation="fade"
height="1.5rem"
......@@ -101,9 +98,9 @@
<router-link
v-else
v-slot="{ href, isExactActive }"
:to="{ name: 'login', query: { redirect: $route.fullPath } }"
custom
v-slot="{ href, isExactActive }"
>
<b-nav-item right :disabled="isExactActive" :href="href">{{
$t("nav.userLogIn")
......
......@@ -2,18 +2,18 @@
<sidebar-menu
:menu="menu"
:collapsed="collapsed"
@toggle-collapse="collapse"
:relative="true"
:showOneChild="true"
:disableHover="true"
:show-one-child="true"
:disable-hover="true"
theme="white-theme"
@toggle-collapse="collapse"
>
<template v-slot:toggle-icon>
<template #toggle-icon>
<b-icon v-if="!collapsed" icon="arrow-bar-left" />
<b-icon v-else icon="arrow-bar-right" />
</template>
<template v-slot:dropdown-icon>
<template #dropdown-icon>
<b-icon icon="caret-right" />
</template>
</sidebar-menu>
......@@ -59,9 +59,6 @@ export default defineComponent({
return { mainStore, projectStore, resourceStore };
},
created() {
this.collapsed = !this.sidebarActive;
},
data() {
return {
collapsed: false,
......@@ -257,6 +254,9 @@ export default defineComponent({
this.collapsed = !this.sidebarActive;
},
},
created() {
this.collapsed = !this.sidebarActive;
},
methods: {
collapse(collapsed: boolean) {
this.collapsed = collapsed;
......
<template>
<div>
<!-- White Overlay Background -->
<b-overlay
no-wrap
:fixed="true"
:show="loginStore.expiredSession"
:variant="'white'"
:opacity="0.9"
:z-index="1000"
:blur="''"
>
<template #overlay>
<!-- Remove the default loading spinner -->
<div />
</template>
</b-overlay>
<!-- Expiry Toast -->
<b-toast
v-model="loginStore.expiredSession"
toaster="b-toaster-bottom-right"
......@@ -8,7 +26,10 @@
no-close-button
:title="$t('toast.session.title')"
>
<!-- Toast Body Text -->
<p class="text-center">{{ $t("toast.session.message") }}</p>
<!-- Login Button -->
<div class="d-flex justify-content-center">
<b-button
class="mt-2"
......@@ -20,6 +41,7 @@
</b-button>
</div>
</b-toast>
</div>
</template>
<script lang="ts">
......@@ -38,37 +60,45 @@ export default defineComponent({
return { mainStore, loginStore };
},
computed: {
token(): string {
return this.mainStore.coscine.authorization.bearer;
},
},
data() {
return {
tokenExpiredInterval: 0,
};
},
created() {
this.initialize();
computed: {
token(): string {
return this.mainStore.coscine.authorization.bearer;
},
},
watch: {
token() {
this.initialize();
},
},
created() {
this.initialize();
},
methods: {
getCurrentTokenExpirationDuration(): number {
try {
const jwt = jose.decodeJwt(this.token);
const now = moment.utc(moment.now());
if (jwt.exp) {
// Use UTC to avoid time conversion errors
const tokenExpiresAt = moment(jwt.exp * 1000).utc();
const untilTokenExpiration = moment.duration(tokenExpiresAt.diff(now));
const untilTokenExpiration = moment.duration(
tokenExpiresAt.diff(now)
);
// Return as milliseconds for setInterval() method
return untilTokenExpiration.asMilliseconds();
} else {
return 0;
}
} catch (error) {
// Provided token is not a valid JWT Token
return 0;
}
},
initialize() {
this.loginStore.expiredSession = false;
......@@ -90,12 +120,12 @@ export default defineComponent({
redirectToLogin() {
this.loginStore.redirectToLogin(this.$route, true);
},
setInterval(fn: () => void, delay: number) {
setInterval(fn: () => void, delay: number): number {
const maxDelay = Math.pow(2, 31) - 1;
if (delay > maxDelay) {
return setInterval(fn, maxDelay);
return window.setInterval(fn, maxDelay);
}
return setInterval(fn, delay);
return window.setInterval(fn, delay);
},
},
});
......
<template>
<div />
</template>
<script lang="ts">
import { defineComponent } from "vue-demi";
import useNotificationStore from "@/store/notification";
import type { NotificationToast } from "@/store/types";
export default defineComponent({
setup() {
const notificationStore = useNotificationStore();
return { notificationStore };
},
computed: {
currentToast(): NotificationToast | undefined {
return this.notificationStore.notificationQueue
? (this.notificationStore.notificationQueue.at(0) as NotificationToast)
: undefined;
},
},
watch: {
currentToast() {
const toast = this.currentToast;
if (toast) {
// Display the notification toast
this.makeToast(toast);
// Toast has been shown, remove it from the queue
this.notificationStore.deleteNotification(toast);
}
},
},
methods: {
async makeToast(toast: NotificationToast) {
if (!toast.toaster) {
toast.toaster = "b-toaster-bottom-right";
}
this.$root.$bvToast.toast(toast.body, toast);
},
},
});
</script>
<style></style>
......@@ -105,6 +105,16 @@ export default {
"Es ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut. Falls der Fehler weiterhin auftritt, wenden Sie sich bitte an @:(email.serviceDeskEmail).",
},
},
apiError: {
general: {
title: "Ein Fehler ist aufgetreten",
body: "Es ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut. Falls der Fehler weiterhin auftritt, wenden Sie sich bitte an @:(email.serviceDeskEmail).",
},
specific: {
title: "Ein Fehler ist aufgetreten",
body: "Es ist ein Fehler aufgetreten {error}. Bitte versuchen Sie es erneut. Falls der Fehler weiterhin auftritt, wenden Sie sich bitte an @:(email.serviceDeskEmail).",
},
},
},
breadcrumbs: {
......@@ -232,9 +242,9 @@ export default {
},
email: {
serviceDeskEmail: "servicedesk@rwth-aachen.de",
serviceDeskEmail: "servicedesk@itc.rwth-aachen.de",
serviceDeskMailTo:
"mailto:servicedesk@rwth-aachen.de?subject=CoScInE%20Pilot%20Program",
"mailto:servicedesk@itc.rwth-aachen.de?subject=CoScInE%20Pilot%20Program",
serviceDeskName: "Servicedesk",
},
} as VueI18n.LocaleMessageObject;
......@@ -89,7 +89,7 @@ export default {
failure: {
title: "Error on saving",
message:
"An error occured. Please try again. If the error persists, please contact @:(email.serviceDeskEmail).",
"An error occurred. Please try again. If the error persists, please contact @:(email.serviceDeskEmail).",
},
},
onDelete: {
......@@ -100,7 +100,17 @@ export default {
failure: {
title: "Error on deletion",
message:
"An error occured. Please try again. If the error persists, please contact @:(email.serviceDeskEmail).",
"An error occurred. Please try again. If the error persists, please contact @:(email.serviceDeskEmail).",
},
},
apiError: {
general: {
title: "An error occurred",
body: "An error occurred. Please try again. If the error persists, please contact @:(email.serviceDeskEmail).",
},
specific: {
title: "An error occurred",
body: "An error occurred {error}. Please try again. If the error persists, please contact @:(email.serviceDeskEmail).",
},
},
},
......@@ -229,9 +239,9 @@ export default {
},
email: {
serviceDeskEmail: "servicedesk@rwth-aachen.de",
serviceDeskEmail: "servicedesk@itc.rwth-aachen.de",
serviceDeskMailTo:
"mailto:servicedesk@rwth-aachen.de?subject=Coscine%20Pilot%20Program",
"mailto:servicedesk@itc.rwth-aachen.de?subject=Coscine%20Pilot%20Program",
serviceDeskName: "Servicedesk",
},
} as VueI18n.LocaleMessageObject;
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment