Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion shell/components/SideNav.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
import { sortBy } from '@shell/utils/sort';
import { ucFirst } from '@shell/utils/string';

import { HCI, UI, SCHEMA } from '@shell/config/types';
import { HCI, UI, SCHEMA, COUNT } from '@shell/config/types';
import { HARVESTER_NAME as HARVESTER } from '@shell/config/features';
import { NAME as EXPLORER } from '@shell/config/product/explorer';
import { TYPE_MODES } from '@shell/store/type-map';
Expand Down Expand Up @@ -54,6 +54,17 @@ export default {
}
},

/**
* When new resource types appear in (or are removed from) the count data,
* it means a CRD was added or removed. Re-build the nav so the new group
* shows up under "More Resources" without requiring a page refresh.
*/
countTypes(a, b) {
Copy link
Copy Markdown
Member

@richard-cox richard-cox May 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we might need to reconsider this.

re-rendering the side nav on count change was something we used to do but hit large performance issues given counts change a lot, and in some scenarios there's 1000s of schemas (i.e. crossplane world).

in theory all new schema's should be picked up via the computed property allSchemasIds which has a watch that calls queueUpdate?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no idea about this logic, as you are the master.
I made this bug go away, but if you have tips, I welcome them and shove them to the AI :)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the bug still reproducible when this part is removed? If not then it should be enough just to remove it. If so though it might be worth debugging the allSchemasIds part and see it can be fixed there

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's the whole point of the fix, and it does not work without it.
If you want to have time to experiment, I leave it to you to play around and keep the PR open till you feel is ok.

if ( !sameContents(a, b) ) {
this.queueUpdate();
}
},

allNavLinksIds(a, b) {
if ( !sameContents(a, b) ) {
this.queueUpdate();
Expand Down Expand Up @@ -177,6 +188,33 @@ export default {
return this.$store.getters[`${ product.inStore }/all`](SCHEMA).map((s) => s.id).sort();
},

/**
* Returns the sorted list of resource type IDs that currently have
* resources in the COUNT data.
* Watching this allows the nav to react when a CRD type transitions from
* hidden to visible (count goes from 0 to >0) or vice-versa so it appears
* under "More Resources" without a page refresh.
*/
countTypes() {
const managementReady = this.managementReady;
const product = this.currentProduct;

if ( !managementReady || !product ) {
return [];
}

const counts = this.$store.getters[`${ product.inStore }/all`](COUNT)?.[0]?.counts || {};

return Object.entries(counts)
.filter(([, entry]) => {
const n = entry?.summary?.count;

return typeof n === 'number' && n > 0;
})
.map(([type]) => type)
.sort();
},

namespaces() {
return this.$store.getters['activeNamespaceCache'];
},
Expand Down
85 changes: 85 additions & 0 deletions shell/components/__tests__/SideNav.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import SideNav from '@shell/components/SideNav.vue';
import { COUNT } from '@shell/config/types';

describe('component: SideNav', () => {
describe('countTypes', () => {
const countTypes = (SideNav as any).computed.countTypes;
const watchCountTypes = (SideNav as any).watch.countTypes;

const getStore = (counts: Record<string, any>) => ({
getters: {
'cluster/all': (type: string) => {
if (type === COUNT) {
return [{ counts }];
}

return [];
},
},
});

it('returns an empty list when management is not ready', () => {
const types = countTypes.call({
managementReady: false,
currentProduct: { inStore: 'cluster' },
$store: getStore({ 'my.io.crd': { summary: { count: 5 } } }),
});

expect(types).toStrictEqual([]);
});

it('returns only types with a positive count, sorted by type id', () => {
const types = countTypes.call({
managementReady: true,
currentProduct: { inStore: 'cluster' },
$store: getStore({
'z.io.hidden': { summary: { count: 0 } },
'a.io.visible': { summary: { count: 3 } },
'c.io.not-visible': { summary: { count: -1 } },
'b.io.visible': { summary: { count: 1 } },
'd.io.bad-type': { summary: {} },
}),
});

expect(types).toStrictEqual(['a.io.visible', 'b.io.visible']);
});

it('returns an empty list when count data is missing', () => {
const types = countTypes.call({
managementReady: true,
currentProduct: { inStore: 'cluster' },
$store: {
getters: {
'cluster/all': (type: string) => {
if (type === COUNT) {
return [];
}

return [];
},
},
},
});

expect(types).toStrictEqual([]);
});

it('queues a nav refresh when visible count types change', () => {
const queueUpdate = jest.fn();
const logSideNavDebug = jest.fn();

watchCountTypes.call({ queueUpdate, logSideNavDebug }, ['a.io.visible'], ['a.io.visible', 'b.io.visible']);

expect(queueUpdate).toHaveBeenCalledWith();
});

it('does not queue a nav refresh when visible count types are unchanged', () => {
const queueUpdate = jest.fn();
const logSideNavDebug = jest.fn();

watchCountTypes.call({ queueUpdate, logSideNavDebug }, ['a.io.visible'], ['a.io.visible']);

expect(queueUpdate).not.toHaveBeenCalled();
});
});
});
74 changes: 74 additions & 0 deletions shell/plugins/steve/__tests__/subscribe.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,80 @@ describe('steve: subscribe', () => {
});
});

describe('ws.resource.create', () => {
const makeCtx = (overrides: Record<string, any> = {}) => ({
state: { debugSocket: false, queue: [] },
getters: {
storeName: 'cluster',
normalizeType: (t: string) => t,
typeEntry: () => ({ revision: 0 }),
havePage: () => null,
haveNamespace: () => null,
...overrides.getters,
},
rootGetters: {
isAllNamespaces: true,
...overrides.rootGetters,
},
dispatch: jest.fn(),
commit: jest.fn(),
...overrides,
});

it('notifies the worker about a new schema via updateSchema and still queues a load', () => {
const postMessage = jest.fn();
const ctx = makeCtx();

// Simulate a worker being registered for this store
(actions['ws.resource.create'] as any).call(
{ $workers: { cluster: { postMessage } } },
ctx,
{ data: { type: 'schema', id: 'my.io.crd' }, revision: '1' }
);

expect(postMessage).toHaveBeenCalledWith({ updateSchema: { type: 'schema', id: 'my.io.crd' } });
expect(ctx.state.queue).toStrictEqual([
{
action: 'dispatch', event: 'load', body: { type: 'schema', id: 'my.io.crd' }
},
]);
});

it('does not notify the worker for non-schema resource creates, but still queues a load', () => {
const postMessage = jest.fn();
const ctx = makeCtx();

(actions['ws.resource.create'] as any).call(
{ $workers: { cluster: { postMessage } } },
ctx,
{ data: { type: 'pod', id: 'my-pod' }, revision: '1' }
);

expect(postMessage).not.toHaveBeenCalled();
expect(ctx.state.queue).toStrictEqual([
{
action: 'dispatch', event: 'load', body: { type: 'pod', id: 'my-pod' }
},
]);
});

it('still queues a load when no worker is registered', () => {
const ctx = makeCtx();

(actions['ws.resource.create'] as any).call(
{ $workers: {} },
ctx,
{ data: { type: 'schema', id: 'my.io.crd' }, revision: '1' }
);

expect(ctx.state.queue).toStrictEqual([
{
action: 'dispatch', event: 'load', body: { type: 'schema', id: 'my.io.crd' }
},
]);
});
});

describe('fetchPageResources', () => {
const dispatch = jest.fn();
const getters = {
Expand Down
13 changes: 13 additions & 0 deletions shell/plugins/steve/subscribe.js
Original file line number Diff line number Diff line change
Expand Up @@ -1385,6 +1385,19 @@ const defaultActions = {
const data = msg.data;
const type = data?.type;

// Notify the web worker about the new schema so its hash map stays in sync.
// This mirrors the handling in ws.resource.remove and prevents spurious
// "changed" notifications for schemas the worker has never seen before.
// Note: we deliberately do NOT return early here – we also call queueChange
// below so that the schema is added to the store immediately.
if (type === SCHEMA) {
const worker = (this.$workers || {})[ctx.getters.storeName];

if (worker) {
worker.postMessage({ updateSchema: data });
}
}

const havePage = ctx.getters['havePage'](type);

if (havePage) {
Expand Down
Loading