@rancher/shell 3.0.8 → 3.0.9-rc.2

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 (192) hide show
  1. package/apis/intf/modal.ts +38 -0
  2. package/apis/intf/slide-in.ts +3 -1
  3. package/apis/shell/__tests__/slide-in.test.ts +36 -0
  4. package/apis/shell/slide-in.ts +5 -1
  5. package/assets/styles/base/_color.scss +1 -0
  6. package/assets/styles/base/_typography.scss +14 -5
  7. package/assets/styles/themes/_light.scss +1 -1
  8. package/assets/styles/themes/_modern.scss +1 -1
  9. package/assets/translations/en-us.yaml +94 -33
  10. package/assets/translations/zh-hans.yaml +0 -2
  11. package/components/ActionMenuShell.vue +4 -4
  12. package/components/CodeMirror.vue +4 -3
  13. package/components/DetailText.vue +54 -7
  14. package/components/Drawer/Chrome.vue +11 -4
  15. package/components/Drawer/DrawerCard.vue +19 -0
  16. package/components/Drawer/ResourceDetailDrawer/ConfigTab.vue +3 -11
  17. package/components/Drawer/ResourceDetailDrawer/__tests__/ConfigTab.test.ts +2 -2
  18. package/components/Drawer/ResourceDetailDrawer/index.vue +3 -20
  19. package/components/Drawer/types.ts +1 -0
  20. package/components/DynamicContent/DynamicContentCloseButton.vue +2 -2
  21. package/components/LocaleSelector.vue +1 -1
  22. package/components/Markdown.vue +1 -1
  23. package/components/PopoverCard.vue +3 -3
  24. package/components/Resource/Detail/Card/ExtrasCard.vue +39 -0
  25. package/components/Resource/Detail/Card/StateCard/__tests__/composables.test.ts +142 -0
  26. package/components/Resource/Detail/Card/StateCard/composables.ts +41 -11
  27. package/components/Resource/Detail/Card/StateCard/index.vue +3 -9
  28. package/components/Resource/Detail/Card/StateCard/types.ts +6 -0
  29. package/components/Resource/Detail/Card/{PodsCard → StatusCard}/index.vue +11 -10
  30. package/components/Resource/Detail/Card/__tests__/PodsCard.test.ts +24 -25
  31. package/components/Resource/Detail/Cards.vue +27 -0
  32. package/components/Resource/Detail/Masthead/__tests__/index.test.ts +70 -0
  33. package/components/Resource/Detail/Masthead/index.vue +5 -0
  34. package/components/Resource/Detail/Metadata/KeyValueRow.vue +4 -2
  35. package/components/Resource/Detail/ResourcePopover/ResourcePopoverCard.vue +2 -2
  36. package/components/Resource/Detail/ResourceRow.types.ts +14 -0
  37. package/components/Resource/Detail/ResourceRow.vue +23 -35
  38. package/components/Resource/Detail/StatusRow.vue +5 -2
  39. package/components/Resource/Detail/TitleBar/__tests__/composables.test.ts +38 -7
  40. package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +106 -2
  41. package/components/Resource/Detail/TitleBar/composables.ts +2 -1
  42. package/components/Resource/Detail/TitleBar/index.vue +41 -6
  43. package/components/ResourceDetail/Masthead/__tests__/index.test.ts +49 -1
  44. package/components/ResourceDetail/Masthead/__tests__/latest.test.ts +85 -0
  45. package/components/ResourceDetail/Masthead/index.vue +1 -0
  46. package/components/ResourceDetail/Masthead/latest.vue +8 -1
  47. package/components/ResourceDetail/Masthead/legacy.vue +1 -1
  48. package/components/Setting.vue +1 -1
  49. package/components/SortableTable/index.vue +25 -0
  50. package/components/SortableTable/selection.js +25 -12
  51. package/components/SortableTable/sorting.js +1 -1
  52. package/components/Tabbed/Tab.vue +1 -0
  53. package/components/Tabbed/index.vue +29 -6
  54. package/components/Window/ContainerShell.vue +10 -13
  55. package/components/fleet/FleetClusterTargets/TargetsList.vue +47 -29
  56. package/components/fleet/FleetClusterTargets/index.vue +82 -29
  57. package/components/fleet/FleetClusters.vue +26 -12
  58. package/components/fleet/FleetGitRepoPaths.vue +2 -2
  59. package/components/fleet/FleetResources.vue +14 -0
  60. package/components/fleet/FleetValuesFrom.vue +2 -2
  61. package/components/fleet/__tests__/FleetClusterTargets.test.ts +531 -0
  62. package/components/fleet/__tests__/FleetClusters.test.ts +576 -0
  63. package/components/fleet/dashboard/ResourceDetails.vue +96 -123
  64. package/components/form/Conditions.vue +1 -15
  65. package/components/form/HookOption.vue +5 -0
  66. package/components/form/LabeledSelect.vue +1 -1
  67. package/components/form/LifecycleHooks.vue +2 -6
  68. package/components/form/ResourceLabeledSelect.vue +12 -1
  69. package/components/form/SeccompProfile.vue +113 -0
  70. package/components/form/Security.vue +244 -133
  71. package/components/form/__tests__/LabeledSelect.test.ts +1 -1
  72. package/components/form/__tests__/SeccompProfile.test.js +124 -0
  73. package/components/form/__tests__/Security.test.ts +125 -37
  74. package/components/formatter/Autoscaler.vue +2 -2
  75. package/components/formatter/FleetSummaryGraph.vue +4 -1
  76. package/components/nav/Group.vue +5 -0
  77. package/components/nav/Header.vue +3 -3
  78. package/components/nav/HeaderPageActionMenu.vue +1 -1
  79. package/components/nav/NamespaceFilter.vue +6 -6
  80. package/components/nav/NotificationCenter/index.vue +1 -1
  81. package/components/nav/TopLevelMenu.helper.ts +41 -16
  82. package/components/nav/TopLevelMenu.vue +45 -25
  83. package/components/nav/WorkspaceSwitcher.vue +1 -1
  84. package/components/nav/__tests__/TopLevelMenu.helper.test.ts +277 -0
  85. package/components/nav/__tests__/TopLevelMenu.test.ts +160 -4
  86. package/components/templates/default.vue +0 -3
  87. package/components/templates/home.vue +0 -3
  88. package/components/templates/plain.vue +0 -3
  89. package/composables/useClickOutside.ts +1 -1
  90. package/config/product/explorer.js +1 -2
  91. package/config/types.js +41 -8
  92. package/detail/__tests__/workload.test.ts +8 -16
  93. package/detail/catalog.cattle.io.app.vue +6 -0
  94. package/detail/fleet.cattle.io.cluster.vue +6 -0
  95. package/detail/workload/index.vue +7 -109
  96. package/edit/__tests__/projectsecret.test.ts +42 -0
  97. package/edit/auth/__tests__/oidc.test.ts +50 -0
  98. package/edit/auth/oidc.vue +68 -44
  99. package/edit/autoscaling.horizontalpodautoscaler/index.vue +140 -59
  100. package/edit/autoscaling.horizontalpodautoscaler/metrics-row.vue +41 -5
  101. package/edit/projectsecret.vue +29 -0
  102. package/edit/provisioning.cattle.io.cluster/__tests__/Basics.test.ts +89 -200
  103. package/edit/provisioning.cattle.io.cluster/__tests__/Networking.test.ts +58 -17
  104. package/edit/provisioning.cattle.io.cluster/rke2.vue +11 -0
  105. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +3 -63
  106. package/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue +82 -14
  107. package/edit/workload/__tests__/index.test.ts +122 -85
  108. package/edit/workload/index.vue +48 -29
  109. package/edit/workload/mixins/workload.js +85 -32
  110. package/list/catalog.cattle.io.clusterrepo.vue +1 -1
  111. package/list/projectsecret.vue +2 -2
  112. package/machine-config/__tests__/vmwarevsphere.test.ts +64 -0
  113. package/machine-config/amazonec2.vue +2 -2
  114. package/machine-config/vmwarevsphere.vue +58 -4
  115. package/mixins/__tests__/brand.spec.ts +18 -13
  116. package/mixins/__tests__/chart.test.ts +63 -0
  117. package/mixins/chart.js +56 -51
  118. package/models/__tests__/catalog.cattle.io.app.test.ts +33 -0
  119. package/models/__tests__/workload.test.ts +333 -0
  120. package/models/catalog.cattle.io.app.js +8 -0
  121. package/models/pod.js +14 -0
  122. package/models/secret.js +1 -1
  123. package/models/workload.js +93 -27
  124. package/package.json +4 -4
  125. package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +91 -0
  126. package/pages/c/_cluster/apps/charts/install.vue +4 -4
  127. package/pages/c/_cluster/explorer/EventsTable.vue +2 -2
  128. package/pages/c/_cluster/fleet/index.vue +18 -12
  129. package/pages/c/_cluster/manager/hostedprovider/index.vue +1 -19
  130. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +1 -1
  131. package/pages/c/_cluster/uiplugins/index.vue +1 -1
  132. package/plugins/dashboard-store/__tests__/resource-class.test.ts +234 -0
  133. package/plugins/dashboard-store/actions.js +9 -8
  134. package/plugins/dashboard-store/resource-class.js +97 -1
  135. package/plugins/steve/__tests__/revision.test.ts +84 -0
  136. package/plugins/steve/__tests__/steve-pagination-utils.test.ts +30 -0
  137. package/plugins/steve/__tests__/subscribe.spec.ts +134 -0
  138. package/plugins/steve/mutations.js +9 -0
  139. package/plugins/steve/revision.ts +26 -0
  140. package/plugins/steve/steve-pagination-utils.ts +6 -5
  141. package/plugins/steve/subscribe.js +211 -51
  142. package/plugins/subscribe-events.ts +2 -2
  143. package/rancher-components/Form/Checkbox/Checkbox.vue +13 -0
  144. package/rancher-components/LabeledTooltip/LabeledTooltip.vue +1 -1
  145. package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.vue +1 -1
  146. package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +3 -1
  147. package/rancher-components/Pill/RcStatusIndicator/RcStatusIndicator.vue +3 -1
  148. package/rancher-components/Pill/RcTag/RcTag.vue +1 -1
  149. package/rancher-components/Pill/index.ts +4 -0
  150. package/rancher-components/RcButton/RcButton.test.ts +53 -9
  151. package/rancher-components/RcButton/RcButton.vue +217 -25
  152. package/rancher-components/RcButton/types.ts +27 -1
  153. package/rancher-components/RcDropdown/RcDropdownMenu.vue +4 -4
  154. package/rancher-components/RcDropdown/types.ts +3 -3
  155. package/rancher-components/RcIcon/RcIcon.test.ts +42 -0
  156. package/rancher-components/RcIcon/RcIcon.vue +9 -6
  157. package/rancher-components/RcIcon/types.ts +13 -9
  158. package/rancher-components/utils/status.test.ts +10 -15
  159. package/rancher-components/utils/status.ts +5 -6
  160. package/store/aws.js +18 -12
  161. package/store/index.js +4 -8
  162. package/store/type-map.utils.ts +1 -1
  163. package/types/kube/kube-api.ts +29 -3
  164. package/types/rancher/steve.api.ts +40 -0
  165. package/types/shell/index.d.ts +99 -0
  166. package/types/store/dashboard-store.types.ts +29 -7
  167. package/types/store/pagination.types.ts +1 -0
  168. package/types/store/subscribe-events.types.ts +1 -0
  169. package/utils/__tests__/azure.test.ts +56 -0
  170. package/utils/__tests__/back-off.test.ts +364 -245
  171. package/utils/__tests__/error.test.ts +44 -0
  172. package/utils/__tests__/fleet.test.ts +8 -1
  173. package/utils/__tests__/pagination-wrapper.test.ts +167 -0
  174. package/utils/__tests__/version.test.ts +55 -1
  175. package/utils/azure.js +12 -0
  176. package/utils/back-off.ts +302 -69
  177. package/utils/cspAdaptor.ts +32 -14
  178. package/utils/dynamic-content/__tests__/index.test.ts +1 -1
  179. package/utils/dynamic-content/__tests__/new-release.test.ts +48 -7
  180. package/utils/dynamic-content/__tests__/support-notice.test.ts +1 -4
  181. package/utils/dynamic-content/index.ts +1 -6
  182. package/utils/dynamic-content/new-release.ts +5 -3
  183. package/utils/dynamic-content/types.d.ts +0 -1
  184. package/utils/error.js +9 -0
  185. package/utils/fleet.ts +2 -2
  186. package/utils/inactivity.ts +2 -3
  187. package/utils/pagination-wrapper.ts +101 -17
  188. package/utils/validators/formRules/index.ts +3 -0
  189. package/utils/version.js +38 -0
  190. package/components/auth/AzureWarning.vue +0 -77
  191. /package/components/Resource/Detail/{Card/PodsCard/Bubble.vue → Bubble.vue} +0 -0
  192. /package/components/Resource/Detail/Card/{PodsCard → StatusCard}/composable.ts +0 -0
@@ -38,13 +38,13 @@ describe('component: ResourceDetailDrawer/ConfigTab', () => {
38
38
  expect(component.props('name')).toStrictEqual('config-tab');
39
39
  });
40
40
 
41
- it('should render a dynamic component within the .container and pass the correct props', () => {
41
+ it('should render a dynamic component within DrawerCard and pass the correct props', () => {
42
42
  const wrapper = mount(ConfigTab, {
43
43
  props: { resource, component: markRaw(DynamicComponent) },
44
44
  global
45
45
  });
46
46
 
47
- const component = wrapper.find('.container').getComponent(DynamicComponent);
47
+ const component = wrapper.findComponent(DynamicComponent);
48
48
 
49
49
  expect(component.props('value')).toStrictEqual(resource);
50
50
  expect(component.props('mode')).toStrictEqual(_VIEW);
@@ -59,7 +59,6 @@ useResourceDetailDrawerProvider();
59
59
  </script>
60
60
  <template>
61
61
  <Drawer
62
- class="resource-detail-drawer"
63
62
  :ariaTarget="title"
64
63
  @close="emit('close')"
65
64
  >
@@ -72,10 +71,10 @@ useResourceDetailDrawerProvider();
72
71
  </template>
73
72
  <template #body>
74
73
  <Tabbed
75
- class="tabbed"
76
74
  :useHash="false"
77
75
  :showExtensionTabs="false"
78
76
  :componentTestid="componentTestid"
77
+ :remove-borders="true"
79
78
  @changed="({selectedName}) => {activeTab = selectedName;}"
80
79
  >
81
80
  <ConfigTab
@@ -92,7 +91,8 @@ useResourceDetailDrawerProvider();
92
91
  <template #additional-actions>
93
92
  <RcButton
94
93
  v-if="canEdit"
95
- :primary="true"
94
+ variant="primary"
95
+ size="large"
96
96
  :aria-label="action.ariaLabel"
97
97
  :data-testid="editBttnDataTestId"
98
98
  @click="action.action"
@@ -102,20 +102,3 @@ useResourceDetailDrawerProvider();
102
102
  </template>
103
103
  </Drawer>
104
104
  </template>
105
-
106
- <style lang="scss" scoped>
107
- .resource-detail-drawer {
108
- :deep() .tabbed {
109
- & > .tabs {
110
- border: none;
111
- }
112
-
113
- & > .tab-container {
114
- border: none;
115
- border-top: 1px solid var(--border);
116
- padding: 0;
117
- padding-top: 24px;
118
- }
119
- }
120
- }
121
- </style>
@@ -1,3 +1,4 @@
1
1
  export interface Props {
2
2
  ariaTarget: string;
3
+ removeFooter?: boolean;
3
4
  }
@@ -18,8 +18,8 @@ const markRead = () => {
18
18
  </script>
19
19
  <template>
20
20
  <RcButton
21
- ghost
22
- small
21
+ variant="ghost"
22
+ size="small"
23
23
  :aria-label="t('dynamicContent.action.close')"
24
24
  tabindex="0"
25
25
  @click="markRead()"
@@ -79,7 +79,7 @@ export default {
79
79
  >
80
80
  <rc-dropdown-trigger
81
81
  data-testid="locale-selector"
82
- link
82
+ variant="link"
83
83
  class="baseline locale-selector-btn"
84
84
  :aria-label="t('locale.menu')"
85
85
  >
@@ -93,7 +93,7 @@ export default {
93
93
  color: rgb(101, 109, 118);
94
94
  border-left: 0.25em solid rgb(208, 215, 222);
95
95
  padding: 0 1em;
96
- margin-bottom: 16px;
96
+ margin: 10px 8px 8px 8px;
97
97
  }
98
98
 
99
99
  table {
@@ -57,7 +57,7 @@ watch(
57
57
  >
58
58
  <slot name="default" />
59
59
  <RcButton
60
- ghost
60
+ variant="ghost"
61
61
  class="focus-button"
62
62
  :aria-label="props.showPopoverAriaLabel"
63
63
  aria-haspopup="true"
@@ -128,7 +128,7 @@ watch(
128
128
  display: inline-block;
129
129
  }
130
130
 
131
- .focus-button {
131
+ .rc-button.btn.focus-button {
132
132
  margin-left: 4px;
133
133
  margin-right: 2px;
134
134
  padding: 0;
@@ -160,7 +160,7 @@ watch(
160
160
  }
161
161
 
162
162
  &:deep() {
163
- & > .v-popper > .btn.role-link {
163
+ & > .v-popper > .btn.variant-link {
164
164
  padding: 0;
165
165
  min-height: initial;
166
166
  line-height: initial;
@@ -0,0 +1,39 @@
1
+ <script lang="ts">
2
+ import Card from '@shell/components/Resource/Detail/Card/index.vue';
3
+ import { useI18n } from '@shell/composables/useI18n';
4
+ import { useStore } from 'vuex';
5
+ import { BLANK_CLUSTER } from '@shell/store/store-types';
6
+ </script>
7
+ <script setup lang="ts">
8
+ import { useRouter } from 'vue-router';
9
+
10
+ const store = useStore();
11
+ const router = useRouter();
12
+ const i18n = useI18n(store);
13
+
14
+ const extensionsUrl = router.resolve({
15
+ name: 'c-cluster-uiplugins',
16
+ params: { cluster: BLANK_CLUSTER }
17
+ }).href;
18
+
19
+ const clusterToolsUrl = router.resolve({
20
+ name: 'c-cluster-apps-charts',
21
+ params: { cluster: BLANK_CLUSTER }
22
+ }).href;
23
+ </script>
24
+
25
+ <template>
26
+ <Card :title="i18n.t('component.resource.detail.card.extrasCard.title')">
27
+ <p
28
+ v-clean-html="i18n.t('component.resource.detail.card.extrasCard.message', { extensionsUrl, clusterToolsUrl }, true)"
29
+ class="message text-deemphasized"
30
+ />
31
+ </Card>
32
+ </template>
33
+
34
+ <style lang="scss" scoped>
35
+ .message {
36
+ margin: 0;
37
+ line-height: 1.5;
38
+ }
39
+ </style>
@@ -0,0 +1,142 @@
1
+ import { useResourceCardRow } from '@shell/components/Resource/Detail/Card/StateCard/composables';
2
+
3
+ describe('useResourceCardRow', () => {
4
+ describe('with default keys', () => {
5
+ it('should return undefined counts when resources is empty', () => {
6
+ const result = useResourceCardRow('Test', []);
7
+
8
+ expect(result.label).toBe('Test');
9
+ expect(result.color).toBeUndefined();
10
+ expect(result.counts).toBeUndefined();
11
+ });
12
+
13
+ it('should aggregate resources by stateDisplay', () => {
14
+ const resources = [
15
+ { stateSimpleColor: 'success', stateDisplay: 'Running' },
16
+ { stateSimpleColor: 'success', stateDisplay: 'Running' },
17
+ { stateSimpleColor: 'error', stateDisplay: 'Failed' }
18
+ ];
19
+
20
+ const result = useResourceCardRow('Pods', resources);
21
+
22
+ expect(result.label).toBe('Pods');
23
+ expect(result.counts).toHaveLength(2);
24
+ });
25
+
26
+ it('should return highest alert color as the main color', () => {
27
+ const resources = [
28
+ { stateSimpleColor: 'success', stateDisplay: 'Running' },
29
+ { stateSimpleColor: 'error', stateDisplay: 'Failed' },
30
+ { stateSimpleColor: 'warning', stateDisplay: 'Pending' }
31
+ ];
32
+
33
+ const result = useResourceCardRow('Pods', resources);
34
+
35
+ expect(result.color).toBe('error');
36
+ });
37
+
38
+ it('should sort counts by alert level (error first)', () => {
39
+ const resources = [
40
+ { stateSimpleColor: 'success', stateDisplay: 'Running' },
41
+ { stateSimpleColor: 'error', stateDisplay: 'Failed' },
42
+ { stateSimpleColor: 'warning', stateDisplay: 'Pending' }
43
+ ];
44
+
45
+ const result = useResourceCardRow('Pods', resources);
46
+
47
+ expect(result.counts![0].color).toBe('error');
48
+ expect(result.counts![1].color).toBe('warning');
49
+ expect(result.counts![2].color).toBe('success');
50
+ });
51
+
52
+ it('should sort by count when colors are equal', () => {
53
+ const resources = [
54
+ { stateSimpleColor: 'success', stateDisplay: 'Running' },
55
+ { stateSimpleColor: 'success', stateDisplay: 'Running' },
56
+ { stateSimpleColor: 'success', stateDisplay: 'Running' },
57
+ { stateSimpleColor: 'success', stateDisplay: 'Completed' }
58
+ ];
59
+
60
+ const result = useResourceCardRow('Pods', resources);
61
+
62
+ expect(result.counts![0].label).toBe('running');
63
+ expect(result.counts![0].count).toBe(3);
64
+ expect(result.counts![1].label).toBe('completed');
65
+ expect(result.counts![1].count).toBe(1);
66
+ });
67
+
68
+ it('should use default color when stateSimpleColor is undefined', () => {
69
+ const resources = [
70
+ { stateDisplay: 'Unknown' }
71
+ ];
72
+
73
+ const result = useResourceCardRow('Pods', resources);
74
+
75
+ expect(result.color).toBe('disabled');
76
+ });
77
+
78
+ it('should include to parameter when provided', () => {
79
+ const to = { hash: '#pods' };
80
+ const result = useResourceCardRow('Pods', [], undefined, undefined, to);
81
+
82
+ expect(result.to).toStrictEqual(to);
83
+ });
84
+ });
85
+
86
+ describe('with custom keys', () => {
87
+ it('should use custom stateColorKey', () => {
88
+ const resources = [
89
+ { customColor: 'error', stateDisplay: 'Failed' },
90
+ { customColor: 'success', stateDisplay: 'Running' }
91
+ ];
92
+
93
+ const result = useResourceCardRow('Conditions', resources, 'customColor');
94
+
95
+ expect(result.color).toBe('error');
96
+ });
97
+
98
+ it('should use custom stateDisplayKey', () => {
99
+ const resources = [
100
+ { stateSimpleColor: 'success', condition: 'Available' },
101
+ { stateSimpleColor: 'success', condition: 'Available' },
102
+ { stateSimpleColor: 'success', condition: 'Progressing' }
103
+ ];
104
+
105
+ const result = useResourceCardRow('Conditions', resources, undefined, 'condition');
106
+
107
+ // Both have same color (success), so sorted by count (higher first)
108
+ expect(result.counts![0].label).toBe('available');
109
+ expect(result.counts![0].count).toBe(2);
110
+ expect(result.counts![1].label).toBe('progressing');
111
+ expect(result.counts![1].count).toBe(1);
112
+ });
113
+
114
+ it('should use both custom keys together', () => {
115
+ const resources = [
116
+ { statusColor: 'error', status: 'False' },
117
+ { statusColor: 'disabled', status: 'True' }
118
+ ];
119
+
120
+ const result = useResourceCardRow('Status', resources, 'statusColor', 'status');
121
+
122
+ expect(result.color).toBe('error');
123
+ expect(result.counts).toContainEqual(expect.objectContaining({ label: 'false', color: 'error' }));
124
+ expect(result.counts).toContainEqual(expect.objectContaining({ label: 'true', color: 'disabled' }));
125
+ });
126
+ });
127
+
128
+ describe('lowercase handling', () => {
129
+ it('should convert stateDisplay to lowercase', () => {
130
+ const resources = [
131
+ { stateSimpleColor: 'success', stateDisplay: 'RUNNING' },
132
+ { stateSimpleColor: 'success', stateDisplay: 'Running' }
133
+ ];
134
+
135
+ const result = useResourceCardRow('Pods', resources);
136
+
137
+ expect(result.counts).toHaveLength(1);
138
+ expect(result.counts![0].label).toBe('running');
139
+ expect(result.counts![0].count).toBe(2);
140
+ });
141
+ });
142
+ });
@@ -1,20 +1,50 @@
1
- import { extractCounts, Props as ResourceRowProps } from '@shell/components/Resource/Detail/ResourceRow.vue';
1
+ import { Count, Props as ResourceRowProps } from '@shell/components/Resource/Detail/ResourceRow.types';
2
2
  import { useI18n } from '@shell/composables/useI18n';
3
3
  import { INGRESS, SERVICE } from '@shell/config/types';
4
- import { getHighestAlertColor } from '@shell/utils/style';
4
+ import { isHigherAlert, StateColor } from '@shell/utils/style';
5
5
  import { computed, Ref, toValue } from 'vue';
6
6
  import { useStore } from 'vuex';
7
- import { Props as StateCardProps } from '@shell/components/Resource/Detail/Card/StateCard/index.vue';
7
+ import { Props as StateCardProps } from '@shell/components/Resource/Detail/Card/StateCard/types';
8
8
  import { RouteLocationRaw } from 'vue-router';
9
9
 
10
- export function useResourceCardRow(label: string, resources: any[], to?: RouteLocationRaw): ResourceRowProps {
11
- const colors = resources.map((r: any) => r.stateSimpleColor);
12
- const states = resources.map((r: any) => r.stateDisplay.toLowerCase());
10
+ export function useResourceCardRow(label: string, resources: any[], stateColorKey = 'stateSimpleColor', stateDisplayKey = 'stateDisplay', to?: RouteLocationRaw): ResourceRowProps {
11
+ const agg: any = {};
12
+
13
+ resources.forEach((r: any) => {
14
+ const state = r[stateDisplayKey]?.toLowerCase();
15
+ const color = r[stateColorKey] || 'disabled';
16
+
17
+ agg[state] = agg[state] || {
18
+ color, label: state, count: 0
19
+ };
20
+ agg[state].count++;
21
+ });
22
+
23
+ interface Tuple extends Count {
24
+ color: StateColor;
25
+ }
26
+ const tuples: Tuple[] = Object.values(agg);
27
+
28
+ tuples.sort((left: any, right: any) => {
29
+ if (isHigherAlert(left.color, right.color)) {
30
+ return -1;
31
+ }
32
+
33
+ if (left.color !== right.color) {
34
+ return 1;
35
+ }
36
+
37
+ if (left.count === right.count) {
38
+ return 0;
39
+ }
40
+
41
+ return left.count > right.count ? -1 : 1;
42
+ });
13
43
 
14
44
  return {
15
45
  label,
16
- color: resources.length ? getHighestAlertColor(colors) : undefined,
17
- counts: resources.length ? extractCounts(states) : undefined,
46
+ color: tuples.length ? tuples[0].color : undefined,
47
+ counts: tuples.length ? tuples : undefined,
18
48
  to
19
49
  };
20
50
  }
@@ -27,7 +57,7 @@ export interface Pairs {
27
57
 
28
58
  export function useDefaultResources(pairs: Ref<Pairs[]>) {
29
59
  const pairsValue = toValue(pairs);
30
- const rows = computed(() => pairsValue.map(({ label, resources, to }) => useResourceCardRow(label, resources, to)));
60
+ const rows = computed(() => pairsValue.map(({ label, resources, to }) => useResourceCardRow(label, resources, undefined, undefined, to)));
31
61
 
32
62
  return rows;
33
63
  }
@@ -93,13 +123,13 @@ export function useDefaultWorkloadInsightsCardProps(): StateCardProps {
93
123
  const rows: ResourceRowProps[] = [
94
124
  {
95
125
  label: i18n.t('component.resource.detail.card.insightsCard.rows.conditions'),
96
- to: '#',
126
+ to: '#conditions',
97
127
  color: 'disabled',
98
128
  counts: [{ label: 'Available', count: 1 }, { label: 'Progressing', count: 1 }]
99
129
  },
100
130
  {
101
131
  label: i18n.t('component.resource.detail.card.insightsCard.rows.events'),
102
- to: '#',
132
+ to: '#events',
103
133
  color: 'disabled',
104
134
  counts: [{ label: 'Normal', count: 2 }]
105
135
  }
@@ -1,14 +1,8 @@
1
- <script lang="ts">
1
+ <script setup lang="ts">
2
2
  import Card from '@shell/components/Resource/Detail/Card/index.vue';
3
- import ResourceRow, { Props as ResourceRowProps } from '@shell/components/Resource/Detail/ResourceRow.vue';
3
+ import ResourceRow from '@shell/components/Resource/Detail/ResourceRow.vue';
4
+ import { Props } from './types';
4
5
 
5
- export interface Props {
6
- title: string;
7
- rows: ResourceRowProps[];
8
- }
9
- </script>
10
-
11
- <script setup lang="ts">
12
6
  const { title, rows } = defineProps<Props>();
13
7
  </script>
14
8
 
@@ -0,0 +1,6 @@
1
+ import { Props as ResourceRowProps } from '@shell/components/Resource/Detail/ResourceRow.types';
2
+
3
+ export interface Props {
4
+ title: string;
5
+ rows: ResourceRowProps[];
6
+ }
@@ -10,7 +10,8 @@ import { computed } from 'vue';
10
10
  import { useStore } from 'vuex';
11
11
 
12
12
  export interface Props {
13
- pods?: any[];
13
+ title: string;
14
+ resources?: any[];
14
15
  showScaling?: boolean;
15
16
  }
16
17
  </script>
@@ -19,7 +20,7 @@ export interface Props {
19
20
  const store = useStore();
20
21
  const i18n = useI18n(store);
21
22
 
22
- const props = withDefaults(defineProps<Props>(), { pods: undefined, showScaling: false });
23
+ const props = withDefaults(defineProps<Props>(), { resources: undefined, showScaling: false });
23
24
  const emit = defineEmits(['decrease', 'increase']);
24
25
 
25
26
  const segmentAccumulator = computed(() => {
@@ -28,8 +29,8 @@ const segmentAccumulator = computed(() => {
28
29
  }
29
30
  const accumulator: {[key in StateColor]?: Value} = {};
30
31
 
31
- props.pods?.forEach((pod: any) => {
32
- const color: StateColor = pod.stateSimpleColor;
32
+ props.resources?.forEach((resource: any) => {
33
+ const color: StateColor = resource.stateSimpleColor;
33
34
 
34
35
  accumulator[color] = accumulator[color] || { count: 0 };
35
36
  accumulator[color].count++;
@@ -45,10 +46,10 @@ const rowAccumulator = computed(() => {
45
46
  }
46
47
  const accumulator: {[key in string]: Value} = {};
47
48
 
48
- props.pods?.forEach((pod: any) => {
49
- accumulator[pod.stateDisplay] = accumulator[pod.stateDisplay] || { count: 0 };
50
- accumulator[pod.stateDisplay].count++;
51
- accumulator[pod.stateDisplay].color = pod.stateSimpleColor.replace('text-', '') as StateColor;
49
+ props.resources?.forEach((resource: any) => {
50
+ accumulator[resource.stateDisplay] = accumulator[resource.stateDisplay] || { count: 0 };
51
+ accumulator[resource.stateDisplay].count++;
52
+ accumulator[resource.stateDisplay].color = resource.stateSimpleColor.replace('text-', '') as StateColor;
52
53
  });
53
54
 
54
55
  return accumulator;
@@ -58,7 +59,7 @@ const percent = (count: number, total: number) => {
58
59
  return count / total * 100;
59
60
  };
60
61
 
61
- const count = computed(() => props.pods?.length || 0);
62
+ const count = computed(() => props.resources?.length || 0);
62
63
 
63
64
  const segmentColors = computed(() => Object.keys(segmentAccumulator.value) as StateColor[]);
64
65
  const segments = computed(() => segmentColors.value.map((color: StateColor) => ({
@@ -82,7 +83,7 @@ const rows = computed(() => {
82
83
  </script>
83
84
 
84
85
  <template>
85
- <Card :title="i18n.t('component.resource.detail.card.podsCard.title')">
86
+ <Card :title="title">
86
87
  <template
87
88
  v-if="props.showScaling"
88
89
  #heading-action
@@ -1,28 +1,11 @@
1
1
  import { mount } from '@vue/test-utils';
2
- import Bubble from '@shell/components/Resource/Detail/Card/PodsCard/Bubble.vue';
3
- import PodsCard from '@shell/components/Resource/Detail/Card/PodsCard/index.vue';
2
+ import PodsCard from '@shell/components/Resource/Detail/Card/StatusCard/index.vue';
4
3
  import Card from '@shell/components/Resource/Detail/Card/index.vue';
5
4
  import Scaler from '@shell/components/Resource/Detail/Card/Scaler.vue';
6
5
  import StatusBar from '@shell/components/Resource/Detail/StatusBar.vue';
7
6
  import StatusRow from '@shell/components/Resource/Detail/StatusRow.vue';
8
7
  import { createStore } from 'vuex';
9
8
 
10
- describe('component: Bubble', () => {
11
- it('should render element with bubble class for styling', async() => {
12
- const content = 'content';
13
- const wrapper = mount(Bubble, { slots: { default: content } });
14
-
15
- expect(wrapper.element.className).toStrictEqual('bubble');
16
- });
17
-
18
- it('should render default slot content', async() => {
19
- const content = 'content';
20
- const wrapper = mount(Bubble, { slots: { default: content } });
21
-
22
- expect(wrapper.find('.bubble').element.innerHTML).toStrictEqual(content);
23
- });
24
- });
25
-
26
9
  describe('component: PodsCard', () => {
27
10
  const store = createStore({});
28
11
 
@@ -30,11 +13,12 @@ describe('component: PodsCard', () => {
30
13
  const podFail = { stateSimpleColor: 'error', stateDisplay: 'Error' };
31
14
 
32
15
  it('should pass title to Card prop correctly', async() => {
33
- const wrapper = mount(PodsCard, { props: { showScaling: true }, global: { provide: { store } } });
16
+ const title = 'component.resource.detail.card.podsCard.title';
17
+ const wrapper = mount(PodsCard, { props: { title, showScaling: true }, global: { provide: { store } } });
34
18
 
35
19
  const card = wrapper.findComponent(Card);
36
20
 
37
- expect(card.props('title')).toStrictEqual('component.resource.detail.card.podsCard.title');
21
+ expect(card.props('title')).toStrictEqual(title);
38
22
  });
39
23
 
40
24
  it('should show Scaler when showScaling is true', async() => {
@@ -50,15 +34,25 @@ describe('component: PodsCard', () => {
50
34
  });
51
35
 
52
36
  it('should pass the appropriate props to the Scaler component', async() => {
53
- const wrapper = mount(PodsCard, { props: { showScaling: true, pods: [podSuccess] }, global: { provide: { store } } });
37
+ const wrapper = mount(PodsCard, {
38
+ props: {
39
+ title: 'Test', showScaling: true, resources: [podSuccess]
40
+ },
41
+ global: { provide: { store } }
42
+ });
54
43
  const scaler = wrapper.findComponent(Scaler);
55
44
 
56
45
  expect(scaler.props('value')).toStrictEqual(1);
57
46
  expect(scaler.props('min')).toStrictEqual(0);
58
47
  });
59
48
 
60
- it('should pass the appropriate props to the StatusBar component based on the pods input', async() => {
61
- const wrapper = mount(PodsCard, { props: { showScaling: true, pods: [podSuccess, podFail] }, global: { provide: { store } } });
49
+ it('should pass the appropriate props to the StatusBar component based on the resources input', async() => {
50
+ const wrapper = mount(PodsCard, {
51
+ props: {
52
+ title: 'Test', showScaling: true, resources: [podSuccess, podFail]
53
+ },
54
+ global: { provide: { store } }
55
+ });
62
56
  const statusBar = wrapper.findComponent(StatusBar);
63
57
 
64
58
  const segments = statusBar.props('segments');
@@ -70,8 +64,13 @@ describe('component: PodsCard', () => {
70
64
  expect(segments[1].percent).toStrictEqual(50);
71
65
  });
72
66
 
73
- it('should pass the appropriate props to the StatusRow component based on the pods input', async() => {
74
- const wrapper = mount(PodsCard, { props: { showScaling: true, pods: [podSuccess, podFail] }, global: { provide: { store } } });
67
+ it('should pass the appropriate props to the StatusRow component based on the resources input', async() => {
68
+ const wrapper = mount(PodsCard, {
69
+ props: {
70
+ title: 'Test', showScaling: true, resources: [podSuccess, podFail]
71
+ },
72
+ global: { provide: { store } }
73
+ });
75
74
  const rows = wrapper.findComponent(StatusRow);
76
75
 
77
76
  expect(rows.props()).toStrictEqual({
@@ -0,0 +1,27 @@
1
+ <script lang="ts">
2
+ import SpacedRow from '@shell/components/Resource/Detail/SpacedRow.vue';
3
+ import ExtrasCard from '@shell/components/Resource/Detail/Card/ExtrasCard.vue';
4
+ import { computed } from 'vue';
5
+
6
+ export interface Props {
7
+ resource: any;
8
+ }
9
+ </script>
10
+
11
+ <script setup lang="ts">
12
+ const { resource } = defineProps<Props>();
13
+ const cards = computed(() => resource?.cards?.filter((c: any) => c) || []);
14
+ const showExtrasCard = computed(() => cards.value.length >= 1 && cards.value.length < 3);
15
+ </script>
16
+
17
+ <template>
18
+ <SpacedRow v-if="cards.length > 0">
19
+ <component
20
+ :is="card.component"
21
+ v-for="(card, i) in cards"
22
+ :key="i"
23
+ v-bind="card.props"
24
+ />
25
+ <ExtrasCard v-if="showExtrasCard" />
26
+ </SpacedRow>
27
+ </template>
@@ -0,0 +1,70 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import Masthead from '@shell/components/Resource/Detail/Masthead/index.vue';
3
+ import Cards from '@shell/components/Resource/Detail/Cards.vue';
4
+
5
+ jest.mock('@shell/utils/clipboard', () => ({ copyTextToClipboard: jest.fn() }));
6
+
7
+ describe('component: Masthead/index', () => {
8
+ const mockResource = {
9
+ name: 'test-resource',
10
+ cards: []
11
+ };
12
+
13
+ const defaultProps = {
14
+ titleBarProps: {
15
+ resource: mockResource,
16
+ resourceTypeLabel: 'ConfigMap',
17
+ resourceName: 'test-resource'
18
+ },
19
+ metadataProps: { items: [] }
20
+ };
21
+
22
+ it('should render the Cards component', () => {
23
+ const wrapper = mount(Masthead, {
24
+ props: defaultProps,
25
+ global: {
26
+ stubs: {
27
+ TitleBar: true,
28
+ Metadata: true,
29
+ Cards: true
30
+ }
31
+ }
32
+ });
33
+
34
+ expect(wrapper.findComponent(Cards).exists()).toBe(true);
35
+ });
36
+
37
+ it('should pass the resource from titleBarProps to Cards', () => {
38
+ const wrapper = mount(Masthead, {
39
+ props: defaultProps,
40
+ global: {
41
+ stubs: {
42
+ TitleBar: true,
43
+ Metadata: true,
44
+ Cards: true
45
+ }
46
+ }
47
+ });
48
+
49
+ const cardsComponent = wrapper.findComponent(Cards);
50
+
51
+ expect(cardsComponent.props('resource')).toStrictEqual(mockResource);
52
+ });
53
+
54
+ it('should render Cards with mb-20 class', () => {
55
+ const wrapper = mount(Masthead, {
56
+ props: defaultProps,
57
+ global: {
58
+ stubs: {
59
+ TitleBar: true,
60
+ Metadata: true,
61
+ Cards: true
62
+ }
63
+ }
64
+ });
65
+
66
+ const cardsComponent = wrapper.findComponent(Cards);
67
+
68
+ expect(cardsComponent.classes()).toContain('mb-20');
69
+ });
70
+ });