mirror of https://github.com/rancher/dashboard.git
working on making sure we show a 404 page with a proper error (#8927)
* working on making sure we show a 404 page with a proper error * code cleanup + add logic to capture 404s for resource instance details * add e2e tests * address PR comments + adjust e2e tests * cover 404 on cluster for dynamic plugins * address PR comments * catching bogus resources on authenticated middleware with redirect to 404 page * fix lint issue * address PR comments + fix issue with e2e tests * Fix l10n - Ensure error messages doesn't reference 'list' when not on a list page - The new way the feature works means going to a list with an unknown resource results in the generic message, but this is preferably over the above * fix e2e tests --------- Co-authored-by: Alexandre Alves <aalves@Alexandres-MacBook-Pro.local> Co-authored-by: Alexandre Alves <aalves@Alexandres-MBP.lan> Co-authored-by: Richard Cox <richard.cox@suse.com>
This commit is contained in:
parent
be22353ed9
commit
01ae80cd88
|
|
@ -0,0 +1,11 @@
|
||||||
|
import PagePo from '@/cypress/e2e/po/pages/page.po';
|
||||||
|
|
||||||
|
export default class NotFoundPagePo extends PagePo {
|
||||||
|
errorTitle() {
|
||||||
|
return this.self().get('.text-center h1');
|
||||||
|
}
|
||||||
|
|
||||||
|
errorMessage() {
|
||||||
|
return this.self().get('.text-center h2');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
import NotFoundPagePo from '@/cypress/e2e/po/pages/not-found-page.po';
|
||||||
|
|
||||||
|
describe('Not found page display', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.login();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Will show a 404 if we do not have a valid Product id on the route path', () => {
|
||||||
|
const notFound = new NotFoundPagePo('/c/local/bogus-product-id');
|
||||||
|
|
||||||
|
notFound.goTo();
|
||||||
|
notFound.waitForPage();
|
||||||
|
|
||||||
|
notFound.errorTitle().contains('Error');
|
||||||
|
notFound.errorMessage().contains('Product bogus-product-id not found');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Will show a 404 if we do not have a valid Resource type on the route path', () => {
|
||||||
|
const notFound = new NotFoundPagePo('/c/local/explorer/bogus-resource-type');
|
||||||
|
|
||||||
|
notFound.goTo();
|
||||||
|
notFound.waitForPage();
|
||||||
|
|
||||||
|
notFound.errorTitle().contains('Error');
|
||||||
|
notFound.errorMessage().contains('Resource type bogus-resource-type not found');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Will show a 404 if we do not have a valid Resource id on the route path', () => {
|
||||||
|
const notFound = new NotFoundPagePo('/c/local/explorer/node/bogus-resource-id');
|
||||||
|
|
||||||
|
notFound.goTo();
|
||||||
|
notFound.waitForPage();
|
||||||
|
|
||||||
|
notFound.errorTitle().contains('Error');
|
||||||
|
notFound.errorMessage().contains('Resource node with id bogus-resource-id not found, unable to display resource details');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Will show a 404 if we do not have a valid product + resource + resource id', () => {
|
||||||
|
const notFound = new NotFoundPagePo('/c/local/bogus-product-id/bogus-resource/bogus-resource-id');
|
||||||
|
|
||||||
|
notFound.goTo();
|
||||||
|
notFound.waitForPage();
|
||||||
|
|
||||||
|
notFound.errorTitle().contains('Error');
|
||||||
|
notFound.errorMessage().contains('Product bogus-product-id not found');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -51,6 +51,8 @@ dynamicPluginLoader.register({
|
||||||
return harvesterClustersLocation;
|
return harvesterClustersLocation;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return harvesterClustersLocation;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -197,6 +197,12 @@ nav:
|
||||||
accountAndKeys: Account & API Keys
|
accountAndKeys: Account & API Keys
|
||||||
logOut: Log Out
|
logOut: Log Out
|
||||||
failWhale:
|
failWhale:
|
||||||
|
authMiddleware: Auth Middleware
|
||||||
|
clusterNotFound: Cluster { clusterId } not found
|
||||||
|
productNotFound: Product { productNotFound } not found
|
||||||
|
resourceNotFound: Resource type { resource } not found
|
||||||
|
resourceListNotFound: Resource type { resource } not found, unable to display list
|
||||||
|
resourceIdNotFound: Resource { resource } with id { fqid } not found, unable to display resource details
|
||||||
reload: Reload
|
reload: Reload
|
||||||
separator: or
|
separator: or
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -193,6 +193,9 @@ export default {
|
||||||
opt: { watch: true }
|
opt: { watch: true }
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (e.status === 404) {
|
||||||
|
store.dispatch('loadingError', new Error(this.t('nav.failWhale.resourceIdNotFound', { resource, fqid }, true)));
|
||||||
|
}
|
||||||
liveModel = {};
|
liveModel = {};
|
||||||
notFound = fqid;
|
notFound = fqid;
|
||||||
}
|
}
|
||||||
|
|
@ -367,18 +370,7 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Loading v-if="$fetchState.pending" />
|
<Loading v-if="$fetchState.pending || notFound" />
|
||||||
<div v-else-if="notFound">
|
|
||||||
<IconMessage icon="icon-warning">
|
|
||||||
<template v-slot:message>
|
|
||||||
{{ t('generic.notFound') }}
|
|
||||||
<div>
|
|
||||||
<div>{{ t('generic.type') }}: {{ resource }}</div>
|
|
||||||
<div>{{ t('generic.id') }}: {{ notFound }}</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</IconMessage>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<Masthead
|
<Masthead
|
||||||
v-if="showMasthead"
|
v-if="showMasthead"
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ export default {
|
||||||
|
|
||||||
if ( !this.hasFetch ) {
|
if ( !this.hasFetch ) {
|
||||||
if ( !schema ) {
|
if ( !schema ) {
|
||||||
store.dispatch('loadingError', new Error(`Type ${ resource } not found, unable to display list`));
|
store.dispatch('loadingError', new Error(this.t('nav.failWhale.resourceListNotFound', { resource }, true)));
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import {
|
||||||
SETUP, TIMED_OUT, UPGRADED, _FLAGGED, _UNFLAG
|
SETUP, TIMED_OUT, UPGRADED, _FLAGGED, _UNFLAG
|
||||||
} from '@shell/config/query-params';
|
} from '@shell/config/query-params';
|
||||||
import { SETTING } from '@shell/config/settings';
|
import { SETTING } from '@shell/config/settings';
|
||||||
import { MANAGEMENT, NORMAN, DEFAULT_WORKSPACE } from '@shell/config/types';
|
import { MANAGEMENT, NORMAN, DEFAULT_WORKSPACE, WORKLOAD } from '@shell/config/types';
|
||||||
import { _ALL_IF_AUTHED } from '@shell/plugins/dashboard-store/actions';
|
import { _ALL_IF_AUTHED } from '@shell/plugins/dashboard-store/actions';
|
||||||
import { applyProducts } from '@shell/store/type-map';
|
import { applyProducts } from '@shell/store/type-map';
|
||||||
import { findBy } from '@shell/utils/array';
|
import { findBy } from '@shell/utils/array';
|
||||||
|
|
@ -26,6 +26,16 @@ const getPackageFromRoute = (route) => {
|
||||||
return arraySafe.find((m) => !!m.pkg)?.pkg;
|
return arraySafe.find((m) => !!m.pkg)?.pkg;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getResourceFromRoute = (to) => {
|
||||||
|
let resource = to.params?.resource;
|
||||||
|
|
||||||
|
if (!resource) {
|
||||||
|
resource = findMeta(to, 'resource');
|
||||||
|
}
|
||||||
|
|
||||||
|
return resource;
|
||||||
|
};
|
||||||
|
|
||||||
let beforeEachSetup = false;
|
let beforeEachSetup = false;
|
||||||
|
|
||||||
function findMeta(route, key) {
|
function findMeta(route, key) {
|
||||||
|
|
@ -71,9 +81,18 @@ export function getProductFromRoute(to) {
|
||||||
return product;
|
return product;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setProduct(store, to) {
|
function setProduct(store, to, redirect) {
|
||||||
let product = getProductFromRoute(to);
|
let product = getProductFromRoute(to);
|
||||||
|
|
||||||
|
// since all products are hardcoded as routes (ex: c-local-explorer), if we match the wildcard route it means that the product does not exist
|
||||||
|
if ((product && (!to.matched.length || (to.matched.length && to.matched[0].path === '/c/:cluster/:product'))) ||
|
||||||
|
// if the product grabbed from the route is not registered, then we don't have it!
|
||||||
|
(product && !store.getters['type-map/isProductRegistered'](product))) {
|
||||||
|
store.dispatch('loadingError', new Error(store.getters['i18n/t']('nav.failWhale.productNotFound', { productNotFound: product }, true)));
|
||||||
|
|
||||||
|
return () => redirect(302, '/fail-whale');
|
||||||
|
}
|
||||||
|
|
||||||
if ( !product ) {
|
if ( !product ) {
|
||||||
product = EXPLORER;
|
product = EXPLORER;
|
||||||
}
|
}
|
||||||
|
|
@ -92,6 +111,8 @@ function setProduct(store, to) {
|
||||||
// There might be management catalog items in it vs cluster.
|
// There might be management catalog items in it vs cluster.
|
||||||
store.commit('catalog/reset');
|
store.commit('catalog/reset');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function({
|
export default async function({
|
||||||
|
|
@ -288,12 +309,21 @@ export default async function({
|
||||||
|
|
||||||
store.app.router.beforeEach((to, from, next) => {
|
store.app.router.beforeEach((to, from, next) => {
|
||||||
// NOTE - This beforeEach runs AFTER this middleware. So anything in this middleware that requires it must set it manually
|
// NOTE - This beforeEach runs AFTER this middleware. So anything in this middleware that requires it must set it manually
|
||||||
setProduct(store, to);
|
const redirected = setProduct(store, to, redirect);
|
||||||
|
|
||||||
|
if (redirected) {
|
||||||
|
return redirected();
|
||||||
|
}
|
||||||
|
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Call it for the initial pageload
|
// Call it for the initial pageload
|
||||||
setProduct(store, route);
|
const redirected = setProduct(store, route, redirect);
|
||||||
|
|
||||||
|
if (redirected) {
|
||||||
|
return redirected();
|
||||||
|
}
|
||||||
|
|
||||||
if (process.client) {
|
if (process.client) {
|
||||||
store.app.router.afterEach((to, from) => {
|
store.app.router.afterEach((to, from) => {
|
||||||
|
|
@ -376,7 +406,12 @@ export default async function({
|
||||||
// When fleet moves to it's own package this should be moved to pkg onEnter/onLeave
|
// When fleet moves to it's own package this should be moved to pkg onEnter/onLeave
|
||||||
if ((oldProduct === FLEET_NAME || product === FLEET_NAME) && oldProduct !== product) {
|
if ((oldProduct === FLEET_NAME || product === FLEET_NAME) && oldProduct !== product) {
|
||||||
// See note above for store.app.router.beforeEach, need to setProduct manually, for the moment do this in a targeted way
|
// See note above for store.app.router.beforeEach, need to setProduct manually, for the moment do this in a targeted way
|
||||||
setProduct(store, route);
|
const redirected = setProduct(store, route, redirect);
|
||||||
|
|
||||||
|
if (redirected) {
|
||||||
|
return redirected();
|
||||||
|
}
|
||||||
|
|
||||||
store.commit('updateWorkspace', {
|
store.commit('updateWorkspace', {
|
||||||
value: store.getters['prefs/get'](WORKSPACE) || DEFAULT_WORKSPACE,
|
value: store.getters['prefs/get'](WORKSPACE) || DEFAULT_WORKSPACE,
|
||||||
getters: store.getters
|
getters: store.getters
|
||||||
|
|
@ -397,6 +432,16 @@ export default async function({
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const resource = getResourceFromRoute(route);
|
||||||
|
|
||||||
|
// if we have resource param, but can't get the schema, it means the resource doesn't exist!
|
||||||
|
// NOTE: workload doesn't have a schema, so let's just bypass this with the identifier
|
||||||
|
if (resource && resource !== WORKLOAD && !store.getters['management/schemaFor'](resource) && !store.getters['rancher/schemaFor'](resource)) {
|
||||||
|
store.dispatch('loadingError', new Error(store.getters['i18n/t']('nav.failWhale.resourceNotFound', { resource }, true)));
|
||||||
|
|
||||||
|
return redirect(302, '/fail-whale');
|
||||||
|
}
|
||||||
|
|
||||||
if (!clusterId) {
|
if (!clusterId) {
|
||||||
clusterId = store.getters['defaultClusterId']; // This needs the cluster list, so no parallel
|
clusterId = store.getters['defaultClusterId']; // This needs the cluster list, so no parallel
|
||||||
const isSingleProduct = store.getters['isSingleProduct'];
|
const isSingleProduct = store.getters['isSingleProduct'];
|
||||||
|
|
@ -422,7 +467,7 @@ export default async function({
|
||||||
return redirect(302, '/home');
|
return redirect(302, '/home');
|
||||||
} else {
|
} else {
|
||||||
// Sets error 500 if lost connection to API
|
// Sets error 500 if lost connection to API
|
||||||
store.commit('setError', { error: e, locationError: new Error('Auth Middleware') });
|
store.commit('setError', { error: e, locationError: new Error(store.getters['i18n/t']('nav.failWhale.authMiddleware')) });
|
||||||
|
|
||||||
return redirect(302, '/fail-whale');
|
return redirect(302, '/fail-whale');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1361,6 +1361,14 @@ export const getters = {
|
||||||
return _rowValueGetter(col);
|
return _rowValueGetter(col);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isProductRegistered(state) {
|
||||||
|
return (productName) => {
|
||||||
|
const prod = state.products.find((p) => p.name === productName);
|
||||||
|
|
||||||
|
return !!prod;
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue