@rancher/shell 3.0.12-rc.4 → 3.0.12-rc.5

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 (81) hide show
  1. package/assets/styles/global/_button.scss +1 -1
  2. package/assets/translations/en-us.yaml +39 -10
  3. package/components/ActionDropdownShell.vue +5 -3
  4. package/components/ButtonGroup.vue +26 -1
  5. package/components/CruResource.vue +51 -2
  6. package/components/PromptRestore.vue +93 -32
  7. package/components/Questions/index.vue +1 -0
  8. package/components/ResourceTable.vue +1 -0
  9. package/components/SortableTable/index.vue +4 -3
  10. package/components/Wizard.vue +14 -1
  11. package/components/__tests__/ButtonGroup.test.ts +56 -0
  12. package/components/__tests__/PromptRestore.test.ts +169 -19
  13. package/components/fleet/GitRepoAdvancedTab.vue +1 -0
  14. package/components/fleet/GitRepoMetadataTab.vue +5 -0
  15. package/components/fleet/HelmOpAppCoConfigTab.vue +4 -0
  16. package/components/fleet/HelmOpMetadataTab.vue +5 -0
  17. package/components/form/FileSelector.vue +39 -1
  18. package/components/form/PrivateRegistry.constants.ts +7 -0
  19. package/components/form/PrivateRegistry.vue +253 -18
  20. package/components/form/SelectOrCreateAuthSecret.vue +140 -17
  21. package/components/form/__tests__/FileSelector.test.ts +23 -0
  22. package/components/form/__tests__/PrivateRegistry.test.ts +463 -73
  23. package/components/form/__tests__/SelectOrCreateAuthSecret.test.ts +122 -0
  24. package/components/formatter/EtcdSnapshotName.vue +73 -0
  25. package/components/nav/Header.vue +8 -1
  26. package/components/templates/default.vue +7 -0
  27. package/config/features.js +1 -0
  28. package/config/labels-annotations.js +2 -0
  29. package/config/product/manager.js +6 -0
  30. package/config/secret.ts +10 -0
  31. package/config/settings.ts +6 -2
  32. package/config/types.js +7 -0
  33. package/detail/provisioning.cattle.io.cluster.vue +79 -3
  34. package/dialog/RotateEncryptionKeyDialog.vue +33 -9
  35. package/dialog/__tests__/RotateEncryptionKeyDialog.test.ts +78 -0
  36. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +92 -0
  37. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +101 -0
  38. package/edit/__tests__/management.cattle.io.setting.test.ts +2 -1
  39. package/edit/compliance.cattle.io.clusterscanprofile.vue +39 -41
  40. package/edit/fleet.cattle.io.gitrepo.vue +70 -16
  41. package/edit/fleet.cattle.io.helmop.vue +51 -5
  42. package/edit/helm.cattle.io.projecthelmchart.vue +1 -0
  43. package/edit/{management.cattle.io.setting.vue → management.cattle.io.setting/index.vue} +32 -9
  44. package/edit/management.cattle.io.setting/system-default-registry-pull-secrets.vue +81 -0
  45. package/edit/provisioning.cattle.io.cluster/SelectCredential.vue +3 -12
  46. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +18 -0
  47. package/edit/provisioning.cattle.io.cluster/rke2.vue +5 -1
  48. package/edit/provisioning.cattle.io.cluster/tabs/etcd/index.vue +0 -1
  49. package/edit/provisioning.cattle.io.cluster/tabs/registries/index.vue +14 -55
  50. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +156 -0
  51. package/models/__tests__/secret.test.ts +68 -1
  52. package/models/management.cattle.io.cluster.js +21 -3
  53. package/models/pod.js +13 -2
  54. package/models/provisioning.cattle.io.cluster.js +59 -9
  55. package/models/rke.cattle.io.etcdsnapshot.js +17 -9
  56. package/models/secret.js +19 -0
  57. package/models/workload.js +12 -7
  58. package/package.json +1 -1
  59. package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +485 -107
  60. package/pages/c/_cluster/apps/charts/install.vue +114 -28
  61. package/pkg/require-asset.lib.js +25 -0
  62. package/pkg/vue.config.js +7 -0
  63. package/plugins/dashboard-store/__tests__/resource-class.test.ts +84 -0
  64. package/plugins/dashboard-store/getters.js +0 -1
  65. package/plugins/dashboard-store/resource-class.js +52 -12
  66. package/rancher-components/Form/TextArea/TextAreaAutoGrow.vue +30 -0
  67. package/rancher-components/Form/TextArea/__tests__/TextAreaAutoGrow.test.ts +95 -0
  68. package/rancher-components/RcButton/index.ts +1 -1
  69. package/rancher-components/RcDropdown/RcDropdownTrigger.vue +6 -1
  70. package/store/__tests__/features.test.ts +131 -0
  71. package/store/__tests__/growl.test.ts +374 -0
  72. package/store/__tests__/modal.test.ts +131 -0
  73. package/store/__tests__/slideInPanel.test.ts +88 -0
  74. package/store/__tests__/type-map.utils.test.ts +433 -0
  75. package/store/features.js +4 -0
  76. package/types/shell/index.d.ts +62 -0
  77. package/utils/__tests__/operation-cr.test.ts +34 -0
  78. package/utils/operation-cr.js +19 -0
  79. package/utils/require-asset.ts +7 -0
  80. package/utils/validators/__tests__/private-registry.test.ts +27 -15
  81. package/utils/validators/private-registry.ts +15 -4
@@ -118,4 +118,60 @@ describe('component: ButtonGroup', () => {
118
118
  expect(wrapper.emitted('update:value')).toHaveLength(1);
119
119
  expect(wrapper.emitted('update:value')![0][0]).toBe('val1');
120
120
  });
121
+
122
+ it.each([
123
+ ['small', 'btn-sm'],
124
+ ['medium', 'btn-md'],
125
+ ])('should apply the size class to each button when size is %s', (size, sizeClass) => {
126
+ const options = [
127
+ {
128
+ label: 'label1',
129
+ value: 'val1'
130
+ },
131
+ {
132
+ label: 'label2',
133
+ value: 'val2'
134
+ },
135
+ ];
136
+
137
+ const wrapper = shallowMount(ButtonGroup, {
138
+ props: {
139
+ options,
140
+ value: 'val1',
141
+ size
142
+ }
143
+ });
144
+
145
+ const buttons = wrapper.findAll('button');
146
+
147
+ expect(buttons).toHaveLength(2);
148
+ buttons.forEach((button) => {
149
+ expect(button.classes()).toContain(sizeClass);
150
+ });
151
+ });
152
+
153
+ it.each([
154
+ [undefined],
155
+ ['large'],
156
+ ])('should not apply a size class when size is %s', (size) => {
157
+ const options = [
158
+ {
159
+ label: 'label1',
160
+ value: 'val1'
161
+ },
162
+ ];
163
+
164
+ const wrapper = shallowMount(ButtonGroup, {
165
+ props: {
166
+ options,
167
+ value: 'val1',
168
+ ...(size ? { size } : {})
169
+ }
170
+ });
171
+
172
+ const button = wrapper.find('button');
173
+
174
+ expect(button.classes()).not.toContain('btn-sm');
175
+ expect(button.classes()).not.toContain('btn-md');
176
+ });
121
177
  });
@@ -4,36 +4,46 @@ import PromptRestore from '@shell/components/PromptRestore.vue';
4
4
  import { createStore } from 'vuex';
5
5
  import { ExtendedVue, Vue } from 'vue/types/vue';
6
6
  import { DefaultProps } from 'vue/types/options';
7
- import { CAPI } from '@shell/config/types';
7
+ import { CAPI, MANAGEMENT, OPERATION, SNAPSHOT } from '@shell/config/types';
8
8
  import { STATES_ENUM } from '@shell/plugins/dashboard-store/resource-class';
9
+ import { createOperationCR } from '@shell/utils/operation-cr';
10
+
11
+ jest.mock('@shell/utils/operation-cr', () => ({ createOperationCR: jest.fn() }));
9
12
 
10
13
  const RKE2_CLUSTER_NAME = 'rke2_cluster_name';
11
14
  const RKE2_SUCCESSFUL_SNAPSHOT_1 = {
12
- clusterName: RKE2_CLUSTER_NAME,
13
- type: CAPI.RANCHER_CLUSTER,
14
- created: 'Thu Jul 20 2023 11:11:39',
15
- snapshotFile: { status: STATES_ENUM.SUCCESSFUL },
16
- id: 'rke2_id_1',
17
- name: 'rke2_name_1'
15
+ clusterName: RKE2_CLUSTER_NAME,
16
+ type: CAPI.RANCHER_CLUSTER,
17
+ created: 'Thu Jul 20 2023 11:11:39',
18
+ restoreEnabled: true,
19
+ snapshotFile: { status: STATES_ENUM.SUCCESSFUL },
20
+ id: 'rke2_id_1',
21
+ name: 'rke2_name_1'
18
22
  };
19
23
  const RKE2_SUCCESSFUL_SNAPSHOT_2 = {
20
- clusterName: RKE2_CLUSTER_NAME,
21
- type: CAPI.RANCHER_CLUSTER,
22
- created: 'Thu Jul 20 2022 11:11:39',
23
- snapshotFile: { status: STATES_ENUM.SUCCESSFUL },
24
- id: 'rke2_id_2',
25
- name: 'rke2_name_2'
24
+ clusterName: RKE2_CLUSTER_NAME,
25
+ type: CAPI.RANCHER_CLUSTER,
26
+ created: 'Thu Jul 20 2022 11:11:39',
27
+ restoreEnabled: true,
28
+ snapshotFile: { status: STATES_ENUM.SUCCESSFUL },
29
+ id: 'rke2_id_2',
30
+ name: 'rke2_name_2'
26
31
  };
27
32
  const RKE2_FAILED_SNAPSHOT = {
28
- clusterName: RKE2_CLUSTER_NAME,
29
- type: CAPI.RANCHER_CLUSTER,
30
- created: 'Thu Jul 20 2021 11:11:39',
31
- snapshotFile: { status: STATES_ENUM.FAILED },
32
- id: 'rke2_id_3',
33
- name: 'rke2_name_3'
33
+ clusterName: RKE2_CLUSTER_NAME,
34
+ type: CAPI.RANCHER_CLUSTER,
35
+ created: 'Thu Jul 20 2021 11:11:39',
36
+ restoreEnabled: false,
37
+ snapshotFile: { status: STATES_ENUM.FAILED },
38
+ id: 'rke2_id_3',
39
+ name: 'rke2_name_3'
34
40
  };
35
41
 
36
42
  describe('component: PromptRestore', () => {
43
+ beforeEach(() => {
44
+ jest.clearAllMocks();
45
+ });
46
+
37
47
  const rke2TestCases = [
38
48
  [[], 0],
39
49
  [[RKE2_FAILED_SNAPSHOT], 0],
@@ -72,4 +82,144 @@ describe('component: PromptRestore', () => {
72
82
 
73
83
  expect(wrapper.vm.clusterSnapshots).toHaveLength(expected);
74
84
  });
85
+
86
+ it('should restore imported cluster via operation CR', async() => {
87
+ (createOperationCR as jest.Mock).mockResolvedValue(undefined);
88
+ const clusterSave = jest.fn();
89
+ const buttonDone = jest.fn();
90
+
91
+ const importedCluster = {
92
+ isImported: true,
93
+ isImportedWithDayTwoOps: true,
94
+ type: CAPI.RANCHER_CLUSTER,
95
+ metadata: { name: 'imported-cluster' },
96
+ mgmt: { id: 'c-m-imported' },
97
+ save: clusterSave,
98
+ };
99
+
100
+ const getters: any = {};
101
+
102
+ getters['i18n/t'] = () => (key: string) => key;
103
+
104
+ const store = createStore({
105
+ modules: {
106
+ 'action-menu': {
107
+ namespaced: true,
108
+ state: {
109
+ showPromptRestore: true,
110
+ toRestore: [importedCluster]
111
+ },
112
+ mutations: { togglePromptRestore: jest.fn() }
113
+ },
114
+ },
115
+ getters,
116
+ actions: {
117
+ 'management/findAll': jest.fn().mockResolvedValue([]),
118
+ 'growl/success': jest.fn(),
119
+ }
120
+ });
121
+
122
+ const wrapper = shallowMount(
123
+ PromptRestore as unknown as ExtendedVue<Vue, {}, {}, {}, DefaultProps>,
124
+ { global: { mocks: { $store: store } } }
125
+ );
126
+
127
+ wrapper.vm.allSnapshots = {
128
+ 'snapshot-1': {
129
+ name: 'snapshot-1',
130
+ snapshotFile: { name: 'snapshot-file-1' }
131
+ }
132
+ };
133
+ wrapper.vm.selectedSnapshot = 'snapshot-1';
134
+
135
+ await wrapper.vm.apply(buttonDone);
136
+
137
+ expect(createOperationCR).toHaveBeenCalledTimes(1);
138
+ expect((createOperationCR as jest.Mock).mock.calls[0][1]).toBe(OPERATION.ETCD_SNAPSHOT_RESTORE);
139
+ expect((createOperationCR as jest.Mock).mock.calls[0][2]).toStrictEqual({
140
+ clusterRef: {
141
+ apiVersion: 'management.cattle.io/v3',
142
+ kind: 'Cluster',
143
+ name: 'c-m-imported',
144
+ },
145
+ args: { name: 'snapshot-file-1' },
146
+ });
147
+ expect((createOperationCR as jest.Mock).mock.calls[0][3]).toBe('c-m-imported');
148
+ expect((createOperationCR as jest.Mock).mock.calls[0][4]).toBe('c-m-imported');
149
+ expect(clusterSave).not.toHaveBeenCalled();
150
+ expect(buttonDone).toHaveBeenCalledWith(true);
151
+ });
152
+
153
+ it('should restore imported snapshot by resolving target cluster from store', async() => {
154
+ (createOperationCR as jest.Mock).mockResolvedValue(undefined);
155
+ const buttonDone = jest.fn();
156
+ const byId = jest.fn();
157
+
158
+ const importedCluster = {
159
+ id: 'fleet-default/imported-cluster',
160
+ isImportedWithDayTwoOps: true,
161
+ mgmt: { id: 'c-m-imported' },
162
+ isImported: true,
163
+ };
164
+
165
+ byId.mockImplementation((type: string, id: string) => {
166
+ if (type === CAPI.RANCHER_CLUSTER && id === 'fleet-default/imported-cluster') {
167
+ return importedCluster;
168
+ }
169
+
170
+ if (type === MANAGEMENT.CLUSTER && id === 'c-m-imported') {
171
+ return importedCluster.mgmt;
172
+ }
173
+
174
+ return null;
175
+ });
176
+
177
+ const getters: any = {};
178
+
179
+ getters['i18n/t'] = () => (key: string) => key;
180
+ getters['management/byId'] = () => byId;
181
+
182
+ const store = createStore({
183
+ modules: {
184
+ 'action-menu': {
185
+ namespaced: true,
186
+ state: {
187
+ showPromptRestore: true,
188
+ toRestore: [{
189
+ type: SNAPSHOT,
190
+ metadata: { namespace: 'fleet-default' },
191
+ spec: { clusterName: 'imported-cluster', clusterRef: { name: 'c-m-imported' } },
192
+ snapshotFile: { name: 'snapshot-file-2' },
193
+ nameDisplay: 'snapshot-2',
194
+ }]
195
+ },
196
+ mutations: { togglePromptRestore: jest.fn() }
197
+ },
198
+ },
199
+ getters,
200
+ actions: { 'growl/success': jest.fn() }
201
+ });
202
+
203
+ const wrapper = shallowMount(
204
+ PromptRestore as unknown as ExtendedVue<Vue, {}, {}, {}, DefaultProps>,
205
+ { global: { mocks: { $store: store } } }
206
+ );
207
+
208
+ await wrapper.vm.apply(buttonDone);
209
+
210
+ expect(createOperationCR).toHaveBeenCalledTimes(1);
211
+ expect((createOperationCR as jest.Mock).mock.calls[0][1]).toBe(OPERATION.ETCD_SNAPSHOT_RESTORE);
212
+ expect((createOperationCR as jest.Mock).mock.calls[0][2]).toStrictEqual({
213
+ clusterRef: {
214
+ apiVersion: 'management.cattle.io/v3',
215
+ kind: 'Cluster',
216
+ name: 'c-m-imported',
217
+ },
218
+ args: { name: 'snapshot-file-2' },
219
+ });
220
+ expect((createOperationCR as jest.Mock).mock.calls[0][3]).toBe('c-m-imported');
221
+ expect((createOperationCR as jest.Mock).mock.calls[0][4]).toBe('c-m-imported');
222
+ expect(buttonDone).toHaveBeenCalledWith(true);
223
+ expect(byId).toHaveBeenCalledWith(CAPI.RANCHER_CLUSTER, 'fleet-default/imported-cluster');
224
+ });
75
225
  });
@@ -157,6 +157,7 @@ const validatePollingInterval = () => {
157
157
  label-key="fleet.gitRepo.auth.git"
158
158
  :cache-secrets="true"
159
159
  :show-ssh-known-hosts="true"
160
+ :allow-github-app="true"
160
161
  :is-github-dot-com-repository="isGithubDotComRepository"
161
162
  @update:value="updateAuth($event, 'clientSecretName')"
162
163
  @inputauthval="updateCachedAuthVal($event, 'clientSecretName')"
@@ -14,6 +14,10 @@ defineProps({
14
14
  isView: {
15
15
  type: Boolean,
16
16
  default: false
17
+ },
18
+ nameRules: {
19
+ type: Array,
20
+ default: () => []
17
21
  }
18
22
  });
19
23
 
@@ -31,6 +35,7 @@ const updateValue = (event) => {
31
35
  :value="value"
32
36
  :namespaced="false"
33
37
  :mode="mode"
38
+ :rules="{ name: nameRules }"
34
39
  @update:value="updateValue"
35
40
  />
36
41
  <Labels
@@ -50,6 +50,7 @@ const props = withDefaults(defineProps<{
50
50
  hideTarget?: boolean;
51
51
  hideAdvanced?: boolean;
52
52
  hideChartConfig?: boolean;
53
+ nameRules?: ((val: any) => string | undefined)[];
53
54
  }>(), {
54
55
  appCoChartEntries: () => ({} as Record<string, ChartEntry[]>),
55
56
  appCoChartsLoading: false,
@@ -69,6 +70,7 @@ const props = withDefaults(defineProps<{
69
70
  hideTarget: false,
70
71
  hideAdvanced: false,
71
72
  hideChartConfig: false,
73
+ nameRules: () => [],
72
74
  });
73
75
 
74
76
  // eslint-disable-next-line func-call-spacing
@@ -309,6 +311,7 @@ defineExpose({ refreshYamlEditor });
309
311
  :mode="mode"
310
312
  :name-label="'fleet.helmOp.appCoConfig.name'"
311
313
  :no-bottom-margin="true"
314
+ :rules="{ name: nameRules }"
312
315
  data-testid="appco-config-name-ns-description"
313
316
  @update:value="emit('update:value', $event)"
314
317
  />
@@ -354,6 +357,7 @@ defineExpose({ refreshYamlEditor });
354
357
  :namespaced="false"
355
358
  :mode="mode"
356
359
  :name-label="'fleet.helmOp.appCoConfig.name'"
360
+ :rules="{ name: nameRules }"
357
361
  data-testid="appco-config-name-ns-description"
358
362
  @update:value="emit('update:value', $event)"
359
363
  />
@@ -14,6 +14,10 @@ defineProps({
14
14
  isView: {
15
15
  type: Boolean,
16
16
  default: false
17
+ },
18
+ nameRules: {
19
+ type: Array,
20
+ default: () => []
17
21
  }
18
22
  });
19
23
 
@@ -31,6 +35,7 @@ const updateValue = (value) => {
31
35
  :value="value"
32
36
  :namespaced="false"
33
37
  :mode="mode"
38
+ :rules="{ name: nameRules }"
34
39
  @update:value="updateValue"
35
40
  />
36
41
  <Labels
@@ -1,6 +1,7 @@
1
1
  <script>
2
2
  import { _EDIT, _VIEW } from '@shell/config/query-params';
3
3
  import { set } from '@shell/utils/object';
4
+ import { RcButton } from '@components/RcButton';
4
5
 
5
6
  export function createOnSelected(field) {
6
7
  return function(contents) {
@@ -11,6 +12,8 @@ export function createOnSelected(field) {
11
12
  export default {
12
13
  emits: ['error', 'selected'],
13
14
 
15
+ components: { RcButton },
16
+
14
17
  props: {
15
18
  label: {
16
19
  type: String,
@@ -70,6 +73,15 @@ export default {
70
73
  class: {
71
74
  type: [String, Array],
72
75
  default: () => [],
76
+ },
77
+
78
+ /**
79
+ * Render the trigger as a small, secondary RcButton instead of the default
80
+ * plain button.
81
+ */
82
+ asRcButton: {
83
+ type: Boolean,
84
+ default: false,
73
85
  }
74
86
 
75
87
  },
@@ -82,6 +94,11 @@ export default {
82
94
  customClass() {
83
95
  return ['file-selector', 'btn', ...(Array.isArray(this.class) ? this.class : [this.class])];
84
96
  },
97
+
98
+ // RcButton provides its own `btn` class, so we omit it here to avoid clashing styles
99
+ rcButtonClass() {
100
+ return ['file-selector', ...(Array.isArray(this.class) ? this.class : [this.class])];
101
+ },
85
102
  },
86
103
 
87
104
  methods: {
@@ -154,8 +171,29 @@ export default {
154
171
  </script>
155
172
 
156
173
  <template>
174
+ <RcButton
175
+ v-if="!isView && asRcButton"
176
+ variant="secondary"
177
+ size="small"
178
+ :disabled="disabled"
179
+ :aria-label="label"
180
+ :class="rcButtonClass"
181
+ data-testid="file-selector__uploader-button"
182
+ @click="selectFile"
183
+ >
184
+ <span>{{ label }}</span>
185
+ <input
186
+ ref="uploader"
187
+ type="file"
188
+ class="hide"
189
+ :multiple="multiple"
190
+ :webkitdirectory="directory"
191
+ :accept="accept"
192
+ @change="fileChange"
193
+ >
194
+ </RcButton>
157
195
  <button
158
- v-if="!isView"
196
+ v-else-if="!isView"
159
197
  :disabled="disabled"
160
198
  :aria-label="label"
161
199
  type="button"
@@ -0,0 +1,7 @@
1
+ export const PRIVATE_REGISTRY_CONTEXT = {
2
+ PROVISIONING: 'provisioning',
3
+ IMPORTING: 'importing',
4
+ CHARTS: 'charts',
5
+ } as const;
6
+
7
+ export type PrivateRegistryContext = typeof PRIVATE_REGISTRY_CONTEXT[keyof typeof PRIVATE_REGISTRY_CONTEXT];