@rancher/shell 3.0.5-rc.8 → 3.0.5

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 (199) hide show
  1. package/assets/styles/base/_color.scss +4 -1
  2. package/assets/styles/global/_tooltip.scss +7 -4
  3. package/assets/styles/themes/_dark.scss +11 -0
  4. package/assets/styles/themes/_light.scss +13 -1
  5. package/assets/styles/themes/_modern.scss +22 -0
  6. package/assets/translations/en-us.yaml +147 -19
  7. package/assets/translations/zh-hans.yaml +0 -1
  8. package/chart/monitoring/grafana/index.vue +8 -2
  9. package/components/ActionMenuShell.vue +3 -1
  10. package/components/Cron/CronExpressionEditor.vue +299 -0
  11. package/components/Cron/CronExpressionEditorModal.vue +247 -0
  12. package/components/Cron/CronTooltip.vue +87 -0
  13. package/components/Cron/types.ts +13 -0
  14. package/components/ForceDirectedTreeChart/composable.ts +11 -0
  15. package/components/PodSecurityAdmission.vue +2 -0
  16. package/components/PromptModal.vue +1 -1
  17. package/components/Resource/Detail/Card/__tests__/StateCard.test.ts +1 -0
  18. package/components/Resource/Detail/CopyToClipboard.vue +78 -0
  19. package/components/Resource/Detail/FetchLoader/__tests__/composables.test.ts +69 -0
  20. package/components/Resource/Detail/FetchLoader/composables.ts +27 -0
  21. package/components/Resource/Detail/Metadata/Annotations/__tests__/index.test.ts +1 -1
  22. package/components/Resource/Detail/Metadata/Annotations/index.vue +1 -1
  23. package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/identifying-fields.test.ts +13 -61
  24. package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/index.test.ts +33 -6
  25. package/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields.ts +24 -38
  26. package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +25 -5
  27. package/components/Resource/Detail/Metadata/KeyValue.vue +12 -23
  28. package/components/Resource/Detail/Metadata/KeyValueRow.vue +144 -0
  29. package/components/Resource/Detail/Metadata/Labels/__tests__/index.test.ts +1 -0
  30. package/components/Resource/Detail/Metadata/Labels/index.vue +1 -0
  31. package/components/Resource/Detail/Metadata/__tests__/KeyValue.test.ts +30 -32
  32. package/components/Resource/Detail/Metadata/__tests__/KeyValueRow.test.ts +108 -0
  33. package/components/Resource/Detail/Metadata/__tests__/composables.test.ts +0 -3
  34. package/components/Resource/Detail/Metadata/__tests__/index.test.ts +12 -5
  35. package/components/Resource/Detail/Metadata/composables.ts +1 -4
  36. package/components/Resource/Detail/Metadata/index.vue +1 -0
  37. package/components/Resource/Detail/Preview/Content.vue +63 -0
  38. package/components/Resource/Detail/Preview/Preview.vue +128 -0
  39. package/components/Resource/Detail/Preview/__tests__/Content.spec.ts +71 -0
  40. package/components/Resource/Detail/Preview/__tests__/Preview.spec.ts +121 -0
  41. package/components/Resource/Detail/ResourcePopover/ResourcePopoverCard.vue +141 -0
  42. package/components/Resource/Detail/ResourcePopover/__tests__/ResourcePopoverCard.test.ts +136 -0
  43. package/components/Resource/Detail/ResourcePopover/__tests__/index.test.ts +245 -0
  44. package/components/Resource/Detail/ResourcePopover/index.vue +226 -0
  45. package/components/Resource/Detail/SpacedRow.vue +1 -0
  46. package/components/Resource/Detail/TitleBar/__tests__/composables.test.ts +0 -5
  47. package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +1 -1
  48. package/components/Resource/Detail/TitleBar/composables.ts +1 -3
  49. package/components/Resource/Detail/TitleBar/index.vue +2 -29
  50. package/components/Resource/Detail/ViewOptions/composable.ts +9 -0
  51. package/components/Resource/Detail/ViewOptions/index.vue +41 -0
  52. package/components/Resource/Detail/__tests__/CopyToClipboard.spec.ts +82 -0
  53. package/components/ResourceDetail/Masthead/legacy.vue +0 -19
  54. package/components/ResourceDetail/index.vue +1 -26
  55. package/components/ResourceTable.vue +24 -0
  56. package/components/SortableTable/index.vue +7 -1
  57. package/components/SortableTable/paging.js +3 -0
  58. package/components/Tabbed/Tab.vue +43 -1
  59. package/components/Tabbed/index.vue +3 -1
  60. package/components/__tests__/Cron/CronExpressionEditor.test.ts +151 -0
  61. package/components/__tests__/Cron/CronExpressionEditorModal.test.ts +81 -0
  62. package/components/auth/login/saml.vue +86 -0
  63. package/components/form/LabeledSelect.vue +8 -8
  64. package/components/form/ProjectMemberEditor.vue +2 -0
  65. package/components/form/ResourceTabs/composable.ts +54 -0
  66. package/components/form/ResourceTabs/index.vue +10 -7
  67. package/components/form/Select.vue +13 -10
  68. package/components/form/__tests__/LabeledSelect.test.ts +133 -0
  69. package/components/form/__tests__/Select.test.ts +134 -0
  70. package/components/nav/Header.vue +6 -5
  71. package/composables/useExtensionManager.ts +17 -0
  72. package/config/home-links.js +12 -0
  73. package/config/labels-annotations.js +0 -1
  74. package/config/page-actions.js +0 -1
  75. package/config/product/explorer.js +3 -1
  76. package/config/product/fleet.js +2 -7
  77. package/config/product/manager.js +0 -5
  78. package/config/query-params.js +1 -0
  79. package/config/router/navigation-guards/clusters.js +2 -1
  80. package/config/router/navigation-guards/products.js +1 -1
  81. package/config/store.js +2 -0
  82. package/core/extension-manager-impl.js +518 -0
  83. package/core/plugins.js +35 -468
  84. package/core/types.ts +8 -2
  85. package/detail/__tests__/autoscaling.horizontalpodautoscaler.test.ts +1 -0
  86. package/detail/catalog.cattle.io.app.vue +7 -4
  87. package/detail/fleet.cattle.io.bundle.vue +1 -5
  88. package/detail/fleet.cattle.io.cluster.vue +3 -2
  89. package/detail/fleet.cattle.io.gitrepo.vue +76 -49
  90. package/detail/fleet.cattle.io.helmop.vue +78 -49
  91. package/dialog/AddonConfigConfirmationDialog.vue +1 -1
  92. package/dialog/GenericPrompt.vue +1 -1
  93. package/dialog/ImportDialog.vue +9 -2
  94. package/dialog/InstallExtensionDialog.vue +18 -10
  95. package/dialog/SloDialog.vue +1 -1
  96. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +2 -1
  97. package/edit/__tests__/resources.cattle.io.restore.test.ts +106 -0
  98. package/edit/auth/oidc.vue +106 -6
  99. package/edit/auth/saml.vue +5 -5
  100. package/edit/cloudcredential.vue +31 -17
  101. package/edit/constraints.gatekeeper.sh.constraint/index.vue +10 -2
  102. package/edit/fleet.cattle.io.cluster.vue +19 -0
  103. package/edit/fleet.cattle.io.gitrepo.vue +23 -16
  104. package/edit/monitoring.coreos.com.alertmanagerconfig/index.vue +12 -11
  105. package/edit/monitoring.coreos.com.alertmanagerconfig/receiverConfig.vue +11 -1
  106. package/edit/provisioning.cattle.io.cluster/index.vue +14 -19
  107. package/edit/provisioning.cattle.io.cluster/rke2.vue +11 -3
  108. package/edit/provisioning.cattle.io.cluster/tabs/AddOnAdditionalManifest.vue +1 -0
  109. package/edit/provisioning.cattle.io.cluster/tabs/AddOnConfig.vue +1 -0
  110. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +1 -0
  111. package/edit/provisioning.cattle.io.cluster/tabs/etcd/S3Config.vue +1 -0
  112. package/edit/provisioning.cattle.io.cluster/tabs/registries/index.vue +2 -0
  113. package/edit/provisioning.cattle.io.cluster/tabs/upgrade/DrainOptions.vue +6 -0
  114. package/edit/resources.cattle.io.restore.vue +5 -8
  115. package/initialize/install-plugins.js +1 -3
  116. package/list/__tests__/workload.test.ts +1 -0
  117. package/list/workload.vue +8 -1
  118. package/machine-config/components/GCEImage.vue +6 -5
  119. package/machine-config/google.vue +11 -6
  120. package/mixins/__tests__/auth-config.test.ts +4 -6
  121. package/mixins/__tests__/chart.test.ts +139 -1
  122. package/mixins/auth-config.js +33 -10
  123. package/mixins/chart.js +58 -18
  124. package/models/__tests__/namespace.test.ts +69 -0
  125. package/models/apps.statefulset.js +8 -10
  126. package/models/chart.js +5 -1
  127. package/models/fleet-application.js +16 -46
  128. package/models/fleet.cattle.io.bundle.js +1 -38
  129. package/models/fleet.cattle.io.gitrepo.js +4 -0
  130. package/models/fleet.cattle.io.helmop.js +4 -0
  131. package/models/management.cattle.io.cluster.js +1 -1
  132. package/models/management.cattle.io.project.js +12 -0
  133. package/models/namespace.js +30 -0
  134. package/models/workload.js +4 -1
  135. package/package.json +10 -10
  136. package/pages/auth/login.vue +8 -3
  137. package/pages/auth/logout.vue +6 -5
  138. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +26 -11
  139. package/pages/c/_cluster/apps/charts/chart.vue +29 -20
  140. package/pages/c/_cluster/apps/charts/index.vue +1 -0
  141. package/pages/c/_cluster/apps/charts/install.vue +6 -5
  142. package/pages/c/_cluster/explorer/tools/__tests__/index.test.ts +102 -12
  143. package/pages/c/_cluster/explorer/tools/index.vue +145 -254
  144. package/pages/c/_cluster/manager/cloudCredential/index.vue +18 -1
  145. package/pages/c/_cluster/manager/drivers/kontainerDriver/index.vue +12 -2
  146. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +1 -1
  147. package/pages/c/_cluster/uiplugins/__tests__/index.spec.ts +318 -0
  148. package/pages/c/_cluster/uiplugins/index.vue +221 -363
  149. package/pages/home.vue +1 -9
  150. package/plugins/axios.js +3 -2
  151. package/plugins/dashboard-store/resource-class.js +49 -0
  152. package/plugins/ember-cookie.js +7 -3
  153. package/plugins/steve/subscribe.js +4 -2
  154. package/public/index.html +2 -1
  155. package/rancher-components/Card/Card.vue +1 -1
  156. package/rancher-components/Form/Checkbox/Checkbox.vue +1 -1
  157. package/rancher-components/Form/Radio/RadioButton.vue +1 -1
  158. package/rancher-components/Form/Radio/RadioGroup.vue +1 -1
  159. package/rancher-components/LabeledTooltip/LabeledTooltip.vue +1 -11
  160. package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.test.ts +53 -0
  161. package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.vue +65 -0
  162. package/rancher-components/Pill/RcCounterBadge/index.ts +1 -0
  163. package/rancher-components/Pill/RcCounterBadge/types.ts +7 -0
  164. package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +1 -1
  165. package/rancher-components/Pill/RcStatusBadge/index.ts +1 -1
  166. package/rancher-components/Pill/RcStatusIndicator/RcStatusIndicator.vue +3 -3
  167. package/rancher-components/Pill/RcStatusIndicator/types.ts +1 -1
  168. package/rancher-components/Pill/RcTag/RcTag.test.ts +64 -0
  169. package/rancher-components/Pill/RcTag/RcTag.vue +94 -0
  170. package/rancher-components/Pill/RcTag/index.ts +1 -0
  171. package/rancher-components/Pill/RcTag/types.ts +9 -0
  172. package/rancher-components/Pill/types.ts +1 -0
  173. package/rancher-components/RcItemCard/RcItemCard.vue +1 -0
  174. package/rancher-components/RcItemCard/RcItemCardAction.vue +12 -0
  175. package/scripts/test-plugins-build.sh +0 -1
  176. package/store/__tests__/catalog.test.ts +63 -0
  177. package/store/__tests__/cookies.test.ts +72 -0
  178. package/store/auth.js +33 -10
  179. package/store/catalog.js +2 -2
  180. package/store/cookies.ts +30 -0
  181. package/store/prefs.js +10 -5
  182. package/store/type-map.js +3 -15
  183. package/types/extension-manager.ts +26 -0
  184. package/types/shell/index.d.ts +123 -27
  185. package/utils/__tests__/product.test.ts +129 -0
  186. package/utils/__tests__/resource.test.ts +87 -0
  187. package/utils/alertmanagerconfig.js +2 -2
  188. package/utils/auth.js +4 -77
  189. package/utils/product.ts +39 -0
  190. package/utils/resource.ts +35 -0
  191. package/utils/select.js +0 -24
  192. package/utils/validators/formRules/__tests__/index.test.ts +3 -0
  193. package/utils/validators/formRules/index.ts +2 -1
  194. package/vue.config.js +1 -1
  195. package/components/Resource/Detail/Metadata/Rectangle.vue +0 -34
  196. package/components/Resource/Detail/Metadata/__tests__/Rectangle.test.ts +0 -24
  197. package/components/ResourceDetail/Masthead/__tests__/legacy.test.ts +0 -65
  198. package/utils/cookie-universal.js +0 -10
  199. /package/components/{ForceDirectedTreeChart.vue → ForceDirectedTreeChart/index.vue} +0 -0
@@ -0,0 +1,144 @@
1
+ <script setup lang="ts">
2
+ import CopyToClipboard from '@shell/components/Resource/Detail/CopyToClipboard.vue';
3
+ import { Row } from '@shell/components/Resource/Detail/Metadata/KeyValue.vue';
4
+ import Preview from '@shell/components/Resource/Detail/Preview/Preview.vue';
5
+ import { nextTick, ref } from 'vue';
6
+ import RcTag from '@components/Pill/RcTag/RcTag.vue';
7
+ import RcButton from '@components/RcButton/RcButton.vue';
8
+ import { Type } from '@components/Pill/types';
9
+ import { useStore } from 'vuex';
10
+ import { useI18n } from '@shell/composables/useI18n';
11
+ import { randomStr } from '@shell/utils/string';
12
+
13
+ export interface KeyValueRowProps {
14
+ row: Row;
15
+ type: Type;
16
+ }
17
+
18
+ const props = defineProps<KeyValueRowProps>();
19
+
20
+ const store = useStore();
21
+ const i18n = useI18n(store);
22
+
23
+ const showPreview = ref(false);
24
+ const element = ref<HTMLElement | null>(null);
25
+ const button = ref<HTMLElement | null>(null);
26
+
27
+ const onClose = (keyboardExit: boolean) => {
28
+ showPreview.value = false;
29
+ if (keyboardExit) {
30
+ nextTick(() => {
31
+ button.value?.focus();
32
+ });
33
+ }
34
+ };
35
+ const previewId = randomStr();
36
+ </script>
37
+
38
+ <template>
39
+ <div
40
+ ref="element"
41
+ class="key-value-row"
42
+ :class="{'show-preview': showPreview, [props.type]: true}"
43
+ >
44
+ <RcButton
45
+ ref="button"
46
+ ghost
47
+ aria-haspopup="dialog"
48
+ :aria-expanded="showPreview"
49
+ :aria-controls="previewId"
50
+ :aria-label="i18n.t('component.resource.detail.metadata.keyValue.ariaLabel.showPreview')"
51
+ @click="() => showPreview = true"
52
+ >
53
+ <RcTag
54
+ :type="type"
55
+ :highlight="showPreview"
56
+ >
57
+ <span class="tag-data">{{ props.row.key }}: {{ props.row.value }}</span>
58
+ </RcTag>
59
+ </RcButton>
60
+ <CopyToClipboard :value="row.value" />
61
+ <Preview
62
+ v-if="showPreview"
63
+ :id="previewId"
64
+ class="preview"
65
+ :title="row.key"
66
+ :value="row.value"
67
+ :anchor-element="element"
68
+ aria-live="polite"
69
+ @close="onClose"
70
+ />
71
+ </div>
72
+ </template>
73
+
74
+ <style lang="scss" scoped>
75
+ .key-value-row {
76
+ display: inline-block;
77
+ position: relative;
78
+ padding: 0;
79
+
80
+ .copy-to-clipboard {
81
+ position: fixed;
82
+
83
+ right: -20px;
84
+ top: -9px;
85
+ z-index: 20px;
86
+ }
87
+
88
+ &, .btn, .rc-tag {
89
+ max-width: calc(100%);
90
+ }
91
+
92
+ .rc-tag {
93
+ display: inline-block;
94
+ line-height: normal;
95
+ }
96
+
97
+ .tag-data {
98
+ display: inline-block;
99
+ overflow: hidden;
100
+ text-overflow: ellipsis;
101
+ white-space: nowrap;
102
+ max-width: calc(100%);
103
+ line-height: normal;
104
+ }
105
+
106
+ & .btn {
107
+ line-height: initial;
108
+ min-height: initial;
109
+ }
110
+
111
+ &.active {
112
+ $ellipsis-padding: 22px;
113
+
114
+ &.show-preview {
115
+ .copy-to-clipboard {
116
+ position: fixed;
117
+ }
118
+ }
119
+
120
+ button:focus-visible, button:hover, .copy-to-clipboard:focus-visible {
121
+ .rc-tag .tag-data {
122
+ // This alters the ellipsis so we show more letters when the clipboard button is visible and occluding parts of the tag
123
+ padding-right: $ellipsis-padding;
124
+ }
125
+
126
+ & + .copy-to-clipboard {
127
+ position: absolute;
128
+ }
129
+ }
130
+
131
+ .copy-to-clipboard:focus-visible, .copy-to-clipboard:hover {
132
+ position: absolute;
133
+
134
+ }
135
+
136
+ .btn:has(+ .copy-to-clipboard:focus-visible), .btn:has(+ .copy-to-clipboard:hover) {
137
+ .rc-tag .tag-data {
138
+ // This alters the ellipsis so we show more letters when the clipboard button is visible and occluding parts of the tag
139
+ padding-right: $ellipsis-padding;
140
+ }
141
+ }
142
+ }
143
+ }
144
+ </style>
@@ -1,6 +1,7 @@
1
1
  import { mount } from '@vue/test-utils';
2
2
  import Labels from '@shell/components/Resource/Detail/Metadata/Labels/index.vue';
3
3
  import { createStore } from 'vuex';
4
+ jest.mock('@shell/utils/clipboard', () => ({ copyTextToClipboard: jest.fn() }));
4
5
 
5
6
  describe('component: Metadata/Labels', () => {
6
7
  it('should render KeyValue with the appropriate props', async() => {
@@ -26,6 +26,7 @@ const i18n = useI18n(store);
26
26
  <KeyValue
27
27
  :propertyName="i18n.t('component.resource.detail.metadata.labels.title')"
28
28
  :rows="labels"
29
+ type="active"
29
30
  @show-configuration="(returnFocusSelector: string) => emit('show-configuration', returnFocusSelector)"
30
31
  />
31
32
  </template>
@@ -1,7 +1,7 @@
1
1
  import { mount, RouterLinkStub } from '@vue/test-utils';
2
2
  import KeyValue from '@shell/components/Resource/Detail/Metadata/KeyValue.vue';
3
3
  import { createStore } from 'vuex';
4
- import Rectangle from '@shell/components/Resource/Detail/Metadata/Rectangle.vue';
4
+ jest.mock('@shell/utils/clipboard', () => ({ copyTextToClipboard: jest.fn() }));
5
5
 
6
6
  describe('component: Metadata/IdentifyingInformation', () => {
7
7
  const propertyName = 'PROPERTY_NAME';
@@ -16,9 +16,13 @@ describe('component: Metadata/IdentifyingInformation', () => {
16
16
 
17
17
  it('should render container with identifying information', async() => {
18
18
  const wrapper = mount(KeyValue, {
19
- props: { propertyName, rows },
19
+ props: {
20
+ propertyName, rows, type: 'active'
21
+ },
20
22
  global: {
21
- stubs: { 'router-link': RouterLinkStub, 'clean-tooltip': true },
23
+ stubs: {
24
+ 'router-link': RouterLinkStub, 'clean-tooltip': true, KeyValueRow: true
25
+ },
22
26
  provide: { store },
23
27
  directives
24
28
  }
@@ -29,9 +33,13 @@ describe('component: Metadata/IdentifyingInformation', () => {
29
33
 
30
34
  it('should render property name and count', async() => {
31
35
  const wrapper = mount(KeyValue, {
32
- props: { propertyName, rows },
36
+ props: {
37
+ propertyName, rows, type: 'active'
38
+ },
33
39
  global: {
34
- stubs: { 'router-link': RouterLinkStub, 'clean-tooltip': true },
40
+ stubs: {
41
+ 'router-link': RouterLinkStub, 'clean-tooltip': true, KeyValueRow: true
42
+ },
35
43
  provide: { store },
36
44
  directives
37
45
  }
@@ -43,9 +51,13 @@ describe('component: Metadata/IdentifyingInformation', () => {
43
51
 
44
52
  it('should render no rows messaging', async() => {
45
53
  const wrapper = mount(KeyValue, {
46
- props: { propertyName, rows: [] },
54
+ props: {
55
+ propertyName, rows: [], type: 'active'
56
+ },
47
57
  global: {
48
- stubs: { 'router-link': RouterLinkStub, 'clean-tooltip': true },
58
+ stubs: {
59
+ 'router-link': RouterLinkStub, 'clean-tooltip': true, KeyValueRow: true
60
+ },
49
61
  provide: { store },
50
62
  directives
51
63
  }
@@ -58,10 +70,12 @@ describe('component: Metadata/IdentifyingInformation', () => {
58
70
  it('should render show all button if rows length exceeds max', async() => {
59
71
  const wrapper = mount(KeyValue, {
60
72
  props: {
61
- propertyName, rows: [...rows, ...rows], maxRows: 1
73
+ propertyName, rows: [...rows, ...rows], maxRows: 1, type: 'active'
62
74
  },
63
75
  global: {
64
- stubs: { 'router-link': RouterLinkStub, 'clean-tooltip': true },
76
+ stubs: {
77
+ 'router-link': RouterLinkStub, 'clean-tooltip': true, KeyValueRow: true
78
+ },
65
79
  provide: { store },
66
80
  directives
67
81
  }
@@ -70,38 +84,22 @@ describe('component: Metadata/IdentifyingInformation', () => {
70
84
  expect(wrapper.find('.show-all').exists()).toBeTruthy();
71
85
  });
72
86
 
73
- it('should pass outline down to rectangle', async() => {
87
+ it('should pass type down to KeyValueRow', async() => {
74
88
  const wrapper = mount(KeyValue, {
75
89
  props: {
76
- propertyName, rows, outline: false
90
+ propertyName, rows, type: 'active'
77
91
  },
78
92
  global: {
79
- stubs: { 'router-link': RouterLinkStub, 'clean-tooltip': true },
80
- provide: { store },
81
- directives
82
- }
83
- });
84
-
85
- const rectangleComponent = wrapper.find('.row').findComponent(Rectangle);
86
-
87
- expect(rectangleComponent.props('outline')).toStrictEqual(false);
88
- });
89
-
90
- it('should render a concatenated string for the tooltip and default slot of the rectangle', async() => {
91
- const wrapper = mount(KeyValue, {
92
- props: { propertyName, rows },
93
- global: {
94
- stubs: { 'router-link': RouterLinkStub, 'clean-tooltip': true },
93
+ stubs: {
94
+ 'router-link': RouterLinkStub, 'clean-tooltip': true, KeyValueRow: true
95
+ },
95
96
  provide: { store },
96
97
  directives
97
98
  }
98
99
  });
99
100
 
100
- const row = rows[0];
101
- const concatenated = `${ row.key }: ${ row.value }`;
102
- const rectangleComponent = wrapper.find('.row').findComponent(Rectangle);
101
+ const keyValueRowComponent: any = wrapper.findComponent('key-value-row-stub');
103
102
 
104
- expect(rectangleComponent.element.innerHTML).toStrictEqual(concatenated);
105
- expect(cleanTooltip.mock.calls[0][1].value).toStrictEqual(concatenated);
103
+ expect(keyValueRowComponent.props('type')).toStrictEqual('active');
106
104
  });
107
105
  });
@@ -0,0 +1,108 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import KeyValueRow from '@shell/components/Resource/Detail/Metadata/KeyValueRow.vue';
3
+ import Preview from '@shell/components/Resource/Detail/Preview/Preview.vue';
4
+ import { copyTextToClipboard } from '@shell/utils/clipboard';
5
+
6
+ jest.mock('@shell/utils/clipboard', () => ({ copyTextToClipboard: jest.fn() }));
7
+ jest.mock('vuex', () => ({ useStore: () => { } }));
8
+
9
+ describe('component: KeyValueRow', () => {
10
+ const mockRow = {
11
+ key: 'test-key',
12
+ value: 'test-value',
13
+ };
14
+
15
+ const global = {
16
+ stubs: {
17
+ RcButton: { template: '<button><slot/></button>' },
18
+ RcTag: { template: '<div><slot/></div>' },
19
+ },
20
+ directives: { cleanHtml: (identity: any) => identity, t: (identity: any) => identity },
21
+ };
22
+
23
+ beforeEach(() => {
24
+ // Create a teleport target
25
+ const teleportTarget = document.createElement('div');
26
+
27
+ teleportTarget.id = 'preview';
28
+ document.body.appendChild(teleportTarget);
29
+ });
30
+
31
+ afterEach(() => {
32
+ document.getElementById('preview')?.remove();
33
+ jest.clearAllMocks();
34
+ });
35
+
36
+ it('should render the key and value', () => {
37
+ const wrapper = mount(KeyValueRow, {
38
+ props: {
39
+ row: mockRow,
40
+ type: 'active',
41
+ },
42
+ global
43
+ });
44
+
45
+ const tagData = wrapper.find('.tag-data');
46
+
47
+ expect(tagData.exists()).toBe(true);
48
+ expect(tagData.text()).toBe(`${ mockRow.key }: ${ mockRow.value }`);
49
+ });
50
+
51
+ it('should show the preview component on click', async() => {
52
+ const wrapper = mount(KeyValueRow, {
53
+ props: {
54
+ row: mockRow,
55
+ type: 'active',
56
+ },
57
+ global
58
+ });
59
+
60
+ expect(wrapper.findComponent(Preview).exists()).toBe(false);
61
+
62
+ await wrapper.find('button').trigger('click');
63
+
64
+ expect(wrapper.findComponent(Preview).exists()).toBe(true);
65
+ });
66
+
67
+ it('should hide the preview component on close', async() => {
68
+ const wrapper = mount(KeyValueRow, {
69
+ props: {
70
+ row: mockRow,
71
+ type: 'active',
72
+ },
73
+ global
74
+ });
75
+
76
+ expect(wrapper.findComponent(Preview).exists()).toBe(false);
77
+
78
+ await wrapper.find('button').trigger('click');
79
+
80
+ expect(wrapper.findComponent(Preview).exists()).toBe(true);
81
+
82
+ const preview = wrapper.findComponent({ name: 'Preview' });
83
+
84
+ preview.vm.$emit('close');
85
+ await wrapper.vm.$nextTick();
86
+
87
+ expect(wrapper.findComponent(Preview).exists()).toBe(false);
88
+ });
89
+
90
+ it('should contain a copy to clipboard component and call copy on click', async() => {
91
+ const wrapper = mount(KeyValueRow, {
92
+ props: {
93
+ row: mockRow,
94
+ type: 'active',
95
+ },
96
+ global
97
+ });
98
+
99
+ const copyToClipboard = wrapper.findComponent({ name: 'CopyToClipboard' });
100
+
101
+ expect(copyToClipboard.exists()).toBe(true);
102
+ expect(copyToClipboard.props('value')).toBe(mockRow.value);
103
+
104
+ await copyToClipboard.trigger('click');
105
+
106
+ expect(copyTextToClipboard).toHaveBeenCalledWith(mockRow.value);
107
+ });
108
+ });
@@ -14,7 +14,6 @@ describe('composables: Metadata/composables', () => {
14
14
  const useWorkspaceSpy = jest.spyOn(IdentifyingFields, 'useWorkspace');
15
15
  const useNamespaceSpy = jest.spyOn(IdentifyingFields, 'useNamespace');
16
16
  const useLiveDateSpy = jest.spyOn(IdentifyingFields, 'useLiveDate');
17
- const useCreatedBySpy = jest.spyOn(IdentifyingFields, 'useCreatedBy');
18
17
  const useResourceDetailsSpy = jest.spyOn(IdentifyingFields, 'useResourceDetails');
19
18
 
20
19
  it('should filter out undefined identifyingInformation', () => {
@@ -39,7 +38,6 @@ describe('composables: Metadata/composables', () => {
39
38
  useWorkspaceSpy.mockReturnValue(computed(() => ({ label: 'WORKSPACE' })));
40
39
  useNamespaceSpy.mockReturnValue(computed(() => ({ label: 'NAMESPACE' })));
41
40
  useLiveDateSpy.mockReturnValue(computed(() => ({ label: 'LIVE_DATE' })));
42
- useCreatedBySpy.mockReturnValue(computed(() => ({ label: 'CREATED_BY' })));
43
41
  useResourceDetailsSpy.mockReturnValue(computed(() => [{ label: 'RESOURCE_DETAILS' }]));
44
42
 
45
43
  const resource = {
@@ -59,7 +57,6 @@ describe('composables: Metadata/composables', () => {
59
57
  { label: 'WORKSPACE' },
60
58
  { label: 'NAMESPACE' },
61
59
  { label: 'LIVE_DATE' },
62
- { label: 'CREATED_BY' },
63
60
  { label: 'RESOURCE_DETAILS' }
64
61
  ]);
65
62
  expect(resource.showConfiguration).toHaveBeenCalledTimes(1);
@@ -2,6 +2,8 @@ import { mount } from '@vue/test-utils';
2
2
  import Metadata from '@shell/components/Resource/Detail/Metadata/index.vue';
3
3
  import { createStore } from 'vuex';
4
4
 
5
+ jest.mock('@shell/utils/clipboard', () => ({ copyTextToClipboard: jest.fn() }));
6
+
5
7
  describe('component: Metadata/index', () => {
6
8
  const store = createStore({});
7
9
  const stubs = ['IdentifyingInformation', 'KeyValue', 'Labels', 'Annotations'];
@@ -14,7 +16,8 @@ describe('component: Metadata/index', () => {
14
16
  props: {
15
17
  identifyingInformation,
16
18
  labels: [],
17
- annotations: []
19
+ annotations: [],
20
+ resource: {}
18
21
  },
19
22
  global: { provide: { store }, stubs }
20
23
  });
@@ -27,7 +30,8 @@ describe('component: Metadata/index', () => {
27
30
  props: {
28
31
  identifyingInformation,
29
32
  labels: [],
30
- annotations: []
33
+ annotations: [],
34
+ resource: {}
31
35
  },
32
36
  global: { provide: { store }, stubs }
33
37
  });
@@ -42,7 +46,8 @@ describe('component: Metadata/index', () => {
42
46
  props: {
43
47
  identifyingInformation,
44
48
  labels: [],
45
- annotations: []
49
+ annotations: [],
50
+ resource: {}
46
51
  },
47
52
  global: { provide: { store }, stubs }
48
53
  });
@@ -60,7 +65,8 @@ describe('component: Metadata/index', () => {
60
65
  props: {
61
66
  identifyingInformation,
62
67
  labels: keyValue,
63
- annotations: []
68
+ annotations: [],
69
+ resource: {}
64
70
  },
65
71
  global: { provide: { store }, stubs }
66
72
  });
@@ -77,7 +83,8 @@ describe('component: Metadata/index', () => {
77
83
  props: {
78
84
  identifyingInformation,
79
85
  labels: [],
80
- annotations: keyValue
86
+ annotations: keyValue,
87
+ resource: {}
81
88
  },
82
89
  global: { provide: { store }, stubs }
83
90
  });
@@ -4,7 +4,6 @@ import { useDefaultLabels } from '@shell/components/Resource/Detail/Metadata/Lab
4
4
  import { useDefaultAnnotations } from '@shell/components/Resource/Detail/Metadata/Annotations/composable';
5
5
  import { computed, toValue, Ref } from 'vue';
6
6
  import {
7
- useCreatedBy,
8
7
  useLiveDate, useNamespace, useProject, useResourceDetails, useWorkspace
9
8
  } from '@shell/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields';
10
9
 
@@ -48,7 +47,6 @@ export const useDefaultMetadataForLegacyPagesProps = (resource: any) => {
48
47
  const workspace = useWorkspace(resource);
49
48
  const namespace = useNamespace(resource);
50
49
  const liveDate = useLiveDate(resource);
51
- const createdBy = useCreatedBy(resource);
52
50
  const resourceValue = toValue(resource);
53
51
 
54
52
  const identifyingInformation = computed((): IdentifyingInformationRow[] => {
@@ -57,7 +55,6 @@ export const useDefaultMetadataForLegacyPagesProps = (resource: any) => {
57
55
  workspace?.value,
58
56
  namespace?.value,
59
57
  liveDate?.value,
60
- createdBy?.value,
61
58
  ];
62
59
  const info = [
63
60
  ...defaultInfo,
@@ -74,7 +71,7 @@ export const useDefaultMetadataForLegacyPagesProps = (resource: any) => {
74
71
  identifyingInformation: identifyingInformation.value,
75
72
  labels: basicMetaData.value.labels,
76
73
  annotations: basicMetaData.value.annotations,
77
- onShowConfiguration: (returnFocusSelector: string) => resourceValue.showConfiguration(returnFocusSelector)
74
+ onShowConfiguration: (returnFocusSelector?: string) => resourceValue.showConfiguration(returnFocusSelector)
78
75
  };
79
76
  });
80
77
  };
@@ -45,6 +45,7 @@ const showBothEmpty = computed(() => labels.length === 0 && annotations.length =
45
45
  class="labels-and-annotations-empty"
46
46
  >
47
47
  <KeyValue
48
+ type="active"
48
49
  :rows="[]"
49
50
  :propertyName="i18n.t('component.resource.detail.metadata.labelsAndAnnotations')"
50
51
  @show-configuration="(returnFocusSelector: string) => emit('show-configuration', returnFocusSelector)"
@@ -0,0 +1,63 @@
1
+ <script lang="ts" setup>
2
+ import CodeMirror from '@shell/components/CodeMirror.vue';
3
+ import { _VIEW } from '@shell/config/query-params';
4
+ import { nlToBr } from '@shell/utils/string';
5
+ import { computed } from 'vue';
6
+
7
+ export interface Props {
8
+ value: string;
9
+ }
10
+
11
+ const props = defineProps<Props>();
12
+
13
+ const isEmpty = computed(() => props.value.length === 0);
14
+ const jsonStr = computed(() => {
15
+ const value = props.value;
16
+
17
+ if ( value && ( value.startsWith('{') || value.startsWith('[') ) ) {
18
+ try {
19
+ let parsed = JSON.parse(value);
20
+
21
+ parsed = JSON.stringify(parsed, null, 2);
22
+
23
+ return parsed;
24
+ } catch {
25
+ }
26
+ }
27
+
28
+ return null;
29
+ });
30
+
31
+ const bodyHtml = computed(() => {
32
+ return nlToBr(props.value);
33
+ });
34
+
35
+ </script>
36
+ <template>
37
+ <div class="content">
38
+ <span
39
+ v-if="isEmpty"
40
+ v-t="'detailText.empty'"
41
+ />
42
+ <CodeMirror
43
+ v-else-if="jsonStr"
44
+ :mode="_VIEW"
45
+ :options="{mode:{name:'javascript', json:true}, lineNumbers:false, foldGutter:false}"
46
+ :value="jsonStr"
47
+ />
48
+ <span
49
+ v-else
50
+ v-clean-html="bodyHtml"
51
+ data-testid="detail-top_html"
52
+ :class="{'monospace': true}"
53
+ />
54
+ </div>
55
+ </template>
56
+
57
+ <style lang="scss" scoped>
58
+ :deep() {
59
+ .CodeMirror-gutters {
60
+ display: none;
61
+ }
62
+ }
63
+ </style>