@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
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import TitleBar, { TitleBarProps } from '@shell/components/Resource/Detail/TitleBar/index.vue';
3
3
  import Metadata, { MetadataProps } from '@shell/components/Resource/Detail/Metadata/index.vue';
4
+ import Cards from '@shell/components/Resource/Detail/Cards.vue';
4
5
 
5
6
  export interface MastheadProps {
6
7
  titleBarProps: TitleBarProps;
@@ -25,6 +26,10 @@ const props = defineProps<MastheadProps>();
25
26
  <Metadata
26
27
  v-bind="props.metadataProps"
27
28
  />
29
+ <Cards
30
+ class="mb-20"
31
+ :resource="props.titleBarProps.resource"
32
+ />
28
33
  </div>
29
34
  </template>
30
35
 
@@ -43,7 +43,7 @@ const previewId = randomStr();
43
43
  >
44
44
  <RcButton
45
45
  ref="button"
46
- ghost
46
+ variant="ghost"
47
47
  aria-haspopup="dialog"
48
48
  :aria-expanded="showPreview"
49
49
  :aria-controls="previewId"
@@ -103,9 +103,11 @@ const previewId = randomStr();
103
103
  line-height: normal;
104
104
  }
105
105
 
106
- & .btn {
106
+ & .btn.btn-medium.rc-button.variant-ghost {
107
107
  line-height: initial;
108
108
  min-height: initial;
109
+
110
+ padding: 0;
109
111
  }
110
112
 
111
113
  &.active {
@@ -81,7 +81,7 @@ const getGlanceItemValueId = (glanceItem: any): string => `value-${ glanceItem.l
81
81
  }
82
82
  }
83
83
 
84
- .v-popper, .btn.role-link {
84
+ .v-popper, .btn.variant-link.rc-button {
85
85
  height: 24px;
86
86
  min-height: initial;
87
87
  padding: 0;
@@ -91,7 +91,7 @@ const getGlanceItemValueId = (glanceItem: any): string => `value-${ glanceItem.l
91
91
  padding: 0;
92
92
  }
93
93
 
94
- .btn.role-link {
94
+ .btn.variant-link.rc-button.variant-ghost {
95
95
  color: #141419;
96
96
  padding: 0 12px;
97
97
  i {
@@ -0,0 +1,14 @@
1
+ import { StateColor } from '@shell/utils/style';
2
+ import { RouteLocationRaw } from 'vue-router';
3
+
4
+ export interface Count {
5
+ label: string;
6
+ count: number;
7
+ }
8
+
9
+ export interface Props {
10
+ label: string;
11
+ to?: RouteLocationRaw;
12
+ color?: StateColor;
13
+ counts?: Count[];
14
+ }
@@ -1,41 +1,11 @@
1
- <script lang="ts">
1
+ <script setup lang="ts">
2
2
  import SubtleLink from '@shell/components/SubtleLink.vue';
3
3
  import StateDot from '@shell/components/StateDot/index.vue';
4
- import { StateColor } from '@shell/utils/style';
5
- import { sortBy, sumBy } from 'lodash';
6
- import { RouteLocationRaw } from 'vue-router';
4
+ import { sumBy } from 'lodash';
7
5
  import { computed } from 'vue';
8
6
  import { useI18n } from '@shell/composables/useI18n';
9
7
  import { useStore } from 'vuex';
10
-
11
- export interface Count {
12
- label: string;
13
- count: number;
14
- }
15
-
16
- export interface Props {
17
- label: string;
18
- to?: RouteLocationRaw;
19
- color?: StateColor;
20
- counts?: Count[];
21
- }
22
-
23
- export function extractCounts(labels: string[]): Count[] {
24
- const accumulator: { [k: string]: number} = {};
25
-
26
- labels.forEach((l: string) => {
27
- accumulator[l] = accumulator[l] || 0;
28
- accumulator[l]++;
29
- });
30
-
31
- const counts: Count[] = Object.entries(accumulator).map(([label, count]) => ({ label, count }));
32
-
33
- return sortBy(counts, 'label');
34
- }
35
-
36
- </script>
37
-
38
- <script setup lang="ts">
8
+ import { Props } from './ResourceRow.types';
39
9
  const {
40
10
  label, to, counts, color
41
11
  } = defineProps<Props>();
@@ -102,7 +72,7 @@ const displayCounts = computed(() => {
102
72
  :key="count.label"
103
73
  class="count"
104
74
  >
105
- {{ count.count }} {{ count.label }}<span class="and">&nbsp;+&nbsp;</span>
75
+ <span class="count-value">{{ count.count }}</span>&nbsp;<span class="count-label">{{ count.label }}</span><span class="and">&nbsp;+&nbsp;</span>
106
76
  </span>
107
77
  </div>
108
78
  </div>
@@ -118,13 +88,31 @@ const displayCounts = computed(() => {
118
88
  .right {
119
89
  flex-grow: 1;
120
90
  text-align: right;
91
+ overflow: hidden;
121
92
  }
122
93
 
123
94
  .counts {
124
- display: inline-flex;
95
+ display: flex;
125
96
  flex-direction: row;
126
97
  justify-content: flex-end;
127
98
  align-items: center;
99
+ max-width: 100%;
100
+ overflow: hidden;
101
+
102
+ .count {
103
+ display: flex;
104
+ justify-content: flex-end;
105
+ min-width: 0;
106
+ }
107
+
108
+ .count:not(.count + .count) {
109
+ max-width: calc(100% - 90px);
110
+
111
+ .count-label {
112
+ overflow: hidden;
113
+ text-overflow: ellipsis;
114
+ }
115
+ }
128
116
  }
129
117
 
130
118
  .count:last-of-type .and {
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import Bubble from '@shell/components/Resource/Detail/Card/PodsCard/Bubble.vue';
2
+ import { RcCounterBadge } from '@components/Pill';
3
3
  import { StateColor, stateColorCssVar } from '@shell/utils/style';
4
4
 
5
5
  export interface Props {
@@ -24,7 +24,10 @@ const {
24
24
  {{ label }}
25
25
  </div>
26
26
  <div class="count">
27
- <Bubble>{{ count }}</Bubble>
27
+ <RcCounterBadge
28
+ :count="count"
29
+ type="inactive"
30
+ />
28
31
  </div>
29
32
  <div class="percent text-muted">
30
33
  {{ percent.toFixed(1) }}%
@@ -16,13 +16,14 @@ jest.mock('vue-router', () => ({ useRoute: () => mockRoute }));
16
16
 
17
17
  describe('composables: TitleBar', () => {
18
18
  const resource = {
19
- nameDisplay: 'RESOURCE_NAME',
20
- namespace: 'RESOURCE_NAMESPACE',
21
- type: 'RESOURCE_TYPE',
22
- stateBackground: 'RESOURCE_STATE_BACKGROUND',
23
- stateDisplay: 'RESOURCE_STATE_DISPLAY',
24
- description: 'RESOURCE_DESCRIPTION',
25
- showConfiguration: jest.fn(),
19
+ nameDisplay: 'RESOURCE_NAME',
20
+ namespace: 'RESOURCE_NAMESPACE',
21
+ type: 'RESOURCE_TYPE',
22
+ stateBackground: 'RESOURCE_STATE_BACKGROUND',
23
+ stateDisplay: 'RESOURCE_STATE_DISPLAY',
24
+ description: 'RESOURCE_DESCRIPTION',
25
+ showConfiguration: jest.fn(),
26
+ detailPageAdditionalActions: undefined as any,
26
27
  };
27
28
  const labelFor = 'LABEL_FOR';
28
29
  const schema = { type: 'SCHEMA' };
@@ -54,4 +55,34 @@ describe('composables: TitleBar', () => {
54
55
  props.value.onShowConfiguration?.('callback');
55
56
  expect(resource.showConfiguration).toHaveBeenCalledTimes(1);
56
57
  });
58
+
59
+ it('should include additionalActions from resource.detailPageAdditionalActions', async() => {
60
+ const additionalActions = [
61
+ {
62
+ label: 'Action 1', variant: 'secondary', onClick: jest.fn()
63
+ }
64
+ ];
65
+
66
+ resource.detailPageAdditionalActions = additionalActions;
67
+
68
+ mockStore.getters['currentStore'].mockImplementation(() => 'cluster');
69
+ mockStore.getters['cluster/schemaFor'].mockImplementation(() => schema);
70
+ mockStore.getters['type-map/labelFor'].mockImplementation(() => labelFor);
71
+
72
+ const props = useDefaultTitleBarProps(resource, ref(undefined));
73
+
74
+ expect(props.value.additionalActions).toStrictEqual(additionalActions);
75
+ });
76
+
77
+ it('should have undefined additionalActions when resource does not define detailPageAdditionalActions', async() => {
78
+ resource.detailPageAdditionalActions = undefined;
79
+
80
+ mockStore.getters['currentStore'].mockImplementation(() => 'cluster');
81
+ mockStore.getters['cluster/schemaFor'].mockImplementation(() => schema);
82
+ mockStore.getters['type-map/labelFor'].mockImplementation(() => labelFor);
83
+
84
+ const props = useDefaultTitleBarProps(resource, ref(undefined));
85
+
86
+ expect(props.value.additionalActions).toBeUndefined();
87
+ });
57
88
  });
@@ -2,6 +2,7 @@ import { mount, RouterLinkStub } from '@vue/test-utils';
2
2
  import TitleBar from '@shell/components/Resource/Detail/TitleBar/index.vue';
3
3
  import ActionMenu from '@shell/components/ActionMenuShell.vue';
4
4
  import { createStore } from 'vuex';
5
+ import { defineComponent, h } from 'vue';
5
6
 
6
7
  describe('component: TitleBar/index', () => {
7
8
  const resourceTypeLabel = 'RESOURCE_TYPE_LABEL';
@@ -77,7 +78,7 @@ describe('component: TitleBar/index', () => {
77
78
  const button = wrapper.find('.top > .actions > .show-configuration');
78
79
  const buttonComponent = button.getComponent<any>('rc-button-stub');
79
80
 
80
- expect(buttonComponent.props('primary')).toStrictEqual(true);
81
+ expect(buttonComponent.props('variant')).toStrictEqual('primary');
81
82
  button.trigger('click');
82
83
 
83
84
  expect(wrapper.emitted()).toHaveProperty('show-configuration');
@@ -107,7 +108,7 @@ describe('component: TitleBar/index', () => {
107
108
  const actions = wrapper.find('.top > .actions');
108
109
  const actionMenuComponent = actions.getComponent<any>('action-menu-stub');
109
110
 
110
- expect(actionMenuComponent.props('buttonRole')).toStrictEqual('multiAction');
111
+ expect(actionMenuComponent.props('buttonVariant')).toStrictEqual('multiAction');
111
112
  expect(actionMenuComponent.props('resource')).toStrictEqual(actionMenuResource);
112
113
  });
113
114
 
@@ -137,4 +138,107 @@ describe('component: TitleBar/index', () => {
137
138
  expect(wrapper.find('.bottom.description').exists()).toBeTruthy();
138
139
  expect(wrapper.find('.bottom.description').element.innerHTML).toStrictEqual(description);
139
140
  });
141
+
142
+ describe('additionalActions', () => {
143
+ it('should not render additional action buttons when additionalActions is not provided', async() => {
144
+ const wrapper = mount(TitleBar, {
145
+ props: { resourceTypeLabel, resourceName },
146
+ global: { stubs: { 'router-link': RouterLinkStub, RcButton: true }, provide: { store } }
147
+ });
148
+
149
+ const actionButtons = wrapper.findAll('.top > .actions > rc-button-stub');
150
+
151
+ expect(actionButtons).toHaveLength(0);
152
+ });
153
+
154
+ it('should render buttons when additionalActions is an array of button props', async() => {
155
+ const onClick1 = jest.fn();
156
+ const onClick2 = jest.fn();
157
+ const additionalActions = [
158
+ {
159
+ label: 'Action 1', variant: 'secondary', onClick: onClick1
160
+ },
161
+ {
162
+ label: 'Action 2', variant: 'primary', size: 'large', onClick: onClick2
163
+ }
164
+ ];
165
+
166
+ const wrapper = mount(TitleBar, {
167
+ props: {
168
+ resourceTypeLabel, resourceName, additionalActions
169
+ },
170
+ global: { stubs: { 'router-link': RouterLinkStub, RcButton: true }, provide: { store } }
171
+ });
172
+
173
+ const actionButtons = wrapper.findAll('.top > .actions > rc-button-stub');
174
+
175
+ expect(actionButtons).toHaveLength(2);
176
+
177
+ const button1 = actionButtons[0].getComponent<any>('rc-button-stub');
178
+ const button2 = actionButtons[1].getComponent<any>('rc-button-stub');
179
+
180
+ expect(button1.props('variant')).toStrictEqual('secondary');
181
+ expect(button2.props('variant')).toStrictEqual('primary');
182
+ expect(button2.props('size')).toStrictEqual('large');
183
+ });
184
+
185
+ it('should call onClick handler when additional action button is clicked', async() => {
186
+ const onClick = jest.fn();
187
+ const additionalActions = [
188
+ {
189
+ label: 'Action 1', variant: 'secondary', onClick
190
+ }
191
+ ];
192
+
193
+ const wrapper = mount(TitleBar, {
194
+ props: {
195
+ resourceTypeLabel, resourceName, additionalActions
196
+ },
197
+ global: { stubs: { 'router-link': RouterLinkStub, RcButton: true }, provide: { store } }
198
+ });
199
+
200
+ const actionButton = wrapper.find('.top > .actions > rc-button-stub');
201
+
202
+ await actionButton.trigger('click');
203
+
204
+ expect(onClick).toHaveBeenCalledTimes(1);
205
+ });
206
+
207
+ it('should render a custom component when additionalActions is a Vue component', async() => {
208
+ const CustomComponent = defineComponent({
209
+ name: 'CustomActions',
210
+ render: () => h('div', { class: 'custom-actions' }, 'Custom Actions')
211
+ });
212
+
213
+ const wrapper = mount(TitleBar, {
214
+ props: {
215
+ resourceTypeLabel, resourceName, additionalActions: CustomComponent
216
+ },
217
+ global: { stubs: { 'router-link': RouterLinkStub }, provide: { store } }
218
+ });
219
+
220
+ expect(wrapper.find('.custom-actions').exists()).toBeTruthy();
221
+ expect(wrapper.find('.custom-actions').text()).toBe('Custom Actions');
222
+ });
223
+
224
+ it('should use slot content when additional-actions slot is provided', async() => {
225
+ const additionalActions = [
226
+ {
227
+ label: 'Action 1', variant: 'secondary', onClick: jest.fn()
228
+ }
229
+ ];
230
+
231
+ const wrapper = mount(TitleBar, {
232
+ props: {
233
+ resourceTypeLabel, resourceName, additionalActions
234
+ },
235
+ slots: { 'additional-actions': '<button class="slot-button">Slot Button</button>' },
236
+ global: { stubs: { 'router-link': RouterLinkStub }, provide: { store } }
237
+ });
238
+
239
+ // Slot content should override the additionalActions prop
240
+ expect(wrapper.find('.slot-button').exists()).toBeTruthy();
241
+ expect(wrapper.find('.slot-button').text()).toBe('Slot Button');
242
+ });
243
+ });
140
244
  });
@@ -36,7 +36,8 @@ export const useDefaultTitleBarProps = (resource: any, resourceSubtype?: Ref<str
36
36
  color: resourceValue.stateBackground,
37
37
  label: resourceValue.stateDisplay
38
38
  },
39
- description: resourceValue.description,
39
+ description: resourceValue.description,
40
+ additionalActions: resourceValue.detailPageAdditionalActions,
40
41
  onShowConfiguration
41
42
  };
42
43
  });
@@ -8,16 +8,23 @@ import { useStore } from 'vuex';
8
8
  import { useI18n } from '@shell/composables/useI18n';
9
9
  import RcButton from '@components/RcButton/RcButton.vue';
10
10
  import TabTitle from '@shell/components/TabTitle';
11
- import { computed, ref, watch } from 'vue';
11
+ import { computed, ref, VueElement, watch } from 'vue';
12
12
  import { _CONFIG, AS } from '@shell/config/query-params';
13
13
  import { ExtensionPoint, PanelLocation } from '@shell/core/types';
14
14
  import ExtensionPanel from '@shell/components/ExtensionPanel.vue';
15
+ import { ButtonVariantNewProps, ButtonSizeNewProps } from '~/pkg/rancher-components/src/components/RcButton/types';
16
+ import { isArray } from 'lodash';
15
17
 
16
18
  export interface Badge {
17
19
  color: 'bg-success' | 'bg-error' | 'bg-warning' | 'bg-info';
18
20
  label: string;
19
21
  }
20
22
 
23
+ export interface AdditionalActionButton extends ButtonVariantNewProps, ButtonSizeNewProps {
24
+ label: string;
25
+ onClick: () => void;
26
+ }
27
+
21
28
  export interface TitleBarProps {
22
29
  resource: any;
23
30
  resourceTypeLabel: string;
@@ -27,6 +34,8 @@ export interface TitleBarProps {
27
34
  description?: string;
28
35
  badge?: Badge;
29
36
 
37
+ additionalActions?: VueElement | AdditionalActionButton[];
38
+
30
39
  // This should be replaced with a list of menu items we want to render.
31
40
  // I don't have the time right now to swap this out though.
32
41
  actionMenuResource?: any;
@@ -36,7 +45,7 @@ export interface TitleBarProps {
36
45
 
37
46
  <script setup lang="ts">
38
47
  const {
39
- resource, resourceTypeLabel, resourceTo, resourceName, description, badge, onShowConfiguration,
48
+ additionalActions, resource, resourceTypeLabel, resourceTo, resourceName, description, badge, onShowConfiguration,
40
49
  } = defineProps<TitleBarProps>();
41
50
 
42
51
  const store = useStore();
@@ -55,6 +64,8 @@ watch(
55
64
  router.push({ query: { [AS]: currentView.value } });
56
65
  }
57
66
  );
67
+
68
+ const showAdditionalActionButtons = computed(() => isArray(additionalActions));
58
69
  </script>
59
70
 
60
71
  <template>
@@ -89,12 +100,31 @@ watch(
89
100
  />
90
101
  </Title>
91
102
  <div class="actions">
92
- <slot name="additional-actions" />
103
+ <slot name="additional-actions">
104
+ <template v-if="additionalActions">
105
+ <template v-if="showAdditionalActionButtons">
106
+ <RcButton
107
+ v-for="(actionButtonProps, i) in (additionalActions as AdditionalActionButton[])"
108
+ :key="`action-button-${i}`"
109
+ :variant="actionButtonProps.variant"
110
+ :size="actionButtonProps.size"
111
+ @click="actionButtonProps.onClick"
112
+ >
113
+ {{ actionButtonProps.label }}
114
+ </RcButton>
115
+ </template>
116
+ <component
117
+ :is="additionalActions"
118
+ v-else
119
+ />
120
+ </template>
121
+ </slot>
93
122
  <RcButton
94
123
  v-if="onShowConfiguration"
95
124
  :data-testid="showConfigurationDataTestId"
96
125
  class="show-configuration"
97
- :primary="true"
126
+ variant="primary"
127
+ size="large"
98
128
  :aria-label="i18n.t('component.resource.detail.titleBar.ariaLabel.showConfiguration', { resource: resourceName })"
99
129
  @click="() => emit('show-configuration', showConfigurationReturnFocusSelector)"
100
130
  >
@@ -106,7 +136,7 @@ watch(
106
136
  </RcButton>
107
137
  <ActionMenu
108
138
  v-if="actionMenuResource"
109
- button-role="multiAction"
139
+ button-variant="multiAction"
110
140
  :resource="actionMenuResource"
111
141
  data-testid="masthead-action-menu"
112
142
  :button-aria-label="i18n.t('component.resource.detail.titleBar.ariaLabel.actionMenu', { resource: resourceName })"
@@ -143,7 +173,12 @@ watch(
143
173
  margin-right: 10px;
144
174
  }
145
175
 
146
- .show-configuration {
176
+ .actions {
177
+ display: flex;
178
+ align-items: center;
179
+ }
180
+
181
+ .show-configuration, &:deep() .actions button {
147
182
  margin-left: 16px;
148
183
  }
149
184
 
@@ -8,7 +8,7 @@ jest.mock('@shell/composables/useIsNewDetailPageEnabled');
8
8
  jest.mock('@shell/components/ResourceDetail/Masthead/latest.vue', () => ({
9
9
  name: 'Latest',
10
10
  template: `<div>Latest</div>`,
11
- props: ['value', 'resourceSubtype']
11
+ props: ['value', 'resourceSubtype', 'isCustomDetailOrEdit']
12
12
  }));
13
13
  jest.mock('@shell/components/ResourceDetail/Masthead/legacy.vue', () => ({
14
14
  name: 'Legacy',
@@ -67,4 +67,52 @@ describe('component: Masthead/index', () => {
67
67
 
68
68
  expect(component).toBeDefined();
69
69
  });
70
+
71
+ it('should pass isCustomDetailOrEdit as true when hasDetail is true', () => {
72
+ useIsNewDetailPageEnabledSpy.mockReturnValue(computed(() => true));
73
+ const props = {
74
+ value: { type: 'VALUE' },
75
+ mode: _VIEW,
76
+ hasDetail: true,
77
+ hasEdit: false
78
+ };
79
+
80
+ const wrapper = mount(Index, { props });
81
+
82
+ const component = wrapper.getComponent({ name: 'Latest' });
83
+
84
+ expect(component.props('isCustomDetailOrEdit')).toBe(true);
85
+ });
86
+
87
+ it('should pass isCustomDetailOrEdit as true when hasEdit is true', () => {
88
+ useIsNewDetailPageEnabledSpy.mockReturnValue(computed(() => true));
89
+ const props = {
90
+ value: { type: 'VALUE' },
91
+ mode: _VIEW,
92
+ hasDetail: false,
93
+ hasEdit: true
94
+ };
95
+
96
+ const wrapper = mount(Index, { props });
97
+
98
+ const component = wrapper.getComponent({ name: 'Latest' });
99
+
100
+ expect(component.props('isCustomDetailOrEdit')).toBe(true);
101
+ });
102
+
103
+ it('should pass isCustomDetailOrEdit as false when both hasDetail and hasEdit are false', () => {
104
+ useIsNewDetailPageEnabledSpy.mockReturnValue(computed(() => true));
105
+ const props = {
106
+ value: { type: 'VALUE' },
107
+ mode: _VIEW,
108
+ hasDetail: false,
109
+ hasEdit: false
110
+ };
111
+
112
+ const wrapper = mount(Index, { props });
113
+
114
+ const component = wrapper.getComponent({ name: 'Latest' });
115
+
116
+ expect(component.props('isCustomDetailOrEdit')).toBe(false);
117
+ });
70
118
  });
@@ -0,0 +1,85 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import Latest from '@shell/components/ResourceDetail/Masthead/latest.vue';
3
+
4
+ jest.mock('@shell/components/Resource/Detail/TitleBar/index.vue', () => ({
5
+ name: 'TitleBar',
6
+ template: `<div data-testid="title-bar">TitleBar</div>`
7
+ }));
8
+ jest.mock('@shell/components/Resource/Detail/TitleBar/composables', () => ({ useDefaultTitleBarProps: jest.fn(() => ({})) }));
9
+ jest.mock('@shell/components/Resource/Detail/Metadata/index.vue', () => ({
10
+ name: 'Metadata',
11
+ template: `<div data-testid="metadata">Metadata</div>`
12
+ }));
13
+ jest.mock('@shell/components/Resource/Detail/Metadata/composables', () => ({ useDefaultMetadataForLegacyPagesProps: jest.fn(() => ({})) }));
14
+ jest.mock('@shell/components/Resource/Detail/composables', () => ({ useResourceDetailBannerProps: jest.fn(() => null) }));
15
+ jest.mock('@shell/components/Resource/Detail/Cards.vue', () => ({
16
+ name: 'Cards',
17
+ template: `<div data-testid="cards">Cards</div>`,
18
+ props: ['resource']
19
+ }));
20
+ jest.mock('@components/Banner', () => ({
21
+ Banner: {
22
+ name: 'Banner',
23
+ template: `<div data-testid="banner">Banner</div>`
24
+ }
25
+ }));
26
+
27
+ const defaultMocks = {
28
+ directives: { 'ui-context': () => {} },
29
+ global: {
30
+ mocks: {
31
+ $store: {
32
+ getters: { 'i18n/t': jest.fn() },
33
+ dispatch: jest.fn()
34
+ }
35
+ }
36
+ }
37
+ };
38
+
39
+ describe('component: Masthead/latest', () => {
40
+ beforeEach(() => {
41
+ jest.clearAllMocks();
42
+ });
43
+
44
+ it('should render Cards when isCustomDetailOrEdit is true', () => {
45
+ const props = {
46
+ value: { name: 'test-resource' },
47
+ isCustomDetailOrEdit: true
48
+ };
49
+
50
+ const wrapper = mount(Latest, { props, ...defaultMocks });
51
+
52
+ expect(wrapper.find('[data-testid="cards"]').exists()).toBe(true);
53
+ });
54
+
55
+ it('should not render Cards when isCustomDetailOrEdit is false', () => {
56
+ const props = {
57
+ value: { name: 'test-resource' },
58
+ isCustomDetailOrEdit: false
59
+ };
60
+
61
+ const wrapper = mount(Latest, { props, ...defaultMocks });
62
+
63
+ expect(wrapper.find('[data-testid="cards"]').exists()).toBe(false);
64
+ });
65
+
66
+ it('should not render Cards when isCustomDetailOrEdit is not provided (defaults to false)', () => {
67
+ const props = { value: { name: 'test-resource' } };
68
+
69
+ const wrapper = mount(Latest, { props, ...defaultMocks });
70
+
71
+ expect(wrapper.find('[data-testid="cards"]').exists()).toBe(false);
72
+ });
73
+
74
+ it('should always render TitleBar and Metadata', () => {
75
+ const props = {
76
+ value: { name: 'test-resource' },
77
+ isCustomDetailOrEdit: false
78
+ };
79
+
80
+ const wrapper = mount(Latest, { props, ...defaultMocks });
81
+
82
+ expect(wrapper.find('[data-testid="title-bar"]').exists()).toBe(true);
83
+ expect(wrapper.find('[data-testid="metadata"]').exists()).toBe(true);
84
+ });
85
+ });
@@ -48,6 +48,7 @@ const showLatestMasthead = computed(() => isNewDetailPageEnabled.value && isView
48
48
  v-if="showLatestMasthead"
49
49
  :value="props.value"
50
50
  :resourceSubtype="props.resourceSubtype"
51
+ :isCustomDetailOrEdit="props.hasDetail || props.hasEdit"
51
52
  />
52
53
  <Legacy
53
54
  v-else
@@ -7,11 +7,13 @@ import Metadata from '@shell/components/Resource/Detail/Metadata/index.vue';
7
7
  import { useDefaultMetadataForLegacyPagesProps } from '@shell/components/Resource/Detail/Metadata/composables';
8
8
  import { useResourceDetailBannerProps } from '@shell/components/Resource/Detail/composables';
9
9
  import { computed } from 'vue';
10
+ import Cards from '@shell/components/Resource/Detail/Cards.vue';
10
11
 
11
12
  // We are disabling eslint for this script to allow the use of the Props interface
12
13
  export interface Props {
13
14
  value?: Object;
14
15
  resourceSubtype?: string;
16
+ isCustomDetailOrEdit?: boolean;
15
17
  }
16
18
 
17
19
  </script>
@@ -19,7 +21,7 @@ export interface Props {
19
21
  <script lang="ts" setup>
20
22
  import { useStore } from 'vuex';
21
23
 
22
- const props = withDefaults(defineProps<Props>(), { value: () => ({}), resourceSubtype: undefined });
24
+ const props = withDefaults(defineProps<Props>(), { value: () => ({}), resourceSubtype: undefined, isCustomDetailOrEdit: false });
23
25
 
24
26
  const uiCtxResource = computed(() => {
25
27
  const {
@@ -64,6 +66,11 @@ const store = useStore();
64
66
  v-bind="metadataProps"
65
67
  class="mmt-4"
66
68
  />
69
+ <Cards
70
+ v-if="props.isCustomDetailOrEdit"
71
+ class="mb-20"
72
+ :resource="props.value"
73
+ />
67
74
  </div>
68
75
  </template>
69
76