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

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 (177) hide show
  1. package/assets/brand/classic/metadata.json +3 -0
  2. package/assets/styles/app.scss +1 -0
  3. package/assets/styles/base/_color.scss +16 -0
  4. package/assets/styles/base/_helpers.scss +10 -0
  5. package/assets/styles/base/_variables.scss +1 -1
  6. package/assets/styles/fonts/_icons.scss +1 -32
  7. package/assets/styles/global/_layout.scss +1 -1
  8. package/assets/styles/themes/_dark.scss +262 -260
  9. package/assets/styles/themes/_light.scss +538 -515
  10. package/assets/styles/themes/_modern.scss +914 -0
  11. package/assets/translations/en-us.yaml +84 -25
  12. package/chart/__tests__/S3.test.ts +2 -1
  13. package/cloud-credential/generic.vue +18 -10
  14. package/cloud-credential/harvester.vue +1 -9
  15. package/components/AdvancedSection.vue +8 -0
  16. package/components/ChartReadme.vue +17 -7
  17. package/components/Drawer/ResourceDetailDrawer/__tests__/composables.test.ts +1 -26
  18. package/components/Drawer/ResourceDetailDrawer/composables.ts +0 -23
  19. package/components/Drawer/ResourceDetailDrawer/index.vue +17 -4
  20. package/components/InstallHelmCharts.vue +656 -0
  21. package/components/LazyImage.vue +60 -4
  22. package/components/LocaleSelector.vue +7 -2
  23. package/components/Markdown.vue +4 -0
  24. package/components/Resource/Detail/Masthead/composable.ts +16 -0
  25. package/components/Resource/Detail/Masthead/index.vue +37 -0
  26. package/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields.ts +5 -5
  27. package/components/Resource/Detail/Metadata/__tests__/composables.test.ts +10 -17
  28. package/components/Resource/Detail/Metadata/composables.ts +9 -7
  29. package/components/Resource/Detail/Metadata/index.vue +17 -2
  30. package/components/Resource/Detail/Page.vue +35 -21
  31. package/components/Resource/Detail/TitleBar/__tests__/composables.test.ts +8 -9
  32. package/components/Resource/Detail/TitleBar/composables.ts +2 -3
  33. package/components/Resource/Detail/TitleBar/index.vue +10 -1
  34. package/components/ResourceDetail/index.vue +569 -74
  35. package/components/SlideInPanelManager.vue +10 -3
  36. package/components/SortableTable/index.vue +4 -4
  37. package/components/Tabbed/index.vue +29 -3
  38. package/components/__tests__/LazyImage.spec.ts +121 -0
  39. package/components/fleet/FleetStatus.vue +4 -0
  40. package/components/form/ClusterAppearance.vue +5 -0
  41. package/components/form/Members/ClusterPermissionsEditor.vue +1 -1
  42. package/components/form/ProjectMemberEditor.vue +1 -1
  43. package/components/form/ResourceLabeledSelect.vue +19 -6
  44. package/components/form/ResourceTabs/index.vue +20 -0
  45. package/components/form/SecretSelector.vue +9 -0
  46. package/components/form/labeled-select-utils/labeled-select-pagination.ts +3 -38
  47. package/components/formatter/FleetApplicationSource.vue +25 -17
  48. package/components/nav/Favorite.vue +4 -0
  49. package/components/nav/NotificationCenter/Notification.vue +1 -27
  50. package/components/nav/WindowManager/index.vue +3 -3
  51. package/config/labels-annotations.js +1 -2
  52. package/detail/__tests__/provisioning.cattle.io.cluster.test.ts +11 -0
  53. package/detail/__tests__/workload.test.ts +164 -0
  54. package/detail/configmap.vue +33 -75
  55. package/detail/projectsecret.vue +11 -0
  56. package/detail/provisioning.cattle.io.cluster.vue +350 -324
  57. package/detail/secret.vue +49 -308
  58. package/detail/workload/index.vue +38 -21
  59. package/dialog/InstallExtensionDialog.vue +8 -5
  60. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +224 -0
  61. package/edit/fleet.cattle.io.gitrepo.vue +5 -6
  62. package/edit/fleet.cattle.io.helmop.vue +78 -56
  63. package/edit/logging.banzaicloud.io.output/index.vue +1 -1
  64. package/edit/logging.banzaicloud.io.output/providers/awsElasticsearch.vue +5 -6
  65. package/edit/networking.k8s.io.ingress/Certificate.vue +9 -11
  66. package/edit/networking.k8s.io.ingress/DefaultBackend.vue +8 -3
  67. package/edit/networking.k8s.io.ingress/Rule.vue +2 -5
  68. package/edit/networking.k8s.io.ingress/RulePath.vue +17 -11
  69. package/edit/networking.k8s.io.networkpolicy/PolicyRuleTarget.vue +11 -10
  70. package/edit/networking.k8s.io.networkpolicy/PolicyRules.vue +1 -3
  71. package/edit/networking.k8s.io.networkpolicy/index.vue +17 -17
  72. package/edit/provisioning.cattle.io.cluster/rke2.vue +21 -13
  73. package/edit/provisioning.cattle.io.cluster/tabs/AgentConfiguration.vue +9 -7
  74. package/edit/provisioning.cattle.io.cluster/tabs/DirectoryConfig.vue +10 -12
  75. package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +39 -38
  76. package/edit/provisioning.cattle.io.cluster/tabs/etcd/S3Config.vue +41 -19
  77. package/edit/provisioning.cattle.io.cluster/tabs/etcd/index.vue +16 -3
  78. package/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryConfigs.vue +30 -31
  79. package/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryMirrors.vue +9 -10
  80. package/edit/provisioning.cattle.io.cluster/tabs/registries/index.vue +1 -3
  81. package/edit/provisioning.cattle.io.cluster/tabs/upgrade/DrainOptions.vue +16 -9
  82. package/edit/workload/index.vue +5 -14
  83. package/list/provisioning.cattle.io.cluster.vue +1 -69
  84. package/machine-config/__tests__/vmwarevsphere.test.ts +5 -7
  85. package/machine-config/google.vue +9 -1
  86. package/machine-config/vmwarevsphere.vue +7 -17
  87. package/mixins/chart.js +0 -2
  88. package/mixins/resource-fetch-api-pagination.js +3 -4
  89. package/models/__tests__/chart.test.ts +111 -80
  90. package/models/__tests__/fleet.cattle.io.helmop.test.ts +224 -0
  91. package/models/__tests__/node.test.ts +7 -63
  92. package/models/catalog.cattle.io.app.js +1 -1
  93. package/models/catalog.cattle.io.operation.js +1 -1
  94. package/models/chart.js +36 -20
  95. package/models/cloudcredential.js +2 -163
  96. package/models/cluster/node.js +7 -7
  97. package/models/cluster.x-k8s.io.machine.js +3 -3
  98. package/models/compliance.cattle.io.clusterscan.js +2 -2
  99. package/models/configmap.js +4 -0
  100. package/models/constraints.gatekeeper.sh.constraint.js +1 -1
  101. package/models/fleet-application.js +0 -17
  102. package/models/fleet.cattle.io.gitrepo.js +15 -1
  103. package/models/fleet.cattle.io.helmop.js +26 -22
  104. package/models/management.cattle.io.setting.js +4 -0
  105. package/models/persistentvolumeclaim.js +1 -1
  106. package/models/pod.js +2 -2
  107. package/models/provisioning.cattle.io.cluster.js +16 -40
  108. package/models/rke.cattle.io.etcdsnapshot.js +1 -1
  109. package/models/secret.js +4 -0
  110. package/models/storage.k8s.io.storageclass.js +2 -2
  111. package/models/workload.js +3 -3
  112. package/package.json +11 -10
  113. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +1 -0
  114. package/pages/c/_cluster/apps/charts/AppChartCardSubHeader.vue +4 -1
  115. package/pages/c/_cluster/apps/charts/__tests__/AppChartCardFooter.spec.js +41 -0
  116. package/pages/c/_cluster/apps/charts/chart.vue +422 -174
  117. package/pages/c/_cluster/apps/charts/install.vue +1 -1
  118. package/pages/c/_cluster/explorer/projectsecret.vue +3 -13
  119. package/pages/c/_cluster/fleet/__tests__/index.test.ts +608 -314
  120. package/pages/c/_cluster/fleet/index.vue +103 -44
  121. package/pages/c/_cluster/manager/cloudCredential/index.vue +2 -59
  122. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +10 -3
  123. package/pages/c/_cluster/uiplugins/index.vue +36 -25
  124. package/plugins/dashboard-store/actions.js +42 -22
  125. package/plugins/dashboard-store/resource-class.js +31 -0
  126. package/plugins/steve/__tests__/getters.test.ts +1 -1
  127. package/plugins/steve/__tests__/subscribe.spec.ts +259 -1
  128. package/plugins/steve/getters.js +8 -2
  129. package/plugins/steve/resourceWatcher.js +10 -3
  130. package/plugins/steve/subscribe.js +192 -19
  131. package/plugins/steve/worker/web-worker.advanced.js +2 -0
  132. package/rancher-components/Card/Card.vue +0 -18
  133. package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.test.ts +15 -0
  134. package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +65 -0
  135. package/rancher-components/Pill/RcStatusBadge/index.ts +2 -0
  136. package/rancher-components/Pill/RcStatusBadge/types.ts +5 -0
  137. package/rancher-components/Pill/RcStatusIndicator/RcStatusIndicator.test.ts +33 -0
  138. package/rancher-components/Pill/RcStatusIndicator/RcStatusIndicator.vue +75 -0
  139. package/rancher-components/Pill/RcStatusIndicator/index.ts +2 -0
  140. package/rancher-components/Pill/RcStatusIndicator/types.ts +7 -0
  141. package/rancher-components/Pill/types.ts +2 -0
  142. package/rancher-components/RcButton/RcButton.vue +1 -1
  143. package/rancher-components/RcDropdown/RcDropdown.test.ts +98 -0
  144. package/rancher-components/RcDropdown/RcDropdown.vue +5 -0
  145. package/rancher-components/RcDropdown/RcDropdownItem.vue +7 -1
  146. package/rancher-components/RcDropdown/RcDropdownItemCheckbox.vue +2 -1
  147. package/rancher-components/RcDropdown/RcDropdownItemSelect.vue +2 -1
  148. package/rancher-components/RcDropdown/useDropdownContext.ts +21 -0
  149. package/rancher-components/RcDropdown/useDropdownItem.ts +30 -1
  150. package/rancher-components/RcItemCard/RcItemCard.test.ts +20 -0
  151. package/rancher-components/RcItemCard/RcItemCard.vue +40 -6
  152. package/store/__tests__/catalog.test.ts +93 -1
  153. package/store/aws.js +19 -8
  154. package/store/catalog.js +8 -3
  155. package/types/resources/settings.d.ts +1 -1
  156. package/types/shell/index.d.ts +28 -28
  157. package/types/uiplugins.ts +73 -0
  158. package/utils/__tests__/back-off.test.ts +354 -0
  159. package/utils/__tests__/kontainer.test.ts +19 -0
  160. package/utils/__tests__/uiplugins.test.ts +84 -0
  161. package/utils/back-off.ts +176 -0
  162. package/utils/dynamic-importer.js +8 -0
  163. package/utils/kontainer.ts +3 -5
  164. package/utils/style.ts +3 -0
  165. package/utils/uiplugins.ts +29 -2
  166. package/utils/validators/__tests__/setting.test.js +92 -0
  167. package/utils/validators/formRules/__tests__/index.test.ts +88 -7
  168. package/utils/validators/formRules/index.ts +83 -8
  169. package/utils/validators/setting.js +17 -0
  170. package/cloud-credential/__tests__/harvester.test.ts +0 -18
  171. package/components/ResourceDetail/__tests__/index.test.ts +0 -135
  172. package/components/ResourceDetail/legacy.vue +0 -562
  173. package/components/formatter/CloudCredExpired.vue +0 -69
  174. package/pages/explorer/resource/detail/configmap.vue +0 -42
  175. package/pages/explorer/resource/detail/projectsecret.vue +0 -9
  176. package/pages/explorer/resource/detail/secret.vue +0 -63
  177. package/utils/aws.js +0 -0
@@ -0,0 +1,65 @@
1
+ <script setup lang="ts">
2
+ import { RcStatusBadgeProps } from '@components/Pill/RcStatusBadge/types';
3
+
4
+ const props = defineProps<RcStatusBadgeProps>();
5
+ </script>
6
+
7
+ <template>
8
+ <div
9
+ class="rc-status-badge"
10
+ :class="{[props.status]: true}"
11
+ >
12
+ <slot name="default" />
13
+ </div>
14
+ </template>
15
+
16
+ <style lang="scss" scoped>
17
+ .rc-status-badge {
18
+ display: inline-flex;
19
+ align-items: center;
20
+ justify-content: center;
21
+ padding: 1px 7px;
22
+
23
+ border: 1px solid transparent;
24
+ border-radius: 30px;
25
+
26
+ font-family: Lato;
27
+ font-size: 12px;
28
+ line-height: 19px;
29
+
30
+ &.info {
31
+ background-color: var(--rc-info-secondary);
32
+ border-color: var(--rc-info-secondary);
33
+ color: var(--rc-info);
34
+ }
35
+
36
+ &.success {
37
+ background-color: var(--rc-success-secondary);
38
+ border-color: var(--rc-success-secondary);
39
+ color: var(--rc-success);
40
+ }
41
+
42
+ &.warning {
43
+ background-color: var(--rc-warning);
44
+ border-color: var(--rc-warning);
45
+ color: var(--rc-warning-secondary);
46
+ }
47
+
48
+ &.error {
49
+ background-color: var(--rc-error);
50
+ border-color: var(--rc-error);
51
+ color: var(--rc-error-secondary);
52
+ }
53
+
54
+ &.unknown {
55
+ background-color: var(--rc-unknown);
56
+ border-color: var(--rc-unknown);
57
+ color: var(--rc-unknown-secondary);
58
+ }
59
+
60
+ &.none {
61
+ border-color: var(--rc-none);
62
+ color: var(--rc-none-secondary);
63
+ }
64
+ }
65
+ </style>
@@ -0,0 +1,2 @@
1
+ export { default } from './RcStatusBadge.vue';
2
+ export type { Status } from '@components/Pill/types';
@@ -0,0 +1,5 @@
1
+ import { Status } from '@components/Pill/types';
2
+
3
+ export interface RcStatusBadgeProps {
4
+ status: Status;
5
+ }
@@ -0,0 +1,33 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import RcStatusIndicator, { Shape } from './index';
3
+ import { Status } from '@components/Pill/types';
4
+
5
+ describe('component: RcStatusIndicator', () => {
6
+ const shapes: Shape[] = ['disc', 'horizontal-bar', 'vertical-bar'];
7
+ const statuses: Status[] = ['info', 'success', 'warning', 'error', 'unknown', 'none'];
8
+
9
+ const combinations: {shape: Shape, status: Status}[] = [];
10
+
11
+ shapes.forEach((shape) => {
12
+ statuses.forEach((status) => {
13
+ combinations.push({
14
+ shape,
15
+ status
16
+ });
17
+ });
18
+ });
19
+
20
+ it.each(combinations)('should apply correct classes for shape "$shape" and status "$status"', ({ shape, status }) => {
21
+ const wrapper = mount(RcStatusIndicator, {
22
+ props: {
23
+ shape,
24
+ status,
25
+ }
26
+ });
27
+
28
+ const shapeEl = wrapper.find('.shape');
29
+
30
+ expect(shapeEl.classes()).toContain(shape);
31
+ expect(shapeEl.classes()).toContain(status);
32
+ });
33
+ });
@@ -0,0 +1,75 @@
1
+ <script setup lang="ts">
2
+ import { RcStatusIndicatorProps } from '@components/Pill/RcStatusIndicator/types';
3
+
4
+ const props = defineProps<RcStatusIndicatorProps>();
5
+ </script>
6
+
7
+ <template>
8
+ <div class="rc-status-indicator">
9
+ <div
10
+ class="shape"
11
+ :class="{[props.shape]: true, [props.status]: true}"
12
+ />
13
+ </div>
14
+ </template>
15
+
16
+ <style lang="scss" scoped>
17
+ .rc-status-indicator {
18
+ display: inline-flex;
19
+ align-items: center;
20
+ justify-content: center;
21
+ height: 21px;
22
+
23
+ .shape {
24
+ display: inline-block;
25
+ border: 1px solid transparent;
26
+
27
+ &.disc {
28
+ width: 6px;
29
+ height: 6px;
30
+ border-radius: 50%;
31
+ }
32
+
33
+ &.horizontal-bar {
34
+ width: 16px;
35
+ height: 4px;
36
+ border-radius: 2px;
37
+ }
38
+
39
+ &.vertical-bar {
40
+ width: 4px;
41
+ height: 16px;
42
+ border-radius: 2px;
43
+ }
44
+
45
+ &.info {
46
+ background-color: var(--rc-info);
47
+ border-color: var(--rc-info);
48
+ }
49
+
50
+ &.success {
51
+ background-color: var(--rc-success);
52
+ border-color: var(--rc-success);
53
+ }
54
+
55
+ &.warning {
56
+ background-color: var(--rc-warning);
57
+ border-color: var(--rc-warning);
58
+ }
59
+
60
+ &.error {
61
+ background-color: var(--rc-error);
62
+ border-color: var(--rc-error);
63
+ }
64
+
65
+ &.unknown {
66
+ background-color: var(--rc-unknown);
67
+ border-color: var(--rc-unknown);
68
+ }
69
+
70
+ &.none {
71
+ border-color: var(--rc-none);
72
+ }
73
+ }
74
+ }
75
+ </style>
@@ -0,0 +1,2 @@
1
+ export { default } from './RcStatusIndicator.vue';
2
+ export type { Shape } from './types';
@@ -0,0 +1,7 @@
1
+ import { Status } from '@components/Pill/types';
2
+ export type Shape = 'disc' | 'horizontal-bar' | 'vertical-bar';
3
+
4
+ export interface RcStatusIndicatorProps {
5
+ shape: Shape;
6
+ status: Status;
7
+ }
@@ -0,0 +1,2 @@
1
+ export type Shape = 'disc' | 'horizontal-bar' | 'vertical-bar';
2
+ export type Status = 'info' | 'success' | 'warning' | 'error' | 'unknown' | 'none';
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * <rc-button primary @click="doAction">Perform an Action</rc-button>
9
9
  */
10
- import { computed, ref, defineExpose } from 'vue';
10
+ import { computed, ref } from 'vue';
11
11
  import { ButtonRoleProps, ButtonSizeProps } from './types';
12
12
 
13
13
  const buttonRoles: { role: keyof ButtonRoleProps, className: string }[] = [
@@ -0,0 +1,98 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import { defineComponent } from 'vue';
3
+ import { RcDropdown } from '@components/RcDropdown';
4
+
5
+ const vDropdownMock = defineComponent({
6
+ template: `
7
+ <div class="popper">
8
+ <slot name="popper" />
9
+ </div>
10
+ `,
11
+ });
12
+
13
+ describe('component: RcDropdown.vue', () => {
14
+ it('should not change the height if the dropdown fits within the screen', async() => {
15
+ Object.defineProperty(window, 'innerHeight', { value: 800 });
16
+
17
+ const wrapper = mount(RcDropdown, { global: { components: { 'v-dropdown': vDropdownMock } } });
18
+
19
+ const dropdownTarget = wrapper.find('[dropdown-menu-collection]').element as HTMLElement;
20
+
21
+ Object.defineProperty(dropdownTarget, 'getBoundingClientRect', {
22
+ value: () => ({
23
+ top: 200,
24
+ bottom: 600,
25
+ height: 400,
26
+ }),
27
+ writable: true,
28
+ });
29
+
30
+ await wrapper.findComponent(vDropdownMock).vm.$emit('apply-show');
31
+ await wrapper.vm.$nextTick();
32
+
33
+ expect(dropdownTarget.style.height).toBe('');
34
+ });
35
+
36
+ it('should apply correct height if dropdown exceeds the top edge', async() => {
37
+ Object.defineProperty(window, 'innerHeight', { value: 800 });
38
+
39
+ const wrapper = mount(RcDropdown, { global: { components: { 'v-dropdown': vDropdownMock } } });
40
+
41
+ const dropdownTarget = wrapper.find('[dropdown-menu-collection]').element as HTMLElement;
42
+
43
+ Object.defineProperty(dropdownTarget, 'getBoundingClientRect', {
44
+ value: () => ({
45
+ top: 2, // Exceeds (top - padding)
46
+ bottom: 300,
47
+ height: 298,
48
+ }),
49
+ });
50
+
51
+ await wrapper.findComponent(vDropdownMock).vm.$emit('apply-show');
52
+ await wrapper.vm.$nextTick();
53
+
54
+ expect(dropdownTarget.style.height).toBe('268px');
55
+ });
56
+
57
+ it('should apply correct height if dropdown exceeds the bottom edge', async() => {
58
+ Object.defineProperty(window, 'innerHeight', { value: 925 });
59
+
60
+ const wrapper = mount(RcDropdown, { global: { components: { 'v-dropdown': vDropdownMock } } });
61
+
62
+ const dropdownTarget = wrapper.find('[dropdown-menu-collection]').element as HTMLElement;
63
+
64
+ Object.defineProperty(dropdownTarget, 'getBoundingClientRect', {
65
+ value: () => ({
66
+ top: 200,
67
+ bottom: 920, // Exceeds (bottom + padding)
68
+ height: 720,
69
+ }),
70
+ });
71
+
72
+ await wrapper.findComponent(vDropdownMock).vm.$emit('apply-show');
73
+ await wrapper.vm.$nextTick();
74
+
75
+ expect(dropdownTarget.style.height).toBe('693px');
76
+ });
77
+
78
+ it('should apply correct height if dropdown exceeds both top and bottom edges', async() => {
79
+ Object.defineProperty(window, 'innerHeight', { value: 400 });
80
+
81
+ const wrapper = mount(RcDropdown, { global: { components: { 'v-dropdown': vDropdownMock } } });
82
+
83
+ const dropdownTarget = wrapper.find('[dropdown-menu-collection]').element as HTMLElement;
84
+
85
+ Object.defineProperty(dropdownTarget, 'getBoundingClientRect', {
86
+ value: () => ({
87
+ top: -800, // Exceeds top
88
+ bottom: 800, // Exceeds bottom
89
+ height: 1600,
90
+ }),
91
+ });
92
+
93
+ await wrapper.findComponent(vDropdownMock).vm.$emit('apply-show');
94
+ await wrapper.vm.$nextTick();
95
+
96
+ expect(dropdownTarget.style.height).toBe('368px');
97
+ });
98
+ });
@@ -47,6 +47,7 @@ const {
47
47
  provideDropdownContext,
48
48
  registerDropdownCollection,
49
49
  handleKeydown,
50
+ setDropdownDimensions
50
51
  } = useDropdownContext(emit);
51
52
 
52
53
  provideDropdownContext();
@@ -57,6 +58,7 @@ const dropdownTarget = ref(null);
57
58
  useClickOutside(dropdownTarget, () => showMenu(false));
58
59
 
59
60
  const applyShow = () => {
61
+ setDropdownDimensions(dropdownTarget.value);
60
62
  registerDropdownCollection(dropdownTarget.value);
61
63
  setFocus('down');
62
64
  };
@@ -129,6 +131,9 @@ const applyShow = () => {
129
131
  }
130
132
 
131
133
  .dropdownTarget {
134
+ overflow: auto;
135
+ padding: 3px 0; // Need padding at top and bottom in order to show the focus border for the notification
136
+
132
137
  &:focus-visible, &:focus {
133
138
  outline: none;
134
139
  }
@@ -7,7 +7,12 @@ import { useDropdownItem } from '@components/RcDropdown/useDropdownItem';
7
7
  const props = defineProps({ disabled: Boolean });
8
8
  const emits = defineEmits(['click']);
9
9
 
10
- const { handleKeydown, close, handleActivate } = useDropdownItem();
10
+ const {
11
+ handleKeydown,
12
+ close,
13
+ handleActivate,
14
+ scrollIntoView,
15
+ } = useDropdownItem();
11
16
 
12
17
  const handleClick = (e: MouseEvent) => {
13
18
  if (props.disabled) {
@@ -31,6 +36,7 @@ const handleClick = (e: MouseEvent) => {
31
36
  @click.stop="handleClick"
32
37
  @keydown.enter.space="handleActivate"
33
38
  @keydown.up.down.prevent.stop="handleKeydown"
39
+ @focusin="scrollIntoView"
34
40
  >
35
41
  <slot name="before">
36
42
  <!--Empty slot content-->
@@ -8,7 +8,7 @@ import { useDropdownItem } from '@components/RcDropdown/useDropdownItem';
8
8
  const props = defineProps({ modelValue: Boolean, disabled: Boolean });
9
9
  const emits = defineEmits(['click']);
10
10
 
11
- const { handleKeydown, handleActivate } = useDropdownItem();
11
+ const { handleKeydown, handleActivate, scrollIntoView } = useDropdownItem();
12
12
 
13
13
  const handleClick = () => {
14
14
  if (props.disabled) {
@@ -30,6 +30,7 @@ const handleClick = () => {
30
30
  @click.stop="handleClick"
31
31
  @keydown.enter.space="handleActivate"
32
32
  @keydown.up.down.prevent.stop="handleKeydown"
33
+ @focusin="scrollIntoView"
33
34
  >
34
35
  <rc-checkbox :value="modelValue">
35
36
  <template #label>
@@ -25,7 +25,7 @@ defineProps({
25
25
  });
26
26
  const emits = defineEmits(['click', 'select']);
27
27
 
28
- const { handleKeydown, handleActivate } = useDropdownItem();
28
+ const { handleKeydown, handleActivate, scrollIntoView } = useDropdownItem();
29
29
 
30
30
  const dropdownMenuItem = ref<HTMLDivElement | null>(null);
31
31
  const menuItemSelect = ref<LabeledSelectComponent | null>(null);
@@ -50,6 +50,7 @@ const focusMenuItem = () => {
50
50
  @click.stop="handleClick"
51
51
  @keydown.enter.space="handleActivate"
52
52
  @keydown.up.down.prevent.stop="handleKeydown"
53
+ @focusin="scrollIntoView"
53
54
  >
54
55
  <LabeledSelect
55
56
  ref="menuItemSelect"
@@ -89,6 +89,26 @@ export const useDropdownContext = (emit: typeof rcDropdownEmits) => {
89
89
  });
90
90
  };
91
91
 
92
+ const setDropdownDimensions = (target: HTMLElement | null) => {
93
+ if (!target) {
94
+ return;
95
+ }
96
+
97
+ const { top, bottom } = target.getBoundingClientRect();
98
+ const padding = 32;
99
+
100
+ // The dropdown exceeds the top or bottom edge of the screen (or both).
101
+ if (top - padding < 0 || bottom + padding > window.innerHeight) {
102
+ const height = Math.min(
103
+ bottom,
104
+ window.innerHeight - top,
105
+ window.innerHeight
106
+ );
107
+
108
+ target.style.height = `${ height - padding }px`;
109
+ }
110
+ };
111
+
92
112
  /**
93
113
  * Provides Dropdown Context data and methods to descendants of RcDropdown.
94
114
  * Accessed in descendents with the `inject()` function.
@@ -115,5 +135,6 @@ export const useDropdownContext = (emit: typeof rcDropdownEmits) => {
115
135
  provideDropdownContext,
116
136
  registerDropdownCollection,
117
137
  handleKeydown,
138
+ setDropdownDimensions,
118
139
  };
119
140
  };
@@ -57,7 +57,36 @@ export const useDropdownItem = () => {
57
57
  }
58
58
  };
59
59
 
60
+ /**
61
+ * Scroll the item into view smoothly
62
+ * @param event FocusIn Event
63
+ */
64
+ const scrollIntoView = (event: Event) => {
65
+ const target = event.target;
66
+
67
+ if (!(target instanceof HTMLElement)) {
68
+ return;
69
+ }
70
+
71
+ const t = target as HTMLElement;
72
+
73
+ // If a button was clicked, then do not scroll into view, as this will scroll to make the button
74
+ // visible and the click will be ignored - so just return, so that the click works as expected
75
+ if (t.tagName === 'BUTTON') {
76
+ return;
77
+ }
78
+
79
+ target?.scrollIntoView({
80
+ behavior: 'smooth',
81
+ block: 'center',
82
+ inline: 'nearest',
83
+ });
84
+ };
85
+
60
86
  return {
61
- handleKeydown, close, handleActivate
87
+ handleKeydown,
88
+ close,
89
+ handleActivate,
90
+ scrollIntoView,
62
91
  };
63
92
  };
@@ -186,4 +186,24 @@ describe('rcItemCard', () => {
186
186
 
187
187
  expect(icon.attributes('style')).toContain('color: red');
188
188
  });
189
+
190
+ it('emits custom action events correctly', async() => {
191
+ const wrapper = mount(RcItemCard, {
192
+ props: {
193
+ ...baseProps,
194
+ actions: [
195
+ { action: 'myActionA', label: 'Edit' },
196
+ { action: 'myActionB', label: 'Delete' }
197
+ ]
198
+ }
199
+ });
200
+
201
+ const listeners = wrapper.vm.$.setupState.actionListeners;
202
+
203
+ listeners.myActionA('payload-1');
204
+ listeners.myActionB('payload-2');
205
+
206
+ expect(wrapper.emitted('myActionA')?.[0]).toStrictEqual(['payload-1']);
207
+ expect(wrapper.emitted('myActionB')?.[0]).toStrictEqual(['payload-2']);
208
+ });
189
209
  });
@@ -5,6 +5,7 @@ import { useI18n } from '@shell/composables/useI18n';
5
5
  import LazyImage from '@shell/components/LazyImage.vue';
6
6
  import { DropdownOption } from '@components/RcDropdown/types';
7
7
  import ActionMenu from '@shell/components/ActionMenuShell.vue';
8
+ import RcItemCardAction from './RcItemCardAction';
8
9
 
9
10
  const store = useStore();
10
11
  const { t } = useI18n(store);
@@ -79,7 +80,23 @@ interface RcItemCardProps {
79
80
  /** Optional image to show in card (position depends on variant). A slot is available for it too #item-card-image */
80
81
  image?: Image;
81
82
 
82
- /** Optional actions that will be displayed inside an action-menu */
83
+ /** Optional actions that will be displayed inside an action-menu
84
+ *
85
+ * Each action should include an `action` name, which is emitted as a custom event when selected.
86
+ * To respond to the event, you must also register a matching event listener using the `@` syntax.
87
+ *
88
+ * Example:
89
+ * <rc-item-card
90
+ * :actions="[
91
+ * {
92
+ * action: 'focusSearch',
93
+ * label: t('catalog.charts.search'),
94
+ * enabled: true
95
+ * }
96
+ * ]"
97
+ * @focusSearch="focusSearch"
98
+ * />
99
+ */
83
100
  actions?: DropdownOption[];
84
101
 
85
102
  /** Text content inside the card body. A slot is available for it too #item-card-content */
@@ -98,9 +115,25 @@ interface RcItemCardProps {
98
115
  const props = defineProps<RcItemCardProps>();
99
116
 
100
117
  /**
101
- * Emits 'card-click' when card is clicked or activated via keyboard.
118
+ * Emits:
119
+ * - 'card-click' when card is clicked or activated via keyboard.
120
+ * - custom events defined in the `actions` prop, but only if the corresponding event listener is explicitly declared on the component.
102
121
  */
103
- const emit = defineEmits<{( e: 'card-click', value: ItemValue): void; }>();
122
+ const emit = defineEmits<{(e: 'card-click', value: ItemValue): void; (e: string, payload: unknown): void;}>();
123
+
124
+ const actionListeners = computed(() => {
125
+ if (!props.actions) return {};
126
+
127
+ const listeners: Record<string, (payload: unknown) => void> = {};
128
+
129
+ for (const a of props.actions) {
130
+ if (a.action) {
131
+ listeners[a.action] = (payload: unknown) => emit(a.action, payload);
132
+ }
133
+ }
134
+
135
+ return listeners;
136
+ });
104
137
 
105
138
  /**
106
139
  * Handles the card click while avoiding nested interactive elements
@@ -245,12 +278,13 @@ const cardMeta = computed(() => ({
245
278
  </div>
246
279
  </template>
247
280
  <template v-else-if="actions">
248
- <div class="item-card-header-action-menu">
281
+ <rc-item-card-action class="item-card-header-action-menu">
249
282
  <ActionMenu
250
283
  data-testid="item-card-header-action-menu"
251
284
  :custom-actions="actions"
285
+ v-on="actionListeners"
252
286
  />
253
- </div>
287
+ </rc-item-card-action>
254
288
  </template>
255
289
  </div>
256
290
  </div>
@@ -370,7 +404,7 @@ $image-medium-box-width: 48px;
370
404
  }
371
405
 
372
406
  &-action-menu {
373
- margin-left: 12px;
407
+ margin-left: 8px;
374
408
  }
375
409
  }
376
410
 
@@ -1,5 +1,7 @@
1
1
  import { CATALOG } from '@shell/config/types';
2
- import { state, getters, actions, mutations } from '../catalog';
2
+ import {
3
+ state, getters, actions, mutations, filterAndArrangeCharts
4
+ } from '../catalog';
3
5
  import { createStore } from 'vuex';
4
6
 
5
7
  const clusterRepo = { _key: 'testClusterRepo' };
@@ -204,4 +206,94 @@ describe('catalog', () => {
204
206
  expect(store.state[catalogStoreName].versionInfos).toStrictEqual({ });
205
207
  });
206
208
  });
209
+
210
+ describe('filterAndArrangeCharts', () => {
211
+ const mockCharts = [
212
+ {
213
+ chartName: 'chart-a',
214
+ chartNameDisplay: 'Chart Alpha',
215
+ chartDescription: 'Description for Alpha',
216
+ keywords: ['keyword1', 'keyword2'],
217
+ versions: [{ annotations: {}, version: '1.0.0' }],
218
+ deprecated: false,
219
+ hidden: false,
220
+ repoKey: 'repo1',
221
+ chartType: 'app',
222
+ categories: [],
223
+ tags: [],
224
+ },
225
+ {
226
+ chartName: 'chart-b',
227
+ chartNameDisplay: 'Chart Beta',
228
+ chartDescription: 'Description for Beta',
229
+ keywords: ['keyword2', 'keyword3'],
230
+ versions: [{ annotations: {}, version: '1.0.0' }],
231
+ deprecated: false,
232
+ hidden: false,
233
+ repoKey: 'repo1',
234
+ chartType: 'app',
235
+ categories: [],
236
+ tags: [],
237
+ },
238
+ {
239
+ chartName: 'chart-c',
240
+ chartNameDisplay: 'Chart Gamma',
241
+ chartDescription: 'Description for Gamma',
242
+ keywords: ['keyword3', 'keyword4'],
243
+ versions: [{ annotations: {}, version: '1.0.0' }],
244
+ deprecated: false,
245
+ hidden: false,
246
+ repoKey: 'repo1',
247
+ chartType: 'app',
248
+ categories: [],
249
+ tags: [],
250
+ },
251
+ ];
252
+
253
+ it('should return all charts when no search query is provided', () => {
254
+ const result = filterAndArrangeCharts(mockCharts, {});
255
+
256
+ expect(result).toHaveLength(mockCharts.length);
257
+ });
258
+
259
+ it('should filter charts by name', () => {
260
+ const result = filterAndArrangeCharts(mockCharts, { searchQuery: 'Chart Alpha' });
261
+
262
+ expect(result).toHaveLength(1);
263
+ expect(result[0].chartNameDisplay).toBe('Chart Alpha');
264
+ });
265
+
266
+ it('should filter charts by description', () => {
267
+ const result = filterAndArrangeCharts(mockCharts, { searchQuery: 'Description for Beta' });
268
+
269
+ expect(result).toHaveLength(1);
270
+ expect(result[0].chartNameDisplay).toBe('Chart Beta');
271
+ });
272
+
273
+ it('should filter charts by keyword', () => {
274
+ const result = filterAndArrangeCharts(mockCharts, { searchQuery: 'keyword1' });
275
+
276
+ expect(result).toHaveLength(1);
277
+ expect(result[0].chartNameDisplay).toBe('Chart Alpha');
278
+ });
279
+
280
+ it('should handle multiple search tokens', () => {
281
+ const result = filterAndArrangeCharts(mockCharts, { searchQuery: 'Chart, keyword2' });
282
+
283
+ expect(result).toHaveLength(2);
284
+ });
285
+
286
+ it('should be case-insensitive', () => {
287
+ const result = filterAndArrangeCharts(mockCharts, { searchQuery: 'chart alpha' });
288
+
289
+ expect(result).toHaveLength(1);
290
+ expect(result[0].chartNameDisplay).toBe('Chart Alpha');
291
+ });
292
+
293
+ it('should return an empty array if no charts match', () => {
294
+ const result = filterAndArrangeCharts(mockCharts, { searchQuery: 'nonexistent' });
295
+
296
+ expect(result).toHaveLength(0);
297
+ });
298
+ });
207
299
  });