@rancher/shell 3.0.5-rc.5 → 3.0.5-rc.6

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 (312) hide show
  1. package/assets/data/aws-regions.json +1 -0
  2. package/assets/images/key.svg +17 -0
  3. package/assets/styles/base/_spacing.scss +2 -2
  4. package/assets/styles/global/_form.scss +1 -1
  5. package/assets/styles/global/_labeled-input.scss +1 -1
  6. package/assets/styles/themes/_dark.scss +3 -0
  7. package/assets/styles/themes/_light.scss +3 -0
  8. package/assets/styles/vendor/vue-select.scss +1 -1
  9. package/assets/translations/en-us.yaml +404 -64
  10. package/assets/translations/zh-hans.yaml +3 -4
  11. package/cloud-credential/gcp.vue +9 -1
  12. package/components/AppModal.vue +2 -0
  13. package/components/CodeMirror.vue +1 -1
  14. package/components/ConfigMapSettings/Settings.vue +377 -0
  15. package/components/ConfigMapSettings/index.vue +354 -0
  16. package/components/CruResource.vue +1 -2
  17. package/components/DetailText.vue +61 -11
  18. package/components/Drawer/Chrome.vue +116 -0
  19. package/components/Drawer/ResourceDetailDrawer/ConfigTab.vue +61 -0
  20. package/components/Drawer/ResourceDetailDrawer/YamlTab.vue +48 -0
  21. package/components/Drawer/ResourceDetailDrawer/__tests__/ConfigTab.test.ts +54 -0
  22. package/components/Drawer/ResourceDetailDrawer/__tests__/YamlTab.test.ts +80 -0
  23. package/components/Drawer/ResourceDetailDrawer/__tests__/composables.test.ts +82 -0
  24. package/components/Drawer/ResourceDetailDrawer/__tests__/helpers.test.ts +42 -0
  25. package/components/Drawer/ResourceDetailDrawer/composables.ts +50 -0
  26. package/components/Drawer/ResourceDetailDrawer/helpers.ts +10 -0
  27. package/components/Drawer/ResourceDetailDrawer/index.vue +110 -0
  28. package/components/GrowlManager.vue +16 -15
  29. package/components/IconOrSvg.vue +5 -0
  30. package/components/KeyValueView.vue +1 -1
  31. package/components/LocaleSelector.vue +9 -1
  32. package/components/ProgressBarMulti.vue +1 -0
  33. package/components/PromptModal.vue +6 -1
  34. package/components/RelatedResources.vue +4 -12
  35. package/components/Resource/Detail/Additional.vue +46 -0
  36. package/components/Resource/Detail/Metadata/Annotations/__tests__/index.test.ts +1 -1
  37. package/components/Resource/Detail/Metadata/Annotations/index.vue +5 -0
  38. package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/identifying-fields.test.ts +223 -0
  39. package/components/Resource/Detail/Metadata/IdentifyingInformation/composable.ts +37 -254
  40. package/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields.ts +298 -0
  41. package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +27 -5
  42. package/components/Resource/Detail/Metadata/KeyValue.vue +25 -17
  43. package/components/Resource/Detail/Metadata/Labels/__tests__/index.test.ts +1 -1
  44. package/components/Resource/Detail/Metadata/Labels/index.vue +4 -0
  45. package/components/Resource/Detail/Metadata/__tests__/KeyValue.test.ts +1 -1
  46. package/components/Resource/Detail/Metadata/__tests__/Rectangle.test.ts +1 -1
  47. package/components/Resource/Detail/Metadata/__tests__/composables.test.ts +75 -0
  48. package/components/Resource/Detail/Metadata/composables.ts +60 -11
  49. package/components/Resource/Detail/Metadata/index.vue +12 -5
  50. package/components/Resource/Detail/Page.vue +15 -0
  51. package/components/Resource/Detail/ResourceRow.vue +37 -18
  52. package/components/Resource/Detail/ResourceTabs/ConfigMapDataTab/__tests__/composables.test.ts +29 -0
  53. package/components/Resource/Detail/ResourceTabs/ConfigMapDataTab/__tests__/index.test.ts +48 -0
  54. package/components/Resource/Detail/ResourceTabs/ConfigMapDataTab/composables.ts +31 -0
  55. package/components/Resource/Detail/ResourceTabs/ConfigMapDataTab/index.vue +50 -0
  56. package/components/Resource/Detail/ResourceTabs/KnownHostsTab/__tests__/composables.test.ts +66 -0
  57. package/components/Resource/Detail/ResourceTabs/KnownHostsTab/composables.ts +21 -0
  58. package/components/Resource/Detail/ResourceTabs/KnownHostsTab/index.vue +31 -0
  59. package/components/Resource/Detail/ResourceTabs/SecretDataTab/Basic.vue +45 -0
  60. package/components/Resource/Detail/ResourceTabs/SecretDataTab/BasicAuth.vue +31 -0
  61. package/components/Resource/Detail/ResourceTabs/SecretDataTab/Certificate.vue +31 -0
  62. package/components/Resource/Detail/ResourceTabs/SecretDataTab/Registry.vue +22 -0
  63. package/components/Resource/Detail/ResourceTabs/SecretDataTab/ServiceAccountToken.vue +31 -0
  64. package/components/Resource/Detail/ResourceTabs/SecretDataTab/Ssh.vue +32 -0
  65. package/components/Resource/Detail/ResourceTabs/SecretDataTab/__tests__/Basic.test.ts +40 -0
  66. package/components/Resource/Detail/ResourceTabs/SecretDataTab/__tests__/BasicAuth.test.ts +33 -0
  67. package/components/Resource/Detail/ResourceTabs/SecretDataTab/__tests__/Certificate.test.ts +33 -0
  68. package/components/Resource/Detail/ResourceTabs/SecretDataTab/__tests__/Registry.test.ts +27 -0
  69. package/components/Resource/Detail/ResourceTabs/SecretDataTab/__tests__/ServiceAccountToken.test.ts +33 -0
  70. package/components/Resource/Detail/ResourceTabs/SecretDataTab/__tests__/Ssh.test.ts +33 -0
  71. package/components/Resource/Detail/ResourceTabs/SecretDataTab/__tests__/auth-types.test.ts +186 -0
  72. package/components/Resource/Detail/ResourceTabs/SecretDataTab/__tests__/composables.test.ts +102 -0
  73. package/components/Resource/Detail/ResourceTabs/SecretDataTab/auth-types.ts +109 -0
  74. package/components/Resource/Detail/ResourceTabs/SecretDataTab/composeables.ts +52 -0
  75. package/components/Resource/Detail/ResourceTabs/SecretDataTab/index.vue +71 -0
  76. package/components/Resource/Detail/TitleBar/Title.vue +2 -1
  77. package/components/Resource/Detail/TitleBar/__tests__/Title.test.ts +1 -1
  78. package/components/Resource/Detail/TitleBar/__tests__/Top.test.ts +1 -1
  79. package/components/Resource/Detail/TitleBar/__tests__/composables.test.ts +63 -0
  80. package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +1 -1
  81. package/components/Resource/Detail/TitleBar/composables.ts +44 -0
  82. package/components/Resource/Detail/TitleBar/index.vue +83 -11
  83. package/components/Resource/Detail/composables.ts +45 -0
  84. package/components/ResourceDetail/Masthead/__tests__/index.test.ts +70 -0
  85. package/components/ResourceDetail/{__tests__/Masthead.test.ts → Masthead/__tests__/legacy.test.ts} +3 -3
  86. package/components/ResourceDetail/Masthead/index.vue +65 -0
  87. package/components/ResourceDetail/Masthead/latest.vue +44 -0
  88. package/components/ResourceDetail/__tests__/index.test.ts +26 -5
  89. package/components/ResourceDetail/index.vue +30 -16
  90. package/components/ResourceDetail/legacy.vue +18 -1
  91. package/components/ResourceList/Masthead.vue +6 -0
  92. package/components/ResourceYaml.vue +14 -1
  93. package/components/SlideInPanelManager.vue +46 -7
  94. package/components/StateDot/index.vue +28 -0
  95. package/components/Tabbed/index.vue +11 -15
  96. package/components/Wizard.vue +4 -2
  97. package/components/__tests__/ConfigMapSettings.test.ts +376 -0
  98. package/components/__tests__/GrowlManager.test.ts +0 -25
  99. package/components/auth/login/ldap.vue +1 -1
  100. package/components/fleet/FleetApplications.vue +0 -7
  101. package/components/fleet/FleetClusterTargets/TargetsList.vue +66 -0
  102. package/components/fleet/FleetClusterTargets/index.vue +455 -0
  103. package/components/fleet/FleetClusters.vue +25 -6
  104. package/components/fleet/FleetGitRepoPaths.vue +476 -0
  105. package/components/fleet/FleetHelmOps.vue +8 -0
  106. package/components/fleet/FleetRepos.vue +1 -6
  107. package/components/fleet/FleetResources.vue +4 -5
  108. package/components/fleet/FleetValuesFrom.vue +295 -0
  109. package/components/fleet/__tests__/FleetClusterTargets.test.ts +1224 -0
  110. package/components/fleet/__tests__/FleetGitRepoPaths.test.ts +265 -0
  111. package/components/fleet/__tests__/FleetOCIStorageSecret.test.ts +13 -13
  112. package/components/fleet/__tests__/FleetValuesFrom.test.ts +300 -0
  113. package/components/fleet/dashboard/ResourceCard.vue +1 -0
  114. package/components/fleet/dashboard/ResourceCardSummary.vue +1 -5
  115. package/components/fleet/dashboard/ResourceDetails.vue +8 -10
  116. package/components/fleet/dashboard/ResourcePanel.vue +15 -8
  117. package/components/form/ArrayList.vue +13 -2
  118. package/components/form/ChangePassword.vue +3 -1
  119. package/components/form/Footer.vue +10 -4
  120. package/components/form/KeyValue.vue +81 -43
  121. package/components/form/LabeledSelect.vue +56 -16
  122. package/components/form/Labels.vue +90 -17
  123. package/components/form/MatchExpressions.vue +46 -5
  124. package/components/form/NameNsDescription.vue +1 -1
  125. package/components/form/ResourceSelector.vue +1 -0
  126. package/components/form/ResourceTabs/index.vue +5 -0
  127. package/components/form/SecretSelector.vue +9 -2
  128. package/components/form/Select.vue +57 -19
  129. package/components/form/SimpleSecretSelector.vue +9 -2
  130. package/components/form/Taints.vue +21 -2
  131. package/components/form/UnitInput.vue +8 -0
  132. package/components/form/ValueFromResource.vue +1 -1
  133. package/components/form/__tests__/LabeledSelect.test.ts +8 -4
  134. package/components/form/__tests__/Labels.test.ts +360 -0
  135. package/components/form/__tests__/MatchExpressions.test.ts +16 -13
  136. package/components/form/__tests__/Select.test.ts +5 -2
  137. package/components/formatter/FleetApplicationSource.vue +1 -1
  138. package/components/formatter/WorkloadHealthScale.vue +1 -1
  139. package/components/google/AccountAccess.vue +211 -0
  140. package/components/google/types/gcp.d.ts +136 -0
  141. package/components/google/types/index.d.ts +101 -0
  142. package/components/google/util/__mocks__/gcp.ts +465 -0
  143. package/components/google/util/formatter.ts +82 -0
  144. package/components/google/util/gcp.ts +134 -0
  145. package/components/google/util/index.d.ts +11 -0
  146. package/components/nav/Favorite.vue +1 -1
  147. package/components/nav/Group.vue +70 -47
  148. package/components/nav/Header.vue +5 -1
  149. package/components/nav/NamespaceFilter.vue +13 -1
  150. package/components/nav/NotificationCenter/Notification.vue +510 -0
  151. package/components/nav/NotificationCenter/NotificationHeader.vue +112 -0
  152. package/components/nav/NotificationCenter/index.vue +148 -0
  153. package/composables/drawer.ts +26 -0
  154. package/composables/resources.test.ts +63 -0
  155. package/composables/resources.ts +38 -0
  156. package/composables/useIsNewDetailPageEnabled.ts +17 -0
  157. package/config/labels-annotations.js +6 -0
  158. package/config/product/auth.js +16 -1
  159. package/config/product/{cis.js → compliance.js} +23 -26
  160. package/config/product/explorer.js +5 -1
  161. package/config/product/fleet.js +7 -0
  162. package/config/product/settings.js +22 -11
  163. package/config/query-params.js +3 -0
  164. package/config/roles.ts +1 -1
  165. package/config/router/navigation-guards/authentication.js +51 -2
  166. package/config/router/routes.js +27 -31
  167. package/config/settings.ts +21 -3
  168. package/config/store.js +2 -0
  169. package/config/system-namespaces.js +1 -1
  170. package/config/table-headers.js +2 -2
  171. package/config/types.js +15 -6
  172. package/core/plugin.ts +32 -7
  173. package/core/types.ts +18 -1
  174. package/detail/{cis.cattle.io.clusterscan.vue → compliance.cattle.io.clusterscan.vue} +22 -18
  175. package/detail/management.cattle.io.fleetworkspace.vue +18 -27
  176. package/detail/management.cattle.io.oidcclient.vue +369 -0
  177. package/detail/node.vue +2 -2
  178. package/detail/pod.vue +2 -2
  179. package/detail/service.vue +10 -1
  180. package/detail/workload/index.vue +8 -2
  181. package/dialog/ExtensionCatalogUninstallDialog.vue +7 -4
  182. package/dialog/GenericPrompt.vue +1 -1
  183. package/dialog/ImportDialog.vue +8 -8
  184. package/dialog/OidcClientSecretDialog.vue +117 -0
  185. package/edit/__tests__/cis.cattle.io.clusterscan.test.ts +3 -3
  186. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +5 -2
  187. package/edit/autoscaling.horizontalpodautoscaler/index.vue +4 -1
  188. package/edit/{cis.cattle.io.clusterscan.vue → compliance.cattle.io.clusterscan.vue} +30 -31
  189. package/edit/{cis.cattle.io.clusterscanbenchmark.vue → compliance.cattle.io.clusterscanbenchmark.vue} +4 -4
  190. package/edit/{cis.cattle.io.clusterscanprofile.vue → compliance.cattle.io.clusterscanprofile.vue} +5 -5
  191. package/edit/configmap.vue +4 -1
  192. package/edit/constraints.gatekeeper.sh.constraint/index.vue +1 -0
  193. package/edit/fleet.cattle.io.gitrepo.vue +44 -222
  194. package/edit/fleet.cattle.io.helmop.vue +44 -269
  195. package/edit/helm.cattle.io.projecthelmchart.vue +1 -0
  196. package/edit/k8s.cni.cncf.io.networkattachmentdefinition.vue +1 -0
  197. package/edit/logging-flow/index.vue +1 -0
  198. package/edit/logging.banzaicloud.io.output/index.vue +1 -0
  199. package/edit/management.cattle.io.fleetworkspace.vue +1 -0
  200. package/edit/management.cattle.io.oidcclient.vue +162 -0
  201. package/edit/management.cattle.io.project.vue +4 -1
  202. package/edit/monitoring.coreos.com.alertmanagerconfig/index.vue +1 -1
  203. package/edit/monitoring.coreos.com.alertmanagerconfig/receiverConfig.vue +5 -0
  204. package/edit/monitoring.coreos.com.prometheusrule/index.vue +1 -0
  205. package/edit/monitoring.coreos.com.receiver/auth.vue +30 -30
  206. package/edit/monitoring.coreos.com.receiver/index.vue +1 -0
  207. package/edit/monitoring.coreos.com.receiver/types/email.vue +1 -1
  208. package/edit/monitoring.coreos.com.route.vue +1 -0
  209. package/edit/namespace.vue +1 -0
  210. package/edit/networking.istio.io.destinationrule/index.vue +4 -1
  211. package/edit/networking.k8s.io.ingress/index.vue +4 -1
  212. package/edit/networking.k8s.io.networkpolicy/PolicyRules.vue +7 -2
  213. package/edit/networking.k8s.io.networkpolicy/index.vue +6 -2
  214. package/edit/node.vue +1 -0
  215. package/edit/persistentvolume/index.vue +4 -1
  216. package/edit/provisioning.cattle.io.cluster/rke2.vue +418 -382
  217. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +27 -27
  218. package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +5 -0
  219. package/edit/resources.cattle.io.restore.vue +1 -1
  220. package/edit/secret/index.vue +1 -0
  221. package/edit/service.vue +4 -1
  222. package/edit/serviceaccount.vue +4 -1
  223. package/edit/storage.k8s.io.storageclass/index.vue +4 -1
  224. package/edit/workload/index.vue +5 -0
  225. package/list/{cis.cattle.io.clusterscan.vue → compliance.cattle.io.clusterscan.vue} +2 -2
  226. package/list/management.cattle.io.oidcclient.vue +108 -0
  227. package/list/node.vue +2 -0
  228. package/machine-config/amazonec2.vue +3 -24
  229. package/machine-config/components/GCEImage.vue +374 -0
  230. package/machine-config/google.vue +617 -0
  231. package/mixins/__tests__/brand.spec.ts +170 -0
  232. package/mixins/brand.js +16 -17
  233. package/mixins/create-edit-view/index.js +5 -0
  234. package/mixins/resource-fetch-api-pagination.js +16 -0
  235. package/mixins/vue-select-overrides.js +1 -0
  236. package/models/{cis.cattle.io.clusterscan.js → compliance.cattle.io.clusterscan.js} +8 -8
  237. package/models/{cis.cattle.io.clusterscanbenchmark.js → compliance.cattle.io.clusterscanbenchmark.js} +1 -1
  238. package/models/{cis.cattle.io.clusterscanprofile.js → compliance.cattle.io.clusterscanprofile.js} +5 -5
  239. package/models/{cis.cattle.io.clusterscanreport.js → compliance.cattle.io.clusterscanreport.js} +1 -1
  240. package/models/fleet-application.js +8 -79
  241. package/models/fleet.cattle.io.cluster.js +11 -0
  242. package/models/fleet.cattle.io.gitrepo.js +2 -2
  243. package/models/fleet.cattle.io.helmop.js +9 -39
  244. package/models/management.cattle.io.fleetworkspace.js +2 -1
  245. package/models/management.cattle.io.oidcclient.js +18 -0
  246. package/models/management.cattle.io.registration.js +3 -0
  247. package/models/provisioning.cattle.io.cluster.js +5 -5
  248. package/models/service.js +4 -0
  249. package/models/workload.js +5 -0
  250. package/package.json +1 -1
  251. package/pages/about.vue +4 -58
  252. package/pages/auth/login.vue +1 -1
  253. package/pages/c/_cluster/apps/charts/AddRepoLink.vue +0 -1
  254. package/pages/c/_cluster/apps/charts/index.vue +285 -81
  255. package/pages/c/_cluster/auth/user.retention/index.vue +87 -78
  256. package/pages/c/_cluster/explorer/index.vue +3 -3
  257. package/pages/c/_cluster/explorer/tools/pages/_page.vue +0 -1
  258. package/pages/c/_cluster/fleet/application/create.vue +3 -2
  259. package/pages/c/_cluster/fleet/index.vue +94 -56
  260. package/pages/c/_cluster/fleet/settings/index.vue +229 -0
  261. package/pages/c/_cluster/longhorn/index.vue +5 -2
  262. package/pages/c/_cluster/uiplugins/CatalogList/index.vue +16 -1
  263. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +2 -2
  264. package/pages/explorer/resource/detail/configmap.vue +30 -7
  265. package/pages/explorer/resource/detail/secret.vue +50 -0
  266. package/pages/home.vue +9 -55
  267. package/pages/support/index.vue +4 -6
  268. package/plugins/dashboard-store/actions.js +19 -5
  269. package/plugins/dashboard-store/getters.js +4 -0
  270. package/plugins/dashboard-store/resource-class.js +16 -2
  271. package/plugins/steve/steve-pagination-utils.ts +26 -18
  272. package/plugins/steve/subscribe.js +6 -1
  273. package/rancher-components/Banner/Banner.vue +13 -0
  274. package/rancher-components/Form/Checkbox/Checkbox.vue +9 -4
  275. package/rancher-components/Form/LabeledInput/LabeledInput.vue +1 -1
  276. package/rancher-components/LabeledTooltip/LabeledTooltip.vue +1 -0
  277. package/rancher-components/RcItemCard/RcItemCard.vue +8 -3
  278. package/store/auth.js +2 -0
  279. package/store/catalog.js +23 -1
  280. package/store/growl.js +97 -8
  281. package/store/index.js +6 -0
  282. package/store/notifications.ts +426 -0
  283. package/store/prefs.js +0 -1
  284. package/store/type-map.js +19 -16
  285. package/store/uiplugins.ts +15 -1
  286. package/types/fleet.d.ts +24 -0
  287. package/types/notifications/index.ts +74 -0
  288. package/types/shell/index.d.ts +46 -32
  289. package/types/store/dashboard-store.types.ts +16 -0
  290. package/utils/__tests__/fleet.test.ts +148 -0
  291. package/utils/__tests__/object.test.ts +54 -1
  292. package/utils/__tests__/string.test.ts +273 -1
  293. package/utils/__tests__/time.test.ts +31 -0
  294. package/utils/auth.js +9 -2
  295. package/utils/crypto/encryption.ts +103 -0
  296. package/utils/cspAdaptor.ts +51 -0
  297. package/utils/fleet.ts +54 -65
  298. package/utils/object.js +36 -0
  299. package/utils/pagination-utils.ts +1 -1
  300. package/utils/release-notes.ts +48 -0
  301. package/utils/selector-typed.ts +7 -2
  302. package/utils/string.js +24 -0
  303. package/utils/{time.js → time.ts} +25 -6
  304. package/utils/uiplugins.ts +22 -0
  305. package/utils/validators/formRules/index.ts +3 -0
  306. package/components/Resource/Detail/TitleBar/composable.ts +0 -31
  307. package/config/product/legacy.js +0 -62
  308. package/pages/c/_cluster/legacy/pages/_page.vue +0 -29
  309. package/pages/c/_cluster/legacy/project/_page.vue +0 -57
  310. package/pages/c/_cluster/legacy/project/index.vue +0 -32
  311. package/pages/c/_cluster/legacy/project/pipelines.vue +0 -96
  312. /package/components/ResourceDetail/{Masthead.vue → Masthead/legacy.vue} +0 -0
@@ -0,0 +1,48 @@
1
+ <script lang="ts">
2
+ import { useI18n } from '@shell/composables/useI18n';
3
+ import { _VIEW } from '@shell/config/query-params';
4
+ import { useStore } from 'vuex';
5
+ import Tab from '@shell/components/Tabbed/Tab.vue';
6
+ import { useTemplateRef } from 'vue';
7
+ import ResourceYaml from '@shell/components/ResourceYaml.vue';
8
+
9
+ export interface Props {
10
+ resource: any;
11
+ yaml: string;
12
+ }
13
+ </script>
14
+ <script setup lang="ts">
15
+ const props = defineProps<Props>();
16
+ const store = useStore();
17
+ const i18n = useI18n(store);
18
+ const yamlComponent: any = useTemplateRef('yaml');
19
+ </script>
20
+ <template>
21
+ <Tab
22
+ class="yaml-tab"
23
+ name="yaml-tab"
24
+ :label="i18n.t('component.drawer.resourceDetailDrawer.yamlTab.title')"
25
+ @active="() => yamlComponent?.refresh()"
26
+ >
27
+ <ResourceYaml
28
+ ref="yaml"
29
+ :value="props.resource"
30
+ :yaml="props.yaml"
31
+ :mode="_VIEW"
32
+ />
33
+ </Tab>
34
+ </template>
35
+
36
+ <style lang="scss" scoped>
37
+ .yaml-tab {
38
+ :deep() .codemirror-container {
39
+ background-color: var(--body-bg);
40
+ border-radius: var(--border-radius-md);
41
+ padding: 16px;
42
+
43
+ .CodeMirror, .CodeMirror-gutter {
44
+ background-color: var(--body-bg);
45
+ }
46
+ }
47
+ }
48
+ </style>
@@ -0,0 +1,54 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import ConfigTab from '@shell/components/Drawer/ResourceDetailDrawer/ConfigTab.vue';
3
+ import { createStore } from 'vuex';
4
+ import { defineComponent, markRaw } from 'vue';
5
+ import Tab from '@shell/components/Tabbed/Tab.vue';
6
+ import { _VIEW } from '@shell/config/query-params';
7
+
8
+ const DynamicComponent = defineComponent({
9
+ template: '<div>DynamicComponent</div>',
10
+ props: {
11
+ value: { type: Object, required: true },
12
+ mode: { type: String, required: true },
13
+ initialValue: { type: Object, required: true },
14
+ useTabbedHash: { type: Boolean, required: true }
15
+ }
16
+ });
17
+
18
+ describe('component: ResourceDetailDrawer/ConfigTab', () => {
19
+ const resource = { resource: 'RESOURCE' };
20
+ const global = {
21
+ provide: {
22
+ addTab: jest.fn(), removeTab: jest.fn(), sideTabs: false, store: createStore({})
23
+ },
24
+ directives: { 'clean-tooltip': jest.fn() }
25
+
26
+ };
27
+
28
+ it('should render container with config-tab class and correct label and name', async() => {
29
+ const wrapper = mount(ConfigTab, {
30
+ props: { resource, component: markRaw(DynamicComponent) },
31
+ global
32
+ });
33
+
34
+ const component = wrapper.getComponent(Tab);
35
+
36
+ expect(wrapper.classes().includes('config-tab')).toBeTruthy();
37
+ expect(component.props('label')).toStrictEqual('component.drawer.resourceDetailDrawer.configTab.title');
38
+ expect(component.props('name')).toStrictEqual('config-tab');
39
+ });
40
+
41
+ it('should render a dynamic component within the .container and pass the correct props', () => {
42
+ const wrapper = mount(ConfigTab, {
43
+ props: { resource, component: markRaw(DynamicComponent) },
44
+ global
45
+ });
46
+
47
+ const component = wrapper.find('.container').getComponent(DynamicComponent);
48
+
49
+ expect(component.props('value')).toStrictEqual(resource);
50
+ expect(component.props('mode')).toStrictEqual(_VIEW);
51
+ expect(component.props('initialValue')).toStrictEqual(resource);
52
+ expect(component.props('useTabbedHash')).toStrictEqual(false);
53
+ });
54
+ });
@@ -0,0 +1,80 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import YamlTab from '@shell/components/Drawer/ResourceDetailDrawer/YamlTab.vue';
3
+ import { createStore } from 'vuex';
4
+
5
+ import Tab from '@shell/components/Tabbed/Tab.vue';
6
+ import { _VIEW } from '@shell/config/query-params';
7
+ import ResourceYaml from '@shell/components/ResourceYaml.vue';
8
+ import { nextTick } from 'vue';
9
+
10
+ jest.mock('@shell/components/ResourceYaml.vue', () => ({
11
+ template: `<div>ResourceYaml</div>`,
12
+ props: {
13
+ value: {
14
+ type: Object,
15
+ required: true
16
+ },
17
+ yaml: {
18
+ type: String,
19
+ required: true
20
+ },
21
+ mode: {
22
+ type: String,
23
+ required: true
24
+ },
25
+ },
26
+ methods: { refresh: jest.fn() }
27
+ }));
28
+
29
+ describe('component: ResourceDetailDrawer/ConfigTab', () => {
30
+ const resource = { resource: 'RESOURCE' };
31
+ const yaml = 'YAML';
32
+ const global = {
33
+ provide: {
34
+ addTab: jest.fn(), removeTab: jest.fn(), sideTabs: false, store: createStore({})
35
+ },
36
+ directives: { 'clean-tooltip': jest.fn() }
37
+
38
+ };
39
+
40
+ it('should render container with yaml-tab class and correct label and name', async() => {
41
+ const wrapper = mount(YamlTab, {
42
+ props: { resource, yaml },
43
+ global
44
+ });
45
+
46
+ const component = wrapper.getComponent(Tab);
47
+
48
+ expect(wrapper.classes().includes('yaml-tab')).toBeTruthy();
49
+ expect(component.props('label')).toStrictEqual('component.drawer.resourceDetailDrawer.yamlTab.title');
50
+ expect(component.props('name')).toStrictEqual('yaml-tab');
51
+ });
52
+
53
+ it('should render a ResourceYaml component and pass the correct props', () => {
54
+ const wrapper = mount(YamlTab, {
55
+ props: { resource, yaml },
56
+ global
57
+ });
58
+
59
+ const component = wrapper.getComponent(ResourceYaml);
60
+
61
+ expect(component.props('value')).toStrictEqual(resource);
62
+ expect(component.props('yaml')).toStrictEqual(yaml);
63
+ expect(component.props('mode')).toStrictEqual(_VIEW);
64
+ });
65
+
66
+ it('should refresh yaml editor when tab is activated, without it the editor will not resize', async() => {
67
+ const wrapper = mount(YamlTab, {
68
+ props: { resource, yaml },
69
+ global
70
+ });
71
+
72
+ const tabComponent = wrapper.getComponent(Tab);
73
+
74
+ expect(ResourceYaml.methods?.refresh).toHaveBeenCalledTimes(0);
75
+ tabComponent.vm.$emit('active');
76
+ await nextTick();
77
+
78
+ expect(ResourceYaml.methods?.refresh).toHaveBeenCalledTimes(1);
79
+ });
80
+ });
@@ -0,0 +1,82 @@
1
+ import { useDefaultConfigTabProps, useDefaultYamlTabProps, useResourceDetailDrawer } from '@shell/components/Drawer/ResourceDetailDrawer/composables';
2
+ import * as helpers from '@shell/components/Drawer/ResourceDetailDrawer/helpers';
3
+ import * as vuex from 'vuex';
4
+ import * as drawer from '@shell/composables/drawer';
5
+
6
+ jest.mock('@shell/components/Drawer/ResourceDetailDrawer/helpers');
7
+ jest.mock('vuex');
8
+ jest.mock('@shell/composables/drawer');
9
+ jest.mock('@shell/components/Drawer/ResourceDetailDrawer/index.vue', () => ({ name: 'ResourceDetailDrawer' } as any));
10
+
11
+ describe('composables: ResourceDetailDrawer', () => {
12
+ const resource = { type: 'RESOURCE' };
13
+ const yaml = 'YAML';
14
+
15
+ describe('useDefaultYamlTabProps', () => {
16
+ it('should return the appropriate values based on input', async() => {
17
+ const getYamlSpy = jest.spyOn(helpers, 'getYaml').mockImplementation(() => Promise.resolve(yaml));
18
+ const props = await useDefaultYamlTabProps(resource);
19
+
20
+ expect(getYamlSpy).toHaveBeenCalledWith(resource);
21
+ expect(props.yaml).toStrictEqual(yaml);
22
+ expect(props.resource).toStrictEqual(resource);
23
+ });
24
+ });
25
+
26
+ describe('useDefaultConfigTabProps', () => {
27
+ const hasCustomEdit = jest.fn();
28
+ const importEdit = jest.fn();
29
+ const editComponent = { component: 'EDIT_COMPONENT' };
30
+ const store: any = {
31
+ getters: {
32
+ 'type-map/hasCustomEdit': hasCustomEdit,
33
+ 'type-map/importEdit': importEdit
34
+ }
35
+ };
36
+
37
+ it('should return undefined if it does not have a customEdit', async() => {
38
+ jest.spyOn(vuex, 'useStore').mockImplementation(() => store);
39
+ const hasCustomEditSpy = hasCustomEdit.mockImplementation(() => false);
40
+ const props = useDefaultConfigTabProps(resource);
41
+
42
+ expect(hasCustomEditSpy).toHaveBeenCalledWith(resource.type);
43
+ expect(props).toBeUndefined();
44
+ });
45
+
46
+ it('should return props if it has a customEdit', async() => {
47
+ jest.spyOn(vuex, 'useStore').mockImplementation(() => store);
48
+ const hasCustomEditSpy = hasCustomEdit.mockImplementation(() => true);
49
+ const importEditSpy = importEdit.mockImplementation(() => editComponent);
50
+ const props = useDefaultConfigTabProps(resource);
51
+
52
+ expect(hasCustomEditSpy).toHaveBeenCalledWith(resource.type);
53
+ expect(importEditSpy).toHaveBeenCalledWith(resource.type);
54
+ expect(props?.component).toStrictEqual(editComponent);
55
+ expect(props?.resource).toStrictEqual(resource);
56
+ });
57
+ });
58
+
59
+ describe('useResourceDetailDrawer', () => {
60
+ it('should create a wrapper that passes that appropriate properties to the drawer methods', () => {
61
+ const openSpy = jest.fn();
62
+ const closeSpy = jest.fn();
63
+ const selector = '.selector';
64
+ const useDrawerSpy = jest.spyOn(drawer, 'useDrawer').mockImplementation(() => ({ open: openSpy, close: closeSpy }));
65
+ const resourceDetailDrawer = useResourceDetailDrawer();
66
+
67
+ resourceDetailDrawer.openResourceDetailDrawer(resource, selector);
68
+
69
+ expect(useDrawerSpy).toHaveBeenCalledTimes(1);
70
+ expect(openSpy).toHaveBeenCalledWith({ name: 'ResourceDetailDrawer' }, selector, {
71
+ resource,
72
+ onClose: closeSpy,
73
+ width: '73%',
74
+ // We want this to be full viewport height top to bottom
75
+ height: '100vh',
76
+ top: '0',
77
+ 'z-index': 101, // We want this to be above the main side menu
78
+ closeOnRouteChange: ['name', 'params', 'query']
79
+ });
80
+ });
81
+ });
82
+ });
@@ -0,0 +1,42 @@
1
+ import { getYaml } from '@shell/components/Drawer/ResourceDetailDrawer/helpers';
2
+
3
+ describe('helpers: ResourceDetailDrawer', () => {
4
+ describe('getYaml', () => {
5
+ const resource = {
6
+ hasLink: jest.fn(),
7
+ followLink: jest.fn(),
8
+ cleanForDownload: jest.fn()
9
+ };
10
+
11
+ const yaml = 'YAML';
12
+
13
+ beforeEach(() => {
14
+ jest.clearAllMocks();
15
+ });
16
+
17
+ it('should skip following a link if it does not have a view link', async() => {
18
+ resource.hasLink.mockImplementation(() => false);
19
+ resource.cleanForDownload.mockImplementation(() => yaml);
20
+
21
+ const response = await getYaml(resource);
22
+
23
+ expect(resource.hasLink).toHaveBeenCalledWith('view');
24
+ expect(resource.followLink).toHaveBeenCalledTimes(0);
25
+ expect(resource.cleanForDownload).toHaveBeenCalledWith(undefined);
26
+ expect(response).toStrictEqual(yaml);
27
+ });
28
+
29
+ it('should follow link if it has a view link', async() => {
30
+ resource.hasLink.mockImplementation(() => true);
31
+ resource.followLink.mockImplementation(() => Promise.resolve({ data: yaml }));
32
+ resource.cleanForDownload.mockImplementation(() => yaml);
33
+
34
+ const response = await getYaml(resource);
35
+
36
+ expect(resource.hasLink).toHaveBeenCalledWith('view');
37
+ expect(resource.followLink).toHaveBeenCalledWith('view', { headers: { accept: 'application/yaml' } });
38
+ expect(resource.cleanForDownload).toHaveBeenCalledWith(yaml);
39
+ expect(response).toStrictEqual(yaml);
40
+ });
41
+ });
42
+ });
@@ -0,0 +1,50 @@
1
+ import ResourceDetailDrawer from '@shell/components/Drawer/ResourceDetailDrawer/index.vue';
2
+ import { Props as YamlTabProps } from '@shell/components/Drawer/ResourceDetailDrawer/YamlTab.vue';
3
+ import { Props as ConfigTabProps } from '@shell/components/Drawer/ResourceDetailDrawer/ConfigTab.vue';
4
+ import { useStore } from 'vuex';
5
+ import { useDrawer } from '@shell/composables/drawer';
6
+ import { getYaml } from '@shell/components/Drawer/ResourceDetailDrawer/helpers';
7
+
8
+ export function useResourceDetailDrawer() {
9
+ const { open, close } = useDrawer();
10
+
11
+ const openResourceDetailDrawer = (resource: any, returnFocusSelector: string) => {
12
+ open(ResourceDetailDrawer,
13
+ returnFocusSelector,
14
+ {
15
+ resource,
16
+ onClose: close,
17
+ width: '73%',
18
+ // We want this to be full viewport height top to bottom
19
+ height: '100vh',
20
+ top: '0',
21
+ 'z-index': 101, // We want this to be above the main side menu
22
+ closeOnRouteChange: ['name', 'params', 'query'] // We want to ignore hash changes, tables in extensions can trigger the drawer to close while opening
23
+ });
24
+ };
25
+
26
+ return { openResourceDetailDrawer };
27
+ }
28
+
29
+ export async function useDefaultYamlTabProps(resource: any): Promise<YamlTabProps> {
30
+ const yaml = await getYaml(resource);
31
+
32
+ return {
33
+ resource,
34
+ yaml
35
+ };
36
+ }
37
+
38
+ export function useDefaultConfigTabProps(resource: any): ConfigTabProps | undefined {
39
+ const store = useStore();
40
+
41
+ if (!store.getters['type-map/hasCustomEdit'](resource.type)) {
42
+ return;
43
+ }
44
+
45
+ return {
46
+ resource,
47
+ component: store.getters['type-map/importEdit'](resource.type),
48
+ resourceType: resource.type
49
+ };
50
+ }
@@ -0,0 +1,10 @@
1
+ export async function getYaml(resource: any): Promise<string> {
2
+ let yaml;
3
+ const opt = { headers: { accept: 'application/yaml' } };
4
+
5
+ if (resource.hasLink('view')) {
6
+ yaml = (await resource.followLink('view', opt)).data;
7
+ }
8
+
9
+ return resource.cleanForDownload(yaml);
10
+ }
@@ -0,0 +1,110 @@
1
+ <script lang="ts">
2
+ import Drawer from '@shell/components/Drawer/Chrome.vue';
3
+ import { useI18n } from '@shell/composables/useI18n';
4
+ import { useStore } from 'vuex';
5
+ import Tabbed from '@shell/components/Tabbed/index.vue';
6
+ import YamlTab, { Props as YamlProps } from '@shell/components/Drawer/ResourceDetailDrawer/YamlTab.vue';
7
+ import { useDefaultConfigTabProps, useDefaultYamlTabProps } from '@shell/components/Drawer/ResourceDetailDrawer/composables';
8
+ import ConfigTab from '@shell/components/Drawer/ResourceDetailDrawer/ConfigTab.vue';
9
+ import { computed, ref } from 'vue';
10
+ import RcButton from '@components/RcButton/RcButton.vue';
11
+ import StateDot from '@shell/components/StateDot/index.vue';
12
+
13
+ export interface Props {
14
+ resource: any;
15
+
16
+ onClose?: () => void;
17
+ }
18
+ </script>
19
+ <script setup lang="ts">
20
+ const props = defineProps<Props>();
21
+ const emit = defineEmits(['close']);
22
+ const store = useStore();
23
+ const i18n = useI18n(store);
24
+
25
+ const yamlTabProps = ref<YamlProps | null>(null);
26
+ const configTabProps = useDefaultConfigTabProps(props.resource);
27
+
28
+ useDefaultYamlTabProps(props.resource).then((props) => {
29
+ yamlTabProps.value = props;
30
+ });
31
+
32
+ const title = computed(() => {
33
+ const resourceType = store.getters['type-map/labelFor']({ id: props.resource.type });
34
+ const resourceName = props.resource.nameDisplay;
35
+
36
+ return i18n.t('component.drawer.resourceDetailDrawer.title', { resourceType, resourceName });
37
+ });
38
+
39
+ const activeTab = ref<string>(configTabProps ? 'config-tab' : 'yaml-tab');
40
+
41
+ const action = computed(() => {
42
+ const isConfig = activeTab.value === 'config-tab';
43
+ const ariaLabel = isConfig ? i18n.t('component.drawer.resourceDetailDrawer.ariaLabel.editConfig') : i18n.t('component.drawer.resourceDetailDrawer.ariaLabel.editYaml');
44
+ const label = isConfig ? i18n.t('component.drawer.resourceDetailDrawer.ariaLabel.editConfig') : i18n.t('component.drawer.resourceDetailDrawer.ariaLabel.editYaml');
45
+ const action = isConfig ? () => props.resource.goToEdit() : () => props.resource.goToEditYaml();
46
+
47
+ return {
48
+ ariaLabel,
49
+ label,
50
+ action
51
+ };
52
+ });
53
+ </script>
54
+ <template>
55
+ <Drawer
56
+ class="resource-detail-drawer"
57
+ :ariaTarget="title"
58
+ @close="emit('close')"
59
+ >
60
+ <template #title>
61
+ <StateDot
62
+ :color="resource.stateSimpleColor"
63
+ class="mmr-3"
64
+ />
65
+ {{ title }}
66
+ </template>
67
+ <template #body>
68
+ <Tabbed
69
+ class="tabbed"
70
+ :useHash="false"
71
+ @changed="({selectedName}) => {activeTab = selectedName;}"
72
+ >
73
+ <ConfigTab
74
+ v-if="configTabProps"
75
+ v-bind="configTabProps"
76
+ />
77
+ <YamlTab
78
+ v-if="yamlTabProps"
79
+ v-bind="yamlTabProps"
80
+ />
81
+ </Tabbed>
82
+ </template>
83
+ <template #additional-actions>
84
+ <RcButton
85
+ :primary="true"
86
+ :aria-label="action.ariaLabel"
87
+ @click="action.action"
88
+ >
89
+ {{ action.label }}
90
+ </RcButton>
91
+ </template>
92
+ </Drawer>
93
+ </template>
94
+
95
+ <style lang="scss" scoped>
96
+ .resource-detail-drawer {
97
+ :deep() .tabbed {
98
+ & > .tabs {
99
+ border: none;
100
+ }
101
+
102
+ & > .tab-container {
103
+ border: none;
104
+ border-top: 1px solid var(--border);
105
+ padding: 0;
106
+ padding-top: 24px;
107
+ }
108
+ }
109
+ }
110
+ </style>
@@ -25,10 +25,18 @@ export default {
25
25
  },
26
26
 
27
27
  methods: {
28
- close(growl) {
28
+ remove(growl) {
29
29
  this.$store.dispatch('growl/remove', growl.id);
30
30
  },
31
31
 
32
+ close(growl) {
33
+ this.$store.dispatch('growl/close', growl.id);
34
+
35
+ // If a user closes a growl, we won't get the mouse leave event, so we won't start the auto removal
36
+ // again, leaving to stuck growls that should timeout
37
+ this.startAutoRemove();
38
+ },
39
+
32
40
  closeAll() {
33
41
  this.$store.dispatch('growl/clear');
34
42
  },
@@ -36,13 +44,18 @@ export default {
36
44
  closeExpired() {
37
45
  const now = new Date().getTime();
38
46
 
47
+ // Check that we should still be running the auto-close interval timer
48
+ if (!this.shouldRun) {
49
+ this.stopAutoRemove();
50
+ }
51
+
39
52
  for ( const growl of this.stack ) {
40
53
  if ( !growl.timeout ) {
41
54
  continue;
42
55
  }
43
56
 
44
57
  if ( growl.started + growl.timeout < now ) {
45
- this.close(growl);
58
+ this.remove(growl);
46
59
  }
47
60
  }
48
61
  },
@@ -124,18 +137,6 @@ export default {
124
137
  </div>
125
138
  </div>
126
139
  </div>
127
- <div
128
- v-if="stack.length > 1"
129
- class="text-right mr-10 mt-10"
130
- >
131
- <button
132
- type="button"
133
- class="btn btn-sm role-primary"
134
- @click="closeAll()"
135
- >
136
- {{ t('growl.clearAll') }}
137
- </button>
138
- </div>
139
140
  </div>
140
141
  </template>
141
142
 
@@ -143,7 +144,7 @@ export default {
143
144
  .growl-container {
144
145
  z-index: 1000;
145
146
  position: absolute;
146
- top: 0;
147
+ top: var(--header-height);
147
148
  right: 0;
148
149
  width: 100%;
149
150
 
@@ -44,6 +44,10 @@ export default {
44
44
  type: String,
45
45
  default: () => undefined,
46
46
  },
47
+ imgAlt: {
48
+ type: String,
49
+ default: () => undefined,
50
+ },
47
51
  color: {
48
52
  type: String,
49
53
  default: () => 'primary',
@@ -143,6 +147,7 @@ export default {
143
147
  :src="src"
144
148
  class="svg-icon"
145
149
  :class="className"
150
+ :alt="imgAlt"
146
151
  >
147
152
  <i
148
153
  v-else-if="icon"
@@ -225,7 +225,7 @@ export default {
225
225
  .key-value {
226
226
  width: 100%;
227
227
 
228
- .kv-container{
228
+ .kv-container {
229
229
  display: grid;
230
230
  align-items: center;
231
231
  grid-template-columns: auto 1fr;
@@ -71,7 +71,12 @@ export default {
71
71
  <template>
72
72
  <div>
73
73
  <div v-if="mode === 'login'">
74
- <rc-dropdown v-if="showLocale">
74
+ <rc-dropdown
75
+ v-if="showLocale"
76
+ :aria-label="$attrs['aria-label'] || undefined"
77
+ :aria-labelledby="$attrs['aria-labelledby'] || undefined"
78
+ :aria-describedby="$attrs['aria-describedby'] || undefined"
79
+ >
75
80
  <rc-dropdown-trigger
76
81
  data-testid="locale-selector"
77
82
  link
@@ -105,6 +110,9 @@ export default {
105
110
  </div>
106
111
  <div v-else>
107
112
  <Select
113
+ :aria-label="$attrs['aria-label'] || undefined"
114
+ :aria-labelledby="$attrs['aria-labelledby'] || undefined"
115
+ :aria-describedby="$attrs['aria-describedby'] || undefined"
108
116
  :value="selectedOption"
109
117
  :options="localesOptions"
110
118
  :is-lang-select="true"
@@ -122,6 +122,7 @@ function toPercent(value, min, max) {
122
122
  v-trim-whitespace
123
123
  :class="{progress: true, multi: pieces.length > 1}"
124
124
  :aria-label="ariaLabelText"
125
+ role="progressbar"
125
126
  >
126
127
  <div
127
128
  v-for="(piece, idx) of pieces"
@@ -92,7 +92,12 @@ export default {
92
92
  }
93
93
 
94
94
  this.errors = [];
95
- this.$store.commit('action-menu/togglePromptModal', data);
95
+
96
+ // Guard against events that can be implicitly passed by components
97
+ const modalData = data instanceof Event ? undefined : data;
98
+
99
+ this.$store.commit('action-menu/togglePromptModal', modalData);
100
+
96
101
  if (this.backgroundClosing) {
97
102
  this.backgroundClosing();
98
103
  }
@@ -1,6 +1,6 @@
1
1
  <script>
2
2
  import ResourceTable from '@shell/components/ResourceTable';
3
- import { colorForState, stateDisplay } from '@shell/plugins/dashboard-store/resource-class';
3
+ import { STATES_ENUM, colorForState, stateDisplay } from '@shell/plugins/dashboard-store/resource-class';
4
4
  import { NAME, NAMESPACE, STATE, TYPE } from '@shell/config/table-headers';
5
5
  import { sortableNumericSuffix } from '@shell/utils/sort';
6
6
  import { NAME as EXPLORER } from '@shell/config/product/explorer';
@@ -70,9 +70,9 @@ export default {
70
70
  const out = [];
71
71
 
72
72
  for ( const r of this.filteredRelationships) {
73
- const state = r.state || 'active';
74
- const stateColor = colorForState(state, r.error, r.transitioning);
75
73
  const type = r[`${ this.direction }Type`];
74
+ const state = r.state || this.$store.getters[`${ inStore }/byId`](type, r[`${ this.direction }Id`])?.state || STATES_ENUM.MISSING;
75
+ const stateColor = colorForState(state, r.error, r.transitioning);
76
76
  const schema = this.$store.getters[`${ inStore }/schemaFor`](type);
77
77
 
78
78
  let name = r[`${ this.direction }Id`];
@@ -104,7 +104,6 @@ export default {
104
104
 
105
105
  out.push({
106
106
  type,
107
- real: this.$store.getters[`${ inStore }/byId`](type, r[`${ this.direction }Id`]),
108
107
  id: r[`${ this.direction }Id`],
109
108
  state,
110
109
  metadata: { namespace, name },
@@ -174,14 +173,7 @@ export default {
174
173
  :groupable="false"
175
174
  >
176
175
  <template #cell:state="{row}">
177
- <BadgeState
178
- v-if="row.real"
179
- :value="row.real"
180
- />
181
- <BadgeState
182
- v-else
183
- :value="row"
184
- />
176
+ <BadgeState :value="row" />
185
177
  </template>
186
178
  </ResourceTable>
187
179
  </template>