Skip to content

Commit

Permalink
Automation: No matching clusters msg fix (rancher#9864)
Browse files Browse the repository at this point in the history
* Remove conditional chaining in template

* Prevent app to break if no setting available

* Prevent to break if no mgmt cluster

* Prevent sidebar to break if no kube cluster

* Prevent breaking if no product available

* Prevent sort utils to break if no value is provided

* Add markup comments

* Replace getter mapping

* Prevent error if missing label for the inspected cluster

* Add tests for sidebar
  • Loading branch information
cnotv authored Oct 11, 2023
1 parent 0076a24 commit 50aac73
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 16 deletions.
51 changes: 37 additions & 14 deletions shell/components/nav/TopLevelMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ export default {
computed: {
...mapGetters(['clusterId']),
...mapGetters(['clusterReady', 'isRancher', 'currentCluster', 'currentProduct', 'isRancherInHarvester']),
...mapGetters('type-map', ['activeProducts']),
...mapGetters({ features: 'features/get' }),
value: {
Expand All @@ -59,9 +58,16 @@ export default {
},
},
sideMenuStyle() {
return {
marginBottom: this.globalBannerSettings?.footerFont,
marginTop: this.globalBannerSettings?.headerFont
};
},
globalBannerSettings() {
const settings = this.$store.getters['management/all'](MANAGEMENT.SETTING);
const bannerSettings = settings.find((s) => s.id === SETTING.BANNERS);
const bannerSettings = settings?.find((s) => s.id === SETTING.BANNERS);
if (bannerSettings) {
const parsed = JSON.parse(bannerSettings.value);
Expand Down Expand Up @@ -104,7 +110,7 @@ export default {
kubeClusters = kubeClusters.filter((c) => !!available[c]);
}
return kubeClusters.map((x) => {
return kubeClusters?.map((x) => {
const pCluster = pClusters?.find((c) => c.mgmt?.id === x.id);
return {
Expand All @@ -120,14 +126,12 @@ export default {
pin: () => x.pin(),
unpin: () => x.unpin()
};
});
}) || [];
},
clustersFiltered() {
const search = (this.clusterFilter || '').toLowerCase();
const out = search ? this.clusters.filter((item) => item.label.toLowerCase().includes(search)) : this.clusters;
const out = search ? this.clusters.filter((item) => item.label?.toLowerCase().includes(search)) : this.clusters;
const sorted = sortBy(out, ['ready:desc', 'label']);
if (search) {
Expand Down Expand Up @@ -203,7 +207,7 @@ export default {
const cluster = this.clusterId || this.$store.getters['defaultClusterId'];
// TODO plugin routes
const entries = this.activeProducts.map((p) => {
const entries = this.$store.getters['type-map/activeProducts']?.map((p) => {
// Try product-specific index first
const to = p.to || {
name: `c-cluster-${ p.name }`,
Expand Down Expand Up @@ -314,22 +318,22 @@ export default {
</script>
<template>
<div>
<!-- Overlay -->
<div
v-if="shown"
class="side-menu-glass"
@click="hide()"
/>
<transition name="fade">
<!-- Side menu -->
<div
data-testid="side-menu"
class="side-menu"
:class="{'menu-open': shown, 'menu-close':!shown}"
:style="{'marginBottom':
globalBannerSettings?.footerFont,
'marginTop':
globalBannerSettings?.headerFont}"
:style="sideMenuStyle"
tabindex="-1"
>
<!-- Logo and name -->
<div class="title">
<div
data-testid="top-level-menu"
Expand All @@ -351,8 +355,11 @@ export default {
<BrandImage file-name="rancher-logo.svg" />
</div>
</div>
<!-- Menu body -->
<div class="body">
<div>
<!-- Home button -->
<nuxt-link
class="option cluster selector home"
:to="{ name: 'home' }"
Expand All @@ -371,6 +378,8 @@ export default {
{{ t('nav.home') }}
</div>
</nuxt-link>
<!-- Search bar -->
<div
v-if="showClusterSearch"
class="clusters-search"
Expand Down Expand Up @@ -400,6 +409,8 @@ export default {
</div>
</div>
</div>
<!-- Harvester extras -->
<template v-if="hciApps.length">
<div class="category" />
<div>
Expand All @@ -416,7 +427,6 @@ export default {
</div>
</a>
</div>
<div
v-for="a in hciApps"
:key="a.label"
Expand All @@ -435,12 +445,14 @@ export default {
</div>
</template>
<!-- Cluster menu -->
<template v-if="clusters && !!clusters.length">
<div
ref="clusterList"
class="clusters"
:style="pinnedClustersHeight"
>
<!-- Pinned Clusters -->
<div
v-if="showPinClusters && pinFiltered.length"
class="clustersPinned"
Expand Down Expand Up @@ -490,10 +502,13 @@ export default {
<hr>
</div>
</div>
<!-- Clusters Search result -->
<div class="clustersList">
<div
v-for="c in clustersFiltered"
v-for="(c, index) in clustersFiltered"
:key="c.id"
:data-testid="`top-level-menu-cluster-${index}`"
@click="hide()"
>
<nuxt-link
Expand Down Expand Up @@ -532,14 +547,18 @@ export default {
</span>
</div>
</div>
<!-- No clusters message -->
<div
v-if="(clustersFiltered.length === 0 || pinFiltered.length === 0) && searchActive"
data-testid="top-level-menu-no-results"
class="none-matching"
>
{{ t('nav.search.noResults') }}
</div>
</div>
<!-- See all clusters -->
<nuxt-link
v-if="clusters.length > maxClustersToShow"
class="clusters-all"
Expand Down Expand Up @@ -611,6 +630,8 @@ export default {
</nuxt-link>
</div>
</template>
<!-- App menu -->
<template v-if="configurationApps.length">
<div
class="category-title"
Expand Down Expand Up @@ -640,6 +661,8 @@ export default {
</template>
</div>
</div>
<!-- Footer -->
<div
class="footer"
>
Expand Down
120 changes: 120 additions & 0 deletions shell/components/nav/__tests__/TopLevelMenu.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { mount, Wrapper } from '@vue/test-utils';
import TopLevelMenu from '@shell/components/nav/TopLevelMenu';

// DISCLAIMER: This should not be added here, although we have several store requests which are irrelevant
const defaultStore = {
'management/byId': jest.fn(),
'management/schemaFor': jest.fn(),
'i18n/t': jest.fn(),
'features/get': jest.fn(),
'prefs/theme': jest.fn(),
defaultClusterId: jest.fn(),
clusterId: jest.fn(),
'type-map/activeProducts': [],
};

describe('topLevelMenu', () => {
it('should display clusters', () => {
const wrapper: Wrapper<InstanceType<typeof TopLevelMenu>> = mount(TopLevelMenu, {
mocks: {
$store: {
getters: {
'management/all': () => [{ name: 'whatever' }],
...defaultStore
},
},
},
stubs: ['BrandImage', 'nuxt-link']
});

const cluster = wrapper.find('[data-testid="top-level-menu-cluster-0"]');

expect(cluster.exists()).toBe(true);
});

describe('searching a term', () => {
describe('should displays a no results message if have clusters but', () => {
it('given no matching clusters', () => {
const wrapper: Wrapper<InstanceType<typeof TopLevelMenu>> = mount(TopLevelMenu, {
data: () => ({ clusterFilter: 'whatever' }),
mocks: {
$store: {
getters: {
'management/all': () => [{ nameDisplay: 'something else' }],
...defaultStore
},
},
},
stubs: ['BrandImage', 'nuxt-link']
});

const noResults = wrapper.find('[data-testid="top-level-menu-no-results"]');

expect(noResults.exists()).toBe(true);
});

it('given no matched pinned clusters', () => {
const wrapper: Wrapper<InstanceType<typeof TopLevelMenu>> = mount(TopLevelMenu, {
data: () => ({ clusterFilter: 'whatever' }),
mocks: {
$store: {
getters: {
'management/all': () => [{ nameDisplay: 'something else', pinned: true }],
...defaultStore
},
},
},
stubs: ['BrandImage', 'nuxt-link']
});

const noResults = wrapper.find('[data-testid="top-level-menu-no-results"]');

expect(noResults.exists()).toBe(true);
});
});

describe('should not displays a no results message', () => {
it('given matching clusters', () => {
const search = 'you found me';
const wrapper: Wrapper<InstanceType<typeof TopLevelMenu>> = mount(TopLevelMenu, {
data: () => ({ clusterFilter: search }),
mocks: {
$store: {
getters: {
'management/all': () => [{ nameDisplay: search }],
...defaultStore
},
},
},
stubs: ['BrandImage', 'nuxt-link']
});

const noResults = wrapper.find('[data-testid="top-level-menu-no-results"]');

expect(wrapper.vm.clustersFiltered).toHaveLength(1);
expect(noResults.exists()).toBe(false);
});

it('given clusters with status pinned', () => {
const search = 'you found me';
const wrapper: Wrapper<InstanceType<typeof TopLevelMenu>> = mount(TopLevelMenu, {
data: () => ({ clusterFilter: search }),
mocks: {
$store: {
getters: {
'management/all': () => [{ nameDisplay: search, pinned: true }],
...defaultStore
},
},
},
stubs: ['BrandImage', 'nuxt-link']
});

const noResults = wrapper.find('[data-testid="top-level-menu-no-results"]');

expect(wrapper.vm.pinFiltered).toHaveLength(1);
expect(noResults.exists()).toBe(false);
});
});
});
});
2 changes: 1 addition & 1 deletion shell/utils/cluster.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { SETTING } from '@shell/config/settings';
export function filterOnlyKubernetesClusters(mgmtClusters, store) {
const openHarvesterContainerWorkload = store.getters['features/get']('harvester-baremetal-container-workload');

return mgmtClusters.filter((c) => {
return mgmtClusters?.filter((c) => {
return openHarvesterContainerWorkload ? true : !isHarvesterCluster(c);
});
}
Expand Down
2 changes: 1 addition & 1 deletion shell/utils/sort.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ export function sortBy(ary, keys, desc) {
keys = [keys];
}

return ary.slice().sort((objA, objB) => {
return (ary || []).slice().sort((objA, objB) => {
for ( let i = 0 ; i < keys.length ; i++ ) {
const parsed = parseField(keys[i]);
const a = get(objA, parsed.field);
Expand Down

0 comments on commit 50aac73

Please sign in to comment.