web/satellite: update Vue error page design
A common error page component that reflects our new designs has been added, replacing our old 404 and 50X Vue pages. Change-Id: Ifc9071674eeda03c5d961c26dce9ff0c80b95c6e
This commit is contained in:
parent
f076238748
commit
49c5e3de9e
@ -4,6 +4,7 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<BrandedLoader v-if="isLoading" />
|
||||
<ErrorPage v-else-if="isErrorPageShown" />
|
||||
<router-view v-else />
|
||||
<!-- Area for displaying notification -->
|
||||
<NotificationArea />
|
||||
@ -11,12 +12,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
|
||||
import { PartneredSatellite } from '@/types/common';
|
||||
import { MetaUtils } from '@/utils/meta';
|
||||
import { useNotify } from '@/utils/hooks';
|
||||
import { useAppStore } from '@/store/modules/appStore';
|
||||
import ErrorPage from '@/views/ErrorPage.vue';
|
||||
|
||||
import BrandedLoader from '@/components/common/BrandedLoader.vue';
|
||||
import NotificationArea from '@/components/notifications/NotificationArea.vue';
|
||||
@ -26,6 +28,13 @@ const notify = useNotify();
|
||||
|
||||
const isLoading = ref<boolean>(true);
|
||||
|
||||
/**
|
||||
* Indicates whether an error page should be shown in place of the router view.
|
||||
*/
|
||||
const isErrorPageShown = computed((): boolean => {
|
||||
return appStore.state.viewsState.error.visible;
|
||||
});
|
||||
|
||||
/**
|
||||
* Fixes the issue where view port height is taller than the visible viewport on
|
||||
* mobile Safari/Webkit. See: https://bugs.webkit.org/show_bug.cgi?id=141832
|
||||
|
@ -1,58 +0,0 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template src="./page40X.html" />
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
import LogoIcon from '@/../static/images/logo.svg';
|
||||
import MainIcon from '@/../static/images/errors/404.svg';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Page404',
|
||||
components: {
|
||||
LogoIcon,
|
||||
MainIcon,
|
||||
},
|
||||
setup() {},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.error-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
-webkit-touch-callout: none;
|
||||
user-select: none;
|
||||
|
||||
&__title {
|
||||
font-family: 'font_regular', sans-serif;
|
||||
margin-bottom: 60px;
|
||||
font-size: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
top: 87px;
|
||||
left: 100px;
|
||||
width: 207px;
|
||||
height: 37px;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-left: 15px;
|
||||
}
|
||||
</style>
|
@ -1,63 +0,0 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template src="./page50X.html" />
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
import LogoIcon from '@/../static/images/logo.svg';
|
||||
import MainIcon from '@/../static/images/errors/50X.svg';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Page50X',
|
||||
components: {
|
||||
LogoIcon,
|
||||
MainIcon,
|
||||
},
|
||||
setup() {},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.error-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
-webkit-touch-callout: none;
|
||||
user-select: none;
|
||||
|
||||
&__title {
|
||||
font-family: 'font_regular', sans-serif;
|
||||
margin-bottom: 60px;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
img,
|
||||
a {
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
top: 87px;
|
||||
left: 100px;
|
||||
width: 207px;
|
||||
height: 37px;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-left: 15px;
|
||||
}
|
||||
</style>
|
@ -1,10 +0,0 @@
|
||||
<!--Copyright (C) 2019 Storj Labs, Inc.-->
|
||||
<!--See LICENSE for copying information.-->
|
||||
|
||||
<div class="error-container">
|
||||
<a href="/" class="logo">
|
||||
<LogoIcon class="header-container__left-area__logo__img"/>
|
||||
</a>
|
||||
<h1 class="error-container__title">404. Something Went Wrong</h1>
|
||||
<MainIcon/>
|
||||
</div>
|
@ -1,10 +0,0 @@
|
||||
<!--Copyright (C) 2019 Storj Labs, Inc.-->
|
||||
<!--See LICENSE for copying information.-->
|
||||
|
||||
<div class="error-container">
|
||||
<a href="/" class="logo">
|
||||
<LogoIcon class="header-container__left-area__logo__img"/>
|
||||
</a>
|
||||
<h1 class="error-container__title">503 Unknown Error</h1>
|
||||
<MainIcon/>
|
||||
</div>
|
@ -417,7 +417,7 @@ onMounted(async (): Promise<void> => {
|
||||
appStore.updateActiveModal(MODALS.createProjectPassphrase);
|
||||
}
|
||||
|
||||
appStore.toggleHasJustLoggenIn();
|
||||
appStore.toggleHasJustLoggedIn();
|
||||
}
|
||||
|
||||
await store.dispatch(PROJECTS_ACTIONS.FETCH_DAILY_DATA, { since: past, before: now });
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
import Vue from 'vue';
|
||||
import VueClipboard from 'vue-clipboard2';
|
||||
import { createPinia, PiniaVuePlugin } from 'pinia';
|
||||
import { createPinia, setActivePinia, PiniaVuePlugin } from 'pinia';
|
||||
|
||||
import App from './App.vue';
|
||||
import { router } from './router';
|
||||
@ -24,6 +24,7 @@ Vue.use(new NotificatorPlugin(store));
|
||||
Vue.use(VueClipboard);
|
||||
Vue.use(PiniaVuePlugin);
|
||||
const pinia = createPinia();
|
||||
setActivePinia(pinia);
|
||||
|
||||
/**
|
||||
* Click outside handlers.
|
||||
|
@ -17,7 +17,6 @@ import BillingOverview from '@/components/account/billing/billingTabs/Overview.v
|
||||
import BillingPaymentMethods from '@/components/account/billing/billingTabs/PaymentMethods.vue';
|
||||
import BillingHistory from '@/components/account/billing/billingTabs/BillingHistory.vue';
|
||||
import BillingCoupons from '@/components/account/billing/billingTabs/Coupons.vue';
|
||||
import Page404 from '@/components/errors/Page404.vue';
|
||||
import BucketsView from '@/components/objects/BucketsView.vue';
|
||||
import ObjectsArea from '@/components/objects/ObjectsArea.vue';
|
||||
import UploadFile from '@/components/objects/UploadFile.vue';
|
||||
@ -436,10 +435,5 @@ export const router = new Router({
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
name: '404',
|
||||
component: Page404,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -40,20 +40,11 @@ export const store = new Vuex.Store<ModulesState>({
|
||||
},
|
||||
});
|
||||
|
||||
store.subscribe((mutation, state) => {
|
||||
const currentRouteName = router.currentRoute.name;
|
||||
const appStore = useAppStore();
|
||||
const satelliteName = appStore.state.satelliteName;
|
||||
|
||||
store.subscribe((mutation) => {
|
||||
switch (mutation.type) {
|
||||
case PROJECTS_MUTATIONS.REMOVE:
|
||||
document.title = `${router.currentRoute.name} | ${satelliteName}`;
|
||||
|
||||
break;
|
||||
case PROJECTS_MUTATIONS.SELECT_PROJECT:
|
||||
if (currentRouteName && !notProjectRelatedRoutes.includes(currentRouteName)) {
|
||||
document.title = `${state.projectsModule.selectedProject.name} | ${currentRouteName} | ${satelliteName}`;
|
||||
}
|
||||
updateTitle();
|
||||
}
|
||||
});
|
||||
|
||||
@ -65,14 +56,21 @@ export default store;
|
||||
relies on store state for the routing behavior.
|
||||
*/
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const appStore = useAppStore();
|
||||
|
||||
if (!to.matched.length) {
|
||||
appStore.setErrorPage(404);
|
||||
return;
|
||||
} else if (appStore.state.viewsState.error.visible) {
|
||||
appStore.removeErrorPage();
|
||||
}
|
||||
|
||||
if (to.name === RouteConfig.ProjectDashboard.name && from.name === RouteConfig.Login.name) {
|
||||
const appStore = useAppStore();
|
||||
appStore.toggleHasJustLoggenIn(true);
|
||||
appStore.toggleHasJustLoggedIn(true);
|
||||
}
|
||||
|
||||
if (to.name === RouteConfig.AllProjectsDashboard.name && from.name === RouteConfig.Login.name) {
|
||||
const appStore = useAppStore();
|
||||
appStore.toggleHasJustLoggenIn(true);
|
||||
appStore.toggleHasJustLoggedIn(true);
|
||||
}
|
||||
|
||||
// On very first login we try to redirect user to project dashboard
|
||||
@ -80,12 +78,10 @@ router.beforeEach(async (to, from, next) => {
|
||||
// That's why we toggle this flag here back to false not show create project passphrase modal again
|
||||
// if user clicks 'Continue in web'.
|
||||
if (to.name === RouteConfig.ProjectDashboard.name && from.name === RouteConfig.OverviewStep.name) {
|
||||
const appStore = useAppStore();
|
||||
appStore.toggleHasJustLoggenIn(false);
|
||||
appStore.toggleHasJustLoggedIn(false);
|
||||
}
|
||||
if (to.name === RouteConfig.ProjectDashboard.name && from.name === RouteConfig.AllProjectsDashboard.name) {
|
||||
const appStore = useAppStore();
|
||||
appStore.toggleHasJustLoggenIn(false);
|
||||
appStore.toggleHasJustLoggedIn(false);
|
||||
}
|
||||
|
||||
// TODO: I disabled this navigation guard because we try to get active pinia before it is initialised.
|
||||
@ -132,23 +128,8 @@ router.beforeEach(async (to, from, next) => {
|
||||
next();
|
||||
});
|
||||
|
||||
router.afterEach(({ name }, _from) => {
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
if (notProjectRelatedRoutes.includes(name)) {
|
||||
document.title = `${router.currentRoute.name} | ${appStore.state.satelliteName}`;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedProjectName = store.state.projectsModule.selectedProject.name ?
|
||||
`${store.state.projectsModule.selectedProject.name} | ` : '';
|
||||
|
||||
document.title = `${selectedProjectName + router.currentRoute.name} | ${appStore.state.satelliteName}`;
|
||||
router.afterEach(() => {
|
||||
updateTitle();
|
||||
});
|
||||
|
||||
/**
|
||||
@ -161,3 +142,18 @@ function navigateToDefaultSubTab(routes: RouteRecord[], tabRoute: NavigationLink
|
||||
return (routes.length === 2 && (routes[1].name as string) === tabRoute.name) ||
|
||||
(routes.length === 3 && (routes[2].name as string) === tabRoute.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the title of the webpage.
|
||||
*/
|
||||
function updateTitle(): void {
|
||||
const appStore = useAppStore();
|
||||
const routeName = router.currentRoute.name;
|
||||
const parts = [routeName, appStore.state.satelliteName];
|
||||
|
||||
if (routeName && !notProjectRelatedRoutes.includes(routeName)) {
|
||||
parts.unshift(store.state.projectsModule.selectedProject.name);
|
||||
}
|
||||
|
||||
document.title = parts.filter(s => !!s).join(' | ');
|
||||
}
|
||||
|
@ -31,6 +31,15 @@ class ViewsState {
|
||||
// this field is mainly used on the all projects dashboard as an exit condition
|
||||
// for when the dashboard opens the pricing plan and the pricing plan navigates back repeatedly.
|
||||
public hasShownPricingPlan = false;
|
||||
public error: ErrorPageState = new ErrorPageState();
|
||||
}
|
||||
|
||||
class ErrorPageState {
|
||||
constructor(
|
||||
public statusCode = 0,
|
||||
public fatal = false,
|
||||
public visible = false,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class State {
|
||||
@ -155,6 +164,14 @@ export const useAppStore = defineStore('app', () => {
|
||||
state.viewsState.activeDropdown = '';
|
||||
}
|
||||
|
||||
function setErrorPage(statusCode: number, fatal = false): void {
|
||||
state.viewsState.error = new ErrorPageState(statusCode, fatal, true);
|
||||
}
|
||||
|
||||
function removeErrorPage(): void {
|
||||
state.viewsState.error.visible = false;
|
||||
}
|
||||
|
||||
function clear(): void {
|
||||
state.viewsState.activeModal = null;
|
||||
state.viewsState.isSuccessfulPasswordResetShown = false;
|
||||
@ -170,6 +187,7 @@ export const useAppStore = defineStore('app', () => {
|
||||
state.viewsState.selectedPricingPlan = null;
|
||||
state.viewsState.hasShownPricingPlan = false;
|
||||
state.viewsState.activeDropdown = '';
|
||||
state.viewsState.error.visible = false;
|
||||
}
|
||||
|
||||
return {
|
||||
@ -177,8 +195,9 @@ export const useAppStore = defineStore('app', () => {
|
||||
getConfig,
|
||||
toggleActiveDropdown,
|
||||
toggleSuccessfulPasswordReset,
|
||||
updateActiveModal,
|
||||
removeActiveModal,
|
||||
toggleHasJustLoggenIn,
|
||||
toggleHasJustLoggedIn: toggleHasJustLoggenIn,
|
||||
changeState,
|
||||
setSatelliteName,
|
||||
setPartneredSatellites,
|
||||
@ -194,7 +213,8 @@ export const useAppStore = defineStore('app', () => {
|
||||
setManagePassphraseStep,
|
||||
setHasShownPricingPlan,
|
||||
closeDropdowns,
|
||||
updateActiveModal,
|
||||
setErrorPage,
|
||||
removeErrorPage,
|
||||
clear,
|
||||
};
|
||||
});
|
||||
|
162
web/satellite/src/views/ErrorPage.vue
Normal file
162
web/satellite/src/views/ErrorPage.vue
Normal file
@ -0,0 +1,162 @@
|
||||
// Copyright (C) 2023 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<div class="error-area">
|
||||
<div class="error-area__logo-wrapper">
|
||||
<Logo class="error-area__logo-wrapper__logo" @click="goToHomepage" />
|
||||
</div>
|
||||
<div class="error-area__container">
|
||||
<h1 class="error-area__container__title">{{ statusCode }}</h1>
|
||||
<h2 class="error-area__container__subtitle">{{ message }}</h2>
|
||||
<VButton
|
||||
class="error-area__container__button"
|
||||
:label="'Go ' + (isFatal ? 'to homepage' : 'back')"
|
||||
width="auto"
|
||||
height="auto"
|
||||
border-radius="26px"
|
||||
:on-press="onButtonClick"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue';
|
||||
|
||||
import { useRouter } from '@/utils/hooks';
|
||||
import { useAppStore } from '@/store/modules/appStore';
|
||||
import { MetaUtils } from '@/utils/meta';
|
||||
|
||||
import VButton from '@/components/common/VButton.vue';
|
||||
|
||||
import Logo from '@/../static/images/logo.svg';
|
||||
|
||||
const router = useRouter();
|
||||
const appStore = useAppStore();
|
||||
|
||||
const messages = new Map<number, string>([
|
||||
[404, 'Oops, page not found.'],
|
||||
[500, 'Internal server error.'],
|
||||
]);
|
||||
|
||||
/**
|
||||
* Retrieves the error's status code from the store.
|
||||
*/
|
||||
const statusCode = computed((): number => {
|
||||
return appStore.state.viewsState.error.statusCode;
|
||||
});
|
||||
|
||||
/**
|
||||
* Retrieves the message corresponding to the error's status code.
|
||||
*/
|
||||
const message = computed((): string => {
|
||||
return messages.get(statusCode.value) || 'An error occurred.';
|
||||
});
|
||||
|
||||
/**
|
||||
* Indicates whether the error is unrecoverable.
|
||||
*/
|
||||
const isFatal = computed((): boolean => {
|
||||
return appStore.state.viewsState.error.fatal;
|
||||
});
|
||||
|
||||
/**
|
||||
* Navigates to the homepage.
|
||||
*/
|
||||
function goToHomepage(): void {
|
||||
window.location.href = MetaUtils.getMetaContent('homepage-url');
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the homepage if fatal or the previous route otherwise.
|
||||
*/
|
||||
function onButtonClick(): void {
|
||||
if (isFatal.value) {
|
||||
goToHomepage();
|
||||
return;
|
||||
}
|
||||
router.back();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle hook after initial render. Sets page title.
|
||||
*/
|
||||
onMounted(() => {
|
||||
const satName = appStore.state.satelliteName;
|
||||
document.title = statusCode.value.toString() + (satName ? ' | ' + satName : '');
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.error-area {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 52px 24px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
background: url('~@/../static/images/errors/dotWorld.png') no-repeat center 178px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
overflow-y: auto;
|
||||
|
||||
&__logo-wrapper {
|
||||
height: 30.89px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
top: 52px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-bottom: 52px;
|
||||
|
||||
&__logo {
|
||||
height: 100%;
|
||||
width: auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&__container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-family: 'font_bold', sans-serif;
|
||||
text-align: center;
|
||||
|
||||
&__title {
|
||||
font-size: 90px;
|
||||
line-height: 90px;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
margin-top: 20px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
&__button {
|
||||
margin-top: 32px;
|
||||
padding: 16px 37.5px;
|
||||
|
||||
:deep(.label) {
|
||||
font-family: 'font_bold', sans-serif;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-height: 500px) {
|
||||
|
||||
.error-area {
|
||||
justify-content: flex-start;
|
||||
|
||||
&__logo-wrapper {
|
||||
position: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
BIN
web/satellite/static/images/errors/dotWorld.png
Normal file
BIN
web/satellite/static/images/errors/dotWorld.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 198 KiB |
Loading…
Reference in New Issue
Block a user