@rancher/shell 3.0.5-rc.8 → 3.0.5-rc.9

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 (171) 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 +136 -14
  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/PromptModal.vue +1 -1
  16. package/components/Resource/Detail/Card/__tests__/StateCard.test.ts +1 -0
  17. package/components/Resource/Detail/CopyToClipboard.vue +78 -0
  18. package/components/Resource/Detail/FetchLoader/__tests__/composables.test.ts +69 -0
  19. package/components/Resource/Detail/FetchLoader/composables.ts +27 -0
  20. package/components/Resource/Detail/Metadata/Annotations/__tests__/index.test.ts +1 -1
  21. package/components/Resource/Detail/Metadata/Annotations/index.vue +1 -1
  22. package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/identifying-fields.test.ts +13 -61
  23. package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/index.test.ts +33 -6
  24. package/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields.ts +24 -38
  25. package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +25 -5
  26. package/components/Resource/Detail/Metadata/KeyValue.vue +12 -23
  27. package/components/Resource/Detail/Metadata/KeyValueRow.vue +144 -0
  28. package/components/Resource/Detail/Metadata/Labels/__tests__/index.test.ts +1 -0
  29. package/components/Resource/Detail/Metadata/Labels/index.vue +1 -0
  30. package/components/Resource/Detail/Metadata/__tests__/KeyValue.test.ts +30 -32
  31. package/components/Resource/Detail/Metadata/__tests__/KeyValueRow.test.ts +108 -0
  32. package/components/Resource/Detail/Metadata/__tests__/composables.test.ts +0 -3
  33. package/components/Resource/Detail/Metadata/__tests__/index.test.ts +12 -5
  34. package/components/Resource/Detail/Metadata/composables.ts +1 -4
  35. package/components/Resource/Detail/Metadata/index.vue +1 -0
  36. package/components/Resource/Detail/Preview/Content.vue +63 -0
  37. package/components/Resource/Detail/Preview/Preview.vue +128 -0
  38. package/components/Resource/Detail/Preview/__tests__/Content.spec.ts +71 -0
  39. package/components/Resource/Detail/Preview/__tests__/Preview.spec.ts +121 -0
  40. package/components/Resource/Detail/ResourcePopover/ResourcePopoverCard.vue +141 -0
  41. package/components/Resource/Detail/ResourcePopover/__tests__/ResourcePopoverCard.test.ts +136 -0
  42. package/components/Resource/Detail/ResourcePopover/__tests__/index.test.ts +245 -0
  43. package/components/Resource/Detail/ResourcePopover/index.vue +226 -0
  44. package/components/Resource/Detail/SpacedRow.vue +1 -0
  45. package/components/Resource/Detail/TitleBar/__tests__/composables.test.ts +0 -5
  46. package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +1 -1
  47. package/components/Resource/Detail/TitleBar/composables.ts +1 -3
  48. package/components/Resource/Detail/TitleBar/index.vue +2 -29
  49. package/components/Resource/Detail/ViewOptions/composable.ts +9 -0
  50. package/components/Resource/Detail/ViewOptions/index.vue +41 -0
  51. package/components/Resource/Detail/__tests__/CopyToClipboard.spec.ts +82 -0
  52. package/components/ResourceDetail/Masthead/legacy.vue +0 -19
  53. package/components/ResourceDetail/index.vue +1 -26
  54. package/components/ResourceTable.vue +24 -0
  55. package/components/SortableTable/index.vue +7 -1
  56. package/components/SortableTable/paging.js +3 -0
  57. package/components/Tabbed/Tab.vue +43 -1
  58. package/components/Tabbed/index.vue +3 -1
  59. package/components/__tests__/Cron/CronExpressionEditor.test.ts +151 -0
  60. package/components/__tests__/Cron/CronExpressionEditorModal.test.ts +81 -0
  61. package/components/auth/login/saml.vue +86 -0
  62. package/components/form/LabeledSelect.vue +8 -8
  63. package/components/form/ResourceTabs/composable.ts +54 -0
  64. package/components/form/ResourceTabs/index.vue +10 -7
  65. package/components/form/Select.vue +13 -10
  66. package/components/form/__tests__/LabeledSelect.test.ts +133 -0
  67. package/components/form/__tests__/Select.test.ts +134 -0
  68. package/composables/useExtensionManager.ts +17 -0
  69. package/config/home-links.js +12 -0
  70. package/config/labels-annotations.js +0 -1
  71. package/config/page-actions.js +0 -1
  72. package/config/product/explorer.js +3 -1
  73. package/config/product/fleet.js +2 -7
  74. package/config/product/manager.js +0 -5
  75. package/config/query-params.js +1 -0
  76. package/config/router/navigation-guards/clusters.js +2 -1
  77. package/config/router/navigation-guards/products.js +1 -1
  78. package/core/extension-manager-impl.js +518 -0
  79. package/core/plugins.js +35 -468
  80. package/core/types.ts +8 -2
  81. package/detail/__tests__/autoscaling.horizontalpodautoscaler.test.ts +1 -0
  82. package/detail/catalog.cattle.io.app.vue +7 -4
  83. package/detail/fleet.cattle.io.bundle.vue +1 -5
  84. package/detail/fleet.cattle.io.cluster.vue +3 -2
  85. package/detail/fleet.cattle.io.gitrepo.vue +76 -49
  86. package/detail/fleet.cattle.io.helmop.vue +78 -49
  87. package/dialog/AddonConfigConfirmationDialog.vue +1 -1
  88. package/dialog/GenericPrompt.vue +1 -1
  89. package/dialog/ImportDialog.vue +9 -2
  90. package/dialog/InstallExtensionDialog.vue +18 -10
  91. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +2 -1
  92. package/edit/__tests__/resources.cattle.io.restore.test.ts +106 -0
  93. package/edit/cloudcredential.vue +31 -17
  94. package/edit/constraints.gatekeeper.sh.constraint/index.vue +10 -2
  95. package/edit/fleet.cattle.io.cluster.vue +19 -0
  96. package/edit/fleet.cattle.io.gitrepo.vue +23 -16
  97. package/edit/monitoring.coreos.com.alertmanagerconfig/index.vue +12 -11
  98. package/edit/monitoring.coreos.com.alertmanagerconfig/receiverConfig.vue +11 -1
  99. package/edit/provisioning.cattle.io.cluster/index.vue +14 -19
  100. package/edit/provisioning.cattle.io.cluster/rke2.vue +11 -3
  101. package/edit/resources.cattle.io.restore.vue +5 -8
  102. package/list/__tests__/workload.test.ts +1 -0
  103. package/list/workload.vue +8 -1
  104. package/machine-config/components/GCEImage.vue +6 -5
  105. package/machine-config/google.vue +11 -6
  106. package/mixins/__tests__/chart.test.ts +139 -1
  107. package/mixins/chart.js +58 -18
  108. package/models/__tests__/namespace.test.ts +69 -0
  109. package/models/apps.statefulset.js +8 -10
  110. package/models/chart.js +5 -1
  111. package/models/fleet-application.js +16 -46
  112. package/models/fleet.cattle.io.bundle.js +1 -38
  113. package/models/fleet.cattle.io.gitrepo.js +4 -0
  114. package/models/fleet.cattle.io.helmop.js +4 -0
  115. package/models/management.cattle.io.project.js +12 -0
  116. package/models/namespace.js +30 -0
  117. package/models/workload.js +3 -0
  118. package/package.json +10 -10
  119. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +26 -11
  120. package/pages/c/_cluster/apps/charts/chart.vue +29 -20
  121. package/pages/c/_cluster/apps/charts/index.vue +1 -0
  122. package/pages/c/_cluster/apps/charts/install.vue +6 -5
  123. package/pages/c/_cluster/explorer/tools/__tests__/index.test.ts +102 -12
  124. package/pages/c/_cluster/explorer/tools/index.vue +145 -254
  125. package/pages/c/_cluster/manager/cloudCredential/index.vue +18 -1
  126. package/pages/c/_cluster/manager/drivers/kontainerDriver/index.vue +12 -2
  127. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +1 -1
  128. package/pages/c/_cluster/uiplugins/__tests__/index.spec.ts +318 -0
  129. package/pages/c/_cluster/uiplugins/index.vue +221 -363
  130. package/pages/home.vue +1 -9
  131. package/plugins/dashboard-store/resource-class.js +49 -0
  132. package/public/index.html +2 -1
  133. package/rancher-components/Card/Card.vue +1 -1
  134. package/rancher-components/Form/Checkbox/Checkbox.vue +1 -1
  135. package/rancher-components/Form/Radio/RadioButton.vue +1 -1
  136. package/rancher-components/Form/Radio/RadioGroup.vue +1 -1
  137. package/rancher-components/LabeledTooltip/LabeledTooltip.vue +1 -11
  138. package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.test.ts +53 -0
  139. package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.vue +65 -0
  140. package/rancher-components/Pill/RcCounterBadge/index.ts +1 -0
  141. package/rancher-components/Pill/RcCounterBadge/types.ts +7 -0
  142. package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +1 -1
  143. package/rancher-components/Pill/RcStatusBadge/index.ts +1 -1
  144. package/rancher-components/Pill/RcStatusIndicator/RcStatusIndicator.vue +3 -3
  145. package/rancher-components/Pill/RcStatusIndicator/types.ts +1 -1
  146. package/rancher-components/Pill/RcTag/RcTag.test.ts +64 -0
  147. package/rancher-components/Pill/RcTag/RcTag.vue +94 -0
  148. package/rancher-components/Pill/RcTag/index.ts +1 -0
  149. package/rancher-components/Pill/RcTag/types.ts +9 -0
  150. package/rancher-components/Pill/types.ts +1 -0
  151. package/rancher-components/RcItemCard/RcItemCard.vue +1 -0
  152. package/rancher-components/RcItemCard/RcItemCardAction.vue +12 -0
  153. package/store/__tests__/catalog.test.ts +63 -0
  154. package/store/catalog.js +2 -2
  155. package/store/type-map.js +3 -15
  156. package/types/extension-manager.ts +26 -0
  157. package/types/shell/index.d.ts +121 -16
  158. package/utils/__tests__/product.test.ts +129 -0
  159. package/utils/__tests__/resource.test.ts +87 -0
  160. package/utils/alertmanagerconfig.js +2 -2
  161. package/utils/auth.js +3 -76
  162. package/utils/product.ts +39 -0
  163. package/utils/resource.ts +35 -0
  164. package/utils/select.js +0 -24
  165. package/utils/validators/formRules/__tests__/index.test.ts +3 -0
  166. package/utils/validators/formRules/index.ts +2 -1
  167. package/vue.config.js +1 -1
  168. package/components/Resource/Detail/Metadata/Rectangle.vue +0 -34
  169. package/components/Resource/Detail/Metadata/__tests__/Rectangle.test.ts +0 -24
  170. package/components/ResourceDetail/Masthead/__tests__/legacy.test.ts +0 -65
  171. /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>