@rancher/shell 3.0.8-rc.8 → 3.0.8

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 (260) hide show
  1. package/apis/impl/apis.ts +61 -0
  2. package/apis/index.ts +40 -0
  3. package/apis/intf/modal.ts +90 -0
  4. package/apis/intf/shell.ts +36 -0
  5. package/apis/intf/slide-in.ts +98 -0
  6. package/apis/intf/system.ts +41 -0
  7. package/apis/shell/__tests__/modal.test.ts +80 -0
  8. package/apis/shell/__tests__/notifications.test.ts +71 -0
  9. package/apis/shell/__tests__/slide-in.test.ts +54 -0
  10. package/apis/shell/__tests__/system.test.ts +129 -0
  11. package/apis/shell/index.ts +38 -0
  12. package/apis/shell/modal.ts +41 -0
  13. package/apis/shell/notifications.ts +65 -0
  14. package/apis/shell/slide-in.ts +33 -0
  15. package/apis/shell/system.ts +65 -0
  16. package/apis/vue-shim.d.ts +11 -0
  17. package/assets/brand/suse/dark/rancher-logo.svg +1 -64
  18. package/assets/styles/global/_tooltip.scss +6 -1
  19. package/assets/translations/en-us.yaml +14 -1
  20. package/components/ActionMenuShell.vue +3 -1
  21. package/components/BackLink.vue +8 -0
  22. package/components/BannerGraphic.vue +1 -5
  23. package/components/BrandImage.vue +17 -6
  24. package/components/Cron/CronExpressionEditor.vue +1 -1
  25. package/components/Cron/CronExpressionEditorModal.vue +1 -1
  26. package/components/CruResource.vue +8 -1
  27. package/components/Drawer/ResourceDetailDrawer/ConfigTab.vue +1 -0
  28. package/components/Drawer/ResourceDetailDrawer/__tests__/composables.test.ts +50 -1
  29. package/components/Drawer/ResourceDetailDrawer/composables.ts +19 -0
  30. package/components/Drawer/ResourceDetailDrawer/index.vue +4 -1
  31. package/components/Drawer/ResourceDetailDrawer/types.ts +2 -1
  32. package/components/LocaleSelector.vue +2 -2
  33. package/components/ModalManager.vue +11 -1
  34. package/components/Questions/__tests__/Yaml.test.ts +1 -1
  35. package/components/Questions/__tests__/index.test.ts +159 -0
  36. package/components/RelatedResources.vue +5 -0
  37. package/components/Resource/Detail/Metadata/Annotations/index.vue +2 -2
  38. package/components/Resource/Detail/Metadata/Labels/index.vue +2 -2
  39. package/components/Resource/Detail/Metadata/index.vue +3 -3
  40. package/components/Resource/Detail/ResourcePopover/index.vue +5 -1
  41. package/components/Resource/Detail/composables.ts +2 -2
  42. package/components/ResourceDetail/Masthead/latest.vue +23 -21
  43. package/components/ResourceDetail/index.vue +3 -0
  44. package/components/ResourceTable.vue +54 -21
  45. package/components/SlideInPanelManager.vue +16 -11
  46. package/components/SortableTable/THead.vue +2 -1
  47. package/components/SortableTable/index.vue +20 -2
  48. package/components/Tabbed/__tests__/index.test.ts +86 -0
  49. package/components/Tabbed/index.vue +37 -2
  50. package/components/__tests__/NamespaceFilter.test.ts +49 -0
  51. package/components/auth/SelectPrincipal.vue +28 -6
  52. package/components/auth/__tests__/SelectPrincipal.test.ts +119 -0
  53. package/components/auth/login/ldap.vue +3 -3
  54. package/components/fleet/FleetSecretSelector.vue +1 -1
  55. package/components/form/KeyValue.vue +1 -1
  56. package/components/form/NameNsDescription.vue +1 -1
  57. package/components/form/NodeScheduling.vue +2 -2
  58. package/components/form/ResourceTabs/composable.ts +2 -2
  59. package/components/form/ResourceTabs/index.vue +0 -2
  60. package/components/form/__tests__/NameNsDescription.test.ts +42 -0
  61. package/components/formatter/InternalExternalIP.vue +4 -1
  62. package/components/formatter/LinkName.vue +5 -0
  63. package/components/formatter/__tests__/InternalExternalIP.test.ts +1 -1
  64. package/components/nav/Group.vue +25 -7
  65. package/components/nav/Header.vue +1 -1
  66. package/components/nav/NamespaceFilter.vue +1 -0
  67. package/components/nav/Type.vue +17 -6
  68. package/components/nav/WindowManager/panels/TabBodyContainer.vue +1 -1
  69. package/components/nav/__tests__/Type.test.ts +59 -0
  70. package/components/templates/standalone.vue +1 -1
  71. package/composables/cruResource.ts +27 -0
  72. package/composables/focusTrap.ts +3 -1
  73. package/composables/resourceDetail.ts +15 -0
  74. package/composables/useI18n.ts +10 -1
  75. package/composables/useLabeledFormElement.ts +3 -4
  76. package/config/__test__/uiplugins.test.ts +309 -0
  77. package/config/labels-annotations.js +1 -0
  78. package/config/product/explorer.js +3 -1
  79. package/config/product/fleet.js +1 -1
  80. package/config/router/navigation-guards/clusters.js +3 -3
  81. package/config/router/navigation-guards/products.js +1 -1
  82. package/config/router/routes.js +7 -7
  83. package/config/types.js +7 -0
  84. package/config/uiplugins.js +46 -2
  85. package/core/__tests__/extension-manager-impl.test.js +437 -0
  86. package/core/extension-manager-impl.js +21 -25
  87. package/core/plugin-helpers.ts +2 -2
  88. package/core/plugin.ts +9 -1
  89. package/core/plugins-loader.js +2 -2
  90. package/core/types-provisioning.ts +5 -1
  91. package/core/types.ts +35 -0
  92. package/detail/provisioning.cattle.io.cluster.vue +9 -6
  93. package/dialog/DeveloperLoadExtensionDialog.vue +13 -4
  94. package/dialog/MoveNamespaceDialog.vue +20 -4
  95. package/dialog/RollbackWorkloadDialog.vue +2 -5
  96. package/dialog/SearchDialog.vue +1 -0
  97. package/dialog/__tests__/MoveNamespaceDialog.test.ts +249 -0
  98. package/directives/__tests__/clean-tooltip.test.ts +298 -0
  99. package/directives/clean-tooltip.ts +234 -0
  100. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +2 -2
  101. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +100 -3
  102. package/edit/autoscaling.horizontalpodautoscaler/index.vue +1 -0
  103. package/edit/configmap.vue +1 -0
  104. package/edit/constraints.gatekeeper.sh.constraint/index.vue +1 -0
  105. package/edit/fleet.cattle.io.helmop.vue +11 -6
  106. package/edit/helm.cattle.io.projecthelmchart.vue +1 -0
  107. package/edit/k8s.cni.cncf.io.networkattachmentdefinition.vue +1 -0
  108. package/edit/logging-flow/index.vue +1 -0
  109. package/edit/logging.banzaicloud.io.output/index.vue +1 -0
  110. package/edit/management.cattle.io.fleetworkspace.vue +1 -1
  111. package/edit/management.cattle.io.project.vue +1 -0
  112. package/edit/monitoring.coreos.com.alertmanagerconfig/index.vue +4 -1
  113. package/edit/monitoring.coreos.com.alertmanagerconfig/receiverConfig.vue +2 -1
  114. package/edit/monitoring.coreos.com.prometheusrule/index.vue +1 -0
  115. package/edit/monitoring.coreos.com.receiver/index.vue +2 -1
  116. package/edit/monitoring.coreos.com.route.vue +1 -1
  117. package/edit/namespace.vue +1 -0
  118. package/edit/networking.istio.io.destinationrule/index.vue +1 -0
  119. package/edit/networking.k8s.io.ingress/index.vue +1 -0
  120. package/edit/networking.k8s.io.networkpolicy/PolicyRules.vue +1 -0
  121. package/edit/networking.k8s.io.networkpolicy/index.vue +1 -0
  122. package/edit/node.vue +1 -0
  123. package/edit/persistentvolume/index.vue +27 -22
  124. package/edit/persistentvolume/plugins/awsElasticBlockStore.vue +13 -14
  125. package/edit/persistentvolume/plugins/azureDisk.vue +49 -48
  126. package/edit/persistentvolume/plugins/azureFile.vue +15 -14
  127. package/edit/persistentvolume/plugins/cephfs.vue +15 -14
  128. package/edit/persistentvolume/plugins/cinder.vue +15 -14
  129. package/edit/persistentvolume/plugins/csi.vue +18 -16
  130. package/edit/persistentvolume/plugins/fc.vue +13 -14
  131. package/edit/persistentvolume/plugins/flexVolume.vue +15 -14
  132. package/edit/persistentvolume/plugins/flocker.vue +1 -3
  133. package/edit/persistentvolume/plugins/gcePersistentDisk.vue +13 -14
  134. package/edit/persistentvolume/plugins/glusterfs.vue +15 -14
  135. package/edit/persistentvolume/plugins/hostPath.vue +40 -39
  136. package/edit/persistentvolume/plugins/iscsi.vue +13 -14
  137. package/edit/persistentvolume/plugins/local.vue +1 -3
  138. package/edit/persistentvolume/plugins/longhorn.vue +23 -22
  139. package/edit/persistentvolume/plugins/nfs.vue +15 -14
  140. package/edit/persistentvolume/plugins/photonPersistentDisk.vue +1 -14
  141. package/edit/persistentvolume/plugins/portworxVolume.vue +15 -14
  142. package/edit/persistentvolume/plugins/quobyte.vue +15 -14
  143. package/edit/persistentvolume/plugins/rbd.vue +15 -14
  144. package/edit/persistentvolume/plugins/scaleIO.vue +15 -14
  145. package/edit/persistentvolume/plugins/storageos.vue +15 -14
  146. package/edit/persistentvolume/plugins/vsphereVolume.vue +1 -3
  147. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +21 -21
  148. package/edit/provisioning.cattle.io.cluster/index.vue +5 -5
  149. package/edit/provisioning.cattle.io.cluster/rke2.vue +9 -8
  150. package/edit/resources.cattle.io.restore.vue +1 -1
  151. package/edit/secret/index.vue +1 -1
  152. package/edit/service.vue +1 -0
  153. package/edit/serviceaccount.vue +1 -0
  154. package/edit/storage.k8s.io.storageclass/index.vue +1 -0
  155. package/edit/workload/Job.vue +2 -2
  156. package/edit/workload/index.vue +2 -1
  157. package/edit/workload/mixins/workload.js +1 -1
  158. package/initialize/App.vue +4 -4
  159. package/initialize/install-plugins.js +19 -5
  160. package/machine-config/azure.vue +1 -1
  161. package/machine-config/components/GCEImage.vue +1 -1
  162. package/mixins/__tests__/brand.spec.ts +2 -2
  163. package/mixins/brand.js +1 -7
  164. package/mixins/create-edit-view/index.js +5 -0
  165. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +128 -5
  166. package/models/chart.js +70 -74
  167. package/models/management.cattle.io.cluster.js +21 -3
  168. package/models/provisioning.cattle.io.cluster.js +31 -11
  169. package/package.json +11 -10
  170. package/pages/auth/login.vue +4 -6
  171. package/pages/auth/setup.vue +1 -1
  172. package/pages/auth/verify.vue +3 -3
  173. package/pages/c/_cluster/apps/charts/__tests__/chart.test.ts +135 -0
  174. package/pages/c/_cluster/apps/charts/chart.vue +33 -15
  175. package/pages/c/_cluster/apps/charts/index.vue +122 -24
  176. package/pages/c/_cluster/apps/charts/install.vue +33 -0
  177. package/pages/c/_cluster/explorer/__tests__/index.test.ts +1 -1
  178. package/pages/c/_cluster/explorer/index.vue +8 -6
  179. package/pages/c/_cluster/fleet/index.vue +4 -7
  180. package/pages/c/_cluster/manager/hostedprovider/index.vue +12 -6
  181. package/pages/c/_cluster/settings/brand.vue +1 -1
  182. package/pages/c/_cluster/settings/index.vue +5 -0
  183. package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +7 -0
  184. package/pages/c/_cluster/uiplugins/catalogs.vue +147 -0
  185. package/pages/c/_cluster/uiplugins/index.vue +126 -184
  186. package/pkg/auto-import.js +3 -3
  187. package/pkg/dynamic-importer.lib.js +1 -1
  188. package/pkg/import.js +1 -1
  189. package/plugins/__tests__/mutations.tests.ts +179 -0
  190. package/plugins/dashboard-client-init.js +3 -0
  191. package/plugins/dashboard-store/getters.js +19 -2
  192. package/plugins/dashboard-store/model-loader.js +1 -1
  193. package/plugins/dashboard-store/mutations.js +23 -2
  194. package/plugins/dashboard-store/resource-class.js +11 -5
  195. package/plugins/i18n.js +8 -0
  196. package/plugins/plugin.js +2 -2
  197. package/plugins/steve/__tests__/steve-pagination-utils.test.ts +506 -0
  198. package/plugins/steve/steve-class.js +1 -1
  199. package/plugins/steve/steve-pagination-utils.ts +131 -47
  200. package/rancher-components/Form/Checkbox/Checkbox.vue +1 -1
  201. package/rancher-components/Form/LabeledInput/LabeledInput.vue +1 -1
  202. package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +6 -42
  203. package/rancher-components/Pill/RcStatusBadge/index.ts +0 -1
  204. package/rancher-components/Pill/RcStatusBadge/types.ts +1 -1
  205. package/rancher-components/Pill/RcStatusIndicator/RcStatusIndicator.vue +5 -28
  206. package/rancher-components/Pill/RcStatusIndicator/types.ts +2 -1
  207. package/rancher-components/Pill/types.ts +0 -1
  208. package/rancher-components/RcDropdown/useDropdownContext.ts +2 -4
  209. package/rancher-components/RcIcon/RcIcon.test.ts +51 -0
  210. package/rancher-components/RcIcon/RcIcon.vue +46 -0
  211. package/rancher-components/RcIcon/index.ts +1 -0
  212. package/rancher-components/RcIcon/types.ts +160 -0
  213. package/rancher-components/RcItemCard/RcItemCard.vue +1 -1
  214. package/rancher-components/utils/status.test.ts +67 -0
  215. package/rancher-components/utils/status.ts +77 -0
  216. package/scripts/publish-shell.sh +25 -0
  217. package/scripts/typegen.sh +1 -0
  218. package/store/__tests__/catalog.test.ts +1 -1
  219. package/store/__tests__/type-map.test.ts +164 -2
  220. package/store/action-menu.js +8 -0
  221. package/store/auth.js +25 -13
  222. package/store/catalog.js +6 -0
  223. package/store/i18n.js +3 -3
  224. package/store/index.js +8 -6
  225. package/store/notifications.ts +2 -0
  226. package/store/prefs.js +6 -7
  227. package/store/type-map.js +17 -7
  228. package/store/wm.ts +4 -4
  229. package/types/internal-api/shell/modal.d.ts +6 -6
  230. package/types/notifications/index.ts +126 -15
  231. package/types/rancher/index.d.ts +9 -0
  232. package/types/shell/index.d.ts +54 -3
  233. package/types/store/__tests__/pagination.types.spec.ts +137 -0
  234. package/types/store/pagination.types.ts +157 -9
  235. package/types/vue-shim.d.ts +5 -4
  236. package/utils/__tests__/provider.test.ts +98 -0
  237. package/utils/__tests__/router.test.js +238 -0
  238. package/utils/__tests__/selector-typed.test.ts +263 -0
  239. package/utils/cluster.js +4 -1
  240. package/utils/color.js +1 -1
  241. package/utils/dynamic-content/__tests__/info.test.ts +6 -0
  242. package/utils/dynamic-content/info.ts +43 -0
  243. package/utils/favicon.js +4 -4
  244. package/utils/fleet.ts +8 -1
  245. package/utils/pagination-utils.ts +2 -2
  246. package/utils/pagination-wrapper.ts +1 -1
  247. package/utils/provider.ts +14 -0
  248. package/utils/router.js +50 -0
  249. package/utils/selector-typed.ts +6 -2
  250. package/utils/unit-tests/pagination-utils.spec.ts +8 -8
  251. package/vue.config.js +3 -3
  252. package/composables/useExtensionManager.ts +0 -17
  253. package/core/plugins.js +0 -38
  254. package/directives/clean-tooltip.js +0 -32
  255. package/plugins/internal-api/index.ts +0 -37
  256. package/plugins/internal-api/shared/base-api.ts +0 -13
  257. package/plugins/internal-api/shell/shell.api.ts +0 -108
  258. package/plugins/nuxt-client-init.js +0 -3
  259. package/types/internal-api/shell/growl.d.ts +0 -25
  260. package/types/internal-api/shell/slideIn.d.ts +0 -15
@@ -36,9 +36,9 @@ export default {
36
36
  };
37
37
  },
38
38
  computed: {
39
- ...mapGetters({ theme: 'prefs/theme' }),
39
+ ...mapGetters({ theme: 'prefs/theme', brand: 'management/brand' }),
40
40
 
41
- brand() {
41
+ brandBase() {
42
42
  const setting = this.managementSettings.filter((setting) => setting.id === SETTING.BRAND)[0] || {};
43
43
 
44
44
  return setting.value;
@@ -78,19 +78,30 @@ export default {
78
78
  }
79
79
  },
80
80
 
81
+ isDark() {
82
+ return this.theme === 'dark';
83
+ },
84
+
81
85
  pathToBrandedImage() {
82
86
  if (this.fileName === 'rancher-logo.svg' || this.supportCustomLogo) {
83
- if (this.theme === 'dark' && this.uiLogoDark) {
87
+ if (this.isDark && this.uiLogoDark) {
84
88
  return this.uiLogoDark;
85
89
  }
86
90
 
87
91
  if (this.uiLogoLight) {
88
92
  return this.uiLogoLight;
89
93
  }
94
+
95
+ // csp, rgs, and federal map to SUSE, but have their own custom logos
96
+ if (this.brandBase !== this.brand) {
97
+ try {
98
+ return require(`~shell/assets/brand/${ this.brandBase }/${ this.isDark ? 'dark/' : '' }${ this.fileName }`);
99
+ } catch { }
100
+ }
90
101
  }
91
102
 
92
103
  if (this.fileName === 'banner.svg') {
93
- if (this.theme === 'dark' && this.uiBannerDark) {
104
+ if (this.isDark && this.uiBannerDark) {
94
105
  return this.uiBannerDark;
95
106
  }
96
107
 
@@ -100,7 +111,7 @@ export default {
100
111
  }
101
112
 
102
113
  if (this.fileName === 'login-landscape.svg') {
103
- if (this.theme === 'dark' && this.uiLoginBackgroundDark) {
114
+ if (this.isDark && this.uiLoginBackgroundDark) {
104
115
  return this.uiLoginBackgroundDark;
105
116
  }
106
117
 
@@ -112,7 +123,7 @@ export default {
112
123
  if (!this.brand) {
113
124
  return this.defaultPathToBrandedImage;
114
125
  } else {
115
- if (this.theme === 'dark' || this.dark) {
126
+ if (this.isDark || this.dark) {
116
127
  try {
117
128
  return require(`~shell/assets/brand/${ this.brand }/dark/${ this.fileName }`);
118
129
  } catch {}
@@ -268,7 +268,7 @@ const handleBlur = (field: CronField) => {
268
268
  </template>
269
269
 
270
270
  <style scoped lang="scss">
271
- $input-max-width: 110px;
271
+ $input-max-width: 120px;
272
272
 
273
273
  .cron-row {
274
274
  display: flex;
@@ -207,7 +207,7 @@ onBeforeUnmount(() => {
207
207
 
208
208
  .custom-cron-editor {
209
209
  margin: 64px auto;
210
- max-width: 600px;
210
+ max-width: 660px;
211
211
  }
212
212
 
213
213
  .cron-info {
@@ -9,9 +9,10 @@ import AsyncButton from '@shell/components/AsyncButton';
9
9
  import { mapGetters, mapState, mapActions } from 'vuex';
10
10
  import { stringify, exceptionToErrorsArray } from '@shell/utils/error';
11
11
  import CruResourceFooter from '@shell/components/CruResourceFooter';
12
+ import { useResourceCreatePageProvider, useResourceEditPageProvider } from '@shell/composables/cruResource';
12
13
 
13
14
  import {
14
- _EDIT, _VIEW, AS, _YAML, _UNFLAG, SUB_TYPE
15
+ _EDIT, _VIEW, AS, _YAML, _UNFLAG, SUB_TYPE, _CREATE
15
16
  } from '@shell/config/query-params';
16
17
 
17
18
  import { BEFORE_SAVE_HOOKS } from '@shell/mixins/child-hook';
@@ -168,6 +169,12 @@ export default {
168
169
  const inStore = this.$store.getters['currentStore'](this.resource);
169
170
  const schema = this.$store.getters[`${ inStore }/schemaFor`](this.resource.type);
170
171
 
172
+ if (this.mode === _CREATE) {
173
+ useResourceCreatePageProvider();
174
+ } else if (this.mode === _EDIT) {
175
+ useResourceEditPageProvider();
176
+ }
177
+
171
178
  return {
172
179
  isCancelModal: false,
173
180
  showAsForm: this.$route.query[AS] !== _YAML,
@@ -25,6 +25,7 @@ const i18n = useI18n(store);
25
25
  :real-mode="_VIEW"
26
26
  :initial-value="props.resource"
27
27
  :use-tabbed-hash="false /* Have to disable hashing on child components or it modifies the url and closes the drawer */"
28
+ :default-tab="props.defaultTab"
28
29
  as="config"
29
30
  />
30
31
  </div>
@@ -1,4 +1,5 @@
1
- import { useDefaultConfigTabProps, useDefaultYamlTabProps } from '@shell/components/Drawer/ResourceDetailDrawer/composables';
1
+ import { provide, inject } from 'vue';
2
+ import { useDefaultConfigTabProps, useDefaultYamlTabProps, useResourceDetailDrawerProvider, useIsInResourceDetailDrawer } from '@shell/components/Drawer/ResourceDetailDrawer/composables';
2
3
  import * as helpers from '@shell/components/Drawer/ResourceDetailDrawer/helpers';
3
4
  import * as vuex from 'vuex';
4
5
 
@@ -6,6 +7,11 @@ jest.mock('@shell/components/Drawer/ResourceDetailDrawer/helpers');
6
7
  jest.mock('vuex');
7
8
  jest.mock('@shell/composables/drawer');
8
9
  jest.mock('@shell/components/Drawer/ResourceDetailDrawer/index.vue', () => ({ name: 'ResourceDetailDrawer' } as any));
10
+ jest.mock('vue', () => ({
11
+ ...jest.requireActual('vue'),
12
+ provide: jest.fn(),
13
+ inject: jest.fn()
14
+ }));
9
15
 
10
16
  describe('composables: ResourceDetailDrawer', () => {
11
17
  const resource = { type: 'RESOURCE' };
@@ -78,4 +84,47 @@ describe('composables: ResourceDetailDrawer', () => {
78
84
  expect(props?.resource).toStrictEqual(resource);
79
85
  });
80
86
  });
87
+
88
+ describe('useResourceDetailDrawerProvider', () => {
89
+ beforeEach(() => {
90
+ jest.clearAllMocks();
91
+ });
92
+
93
+ it('should call provide with the correct key and value', () => {
94
+ useResourceDetailDrawerProvider();
95
+
96
+ expect(provide).toHaveBeenCalledWith('isInResourceDetailDrawerKey', true);
97
+ });
98
+ });
99
+
100
+ describe('useIsInResourceDetailDrawer', () => {
101
+ beforeEach(() => {
102
+ jest.clearAllMocks();
103
+ });
104
+
105
+ it('should call inject with the correct key and default value', () => {
106
+ (inject as jest.Mock).mockReturnValue(false);
107
+
108
+ const result = useIsInResourceDetailDrawer();
109
+
110
+ expect(inject).toHaveBeenCalledWith('isInResourceDetailDrawerKey', false);
111
+ expect(result).toBe(false);
112
+ });
113
+
114
+ it('should return true when inside a ResourceDetailDrawer', () => {
115
+ (inject as jest.Mock).mockReturnValue(true);
116
+
117
+ const result = useIsInResourceDetailDrawer();
118
+
119
+ expect(result).toBe(true);
120
+ });
121
+
122
+ it('should return false when not inside a ResourceDetailDrawer', () => {
123
+ (inject as jest.Mock).mockReturnValue(false);
124
+
125
+ const result = useIsInResourceDetailDrawer();
126
+
127
+ expect(result).toBe(false);
128
+ });
129
+ });
81
130
  });
@@ -1,6 +1,7 @@
1
1
  import { useStore } from 'vuex';
2
2
  import { getYaml } from '@shell/components/Drawer/ResourceDetailDrawer/helpers';
3
3
  import { ConfigProps, YamlProps } from '@shell/components/Drawer/ResourceDetailDrawer/types';
4
+ import { inject, provide } from 'vue';
4
5
 
5
6
  export async function useDefaultYamlTabProps(resource: any): Promise<YamlProps> {
6
7
  const yaml = await getYaml(resource);
@@ -27,3 +28,21 @@ export function useDefaultConfigTabProps(resource: any): ConfigProps | undefined
27
28
  resourceType: resource.type
28
29
  };
29
30
  }
31
+
32
+ const IS_IN_RESOURCE_DETAIL_DRAWER_KEY = 'isInResourceDetailDrawerKey';
33
+
34
+ /**
35
+ * Used to add a provide method which will indicate to all ancestors that they're inside the ResourceDetailDrawer. This is useful because we show
36
+ * config page components both independently and within the ResourceDetailDrawer and we sometimes want to distinguish between the two use cases.
37
+ */
38
+ export function useResourceDetailDrawerProvider() {
39
+ provide(IS_IN_RESOURCE_DETAIL_DRAWER_KEY, true);
40
+ }
41
+
42
+ /**
43
+ * A composable used to determine if the current component was instantiated as an ancestor of a ResourceDetailDrawer.
44
+ * @returns true if the component is an ancestor of ResourceDetailDrawer, otherwise false
45
+ */
46
+ export function useIsInResourceDetailDrawer() {
47
+ return inject(IS_IN_RESOURCE_DETAIL_DRAWER_KEY, false);
48
+ }
@@ -4,7 +4,7 @@ import { useI18n } from '@shell/composables/useI18n';
4
4
  import { useStore } from 'vuex';
5
5
  import Tabbed from '@shell/components/Tabbed/index.vue';
6
6
  import YamlTab, { Props as YamlProps } from '@shell/components/Drawer/ResourceDetailDrawer/YamlTab.vue';
7
- import { useDefaultConfigTabProps, useDefaultYamlTabProps } from '@shell/components/Drawer/ResourceDetailDrawer/composables';
7
+ import { useDefaultConfigTabProps, useDefaultYamlTabProps, useResourceDetailDrawerProvider } from '@shell/components/Drawer/ResourceDetailDrawer/composables';
8
8
  import ConfigTab from '@shell/components/Drawer/ResourceDetailDrawer/ConfigTab.vue';
9
9
  import { computed, ref } from 'vue';
10
10
  import RcButton from '@components/RcButton/RcButton.vue';
@@ -54,6 +54,8 @@ const canEdit = computed(() => {
54
54
  return isConfig.value ? props.resource.canEdit : props.resource.canEditYaml;
55
55
  });
56
56
 
57
+ useResourceDetailDrawerProvider();
58
+
57
59
  </script>
58
60
  <template>
59
61
  <Drawer
@@ -79,6 +81,7 @@ const canEdit = computed(() => {
79
81
  <ConfigTab
80
82
  v-if="configTabProps"
81
83
  v-bind="configTabProps"
84
+ :default-tab="props.defaultTab"
82
85
  />
83
86
  <YamlTab
84
87
  v-if="yamlTabProps"
@@ -7,10 +7,11 @@ export interface ConfigProps {
7
7
  resource: any;
8
8
  component: any;
9
9
  resourceType: string;
10
+ defaultTab?: string;
10
11
  }
11
12
 
12
13
  export interface ResourceDetailDrawerProps {
13
14
  resource: any;
14
-
15
+ defaultTab?: string;
15
16
  onClose?: () => void;
16
17
  }
@@ -45,11 +45,11 @@ export default {
45
45
  },
46
46
 
47
47
  showLocale() {
48
- return (this.availableLocales && Object.keys(this.availableLocales).length > 1) || this.dev;
48
+ return (this.availableLocales && Object.keys(this.availableLocales).length > 1) || this.showNone;
49
49
  },
50
50
 
51
51
  showNone() {
52
- return !!process.env.dev && this.dev;
52
+ return !!process.env.dev;
53
53
  },
54
54
  },
55
55
 
@@ -5,6 +5,7 @@ import { useStore } from 'vuex';
5
5
  import AppModal from '@shell/components/AppModal.vue';
6
6
 
7
7
  const store = useStore();
8
+ const componentRendered = ref(false);
8
9
 
9
10
  const isOpen = computed(() => store.getters['modal/isOpen']);
10
11
  const component = computed(() => store.getters['modal/component']);
@@ -23,12 +24,20 @@ function close() {
23
24
  backgroundClosing.value();
24
25
  }
25
26
 
27
+ componentRendered.value = false;
26
28
  store.commit('modal/closeModal');
27
29
  }
28
30
 
29
31
  function registerBackgroundClosing(fn: Function) {
30
32
  backgroundClosing.value = fn;
31
33
  }
34
+
35
+ function onSlotComponentMounted() {
36
+ // variable for the watcher based focus-trap
37
+ // so that we know when the component is rendered
38
+ // works in tandem with trigger-focus-trap="true"
39
+ componentRendered.value = true;
40
+ }
32
41
  </script>
33
42
 
34
43
  <template>
@@ -39,7 +48,7 @@ function registerBackgroundClosing(fn: Function) {
39
48
  :width="modalWidth"
40
49
  :style="{ '--prompt-modal-width': modalWidth }"
41
50
  :trigger-focus-trap="true"
42
- tabindex="0"
51
+ :focus-trap-watcher-based-variable="componentRendered"
43
52
  @close="close"
44
53
  >
45
54
  <component
@@ -48,6 +57,7 @@ function registerBackgroundClosing(fn: Function) {
48
57
  data-testid="modal-manager-component"
49
58
  :resources="resources"
50
59
  :register-background-closing="registerBackgroundClosing"
60
+ @vue:mounted="onSlotComponentMounted"
51
61
  @close="close"
52
62
  />
53
63
  </app-modal>
@@ -118,7 +118,7 @@ describe('the yaml Component', () => {
118
118
 
119
119
  expect(inputFields).toHaveLength(1);
120
120
 
121
- const labelFields = wrapper.findAll('[data-testid="yaml-row-var_name"] .v-popper--has-tooltip');
121
+ const labelFields = wrapper.findAll('[data-testid="yaml-row-var_name"] .has-clean-tooltip');
122
122
 
123
123
  expect(labelFields).toHaveLength(1);
124
124
  });
@@ -0,0 +1,159 @@
1
+ import { shallowMount } from '@vue/test-utils';
2
+ import Questions from '@shell/components/Questions/index.vue';
3
+
4
+ const mockT = jest.fn((key) => key);
5
+ const mockWithFallback = jest.fn((key, args, fallback) => fallback || key);
6
+
7
+ const defaultMocks = {
8
+ $fetchState: { pending: false },
9
+ $store: {
10
+ getters: {
11
+ 'i18n/t': mockT,
12
+ 'i18n/withFallback': mockWithFallback
13
+ }
14
+ }
15
+ };
16
+
17
+ const defaultProps = {
18
+ source: { questions: { questions: [] } },
19
+ value: {},
20
+ targetNamespace: 'default',
21
+ mode: 'edit',
22
+ };
23
+
24
+ describe('component: Questions', () => {
25
+ describe('computed: groups', () => {
26
+ it('should group questions with the same group into a single group', () => {
27
+ const questions = [
28
+ { variable: 'q1', group: 'Group 1' },
29
+ { variable: 'q2', group: 'Group 1' },
30
+ ];
31
+ const wrapper = shallowMount(Questions, {
32
+ props: { ...defaultProps, source: { questions: { questions } } },
33
+ global: { mocks: defaultMocks },
34
+ });
35
+
36
+ const groups = wrapper.vm.groups;
37
+
38
+ expect(groups).toHaveLength(1);
39
+ expect(groups[0].name).toBe('Group 1');
40
+ expect(groups[0].questions).toHaveLength(2);
41
+ });
42
+
43
+ it('should place questions without a group into a default group', () => {
44
+ const questions = [
45
+ { variable: 'q1' },
46
+ { variable: 'q2' },
47
+ ];
48
+ const wrapper = shallowMount(Questions, {
49
+ props: { ...defaultProps, source: { questions: { questions } } },
50
+ global: { mocks: defaultMocks },
51
+ });
52
+
53
+ const groups = wrapper.vm.groups;
54
+
55
+ expect(groups).toHaveLength(1);
56
+ expect(groups[0].name).toBe('Questions'); // Default group name
57
+ expect(groups[0].questions).toHaveLength(2);
58
+ });
59
+
60
+ it('should create multiple groups for questions with different groups', () => {
61
+ const questions = [
62
+ { variable: 'q1', group: 'Group 1' },
63
+ { variable: 'q2', group: 'Group 2' },
64
+ ];
65
+ const wrapper = shallowMount(Questions, {
66
+ props: { ...defaultProps, source: { questions: { questions } } },
67
+ global: { mocks: defaultMocks },
68
+ });
69
+
70
+ const groups = wrapper.vm.groups;
71
+
72
+ expect(groups).toHaveLength(2);
73
+ });
74
+
75
+ it('should correctly group a mix of grouped and ungrouped questions', () => {
76
+ const questions = [
77
+ { variable: 'q1', group: 'Group 1' },
78
+ { variable: 'q2' },
79
+ { variable: 'q3', group: 'Group 1' },
80
+ ];
81
+ const wrapper = shallowMount(Questions, {
82
+ props: { ...defaultProps, source: { questions: { questions } } },
83
+ global: { mocks: defaultMocks },
84
+ });
85
+
86
+ const groups = wrapper.vm.groups;
87
+
88
+ expect(groups).toHaveLength(2); // 'Group 1' and 'Questions'
89
+ const group1 = groups.find((g: any) => g.name === 'Group 1');
90
+ const defaultGroup = groups.find((g: any) => g.name === 'Questions');
91
+
92
+ expect(group1.questions).toHaveLength(2);
93
+ expect(defaultGroup.questions).toHaveLength(1);
94
+ });
95
+ });
96
+
97
+ describe('computed: asTabs', () => {
98
+ it('should be true by default', () => {
99
+ const wrapper = shallowMount(Questions, {
100
+ props: defaultProps,
101
+ global: { mocks: defaultMocks },
102
+ });
103
+
104
+ expect(wrapper.vm.asTabs).toBe(true);
105
+ });
106
+
107
+ it('should be true when tabbed is true', () => {
108
+ const wrapper = shallowMount(Questions, {
109
+ props: { ...defaultProps, tabbed: true },
110
+ global: { mocks: defaultMocks },
111
+ });
112
+
113
+ expect(wrapper.vm.asTabs).toBe(true);
114
+ });
115
+
116
+ it('should be false when tabbed is false', () => {
117
+ const wrapper = shallowMount(Questions, {
118
+ props: { ...defaultProps, tabbed: false },
119
+ global: { mocks: defaultMocks },
120
+ });
121
+
122
+ expect(wrapper.vm.asTabs).toBe(false);
123
+ });
124
+
125
+ it('should be false when tabbed is "never"', () => {
126
+ const wrapper = shallowMount(Questions, {
127
+ props: { ...defaultProps, tabbed: 'never' },
128
+ global: { mocks: defaultMocks },
129
+ });
130
+
131
+ expect(wrapper.vm.asTabs).toBe(false);
132
+ });
133
+
134
+ describe('when tabbed is "multiple"', () => {
135
+ it('should be true if there are groups', () => {
136
+ const questions = [{ variable: 'q1', group: 'Group 1' }];
137
+ const wrapper = shallowMount(Questions, {
138
+ props: {
139
+ ...defaultProps, source: { questions: { questions } }, tabbed: 'multiple'
140
+ },
141
+ global: { mocks: defaultMocks },
142
+ });
143
+
144
+ expect(wrapper.vm.groups).toHaveLength(1);
145
+ expect(wrapper.vm.asTabs).toBe(true);
146
+ });
147
+
148
+ it('should be false if there are no groups', () => {
149
+ const wrapper = shallowMount(Questions, {
150
+ props: { ...defaultProps, tabbed: 'multiple' },
151
+ global: { mocks: defaultMocks },
152
+ });
153
+
154
+ expect(wrapper.vm.groups).toHaveLength(0);
155
+ expect(wrapper.vm.asTabs).toBe(false);
156
+ });
157
+ });
158
+ });
159
+ });
@@ -102,6 +102,11 @@ export default {
102
102
  }
103
103
  };
104
104
 
105
+ // Having an undefined param can yield a console warning like [Vue Router warn]: Discarded invalid param(s) "namespace" when navigating
106
+ if (!detailLocation.params.namespace) {
107
+ delete detailLocation.params.namespace;
108
+ }
109
+
105
110
  out.push({
106
111
  type,
107
112
  id: r[`${ this.direction }Id`],
@@ -8,7 +8,7 @@ export type Annotation = Row;
8
8
  export interface AnnotationsProps {
9
9
  annotations: Annotation[];
10
10
 
11
- onShowConfiguration?: (returnFocusSelector: string) => void;
11
+ onShowConfiguration?: (returnFocusSelector: string, defaultTab: string) => void;
12
12
  }
13
13
 
14
14
  </script>
@@ -26,6 +26,6 @@ const i18n = useI18n(store);
26
26
  :rows="annotations"
27
27
  type="active"
28
28
 
29
- @show-configuration="(returnFocusSelector: string) => emit('show-configuration', returnFocusSelector)"
29
+ @show-configuration="(returnFocusSelector: string) => emit('show-configuration', returnFocusSelector, 'labels-and-annotations')"
30
30
  />
31
31
  </template>
@@ -8,7 +8,7 @@ export type Label = Row;
8
8
  export interface LabelsProps {
9
9
  labels: Label[];
10
10
 
11
- onShowConfiguration?: (returnFocusSelector: string) => void;
11
+ onShowConfiguration?: (returnFocusSelector: string, defaultTab: string) => void;
12
12
  }
13
13
 
14
14
  </script>
@@ -27,6 +27,6 @@ const i18n = useI18n(store);
27
27
  :propertyName="i18n.t('component.resource.detail.metadata.labels.title')"
28
28
  :rows="labels"
29
29
  type="active"
30
- @show-configuration="(returnFocusSelector: string) => emit('show-configuration', returnFocusSelector)"
30
+ @show-configuration="(returnFocusSelector: string) => emit('show-configuration', returnFocusSelector, 'labels-and-annotations')"
31
31
  />
32
32
  </template>
@@ -48,7 +48,7 @@ const showBothEmpty = computed(() => labels.length === 0 && annotations.length =
48
48
  type="active"
49
49
  :rows="[]"
50
50
  :propertyName="i18n.t('component.resource.detail.metadata.labelsAndAnnotations')"
51
- @show-configuration="(returnFocusSelector: string) => emit('show-configuration', returnFocusSelector)"
51
+ @show-configuration="(returnFocusSelector: string, defaultTab: string) => emit('show-configuration', returnFocusSelector, defaultTab)"
52
52
  />
53
53
  </div>
54
54
  <!-- I'm not using v-else here so I can maintain the spacing correctly with the other columns in other rows. -->
@@ -58,7 +58,7 @@ const showBothEmpty = computed(() => labels.length === 0 && annotations.length =
58
58
  >
59
59
  <Labels
60
60
  :labels="labels"
61
- @show-configuration="(returnFocusSelector: string) => emit('show-configuration', returnFocusSelector)"
61
+ @show-configuration="(returnFocusSelector: string, defaultTab: string) => emit('show-configuration', returnFocusSelector, defaultTab)"
62
62
  />
63
63
  </div>
64
64
  <div
@@ -67,7 +67,7 @@ const showBothEmpty = computed(() => labels.length === 0 && annotations.length =
67
67
  >
68
68
  <Annotations
69
69
  :annotations="annotations"
70
- @show-configuration="(returnFocusSelector: string) => emit('show-configuration', returnFocusSelector)"
70
+ @show-configuration="(returnFocusSelector: string, defaultTab: string) => emit('show-configuration', returnFocusSelector, defaultTab)"
71
71
  />
72
72
  </div>
73
73
  </SpacedRow>
@@ -31,6 +31,10 @@ const fetch = useFetch(async() => {
31
31
  return r;
32
32
  });
33
33
 
34
+ const stateBackground = computed(() => {
35
+ return fetch.value.data?.stateSimpleColor || 'unknown';
36
+ });
37
+
34
38
  const resourceTypeLabel = computed(() => {
35
39
  if (!fetch.value.data) {
36
40
  return '';
@@ -67,7 +71,7 @@ const actionInvoked = () => {
67
71
  >
68
72
  <RcStatusIndicator
69
73
  shape="disc"
70
- :status="fetch.data?.stateBackground || 'unknown'"
74
+ :status="stateBackground"
71
75
  />
72
76
  <router-link
73
77
  :to="props.detailLocation || fetch.data.detailLocation || '#'"
@@ -46,12 +46,12 @@ export const useResourceDetailBannerProps = (resource: any): Ref<BannerProps | u
46
46
  };
47
47
 
48
48
  export const useOnShowConfiguration = (resource: any) => {
49
- return (returnFocusSelector?: string) => {
49
+ return (returnFocusSelector?: string, defaultTab?: string) => {
50
50
  const resourceValue = toValue(resource);
51
51
  // Because extensions can make a copy of the resource-class it's possible that an extension will have a resource-class which predates the inclusion of showConfiguration
52
52
  // to still the rest of shell to consume
53
53
  const showConfiguration = resourceValue.showConfiguration ? resourceValue.showConfiguration.bind(resourceValue) : ResourceClass.prototype.showConfiguration.bind(resourceValue);
54
54
 
55
- showConfiguration(returnFocusSelector);
55
+ showConfiguration(returnFocusSelector, defaultTab);
56
56
  };
57
57
  };
@@ -42,27 +42,29 @@ const store = useStore();
42
42
  </script>
43
43
 
44
44
  <template>
45
- <TitleBar v-bind="titleBarProps" />
46
- <Banner
47
- v-if="bannerProps"
48
- v-ui-context="{
49
- store: store,
50
- icon: 'icon-info',
51
- hookable: true,
52
- value: {
53
- bannerProps,
54
- resource: uiCtxResource
55
- },
56
- tag: '__details-state-banner',
57
- description: 'Status Message'
58
- }"
59
- class="new state-banner"
60
- v-bind="bannerProps"
61
- />
62
- <Metadata
63
- v-bind="metadataProps"
64
- class="mmt-4"
65
- />
45
+ <div>
46
+ <TitleBar v-bind="titleBarProps" />
47
+ <Banner
48
+ v-if="bannerProps"
49
+ v-ui-context="{
50
+ store: store,
51
+ icon: 'icon-info',
52
+ hookable: true,
53
+ value: {
54
+ bannerProps,
55
+ resource: uiCtxResource
56
+ },
57
+ tag: '__details-state-banner',
58
+ description: 'Status Message'
59
+ }"
60
+ class="new state-banner"
61
+ v-bind="bannerProps"
62
+ />
63
+ <Metadata
64
+ v-bind="metadataProps"
65
+ class="mmt-4"
66
+ />
67
+ </div>
66
68
  </template>
67
69
 
68
70
  <style lang="scss" scoped>
@@ -14,6 +14,7 @@ import { clone, diff } from '@shell/utils/object';
14
14
  import IconMessage from '@shell/components/IconMessage';
15
15
  import { stringify } from '@shell/utils/error';
16
16
  import { Banner } from '@components/Banner';
17
+ import { useResourceDetailPageProvider } from '@shell/composables/resourceDetail';
17
18
 
18
19
  function modeFor(route) {
19
20
  if ( route.query?.mode === _IMPORT ) {
@@ -116,6 +117,7 @@ export default {
116
117
 
117
118
  if ( mode === _VIEW && hasCustomDetail && (!requested || requested === _DETAIL) ) {
118
119
  as = _DETAIL;
120
+ useResourceDetailPageProvider();
119
121
  } else if ( hasCustomEdit && (!requested || requested === _CONFIG) ) {
120
122
  as = _CONFIG;
121
123
  } else {
@@ -351,6 +353,7 @@ export default {
351
353
 
352
354
  methods: {
353
355
  stringify,
356
+
354
357
  setSubtype(subtype) {
355
358
  this.resourceSubtype = subtype;
356
359
  },