Skip to content

Commit

Permalink
Optimize activeProducts to improve nav performance
Browse files Browse the repository at this point in the history
  • Loading branch information
nwmac committed Mar 5, 2024
1 parent 2bd19c0 commit c4ba13a
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 55 deletions.
4 changes: 3 additions & 1 deletion shell/components/ResourceList/ResourceLoadingIndicator.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ export default {
// Total count of all of the resources for all of the resources being loaded
count() {
return this.resources.reduce((acc, r) => {
return acc + (this.$store.getters[`${ this.inStore }/all`](r) || []).length;
const add = !r ? 0 : (this.$store.getters[`${ this.inStore }/all`](r) || []).length;
return acc + add;
}, 0);
},
Expand Down
25 changes: 19 additions & 6 deletions shell/components/SideNav.vue
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,11 @@ export default {
return this.rootProduct.name === HARVESTER;
},
// Nav links are only available for explorer (via the cluster store)
allNavLinks() {
if ( !this.clusterId || !this.$store.getters['cluster/schemaFor'](UI.NAV_LINK) ) {
const isExplorer = this.rootProduct === 'explorer';
if (!isExplorer || !this.clusterId || !this.$store.getters['cluster/schemaFor'](UI.NAV_LINK, false, false)) {
return [];
}
Expand Down Expand Up @@ -209,19 +212,29 @@ export default {
}
const currentProduct = this.$store.getters['productId'];
const rootProduct = this.$store.getters['rootProduct']?.name;
// Always show cluster-level types, regardless of the namespace filter
const namespaceMode = 'both';
const out = [];
const loadProducts = this.isExplorer ? [EXPLORER] : [];
const productMap = this.activeProducts.reduce((acc, p) => {
// Filter the set of products that have this product as the root product
// then only check that those are active, rather than the other way around
// this is slightly more performant, as we don't check the activeness of all products
const cache = {};
const allProducts = this.$store.getters['allProducts'];
const productMap = allProducts.reduce((acc, p) => {
return { ...acc, [p.name]: p };
}, {});
}, {});
if ( this.isExplorer ) {
for ( const product of this.activeProducts ) {
if ( product.inStore === 'cluster' ) {
for (const product of allProducts) {
if (product.rootProduct === rootProduct) {
// Check child product is active
const isActive = this.$store.getters['type-map/isProductActive'](product, cache);
if (isActive) {
addObject(loadProducts, product.name);
}
}
Expand Down
2 changes: 1 addition & 1 deletion shell/components/nav/Header.vue
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export default {
// Don't show if the header is in 'simple' mode
const notSimple = !this.simple;
// One of these must be enabled, otherwise t here's no component to show
const validFilterSettings = this.currentProduct.showNamespaceFilter || this.currentProduct.showWorkspaceSwitcher;
const validFilterSettings = this.currentProduct?.showNamespaceFilter || this.currentProduct?.showWorkspaceSwitcher;
return validClusterOrProduct && notSimple && validFilterSettings;
},
Expand Down
4 changes: 3 additions & 1 deletion shell/components/nav/TopLevelMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,9 @@ export default {
const cluster = this.clusterId || this.$store.getters['defaultClusterId'];
// TODO plugin routes
const entries = this.$store.getters['type-map/activeProducts']?.map((p) => {
// Note: Only need to look at root (top-level) products for the app bar
// This avoids having to re-calculate the app bar when a child product changes (e.g. within explorer)
const entries = this.$store.getters['type-map/activeRootProducts']?.map((p) => {
// Try product-specific index first
const to = p.to || {
name: `c-cluster-${ p.name }`,
Expand Down
4 changes: 4 additions & 0 deletions shell/plugins/dashboard-store/getters.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,10 @@ export default {
return out;
},

schemasLoaded: (state) => {
return !!state.types[SCHEMA];
},

defaultFor: (state, getters) => (type, rootSchema, schemaDefinitions = null) => {
let resourceFields;

Expand Down
38 changes: 27 additions & 11 deletions shell/store/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -292,20 +292,34 @@ export const getters = {
return getters['management/byId'](MANAGEMENT.CLUSTER, state.clusterId);
},

// Current product checks that the current product is active, if it is not, if
// falls back to the explorer product and then the first active product.
currentProduct(state, getters) {
const active = getters['type-map/activeProducts'];
let product = state.productId || EXPLORER;
let cache = {};

let out = findBy(active, 'name', state.productId);
// Rather than determining all active products, just check the current product (more performant)
let res = getters['type-map/isProductActive'](product, cache);

if ( !out ) {
out = findBy(active, 'name', EXPLORER);
}
if (!res) {
// Not active
product = EXPLORER;

res = getters['type-map/isProductActive'](product, cache);

if (!res) {
const allActive = getters['type-map/activeProducts'];

if ( !out ) {
out = active[0];
return allActive[0];
}
}

return out;
// Lookup the product
return state['type-map']?.products.find((p) => p.name === product);
},

allProducts(state) {
return state['type-map']?.products || [];
},

// Get the root product - this is either the current product or the current product's root (if set)
Expand Down Expand Up @@ -656,6 +670,7 @@ export const mutations = {
state.clusterId = neu;
},

// Note: This is only used by the authenticated middleware to change the current product
setProduct(state, value) {
state.productId = value;

Expand Down Expand Up @@ -764,7 +779,6 @@ export const actions = {

res = await allHash(promises);
dispatch('i18n/init');
const isMultiCluster = getters['isMultiCluster'];

// If the local cluster is a Harvester cluster and 'rancher-manager-support' is true, it means that the embedded Rancher is being used.
const localCluster = res.clusters?.find((c) => c.id === 'local');
Expand Down Expand Up @@ -808,6 +822,9 @@ export const actions = {
});
}

// Should have loaded now, so can check isMultiCluster
const isMultiCluster = getters['isMultiCluster'];

console.log(`Done loading management; isRancher=${ isRancher }; isMultiCluster=${ isMultiCluster }`); // eslint-disable-line no-console
},

Expand All @@ -823,7 +840,6 @@ export const actions = {
const sameCluster = state.clusterId && state.clusterId === id;
const samePackage = oldPkg?.name === newPkg?.name;
const sameProduct = oldProduct === product;
const isMultiCluster = getters['isMultiCluster'];

const productConfig = state['type-map']?.products?.find((p) => p.name === product);
const oldProductConfig = state['type-map']?.products?.find((p) => p.name === oldProduct);
Expand Down Expand Up @@ -892,7 +908,7 @@ export const actions = {
return;
}

console.log(`Loading ${ isMultiCluster ? 'ECM ' : '' }cluster...`); // eslint-disable-line no-console
console.log(`Loading cluster...`); // eslint-disable-line no-console

// If we've entered a new store ensure everything has loaded correctly
if (newPkgClusterStore) {
Expand Down
110 changes: 75 additions & 35 deletions shell/store/type-map.js
Original file line number Diff line number Diff line change
Expand Up @@ -1359,44 +1359,65 @@ export const getters = {
};
},

activeProducts(state, getters, rootState, rootGetters) {
const knownTypes = {};
const knownGroups = {};
const isDev = rootGetters['prefs/get'](VIEW_IN_API);
/**
* Returns a getter for testing if the specific product is active
*
* A cache object can be passed in, so if you need to call this multiple times, the cache
* can be re-used to avoid having to enumerate schemas to calculate knownTypes and groups
* if required
*/
isProductActive(state, getters, rootState, rootGetters) {
// Return a named function so that we can trace it in the browser performance log
return function isProductActive(productOrId, previousCache) {
// Use the supplied cache object if there is one, otherwise a new local object
let cache = previousCache || {};

if (!cache.knownTypes) {
cache.knownTypes = {};
}

if (!cache.knownGroups) {
cache.knownGroups = {};
}

if ( state.schemaGeneration < 0 ) {
// This does nothing, but makes activeProducts depend on schemaGeneration
// so that it can be used to update the product list on schema change.
return;
}
// Get the product specified (can be product or a product id)
const p = (typeof productOrId === 'string') ? state.products.find((p) => p.name === productOrId) : productOrId;

// Product not known
if (!p) {
return false;
}

return state.products.filter((p) => {
const module = p.inStore;

if ( p['public'] === false && !isDev ) {
if ( !p.public && !isDev ) {
return false;
}

if ( p.ifGetter && !rootGetters[p.ifGetter] ) {
if (p.ifGetter && !rootGetters[p.ifGetter]) {
return false;
}

if ( !knownTypes[module] ) {
// Don't need to build the cache if we don't need it (check ifHaveType)
if ((p.ifHaveType || p.ifHaveGroup) && !cache.knownTypes[module]) {
const schemas = rootGetters[`${ module }/all`](SCHEMA);
const seenGroups = {};

knownTypes[module] = [];
knownGroups[module] = [];
cache.knownTypes[module] = [];
cache.knownGroups[module] = [];

for ( const s of schemas ) {
knownTypes[module].push(s._id);
for (const s of schemas) {
cache.knownTypes[module].push(s._id);

if ( s._group ) {
addObject(knownGroups[module], s._group);
// Can't gate this on ifHaveGroup because we may re-use the cache
if (s._group && !seenGroups[s._group]) {
cache.knownGroups[module].push(s._group);
seenGroups[s._group] = true;
}
}
}

if ( p.ifFeature) {
if (p.ifFeature) {
const features = Array.isArray(p.ifFeature) ? p.ifFeature : [p.ifFeature];

for (const f of features) {
Expand All @@ -1406,38 +1427,53 @@ export const getters = {
}
}

if ( p.ifHave && !ifHave(rootGetters, p.ifHave)) {
return false;
if (p.ifHave && !ifHave(rootGetters, p.ifHave)) {
return false;
}

if ( p.ifHaveType ) {
const haveIds = knownTypes[module].filter((t) => t.match(stringToRegex(p.ifHaveType)) );
if (p.ifHaveType) {
const haveIds = cache.knownTypes[module].filter((t) => t.match(stringToRegex(p.ifHaveType)));

if ( !haveIds.length ) {
if (!haveIds.length) {
return false;
}

if ( p.ifHaveVerb && !ifHaveVerb(rootGetters, module, p.ifHaveVerb, haveIds)) {
if (p.ifHaveVerb && !ifHaveVerb(rootGetters, module, p.ifHaveVerb, haveIds)) {
return false;
}
}

if ( p.ifHaveGroup && !knownGroups[module].find((t) => t.match(stringToRegex(p.ifHaveGroup)) ) ) {
if (p.ifHaveGroup && !cache.knownGroups[module].find((t) => t.match(stringToRegex(p.ifHaveGroup)))) {
return false;
}

// Product is active
return true;
});
};
},

isProductActive(state, getters) {
return (name) => {
if ( findBy(getters['activeProducts'], 'name', name) ) {
return true;
}
activeProducts(state, getters, rootState, rootGetters) {
if (state.schemaGeneration < 0) {
// This does nothing, but makes activeProducts depend on schemaGeneration
// so that it can be used to update the product list on schema change.
return;
}

return false;
};
let cache = {};

return state.products.filter((p) => getters['isProductActive'](p, cache));
},

activeRootProducts(state, getters, rootState, rootGetters) {
if (state.schemaGeneration < 0) {
// This does nothing, but makes activeProducts depend on schemaGeneration
// so that it can be used to update the product list on schema change.
return;
}

let cache = {};

return state.products.filter((p) => !p.rootProduct || p.rootProduct === p.name).filter((p) => getters['isProductActive'](p, cache));
},

rowValueGetter(state) {
Expand Down Expand Up @@ -1917,6 +1953,10 @@ function ifHave(getters, option) {

// Could list a larger set of resources that typically only an admin user would have
export function isAdminUser(getters) {
if (!getters['management/schemasLoaded']) {
return false;
}

const canEditSettings = (getters['management/schemaFor'](MANAGEMENT.SETTING)?.resourceMethods || []).includes('PUT');
const canEditFeatureFlags = (getters['management/schemaFor'](MANAGEMENT.FEATURE)?.resourceMethods || []).includes('PUT');
const canInstallApps = (getters['management/schemaFor'](CATALOG.APP)?.resourceMethods || []).includes('PUT');
Expand Down

0 comments on commit c4ba13a

Please sign in to comment.