@rancher/shell 3.0.11 → 3.0.12-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/assets/images/providers/entraid-black.svg +4 -0
  2. package/assets/images/providers/entraid.svg +9 -0
  3. package/assets/images/vendor/entraid.svg +9 -0
  4. package/assets/styles/app.scss +0 -1
  5. package/assets/styles/base/_mixins.scss +31 -0
  6. package/assets/styles/base/_variables.scss +2 -0
  7. package/assets/styles/themes/_modern.scss +6 -5
  8. package/assets/translations/en-us.yaml +24 -21
  9. package/assets/translations/zh-hans.yaml +4 -11
  10. package/chart/__tests__/S3.test.ts +10 -3
  11. package/components/CountBox.vue +20 -0
  12. package/components/CreateDriver.vue +0 -12
  13. package/components/DetailText.vue +12 -3
  14. package/components/EmptyProductPage.vue +76 -0
  15. package/components/Resource/Detail/CopyToClipboard.vue +1 -2
  16. package/components/Resource/Detail/Metadata/KeyValueRow.vue +9 -3
  17. package/components/Resource/Detail/TitleBar/__tests__/__snapshots__/index.test.ts.snap +31 -0
  18. package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +45 -1
  19. package/components/Resource/Detail/TitleBar/index.vue +1 -1
  20. package/components/Resource/Detail/ViewOptions/__tests__/__snapshots__/index.test.ts.snap +9 -0
  21. package/components/Resource/Detail/ViewOptions/__tests__/index.test.ts +62 -0
  22. package/components/Resource/Detail/ViewOptions/index.vue +2 -1
  23. package/components/ResourceList/Masthead.vue +25 -2
  24. package/components/SelectIconGrid.vue +5 -0
  25. package/components/SideNav.vue +13 -0
  26. package/components/__tests__/CountBox.test.ts +72 -0
  27. package/components/__tests__/DetailText.test.ts +113 -0
  28. package/components/__tests__/PromptModal.test.ts +2 -0
  29. package/components/fleet/FleetClusterTargets/index.vue +18 -1
  30. package/components/fleet/FleetClusters.vue +1 -0
  31. package/components/fleet/__tests__/FleetClusters.test.ts +71 -0
  32. package/components/form/InputWithSelect.vue +18 -10
  33. package/components/form/KeyValue.vue +17 -1
  34. package/components/form/LabeledSelect.vue +82 -24
  35. package/components/form/NodeScheduling.vue +17 -3
  36. package/components/form/PrivateRegistry.vue +69 -0
  37. package/components/form/Select.vue +73 -56
  38. package/components/form/ServiceNameSelect.vue +13 -11
  39. package/components/form/__tests__/KeyValue.test.ts +66 -0
  40. package/components/form/__tests__/NodeScheduling.test.ts +9 -0
  41. package/components/form/__tests__/PrivateRegistry.test.ts +133 -0
  42. package/components/form/labeled-select-utils/useLabeledSelectPagination.ts +138 -0
  43. package/components/formatter/WorkloadHealthScale.vue +3 -1
  44. package/components/nav/Group.vue +33 -9
  45. package/components/nav/Header.vue +56 -10
  46. package/components/nav/NotificationCenter/Notification.vue +4 -1
  47. package/components/nav/NotificationCenter/NotificationHeader.vue +20 -8
  48. package/components/nav/NotificationCenter/__tests__/NotificationHeader.test.ts +80 -0
  49. package/components/nav/TopLevelMenu.vue +15 -1
  50. package/components/nav/Type.vue +8 -7
  51. package/components/nav/WindowManager/index.vue +2 -1
  52. package/components/nav/WorkspaceSwitcher.vue +13 -0
  53. package/components/nav/__tests__/Group.test.ts +67 -0
  54. package/components/nav/__tests__/Header.test.ts +235 -0
  55. package/components/nav/__tests__/Type.test.ts +20 -3
  56. package/components/templates/default.vue +34 -4
  57. package/components/templates/home.vue +12 -25
  58. package/components/templates/plain.vue +13 -26
  59. package/composables/useLabeledFormElement.ts +10 -2
  60. package/composables/useLabeledSelect.ts +60 -0
  61. package/composables/useUserRetentionValidation.ts +1 -49
  62. package/config/cookies.js +0 -1
  63. package/config/labels-annotations.js +1 -0
  64. package/config/pagination-table-headers.js +8 -1
  65. package/config/product/apps.js +2 -1
  66. package/config/product/auth.js +1 -0
  67. package/config/product/backup.js +1 -0
  68. package/config/product/compliance.js +1 -1
  69. package/config/product/explorer.js +25 -6
  70. package/config/product/fleet.js +1 -0
  71. package/config/product/gatekeeper.js +1 -0
  72. package/config/product/istio.js +1 -0
  73. package/config/product/logging.js +1 -0
  74. package/config/product/longhorn.js +2 -1
  75. package/config/product/manager.js +1 -0
  76. package/config/product/monitoring.js +1 -0
  77. package/config/product/navlinks.js +1 -0
  78. package/config/product/neuvector.js +2 -1
  79. package/config/product/settings.js +1 -0
  80. package/config/product/uiplugins.js +1 -0
  81. package/config/query-params.js +1 -0
  82. package/config/router/routes.js +0 -8
  83. package/core/__tests__/plugin-products-helpers.test.ts +454 -0
  84. package/core/__tests__/plugin-products.test.ts +3810 -0
  85. package/core/extension-manager-impl.js +30 -1
  86. package/core/plugin-products-base.ts +392 -0
  87. package/core/plugin-products-extending.ts +44 -0
  88. package/core/plugin-products-helpers.ts +263 -0
  89. package/core/plugin-products-top-level.ts +66 -0
  90. package/core/plugin-products-type-guards.ts +33 -0
  91. package/core/plugin-products.ts +50 -0
  92. package/core/plugin-types.ts +237 -0
  93. package/core/plugin.ts +45 -10
  94. package/core/productDebugger.js +48 -0
  95. package/core/types.ts +97 -11
  96. package/detail/__tests__/__snapshots__/fleet.cattle.io.bundle.test.ts.snap +52 -0
  97. package/detail/__tests__/fleet.cattle.io.bundle.test.ts +171 -0
  98. package/detail/__tests__/management.cattle.io.fleetworkspace.test.ts +128 -0
  99. package/detail/fleet.cattle.io.bundle.vue +21 -34
  100. package/detail/management.cattle.io.fleetworkspace.vue +49 -0
  101. package/dialog/ExtensionCatalogInstallDialog.vue +1 -1
  102. package/dialog/InstallExtensionDialog.vue +6 -27
  103. package/dialog/UninstallExistingExtensionDialog.vue +141 -0
  104. package/dialog/UninstallExtensionDialog.vue +4 -26
  105. package/dialog/__tests__/UninstallExistingExtensionDialog.test.ts +114 -0
  106. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +1 -0
  107. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +9 -0
  108. package/edit/__tests__/kontainerDriver.test.ts +0 -13
  109. package/edit/__tests__/nodeDriver.test.ts +5 -11
  110. package/edit/__tests__/resources.cattle.io.restore.test.ts +9 -0
  111. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/General.test.ts.snap +6 -0
  112. package/edit/auth/__tests__/oidc.test.ts +54 -0
  113. package/edit/auth/azuread.vue +1 -1
  114. package/edit/auth/oidc.vue +8 -0
  115. package/edit/kontainerDriver.vue +1 -2
  116. package/edit/nodeDriver.vue +0 -2
  117. package/edit/provisioning.cattle.io.cluster/AgentEnv.vue +1 -0
  118. package/edit/provisioning.cattle.io.cluster/__tests__/AgentEnv.test.ts +25 -0
  119. package/edit/provisioning.cattle.io.cluster/__tests__/Ingress.test.ts +176 -0
  120. package/edit/provisioning.cattle.io.cluster/index.vue +70 -99
  121. package/edit/provisioning.cattle.io.cluster/rke2.vue +4 -1
  122. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +6 -0
  123. package/edit/provisioning.cattle.io.cluster/tabs/Ingress.vue +7 -2
  124. package/initialize/App.vue +29 -2
  125. package/initialize/install-plugins.js +0 -2
  126. package/list/__tests__/management.cattle.io.feature.test.ts +105 -0
  127. package/list/catalog.cattle.io.app.vue +25 -5
  128. package/list/management.cattle.io.feature.vue +1 -1
  129. package/list/management.cattle.io.fleetworkspace.vue +8 -0
  130. package/list/provisioning.cattle.io.cluster.vue +0 -1
  131. package/list/workload.vue +11 -4
  132. package/machine-config/amazonec2.vue +1 -0
  133. package/mixins/chart.js +40 -9
  134. package/mixins/resource-fetch.js +12 -3
  135. package/models/__tests__/catalog.cattle.io.app.test.ts +15 -1
  136. package/models/__tests__/catalog.cattle.io.clusterrepo.test.ts +84 -0
  137. package/models/__tests__/chart.test.ts +99 -6
  138. package/models/__tests__/management.cattle.io.feature.test.ts +131 -0
  139. package/models/__tests__/monitoring.coreos.com.alertmanagerconfig.test.ts +98 -0
  140. package/models/catalog.cattle.io.app.js +21 -17
  141. package/models/catalog.cattle.io.clusterrepo.js +39 -11
  142. package/models/chart.js +33 -19
  143. package/models/fleet-application.js +1 -1
  144. package/models/fleet.cattle.io.bundle.js +1 -1
  145. package/models/kontainerdriver.js +11 -0
  146. package/models/management.cattle.io.authconfig.js +5 -1
  147. package/models/management.cattle.io.cluster.js +0 -53
  148. package/models/management.cattle.io.feature.js +3 -3
  149. package/models/management.cattle.io.kontainerdriver.js +1 -26
  150. package/models/monitoring.coreos.com.alertmanagerconfig.js +31 -17
  151. package/models/nodedriver.js +7 -0
  152. package/models/pod.js +18 -0
  153. package/models/workload.js +20 -2
  154. package/package.json +13 -13
  155. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +0 -1
  156. package/pages/c/_cluster/apps/charts/__tests__/chart.test.ts +189 -0
  157. package/pages/c/_cluster/apps/charts/__tests__/index.test.ts +55 -0
  158. package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +53 -0
  159. package/pages/c/_cluster/apps/charts/chart.vue +217 -33
  160. package/pages/c/_cluster/apps/charts/index.vue +2 -2
  161. package/pages/c/_cluster/apps/charts/install.vue +8 -3
  162. package/pages/c/_cluster/auth/user.retention/index.vue +55 -22
  163. package/pages/c/_cluster/manager/drivers/kontainerDriver/index.vue +5 -7
  164. package/pages/c/_cluster/settings/brand.vue +4 -4
  165. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +39 -2
  166. package/pages/c/_cluster/uiplugins/__tests__/PluginInfoPanel.test.ts +61 -0
  167. package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +246 -23
  168. package/pages/c/_cluster/uiplugins/index.vue +166 -62
  169. package/plugins/dashboard-store/__tests__/resource-class.test.ts +1 -0
  170. package/plugins/dashboard-store/actions.js +3 -2
  171. package/plugins/dashboard-store/resource-class.js +62 -6
  172. package/plugins/plugin.js +16 -0
  173. package/plugins/steve/steve-pagination-utils.ts +7 -0
  174. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +205 -1
  175. package/rancher-components/Form/LabeledInput/LabeledInput.vue +82 -4
  176. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +1 -1
  177. package/scripts/test-plugins-build.sh +5 -2
  178. package/scripts/typegen.sh +13 -1
  179. package/server/server-middleware.js +2 -2
  180. package/static/humans.txt +1 -0
  181. package/static/robots.txt +34 -0
  182. package/static/welcome-cow.svg +18 -0
  183. package/store/__tests__/catalog.test.ts +161 -11
  184. package/store/__tests__/type-map.test.ts +84 -24
  185. package/store/auth.js +0 -3
  186. package/store/catalog.js +60 -8
  187. package/store/type-map.js +42 -3
  188. package/tsconfig.paths.json +1 -0
  189. package/types/resources/pod.ts +18 -0
  190. package/types/shell/index.d.ts +8539 -2938
  191. package/types/store/dashboard-store.types.ts +5 -0
  192. package/types/store/pagination.types.ts +6 -0
  193. package/utils/__tests__/git.test.ts +270 -0
  194. package/utils/__tests__/inactivity.test.ts +316 -0
  195. package/utils/__tests__/object.test.ts +77 -0
  196. package/utils/__tests__/time.test.ts +14 -1
  197. package/utils/__tests__/url.test.ts +246 -0
  198. package/utils/axios.js +1 -4
  199. package/utils/dynamic-importer.js +3 -2
  200. package/utils/object.js +33 -2
  201. package/utils/pagination-utils.ts +1 -1
  202. package/utils/time.ts +5 -0
  203. package/utils/uiplugins.ts +12 -16
  204. package/utils/validators/__tests__/private-registry.test.ts +76 -0
  205. package/utils/validators/private-registry.ts +28 -0
  206. package/vue.config.js +0 -9
  207. package/assets/images/providers/azuread-black.svg +0 -22
  208. package/assets/images/providers/azuread.svg +0 -25
  209. package/assets/images/vendor/azuread.svg +0 -18
  210. package/assets/styles/fonts/_dots.scss +0 -18
  211. package/components/EmberPage.vue +0 -622
  212. package/components/EmberPageView.vue +0 -39
  213. package/components/form/labeled-select-utils/labeled-select-pagination.ts +0 -116
  214. package/mixins/labeled-form-element.ts +0 -225
  215. package/pages/c/_cluster/explorer/tools/pages/_page.vue +0 -28
  216. package/pages/c/_cluster/manager/pages/_page.vue +0 -22
  217. package/pages/c/_cluster/mcapps/pages/_page.vue +0 -22
  218. package/plugins/ember-cookie.js +0 -17
  219. package/utils/ember-page.js +0 -30
@@ -0,0 +1,133 @@
1
+ import { shallowMount } from '@vue/test-utils';
2
+ import PrivateRegistry from '@shell/components/form/PrivateRegistry.vue';
3
+ import { Checkbox } from '@components/Form/Checkbox';
4
+ import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
5
+
6
+ const defaultMocks = {
7
+ $store: {
8
+ getters: {
9
+ 'i18n/t': (text: string) => text,
10
+ t: (text: string) => text,
11
+ }
12
+ }
13
+ };
14
+
15
+ const mountPrivateRegistry = (props = {}) => {
16
+ return shallowMount(PrivateRegistry, {
17
+ props: {
18
+ mode: 'edit',
19
+ ...props
20
+ },
21
+ global: { mocks: defaultMocks }
22
+ });
23
+ };
24
+
25
+ describe('privateRegistry', () => {
26
+ it('should render the info banner', () => {
27
+ const wrapper = mountPrivateRegistry();
28
+ const banner = wrapper.find('[color="info"]');
29
+
30
+ expect(banner.exists()).toBe(true);
31
+ });
32
+
33
+ it('should render the enable checkbox', () => {
34
+ const wrapper = mountPrivateRegistry();
35
+ const checkbox = wrapper.findComponent(Checkbox);
36
+
37
+ expect(checkbox.exists()).toBe(true);
38
+ });
39
+
40
+ it('should not show the URL input when no value is provided', () => {
41
+ const wrapper = mountPrivateRegistry();
42
+
43
+ expect(wrapper.findComponent(LabeledInput).exists()).toBe(false);
44
+ });
45
+
46
+ it('should show the URL input when a value is provided', () => {
47
+ const wrapper = mountPrivateRegistry({ value: 'registry.example.com' });
48
+
49
+ expect(wrapper.findComponent(LabeledInput).exists()).toBe(true);
50
+ });
51
+
52
+ it('should show the URL input when checkbox is checked', async() => {
53
+ const wrapper = mountPrivateRegistry();
54
+
55
+ expect(wrapper.findComponent(LabeledInput).exists()).toBe(false);
56
+
57
+ const checkbox = wrapper.findComponent(Checkbox);
58
+
59
+ await checkbox.vm.$emit('update:value', true);
60
+ await wrapper.vm.$nextTick();
61
+
62
+ expect(wrapper.findComponent(LabeledInput).exists()).toBe(true);
63
+ });
64
+
65
+ it('should emit update:value with null when checkbox is unchecked', async() => {
66
+ const wrapper = mountPrivateRegistry({ value: 'registry.example.com' });
67
+
68
+ const checkbox = wrapper.findComponent(Checkbox);
69
+
70
+ await checkbox.vm.$emit('update:value', false);
71
+ await wrapper.vm.$nextTick();
72
+
73
+ expect(wrapper.emitted('update:value')).toHaveLength(1);
74
+ expect(wrapper.emitted('update:value')![0]).toStrictEqual([null]);
75
+ });
76
+
77
+ it('should emit update:value when the URL input changes', async() => {
78
+ const wrapper = mountPrivateRegistry({ value: 'registry.example.com' });
79
+ const input = wrapper.findComponent(LabeledInput);
80
+
81
+ await input.vm.$emit('update:value', 'new-registry.example.com');
82
+
83
+ expect(wrapper.emitted('update:value')).toHaveLength(1);
84
+ expect(wrapper.emitted('update:value')![0]).toStrictEqual(['new-registry.example.com']);
85
+ });
86
+
87
+ it('should auto-enable the checkbox when value changes from null to a string', async() => {
88
+ const wrapper = mountPrivateRegistry();
89
+
90
+ expect(wrapper.findComponent(LabeledInput).exists()).toBe(false);
91
+
92
+ await wrapper.setProps({ value: 'registry.example.com' });
93
+
94
+ expect(wrapper.findComponent(LabeledInput).exists()).toBe(true);
95
+ });
96
+
97
+ it('should pass rules to the URL input', () => {
98
+ const mockRule = jest.fn();
99
+ const wrapper = mountPrivateRegistry({
100
+ value: 'registry.example.com',
101
+ rules: [mockRule]
102
+ });
103
+ const input = wrapper.findComponent(LabeledInput);
104
+
105
+ expect(input.attributes('rules')).toBeDefined();
106
+ });
107
+
108
+ it('should apply custom data-testid to checkbox when provided', () => {
109
+ const wrapper = mountPrivateRegistry({ checkboxTestId: 'my-checkbox' });
110
+ const checkbox = wrapper.findComponent(Checkbox);
111
+
112
+ expect(checkbox.attributes('data-testid')).toBe('my-checkbox');
113
+ });
114
+
115
+ it('should apply custom data-testid to input when provided', () => {
116
+ const wrapper = mountPrivateRegistry({
117
+ value: 'registry.example.com',
118
+ inputTestId: 'my-input'
119
+ });
120
+ const input = wrapper.findComponent(LabeledInput);
121
+
122
+ expect(input.attributes('data-testid')).toBe('my-input');
123
+ });
124
+
125
+ it('should not set data-testid when not provided', () => {
126
+ const wrapper = mountPrivateRegistry({ value: 'registry.example.com' });
127
+ const checkbox = wrapper.findComponent(Checkbox);
128
+ const input = wrapper.findComponent(LabeledInput);
129
+
130
+ expect(checkbox.attributes('data-testid')).toBeUndefined();
131
+ expect(input.attributes('data-testid')).toBeUndefined();
132
+ });
133
+ });
@@ -0,0 +1,138 @@
1
+ /**
2
+ * composable to provide pagination support to LabeledSelect
3
+ */
4
+ import {
5
+ ref, computed, onMounted, ComputedRef, Ref, PropType
6
+ } from 'vue';
7
+ import { useStore } from 'vuex';
8
+ import { debounce } from 'lodash';
9
+ import { LabelSelectPaginateFn, LABEL_SELECT_NOT_OPTION_KINDS, LABEL_SELECT_KINDS } from '@shell/types/components/labeledSelect';
10
+
11
+ interface LabeledSelectPaginationProps {
12
+ paginate?: LabelSelectPaginateFn | null;
13
+ inStore?: string;
14
+ resourceType?: string | null;
15
+ options?: Array<any>;
16
+ }
17
+
18
+ interface UseLabeledSelectPagination {
19
+ canPaginate: ComputedRef<boolean>;
20
+ canLoadMore: ComputedRef<boolean>;
21
+ optionCounts: ComputedRef<string>;
22
+ _options: ComputedRef<any[]>;
23
+ pages: Ref<number>;
24
+ totalResults: Ref<number>;
25
+ paginating: Ref<boolean>;
26
+ loadMore: () => void;
27
+ setPaginationFilter: (filter: string) => void;
28
+ }
29
+
30
+ export const labeledSelectPaginationProps = {
31
+ paginate: {
32
+ default: null,
33
+ type: Function as PropType<LabelSelectPaginateFn>,
34
+ },
35
+
36
+ inStore: {
37
+ type: String,
38
+ default: 'cluster',
39
+ },
40
+
41
+ /**
42
+ * Resource to show
43
+ */
44
+ resourceType: {
45
+ type: String,
46
+ default: null,
47
+ },
48
+ };
49
+
50
+ export const useLabeledSelectPagination = (props: LabeledSelectPaginationProps): UseLabeledSelectPagination => {
51
+ const store = useStore();
52
+
53
+ // Internal
54
+ const currentPage = ref(1);
55
+ const search = ref('');
56
+ const pageSize = ref(10);
57
+ const pages = ref(0);
58
+
59
+ // External
60
+ const page = ref<any[]>([]);
61
+ const totalResults = ref(0);
62
+ const paginating = ref(false);
63
+
64
+ const canPaginate = computed(() => {
65
+ return !!props.paginate && !!props.resourceType && store.getters[`${ props.inStore }/paginationEnabled`](props.resourceType);
66
+ });
67
+
68
+ const _options = computed(() => canPaginate.value ? page.value : (props.options || []));
69
+
70
+ const canLoadMore = computed(() => pages.value > currentPage.value);
71
+
72
+ const optionsInPage = computed(() => {
73
+ // Number of genuine options (not groups, dividers, etc)
74
+ return canPaginate.value ? _options.value.filter((o: any) => {
75
+ return o.kind !== LABEL_SELECT_KINDS.NONE && !LABEL_SELECT_NOT_OPTION_KINDS.includes(o.kind);
76
+ }).length : 0;
77
+ });
78
+
79
+ const optionCounts = computed(() => {
80
+ if (!canPaginate.value || optionsInPage.value === totalResults.value) {
81
+ return '';
82
+ }
83
+
84
+ return store.getters['i18n/t']('labelSelect.pagination.counts', {
85
+ count: optionsInPage.value,
86
+ totalCount: totalResults.value
87
+ });
88
+ });
89
+
90
+ const requestPagination = async(resetPage = false) => {
91
+ paginating.value = true;
92
+
93
+ const { page: p, pages: pg, total } = await (props.paginate as LabelSelectPaginateFn)({
94
+ resetPage,
95
+ pageContent: page.value || [],
96
+ page: currentPage.value,
97
+ filter: search.value,
98
+ pageSize: pageSize.value,
99
+ });
100
+
101
+ page.value = p;
102
+ pages.value = pg || 0;
103
+ totalResults.value = total || 0;
104
+ paginating.value = false;
105
+ };
106
+
107
+ const debouncedRequestPagination = debounce(requestPagination, 700);
108
+
109
+ const setPaginationFilter = (filter: string) => {
110
+ paginating.value = true; // Do this before debounce
111
+ currentPage.value = 1;
112
+ search.value = filter;
113
+ debouncedRequestPagination(true);
114
+ };
115
+
116
+ const loadMore = () => {
117
+ currentPage.value++;
118
+ requestPagination();
119
+ };
120
+
121
+ onMounted(async() => {
122
+ if (canPaginate.value) {
123
+ await requestPagination();
124
+ }
125
+ });
126
+
127
+ return {
128
+ canPaginate,
129
+ canLoadMore,
130
+ optionCounts,
131
+ _options,
132
+ pages,
133
+ totalResults,
134
+ paginating,
135
+ loadMore,
136
+ setPaginationFilter,
137
+ };
138
+ };
@@ -67,7 +67,9 @@ export default {
67
67
  color: `bg-${ value.color }`,
68
68
  value: value.count || 0,
69
69
  label: ucFirst(name)
70
- })).filter((x) => x.value > 0);
70
+ }))
71
+ .filter((x) => x.value > 0)
72
+ .sort((a, b) => a.label.localeCompare(b.label));
71
73
 
72
74
  return 5;
73
75
  },
@@ -82,7 +82,8 @@ export default {
82
82
  const validRoute = filterLocationValidParams(this.$router, overviewRoute || {});
83
83
  const route = this.$router.resolve(validRoute);
84
84
 
85
- return this.$route.fullPath.split('#')[0] === route?.fullPath;
85
+ // Use .path instead of .fullPath to ignore query parameters and hashes when comparing routes
86
+ return this.$route.path === route?.path;
86
87
  }
87
88
  }
88
89
 
@@ -204,14 +205,14 @@ export default {
204
205
  } else if (item.route) {
205
206
  const navLevels = ['cluster', 'product', 'resource'];
206
207
  const matchesNavLevel = navLevels.filter((param) => !this.$route.params[param] || this.$route.params[param] !== item.route.params[param]).length === 0;
207
- const withoutHash = this.$route.hash ? this.$route.fullPath.slice(0, this.$route.fullPath.indexOf(this.$route.hash)) : this.$route.fullPath;
208
- const withoutQuery = withoutHash.split('?')[0];
209
208
  const validItemRoute = filterLocationValidParams(this.$router, item.route);
210
- const itemFullPath = this.$router.resolve(validItemRoute).fullPath;
211
209
 
212
- if (matchesNavLevel || itemFullPath === withoutQuery) {
210
+ // Use .path instead of .fullPath to ignore query parameters and hashes when comparing routes
211
+ const itemPath = this.$router.resolve(validItemRoute).path;
212
+
213
+ if (matchesNavLevel || itemPath === this.$route.path) {
213
214
  return true;
214
- } else if (parentPath && itemFullPath === parentPath) {
215
+ } else if (parentPath && itemPath === parentPath) {
215
216
  return true;
216
217
  }
217
218
  }
@@ -269,8 +270,9 @@ export default {
269
270
  @keyup.space="groupSelected()"
270
271
  >
271
272
  <slot name="header">
273
+ <!-- Group overview with link -->
272
274
  <router-link
273
- v-if="hasOverview"
275
+ v-if="hasOverview && hasChildren"
274
276
  :to="headerRoute"
275
277
  :exact="group.children[0].exact"
276
278
  :tabindex="-1"
@@ -279,15 +281,32 @@ export default {
279
281
  <span v-clean-html="group.labelDisplay || group.label" />
280
282
  </h6>
281
283
  </router-link>
284
+ <!-- Non-linked group header -->
282
285
  <h6
283
- v-else
286
+ v-else-if="hasChildren"
284
287
  >
285
288
  <span v-clean-html="group.labelDisplay || group.label" />
286
289
  </h6>
290
+ <!-- Simple child (nav item) -->
291
+ <ul
292
+ v-else
293
+ class="list-unstyled body root-depth"
294
+ v-bind="$attrs"
295
+ >
296
+ <Type
297
+
298
+ :key="id+'_' + group.name + '_type'"
299
+ :is-root="depth == 0 && !showHeader"
300
+ :type="group"
301
+ :depth="depth"
302
+ :highlight-route="highlightRoute"
303
+ @selected="selectType($event)"
304
+ />
305
+ </ul>
287
306
  </slot>
288
307
  </div>
289
308
  <i
290
- v-if="!onlyHasOverview && canCollapse"
309
+ v-if="!onlyHasOverview && canCollapse && hasChildren"
291
310
  class="icon toggle toggle-accordion"
292
311
  :class="{'icon-chevron-right': !isExpanded, 'icon-chevron-down': isExpanded}"
293
312
  role="button"
@@ -377,6 +396,7 @@ export default {
377
396
  display: block;
378
397
  box-sizing:border-box;
379
398
  height: 100%;
399
+
380
400
  &:hover{
381
401
  text-decoration: none;
382
402
  }
@@ -484,6 +504,10 @@ export default {
484
504
  }
485
505
  }
486
506
  }
507
+
508
+ .root-depth :deep() > .child.nav-type a {
509
+ padding-left: 14px;
510
+ }
487
511
  }
488
512
 
489
513
  &.depth-1 {
@@ -189,12 +189,30 @@ export default {
189
189
  (this.currentProduct && this.currentProduct.showWorkspaceSwitcher);
190
190
  // Don't show if the header is in 'simple' mode
191
191
  const notSimple = !this.simple;
192
- // One of these must be enabled, otherwise t here's no component to show
193
- const validFilterSettings = this.currentProduct?.showNamespaceFilter || this.currentProduct?.showWorkspaceSwitcher;
192
+ // One of these must be enabled, otherwise there's no component to show
193
+ const validFilterSettings = this.currentProduct?.showNamespaceFilter || this.showWorkspaceSwitcher;
194
194
 
195
195
  return validClusterOrProduct && notSimple && validFilterSettings;
196
196
  },
197
197
 
198
+ /**
199
+ * The workspace switcher should be disabled on detail, edit and create pages.
200
+ * Only list pages should allow changing the workspace.
201
+ */
202
+ disableWorkspaceSwitcher() {
203
+ // Disable on detail/edit pages (route has an id param)
204
+ if (this.$route?.params?.id) {
205
+ return true;
206
+ }
207
+
208
+ // Disable on create pages (route names end with '-create')
209
+ if (this.$route?.name?.endsWith('-create')) {
210
+ return true;
211
+ }
212
+
213
+ return false;
214
+ },
215
+
198
216
  featureRancherDesktop() {
199
217
  return this.$config.rancherEnv === 'desktop';
200
218
  },
@@ -203,12 +221,6 @@ export default {
203
221
  return !!this.currentCluster?.actions?.apply;
204
222
  },
205
223
 
206
- prod() {
207
- const name = this.rootProduct.name;
208
-
209
- return this.$store.getters['i18n/withFallback'](`product."${ name }"`, null, ucFirst(name));
210
- },
211
-
212
224
  showSearch() {
213
225
  return this.rootProduct?.inStore === 'cluster';
214
226
  },
@@ -239,6 +251,30 @@ export default {
239
251
  isHarvester() {
240
252
  return this.$store.getters['currentProduct'].inStore === HARVESTER;
241
253
  },
254
+
255
+ productLabel() {
256
+ const name = this.rootProduct.name;
257
+
258
+ // single products do their own thing, which is the previous default behavior as per next line
259
+ if (this.isSingleProduct) {
260
+ return this.$store.getters['i18n/withFallback'](`product."${ name }"`, null, ucFirst(name));
261
+ } else {
262
+ if (this.rootProduct?.label) {
263
+ return this.rootProduct.label;
264
+ }
265
+ if (this.rootProduct?.labelKey) {
266
+ return this.$store.getters['i18n/t'](this.rootProduct.labelKey);
267
+ }
268
+
269
+ return this.$store.getters['i18n/withFallback'](`product."${ name }"`, null, ucFirst(name));
270
+ }
271
+ },
272
+
273
+ // Determine if we are on a route that shows the logo instead of the product label
274
+ // This is to enforce the logo display on certain routes like home, about, prefs, account, etc
275
+ isLogoRoute() {
276
+ return !this.$route.name.includes('c-cluster');
277
+ }
242
278
  },
243
279
 
244
280
  watch: {
@@ -518,7 +554,7 @@ export default {
518
554
  :alt="t('branding.logos.label')"
519
555
  >
520
556
  <div class="product-name">
521
- {{ prod }}
557
+ {{ productLabel }}
522
558
  </div>
523
559
  </div>
524
560
  </div>
@@ -534,6 +570,13 @@ export default {
534
570
  {{ t(isSingleProduct.productNameKey) }}
535
571
  </div>
536
572
 
573
+ <div
574
+ v-else-if="productLabel && !isLogoRoute"
575
+ class="product-name"
576
+ >
577
+ {{ productLabel }}
578
+ </div>
579
+
537
580
  <div
538
581
  v-else
539
582
  class="side-menu-logo"
@@ -556,7 +599,10 @@ export default {
556
599
  class="top"
557
600
  >
558
601
  <NamespaceFilter v-if="clusterReady && currentProduct && (currentProduct.showNamespaceFilter || isExplorer)" />
559
- <WorkspaceSwitcher v-else-if="clusterReady && currentProduct && currentProduct.showWorkspaceSwitcher && showWorkspaceSwitcher" />
602
+ <WorkspaceSwitcher
603
+ v-else-if="clusterReady && showWorkspaceSwitcher"
604
+ :disabled="disableWorkspaceSwitcher"
605
+ />
560
606
  </div>
561
607
  <div
562
608
  v-if="currentCluster && !simple"
@@ -246,7 +246,10 @@ const findNewIndex = (shouldAdvance: boolean, activeIndex: number, itemsArr: Ele
246
246
  :class="clz"
247
247
  />
248
248
  </div>
249
- <div class="item-title">
249
+ <div
250
+ v-clean-tooltip="item.title"
251
+ class="item-title"
252
+ >
250
253
  {{ item.title }}
251
254
  </div>
252
255
  <button
@@ -2,17 +2,19 @@
2
2
  import { useStore } from 'vuex';
3
3
  import { computed, inject, ref } from 'vue';
4
4
  import { DropdownContext, defaultContext } from '@components/RcDropdown/types';
5
+ import RcButton from '@components/RcButton/RcButton.vue';
6
+ import { RcButtonType } from '@components/RcButton/types';
5
7
 
6
8
  const { dropdownItems } = inject<DropdownContext>('dropdownContext') || defaultContext;
7
9
  const store = useStore();
8
10
  const unreadCount = computed<number>(() => store.getters['notifications/unreadCount']);
9
- const markAllReadButton = ref<HTMLElement>();
11
+ const markAllReadButton = ref<RcButtonType | null>(null);
10
12
 
11
13
  const markAllRead = (keyboard: boolean) => {
12
14
  store.dispatch('notifications/markAllRead');
13
15
 
14
- // If we have focus, then move to the next item if activated by the keyboard
15
- if (keyboard && document.activeElement === markAllReadButton?.value) {
16
+ // If activated via keyboard, move focus to the next dropdown item
17
+ if (keyboard) {
16
18
  moveFocus(true);
17
19
  }
18
20
  };
@@ -65,18 +67,19 @@ const gotFocus = (e: Event) => {
65
67
  {{ t('notificationCenter.title') }}
66
68
  </div>
67
69
  <div v-if="unreadCount !== 0">
68
- <a
70
+ <RcButton
69
71
  ref="markAllReadButton"
70
- role="button"
72
+ variant="ghost"
73
+ size="small"
71
74
  tabindex="-1"
72
- href="#"
75
+ class="mark-all-read"
73
76
  data-testid="notifications-center-markall-read"
74
77
  @keydown.up.down.stop.prevent="handleKeydown"
75
78
  @keydown.enter.space.stop="markAllRead(true)"
76
79
  @click="markAllRead(false)"
77
80
  >
78
81
  {{ t('notificationCenter.markAllRead') }}
79
- </a>
82
+ </RcButton>
80
83
  </div>
81
84
  </div>
82
85
  <div class="notification-border" />
@@ -104,8 +107,17 @@ const gotFocus = (e: Event) => {
104
107
  flex: 1;
105
108
  }
106
109
 
107
- A {
110
+ .mark-all-read {
111
+ padding: 0;
112
+ min-height: auto;
113
+ font-size: inherit;
114
+ line-height: inherit;
108
115
  color: var(--link);
116
+
117
+ &:hover {
118
+ color: var(--body-text);
119
+ text-decoration: underline;
120
+ }
109
121
  }
110
122
  }
111
123
  }
@@ -0,0 +1,80 @@
1
+ import { ref } from 'vue';
2
+ import { mount, shallowMount } from '@vue/test-utils';
3
+ import NotificationHeader from '@shell/components/nav/NotificationCenter/NotificationHeader.vue';
4
+ import { defaultContext } from '@components/RcDropdown/types';
5
+
6
+ const buildStore = (unreadCount = 1) => {
7
+ const dispatch = jest.fn();
8
+ const store = {
9
+ dispatch,
10
+ getters: { 'notifications/unreadCount': unreadCount },
11
+ };
12
+
13
+ return { store, dispatch };
14
+ };
15
+
16
+ const buildGlobal = (store: any) => ({
17
+ provide: {
18
+ store,
19
+ dropdownContext: { ...defaultContext, dropdownItems: ref<HTMLElement[]>([]) },
20
+ },
21
+ mocks: { $store: store },
22
+ });
23
+
24
+ jest.mock('vuex', () => ({ useStore: () => (globalThis as any).__testStore }));
25
+
26
+ describe('component: NotificationHeader', () => {
27
+ afterEach(() => {
28
+ (globalThis as any).__testStore = undefined;
29
+ });
30
+
31
+ it('renders the mark all read action when there are unread notifications', () => {
32
+ const { store } = buildStore(3);
33
+
34
+ (globalThis as any).__testStore = store;
35
+
36
+ const wrapper = shallowMount(NotificationHeader, { global: buildGlobal(store) });
37
+
38
+ expect(wrapper.find('[data-testid="notifications-center-markall-read"]').exists()).toBe(true);
39
+ });
40
+
41
+ it('hides the mark all read action when there are no unread notifications', () => {
42
+ const { store } = buildStore(0);
43
+
44
+ (globalThis as any).__testStore = store;
45
+
46
+ const wrapper = shallowMount(NotificationHeader, { global: buildGlobal(store) });
47
+
48
+ expect(wrapper.find('[data-testid="notifications-center-markall-read"]').exists()).toBe(false);
49
+ });
50
+
51
+ it('dispatches notifications/markAllRead when clicked', async() => {
52
+ const { store, dispatch } = buildStore(2);
53
+
54
+ (globalThis as any).__testStore = store;
55
+
56
+ const wrapper = mount(NotificationHeader, { global: buildGlobal(store) });
57
+
58
+ await wrapper.find('[data-testid="notifications-center-markall-read"]').trigger('click');
59
+
60
+ expect(dispatch).toHaveBeenCalledWith('notifications/markAllRead');
61
+ });
62
+
63
+ // Regression test for https://github.com/rancher/dashboard/issues/16923
64
+ // "Mark all as read" was originally an <a href="#"> which, on click, navigated
65
+ // to "#" and stripped any existing URL hash fragment (e.g. #pod). Rendering it
66
+ // as a <button> (via RcButton) removes the default navigation behavior entirely,
67
+ // so the URL hash is preserved and extensions scoped via LocationConfig.hash
68
+ // continue to match after activation.
69
+ it('renders mark all read as a <button> so activating it cannot strip the URL hash', () => {
70
+ const { store } = buildStore(2);
71
+
72
+ (globalThis as any).__testStore = store;
73
+
74
+ const wrapper = mount(NotificationHeader, { global: buildGlobal(store) });
75
+ const markAll = wrapper.find('[data-testid="notifications-center-markall-read"]');
76
+
77
+ expect(markAll.element.tagName).toBe('BUTTON');
78
+ expect(markAll.attributes('href')).toBeUndefined();
79
+ });
80
+ });
@@ -186,8 +186,22 @@ export default {
186
186
  to.params.product = p.name;
187
187
  }
188
188
 
189
+ let label;
190
+
191
+ // Allow product to specify its label (old DSL product() did not have "label" or "labelKey")
192
+ // new extensions product registration supports both "label" and "labelKey" (with "labelKey" taking precedence if both are provided)
193
+ if (p.labelKey) {
194
+ label = this.$store.getters['i18n/t'](p.labelKey);
195
+ } else if (p.label) {
196
+ label = p.label;
197
+ }
198
+
199
+ if (!label) {
200
+ label = this.$store.getters['i18n/withFallback'](`product.${ p.name }`, null, ucFirst(p.name));
201
+ }
202
+
189
203
  return {
190
- label: this.$store.getters['i18n/withFallback'](`product."${ p.name }"`, null, ucFirst(p.name)),
204
+ label,
191
205
  icon: `icon-${ p.icon || 'copy' }`,
192
206
  svg: p.svg,
193
207
  value: p.name,