@rancher/shell 3.0.7 → 3.0.8-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/assets/images/vendor/githubapp.svg +13 -0
  2. package/assets/styles/base/_typography.scss +1 -1
  3. package/assets/styles/global/_layout.scss +21 -35
  4. package/assets/styles/themes/_modern.scss +5 -5
  5. package/assets/translations/en-us.yaml +102 -17
  6. package/assets/translations/zh-hans.yaml +0 -4
  7. package/components/EmberPage.vue +1 -1
  8. package/components/Inactivity.vue +222 -106
  9. package/components/InstallHelmCharts.vue +2 -2
  10. package/components/Resource/Detail/CopyToClipboard.vue +1 -1
  11. package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +0 -2
  12. package/components/Resource/Detail/TitleBar/index.vue +10 -6
  13. package/components/ResourceDetail/index.vue +4 -1
  14. package/components/SortableTable/index.vue +18 -2
  15. package/components/{nav/WindowManager → Window}/ContainerLogs.vue +1 -1
  16. package/components/{nav/WindowManager → Window}/ContainerLogsActions.vue +1 -0
  17. package/components/{nav/WindowManager → Window}/__tests__/ContainerLogs.test.ts +1 -1
  18. package/components/{nav/WindowManager → Window}/__tests__/ContainerShell.test.ts +2 -2
  19. package/components/fleet/FleetConfigMapSelector.vue +117 -0
  20. package/components/fleet/FleetSecretSelector.vue +127 -0
  21. package/components/fleet/__tests__/FleetConfigMapSelector.test.ts +125 -0
  22. package/components/fleet/__tests__/FleetSecretSelector.test.ts +82 -0
  23. package/components/form/FileImageSelector.vue +13 -4
  24. package/components/form/FileSelector.vue +11 -2
  25. package/components/form/ResourceLabeledSelect.vue +1 -0
  26. package/components/form/__tests__/ResourceLabeledSelect.test.ts +90 -0
  27. package/components/nav/Header.vue +34 -13
  28. package/components/{DraggableZone.vue → nav/WindowManager/PinArea.vue} +47 -80
  29. package/components/nav/WindowManager/composables/useComponentsMount.ts +70 -0
  30. package/components/nav/WindowManager/composables/useDimensionsHandler.ts +105 -0
  31. package/components/nav/WindowManager/composables/useDragHandler.ts +99 -0
  32. package/components/nav/WindowManager/composables/usePanelHandler.ts +72 -0
  33. package/components/nav/WindowManager/composables/usePanelsHandler.ts +14 -0
  34. package/components/nav/WindowManager/composables/useResizeHandler.ts +167 -0
  35. package/components/nav/WindowManager/composables/useTabsHandler.ts +51 -0
  36. package/components/nav/WindowManager/constants.ts +23 -0
  37. package/components/nav/WindowManager/index.vue +61 -575
  38. package/components/nav/WindowManager/panels/HorizontalPanel.vue +265 -0
  39. package/components/nav/WindowManager/panels/TabBodyContainer.vue +39 -0
  40. package/components/nav/WindowManager/panels/VerticalPanel.vue +308 -0
  41. package/components/templates/default.vue +4 -40
  42. package/components/templates/home.vue +31 -5
  43. package/config/product/auth.js +1 -0
  44. package/config/query-params.js +1 -0
  45. package/config/settings.ts +8 -1
  46. package/config/store.js +4 -2
  47. package/config/types.js +2 -0
  48. package/detail/pod.vue +1 -0
  49. package/dialog/AddonConfigConfirmationDialog.vue +45 -1
  50. package/directives/ui-context.ts +97 -0
  51. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +52 -11
  52. package/edit/auth/AuthProviderWarningBanners.vue +14 -1
  53. package/edit/auth/github-app-steps.vue +97 -0
  54. package/edit/auth/github-steps.vue +75 -0
  55. package/edit/auth/github.vue +94 -65
  56. package/edit/fleet.cattle.io.helmop.vue +51 -2
  57. package/edit/networking.k8s.io.networkpolicy/PolicyRuleTarget.vue +15 -5
  58. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +11 -9
  59. package/edit/provisioning.cattle.io.cluster/rke2.vue +56 -9
  60. package/edit/provisioning.cattle.io.cluster/tabs/AddOnConfig.vue +28 -2
  61. package/initialize/install-directives.js +2 -0
  62. package/list/projectsecret.vue +1 -1
  63. package/machine-config/azure.vue +1 -1
  64. package/mixins/chart.js +1 -1
  65. package/models/__tests__/chart.test.ts +17 -9
  66. package/models/__tests__/compliance.cattle.io.clusterscanprofile.spec.js +30 -0
  67. package/models/catalog.cattle.io.app.js +1 -1
  68. package/models/chart.js +3 -1
  69. package/models/compliance.cattle.io.clusterscanprofile.js +1 -1
  70. package/models/management.cattle.io.authconfig.js +1 -0
  71. package/package.json +2 -2
  72. package/pages/auth/login.vue +5 -2
  73. package/pages/auth/verify.vue +1 -1
  74. package/pages/c/_cluster/apps/charts/AppChartCardSubHeader.vue +3 -2
  75. package/pages/c/_cluster/apps/charts/chart.vue +2 -2
  76. package/pages/c/_cluster/explorer/EventsTable.vue +89 -3
  77. package/pages/c/_cluster/explorer/tools/index.vue +3 -3
  78. package/pages/c/_cluster/settings/performance.vue +12 -25
  79. package/pages/home.vue +313 -12
  80. package/plugins/axios.js +2 -1
  81. package/plugins/dashboard-store/actions.js +1 -1
  82. package/plugins/dashboard-store/resource-class.js +17 -2
  83. package/plugins/steve/steve-pagination-utils.ts +2 -2
  84. package/rancher-components/RcDropdown/RcDropdownItemSelect.vue +5 -1
  85. package/scripts/extension/publish +1 -1
  86. package/store/auth.js +8 -3
  87. package/store/aws.js +8 -6
  88. package/store/features.js +1 -0
  89. package/store/index.js +9 -3
  90. package/store/prefs.js +6 -0
  91. package/store/ui-context.ts +86 -0
  92. package/store/wm.ts +244 -0
  93. package/types/kube/kube-api.ts +2 -1
  94. package/types/rancher/index.d.ts +1 -0
  95. package/types/resources/settings.d.ts +29 -7
  96. package/types/shell/index.d.ts +59 -0
  97. package/types/window-manager.ts +22 -0
  98. package/utils/__tests__/cluster.test.ts +379 -1
  99. package/utils/cluster.js +157 -3
  100. package/utils/dynamic-content/__tests__/config.test.ts +187 -0
  101. package/utils/dynamic-content/__tests__/index.test.ts +390 -0
  102. package/utils/dynamic-content/__tests__/info.test.ts +263 -0
  103. package/utils/dynamic-content/__tests__/new-release.test.ts +216 -0
  104. package/utils/dynamic-content/__tests__/support-notice.test.ts +262 -0
  105. package/utils/dynamic-content/__tests__/util.test.ts +235 -0
  106. package/utils/dynamic-content/config.ts +55 -0
  107. package/utils/dynamic-content/index.ts +273 -0
  108. package/utils/dynamic-content/info.ts +219 -0
  109. package/utils/dynamic-content/new-release.ts +126 -0
  110. package/utils/dynamic-content/support-notice.ts +169 -0
  111. package/utils/dynamic-content/types.d.ts +101 -0
  112. package/utils/dynamic-content/util.ts +122 -0
  113. package/utils/dynamic-importer.js +2 -2
  114. package/utils/inactivity.ts +104 -0
  115. package/utils/pagination-utils.ts +19 -4
  116. package/utils/release-notes.ts +1 -1
  117. package/assets/images/icons/document.svg +0 -3
  118. package/store/wm.js +0 -95
  119. /package/components/{nav/WindowManager → Window}/ChartReadme.vue +0 -0
  120. /package/components/{nav/WindowManager → Window}/ContainerShell.vue +0 -0
  121. /package/components/{nav/WindowManager → Window}/KubectlShell.vue +0 -0
  122. /package/components/{nav/WindowManager → Window}/MachineSsh.vue +0 -0
  123. /package/components/{nav/WindowManager → Window}/Window.vue +0 -0
@@ -0,0 +1,117 @@
1
+ <script lang="ts" setup>
2
+ import { ref } from 'vue';
3
+ import { _EDIT } from '@shell/config/query-params';
4
+ import { CONFIG_MAP } from '@shell/config/types';
5
+ import { PaginationParamFilter } from '@shell/types/store/pagination.types';
6
+ import ResourceLabeledSelect from '@shell/components/form/ResourceLabeledSelect.vue';
7
+
8
+ interface ConfigMap {
9
+ id?: string;
10
+ name: string;
11
+ namespace: string;
12
+ }
13
+
14
+ const props = defineProps({
15
+ value: {
16
+ type: Object,
17
+ required: true,
18
+ },
19
+ namespace: {
20
+ type: String,
21
+ required: true,
22
+ },
23
+ inStore: {
24
+ type: String,
25
+ default: 'management',
26
+ },
27
+ mode: {
28
+ type: String,
29
+ default: _EDIT
30
+ },
31
+ label: {
32
+ type: String,
33
+ default: '',
34
+ },
35
+ });
36
+
37
+ const emit = defineEmits(['update:value']);
38
+
39
+ const configMaps = ref<ConfigMap[]>([]);
40
+
41
+ const allConfigMapsSettings = {
42
+ updateResources: (configMapsList: ConfigMap[]) => {
43
+ const allConfigMapsInNamespace = configMapsList.filter((configMap) => configMap.namespace === props.namespace);
44
+ const mappedConfigMaps = mapConfigMaps(allConfigMapsInNamespace.sort((a, b) => a.name.localeCompare(b.name)));
45
+
46
+ configMaps.value = allConfigMapsInNamespace;
47
+
48
+ return mappedConfigMaps;
49
+ }
50
+ };
51
+
52
+ const paginateConfigMapsSetting = {
53
+ requestSettings: paginatePageOptions,
54
+ updateResources: (configMapsList: ConfigMap[]) => {
55
+ const mappedConfigMaps = mapConfigMaps(configMapsList);
56
+
57
+ configMaps.value = configMapsList;
58
+
59
+ return mappedConfigMaps;
60
+ }
61
+ };
62
+
63
+ function mapConfigMaps(configMapsList: ConfigMap[]) {
64
+ return configMapsList.reduce<{ label: string; value: string }[]>((res, c) => {
65
+ if (c.id) {
66
+ res.push({ label: c.name, value: c.name });
67
+ } else {
68
+ res.push(c as any);
69
+ }
70
+
71
+ return res;
72
+ }, []);
73
+ }
74
+
75
+ function paginatePageOptions(opts: any) {
76
+ const { opts: { filter } } = opts;
77
+
78
+ const filters = !!filter ? [PaginationParamFilter.createSingleField({
79
+ field: 'metadata.name', value: filter, exact: false, equals: true
80
+ })] : [];
81
+
82
+ filters.push(
83
+ PaginationParamFilter.createSingleField({ field: 'metadata.namespace', value: props.namespace }),
84
+ );
85
+
86
+ return {
87
+ ...opts,
88
+ filters,
89
+ groupByNamespace: false,
90
+ classify: true,
91
+ sort: [{ asc: true, field: 'metadata.name' }],
92
+ };
93
+ }
94
+
95
+ function update(value: any) {
96
+ emit('update:value', value);
97
+ }
98
+ </script>
99
+
100
+ <template>
101
+ <ResourceLabeledSelect
102
+ :key="namespace"
103
+ :value="value"
104
+ :label="label || t('fleet.configMaps.label')"
105
+ :mode="mode"
106
+ :resource-type="CONFIG_MAP"
107
+ :loading="$fetchState.pending"
108
+ :in-store="inStore"
109
+ :paginated-resource-settings="paginateConfigMapsSetting"
110
+ :all-resources-settings="allConfigMapsSettings"
111
+ :multiple="true"
112
+ @update:value="update"
113
+ />
114
+ </template>
115
+
116
+ <style lang="scss" scoped>
117
+ </style>
@@ -0,0 +1,127 @@
1
+ <script lang="ts" setup>
2
+ import { ref, computed, defineProps, defineEmits } from 'vue';
3
+ import { _EDIT } from '@shell/config/query-params';
4
+ import { TYPES } from '@shell/models/secret';
5
+ import { SECRET } from '@shell/config/types';
6
+ import { PaginationParamFilter } from '@shell/types/store/pagination.types';
7
+ import ResourceLabeledSelect from '@shell/components/form/ResourceLabeledSelect.vue';
8
+
9
+ interface Secret {
10
+ id?: string;
11
+ name: string;
12
+ namespace: string;
13
+ _type: string;
14
+ }
15
+
16
+ const props = defineProps({
17
+ value: {
18
+ type: Object,
19
+ required: true,
20
+ },
21
+ namespace: {
22
+ type: String,
23
+ required: true,
24
+ },
25
+ inStore: {
26
+ type: String,
27
+ default: 'management',
28
+ },
29
+ mode: {
30
+ type: String,
31
+ default: _EDIT
32
+ },
33
+ label: {
34
+ type: String,
35
+ default: '',
36
+ },
37
+ });
38
+
39
+ const emit = defineEmits(['update:value']);
40
+
41
+ const types = computed<string[]>(() => Object.values(TYPES));
42
+
43
+ const secrets = ref<Secret[]>([]);
44
+
45
+ const allSecretsSettings = {
46
+ updateResources: (secretsList: Secret[]) => {
47
+ const allSecretsInNamespace = secretsList.filter((secret) => types.value.includes(secret._type) && secret.namespace === props.namespace);
48
+ const mappedSecrets = mapSecrets(allSecretsInNamespace.sort((a, b) => a.name.localeCompare(b.name)));
49
+
50
+ secrets.value = allSecretsInNamespace;
51
+
52
+ return mappedSecrets;
53
+ }
54
+ };
55
+
56
+ const paginateSecretsSetting = {
57
+ requestSettings: paginatePageOptions,
58
+ updateResources: (secretsList: Secret[]) => {
59
+ const mappedSecrets = mapSecrets(secretsList);
60
+
61
+ secrets.value = secretsList;
62
+
63
+ return mappedSecrets;
64
+ }
65
+ };
66
+
67
+ function mapSecrets(secretsList: Secret[]) {
68
+ return secretsList.reduce<{ label: string; value: string }[]>((res, s) => {
69
+ if (s.id) {
70
+ res.push({ label: s.name, value: s.name });
71
+ } else {
72
+ res.push(s as any);
73
+ }
74
+
75
+ return res;
76
+ }, []);
77
+ }
78
+
79
+ function update(value: any) {
80
+ emit('update:value', value);
81
+ }
82
+
83
+ function paginatePageOptions(opts: any) {
84
+ const { opts: { filter } } = opts;
85
+
86
+ const filters = !!filter ? [PaginationParamFilter.createSingleField({
87
+ field: 'metadata.name', value: filter, exact: false, equals: true
88
+ })] : [];
89
+
90
+ filters.push(
91
+ PaginationParamFilter.createSingleField({ field: 'metadata.namespace', value: props.namespace }),
92
+ PaginationParamFilter.createMultipleFields(types.value.map((t) => ({
93
+ field: 'metadata.fields.1',
94
+ equals: true,
95
+ exact: true,
96
+ value: t
97
+ })))
98
+ );
99
+
100
+ return {
101
+ ...opts,
102
+ filters,
103
+ groupByNamespace: false,
104
+ classify: true,
105
+ sort: [{ asc: true, field: 'metadata.name' }],
106
+ };
107
+ }
108
+ </script>
109
+
110
+ <template>
111
+ <ResourceLabeledSelect
112
+ :key="namespace"
113
+ :value="value"
114
+ :label="label || t('fleet.secrets.label')"
115
+ :mode="mode"
116
+ :resource-type="SECRET"
117
+ :loading="$fetchState.pending"
118
+ :in-store="inStore"
119
+ :paginated-resource-settings="paginateSecretsSetting"
120
+ :all-resources-settings="allSecretsSettings"
121
+ :multiple="true"
122
+ @update:value="update"
123
+ />
124
+ </template>
125
+
126
+ <style lang="scss" scoped>
127
+ </style>
@@ -0,0 +1,125 @@
1
+ import { shallowMount } from '@vue/test-utils';
2
+ import { _EDIT } from '@shell/config/query-params';
3
+ import FleetConfigMapSelector from '@shell/components/fleet/FleetConfigMapSelector.vue';
4
+ import ResourceLabeledSelect from '@shell/components/form/ResourceLabeledSelect.vue';
5
+
6
+ describe('fleetConfigMapSelector.vue', () => {
7
+ const defaultProps = {
8
+ value: {},
9
+ namespace: 'fleet-default',
10
+ inStore: 'management',
11
+ mode: _EDIT,
12
+ label: 'Config Map',
13
+ };
14
+
15
+ const global = {
16
+ stubs: { ResourceLabeledSelect },
17
+ mocks: {
18
+ $fetchState: { pending: false },
19
+ $store: { getters: { 'management/all': () => [] } },
20
+ },
21
+ };
22
+
23
+ it('should emit update:value when update is called', async() => {
24
+ const wrapper = shallowMount(FleetConfigMapSelector, {
25
+ props: defaultProps,
26
+ global,
27
+ });
28
+
29
+ const vm = wrapper.vm as any;
30
+
31
+ await vm.update('cm1');
32
+
33
+ expect(wrapper.emitted('update:value')).toBeTruthy();
34
+ expect(wrapper.emitted('update:value')?.[0]).toStrictEqual(['cm1']);
35
+ });
36
+
37
+ it('should correctly map configMaps', () => {
38
+ const wrapper = shallowMount(FleetConfigMapSelector, {
39
+ props: defaultProps,
40
+ global
41
+ });
42
+
43
+ const configMapsList = [
44
+ {
45
+ id: '1', name: 'cm1', namespace: 'fleet-default'
46
+ },
47
+ { name: 'cm2', namespace: 'fleet-default' }
48
+ ];
49
+
50
+ const vm = wrapper.vm as any;
51
+ const result = vm.mapConfigMaps(configMapsList);
52
+
53
+ expect(result).toStrictEqual([
54
+ { label: 'cm1', value: 'cm1' },
55
+ { name: 'cm2', namespace: 'fleet-default' }
56
+ ]);
57
+ });
58
+
59
+ it('should return correct filter options from paginatePageOptions', () => {
60
+ const wrapper = shallowMount(FleetConfigMapSelector, {
61
+ props: defaultProps,
62
+ global
63
+ });
64
+
65
+ const opts = { opts: { filter: 'test' } };
66
+
67
+ const vm = wrapper.vm as any;
68
+ const result = vm.paginatePageOptions(opts);
69
+
70
+ expect(result.filters).toHaveLength(2);
71
+ expect(result.groupByNamespace).toStrictEqual(false);
72
+ expect(result.classify).toStrictEqual(true);
73
+ expect(result.sort).toStrictEqual([{ asc: true, field: 'metadata.name' }]);
74
+ });
75
+
76
+ it('should correctly filter and map configMaps in allConfigMapsSettings.updateResources', () => {
77
+ const wrapper = shallowMount(FleetConfigMapSelector, {
78
+ props: defaultProps,
79
+ global
80
+ });
81
+
82
+ const configMapsList = [
83
+ {
84
+ id: '1', name: 'cm1', namespace: 'fleet-default'
85
+ },
86
+ {
87
+ id: '2', name: 'cm2', namespace: 'other'
88
+ }
89
+ ];
90
+
91
+ const vm = wrapper.vm as any;
92
+
93
+ const result = vm.allConfigMapsSettings.updateResources(configMapsList);
94
+
95
+ expect(result).toStrictEqual([{ label: 'cm1', value: 'cm1' }]);
96
+ expect(vm.configMaps).toStrictEqual([{
97
+ id: '1', name: 'cm1', namespace: 'fleet-default'
98
+ }]);
99
+ });
100
+
101
+ it('should correctly map configMaps in paginateConfigMapsSetting.updateResources', () => {
102
+ const wrapper = shallowMount(FleetConfigMapSelector, {
103
+ props: defaultProps,
104
+ global
105
+ });
106
+
107
+ const configMapsList = [
108
+ {
109
+ id: '1', name: 'cm1', namespace: 'fleet-default'
110
+ },
111
+ {
112
+ id: '2', name: 'cm2', namespace: 'fleet-default'
113
+ }
114
+ ];
115
+
116
+ const vm = wrapper.vm as any;
117
+ const result = vm.paginateConfigMapsSetting.updateResources(configMapsList);
118
+
119
+ expect(result).toStrictEqual([
120
+ { label: 'cm1', value: 'cm1' },
121
+ { label: 'cm2', value: 'cm2' }
122
+ ]);
123
+ expect(vm.configMaps).toStrictEqual(configMapsList);
124
+ });
125
+ });
@@ -0,0 +1,82 @@
1
+ import { shallowMount } from '@vue/test-utils';
2
+ import FleetSecretSelector from '@shell/components/fleet/FleetSecretSelector.vue';
3
+ import { _EDIT } from '@shell/config/query-params';
4
+
5
+ describe('component: FleetSecretSelector.vue', () => {
6
+ const secretsMock = [
7
+ {
8
+ id: '1', name: 'secret1', namespace: 'fleet-default', _type: 'Opaque'
9
+ },
10
+ {
11
+ id: '2', name: 'secret2', namespace: 'fleet-default', _type: 'Opaque'
12
+ },
13
+ {
14
+ id: '3', name: 'secret3', namespace: 'other', _type: 'Opaque'
15
+ },
16
+ ];
17
+
18
+ const props = {
19
+ value: {},
20
+ namespace: 'fleet-default',
21
+ inStore: 'management',
22
+ mode: _EDIT,
23
+ };
24
+
25
+ const global = { mocks: { $fetchState: { pending: false, error: false } } };
26
+
27
+ it('should emit update:value when update is called', async() => {
28
+ const wrapper = shallowMount(FleetSecretSelector, { props, global });
29
+
30
+ await (wrapper.vm as any).update('secret1');
31
+
32
+ expect(wrapper.emitted('update:value')).toBeTruthy();
33
+ expect(wrapper.emitted('update:value')?.[0]).toStrictEqual(['secret1']);
34
+ });
35
+
36
+ it('should correctly map secrets', () => {
37
+ const wrapper = shallowMount(FleetSecretSelector, { props, global });
38
+ const mapped = (wrapper.vm as any).mapSecrets(secretsMock);
39
+
40
+ expect(mapped).toStrictEqual([
41
+ { label: 'secret1', value: 'secret1' },
42
+ { label: 'secret2', value: 'secret2' },
43
+ { label: 'secret3', value: 'secret3' }
44
+ ]);
45
+ });
46
+
47
+ it('should filter and sort secrets by namespace and type', () => {
48
+ const wrapper = shallowMount(FleetSecretSelector, { props, global });
49
+
50
+ const result = (wrapper.vm as any).allSecretsSettings.updateResources(secretsMock);
51
+
52
+ expect(result).toStrictEqual([
53
+ { label: 'secret1', value: 'secret1' },
54
+ { label: 'secret2', value: 'secret2' }
55
+ ]);
56
+ expect((wrapper.vm as any).secrets).toStrictEqual([
57
+ {
58
+ id: '1', name: 'secret1', namespace: 'fleet-default', _type: 'Opaque'
59
+ },
60
+ {
61
+ id: '2', name: 'secret2', namespace: 'fleet-default', _type: 'Opaque'
62
+ }
63
+ ]);
64
+ });
65
+
66
+ it('should return correct filter structure from paginatePageOptions', () => {
67
+ const wrapper = shallowMount(FleetSecretSelector, { props, global });
68
+
69
+ const opts = { opts: { filter: 'secret' } };
70
+ const result = (wrapper.vm as any).paginatePageOptions(opts);
71
+
72
+ expect(result.filters).toStrictEqual(
73
+ expect.arrayContaining([
74
+ expect.objectContaining({ fields: expect.arrayContaining([expect.objectContaining({ field: 'metadata.name' })]) }),
75
+ expect.objectContaining({ fields: expect.arrayContaining([expect.objectContaining({ field: 'metadata.namespace' })]) }),
76
+ expect.objectContaining({ fields: expect.arrayContaining([expect.objectContaining({ field: 'metadata.fields.1' })]) }),
77
+ ])
78
+ );
79
+
80
+ expect(result.sort).toStrictEqual([{ asc: true, field: 'metadata.name' }]);
81
+ });
82
+ });
@@ -7,7 +7,8 @@ export default {
7
7
  emits: ['update:value', 'error'],
8
8
 
9
9
  components: { FileSelector, LazyImage },
10
- props: {
10
+
11
+ props: {
11
12
  value: {
12
13
  type: String,
13
14
  default: null,
@@ -36,12 +37,20 @@ export default {
36
37
  accept: {
37
38
  type: String,
38
39
  default: 'image/*'
39
- }
40
+ },
41
+
42
+ class: {
43
+ type: [String, Array],
44
+ default: 'role-primary',
45
+ },
40
46
  },
41
47
  computed: {
42
48
  isView() {
43
49
  return this.mode === _VIEW;
44
- }
50
+ },
51
+ customClass() {
52
+ return [...(Array.isArray(this.class) ? this.class : [this.class])];
53
+ },
45
54
  },
46
55
  methods: {
47
56
  /**
@@ -62,7 +71,7 @@ export default {
62
71
  <FileSelector
63
72
  v-if="!value && !isView"
64
73
  :value="value"
65
- class="btn role-primary"
74
+ :class="customClass"
66
75
  :mode="mode"
67
76
  :read-as-data-url="true"
68
77
  :byte-limit="byteLimit"
@@ -67,12 +67,21 @@ export default {
67
67
  default: '*'
68
68
  },
69
69
 
70
+ class: {
71
+ type: [String, Array],
72
+ default: () => [],
73
+ }
74
+
70
75
  },
71
76
 
72
77
  computed: {
73
78
  isView() {
74
79
  return this.mode === _VIEW;
75
- }
80
+ },
81
+
82
+ customClass() {
83
+ return ['file-selector', 'btn', ...(Array.isArray(this.class) ? this.class : [this.class])];
84
+ },
76
85
  },
77
86
 
78
87
  methods: {
@@ -151,7 +160,7 @@ export default {
151
160
  :aria-label="label"
152
161
  type="button"
153
162
  role="button"
154
- class="file-selector btn"
163
+ :class="customClass"
155
164
  data-testid="file-selector__uploader-button"
156
165
  @click="selectFile"
157
166
  >
@@ -163,6 +163,7 @@ export default defineComponent({
163
163
  :loading="$fetchState.pending"
164
164
  :options="allOfType"
165
165
  :paginate="paginateType"
166
+ :multiple="$attrs.multiple || false"
166
167
  @update:value="$emit('update:value', $event)"
167
168
  />
168
169
  </template>
@@ -0,0 +1,90 @@
1
+ import { shallowMount } from '@vue/test-utils';
2
+ import ResourceLabeledSelect from '@shell/components/form/ResourceLabeledSelect.vue';
3
+ import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
4
+ import { RESOURCE_LABEL_SELECT_MODE } from '@shell/types/components/resourceLabeledSelect';
5
+
6
+ const mockStore = {
7
+ getters: {
8
+ currentStore: jest.fn().mockReturnValue('cluster'),
9
+ 'cluster/paginationEnabled': jest.fn().mockReturnValue(true),
10
+ 'cluster/all': jest.fn().mockReturnValue([{ id: 'foo', name: 'Foo' }]),
11
+ },
12
+ dispatch: jest.fn().mockResolvedValue(undefined),
13
+ };
14
+
15
+ const requiredSetup = () => {
16
+ return {
17
+ global: {
18
+ components: { LabeledSelect },
19
+ mocks: { $store: mockStore, $fetchState: {} }
20
+ }
21
+ };
22
+ };
23
+
24
+ describe('component: ResourceLabeledSelect.vue', () => {
25
+ it('should render LabeledSelect', async() => {
26
+ const wrapper = shallowMount(ResourceLabeledSelect, {
27
+ ...requiredSetup(),
28
+ props: { resourceType: 'testResource' }
29
+ });
30
+
31
+ expect(wrapper.findComponent(LabeledSelect).exists()).toBe(true);
32
+ });
33
+
34
+ it('should call paginateType with overrideRequest if provided', async() => {
35
+ const overrideRequest = jest.fn().mockResolvedValue({ page: [{ id: 'bar', name: 'Bar' }], total: 1 });
36
+ const wrapper = shallowMount(ResourceLabeledSelect, {
37
+ ...requiredSetup(),
38
+ props: {
39
+ resourceType: 'testResource',
40
+ paginateMode: RESOURCE_LABEL_SELECT_MODE.DYNAMIC,
41
+ paginatedResourceSettings: { overrideRequest }
42
+ }
43
+ });
44
+
45
+ const result = await wrapper.vm.paginateType({
46
+ filter: 'bar',
47
+ page: 1,
48
+ pageSize: 10,
49
+ pageContent: [],
50
+ resetPage: false
51
+ });
52
+
53
+ expect(overrideRequest).toHaveBeenCalledWith({
54
+ filter: 'bar',
55
+ page: 1,
56
+ pageSize: 10,
57
+ pageContent: [],
58
+ resetPage: false
59
+ });
60
+ expect(result.page[0].name).toBe('Bar');
61
+ });
62
+
63
+ it('should emit update:value when LabeledSelect emits update:value', async() => {
64
+ const wrapper = shallowMount(ResourceLabeledSelect, {
65
+ ...requiredSetup(),
66
+ props: { resourceType: 'testResource' }
67
+ });
68
+
69
+ wrapper.findComponent(LabeledSelect).vm.$emit('update:value', 'baz');
70
+
71
+ expect(wrapper.emitted('update:value')).toBeTruthy();
72
+ expect(wrapper.emitted('update:value')?.[0]).toStrictEqual(['baz']);
73
+ });
74
+
75
+ it('should pass correct props and attrs to LabeledSelect', async() => {
76
+ const wrapper = shallowMount(ResourceLabeledSelect, {
77
+ ...requiredSetup(),
78
+ props: {
79
+ resourceType: 'testResource',
80
+ allResourcesSettings: { labelSelectOptions: { placeholder: 'Select a resource' } }
81
+ },
82
+ attrs: { multiple: true }
83
+ });
84
+
85
+ const labeledSelect = wrapper.findComponent(LabeledSelect);
86
+
87
+ expect(labeledSelect.attributes('placeholder')).toBe('Select a resource');
88
+ expect(labeledSelect.attributes('multiple')).toBe('true');
89
+ });
90
+ });