@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
@@ -1,5 +1,5 @@
1
1
  import { mount, RouterLinkStub } from '@vue/test-utils';
2
- import TitleBar from '@shell/components/Resource/Detail/TitleBar/index.vue';
2
+ import TitleBar, { AdditionalActionButton } from '@shell/components/Resource/Detail/TitleBar/index.vue';
3
3
  import ActionMenu from '@shell/components/ActionMenuShell.vue';
4
4
  import { createStore } from 'vuex';
5
5
  import { defineComponent, h } from 'vue';
@@ -240,5 +240,49 @@ describe('component: TitleBar/index', () => {
240
240
  expect(wrapper.find('.slot-button').exists()).toBeTruthy();
241
241
  expect(wrapper.find('.slot-button').text()).toBe('Slot Button');
242
242
  });
243
+
244
+ it('should render the actions container correctly when additional-actions slot contains nested buttons', async() => {
245
+ const wrapper = mount(TitleBar, {
246
+ props: { resourceTypeLabel, resourceName },
247
+ slots: { 'additional-actions': '<div class="btn-group"><button class="nested-btn">A</button><button class="nested-btn">B</button></div>' },
248
+ global: { stubs: { 'router-link': RouterLinkStub }, provide: { store } }
249
+ });
250
+
251
+ const actions = wrapper.find('.actions');
252
+
253
+ expect(actions.find('.btn-group').exists()).toBeTruthy();
254
+ expect(actions.findAll('.btn-group .nested-btn')).toHaveLength(2);
255
+ });
256
+ });
257
+
258
+ it('should match the full component snapshot', () => {
259
+ const additionalActions: AdditionalActionButton[] = [
260
+ {
261
+ label: 'Deploy', variant: 'primary', onClick: jest.fn()
262
+ },
263
+ {
264
+ label: 'Rollback', variant: 'secondary', size: 'large', onClick: jest.fn()
265
+ },
266
+ ];
267
+
268
+ const wrapper = mount(TitleBar, {
269
+ props: {
270
+ resource: {},
271
+ resourceTypeLabel,
272
+ resourceName,
273
+ resourceTo,
274
+ description: 'A test description',
275
+ badge: { color: 'bg-success', label: 'Active' },
276
+ additionalActions,
277
+ actionMenuResource: { resource: 'test-menu' },
278
+ onShowConfiguration() {},
279
+ },
280
+ global: {
281
+ stubs: { 'router-link': RouterLinkStub },
282
+ provide: { store }
283
+ }
284
+ });
285
+
286
+ expect(wrapper.html()).toMatchSnapshot();
243
287
  });
244
288
  });
@@ -178,7 +178,7 @@ const showAdditionalActionButtons = computed(() => isArray(additionalActions));
178
178
  align-items: center;
179
179
  }
180
180
 
181
- .show-configuration, &:deep() .actions button {
181
+ .show-configuration, &:deep() .actions > button {
182
182
  margin-left: 16px;
183
183
  }
184
184
 
@@ -0,0 +1,9 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`component: ViewOptions/index should match the snapshot 1`] = `
4
+ <div class="btn-group"><button data-testid="button-group-child-0" type="button" class="btn bg-primary" role="button" aria-label="%resourceDetail.masthead.detail%" aria-pressed="true">
5
+ <!--v-if--><span k="resourceDetail.masthead.detail"></span>
6
+ </button><button data-testid="button-group-child-1" type="button" class="btn bg-disabled" role="button" aria-label="%resourceDetail.masthead.graph%" aria-pressed="false">
7
+ <!--v-if--><span k="resourceDetail.masthead.graph"></span>
8
+ </button></div>
9
+ `;
@@ -0,0 +1,62 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import ViewOptions from '@shell/components/Resource/Detail/ViewOptions/index.vue';
3
+ import ButtonGroup from '@shell/components/ButtonGroup.vue';
4
+ import { _CONFIG, _GRAPH } from '@shell/config/query-params';
5
+
6
+ const mockPush = jest.fn();
7
+ const mockQuery = { view: _CONFIG };
8
+
9
+ jest.mock('vue-router', () => ({
10
+ useRouter: () => ({ push: mockPush }),
11
+ useRoute: () => ({ query: mockQuery }),
12
+ }));
13
+
14
+ describe('component: ViewOptions/index', () => {
15
+ beforeEach(() => {
16
+ jest.clearAllMocks();
17
+ mockQuery.view = _CONFIG;
18
+ });
19
+
20
+ const createWrapper = () => {
21
+ return mount(ViewOptions);
22
+ };
23
+
24
+ it('should render a ButtonGroup component', () => {
25
+ const wrapper = createWrapper();
26
+
27
+ expect(wrapper.findComponent(ButtonGroup).exists()).toBeTruthy();
28
+ });
29
+
30
+ it('should provide two view options: detail and graph', () => {
31
+ const wrapper = createWrapper();
32
+ const buttonGroup = wrapper.findComponent(ButtonGroup);
33
+ const options = buttonGroup.props('options');
34
+
35
+ expect(options).toHaveLength(2);
36
+ expect(options[0].value).toStrictEqual(_CONFIG);
37
+ expect(options[1].value).toStrictEqual(_GRAPH);
38
+ });
39
+
40
+ it('should set the initial view from the route query', () => {
41
+ const wrapper = createWrapper();
42
+ const buttonGroup = wrapper.findComponent(ButtonGroup);
43
+
44
+ expect(buttonGroup.props('value')).toStrictEqual(_CONFIG);
45
+ });
46
+
47
+ it('should push to router when view changes', async() => {
48
+ const wrapper = createWrapper();
49
+ const buttons = wrapper.findAll('.btn-group button');
50
+ const graphButton = buttons[1];
51
+
52
+ await graphButton.trigger('click');
53
+
54
+ expect(mockPush).toHaveBeenCalledWith({ query: { view: _GRAPH } });
55
+ });
56
+
57
+ it('should match the snapshot', () => {
58
+ const wrapper = createWrapper();
59
+
60
+ expect(wrapper.html()).toMatchSnapshot();
61
+ });
62
+ });
@@ -14,7 +14,8 @@ const view = ref(currentView.value);
14
14
  const viewOptions = computed(() => {
15
15
  return [
16
16
  {
17
- labelKey: 'resourceDetail.masthead.config',
17
+ labelKey: 'resourceDetail.masthead.detail',
18
+ // _CONFIG is the default when there is no query on the router
18
19
  value: _CONFIG,
19
20
  },
20
21
  {
@@ -85,7 +85,29 @@ export default {
85
85
  data() {
86
86
  const params = { ...this.$route.params };
87
87
 
88
- const formRoute = { name: `${ this.$route.name }-create`, params };
88
+ // Determine if the current product has a topLevelProduct defined, and if so,
89
+ // use that for the formRoute instead of the current route's product.
90
+ // This allows resources from extensions (new product registration) to use the correct route for creation,
91
+ // which may be different from the route of the resource list.
92
+ let currPluginName = '';
93
+ let formRoute;
94
+ let overrideCreateLocationByExtension = false;
95
+ const plugins = this.$extension.getPlugins();
96
+
97
+ Object.keys(plugins).forEach((key) => {
98
+ if (plugins[key].productNames.includes(this.$store.getters['productId'])) {
99
+ currPluginName = key;
100
+ }
101
+ });
102
+
103
+ if (currPluginName && plugins[currPluginName]?.topLevelProduct) {
104
+ // override create route for extension resource lists
105
+ formRoute = { name: `${ this.$route.name }-create`, params: { ...params, product: this.$store.getters['productId'] } };
106
+ overrideCreateLocationByExtension = true;
107
+ } else {
108
+ // this was the original logic before the topLevelProduct override was added
109
+ formRoute = { name: `${ this.$route.name }-create`, params };
110
+ }
89
111
 
90
112
  const hasEditComponent = this.$store.getters['type-map/hasCustomEdit'](this.resource);
91
113
 
@@ -96,6 +118,7 @@ export default {
96
118
  };
97
119
 
98
120
  return {
121
+ overrideCreateLocationByExtension,
99
122
  formRoute,
100
123
  yamlRoute,
101
124
  hasEditComponent,
@@ -149,7 +172,7 @@ export default {
149
172
  },
150
173
 
151
174
  _createLocation() {
152
- return this.createLocation || this.formRoute;
175
+ return this.overrideCreateLocationByExtension ? this.formRoute : this.createLocation || this.formRoute;
153
176
  },
154
177
 
155
178
  _yamlCreateLocation() {
@@ -38,6 +38,10 @@ export default {
38
38
  type: String,
39
39
  default: 'disabled',
40
40
  },
41
+ tooltipField: {
42
+ type: String,
43
+ default: 'tooltip',
44
+ },
41
45
 
42
46
  asLink: {
43
47
  type: Boolean,
@@ -104,6 +108,7 @@ export default {
104
108
  :is="asLink ? 'a' : 'div'"
105
109
  v-for="(r, idx) in rows"
106
110
  :key="get(r, keyField)"
111
+ v-clean-tooltip="get(r, tooltipField) || null"
107
112
  :role="asLink ? 'link' : null"
108
113
  :aria-disabled="asLink && get(r, disabledField) === true ? true : null"
109
114
  :aria-label="get(r, nameField)"
@@ -229,6 +229,19 @@ export default {
229
229
 
230
230
  this.getExplorerGroups(out);
231
231
 
232
+ // If there's a root group, pull its children up to the top level
233
+ // so that we can order them alongside group items in the nav
234
+ const rootGroupIndex = out.findIndex((g) => g.name.toLowerCase() === 'root');
235
+ const rootGroup = out[rootGroupIndex];
236
+
237
+ if (rootGroup && rootGroup.children?.length) {
238
+ out.splice(rootGroupIndex, 1);
239
+
240
+ rootGroup.children.forEach((child) => {
241
+ addObject(out, { ...child, children: [] });
242
+ });
243
+ }
244
+
232
245
  replaceWith(this.groups, ...sortBy(out, ['weight:desc', 'label']));
233
246
 
234
247
  this.gettingGroups = false;
@@ -0,0 +1,72 @@
1
+ import { shallowMount } from '@vue/test-utils';
2
+ import CountBox from '@shell/components/CountBox.vue';
3
+
4
+ describe('component: CountBox', () => {
5
+ const defaultProps = {
6
+ name: 'Test',
7
+ count: 5,
8
+ primaryColorVar: '--sizzle-1',
9
+ };
10
+
11
+ describe('when clickable is false', () => {
12
+ it('should render as a div', () => {
13
+ const wrapper = shallowMount(CountBox, { props: defaultProps });
14
+
15
+ expect(wrapper.element.tagName).toBe('DIV');
16
+ });
17
+
18
+ it('should not have the clickable class', () => {
19
+ const wrapper = shallowMount(CountBox, { props: defaultProps });
20
+
21
+ expect(wrapper.classes()).not.toContain('clickable');
22
+ });
23
+
24
+ it('should not emit click event when clicked', async() => {
25
+ const wrapper = shallowMount(CountBox, { props: defaultProps });
26
+
27
+ await wrapper.trigger('click');
28
+
29
+ expect(wrapper.emitted('click')).toBeUndefined();
30
+ });
31
+ });
32
+
33
+ describe('when clickable is true', () => {
34
+ it('should have the clickable class', () => {
35
+ const wrapper = shallowMount(CountBox, {
36
+ props: {
37
+ ...defaultProps,
38
+ clickable: true,
39
+ },
40
+ });
41
+
42
+ expect(wrapper.classes()).toContain('clickable');
43
+ });
44
+
45
+ it('should emit click event when clicked', async() => {
46
+ const wrapper = shallowMount(CountBox, {
47
+ props: {
48
+ ...defaultProps,
49
+ clickable: true,
50
+ },
51
+ });
52
+
53
+ await wrapper.trigger('click');
54
+
55
+ expect(wrapper.emitted('click')).toHaveLength(1);
56
+ });
57
+ });
58
+
59
+ describe('display', () => {
60
+ it('should display the count', () => {
61
+ const wrapper = shallowMount(CountBox, { props: defaultProps });
62
+
63
+ expect(wrapper.find('h1').text()).toBe('5');
64
+ });
65
+
66
+ it('should display the name', () => {
67
+ const wrapper = shallowMount(CountBox, { props: defaultProps });
68
+
69
+ expect(wrapper.find('label').text()).toBe('Test');
70
+ });
71
+ });
72
+ });
@@ -0,0 +1,113 @@
1
+ import { mount } from '@vue/test-utils';
2
+
3
+ import DetailText from '@shell/components/DetailText.vue';
4
+
5
+ jest.mock('@shell/utils/clipboard', () => ({ copyTextToClipboard: jest.fn() }));
6
+
7
+ describe('component: DetailText', () => {
8
+ const defaultMocks = {
9
+ $store: {
10
+ getters: {
11
+ 'i18n/t': jest.fn((key: string) => `%${ key }%`),
12
+ 'prefs/get': jest.fn(() => true),
13
+ }
14
+ }
15
+ };
16
+
17
+ describe('concealment', () => {
18
+ it('should not render the actual secret value in the content area when concealed', () => {
19
+ const secretValue = 'super-secret-password-xyz';
20
+ const wrapper = mount(DetailText, {
21
+ props: {
22
+ value: secretValue,
23
+ conceal: true,
24
+ label: 'Password',
25
+ },
26
+
27
+ global: {
28
+ mocks: defaultMocks,
29
+ directives: {
30
+ 'clean-html': () => {},
31
+ 'clean-tooltip': () => {},
32
+ t: () => {},
33
+ },
34
+ stubs: {
35
+ CopyToClipboard: true,
36
+ CodeMirror: true,
37
+ },
38
+ },
39
+ });
40
+
41
+ const concealedSpan = wrapper.find('[data-testid="detail-top_html"]');
42
+
43
+ expect(concealedSpan.exists()).toBe(true);
44
+ expect(concealedSpan.classes()).toContain('conceal');
45
+ expect(concealedSpan.text()).not.toContain(secretValue);
46
+ });
47
+
48
+ it('should render the actual value when not concealed', () => {
49
+ const visibleValue = 'visible-value-123';
50
+ const wrapper = mount(DetailText, {
51
+ props: {
52
+ value: visibleValue,
53
+ conceal: false,
54
+ label: 'Data',
55
+ },
56
+
57
+ global: {
58
+ mocks: defaultMocks,
59
+ directives: {
60
+ 'clean-html': (el: HTMLElement, binding: { value: string }) => {
61
+ el.innerHTML = binding.value;
62
+ },
63
+ 'clean-tooltip': () => {},
64
+ t: () => {},
65
+ },
66
+ stubs: {
67
+ CopyToClipboard: true,
68
+ CodeMirror: true,
69
+ },
70
+ },
71
+ });
72
+
73
+ const contentSpan = wrapper.find('[data-testid="detail-top_html"]');
74
+
75
+ expect(contentSpan.exists()).toBe(true);
76
+ expect(contentSpan.classes()).not.toContain('conceal');
77
+ });
78
+
79
+ it('should not render JSON secret values in CodeMirror when concealed', () => {
80
+ const jsonSecret = '{"api_key": "secret-key-123"}';
81
+ const wrapper = mount(DetailText, {
82
+ props: {
83
+ value: jsonSecret,
84
+ conceal: true,
85
+ label: 'Config',
86
+ },
87
+
88
+ global: {
89
+ mocks: defaultMocks,
90
+ directives: {
91
+ 'clean-html': () => {},
92
+ 'clean-tooltip': () => {},
93
+ t: () => {},
94
+ },
95
+ stubs: {
96
+ CopyToClipboard: true,
97
+ CodeMirror: true,
98
+ },
99
+ },
100
+ });
101
+
102
+ const codeMirror = wrapper.findComponent({ name: 'CodeMirror' });
103
+
104
+ expect(codeMirror.exists()).toBe(false);
105
+
106
+ const concealedSpan = wrapper.find('[data-testid="detail-top_html"]');
107
+
108
+ expect(concealedSpan.exists()).toBe(true);
109
+ expect(concealedSpan.classes()).toContain('conceal');
110
+ expect(concealedSpan.text()).not.toContain('secret-key-123');
111
+ });
112
+ });
113
+ });
@@ -24,6 +24,7 @@ import DeveloperLoadExtensionDialog from '@shell/dialog/DeveloperLoadExtensionDi
24
24
  import AddExtensionReposDialog from '@shell/dialog/AddExtensionReposDialog.vue';
25
25
  import InstallExtensionDialog from '@shell/dialog/InstallExtensionDialog.vue';
26
26
  import UninstallExtensionDialog from '@shell/dialog/UninstallExtensionDialog.vue';
27
+ import UninstallExistingExtensionDialog from '@shell/dialog/UninstallExistingExtensionDialog.vue';
27
28
  import KnownHostsEditDialog from '@shell/dialog/KnownHostsEditDialog.vue';
28
29
  import ImportDialog from '@shell/dialog/ImportDialog.vue';
29
30
  import SearchDialog from '@shell/dialog/SearchDialog.vue';
@@ -110,6 +111,7 @@ describe('component: PromptModal', () => {
110
111
  ['AddExtensionReposDialog', AddExtensionReposDialog],
111
112
  ['InstallExtensionDialog', InstallExtensionDialog],
112
113
  ['UninstallExtensionDialog', UninstallExtensionDialog],
114
+ ['UninstallExistingExtensionDialog', UninstallExistingExtensionDialog],
113
115
  ['KnownHostsEditDialog', KnownHostsEditDialog],
114
116
  ['ImportDialog', ImportDialog],
115
117
  ['SearchDialog', SearchDialog],
@@ -124,6 +124,15 @@ export default {
124
124
  this.update();
125
125
  }
126
126
  },
127
+
128
+ allClusters(clusters: any[]) {
129
+ if (clusters.length) {
130
+ // Resolve metadata.name values to nameDisplay for UI display
131
+ this.selectedClusters = this.selectedClusters.map(
132
+ (name) => this.resolveClusterDisplayName(name)
133
+ );
134
+ }
135
+ },
127
136
  },
128
137
 
129
138
  computed: {
@@ -159,7 +168,7 @@ export default {
159
168
  clustersOptions() {
160
169
  return this.allClusters
161
170
  .filter((x) => x.metadata.namespace === this.namespace && !isHarvesterCluster(x))
162
- .map((x) => ({ label: x.nameDisplay, value: x.metadata.name }));
171
+ .map((x) => ({ label: x.nameDisplay, value: x.nameDisplay }));
163
172
  },
164
173
 
165
174
  clusterGroupsOptions() {
@@ -352,6 +361,14 @@ export default {
352
361
  return undefined;
353
362
  },
354
363
 
364
+ resolveClusterDisplayName(name: string): string {
365
+ const cluster = this.allClusters.find(
366
+ (c: any) => c.metadata.namespace === this.namespace && c.metadata.name === name
367
+ );
368
+
369
+ return cluster ? cluster.nameDisplay : name;
370
+ },
371
+
355
372
  reset() {
356
373
  this.targetMode = 'all';
357
374
  this.selectedClusters = [];
@@ -107,6 +107,7 @@ export default {
107
107
  :schema="schema"
108
108
  :headers="headers"
109
109
  :rows="rows"
110
+ :sub-rows="true"
110
111
  :loading="loading"
111
112
  :use-query-params-for-simple-filtering="useQueryParamsForSimpleFiltering"
112
113
  :ignore-filter="ignoreFilter"
@@ -573,4 +573,75 @@ describe('component: FleetClusters', () => {
573
573
  expect(additionalSubRow.exists()).toBe(false);
574
574
  });
575
575
  });
576
+
577
+ describe('labels visibility regardless of error state', () => {
578
+ it('should pass sub-rows prop as true to ResourceTable so labels always render', () => {
579
+ const wrapper = createWrapper();
580
+
581
+ // sub-rows=true ensures SortableTable.showSubRow() returns true,
582
+ // which makes the #additional-sub-row slot render regardless of stateDescription.
583
+ // Without this, labels only appear when there is an error (stateDescription).
584
+ const resourceTableStub = wrapper.findComponent('.resource-table') as any;
585
+
586
+ expect(resourceTableStub.props('subRows')).toBe(true);
587
+ });
588
+
589
+ it('should render labels when cluster has no stateDescription (no error)', () => {
590
+ const rows = [{
591
+ customLabels: ['env:prod', 'team:backend'],
592
+ displayCustomLabels: false,
593
+ stateDescription: undefined,
594
+ }];
595
+
596
+ const wrapper = createWrapper({ rows });
597
+ const tags = wrapper.findAll('.tag');
598
+
599
+ expect(tags).toHaveLength(2);
600
+ expect(tags[0].text()).toBe('env:prod');
601
+ expect(tags[1].text()).toBe('team:backend');
602
+ });
603
+
604
+ it('should render labels when cluster has a stateDescription (error)', () => {
605
+ const rows = [{
606
+ customLabels: ['env:prod', 'team:backend'],
607
+ displayCustomLabels: false,
608
+ stateDescription: 'Something went wrong',
609
+ }];
610
+
611
+ const wrapper = createWrapper({ rows });
612
+ const tags = wrapper.findAll('.tag');
613
+
614
+ expect(tags).toHaveLength(2);
615
+ expect(tags[0].text()).toBe('env:prod');
616
+ expect(tags[1].text()).toBe('team:backend');
617
+ });
618
+
619
+ it('should render labels when stateDescription is empty string', () => {
620
+ const rows = [{
621
+ customLabels: ['env:staging'],
622
+ displayCustomLabels: false,
623
+ stateDescription: '',
624
+ }];
625
+
626
+ const wrapper = createWrapper({ rows });
627
+ const tags = wrapper.findAll('.tag');
628
+
629
+ expect(tags).toHaveLength(1);
630
+ expect(tags[0].text()).toBe('env:staging');
631
+ });
632
+
633
+ it('should render labels when stateDescription is null', () => {
634
+ const rows = [{
635
+ customLabels: ['region:eu-west'],
636
+ displayCustomLabels: false,
637
+ stateDescription: null,
638
+ }];
639
+
640
+ const wrapper = createWrapper({ rows });
641
+ const tags = wrapper.findAll('.tag');
642
+
643
+ expect(tags).toHaveLength(1);
644
+ expect(tags[0].text()).toBe('region:eu-west');
645
+ });
646
+ });
576
647
  });
@@ -1,18 +1,22 @@
1
1
  <script>
2
- import labeledFormElement from '@shell/mixins/labeled-form-element';
3
2
  import { LabeledInput } from '@components/Form/LabeledInput';
4
3
  import LabeledSelect from '@shell/components/form/LabeledSelect';
5
4
  import Select from '@shell/components/form/Select';
5
+ import { computed } from 'vue';
6
+ import { _VIEW, _EDIT } from '@shell/config/query-params';
7
+
6
8
  export default {
7
- name: 'InputWithSelect',
9
+ name: 'InputWithSelect',
10
+
11
+ inheritAttrs: false,
12
+
8
13
  emits: ['update:value'],
9
14
  components: {
10
15
  LabeledInput,
11
16
  LabeledSelect,
12
17
  Select,
13
18
  },
14
- mixins: [labeledFormElement],
15
- props: {
19
+ props: {
16
20
  disabled: {
17
21
  type: Boolean,
18
22
  default: false,
@@ -84,10 +88,20 @@ export default {
84
88
  selectRules: {
85
89
  default: () => [],
86
90
  type: Array,
91
+ },
92
+ mode: {
93
+ type: String,
94
+ default: _EDIT,
87
95
  }
88
96
 
89
97
  },
90
98
 
99
+ setup(props) {
100
+ const isView = computed(() => props.mode === _VIEW);
101
+
102
+ return { isView };
103
+ },
104
+
91
105
  data() {
92
106
  return {
93
107
  selected: this.selectValue || this.options[0].value,
@@ -95,12 +109,6 @@ export default {
95
109
  };
96
110
  },
97
111
 
98
- computed: {
99
- canPaginate() {
100
- return false;
101
- }
102
- },
103
-
104
112
  methods: {
105
113
  focus() {
106
114
  const comp = this.$refs.text;
@@ -778,11 +778,16 @@ export default {
778
778
  @onInput="onInputMarkdownMultiline(i, $event)"
779
779
  @onFocus="onFocusMarkdownMultiline(i, $event)"
780
780
  />
781
+ <div
782
+ v-else-if="valueConcealed"
783
+ class="concealed-value conceal"
784
+ data-testid="concealed-value"
785
+ :aria-label="t('generic.ariaLabel.value', {index: i+1})"
786
+ />
781
787
  <TextAreaAutoGrow
782
788
  v-else-if="valueMultiline && row[valueName] !== undefined"
783
789
  v-model:value="row[valueName]"
784
790
  data-testid="value-multiline"
785
- :class="{'conceal': valueConcealed}"
786
791
  :disabled="disabled"
787
792
  :mode="mode"
788
793
  :placeholder="_valuePlaceholder"
@@ -809,6 +814,7 @@ export default {
809
814
  class="btn btn-sm role-secondary file-selector"
810
815
  :label="t('generic.upload')"
811
816
  :include-file-name="true"
817
+ :accept="readAccept"
812
818
  :aria-label="t('generic.ariaLabel.value', {index: i+1})"
813
819
  @selected="onValueFileSelected(i, $event)"
814
820
  />
@@ -896,6 +902,7 @@ export default {
896
902
  class="role-tertiary"
897
903
  :label="t('generic.readFromFile')"
898
904
  :include-file-name="true"
905
+ :accept="readAccept"
899
906
  data-testid="read_all_key_value_button"
900
907
  @selected="onFileSelected"
901
908
  />
@@ -941,6 +948,15 @@ export default {
941
948
  padding: 10px 10px 10px 10px;
942
949
  }
943
950
 
951
+ .concealed-value {
952
+ padding: 10px;
953
+ min-height: 40px;
954
+ user-select: none;
955
+ &::before {
956
+ content: '••••••••••••••••••••';
957
+ }
958
+ }
959
+
944
960
  .text-monospace:not(.conceal) {
945
961
  font-family: monospace, monospace;
946
962
  }