@rancher/shell 3.0.12-rc.1 → 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 (134) 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/translations/en-us.yaml +19 -17
  6. package/assets/translations/zh-hans.yaml +4 -8
  7. package/chart/__tests__/S3.test.ts +10 -3
  8. package/components/CountBox.vue +20 -0
  9. package/components/CreateDriver.vue +0 -12
  10. package/components/DetailText.vue +12 -3
  11. package/components/SelectIconGrid.vue +5 -0
  12. package/components/__tests__/CountBox.test.ts +72 -0
  13. package/components/__tests__/DetailText.test.ts +113 -0
  14. package/components/fleet/FleetClusterTargets/index.vue +18 -1
  15. package/components/form/InputWithSelect.vue +18 -10
  16. package/components/form/KeyValue.vue +17 -1
  17. package/components/form/LabeledSelect.vue +82 -24
  18. package/components/form/Select.vue +73 -56
  19. package/components/form/ServiceNameSelect.vue +13 -11
  20. package/components/form/__tests__/KeyValue.test.ts +66 -0
  21. package/components/form/__tests__/NodeScheduling.test.ts +9 -0
  22. package/components/form/labeled-select-utils/useLabeledSelectPagination.ts +138 -0
  23. package/components/nav/Group.vue +7 -6
  24. package/components/nav/Header.vue +24 -3
  25. package/components/nav/NotificationCenter/Notification.vue +4 -1
  26. package/components/nav/NotificationCenter/NotificationHeader.vue +20 -8
  27. package/components/nav/NotificationCenter/__tests__/NotificationHeader.test.ts +80 -0
  28. package/components/nav/Type.vue +8 -7
  29. package/components/nav/WindowManager/index.vue +2 -1
  30. package/components/nav/WorkspaceSwitcher.vue +13 -0
  31. package/components/nav/__tests__/Group.test.ts +67 -0
  32. package/components/nav/__tests__/Header.test.ts +235 -0
  33. package/components/nav/__tests__/Type.test.ts +20 -3
  34. package/components/templates/default.vue +34 -4
  35. package/components/templates/home.vue +12 -25
  36. package/components/templates/plain.vue +13 -26
  37. package/composables/useLabeledFormElement.ts +10 -2
  38. package/composables/useLabeledSelect.ts +60 -0
  39. package/composables/useUserRetentionValidation.ts +1 -49
  40. package/config/cookies.js +0 -1
  41. package/config/labels-annotations.js +1 -0
  42. package/config/query-params.js +1 -0
  43. package/config/router/routes.js +0 -8
  44. package/core/__tests__/plugin-products.test.ts +616 -25
  45. package/core/plugin-products-base.ts +31 -14
  46. package/core/plugin-products-helpers.ts +5 -4
  47. package/core/plugin-types.ts +18 -3
  48. package/core/types.ts +3 -1
  49. package/detail/__tests__/management.cattle.io.fleetworkspace.test.ts +128 -0
  50. package/detail/management.cattle.io.fleetworkspace.vue +49 -0
  51. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +9 -0
  52. package/edit/__tests__/kontainerDriver.test.ts +0 -13
  53. package/edit/__tests__/nodeDriver.test.ts +5 -11
  54. package/edit/__tests__/resources.cattle.io.restore.test.ts +9 -0
  55. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/General.test.ts.snap +6 -0
  56. package/edit/auth/__tests__/oidc.test.ts +54 -0
  57. package/edit/auth/azuread.vue +1 -1
  58. package/edit/auth/oidc.vue +8 -0
  59. package/edit/kontainerDriver.vue +1 -2
  60. package/edit/nodeDriver.vue +0 -2
  61. package/edit/provisioning.cattle.io.cluster/AgentEnv.vue +1 -0
  62. package/edit/provisioning.cattle.io.cluster/__tests__/AgentEnv.test.ts +25 -0
  63. package/edit/provisioning.cattle.io.cluster/index.vue +70 -99
  64. package/initialize/App.vue +29 -2
  65. package/initialize/install-plugins.js +0 -2
  66. package/list/__tests__/management.cattle.io.feature.test.ts +105 -0
  67. package/list/catalog.cattle.io.app.vue +25 -5
  68. package/list/management.cattle.io.feature.vue +1 -1
  69. package/list/management.cattle.io.fleetworkspace.vue +8 -0
  70. package/machine-config/amazonec2.vue +1 -0
  71. package/mixins/chart.js +40 -9
  72. package/models/__tests__/catalog.cattle.io.app.test.ts +15 -1
  73. package/models/__tests__/catalog.cattle.io.clusterrepo.test.ts +84 -0
  74. package/models/__tests__/chart.test.ts +99 -6
  75. package/models/__tests__/management.cattle.io.feature.test.ts +131 -0
  76. package/models/__tests__/monitoring.coreos.com.alertmanagerconfig.test.ts +98 -0
  77. package/models/catalog.cattle.io.app.js +21 -17
  78. package/models/catalog.cattle.io.clusterrepo.js +39 -11
  79. package/models/chart.js +33 -19
  80. package/models/fleet-application.js +1 -1
  81. package/models/fleet.cattle.io.bundle.js +1 -1
  82. package/models/kontainerdriver.js +11 -0
  83. package/models/management.cattle.io.authconfig.js +5 -1
  84. package/models/management.cattle.io.cluster.js +0 -53
  85. package/models/management.cattle.io.feature.js +3 -3
  86. package/models/management.cattle.io.kontainerdriver.js +1 -26
  87. package/models/monitoring.coreos.com.alertmanagerconfig.js +31 -17
  88. package/models/nodedriver.js +7 -0
  89. package/package.json +13 -12
  90. package/pages/c/_cluster/apps/charts/__tests__/chart.test.ts +189 -0
  91. package/pages/c/_cluster/apps/charts/__tests__/index.test.ts +55 -0
  92. package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +53 -0
  93. package/pages/c/_cluster/apps/charts/chart.vue +217 -33
  94. package/pages/c/_cluster/apps/charts/index.vue +2 -2
  95. package/pages/c/_cluster/apps/charts/install.vue +8 -3
  96. package/pages/c/_cluster/auth/user.retention/index.vue +55 -22
  97. package/pages/c/_cluster/manager/drivers/kontainerDriver/index.vue +5 -7
  98. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +39 -2
  99. package/pages/c/_cluster/uiplugins/__tests__/PluginInfoPanel.test.ts +61 -0
  100. package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +15 -10
  101. package/pages/c/_cluster/uiplugins/index.vue +23 -25
  102. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +205 -1
  103. package/rancher-components/Form/LabeledInput/LabeledInput.vue +82 -4
  104. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +1 -1
  105. package/scripts/test-plugins-build.sh +5 -2
  106. package/server/server-middleware.js +2 -2
  107. package/static/humans.txt +1 -0
  108. package/static/robots.txt +34 -0
  109. package/static/welcome-cow.svg +18 -0
  110. package/store/__tests__/catalog.test.ts +161 -11
  111. package/store/auth.js +0 -3
  112. package/store/catalog.js +60 -8
  113. package/types/shell/index.d.ts +26 -22
  114. package/utils/__tests__/git.test.ts +270 -0
  115. package/utils/__tests__/inactivity.test.ts +316 -0
  116. package/utils/__tests__/object.test.ts +77 -0
  117. package/utils/__tests__/time.test.ts +14 -1
  118. package/utils/__tests__/url.test.ts +246 -0
  119. package/utils/object.js +33 -2
  120. package/utils/time.ts +5 -0
  121. package/vue.config.js +0 -9
  122. package/assets/images/providers/azuread-black.svg +0 -22
  123. package/assets/images/providers/azuread.svg +0 -25
  124. package/assets/images/vendor/azuread.svg +0 -18
  125. package/assets/styles/fonts/_dots.scss +0 -18
  126. package/components/EmberPage.vue +0 -622
  127. package/components/EmberPageView.vue +0 -39
  128. package/components/form/labeled-select-utils/labeled-select-pagination.ts +0 -116
  129. package/mixins/labeled-form-element.ts +0 -225
  130. package/pages/c/_cluster/explorer/tools/pages/_page.vue +0 -28
  131. package/pages/c/_cluster/manager/pages/_page.vue +0 -22
  132. package/pages/c/_cluster/mcapps/pages/_page.vue +0 -22
  133. package/plugins/ember-cookie.js +0 -17
  134. package/utils/ember-page.js +0 -30
@@ -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
+ };
@@ -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
  }
@@ -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
  },
@@ -581,7 +599,10 @@ export default {
581
599
  class="top"
582
600
  >
583
601
  <NamespaceFilter v-if="clusterReady && currentProduct && (currentProduct.showNamespaceFilter || isExplorer)" />
584
- <WorkspaceSwitcher v-else-if="clusterReady && currentProduct && currentProduct.showWorkspaceSwitcher && showWorkspaceSwitcher" />
602
+ <WorkspaceSwitcher
603
+ v-else-if="clusterReady && showWorkspaceSwitcher"
604
+ :disabled="disableWorkspaceSwitcher"
605
+ />
585
606
  </div>
586
607
  <div
587
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
+ });
@@ -68,8 +68,9 @@ export default {
68
68
  },
69
69
 
70
70
  isActive() {
71
- const typeFullPath = this.$router.resolve(this.typeRoute)?.fullPath.toLowerCase();
72
- const pageFullPath = this.$route.fullPath?.toLowerCase().split('#')[0]; // Ignore the shebang when comparing routes
71
+ // Use .path instead of .fullPath to ignore query parameters and hashes when comparing routes
72
+ const typePath = this.$router.resolve(this.typeRoute)?.path.toLowerCase();
73
+ const pagePath = this.$route.path?.toLowerCase();
73
74
  const routeMetaNav = this.$route.meta?.nav;
74
75
 
75
76
  // If the route explicitly declares the nav path that should be highlighted, then use that
@@ -80,14 +81,14 @@ export default {
80
81
  .replace(':cluster', cluster)
81
82
  .replace(':product', product);
82
83
 
83
- if (navPath === typeFullPath) {
84
+ if (navPath === typePath) {
84
85
  return true;
85
86
  }
86
87
  }
87
88
 
88
89
  if ( !this.type.exact) {
89
- const typeSplit = typeFullPath.split('/');
90
- const pageSplit = pageFullPath.split('/');
90
+ const typeSplit = typePath.split('/');
91
+ const pageSplit = pagePath.split('/');
91
92
 
92
93
  for (let index = 0; index < typeSplit.length; ++index) {
93
94
  if ( index >= pageSplit.length || typeSplit[index] !== pageSplit[index] ) {
@@ -98,7 +99,7 @@ export default {
98
99
  return true;
99
100
  }
100
101
 
101
- return typeFullPath === pageFullPath;
102
+ return typePath === pagePath;
102
103
  },
103
104
 
104
105
  typeRoute() {
@@ -131,7 +132,7 @@ export default {
131
132
  <router-link
132
133
  v-if="type.route"
133
134
  :key="type.name"
134
- v-slot="{ href, navigate,isExactActive }"
135
+ v-slot="{ href, navigate, isExactActive }"
135
136
  custom
136
137
  :to="typeRoute"
137
138
  >
@@ -36,7 +36,7 @@ const props = defineProps({
36
36
 
37
37
  const { loadComponent } = useComponentsMount();
38
38
 
39
- const { isPanelEnabled } = usePanelsHandler({ layout: props.layout, positions: props.positions });
39
+ const { isPanelEnabled } = usePanelsHandler(props);
40
40
  const { tabs } = useTabsHandler();
41
41
  </script>
42
42
 
@@ -66,6 +66,7 @@ const { tabs } = useTabsHandler();
66
66
  :active="true"
67
67
  :height="tab.containerHeight"
68
68
  :width="tab.containerWidth"
69
+ :layout="layout"
69
70
  v-bind="tab.attrs"
70
71
  />
71
72
  </keep-alive>
@@ -8,6 +8,13 @@ export default {
8
8
  name: 'WorkspaceSwitcher',
9
9
  components: { Select },
10
10
 
11
+ props: {
12
+ disabled: {
13
+ type: Boolean,
14
+ default: false,
15
+ },
16
+ },
17
+
11
18
  computed: {
12
19
  ...mapState(['allWorkspaces', 'workspace', 'allNamespaces', 'defaultNamespace', 'getActiveNamespaces']),
13
20
 
@@ -94,6 +101,7 @@ export default {
94
101
  label="label"
95
102
  :options="options"
96
103
  :clearable="false"
104
+ :disabled="disabled"
97
105
  :reduce="(opt) => opt.value"
98
106
  />
99
107
  <!--button v-shortkey.once="['w']" class="hide" @shortkey="focus()" /-->
@@ -187,4 +195,9 @@ export default {
187
195
  .filter :deep() .unlabeled-select INPUT[type='search'] {
188
196
  padding: 7px;
189
197
  }
198
+
199
+ .filter :deep() .unlabeled-select.disabled {
200
+ opacity: 0.5;
201
+ cursor: not-allowed;
202
+ }
190
203
  </style>
@@ -0,0 +1,67 @@
1
+ import { shallowMount } from '@vue/test-utils';
2
+ import Group from '@shell/components/nav/Group.vue';
3
+
4
+ describe('component: Group', () => {
5
+ it('isOverview ignores query parameters and hash strings when checking active state', () => {
6
+ const group = {
7
+ name: 'test',
8
+ children: [
9
+ {
10
+ route: { name: 'overview-route' },
11
+ overview: true
12
+ }
13
+ ]
14
+ };
15
+
16
+ const wrapper = shallowMount(Group as any, {
17
+ props: {
18
+ group, canCollapse: true, idPrefix: ''
19
+ },
20
+ global: {
21
+ mocks: {
22
+ $route: { path: '/test/route', fullPath: '/test/route?query=val#hash' },
23
+ $router: {
24
+ resolve: jest.fn().mockReturnValue({ path: '/test/route', fullPath: '/test/route' }),
25
+ getRoutes: jest.fn().mockReturnValue([])
26
+ },
27
+ t: (key: string) => key
28
+ }
29
+ }
30
+ });
31
+
32
+ expect((wrapper.vm as any).isOverview).toBe(true);
33
+ });
34
+
35
+ it('hasActiveRoute ignores query parameters when checking item paths', () => {
36
+ const group = {
37
+ name: 'test',
38
+ children: [
39
+ { route: { name: 'child-route', params: {} } }
40
+ ]
41
+ };
42
+
43
+ const wrapper = shallowMount(Group as any, {
44
+ props: {
45
+ group, canCollapse: true, idPrefix: ''
46
+ },
47
+ global: {
48
+ mocks: {
49
+ $route: {
50
+ params: {},
51
+ hash: '#hash',
52
+ path: '/child/route',
53
+ fullPath: '/child/route?query=val#hash',
54
+ matched: []
55
+ },
56
+ $router: {
57
+ resolve: jest.fn().mockReturnValue({ path: '/child/route', fullPath: '/child/route' }),
58
+ getRoutes: jest.fn().mockReturnValue([])
59
+ },
60
+ t: (key: string) => key
61
+ }
62
+ }
63
+ });
64
+
65
+ expect((wrapper.vm as any).hasActiveRoute()).toBe(true);
66
+ });
67
+ });