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:
Alexandre Alves 2023-07-13 15:01:14 +01:00 committed by GitHub
parent be22353ed9
commit 01ae80cd88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 130 additions and 19 deletions

View File

@ -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');
}
}

View File

@ -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');
});
});

View File

@ -51,6 +51,8 @@ dynamicPluginLoader.register({
return harvesterClustersLocation; return harvesterClustersLocation;
} }
} }
} else {
return harvesterClustersLocation;
} }
} }
} }

View File

@ -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

View File

@ -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"

View File

@ -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;
} }

View File

@ -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');
} }

View File

@ -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 = {