Skip to content
Draft
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c5cb980
Added workload-dashboard
marcelofukumoto May 18, 2026
7c1a8a4
Removed the TO added on STATUS CARD
marcelofukumoto May 19, 2026
4091873
Remove headless option and make the title only
marcelofukumoto May 19, 2026
17992fe
Fix lint
marcelofukumoto May 19, 2026
ecdfeef
Added the workload-dashboard type.
marcelofukumoto May 19, 2026
27fff42
Upgrades to the code and translation file
marcelofukumoto May 19, 2026
37bab37
Added fetch error and empty state
marcelofukumoto May 19, 2026
42bbb23
Added some errors and changed to ts and composition
marcelofukumoto May 20, 2026
2f82e5f
Added STORE cache for the state colors.
marcelofukumoto May 20, 2026
37c8f3d
Fixed the filter, used the same logic as the other pages.
marcelofukumoto May 20, 2026
100cf2d
Update the subtitle, fixed api request,
marcelofukumoto May 20, 2026
35451b7
Fixed some UIs Added the filter
marcelofukumoto May 21, 2026
15e0278
Remove the route, not useful
marcelofukumoto May 21, 2026
9d93cec
Broke into composables and 2 components. Separate type.ts as well.
marcelofukumoto May 21, 2026
f1f339c
Added unit tests
marcelofukumoto May 21, 2026
1d5116e
Added e2e test
marcelofukumoto May 22, 2026
c9b94cc
Fixed e2e test
marcelofukumoto May 22, 2026
16dd1c5
Changed to reduce specificity
marcelofukumoto May 26, 2026
0d9ee46
Applied code review improvements
marcelofukumoto May 26, 2026
36431d6
Removed prefs STATE_COLOR and fixed bug on state color request by clu…
marcelofukumoto May 26, 2026
201d7bd
Move the stateColor to a composable
marcelofukumoto May 26, 2026
e618784
e2e test fixes
marcelofukumoto May 26, 2026
4ba249b
Fix e2e test
marcelofukumoto May 26, 2026
e10e265
Stopped reusing the components
marcelofukumoto May 26, 2026
5d89ac0
Added some aria labels and focus
marcelofukumoto May 26, 2026
c518a45
Added missing testid
marcelofukumoto May 26, 2026
2f193b6
Removed unnecesary changes on SubtleLink Fixed any cases
marcelofukumoto May 27, 2026
6a9058b
Reposition files
marcelofukumoto May 27, 2026
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
23 changes: 23 additions & 0 deletions shell/assets/translations/en-us.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7425,6 +7425,29 @@ wm:
kubectlShell:
title: "Kubectl: {name}"

workloadDashboard:
title: Workloads Overview
subtitle:
allNamespaces: "For all namespaces ({count})"
userNamespaces: "Only for user namespaces ({count})"
systemNamespaces: "Only for system namespaces ({count})"
namespacedOnly: "Only for namespaced resources ({count})"
clusterOnly: "Only for cluster resources ({count})"
project: "For the namespaces of Project {name} ({count})"
namespace: "For the namespace {name} ({count})"
multipleSelected: "For {selected} items selected ({count})"
sections:
byState: By State
byType: By Type
errors:
noAccess: "No access to {type}"
fetchType: "Failed to fetch {type}"
fetchAll: Failed to fetch workload summaries
empty:
title: No workloads to show
message: "Tips: undo the last namespace filter you applied or <resetLink>reset the namespaces filter</resetLink>."
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.

this use case is different, the user may have not set any and the default only user namespaces is used

Suggested change
message: "Tips: undo the last namespace filter you applied or <resetLink>reset the namespaces filter</resetLink>."
message: "Tips: Update the namespace filter above or <resetLink>reset the namespaces filter</resetLink>."

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.

This one is provided by @oboc-sts from the Figma.
Let me ask him to check here directly.

Copy link
Copy Markdown

@oboc-sts oboc-sts May 27, 2026

Choose a reason for hiding this comment

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

I totally missed the "suggested change"...

Yeah, we can go with the suggestion @richard-cox made above, it makes sense, I like it ;)

docsMessage: "Want to learn more about Workloads? Read our <docsLink>documentation</docsLink>."

workload:
scaleWorkloads: Scale workloads
healthWorkloads: Jobs/Pods health status
Expand Down
36 changes: 31 additions & 5 deletions shell/components/Resource/Detail/Card/StatusCard/index.vue
Comment thread
marcelofukumoto marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ import { useI18n } from '@shell/composables/useI18n';
import { StateColor } from '@shell/utils/style';
import { computed } from 'vue';
import { useStore } from 'vuex';
import type { RouteLocationRaw } from 'vue-router';
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.

@codyrancher would you be able to review these component updates / new components? your composition api fu is greater than mine


export interface Props {
title: string;
resources?: any[];
showScaling?: boolean;
showPercent?: boolean;
noResourcesMessage?: string;
to?: RouteLocationRaw;
rowTo?: RouteLocationRaw | string;
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.

RouteLocationRaw already includes string, so | string appears to be redundant.

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.

Not using this anymore! Thanks

}
</script>

Expand All @@ -24,7 +28,10 @@ const i18n = useI18n(store);
const props = withDefaults(defineProps<Props>(), {
resources: undefined,
showScaling: false,
noResourcesMessage: undefined
showPercent: true,
noResourcesMessage: undefined,
to: undefined,
rowTo: undefined,
});
const emit = defineEmits(['decrease', 'increase']);

Expand All @@ -38,7 +45,7 @@ const segmentAccumulator = computed(() => {
const color: StateColor = resource.stateSimpleColor;

accumulator[color] = accumulator[color] || { count: 0 };
accumulator[color].count++;
accumulator[color].count += resource.count || 1;
});

return accumulator;
Expand All @@ -48,12 +55,13 @@ const rowAccumulator = computed(() => {
interface Value {
count: number;
color: StateColor;
stateId: string;
}
const accumulator: {[key in string]: Value} = {};

props.resources?.forEach((resource: any) => {
accumulator[resource.stateDisplay] = accumulator[resource.stateDisplay] || { count: 0 };
accumulator[resource.stateDisplay].count++;
accumulator[resource.stateDisplay] = accumulator[resource.stateDisplay] || { count: 0, stateId: resource.stateId || resource.stateDisplay };
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.

I believe that falling back on stateDisplay for stateId could cause a problem since the stateId is used to create query strings. I don't think In Progress in the query string is what was intended.

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.

Not using stateDisplay anymore! Only the ID

accumulator[resource.stateDisplay].count += resource.count || 1;
accumulator[resource.stateDisplay].color = resource.stateSimpleColor.replace('text-', '') as StateColor;
});

Expand All @@ -64,7 +72,13 @@ const percent = (count: number, total: number) => {
return count / total * 100;
};

const count = computed(() => props.resources?.length || 0);
const count = computed(() => {
if (!props.resources?.length) {
return 0;
}

return props.resources.reduce((sum: number, r: any) => sum + (r.count || 1), 0);
});

const segmentColors = computed(() => Object.keys(segmentAccumulator.value) as StateColor[]);
const segments = computed(() => segmentColors.value.map((color: StateColor) => ({
Expand All @@ -80,17 +94,27 @@ const rows = computed(() => {
return rowStates.value.map((state) => ({
color: rowAccumulator.value[state].color,
label: state,
stateId: rowAccumulator.value[state].stateId,
count: rowAccumulator.value[state].count,
percent: percent(rowAccumulator.value[state].count, count.value)
}));
});

function rowRoute(stateId: string): RouteLocationRaw | undefined {
if (!props.rowTo || typeof props.rowTo === 'string') {
return undefined;
}

return { ...props.rowTo, query: { q: `"metadata.state.name":"${ stateId }"` } };
}

</script>

<template>
<Card
:title="title"
data-testid="resource-detail-status-card"
:to="props.to"
>
<template
v-if="props.showScaling"
Expand Down Expand Up @@ -120,6 +144,8 @@ const rows = computed(() => {
:label="row.label"
:count="row.count"
:percent="row.percent"
:showPercent="props.showPercent"
:to="rowRoute(row.stateId)"
/>
</div>
<div
Expand Down
92 changes: 80 additions & 12 deletions shell/components/Resource/Detail/Card/__tests__/Card.test.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,104 @@
import { mount } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import Card from '@shell/components/Resource/Detail/Card/index.vue';

const mockPush = jest.fn();

jest.mock('vue-router', () => ({ useRouter: () => ({ push: mockPush }) }));

describe('component: Card/index', () => {
it('should render title and default slot', async() => {
it('should render title and default slot', () => {
const title = 'title';
const content = 'content';
const wrapper = mount(Card, { props: { title }, slots: { default: content } });
const wrapper = shallowMount(Card, { props: { title }, slots: { default: content } });

expect(wrapper.find('.title').element.innerHTML).toStrictEqual(title);
expect(wrapper.find('.title').text()).toStrictEqual(title);
});

it('should allow you to override the heading with slot', async() => {
it('should allow you to override the heading with slot', () => {
const title = 'title';
const content = 'content';
const wrapper = mount(Card, { props: { title }, slots: { heading: content } });
const wrapper = shallowMount(Card, { props: { title }, slots: { heading: content } });

expect(wrapper.find('.title').exists()).toBeFalsy();
expect(wrapper.find('.heading').element.innerHTML).toStrictEqual(content);
expect(wrapper.find('.heading').text()).toStrictEqual(content);
});

it('should allow you to override the title with slot', async() => {
it('should allow you to override the title with slot', () => {
const title = 'title';
const content = 'content';
const wrapper = mount(Card, { props: { title }, slots: { title: content } });
const wrapper = shallowMount(Card, { props: { title }, slots: { title: content } });

expect(wrapper.find('.title').element.innerHTML).toStrictEqual(content);
expect(wrapper.find('.title').text()).toStrictEqual(content);
});

it('should allow you to insert heading-action with slot', async() => {
it('should allow you to insert heading-action with slot', () => {
const content = '<div id="test">content</div>';
const wrapper = mount(Card, { slots: { 'heading-action': content } });
const wrapper = shallowMount(Card, { slots: { 'heading-action': content } });

expect(wrapper.find('.heading #test').exists()).toBeTruthy();
});

it('should hide heading when no title or heading slots are provided', () => {
const wrapper = shallowMount(Card, { slots: { default: 'body' } });

expect(wrapper.find('.heading').exists()).toBeFalsy();
});

describe('click navigation', () => {
const to = { name: 'test-route', params: { id: '1' } };

beforeEach(() => {
mockPush.mockClear();
});

it('should navigate when card body is clicked and to prop is set', async() => {
const wrapper = shallowMount(Card, { props: { to }, slots: { default: '<span>content</span>' } });

await wrapper.find('.body span').trigger('click');

expect(mockPush).toHaveBeenCalledWith(to);
});

it('should not navigate when no to prop is set', async() => {
const wrapper = shallowMount(Card, { props: { title: 'test' }, slots: { default: '<span>content</span>' } });

await wrapper.find('.body span').trigger('click');

expect(mockPush).not.toHaveBeenCalled();
});

it('should not navigate when clicking on a child anchor element', async() => {
const wrapper = shallowMount(Card, {
props: { to },
slots: { default: '<a href="#" class="inner-link">link</a>' },
});

await wrapper.find('.inner-link').trigger('click');

expect(mockPush).not.toHaveBeenCalled();
});

it('should not navigate when clicking on a child button element', async() => {
const wrapper = shallowMount(Card, {
props: { to },
slots: { default: '<button class="inner-btn">btn</button>' },
});

await wrapper.find('.inner-btn').trigger('click');

expect(mockPush).not.toHaveBeenCalled();
});

it('should add clickable class when to prop is set', () => {
const wrapper = shallowMount(Card, { props: { to }, slots: { default: 'body' } });

expect(wrapper.find('.detail-card').classes()).toContain('clickable');
});

it('should not add clickable class when to prop is not set', () => {
const wrapper = shallowMount(Card, { slots: { default: 'body' } });

expect(wrapper.find('.detail-card').classes()).not.toContain('clickable');
});
});
});
Loading
Loading