@rancher/shell 3.0.9-rc.1 → 3.0.9-rc.3

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 (208) hide show
  1. package/assets/styles/base/_color.scss +1 -0
  2. package/assets/styles/base/_typography.scss +14 -5
  3. package/assets/styles/themes/_light.scss +1 -1
  4. package/assets/styles/themes/_modern.scss +1 -1
  5. package/assets/translations/en-us.yaml +104 -33
  6. package/assets/translations/zh-hans.yaml +13 -2
  7. package/components/ActionMenu.vue +7 -8
  8. package/components/ActionMenuShell.vue +23 -24
  9. package/components/CodeMirror.vue +4 -3
  10. package/components/DetailText.vue +54 -7
  11. package/components/Drawer/Chrome.vue +11 -4
  12. package/components/Drawer/DrawerCard.vue +19 -0
  13. package/components/Drawer/ResourceDetailDrawer/ConfigTab.vue +3 -11
  14. package/components/Drawer/ResourceDetailDrawer/__tests__/ConfigTab.test.ts +2 -2
  15. package/components/Drawer/ResourceDetailDrawer/index.vue +3 -20
  16. package/components/Drawer/types.ts +1 -0
  17. package/components/DynamicContent/DynamicContentCloseButton.vue +2 -2
  18. package/components/LocaleSelector.vue +1 -1
  19. package/components/Markdown.vue +1 -1
  20. package/components/PopoverCard.vue +3 -3
  21. package/components/Resource/Detail/Card/ExtrasCard.vue +39 -0
  22. package/components/Resource/Detail/Card/Scaler.vue +10 -2
  23. package/components/Resource/Detail/Card/StateCard/__tests__/composables.test.ts +142 -0
  24. package/components/Resource/Detail/Card/StateCard/composables.ts +41 -11
  25. package/components/Resource/Detail/Card/StateCard/index.vue +3 -9
  26. package/components/Resource/Detail/Card/StateCard/types.ts +6 -0
  27. package/components/Resource/Detail/Card/{PodsCard → StatusCard}/index.vue +14 -10
  28. package/components/Resource/Detail/Card/__tests__/PodsCard.test.ts +24 -25
  29. package/components/Resource/Detail/Cards.vue +27 -0
  30. package/components/Resource/Detail/Masthead/__tests__/index.test.ts +70 -0
  31. package/components/Resource/Detail/Masthead/index.vue +5 -0
  32. package/components/Resource/Detail/Metadata/KeyValueRow.vue +4 -2
  33. package/components/Resource/Detail/ResourcePopover/ResourcePopoverCard.vue +2 -2
  34. package/components/Resource/Detail/ResourceRow.types.ts +14 -0
  35. package/components/Resource/Detail/ResourceRow.vue +23 -35
  36. package/components/Resource/Detail/StatusRow.vue +5 -2
  37. package/components/Resource/Detail/TitleBar/__tests__/composables.test.ts +38 -7
  38. package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +106 -2
  39. package/components/Resource/Detail/TitleBar/composables.ts +2 -1
  40. package/components/Resource/Detail/TitleBar/index.vue +41 -6
  41. package/components/ResourceDetail/Masthead/__tests__/index.test.ts +49 -1
  42. package/components/ResourceDetail/Masthead/__tests__/latest.test.ts +85 -0
  43. package/components/ResourceDetail/Masthead/index.vue +1 -0
  44. package/components/ResourceDetail/Masthead/latest.vue +8 -1
  45. package/components/ResourceDetail/Masthead/legacy.vue +1 -1
  46. package/components/ResourceTable.vue +1 -1
  47. package/components/Setting.vue +1 -1
  48. package/components/SortableTable/index.vue +25 -0
  49. package/components/SortableTable/selection.js +25 -12
  50. package/components/SortableTable/sorting.js +1 -1
  51. package/components/Tabbed/Tab.vue +5 -0
  52. package/components/Tabbed/index.vue +40 -9
  53. package/components/Window/ContainerShell.vue +10 -13
  54. package/components/__tests__/ProjectRow.test.ts +102 -15
  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/ResourceQuota/Project.vue +59 -8
  70. package/components/form/ResourceQuota/ProjectRow.vue +116 -21
  71. package/components/form/ResourceQuota/shared.js +42 -18
  72. package/components/form/SeccompProfile.vue +113 -0
  73. package/components/form/Security.vue +244 -133
  74. package/components/form/__tests__/LabeledSelect.test.ts +1 -1
  75. package/components/form/__tests__/SeccompProfile.test.js +124 -0
  76. package/components/form/__tests__/Security.test.ts +125 -37
  77. package/components/formatter/Autoscaler.vue +2 -2
  78. package/components/formatter/FleetSummaryGraph.vue +4 -1
  79. package/components/formatter/LinkName.vue +3 -2
  80. package/components/nav/Group.vue +5 -0
  81. package/components/nav/Header.vue +3 -3
  82. package/components/nav/HeaderPageActionMenu.vue +1 -1
  83. package/components/nav/NamespaceFilter.vue +6 -6
  84. package/components/nav/NotificationCenter/index.vue +1 -1
  85. package/components/nav/TopLevelMenu.helper.ts +41 -16
  86. package/components/nav/TopLevelMenu.vue +45 -25
  87. package/components/nav/WorkspaceSwitcher.vue +1 -1
  88. package/components/nav/__tests__/TopLevelMenu.helper.test.ts +277 -0
  89. package/components/nav/__tests__/TopLevelMenu.test.ts +160 -4
  90. package/components/templates/default.vue +0 -3
  91. package/components/templates/home.vue +0 -3
  92. package/components/templates/plain.vue +0 -3
  93. package/composables/useClickOutside.ts +1 -1
  94. package/config/product/explorer.js +2 -3
  95. package/config/table-headers.js +9 -7
  96. package/config/types.js +45 -9
  97. package/detail/__tests__/workload.test.ts +8 -16
  98. package/detail/catalog.cattle.io.app.vue +5 -0
  99. package/detail/fleet.cattle.io.cluster.vue +6 -0
  100. package/detail/management.cattle.io.oidcclient.vue +15 -4
  101. package/detail/workload/index.vue +7 -109
  102. package/edit/__tests__/management.cattle.io.project.test.js +137 -0
  103. package/edit/__tests__/projectsecret.test.ts +42 -0
  104. package/edit/auth/__tests__/oidc.test.ts +50 -0
  105. package/edit/auth/oidc.vue +68 -44
  106. package/edit/autoscaling.horizontalpodautoscaler/index.vue +140 -59
  107. package/edit/autoscaling.horizontalpodautoscaler/metrics-row.vue +41 -5
  108. package/edit/management.cattle.io.project.vue +36 -6
  109. package/edit/monitoring.coreos.com.alertmanagerconfig/index.vue +16 -3
  110. package/edit/projectsecret.vue +29 -0
  111. package/edit/provisioning.cattle.io.cluster/__tests__/Basics.test.ts +89 -200
  112. package/edit/provisioning.cattle.io.cluster/__tests__/Networking.test.ts +58 -17
  113. package/edit/provisioning.cattle.io.cluster/rke2.vue +11 -0
  114. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +3 -63
  115. package/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue +82 -14
  116. package/edit/workload/__tests__/index.test.ts +3 -4
  117. package/edit/workload/index.vue +47 -28
  118. package/edit/workload/mixins/workload.js +66 -31
  119. package/initialize/install-plugins.js +0 -2
  120. package/list/catalog.cattle.io.clusterrepo.vue +1 -1
  121. package/list/projectsecret.vue +2 -2
  122. package/machine-config/__tests__/vmwarevsphere.test.ts +64 -0
  123. package/machine-config/amazonec2.vue +2 -2
  124. package/machine-config/vmwarevsphere.vue +58 -4
  125. package/mixins/__tests__/chart.test.ts +63 -0
  126. package/mixins/chart.js +56 -51
  127. package/models/__tests__/catalog.cattle.io.app.test.ts +33 -0
  128. package/models/__tests__/workload.test.ts +333 -0
  129. package/models/catalog.cattle.io.app.js +8 -0
  130. package/models/management.cattle.io.cluster.js +22 -30
  131. package/models/pod.js +14 -0
  132. package/models/provisioning.cattle.io.cluster.js +2 -2
  133. package/models/secret.js +1 -1
  134. package/models/workload.js +93 -27
  135. package/package.json +4 -4
  136. package/pages/__tests__/diagnostic.test.ts +71 -0
  137. package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +91 -0
  138. package/pages/c/_cluster/apps/charts/install.vue +4 -4
  139. package/pages/c/_cluster/explorer/EventsTable.vue +2 -2
  140. package/pages/c/_cluster/explorer/tools/index.vue +23 -5
  141. package/pages/c/_cluster/fleet/index.vue +14 -8
  142. package/pages/c/_cluster/manager/hostedprovider/index.vue +1 -19
  143. package/pages/c/_cluster/monitoring/alertmanagerconfig/_alertmanagerconfigid/receiver.vue +18 -5
  144. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +1 -1
  145. package/pages/c/_cluster/uiplugins/index.vue +41 -9
  146. package/pages/diagnostic.vue +17 -3
  147. package/plugins/dashboard-store/__tests__/resource-class.test.ts +234 -0
  148. package/plugins/dashboard-store/actions.js +9 -8
  149. package/plugins/dashboard-store/resource-class.js +97 -1
  150. package/plugins/steve/__tests__/revision.test.ts +84 -0
  151. package/plugins/steve/__tests__/steve-pagination-utils.test.ts +30 -0
  152. package/plugins/steve/__tests__/subscribe.spec.ts +134 -0
  153. package/plugins/steve/revision.ts +26 -0
  154. package/plugins/steve/steve-pagination-utils.ts +6 -5
  155. package/plugins/steve/subscribe.js +188 -49
  156. package/plugins/subscribe-events.ts +2 -2
  157. package/rancher-components/Form/Checkbox/Checkbox.vue +13 -0
  158. package/rancher-components/LabeledTooltip/LabeledTooltip.vue +1 -1
  159. package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.vue +2 -1
  160. package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +3 -1
  161. package/rancher-components/Pill/RcStatusIndicator/RcStatusIndicator.vue +3 -1
  162. package/rancher-components/Pill/RcTag/RcTag.vue +1 -1
  163. package/rancher-components/Pill/index.ts +4 -0
  164. package/rancher-components/RcButton/RcButton.test.ts +53 -9
  165. package/rancher-components/RcButton/RcButton.vue +217 -25
  166. package/rancher-components/RcButton/types.ts +27 -1
  167. package/rancher-components/RcDropdown/RcDropdownMenu.vue +4 -4
  168. package/rancher-components/RcDropdown/types.ts +3 -3
  169. package/rancher-components/RcIcon/RcIcon.test.ts +42 -0
  170. package/rancher-components/RcIcon/RcIcon.vue +9 -6
  171. package/rancher-components/RcIcon/types.ts +13 -9
  172. package/rancher-components/RcItemCard/RcItemCard.test.ts +16 -6
  173. package/rancher-components/RcItemCard/RcItemCard.vue +13 -23
  174. package/rancher-components/utils/status.test.ts +10 -15
  175. package/rancher-components/utils/status.ts +5 -6
  176. package/store/__tests__/auth.test.ts +21 -5
  177. package/store/auth.js +6 -3
  178. package/store/aws.js +18 -12
  179. package/store/index.js +4 -8
  180. package/store/type-map.utils.ts +1 -1
  181. package/types/kube/kube-api.ts +29 -3
  182. package/types/rancher/steve.api.ts +40 -0
  183. package/types/shell/index.d.ts +262 -156
  184. package/types/store/pagination.types.ts +1 -0
  185. package/types/store/subscribe-events.types.ts +1 -0
  186. package/utils/__tests__/azure.test.ts +56 -0
  187. package/utils/__tests__/back-off.test.ts +364 -245
  188. package/utils/__tests__/error.test.ts +44 -0
  189. package/utils/__tests__/fleet.test.ts +8 -1
  190. package/utils/__tests__/pagination-wrapper.test.ts +167 -0
  191. package/utils/__tests__/version.test.ts +55 -1
  192. package/utils/azure.js +12 -0
  193. package/utils/back-off.ts +302 -69
  194. package/utils/dynamic-content/__tests__/index.test.ts +1 -1
  195. package/utils/dynamic-content/__tests__/new-release.test.ts +48 -7
  196. package/utils/dynamic-content/__tests__/support-notice.test.ts +1 -4
  197. package/utils/dynamic-content/index.ts +1 -6
  198. package/utils/dynamic-content/new-release.ts +5 -3
  199. package/utils/dynamic-content/types.d.ts +0 -1
  200. package/utils/error.js +9 -0
  201. package/utils/fleet.ts +2 -2
  202. package/utils/inactivity.ts +2 -3
  203. package/utils/pagination-wrapper.ts +99 -15
  204. package/utils/validators/formRules/index.ts +3 -0
  205. package/utils/version.js +38 -0
  206. package/components/auth/AzureWarning.vue +0 -77
  207. /package/components/Resource/Detail/{Card/PodsCard/Bubble.vue → Bubble.vue} +0 -0
  208. /package/components/Resource/Detail/Card/{PodsCard → StatusCard}/composable.ts +0 -0
@@ -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
+ });
@@ -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
  });