@rancher/shell 3.0.2-rc.5 → 3.0.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 (219) hide show
  1. package/assets/images/providers/nutanix.svg +12 -1
  2. package/assets/styles/base/_basic.scss +2 -1
  3. package/assets/styles/base/_helpers.scss +4 -0
  4. package/assets/styles/base/_variables.scss +2 -0
  5. package/assets/styles/global/_labeled-input.scss +5 -13
  6. package/assets/styles/global/_layout.scss +4 -1
  7. package/assets/styles/global/_select.scss +5 -0
  8. package/assets/styles/themes/_dark.scss +1 -3
  9. package/assets/styles/themes/_light.scss +5 -1
  10. package/assets/translations/en-us.yaml +130 -23
  11. package/assets/translations/zh-hans.yaml +0 -3
  12. package/cloud-credential/azure.vue +1 -1
  13. package/components/ActionMenuShell.vue +105 -0
  14. package/components/AppModal.vue +2 -2
  15. package/components/AsyncButton.vue +2 -0
  16. package/components/ButtonGroup.vue +9 -2
  17. package/components/ClusterBadge.vue +1 -0
  18. package/components/ClusterIconMenu.vue +3 -0
  19. package/components/ClusterProviderIcon.vue +14 -1
  20. package/components/CodeMirror.vue +96 -5
  21. package/components/Collapse.vue +16 -3
  22. package/components/CruResource.vue +9 -0
  23. package/components/CruResourceFooter.vue +1 -1
  24. package/components/ExplorerMembers.vue +2 -1
  25. package/components/FixedBanner.vue +19 -12
  26. package/components/Import.vue +14 -1
  27. package/components/LandingPagePreference.vue +4 -2
  28. package/components/PodSecurityAdmission.vue +8 -6
  29. package/components/PromptChangePassword.vue +1 -0
  30. package/components/PromptRemove.vue +23 -21
  31. package/components/ResourceDetail/Masthead.vue +30 -11
  32. package/components/ResourceDetail/__tests__/Masthead.test.ts +61 -0
  33. package/components/ResourceDetail/index.vue +6 -0
  34. package/components/ResourceTable.vue +6 -1
  35. package/components/ResourceYaml.vue +1 -0
  36. package/components/Setting.vue +115 -0
  37. package/components/SortableTable/THead.vue +2 -0
  38. package/components/SortableTable/index.vue +7 -12
  39. package/components/StatusBadge.vue +71 -0
  40. package/components/Tabbed/index.vue +16 -15
  41. package/components/Wizard.vue +108 -104
  42. package/components/YamlEditor.vue +12 -2
  43. package/components/__tests__/Collapse.test.ts +2 -2
  44. package/components/__tests__/FixedBanner.test.ts +3 -3
  45. package/components/auth/Principal.vue +29 -17
  46. package/components/auth/__tests__/Principal.test.ts +40 -0
  47. package/components/auth/login/ldap.vue +7 -0
  48. package/components/fleet/FleetBundles.vue +1 -1
  49. package/components/fleet/FleetRepos.vue +1 -1
  50. package/components/fleet/FleetResources.vue +0 -2
  51. package/components/fleet/FleetSummary.vue +60 -65
  52. package/components/fleet/ForceDirectedTreeChart/index.vue +5 -1
  53. package/components/fleet/__tests__/FleetSummary.test.ts +49 -9
  54. package/components/form/ArrayList.vue +6 -2
  55. package/components/form/ColorInput.vue +1 -0
  56. package/components/form/KeyValue.vue +11 -12
  57. package/components/form/LabeledSelect.vue +15 -3
  58. package/components/form/Labels.vue +8 -1
  59. package/components/form/Members/MembershipEditor.vue +230 -222
  60. package/components/form/Members/__tests__/MembershipEditor.test.ts +62 -0
  61. package/components/form/Password.vue +3 -0
  62. package/components/form/ProjectMemberEditor.vue +6 -3
  63. package/components/form/ResourceTabs/index.vue +15 -13
  64. package/components/form/SSHKnownHosts/KnownHostsEditDialog.vue +5 -4
  65. package/components/form/SchedulingCustomization.vue +85 -0
  66. package/components/form/Select.vue +3 -2
  67. package/components/form/SelectOrCreateAuthSecret.vue +2 -1
  68. package/components/form/UnitInput.vue +3 -4
  69. package/components/form/__tests__/ArrayList.test.ts +9 -6
  70. package/components/form/__tests__/LabeledSelect.test.ts +37 -0
  71. package/components/form/__tests__/SelectOrCreateAuthSecret.test.ts +34 -0
  72. package/components/form/__tests__/UnitInput.test.ts +4 -5
  73. package/components/formatter/LiveDate.vue +3 -1
  74. package/components/formatter/ServiceType.vue +12 -4
  75. package/components/formatter/WorkloadHealthScale.vue +2 -1
  76. package/components/nav/Header.vue +35 -2
  77. package/components/nav/HeaderPageActionMenu.vue +11 -40
  78. package/components/nav/Jump.vue +8 -2
  79. package/components/nav/NamespaceFilter.vue +5 -4
  80. package/components/nav/Pinned.vue +1 -1
  81. package/components/nav/TopLevelMenu.helper.ts +5 -5
  82. package/components/nav/TopLevelMenu.vue +1 -12
  83. package/components/nav/WindowManager/ContainerLogs.vue +96 -58
  84. package/components/nav/WindowManager/ContainerShell.vue +99 -18
  85. package/components/nav/WindowManager/index.vue +74 -6
  86. package/components/nav/__tests__/TopLevelMenu.test.ts +0 -40
  87. package/components/templates/default.vue +2 -47
  88. package/config/features.js +1 -0
  89. package/config/labels-annotations.js +11 -1
  90. package/config/router/navigation-guards/index.js +2 -1
  91. package/config/router/navigation-guards/record-last-route.js +24 -0
  92. package/config/settings.ts +66 -98
  93. package/config/version.js +1 -1
  94. package/core/types-provisioning.ts +7 -0
  95. package/detail/fleet.cattle.io.bundle.vue +7 -0
  96. package/detail/fleet.cattle.io.cluster.vue +0 -3
  97. package/detail/fleet.cattle.io.gitrepo.vue +8 -15
  98. package/detail/provisioning.cattle.io.cluster.vue +8 -2
  99. package/dialog/DeactivateDriverDialog.vue +5 -5
  100. package/dialog/GitRepoForceUpdateDialog.vue +132 -0
  101. package/directives/strip-html-aria-label.js +19 -0
  102. package/edit/__tests__/cis.cattle.io.clusterscan.test.ts +87 -0
  103. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +217 -37
  104. package/edit/auth/__tests__/oidc.test.ts +60 -12
  105. package/edit/auth/ldap/__tests__/config.test.ts +40 -0
  106. package/edit/auth/ldap/config.vue +67 -89
  107. package/edit/auth/oidc.vue +16 -2
  108. package/edit/catalog.cattle.io.clusterrepo.vue +12 -8
  109. package/edit/cis.cattle.io.clusterscan.vue +13 -1
  110. package/edit/fleet.cattle.io.gitrepo.vue +198 -72
  111. package/edit/logging-flow/Match.vue +0 -21
  112. package/edit/management.cattle.io.project.vue +1 -1
  113. package/edit/monitoring.coreos.com.prometheusrule/AlertingRule.vue +10 -3
  114. package/edit/monitoring.coreos.com.prometheusrule/RecordingRule.vue +5 -1
  115. package/edit/monitoring.coreos.com.prometheusrule/index.vue +5 -2
  116. package/edit/provisioning.cattle.io.cluster/CustomCommand.vue +8 -1
  117. package/edit/provisioning.cattle.io.cluster/SelectCredential.vue +2 -0
  118. package/edit/provisioning.cattle.io.cluster/__tests__/Advanced.test.ts +0 -2
  119. package/edit/provisioning.cattle.io.cluster/__tests__/CustomCommand.test.ts +55 -15
  120. package/edit/provisioning.cattle.io.cluster/index.vue +28 -30
  121. package/edit/provisioning.cattle.io.cluster/rke2.vue +64 -13
  122. package/edit/provisioning.cattle.io.cluster/tabs/AgentConfiguration.vue +37 -2
  123. package/edit/provisioning.cattle.io.cluster/tabs/etcd/index.vue +3 -2
  124. package/edit/resources.cattle.io.backup.vue +150 -15
  125. package/edit/secret/__tests__/ssh.test.ts +79 -0
  126. package/edit/secret/ssh.vue +7 -1
  127. package/edit/service.vue +0 -3
  128. package/edit/workload/Job.vue +8 -8
  129. package/edit/workload/__tests__/Job.test.ts +0 -1
  130. package/edit/workload/index.vue +3 -1
  131. package/initialize/install-directives.js +2 -0
  132. package/initialize/install-plugins.js +6 -1
  133. package/list/catalog.cattle.io.app.vue +21 -4
  134. package/list/fleet.cattle.io.bundle.vue +1 -1
  135. package/list/management.cattle.io.setting.vue +34 -132
  136. package/list/provisioning.cattle.io.cluster.vue +11 -3
  137. package/machine-config/vmwarevsphere.vue +15 -8
  138. package/mixins/__tests__/auth-config.test.ts +74 -0
  139. package/mixins/__tests__/chart.test.ts +5 -4
  140. package/mixins/__tests__/create-edit-view.test.ts +38 -0
  141. package/mixins/auth-config.js +8 -0
  142. package/mixins/chart.js +2 -2
  143. package/mixins/create-edit-view/impl.js +4 -1
  144. package/mixins/vue-select-overrides.js +10 -0
  145. package/models/__tests__/catalog.cattle.io.app.test.ts +148 -0
  146. package/models/__tests__/fleet.cattle.io.gitrepo.test.ts +157 -0
  147. package/models/__tests__/secret.test.ts +56 -13
  148. package/models/catalog.cattle.io.app.js +112 -37
  149. package/models/cluster.js +11 -0
  150. package/models/fleet.cattle.io.bundle.js +40 -2
  151. package/models/fleet.cattle.io.gitrepo.js +169 -109
  152. package/models/management.cattle.io.fleetworkspace.js +4 -0
  153. package/models/management.cattle.io.kontainerdriver.js +7 -0
  154. package/models/nodedriver.js +4 -1
  155. package/models/provisioning.cattle.io.cluster.js +24 -0
  156. package/models/secret.js +1 -1
  157. package/package.json +5 -5
  158. package/pages/auth/login.vue +5 -11
  159. package/pages/auth/verify.vue +11 -1
  160. package/pages/c/_cluster/apps/charts/index.vue +6 -4
  161. package/pages/c/_cluster/apps/charts/install.vue +1 -1
  162. package/pages/c/_cluster/explorer/ConfigBadge.vue +3 -5
  163. package/pages/c/_cluster/explorer/EventsTable.vue +3 -2
  164. package/pages/c/_cluster/explorer/__tests__/index.test.ts +9 -9
  165. package/pages/c/_cluster/explorer/index.vue +33 -35
  166. package/pages/c/_cluster/explorer/tools/index.vue +3 -3
  167. package/pages/c/_cluster/fleet/index.vue +0 -5
  168. package/pages/c/_cluster/legacy/project/index.vue +1 -1
  169. package/pages/c/_cluster/settings/performance.vue +52 -53
  170. package/pages/c/_cluster/uiplugins/index.vue +19 -22
  171. package/pages/home.vue +17 -12
  172. package/pages/prefs.vue +5 -1
  173. package/plugins/shortkey.js +10 -1
  174. package/plugins/steve/steve-pagination-utils.ts +58 -8
  175. package/promptRemove/management.cattle.io.fleetworkspace.vue +98 -0
  176. package/promptRemove/management.cattle.io.globalrole.vue +1 -1
  177. package/promptRemove/management.cattle.io.project.vue +2 -8
  178. package/promptRemove/management.cattle.io.roletemplate.vue +1 -1
  179. package/promptRemove/mixin/roleDeletionCheck.js +1 -7
  180. package/promptRemove/pod.vue +7 -28
  181. package/rancher-components/Card/Card.vue +9 -1
  182. package/rancher-components/Form/Checkbox/Checkbox.vue +42 -6
  183. package/rancher-components/Form/LabeledInput/LabeledInput.vue +30 -3
  184. package/rancher-components/Form/Radio/RadioButton.vue +18 -3
  185. package/rancher-components/Form/Radio/RadioGroup.vue +39 -5
  186. package/rancher-components/LabeledTooltip/LabeledTooltip.vue +13 -1
  187. package/rancher-components/RcButton/RcButton.test.ts +97 -0
  188. package/rancher-components/RcButton/RcButton.vue +14 -9
  189. package/rancher-components/RcDropdown/RcDropdown.vue +3 -1
  190. package/rancher-components/RcDropdown/RcDropdownItem.vue +8 -2
  191. package/rancher-components/RcDropdown/RcDropdownMenu.vue +66 -0
  192. package/rancher-components/RcDropdown/index.ts +1 -0
  193. package/rancher-components/RcDropdown/types.ts +27 -0
  194. package/rancher-components/RcDropdown/useDropdownContext.ts +5 -2
  195. package/scripts/extension/helm/charts/ui-plugin-server/templates/_helpers.tpl +2 -2
  196. package/scripts/typegen.sh +1 -0
  197. package/store/__tests__/auth.test.ts +120 -0
  198. package/store/action-menu.js +13 -3
  199. package/store/auth.js +14 -9
  200. package/store/aws.js +9 -2
  201. package/store/catalog.js +14 -7
  202. package/store/features.js +1 -0
  203. package/store/prefs.js +9 -28
  204. package/store/type-map.utils.ts +4 -0
  205. package/types/resources/settings.d.ts +27 -20
  206. package/types/shell/index.d.ts +18 -12
  207. package/utils/__tests__/array.test.ts +13 -1
  208. package/utils/__tests__/string.test.ts +80 -1
  209. package/utils/array.ts +13 -0
  210. package/utils/auth.js +4 -0
  211. package/utils/banners.js +0 -45
  212. package/utils/cluster.js +1 -1
  213. package/{edit/monitoring.coreos.com.prometheusrule → utils}/duration.js +5 -3
  214. package/utils/object.js +0 -3
  215. package/utils/pagination-utils.ts +15 -2
  216. package/utils/string.js +31 -7
  217. package/utils/validators/formRules/__tests__/index.test.ts +27 -0
  218. package/utils/validators/formRules/index.ts +16 -0
  219. package/edit/provisioning.cattle.io.cluster/import.vue +0 -198
@@ -105,6 +105,10 @@ export default defineComponent({
105
105
 
106
106
  emits: ['update:value'],
107
107
 
108
+ data() {
109
+ return { currFocusedElem: undefined as undefined | EventTarget | null };
110
+ },
111
+
108
112
  computed: {
109
113
  /**
110
114
  * Creates a collection of Options from the provided props.
@@ -151,12 +155,33 @@ export default defineComponent({
151
155
  }
152
156
  },
153
157
 
158
+ beforeUnmount() {
159
+ const radioGroup = this.$refs?.radioGroup as HTMLInputElement;
160
+
161
+ radioGroup.removeEventListener('focusin', this.focusChanged);
162
+ },
163
+
164
+ mounted() {
165
+ const radioGroup = this.$refs?.radioGroup as HTMLInputElement;
166
+
167
+ radioGroup.addEventListener('focusin', this.focusChanged);
168
+ },
169
+
154
170
  methods: {
171
+ focusChanged(ev: Event) {
172
+ this.currFocusedElem = ev.target;
173
+ },
155
174
  /**
156
175
  * Keyboard left/right event listener to select next/previous option. Emits
157
176
  * the input event.
158
177
  */
159
178
  clickNext(direction: number): void {
179
+ // moving focus away from a custom group element and pressing arrow keys
180
+ // should not have any effect on the group - custom UI for radiogroup option(s)
181
+ if (this.currFocusedElem !== this.$refs?.radioGroup) {
182
+ return;
183
+ }
184
+
160
185
  const opts = this.normalizedOptions;
161
186
  const selected = opts.find((x) => x.value === this.value);
162
187
  let newIndex = (selected ? opts.indexOf(selected) : -1) + direction;
@@ -205,12 +230,15 @@ export default defineComponent({
205
230
 
206
231
  <!-- Group -->
207
232
  <div
233
+ ref="radioGroup"
208
234
  role="radiogroup"
209
235
  :aria-label="radioGroupLabel"
210
236
  class="radio-group"
211
237
  :class="{'row':row}"
212
- @keyup.down.stop="clickNext(1)"
213
- @keyup.up.stop="clickNext(-1)"
238
+ tabindex="0"
239
+ @keydown.down.prevent.stop="clickNext(1)"
240
+ @keydown.up.prevent.stop="clickNext(-1)"
241
+ @keydown.space.enter.stop.prevent
214
242
  >
215
243
  <div
216
244
  v-for="(option, i) in normalizedOptions"
@@ -230,7 +258,9 @@ export default defineComponent({
230
258
  :description="option.description"
231
259
  :val="option.value"
232
260
  :disabled="isDisabled"
261
+ :data-testid="`radio-button-${i}`"
233
262
  :mode="mode"
263
+ :prevent-focus-on-radio-groups="true"
234
264
  @update:value="$emit('update:value', $event)"
235
265
  />
236
266
  </slot>
@@ -241,9 +271,13 @@ export default defineComponent({
241
271
 
242
272
  <style lang='scss'>
243
273
  .radio-group {
244
- &:focus {
245
- border:none;
246
- outline:none;
274
+ &:focus, &:focus-visible {
275
+ border: none;
276
+ outline: none;
277
+ }
278
+
279
+ &:focus-visible .radio-button-checked {
280
+ @include focus-outline;
247
281
  }
248
282
 
249
283
  h3 {
@@ -31,6 +31,16 @@ export default defineComponent({
31
31
  computed: {
32
32
  iconClass(): string {
33
33
  return this.status === 'error' ? 'icon-warning' : 'icon-info';
34
+ },
35
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
+ tooltipContent(): {[key: string]: any} | string {
37
+ if (this.isObject(this.value)) {
38
+ return {
39
+ ...{ content: this.value.content, popperClass: [`tooltip-${ status }`] }, ...this.value, triggers: ['hover', 'touch', 'focus']
40
+ };
41
+ }
42
+
43
+ return this.value ? { content: this.value, triggers: ['hover', 'touch', 'focus'] } : '';
34
44
  }
35
45
  },
36
46
  methods: {
@@ -49,9 +59,11 @@ export default defineComponent({
49
59
  >
50
60
  <template v-if="hover">
51
61
  <i
52
- v-clean-tooltip="isObject(value) ? { ...{content: value.content, popperClass: [`tooltip-${status}`]}, ...value } : value"
62
+ v-clean-tooltip="tooltipContent"
63
+ v-stripped-aria-label="isObject(value) ? value.content : value"
53
64
  :class="{'hover':!value, [iconClass]: true}"
54
65
  class="icon status-icon"
66
+ tabindex="0"
55
67
  />
56
68
  </template>
57
69
  <template v-else>
@@ -0,0 +1,97 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import RcButton from './RcButton.vue';
3
+
4
+ describe('rcButton.vue', () => {
5
+ it('renders with default role', () => {
6
+ const wrapper = mount(RcButton);
7
+ const button = wrapper.find('button');
8
+
9
+ expect(button.classes()).toContain('btn');
10
+ expect(button.classes()).toContain('role-primary');
11
+ });
12
+
13
+ it('applies correct role', () => {
14
+ const wrapper = mount(RcButton, { props: { primary: true } });
15
+ const button = wrapper.find('button');
16
+
17
+ expect(button.classes()).toContain('role-primary');
18
+ });
19
+
20
+ it('defaults to primary role if multiple roles are provided', () => {
21
+ const wrapper = mount(
22
+ RcButton,
23
+ {
24
+ props: {
25
+ primary: true,
26
+ secondary: true,
27
+ tertiary: true,
28
+ }
29
+ }
30
+ );
31
+ const button = wrapper.find('button');
32
+
33
+ expect(button.classes()).toContain('role-primary');
34
+ });
35
+
36
+ it('defaults to secondary role if both secondary and tertiary roles are provided', () => {
37
+ const wrapper = mount(
38
+ RcButton,
39
+ {
40
+ props: {
41
+ secondary: true,
42
+ tertiary: true,
43
+ }
44
+ }
45
+ );
46
+ const button = wrapper.find('button');
47
+
48
+ expect(button.classes()).toContain('role-secondary');
49
+ });
50
+
51
+ it('applies correct size class', () => {
52
+ const wrapper = mount(RcButton, { props: { small: true } });
53
+ const button = wrapper.find('button');
54
+
55
+ expect(button.classes()).toContain('btn-sm');
56
+ });
57
+
58
+ it('renders slots correctly', () => {
59
+ const wrapper = mount(RcButton, {
60
+ slots: {
61
+ default: 'Click Me',
62
+ before: 'Before',
63
+ after: 'After',
64
+ },
65
+ });
66
+
67
+ expect(wrapper.text()).toContain('Before');
68
+ expect(wrapper.text()).toContain('Click Me');
69
+ expect(wrapper.text()).toContain('After');
70
+ });
71
+
72
+ it('focuses the button when focus method is called', async() => {
73
+ const wrapper = mount(
74
+ RcButton,
75
+ { attachTo: document.body }
76
+ );
77
+
78
+ await wrapper.vm.focus();
79
+ const button = wrapper.find('button');
80
+
81
+ expect(button.element).toBe(document.activeElement);
82
+ });
83
+
84
+ it('applies additional classes from $attrs', () => {
85
+ const wrapper = mount(RcButton, { attrs: { class: 'extra-class' } });
86
+ const button = wrapper.find('button');
87
+
88
+ expect(button.classes()).toContain('extra-class');
89
+ });
90
+
91
+ it('applies ghost button styles correctly', () => {
92
+ const wrapper = mount(RcButton, { props: { ghost: true } });
93
+ const button = wrapper.find('button');
94
+
95
+ expect(button.classes()).toContain('role-ghost');
96
+ });
97
+ });
@@ -65,25 +65,30 @@ defineExpose({ focus });
65
65
  </template>
66
66
 
67
67
  <style lang="scss" scoped>
68
- .role-link {
69
- &:focus, &.focused {
70
- outline: var(--outline-width) solid var(--border);
71
- box-shadow: 0 0 0 var(--outline-width) var(--outline);
72
- }
73
- }
74
-
75
68
  button {
69
+ &.role-link {
70
+ &:focus, &.focused {
71
+ @include focus-outline;
72
+ outline-offset: -2px;
73
+ }
74
+
75
+ &:hover {
76
+ background-color: var(--accent-btn);
77
+ box-shadow: none;
78
+ }
79
+ }
80
+
76
81
  &.role-ghost {
77
82
  padding: 0;
78
83
  background-color: transparent;
79
84
 
80
85
  &:focus, &.focused {
81
- outline: 2px solid var(--primary-keyboard-focus);
86
+ @include focus-outline;
82
87
  outline-offset: 0;
83
88
  }
84
89
 
85
90
  &:focus-visible {
86
- outline: 2px solid var(--primary-keyboard-focus);
91
+ @include focus-outline;
87
92
  outline-offset: 0;
88
93
  }
89
94
  }
@@ -28,6 +28,8 @@ defineProps<{
28
28
  ariaLabel?: string
29
29
  }>();
30
30
 
31
+ const emit = defineEmits(['update:open']);
32
+
31
33
  const {
32
34
  isMenuOpen,
33
35
  showMenu,
@@ -36,7 +38,7 @@ const {
36
38
  provideDropdownContext,
37
39
  registerDropdownCollection,
38
40
  handleKeydown,
39
- } = useDropdownContext();
41
+ } = useDropdownContext(emit);
40
42
 
41
43
  provideDropdownContext();
42
44
 
@@ -53,12 +53,12 @@ const findNewIndex = (shouldAdvance: boolean, activeIndex: number, itemsArr: Ele
53
53
  return newIndex;
54
54
  };
55
55
 
56
- const handleClick = () => {
56
+ const handleClick = (e: MouseEvent) => {
57
57
  if (props.disabled) {
58
58
  return;
59
59
  }
60
60
 
61
- emits('click');
61
+ emits('click', e);
62
62
  close();
63
63
  };
64
64
 
@@ -85,6 +85,9 @@ const handleActivate = (e: KeyboardEvent) => {
85
85
  @keydown.enter.space="handleActivate"
86
86
  @keydown.up.down.stop="handleKeydown"
87
87
  >
88
+ <slot name="before">
89
+ <!--Empty slot content-->
90
+ </slot>
88
91
  <slot name="default">
89
92
  <!--Empty slot content-->
90
93
  </slot>
@@ -93,6 +96,9 @@ const handleActivate = (e: KeyboardEvent) => {
93
96
 
94
97
  <style lang="scss" scoped>
95
98
  [dropdown-menu-item] {
99
+ display: flex;
100
+ gap: 8px;
101
+ align-items: center;
96
102
  padding: 9px 8px;
97
103
  margin: 0 9px;
98
104
  border-radius: 4px;
@@ -0,0 +1,66 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ RcDropdown,
4
+ RcDropdownItem,
5
+ RcDropdownSeparator,
6
+ RcDropdownTrigger
7
+ } from '@components/RcDropdown';
8
+ import { RcDropdownMenuComponentProps, DropdownOption } from './types';
9
+ import IconOrSvg from '@shell/components/IconOrSvg';
10
+
11
+ // eslint-disable-next-line vue/no-setup-props-destructure
12
+ const { buttonRole = 'primary', buttonSize = '' } = defineProps<RcDropdownMenuComponentProps>();
13
+
14
+ const emit = defineEmits(['update:open', 'select']);
15
+
16
+ const hasOptions = (options: DropdownOption[]) => {
17
+ return options.length !== undefined ? options.length : Object.keys(options).length > 0;
18
+ };
19
+ </script>
20
+
21
+ <template>
22
+ <rc-dropdown
23
+ :aria-label="dropdownAriaLabel"
24
+ @update:open="(e: boolean) => emit('update:open', e)"
25
+ >
26
+ <rc-dropdown-trigger
27
+ :[buttonRole]="true"
28
+ :[buttonSize]="true"
29
+ :data-testid="dataTestid"
30
+ :aria-label="buttonAriaLabel"
31
+ >
32
+ <i class="icon icon-actions" />
33
+ </rc-dropdown-trigger>
34
+ <template #dropdownCollection>
35
+ <template
36
+ v-for="(a) in options"
37
+ :key="a.label"
38
+ >
39
+ <rc-dropdown-item
40
+ v-if="!a.divider"
41
+ @click="(e: MouseEvent) => emit('select', e, a)"
42
+ >
43
+ <template #before>
44
+ <IconOrSvg
45
+ v-if="a.icon || a.svg"
46
+ :icon="a.icon"
47
+ :src="a.svg"
48
+ class="icon"
49
+ color="header"
50
+ />
51
+ </template>
52
+ {{ a.label }}
53
+ </rc-dropdown-item>
54
+ <rc-dropdown-separator
55
+ v-else
56
+ />
57
+ </template>
58
+ <rc-dropdown-item
59
+ v-if="!hasOptions(options)"
60
+ disabled
61
+ >
62
+ No actions available
63
+ </rc-dropdown-item>
64
+ </template>
65
+ </rc-dropdown>
66
+ </template>
@@ -2,3 +2,4 @@ export { default as RcDropdown } from './RcDropdown.vue';
2
2
  export { default as RcDropdownItem } from './RcDropdownItem.vue';
3
3
  export { default as RcDropdownSeparator } from './RcDropdownSeparator.vue';
4
4
  export { default as RcDropdownTrigger } from './RcDropdownTrigger.vue';
5
+ export { default as RcDropdownMenu } from './RcDropdownMenu.vue';
@@ -1,5 +1,6 @@
1
1
  import { Ref, ref } from 'vue';
2
2
  import type { RcButtonType } from '@components/RcButton';
3
+ import { ButtonRoleProps, ButtonSizeProps } from '@components/RcButton/types';
3
4
 
4
5
  export type DropdownContext = {
5
6
  handleKeydown: () => void;
@@ -20,3 +21,29 @@ export const defaultContext: DropdownContext = {
20
21
  isMenuOpen: ref(false),
21
22
  close: () => null,
22
23
  };
24
+
25
+ export type DropdownOption = {
26
+ action?: string;
27
+ divider?: boolean;
28
+ enabled: boolean;
29
+ icon?: string;
30
+ svg?: string;
31
+ label?: string;
32
+ total: number;
33
+ allEnabled: boolean;
34
+ anyEnabled: boolean;
35
+ available: number;
36
+ bulkable?: boolean;
37
+ bulkAction?: string;
38
+ altAction?: string;
39
+ weight?: number;
40
+ }
41
+
42
+ export type RcDropdownMenuComponentProps = {
43
+ options: DropdownOption[];
44
+ buttonRole?: keyof ButtonRoleProps;
45
+ buttonSize?: keyof ButtonSizeProps;
46
+ buttonAriaLabel?: string;
47
+ dropdownAriaLabel?: string;
48
+ dataTestid?: string;
49
+ }
@@ -1,7 +1,9 @@
1
- import { ref, provide, nextTick } from 'vue';
1
+ import { ref, provide, nextTick, defineEmits } from 'vue';
2
2
  import { useDropdownCollection } from './useDropdownCollection';
3
3
  import { RcButtonType } from '@components/RcButton';
4
4
 
5
+ const rcDropdownEmits = defineEmits(['update:open']);
6
+
5
7
  /**
6
8
  * Composable that provides the context for a dropdown menu. Includes methods
7
9
  * and state for managing the dropdown's visibility, focus, and keyboard
@@ -11,7 +13,7 @@ import { RcButtonType } from '@components/RcButton';
11
13
  * @returns Dropdown context methods and state. Used for programmatic
12
14
  * interactions and setting focus.
13
15
  */
14
- export const useDropdownContext = () => {
16
+ export const useDropdownContext = (emit: typeof rcDropdownEmits) => {
15
17
  const {
16
18
  dropdownItems,
17
19
  firstDropdownItem,
@@ -30,6 +32,7 @@ export const useDropdownContext = () => {
30
32
  didKeydown.value = false;
31
33
  }
32
34
  isMenuOpen.value = show;
35
+ emit('update:open', show);
33
36
  };
34
37
 
35
38
  /**
@@ -38,7 +38,7 @@ Common labels
38
38
  helm.sh/chart: {{ include "extension-server.chart" . }}
39
39
  {{ include "extension-server.selectorLabels" . }}
40
40
  {{- if .Chart.AppVersion }}
41
- app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
41
+ app.kubernetes.io/version: {{ .Chart.AppVersion | replace "+" "_" | quote }}
42
42
  {{- end }}
43
43
  app.kubernetes.io/managed-by: {{ .Release.Service }}
44
44
  {{- end }}
@@ -60,4 +60,4 @@ Pkg annotations
60
60
  {{ $key }}: {{ $value | quote }}
61
61
  {{- end }}
62
62
  {{- end }}
63
- {{- end }}
63
+ {{- end }}
@@ -34,6 +34,7 @@ ${BASE_DIR}/node_modules/.bin/tsc ${SHELL_DIR}/plugins/dashboard-store/resource-
34
34
  ${BASE_DIR}/node_modules/.bin/tsc ${SHELL_DIR}/plugins/dashboard-store/classify.js --declaration --allowJs --emitDeclarationOnly --outDir ${SHELL_DIR}/tmp/plugins/dashboard-store/ > /dev/null
35
35
  ${BASE_DIR}/node_modules/.bin/tsc ${SHELL_DIR}/plugins/dashboard-store/actions.js --declaration --allowJs --emitDeclarationOnly --outDir ${SHELL_DIR}/tmp/plugins/dashboard-store/ > /dev/null
36
36
  ${BASE_DIR}/node_modules/.bin/tsc ${SHELL_DIR}/plugins/steve/steve-class.js --declaration --allowJs --emitDeclarationOnly --outDir ${SHELL_DIR}/tmp/plugins/steve/ > /dev/null
37
+ ${BASE_DIR}/node_modules/.bin/tsc ${SHELL_DIR}/plugins/steve/hybrid-class.js --declaration --allowJs --emitDeclarationOnly --outDir ${SHELL_DIR}/tmp/plugins/steve/ > /dev/null
37
38
 
38
39
  # # mixins
39
40
  ${BASE_DIR}/node_modules/.bin/tsc ${SHELL_DIR}/mixins/create-edit-view/index.js --declaration --allowJs --emitDeclarationOnly --outDir ${SHELL_DIR}/tmp/mixins/create-edit-view > /dev/null
@@ -0,0 +1,120 @@
1
+ import { actions } from '@shell/store/auth';
2
+ import { createStore } from 'vuex';
3
+
4
+ // jest.mock('@shell/utils/url', () => ({
5
+ // addParams: () => ({}),
6
+ // parse: () => ({}),
7
+ // removeParam: () => ({}),
8
+ // }));
9
+
10
+ describe('action: redirectTo', () => {
11
+ it('should include query parameters from redirect', async() => {
12
+ jest.spyOn(window, 'window', 'get');
13
+ const store = { dispatch: jest.fn() };
14
+ const clientId = '123';
15
+ const uri = 'anyURI';
16
+ const scope = 'anything';
17
+ const expectation = `:///?client_id=${ clientId }&redirect_uri=${ uri }&response_type=code&response_mode=query&scope=${ scope }&state=undefined`;
18
+ const options = {
19
+ provider: 'azuread',
20
+ redirect: false,
21
+ redirectUrl: `?client_id=${ clientId }&redirect_uri=${ uri }&scope=${ scope }`,
22
+ };
23
+
24
+ const url = await actions.redirectTo(store as any, options);
25
+
26
+ expect(url).toStrictEqual(expectation);
27
+ });
28
+
29
+ it.each([
30
+ ['genericoidc', '://myhost/?redirect_uri=anyURI&scope=openid%20profile%20email&state=undefined'],
31
+ ])('given provider %p should return URL %p', async(provider, expectation) => {
32
+ jest.spyOn(window, 'window', 'get');
33
+ const store = { dispatch: jest.fn() };
34
+ const uri = 'anyURI'; // This field is added anyway, so we set a random value
35
+ const options = {
36
+ provider,
37
+ redirect: false,
38
+ redirectUrl: `myhost?redirect_uri=${ uri }`,
39
+ };
40
+
41
+ const url = await actions.redirectTo(store as any, options);
42
+
43
+ expect(url).toStrictEqual(expectation);
44
+ });
45
+
46
+ describe.each([
47
+ // 'whatever',
48
+ // 'github',
49
+ // 'googleoauth',
50
+ // 'azuread',
51
+ // 'keycloakoidc',
52
+ 'genericoidc',
53
+ ])('given provider %p', (provider) => {
54
+ it('should keep scope from options', async() => {
55
+ const customScope = 'myScope';
56
+ const options = {
57
+ provider,
58
+ redirectUrl: 'anyURL',
59
+ scopes: customScope,
60
+ // scopesJoinChar: ' ', // it's not used for genericoidc
61
+ test: true,
62
+ redirect: false
63
+ };
64
+ const store = { dispatch: jest.fn() };
65
+
66
+ jest.spyOn(window, 'window', 'get');
67
+ const url = await actions.redirectTo(store as any, options);
68
+
69
+ expect(url).toContain(customScope);
70
+ });
71
+
72
+ it('should merge scopes into a single string and avoid duplication', async() => {
73
+ const defaultScopes = 'openid profile email';
74
+ const customScope = 'myScope';
75
+ const options = {
76
+ provider,
77
+ redirectUrl: 'anyURL',
78
+ scopes: `${ defaultScopes } ${ customScope }`,
79
+ test: true,
80
+ redirect: false
81
+ };
82
+ const store = { dispatch: jest.fn() };
83
+
84
+ jest.spyOn(window, 'window', 'get');
85
+ const url = await actions.redirectTo(store as any, options);
86
+
87
+ expect(url).toContain('scope=openid%20profile%20email%20myScope&');
88
+ });
89
+ });
90
+ });
91
+
92
+ jest.mock('@shell/utils/auth');
93
+
94
+ describe('action: test', () => {
95
+ describe('given providers with an action (github, google, azuread, oidc)', () => {
96
+ it('should call redirectTo with all the options', async() => {
97
+ const dispatchSpy = jest.fn().mockReturnValue('anyURL');
98
+ const store = createStore({
99
+ actions: {
100
+ getAuthConfig: () => ({ doAction: () => 'no action' }),
101
+ redirectTo: dispatchSpy,
102
+ }
103
+ });
104
+ const provider = 'anyProvider';
105
+ const redirectUrl = undefined;
106
+ const body = { scope: ['any scope'] };
107
+ const options = {
108
+ provider,
109
+ redirectUrl,
110
+ scopes: body.scope,
111
+ test: true,
112
+ redirect: false
113
+ };
114
+
115
+ await actions.test(store, { provider, body });
116
+
117
+ expect(dispatchSpy.mock.calls[0][1]).toStrictEqual(options);
118
+ });
119
+ });
120
+ });
@@ -27,7 +27,7 @@ export const getters = {
27
27
  event: (state) => state.event,
28
28
  resources: (state) => state.resources,
29
29
 
30
- options(state) {
30
+ optionsArray(state) {
31
31
  let selected = state.resources;
32
32
 
33
33
  if ( !selected ) {
@@ -41,7 +41,7 @@ export const getters = {
41
41
  const map = {};
42
42
 
43
43
  for ( const node of selected ) {
44
- if (node.availableActions) {
44
+ if (node?.availableActions) {
45
45
  for ( const act of node.availableActions ) {
46
46
  _add(map, act);
47
47
  }
@@ -50,7 +50,10 @@ export const getters = {
50
50
 
51
51
  const out = _filter(map);
52
52
 
53
- return { ...out };
53
+ return [...out];
54
+ },
55
+ options(_state, getters) {
56
+ return { ...getters.optionsArray };
54
57
  },
55
58
 
56
59
  };
@@ -144,12 +147,19 @@ export const mutations = {
144
147
 
145
148
  state.modalData = data;
146
149
  },
150
+
151
+ SET_RESOURCE(state, resources) {
152
+ state.resources = !isArray(resources) ? [resources] : resources;
153
+ }
147
154
  };
148
155
 
149
156
  export const actions = {
150
157
  execute({ state }, { action, args, opts }) {
151
158
  return _execute(state.resources, action, args, opts);
152
159
  },
160
+ setResource({ commit }, resource) {
161
+ commit('SET_RESOURCE', resource);
162
+ }
153
163
  };
154
164
 
155
165
  // -----------------------------