@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
@@ -2,7 +2,7 @@ import { IExtension } from '@shell/core/types';
2
2
  import {
3
3
  ProductChild, ProductMetadata,
4
4
  ConfigureTypeConfiguration, VirtualTypeConfiguration,
5
- ProductChildCustomPage
5
+ ProductChildCustomPage, VueRouteComponent, OverviewPageRoutingMetadata
6
6
  } from '@shell/core/plugin-types';
7
7
  import EmptyProductPage from '@shell/components/EmptyProductPage.vue';
8
8
  import pluginProductsHelpers from '@shell/core/plugin-products-helpers';
@@ -25,6 +25,8 @@ export abstract class BasePluginProduct {
25
25
 
26
26
  protected addedResourceRoutes = false;
27
27
 
28
+ protected registeredPageNames: Set<string> = new Set();
29
+
28
30
  protected DSLMethods: any;
29
31
 
30
32
  protected config: ProductChild[];
@@ -68,6 +70,13 @@ export abstract class BasePluginProduct {
68
70
  }
69
71
  }
70
72
 
73
+ /**
74
+ * Generates data for group overview page routing
75
+ */
76
+ protected generateMetadataForGroupOverviewPageRouting(name: string, component: VueRouteComponent): OverviewPageRoutingMetadata {
77
+ return { name, component };
78
+ }
79
+
71
80
  /**
72
81
  * This is where we register the product and its children via the DSL
73
82
  */
@@ -198,16 +207,12 @@ export abstract class BasePluginProduct {
198
207
  defaultRoute = pluginProductsHelpers.generateVirtualTypeRoute(this.name, entryChild, { omitPath: true, extendProduct: !this.isNewProduct });
199
208
  }
200
209
  } else {
201
- // Group with component - route to the group page itself
202
- defaultRoute = pluginProductsHelpers.generateVirtualTypeRoute(this.name, undefined, {
203
- omitPath: true, component: firstConfig.component, extendProduct: !this.isNewProduct
204
- });
210
+ // Group with component - route to the group overview page (which will render the group's component and side-menu)
211
+ defaultRoute = pluginProductsHelpers.generateVirtualTypeRoute(this.name, this.generateMetadataForGroupOverviewPageRouting(firstConfig.name, firstConfig.component), { omitPath: true, extendProduct: !this.isNewProduct });
205
212
  }
206
213
  } else if (firstConfig.component) {
207
214
  // Group with component but no children - route to the group page itself
208
- defaultRoute = pluginProductsHelpers.generateVirtualTypeRoute(this.name, undefined, {
209
- omitPath: true, component: firstConfig.component, extendProduct: !this.isNewProduct
210
- });
215
+ defaultRoute = pluginProductsHelpers.generateVirtualTypeRoute(this.name, this.generateMetadataForGroupOverviewPageRouting(firstConfig.name, firstConfig.component), { omitPath: true, extendProduct: !this.isNewProduct });
211
216
  }
212
217
  } else if (isProductChildWithType(firstConfig)) {
213
218
  // Simple configureType page (resource page)
@@ -248,6 +253,13 @@ export abstract class BasePluginProduct {
248
253
  const name = `${ parentName }-${ item.name }`;
249
254
  const finalName = groupNaming ? `${ parentName }-${ groupNaming }-${ item.name }` : name;
250
255
 
256
+ // Check for duplicate page names within the same product
257
+ if (this.registeredPageNames.has(finalName)) {
258
+ this.surfaceError(`Duplicate page name "${ item.name }" - each page must have a unique name within a product`);
259
+ }
260
+
261
+ this.registeredPageNames.add(finalName);
262
+
251
263
  const virtualTypeConfig: VirtualTypeConfiguration = {
252
264
  label: item.label,
253
265
  labelKey: item.labelKey,
@@ -261,7 +273,8 @@ export abstract class BasePluginProduct {
261
273
  if (isProductChildGroup(item)) {
262
274
  virtualTypeConfig.exact = true;
263
275
  virtualTypeConfig.overview = true;
264
- virtualTypeConfig.route = pluginProductsHelpers.generateVirtualTypeRoute(parentName, undefined, { extendProduct: !this.isNewProduct });
276
+ // Pass group metadata as pageChild so the route gets a unique path segment (e.g. /product/c/:cluster/groupName)
277
+ virtualTypeConfig.route = pluginProductsHelpers.generateVirtualTypeRoute(parentName, this.generateMetadataForGroupOverviewPageRouting(item.name, item.component as ProductChildCustomPage['component']), { extendProduct: !this.isNewProduct });
265
278
  } else {
266
279
  virtualTypeConfig.route = pluginProductsHelpers.generateVirtualTypeRoute(parentName, item, { extendProduct: !this.isNewProduct });
267
280
  }
@@ -270,6 +283,14 @@ export abstract class BasePluginProduct {
270
283
  } else if (isProductChildWithType(item)) {
271
284
  // Page with a "type" specified maps to a configureType
272
285
  const typeValue = item.type;
286
+
287
+ // Check for duplicate resource type within the same product
288
+ if (this.registeredPageNames.has(typeValue)) {
289
+ this.surfaceError(`Duplicate resource type "${ typeValue }" - each resource type must be unique within a product`);
290
+ }
291
+
292
+ this.registeredPageNames.add(typeValue);
293
+
273
294
  const route = pluginProductsHelpers.generateConfigureTypeRoute(parentName, item, { extendProduct: !this.isNewProduct });
274
295
 
275
296
  const configureTypeConfig: ConfigureTypeConfiguration = {
@@ -300,10 +321,6 @@ export abstract class BasePluginProduct {
300
321
  this.surfaceError('Group items cannot have a "type" property - only custom pages can have groups.');
301
322
  }
302
323
 
303
- if (child.component && !this.isNewProduct) {
304
- this.surfaceError('When extending an existing product, group parent items cannot have a component because of route matching conflicts.');
305
- }
306
-
307
324
  let route;
308
325
 
309
326
  if (!child.component) {
@@ -316,7 +333,7 @@ export abstract class BasePluginProduct {
316
333
 
317
334
  route = pluginProductsHelpers.generateVirtualTypeRoute(parentName, pageForRoute, { extendProduct: !this.isNewProduct });
318
335
  } else {
319
- route = pluginProductsHelpers.generateVirtualTypeRoute(parentName, undefined, { component: child.component, extendProduct: !this.isNewProduct });
336
+ route = pluginProductsHelpers.generateVirtualTypeRoute(parentName, this.generateMetadataForGroupOverviewPageRouting(child.name, child.component), { component: child.component, extendProduct: !this.isNewProduct });
320
337
  }
321
338
 
322
339
  // add the route for the group page/parent
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  RouteRecordRawWithParams, ProductChildGroup, ProductChild,
3
- ProductChildCustomPage, ProductChildResourcePage, ProductRegistrationRouteGenerationOptions
3
+ ProductChildCustomPage, ProductChildResourcePage, ProductRegistrationRouteGenerationOptions,
4
+ OverviewPageRoutingMetadata
4
5
  } from '@shell/core/plugin-types';
5
6
  import { BLANK_CLUSTER } from '@shell/store/store-types';
6
7
 
@@ -62,7 +63,7 @@ class PluginProductsHelpers {
62
63
  }
63
64
 
64
65
  // VIRTUAL TYPE ROUTES
65
- generateVirtualTypeRoute(parentName: string, pageChild: ProductChildCustomPage | undefined, options: ProductRegistrationRouteGenerationOptions = {}): RouteRecordRawWithParams {
66
+ generateVirtualTypeRoute(parentName: string, pageChild: ProductChildCustomPage | OverviewPageRoutingMetadata | undefined, options: ProductRegistrationRouteGenerationOptions = {}): RouteRecordRawWithParams {
66
67
  if (options.extendProduct) {
67
68
  return this.generateVirtualTypeRouteForExistingProduct(parentName, pageChild, options);
68
69
  } else {
@@ -71,7 +72,7 @@ class PluginProductsHelpers {
71
72
  }
72
73
 
73
74
  // VIRTUAL TYPE ROUTES - CLUSTER LEVEL EXTENSION
74
- private generateVirtualTypeRouteForExistingProduct(parentName: string, pageChild: ProductChildCustomPage | undefined, options: ProductRegistrationRouteGenerationOptions = {}): RouteRecordRawWithParams {
75
+ private generateVirtualTypeRouteForExistingProduct(parentName: string, pageChild: ProductChildCustomPage | OverviewPageRoutingMetadata | undefined, options: ProductRegistrationRouteGenerationOptions = {}): RouteRecordRawWithParams {
75
76
  const { component, omitPath } = options;
76
77
  const name = pageChild ? `c-cluster-${ parentName }-${ pageChild.name }` : `c-cluster-${ parentName }`;
77
78
  const path = pageChild ? `c/:cluster/${ parentName }/${ pageChild.name }` : `c/:cluster/${ parentName }`;
@@ -95,7 +96,7 @@ class PluginProductsHelpers {
95
96
  }
96
97
 
97
98
  // VIRTUAL TYPE ROUTES - TOP LEVEL EXTENSION
98
- private generateVirtualTypeRouteForNewProduct(parentName: string, pageChild: ProductChildCustomPage | undefined, options: ProductRegistrationRouteGenerationOptions = {}): RouteRecordRawWithParams {
99
+ private generateVirtualTypeRouteForNewProduct(parentName: string, pageChild: ProductChildCustomPage | OverviewPageRoutingMetadata | undefined, options: ProductRegistrationRouteGenerationOptions = {}): RouteRecordRawWithParams {
99
100
  const { component, omitPath } = options;
100
101
  const name = pageChild ? `${ parentName }-c-cluster-${ pageChild.name }` : `${ parentName }-c-cluster`;
101
102
  const path = pageChild ? `${ parentName }/c/:cluster/${ pageChild.name }` : `${ parentName }/c/:cluster`;
@@ -150,12 +150,27 @@ export type ConfigureTypeConfiguration = {
150
150
  // ]
151
151
  }
152
152
 
153
+ /**
154
+ * Represents a Vue component or an async function that resolves to a Vue component, used for route components in product configuration
155
+ */
156
+ export type VueRouteComponent = RouteComponent | Async<RouteComponent>;
157
+
158
+ /**
159
+ * Metadata for route generation to a product overview page
160
+ */
161
+ export type OverviewPageRoutingMetadata = {
162
+ /** Name of the overview page */
163
+ name: string;
164
+ /** Component to render for the overview page */
165
+ component: VueRouteComponent;
166
+ }
167
+
153
168
  /**
154
169
  * Represents a custom page with a component
155
170
  */
156
171
  export type ProductChildCustomPage = ProductChildMetadata & {
157
172
  /** Component to render for this custom page */
158
- component: RouteComponent | Async<RouteComponent>;
173
+ component: VueRouteComponent;
159
174
  /** Optional configuration for the page */
160
175
  config?: VirtualTypeConfiguration;
161
176
  };
@@ -189,7 +204,7 @@ export type ProductChild = ProductChildGroup | ProductChildPage; // eslint-disab
189
204
  * Represents a group of child pages in a product configuration
190
205
  */
191
206
  export type ProductChildGroup = ProductChildMetadata & {
192
- component?: RouteComponent | Async<RouteComponent>;
207
+ component?: VueRouteComponent;
193
208
  children: ProductChild[];
194
209
  /** Default child to navigate to */
195
210
  default?: string;
@@ -218,5 +233,5 @@ export type ProductMetadata = Omit<ProductOptions, 'name' | 'label' | 'labelKey'
218
233
  */
219
234
  export type ProductSinglePage = ProductMetadata & {
220
235
  /** Component to render for this product (single page product) */
221
- component: RouteComponent | Async<RouteComponent>;
236
+ component: VueRouteComponent;
222
237
  };
package/core/types.ts CHANGED
@@ -5,7 +5,7 @@ import { PaginationSettingsStores } from '@shell/types/resources/settings';
5
5
  import type {
6
6
  ProductMetadata, ProductSinglePage,
7
7
  StandardProductName, RouteRecordRawWithParams, ProductChildGroup,
8
- ProductChildPage
8
+ ProductChildPage, ProductChild
9
9
  } from './plugin-types';
10
10
 
11
11
  // Cluster Provisioning types
@@ -665,6 +665,7 @@ export interface IExtension {
665
665
  */
666
666
  addProduct(product: ProductMetadata, config: ProductChildGroup[]): void;
667
667
  addProduct(product: ProductMetadata, config: ProductChildPage[]): void;
668
+ addProduct(product: ProductMetadata, config: ProductChild[]): void;
668
669
 
669
670
  /**
670
671
  * Add a product to the sidebar, without children (no side menu, single page only)
@@ -695,6 +696,7 @@ export interface IExtension {
695
696
  */
696
697
  extendProduct(product: StandardProductName | string, config: ProductChildGroup[]): void;
697
698
  extendProduct(product: StandardProductName | string, config: ProductChildPage[]): void;
699
+ extendProduct(product: StandardProductName | string, config: ProductChild[]): void;
698
700
 
699
701
  /**
700
702
  * Add a locale to the i18n store
@@ -0,0 +1,128 @@
1
+ import { shallowMount } from '@vue/test-utils';
2
+ import DetailWorkspace from '@shell/detail/management.cattle.io.fleetworkspace.vue';
3
+ import { FLEET } from '@shell/config/types';
4
+ import { NAME as FLEET_NAME } from '@shell/config/product/fleet';
5
+ import { BLANK_CLUSTER } from '@shell/store/store-types.js';
6
+
7
+ describe('component: DetailWorkspace', () => {
8
+ const mockValue = {
9
+ id: 'fleet-default',
10
+ counts: {
11
+ gitRepos: 3,
12
+ helmOps: 2,
13
+ clusters: 5,
14
+ cluster: 5,
15
+ clusterGroup: 1,
16
+ clusterGroups: 1,
17
+ },
18
+ };
19
+
20
+ const mockRouter = { push: jest.fn() };
21
+
22
+ const defaultStore = {
23
+ commit: jest.fn(),
24
+ dispatch: jest.fn(),
25
+ getters: {
26
+ 'i18n/t': (key: string) => key,
27
+ 'i18n/exists': () => true,
28
+ currentProduct: { name: FLEET_NAME },
29
+ },
30
+ };
31
+
32
+ const createWrapper = (props = {}) => {
33
+ return shallowMount(DetailWorkspace, {
34
+ props: { value: mockValue, ...props },
35
+ global: {
36
+ mocks: {
37
+ $store: defaultStore,
38
+ $route: { params: {} },
39
+ $router: mockRouter,
40
+ },
41
+ stubs: {
42
+ CountBox: { template: '<div />', props: ['clickable', 'count', 'name', 'primaryColorVar'] },
43
+ ResourceTabs: { template: '<div />' },
44
+ },
45
+ },
46
+ });
47
+ };
48
+
49
+ beforeEach(() => {
50
+ jest.clearAllMocks();
51
+ });
52
+
53
+ describe('applicationRoute', () => {
54
+ it('should return the fleet application route', () => {
55
+ const wrapper = createWrapper();
56
+
57
+ expect(wrapper.vm.applicationRoute).toStrictEqual({
58
+ name: 'c-cluster-fleet-application',
59
+ params: { cluster: BLANK_CLUSTER },
60
+ });
61
+ });
62
+ });
63
+
64
+ describe('clustersRoute', () => {
65
+ it('should return the fleet clusters list route', () => {
66
+ const wrapper = createWrapper();
67
+
68
+ expect(wrapper.vm.clustersRoute).toStrictEqual({
69
+ name: 'c-cluster-product-resource',
70
+ params: {
71
+ cluster: BLANK_CLUSTER,
72
+ product: FLEET_NAME,
73
+ resource: FLEET.CLUSTER,
74
+ },
75
+ });
76
+ });
77
+ });
78
+
79
+ describe('clusterGroupsRoute', () => {
80
+ it('should return the fleet cluster groups list route', () => {
81
+ const wrapper = createWrapper();
82
+
83
+ expect(wrapper.vm.clusterGroupsRoute).toStrictEqual({
84
+ name: 'c-cluster-product-resource',
85
+ params: {
86
+ cluster: BLANK_CLUSTER,
87
+ product: FLEET_NAME,
88
+ resource: FLEET.CLUSTER_GROUP,
89
+ },
90
+ });
91
+ });
92
+ });
93
+
94
+ describe('setWorkspaceAndNavigate', () => {
95
+ it('should commit updateWorkspace with the workspace id', () => {
96
+ const wrapper = createWrapper();
97
+ const route = wrapper.vm.applicationRoute;
98
+
99
+ wrapper.vm.setWorkspaceAndNavigate(route);
100
+
101
+ expect(defaultStore.commit).toHaveBeenCalledWith('updateWorkspace', {
102
+ value: 'fleet-default',
103
+ getters: defaultStore.getters,
104
+ });
105
+ });
106
+
107
+ it('should dispatch prefs/set with the workspace id', () => {
108
+ const wrapper = createWrapper();
109
+ const route = wrapper.vm.applicationRoute;
110
+
111
+ wrapper.vm.setWorkspaceAndNavigate(route);
112
+
113
+ expect(defaultStore.dispatch).toHaveBeenCalledWith('prefs/set', {
114
+ key: expect.any(String),
115
+ value: 'fleet-default',
116
+ });
117
+ });
118
+
119
+ it('should navigate to the given route', () => {
120
+ const wrapper = createWrapper();
121
+ const route = wrapper.vm.clustersRoute;
122
+
123
+ wrapper.vm.setWorkspaceAndNavigate(route);
124
+
125
+ expect(mockRouter.push).toHaveBeenCalledWith(route);
126
+ });
127
+ });
128
+ });
@@ -4,6 +4,8 @@ import ResourceTabs from '@shell/components/form/ResourceTabs';
4
4
  import { SCOPE_NAMESPACE, SCOPE_CLUSTER } from '@shell/components/RoleBindings.vue';
5
5
  import { NAME as FLEET_NAME } from '@shell/config/product/fleet';
6
6
  import { FLEET } from '@shell/config/types';
7
+ import { BLANK_CLUSTER } from '@shell/store/store-types.js';
8
+ import { WORKSPACE } from '@shell/store/prefs';
7
9
 
8
10
  export default {
9
11
  name: 'DetailWorkspace',
@@ -34,6 +36,35 @@ export default {
34
36
  return this.t(`typeLabel."${ FLEET.HELM_OP }"`, { count: this.value.counts.helmOps });
35
37
  },
36
38
 
39
+ applicationRoute() {
40
+ return {
41
+ name: 'c-cluster-fleet-application',
42
+ params: { cluster: BLANK_CLUSTER }
43
+ };
44
+ },
45
+
46
+ clustersRoute() {
47
+ return {
48
+ name: 'c-cluster-product-resource',
49
+ params: {
50
+ cluster: BLANK_CLUSTER,
51
+ product: FLEET_NAME,
52
+ resource: FLEET.CLUSTER,
53
+ }
54
+ };
55
+ },
56
+
57
+ clusterGroupsRoute() {
58
+ return {
59
+ name: 'c-cluster-product-resource',
60
+ params: {
61
+ cluster: BLANK_CLUSTER,
62
+ product: FLEET_NAME,
63
+ resource: FLEET.CLUSTER_GROUP,
64
+ }
65
+ };
66
+ },
67
+
37
68
  SCOPE_NAMESPACE() {
38
69
  return SCOPE_NAMESPACE;
39
70
  },
@@ -46,6 +77,16 @@ export default {
46
77
  return FLEET_NAME;
47
78
  }
48
79
  },
80
+
81
+ methods: {
82
+ setWorkspaceAndNavigate(route) {
83
+ const workspaceId = this.value.id;
84
+
85
+ this.$store.commit('updateWorkspace', { value: workspaceId, getters: this.$store.getters });
86
+ this.$store.dispatch('prefs/set', { key: WORKSPACE, value: workspaceId });
87
+ this.$router.push(route);
88
+ }
89
+ }
49
90
  };
50
91
  </script>
51
92
 
@@ -58,6 +99,8 @@ export default {
58
99
  :count="value.counts.gitRepos"
59
100
  :name="gitRepoLabel"
60
101
  :primary-color-var="'--sizzle-3'"
102
+ :clickable="true"
103
+ @click="setWorkspaceAndNavigate(applicationRoute)"
61
104
  />
62
105
  </div>
63
106
  <div class="col span-3">
@@ -65,6 +108,8 @@ export default {
65
108
  :count="value.counts.helmOps"
66
109
  :name="helmOpsLabel"
67
110
  :primary-color-var="'--sizzle-3'"
111
+ :clickable="true"
112
+ @click="setWorkspaceAndNavigate(applicationRoute)"
68
113
  />
69
114
  </div>
70
115
  <div class="col span-3">
@@ -72,6 +117,8 @@ export default {
72
117
  :count="value.counts.clusters"
73
118
  :name="clustersLabel"
74
119
  :primary-color-var="'--sizzle-1'"
120
+ :clickable="true"
121
+ @click="setWorkspaceAndNavigate(clustersRoute)"
75
122
  />
76
123
  </div>
77
124
  <div class="col span-3">
@@ -79,6 +126,8 @@ export default {
79
126
  :count="value.counts.clusterGroups"
80
127
  :name="clusterGroupsLabel"
81
128
  :primary-color-var="'--sizzle-2'"
129
+ :clickable="true"
130
+ @click="setWorkspaceAndNavigate(clusterGroupsRoute)"
82
131
  />
83
132
  </div>
84
133
  </div>
@@ -4,6 +4,7 @@ import HelmOp from '@shell/models/fleet.cattle.io.helmop';
4
4
  import HelmOpComponent from '@shell/edit/fleet.cattle.io.helmop.vue';
5
5
  import FleetSecretSelector from '@shell/components/fleet/FleetSecretSelector.vue';
6
6
  import FleetConfigMapSelector from '@shell/components/fleet/FleetConfigMapSelector.vue';
7
+ import { createStore } from 'vuex';
7
8
 
8
9
  const mockStore = {
9
10
  dispatch: jest.fn(),
@@ -85,6 +86,14 @@ const initHelmOp = (props: any, options = {}) => {
85
86
  value,
86
87
  ...props
87
88
  },
89
+ provide: {
90
+ store: createStore({
91
+ getters: {
92
+ currentStore: () => 'current_store',
93
+ 'management/paginationEnabled': () => () => false
94
+ }
95
+ })
96
+ },
88
97
  computed: mockComputed,
89
98
  global: { mocks },
90
99
  };
@@ -30,7 +30,6 @@ describe('view: kontainerdriver should', () => {
30
30
  active: true,
31
31
  checksum: '',
32
32
  url: '',
33
- uiUrl: '',
34
33
  whitelistDomains: []
35
34
  }
36
35
  },
@@ -65,32 +64,22 @@ describe('view: kontainerdriver should', () => {
65
64
 
66
65
  it('have "Create" button enabled and disabled depending on validation results', async() => {
67
66
  const urlField = wrapper.find('[data-testid="driver-create-url-field"]');
68
- const uiurlField = wrapper.find('[data-testid="driver-create-uiurl-field"]');
69
67
  const checksumField = wrapper.find('[data-testid="driver-create-checksum-field"]');
70
68
  const saveButton = wrapper.find('[data-testid="kontainer-driver-edit-save"]').element as HTMLInputElement;
71
69
 
72
70
  const testCases = [
73
71
  {
74
72
  url: '1111',
75
- uiurl: 'http://test.com',
76
73
  checksum: 'aaaaaBBBBdddd',
77
74
  result: true
78
75
  },
79
76
  {
80
77
  url: 'http://test.com',
81
- uiurl: '1111',
82
- checksum: 'aaaaaBBBBdddd',
83
- result: true
84
- },
85
- {
86
- url: 'http://test.com',
87
- uiurl: 'http://test.com',
88
78
  checksum: '!!!',
89
79
  result: true
90
80
  },
91
81
  {
92
82
  url: 'http://test.com',
93
- uiurl: 'http://test.com',
94
83
  checksum: 'aaaaaBBBBdddd',
95
84
  result: false
96
85
  }
@@ -99,8 +88,6 @@ describe('view: kontainerdriver should', () => {
99
88
  for (const testCase of testCases) {
100
89
  urlField.setValue(testCase.url);
101
90
  await nextTick();
102
- uiurlField.setValue(testCase.uiurl);
103
- await nextTick();
104
91
  checksumField.setValue(testCase.checksum);
105
92
  await nextTick();
106
93
 
@@ -31,7 +31,6 @@ describe('view: nodedriver should', () => {
31
31
  active: true,
32
32
  checksum: '',
33
33
  url: '',
34
- uiUrl: '',
35
34
  whitelistDomains: []
36
35
  }
37
36
  },
@@ -65,21 +64,16 @@ describe('view: nodedriver should', () => {
65
64
  });
66
65
 
67
66
  it.each`
68
- url | uiurl | checksum | expected
69
- ${ '1111' } | ${ 'http://test.com' } | ${ 'aaaaaBBBBdddd' } | ${ true }
70
- ${ 'http://test.com' } | ${ '1111' } | ${ 'aaaaaBBBBdddd' } | ${ true }
71
- ${ 'http://test.com' } | ${ 'http://test.com' } | ${ '!!!' } | ${ true }
72
- ${ 'http://test.com' } | ${ 'http://test.com' } | ${ 'aaaaaBBBBdddd' } | ${ false }
73
- `('have "Create" button enabled and disabled depending on validation results', async({
74
- url, uiurl, checksum, expected
75
- }) => {
67
+ url | checksum | expected
68
+ ${ '1111' } | ${ 'aaaaaBBBBdddd' } | ${ true }
69
+ ${ 'http://test.com' } | ${ '!!!' } | ${ true }
70
+ ${ 'http://test.com' } | ${ 'aaaaaBBBBdddd' } | ${ false }
71
+ `('have "Create" button enabled and disabled depending on validation results', async({ url, checksum, expected }) => {
76
72
  const urlField = wrapper.find('[data-testid="driver-create-url-field"]');
77
- const uiurlField = wrapper.find('[data-testid="driver-create-uiurl-field"]');
78
73
  const checksumField = wrapper.find('[data-testid="driver-create-checksum-field"]');
79
74
  const saveButton = wrapper.find('[data-testid="node-driver-edit-save"]').element as HTMLInputElement;
80
75
 
81
76
  urlField.setValue(url);
82
- uiurlField.setValue(uiurl);
83
77
  checksumField.setValue(checksum);
84
78
 
85
79
  await nextTick();
@@ -2,6 +2,7 @@ import { nextTick } from 'vue';
2
2
  import { mount } from '@vue/test-utils';
3
3
  import RestoreComponent from '@shell/edit/resources.cattle.io.restore.vue';
4
4
  import { _CREATE } from '@shell/config/query-params';
5
+ import { createStore } from 'vuex';
5
6
 
6
7
  describe('view: restore storage source switching', () => {
7
8
  let wrapper: any;
@@ -30,6 +31,14 @@ describe('view: restore storage source switching', () => {
30
31
  };
31
32
 
32
33
  return mount(RestoreComponent, {
34
+ provide: {
35
+ store: createStore({
36
+ getters: {
37
+ currentStore: () => 'current_store',
38
+ 'cluster/paginationEnabled': () => () => false
39
+ }
40
+ })
41
+ },
33
42
  global: {
34
43
  mocks: {
35
44
  $store: mockStore,
@@ -65,7 +65,9 @@ exports[`component: General rendering & initial state should render with default
65
65
  clearable="false"
66
66
  closeonselect="true"
67
67
  disabled="false"
68
+ filterable="true"
68
69
  hovertooltip="true"
70
+ instore="cluster"
69
71
  label="auditPolicy.general.verbosity.level.label"
70
72
  loading="false"
71
73
  localizedlabel="false"
@@ -73,8 +75,12 @@ exports[`component: General rendering & initial state should render with default
73
75
  nooptionslabelkey="labelSelect.noOptions.empty"
74
76
  optionlabel="label"
75
77
  options="[object Object],[object Object],[object Object],[object Object]"
78
+ placeholder=""
76
79
  reduce="[Function]"
77
80
  required="false"
81
+ requiredirty="true"
82
+ rules=""
83
+ searchable="false"
78
84
  selectable="[Function]"
79
85
  value="0"
80
86
  />
@@ -297,5 +297,59 @@ describe('oidc.vue', () => {
297
297
  expect(groupsClaim.exists()).toBe(false);
298
298
  expect(emailClaim.exists()).toBe(false);
299
299
  });
300
+
301
+ describe('clientAuthenticatedSearch checkbox', () => {
302
+ it('is not rendered for genericoidc', async() => {
303
+ const checkbox = wrapper.find('[data-testid="input-client-authenticated-group-search"]');
304
+
305
+ expect(checkbox.exists()).toBe(false);
306
+ });
307
+
308
+ it('is not rendered for cognito', async() => {
309
+ await wrapper.setData({ model: { ...mockModel, id: 'cognito' } });
310
+
311
+ const checkbox = wrapper.find('[data-testid="input-client-authenticated-group-search"]');
312
+
313
+ expect(checkbox.exists()).toBe(false);
314
+ });
315
+
316
+ it('is rendered for keycloakoidc', async() => {
317
+ await wrapper.setData({ model: { ...mockModel, id: 'keycloakoidc' } });
318
+
319
+ const checkbox = wrapper.find('[data-testid="input-client-authenticated-group-search"]');
320
+
321
+ expect(checkbox.exists()).toBe(true);
322
+ });
323
+
324
+ it('defaults to falsy when not set on keycloakoidc', async() => {
325
+ await wrapper.setData({ model: { ...mockModel, id: 'keycloakoidc' } });
326
+
327
+ expect(wrapper.vm.model.clientAuthenticatedSearch).toBeFalsy();
328
+ });
329
+
330
+ it('updates model when checkbox is clicked', async() => {
331
+ await wrapper.setData({
332
+ model: {
333
+ ...mockModel, id: 'keycloakoidc', clientAuthenticatedSearch: false
334
+ }
335
+ });
336
+
337
+ const checkbox = wrapper.getComponent('[data-testid="input-client-authenticated-group-search"]');
338
+
339
+ await checkbox.find('[role="checkbox"]').trigger('click');
340
+
341
+ expect(wrapper.vm.model.clientAuthenticatedSearch).toBe(true);
342
+ });
343
+
344
+ it('reflects a pre-existing true value from the model', async() => {
345
+ await wrapper.setData({
346
+ model: {
347
+ ...mockModel, id: 'keycloakoidc', clientAuthenticatedSearch: true
348
+ }
349
+ });
350
+
351
+ expect(wrapper.vm.model.clientAuthenticatedSearch).toBe(true);
352
+ });
353
+ });
300
354
  });
301
355
  });
@@ -144,7 +144,7 @@ export default {
144
144
  config: {
145
145
  ...this.model,
146
146
  enabled: true,
147
- description: 'Enable AzureAD'
147
+ description: 'Enable Microsoft Entra ID'
148
148
  }
149
149
  };
150
150
  },