@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,78 @@
1
+ <script lang="ts" setup>
2
+ import { useI18n } from '@shell/composables/useI18n';
3
+ import { copyTextToClipboard } from '@shell/utils/clipboard';
4
+ import { ref } from 'vue';
5
+ import { useStore } from 'vuex';
6
+
7
+ export interface Props {
8
+ value: string;
9
+ }
10
+
11
+ const props = defineProps<Props>();
12
+ const store = useStore();
13
+ const i18n = useI18n(store);
14
+ const copied = ref(false);
15
+ const timeout = ref<null | ReturnType<typeof setTimeout>>(null);
16
+
17
+ const onClick = (ev: MouseEvent) => {
18
+ ev.stopPropagation();
19
+
20
+ copyTextToClipboard(props.value);
21
+ copied.value = true;
22
+
23
+ if (timeout.value) {
24
+ return;
25
+ }
26
+
27
+ timeout.value = setTimeout(() => {
28
+ copied.value = false;
29
+ timeout.value = null;
30
+ }, 2000);
31
+ };
32
+ </script>
33
+ <template>
34
+ <button
35
+ class="copy-to-clipboard"
36
+ :class="{copied}"
37
+ :aria-label="i18n.t('component.resource.detail.copyToClipboard.ariaLabel.copy')"
38
+ @click="onClick"
39
+ >
40
+ <i class="icon icon-copy" />
41
+ </button>
42
+ </template>
43
+
44
+ <style lang="scss" scoped>
45
+ .copy-to-clipboard {
46
+ z-index: 2;
47
+ display: inline-flex;
48
+ $size: 36px;
49
+ width: $size;
50
+ height: $size;
51
+ font-size: 14px;
52
+ border-radius: 50%;
53
+ justify-content: center;
54
+ align-items: center;
55
+
56
+ border: 1px solid var(--primary);
57
+ color: var(--primary);
58
+
59
+ background-color: var(--body-bg);
60
+
61
+ &:hover {
62
+ color: var(--body-text);
63
+ }
64
+
65
+ &.copied {
66
+ background-color: var(--success);
67
+ border-color: var(--success-border);
68
+ color: var(--success-text);
69
+
70
+ transition: all 0.25s;
71
+ transition-timing-function: ease;
72
+ }
73
+
74
+ &:focus-visible {
75
+ @include focus-outline;
76
+ }
77
+ }
78
+ </style>
@@ -0,0 +1,69 @@
1
+ import { useFetch } from '@shell/components/Resource/Detail/FetchLoader/composables';
2
+ import { nextTick } from 'vue';
3
+
4
+ describe('composables: useFetch', () => {
5
+ it('should be in a loading state initially and call the fetch function immediately', () => {
6
+ const mockFetch = jest.fn(() => new Promise(() => {})); // A promise that never resolves
7
+
8
+ const result = useFetch(mockFetch);
9
+
10
+ expect(result.value.loading).toBe(true);
11
+ expect(result.value.data).toBeUndefined();
12
+ expect(result.value.error).toBeUndefined();
13
+ expect(mockFetch).toHaveBeenCalledTimes(1);
14
+ });
15
+
16
+ it('should successfully fetch data and update state', async() => {
17
+ const mockData = { id: 1, name: 'Test Data' };
18
+ const mockFetch = jest.fn(() => Promise.resolve(mockData));
19
+
20
+ const result = useFetch(mockFetch);
21
+
22
+ // Initial state check
23
+ expect(result.value.loading).toBe(true);
24
+ expect(mockFetch).toHaveBeenCalledTimes(1);
25
+
26
+ // Wait for the promise to resolve and Vue to react
27
+ await nextTick();
28
+
29
+ // Final state check
30
+ expect(result.value.loading).toBe(false);
31
+ expect(result.value.data).toStrictEqual(mockData);
32
+ expect(result.value.error).toBeUndefined();
33
+ });
34
+
35
+ it('should handle a fetch error and update state', async() => {
36
+ const mockError = new Error('Network Failed');
37
+ const mockFetch = jest.fn(() => Promise.reject(mockError));
38
+
39
+ const result = useFetch(mockFetch);
40
+
41
+ // Initial state check
42
+ expect(result.value.loading).toBe(true);
43
+ expect(mockFetch).toHaveBeenCalledTimes(1);
44
+
45
+ // Wait for the promise to reject and Vue to react
46
+ await nextTick();
47
+
48
+ // Final state check
49
+ expect(result.value.loading).toBe(false);
50
+ expect(result.value.data).toBeUndefined();
51
+ expect(result.value.error).toStrictEqual(mockError);
52
+ });
53
+
54
+ it('should correctly handle a fetch function that returns a non-promise value', async() => {
55
+ const mockData = 'just a string';
56
+ // The composable uses `await`, which will wrap non-promise values.
57
+ const mockFetch = jest.fn(() => mockData as any);
58
+
59
+ const result = useFetch(mockFetch);
60
+
61
+ expect(result.value.loading).toBe(true);
62
+
63
+ await nextTick();
64
+
65
+ expect(result.value.loading).toBe(false);
66
+ expect(result.value.data).toBe(mockData);
67
+ expect(result.value.error).toBeUndefined();
68
+ });
69
+ });
@@ -0,0 +1,27 @@
1
+ import { computed, ref, toValue } from 'vue';
2
+
3
+ export const useFetch = <T>(fetch: () => Promise<T> ) => {
4
+ const loading = ref<boolean>(true);
5
+ const data = ref<T>();
6
+ const error = ref<any>();
7
+
8
+ const load = async() => {
9
+ try {
10
+ loading.value = true;
11
+
12
+ data.value = toValue(await fetch());
13
+ } catch (ex) {
14
+ error.value = ex;
15
+ } finally {
16
+ loading.value = false;
17
+ }
18
+ };
19
+
20
+ load();
21
+
22
+ return computed(() => ({
23
+ loading: loading.value,
24
+ data: data.value,
25
+ error: error.value
26
+ }));
27
+ };
@@ -1,6 +1,7 @@
1
1
  import { mount } from '@vue/test-utils';
2
2
  import Annotations from '@shell/components/Resource/Detail/Metadata/Annotations/index.vue';
3
3
  import { createStore } from 'vuex';
4
+ jest.mock('@shell/utils/clipboard', () => ({ copyTextToClipboard: jest.fn() }));
4
5
 
5
6
  describe('component: Metadata/Annotations', () => {
6
7
  it('should render KeyValue with the appropriate props', async() => {
@@ -14,6 +15,5 @@ describe('component: Metadata/Annotations', () => {
14
15
 
15
16
  expect(keyValue.props('propertyName')).toStrictEqual('component.resource.detail.metadata.annotations.title');
16
17
  expect(keyValue.props('rows')).toStrictEqual(annotations);
17
- expect(keyValue.props('outline')).toStrictEqual(true);
18
18
  });
19
19
  });
@@ -24,7 +24,7 @@ const i18n = useI18n(store);
24
24
  <KeyValue
25
25
  :propertyName="i18n.t('component.resource.detail.metadata.annotations.title')"
26
26
  :rows="annotations"
27
- :outline="true"
27
+ type="active"
28
28
 
29
29
  @show-configuration="(returnFocusSelector: string) => emit('show-configuration', returnFocusSelector)"
30
30
  />
@@ -1,8 +1,7 @@
1
1
  import {
2
- useNamespace, useWorkspace, useLiveDate, useCreatedBy, useProject,
3
- useResourceDetails
2
+ useNamespace, useWorkspace, useLiveDate, useProject, useResourceDetails
4
3
  } from '@shell/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields';
5
- import { NAMESPACE, FLEET } from '@shell/config/types';
4
+ import { NAMESPACE, FLEET, MANAGEMENT } from '@shell/config/types';
6
5
  import { NAME as FLEET_NAME } from '@shell/config/product/fleet';
7
6
 
8
7
  const mockStore = {
@@ -33,29 +32,12 @@ describe('composables: IdentifyingFields', () => {
33
32
  expect(result).toBeUndefined();
34
33
  });
35
34
 
36
- it('should return a valid namespace row with namespaceLocation', () => {
37
- const resource = { namespace: 'NAMESPACE', namespaceLocation: 'NAMESPACE_LOCATION' };
38
- const result = useNamespace(resource);
39
-
40
- expect(result?.value.to).toStrictEqual(resource.namespaceLocation);
41
- expect(result?.value.value).toStrictEqual(resource.namespace);
42
- expect(result?.value.label).toStrictEqual('component.resource.detail.metadata.identifyingInformation.namespace');
43
- expect(result?.value.valueDataTestid).toStrictEqual('masthead-subheader-namespace');
44
- });
45
-
46
- it('should return a valid namespace row with computed `to`', () => {
35
+ it('should return a valid namespace row', () => {
47
36
  const resource = { namespace: 'NAMESPACE' };
48
37
  const result = useNamespace(resource);
49
38
 
50
- expect(result?.value.to).toStrictEqual({
51
- name: `c-cluster-product-resource-id`,
52
- params: {
53
- product: 'PRODUCT_ID',
54
- cluster: 'CLUSTER_ID',
55
- resource: NAMESPACE,
56
- id: resource.namespace
57
- }
58
- });
39
+ expect(result?.value.valueOverride?.props.type).toStrictEqual(NAMESPACE);
40
+ expect(result?.value.valueOverride?.props.id).toStrictEqual(resource.namespace);
59
41
  expect(result?.value.value).toStrictEqual(resource.namespace);
60
42
  expect(result?.value.label).toStrictEqual('component.resource.detail.metadata.identifyingInformation.namespace');
61
43
  expect(result?.value.valueDataTestid).toStrictEqual('masthead-subheader-namespace');
@@ -122,41 +104,6 @@ describe('composables: IdentifyingFields', () => {
122
104
  });
123
105
  });
124
106
 
125
- describe('useCreatedBy', () => {
126
- it('should return undefined if showCreatedBy is falsy', () => {
127
- const resource = {};
128
- const result = useCreatedBy(resource);
129
-
130
- expect(result).toBeUndefined();
131
- });
132
-
133
- it('should return a valid createdBy row', () => {
134
- mockStore.getters[`type-map/optionsFor`].mockReturnValue({ showAge: true });
135
-
136
- const resource = { showCreatedBy: true, createdBy: { displayName: 'CREATED_BY' } };
137
- const result = useCreatedBy(resource);
138
-
139
- expect(result?.value.to).toBeUndefined();
140
- expect(result?.value.value).toStrictEqual(resource.createdBy.displayName);
141
- expect(result?.value.label).toStrictEqual('component.resource.detail.metadata.identifyingInformation.createdBy');
142
- expect(result?.value.dataTestid).toStrictEqual('masthead-subheader-createdBy');
143
- expect(result?.value.valueDataTestid).toStrictEqual('masthead-subheader-createdBy_plain-text');
144
- });
145
-
146
- it('should return a valid createdBy row with createdBy.location', () => {
147
- mockStore.getters[`type-map/optionsFor`].mockReturnValue({ showAge: true });
148
-
149
- const resource = { showCreatedBy: true, createdBy: { displayName: 'CREATED_BY', location: 'LOCATION' } };
150
- const result = useCreatedBy(resource);
151
-
152
- expect(result?.value.to).toStrictEqual(resource.createdBy.location);
153
- expect(result?.value.value).toStrictEqual(resource.createdBy.displayName);
154
- expect(result?.value.label).toStrictEqual('component.resource.detail.metadata.identifyingInformation.createdBy');
155
- expect(result?.value.dataTestid).toStrictEqual('masthead-subheader-createdBy');
156
- expect(result?.value.valueDataTestid).toStrictEqual('masthead-subheader-createdBy-link');
157
- });
158
- });
159
-
160
107
  describe('useProject', () => {
161
108
  it('should return undefined if type is not namespace', () => {
162
109
  const resource = { type: 'anything' };
@@ -173,11 +120,16 @@ describe('composables: IdentifyingFields', () => {
173
120
  });
174
121
 
175
122
  it('should return a valid project row', () => {
176
- const resource = { type: NAMESPACE, project: { nameDisplay: 'PROJECT', detailLocation: 'LOCATION' } };
123
+ const resource = {
124
+ type: NAMESPACE,
125
+ project: {
126
+ id: 'ID', nameDisplay: 'PROJECT', detailLocation: 'LOCATION'
127
+ }
128
+ };
177
129
  const result = useProject(resource);
178
130
 
179
- expect(result?.value.to).toStrictEqual(resource.project.detailLocation);
180
- expect(result?.value.value).toStrictEqual(resource.project.nameDisplay);
131
+ expect(result?.value.valueOverride?.props.type).toStrictEqual(MANAGEMENT.PROJECT);
132
+ expect(result?.value.valueOverride?.props.id).toStrictEqual(resource.project.id);
181
133
  expect(result?.value.label).toStrictEqual('component.resource.detail.metadata.identifyingInformation.project');
182
134
  });
183
135
  });
@@ -1,7 +1,9 @@
1
1
  import { mount, RouterLinkStub } from '@vue/test-utils';
2
2
  import IdentifyingInformation from '@shell/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue';
3
- import Rectangle from '@shell/components/Resource/Detail/Metadata/Rectangle.vue';
3
+ import KeyValueRow from '@shell/components/Resource/Detail/Metadata/KeyValueRow.vue';
4
4
  import { markRaw } from 'vue';
5
+ jest.mock('@shell/utils/clipboard', () => ({ copyTextToClipboard: jest.fn() }));
6
+ jest.mock('vuex', () => ({ useStore: () => { } }));
5
7
 
6
8
  describe('component: Metadata/IdentifyingInformation', () => {
7
9
  const label = 'LABEL';
@@ -76,10 +78,35 @@ describe('component: Metadata/IdentifyingInformation', () => {
76
78
  expect(wrapper.find(`.value .status.${ status }`).exists()).toBeTruthy();
77
79
  });
78
80
 
79
- it('should render a valueOverride', async() => {
81
+ it('should render a .full-custom-value valueOverride', async() => {
80
82
  const valueOverride = {
81
- component: markRaw(Rectangle),
82
- props: { outline: false }
83
+ component: markRaw(KeyValueRow),
84
+ props: { type: 'interactive' }
85
+ };
86
+ const wrapper = mount(IdentifyingInformation, {
87
+ props: {
88
+ rows: [
89
+ {
90
+ label,
91
+ value,
92
+ valueOverride
93
+ }
94
+ ]
95
+ },
96
+ global: { stubs: { 'router-link': RouterLinkStub, KeyValueRow: true } }
97
+ });
98
+
99
+ expect(wrapper.find('.label').element.innerHTML.trim()).toStrictEqual(label);
100
+
101
+ const testComponent = wrapper.find('.full-custom-value').getComponent(KeyValueRow);
102
+
103
+ expect(testComponent.props('type')).toStrictEqual(valueOverride.props.type);
104
+ });
105
+
106
+ it('should render a formatter valueOverride', async() => {
107
+ const valueOverride = {
108
+ component: 'router-link',
109
+ props: { to: '#' }
83
110
  };
84
111
  const wrapper = mount(IdentifyingInformation, {
85
112
  props: {
@@ -96,8 +123,8 @@ describe('component: Metadata/IdentifyingInformation', () => {
96
123
 
97
124
  expect(wrapper.find('.label').element.innerHTML.trim()).toStrictEqual(label);
98
125
 
99
- const testComponent = wrapper.find('.value').getComponent(Rectangle);
126
+ const testComponent: any = wrapper.find('.value').getComponent('a');
100
127
 
101
- expect(testComponent.props('outline')).toStrictEqual(valueOverride.props.outline);
128
+ expect(testComponent.props('to')).toStrictEqual('#');
102
129
  });
103
130
  });
@@ -1,9 +1,12 @@
1
1
  import { useI18n } from '@shell/composables/useI18n';
2
- import { computed, ComputedRef, markRaw, toValue } from 'vue';
2
+ import {
3
+ computed, ComputedRef, defineAsyncComponent, markRaw, toValue
4
+ } from 'vue';
3
5
  import Additional from '@shell/components/Resource/Detail/Additional.vue';
4
6
  import { useStore } from 'vuex';
5
7
  import {
6
- NAMESPACE, FLEET, SERVICE_ACCOUNT, SECRET, CAPI
8
+ NAMESPACE, FLEET, SERVICE_ACCOUNT, SECRET, CAPI,
9
+ MANAGEMENT
7
10
  } from '@shell/config/types';
8
11
  import { Row } from '@shell/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue';
9
12
  import { NAME as FLEET_NAME } from '@shell/config/product/fleet';
@@ -21,21 +24,18 @@ export const useNamespace = (resource: any): ComputedRef<Row> | undefined => {
21
24
  }
22
25
 
23
26
  return computed(() => {
24
- const to = resourceValue.namespaceLocation || {
25
- name: `c-cluster-product-resource-id`,
26
- params: {
27
- product: store.getters['productId'],
28
- cluster: store.getters['clusterId'],
29
- resource: NAMESPACE,
30
- id: resourceValue.namespace
31
- }
32
- };
33
-
34
27
  return {
35
28
  label: i18n.t('component.resource.detail.metadata.identifyingInformation.namespace'),
36
29
  value: resourceValue.namespace,
37
30
  valueDataTestid: 'masthead-subheader-namespace',
38
- to
31
+ valueOverride: {
32
+ component: markRaw(defineAsyncComponent(() => import('@shell/components/Resource/Detail/ResourcePopover/index.vue'))),
33
+ props: {
34
+ type: NAMESPACE,
35
+ id: resourceValue.namespace,
36
+ detailLocation: resourceValue.namespaceLocation
37
+ }
38
+ }
39
39
  };
40
40
  });
41
41
  };
@@ -88,28 +88,6 @@ export const useLiveDate = (resource: any): ComputedRef<Row> | undefined => {
88
88
  }));
89
89
  };
90
90
 
91
- export const useCreatedBy = (resource: any): ComputedRef<Row> | undefined => {
92
- const store = useStore();
93
- const i18n = useI18n(store);
94
- const resourceValue = toValue(resource);
95
-
96
- if (!resourceValue.showCreatedBy) {
97
- return;
98
- }
99
-
100
- return computed(() => {
101
- const to = resourceValue.createdBy.location || undefined;
102
-
103
- return {
104
- label: i18n.t('component.resource.detail.metadata.identifyingInformation.createdBy'),
105
- value: resourceValue.createdBy.displayName,
106
- to,
107
- dataTestid: 'masthead-subheader-createdBy',
108
- valueDataTestid: to ? 'masthead-subheader-createdBy-link' : 'masthead-subheader-createdBy_plain-text'
109
- };
110
- });
111
- };
112
-
113
91
  export const useProject = (resource: any): ComputedRef<Row> | undefined => {
114
92
  const store = useStore();
115
93
  const i18n = useI18n(store);
@@ -126,9 +104,17 @@ export const useProject = (resource: any): ComputedRef<Row> | undefined => {
126
104
 
127
105
  return computed(() => {
128
106
  return {
129
- label: i18n.t('component.resource.detail.metadata.identifyingInformation.project'),
130
- value: resourceValue.project?.nameDisplay,
131
- to: resourceValue.project?.detailLocation
107
+ label: i18n.t('component.resource.detail.metadata.identifyingInformation.project'),
108
+ value: resourceValue.project?.nameDisplay,
109
+ valueDataTestid: 'masthead-subheader-project',
110
+ valueOverride: {
111
+ component: markRaw(defineAsyncComponent(() => import('@shell/components/Resource/Detail/ResourcePopover/index.vue'))),
112
+ props: {
113
+ type: MANAGEMENT.PROJECT,
114
+ id: resourceValue.project?.id,
115
+ currentStore: 'management'
116
+ }
117
+ }
132
118
  };
133
119
  });
134
120
  };
@@ -39,8 +39,22 @@ const getRowValueId = (row:Row): string => `value-${ row.label }:${ row.value }`
39
39
  >
40
40
  {{ row.label }}
41
41
  </label>
42
+ <!-- A custom component specified as an object, responsible for it's own styling -->
42
43
  <div
43
- v-if="row.valueOverride?.component && row.value"
44
+ v-if="typeof row.valueOverride?.component !== 'string' && row.valueOverride?.component && row.value"
45
+ :id="getRowValueId(row)"
46
+ class="full-custom-value"
47
+ >
48
+ <component
49
+ :is="row.valueOverride?.component"
50
+ v-if="row.valueOverride?.component"
51
+ v-bind="row.valueOverride?.props"
52
+ :data-testid="row.valueDataTestid"
53
+ />
54
+ </div>
55
+ <!-- A formatter with a component specified as a string -->
56
+ <div
57
+ v-else-if="row.valueOverride?.component && row.value"
44
58
  :id="getRowValueId(row)"
45
59
  class="value"
46
60
  >
@@ -91,16 +105,22 @@ const getRowValueId = (row:Row): string => `value-${ row.label }:${ row.value }`
91
105
  .row {
92
106
  margin-bottom: 8px;
93
107
 
108
+ .full-custom-value {
109
+ flex: 1;
110
+ }
111
+
94
112
  .value {
95
113
  display: flex;
96
114
  flex-direction: row;
97
115
  align-items: center;
116
+ flex: 1;
98
117
 
99
- &, & * {
118
+ & > div, & > span {
100
119
  max-width: 100%;
101
- overflow: hidden;
102
- text-overflow: ellipsis;
103
- white-space: nowrap;
120
+ }
121
+
122
+ &, & > div, & > span {
123
+ @include clip;
104
124
  }
105
125
  }
106
126
 
@@ -1,8 +1,9 @@
1
1
  <script lang="ts">
2
2
  import { computed, toRefs } from 'vue';
3
- import Rectangle from '@shell/components/Resource/Detail/Metadata/Rectangle.vue';
3
+ import KeyValueRow from '@shell/components/Resource/Detail/Metadata/KeyValueRow.vue';
4
4
  import { useStore } from 'vuex';
5
5
  import { useI18n } from '@shell/composables/useI18n';
6
+ import { Type } from '@components/Pill/types';
6
7
 
7
8
  export type KeyValueType = {[key: string]: string};
8
9
 
@@ -14,9 +15,9 @@ export interface Row {
14
15
  export interface KeyValueProps {
15
16
  propertyName: string;
16
17
  rows: Row[];
17
- maxRows?: number;
18
- outline?: boolean;
18
+ type: Type;
19
19
 
20
+ maxRows?: number;
20
21
  onShowConfiguration?: (returnFocusSelector: string) => void;
21
22
  }
22
23
  </script>
@@ -24,13 +25,9 @@ export interface KeyValueProps {
24
25
  <script setup lang="ts">
25
26
  const props = withDefaults(
26
27
  defineProps<KeyValueProps>(),
27
- {
28
- outline: false, maxRows: 4, onShowConfiguration: undefined
29
- }
28
+ { maxRows: 4, onShowConfiguration: undefined }
30
29
  );
31
- const {
32
- propertyName, rows, maxRows, outline
33
- } = toRefs(props);
30
+ const { propertyName, rows, maxRows } = toRefs(props);
34
31
 
35
32
  const store = useStore();
36
33
  const i18n = useI18n(store);
@@ -80,12 +77,11 @@ const showConfigurationMoreFocusSelector = computed(() => `[data-testid="${ show
80
77
  :key="displayValue(row)"
81
78
  class="row"
82
79
  >
83
- <Rectangle
84
- v-clean-tooltip="displayValue(row)"
85
- :outline="outline"
86
- >
87
- {{ displayValue(row) }}
88
- </Rectangle>
80
+ <KeyValueRow
81
+ class="rectangle"
82
+ :type="props.type"
83
+ :row="row"
84
+ />
89
85
  </div>
90
86
  <a
91
87
  v-if="showShowAllButton"
@@ -116,6 +112,7 @@ const showConfigurationMoreFocusSelector = computed(() => `[data-testid="${ show
116
112
  .row {
117
113
  display: block;
118
114
  width: 100%;
115
+ display: inline-block;
119
116
 
120
117
  &:not(:nth-child(2)) {
121
118
  margin-top: 4px;
@@ -125,14 +122,6 @@ const showConfigurationMoreFocusSelector = computed(() => `[data-testid="${ show
125
122
  margin-top: 8px;
126
123
  }
127
124
 
128
- .rectangle {
129
- display: inline-block;
130
- max-width: 100%;
131
- overflow: hidden;
132
- text-overflow: ellipsis;
133
- white-space: nowrap;
134
- }
135
-
136
125
  .no-rows {
137
126
  line-height: 21px;
138
127
  }