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

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 (159) hide show
  1. package/apis/intf/resources-api/resource-base.ts +42 -2
  2. package/apis/intf/resources-api/resource-instance.ts +101 -0
  3. package/apis/intf/resources-api/resources-api.ts +177 -23
  4. package/apis/intf/resources.ts +8 -6
  5. package/apis/resources/__tests__/resources-api-class.test.ts +309 -28
  6. package/apis/resources/resources-api-class.ts +232 -43
  7. package/assets/images/providers/traefik.png +0 -0
  8. package/assets/styles/global/_button.scss +1 -1
  9. package/assets/translations/en-us.yaml +49 -14
  10. package/chart/__tests__/rancher-monitoring-dashboards-index.test.ts +269 -0
  11. package/chart/rancher-monitoring-dashboards/index.vue +155 -0
  12. package/cloud-credential/__tests__/generic.test.ts +132 -0
  13. package/cloud-credential/generic.vue +33 -3
  14. package/components/ActionDropdownShell.vue +5 -3
  15. package/components/ButtonGroup.vue +26 -1
  16. package/components/CruResource.vue +51 -2
  17. package/components/EtcdInfoBanner.vue +2 -12
  18. package/components/GrafanaDashboard.vue +2 -8
  19. package/components/PromptRestore.vue +93 -32
  20. package/components/Questions/index.vue +1 -0
  21. package/components/Resource/Detail/Masthead/__tests__/index.test.ts +68 -14
  22. package/components/Resource/Detail/Masthead/index.vue +8 -3
  23. package/components/ResourceTable.vue +1 -0
  24. package/components/SortableTable/index.vue +4 -3
  25. package/components/Wizard.vue +14 -1
  26. package/components/__tests__/ButtonGroup.test.ts +56 -0
  27. package/components/__tests__/PromptRestore.test.ts +169 -19
  28. package/components/fleet/GitRepoAdvancedTab.vue +1 -0
  29. package/components/fleet/GitRepoMetadataTab.vue +5 -0
  30. package/components/fleet/HelmOpAppCoConfigTab.vue +4 -0
  31. package/components/fleet/HelmOpMetadataTab.vue +5 -0
  32. package/components/form/FileSelector.vue +39 -1
  33. package/components/form/PrivateRegistry.constants.ts +7 -0
  34. package/components/form/PrivateRegistry.vue +253 -18
  35. package/components/form/Security.vue +1 -1
  36. package/components/form/SelectOrCreateAuthSecret.vue +140 -17
  37. package/components/form/__tests__/FileSelector.test.ts +23 -0
  38. package/components/form/__tests__/PrivateRegistry.test.ts +463 -73
  39. package/components/form/__tests__/SelectOrCreateAuthSecret.test.ts +122 -0
  40. package/components/formatter/EtcdSnapshotName.vue +73 -0
  41. package/components/formatter/Translate.vue +22 -1
  42. package/components/nav/Group.vue +5 -10
  43. package/components/nav/Header.vue +8 -1
  44. package/components/nav/TopLevelMenu.vue +1 -30
  45. package/components/nav/__tests__/Group.test.ts +61 -0
  46. package/components/templates/default.vue +7 -0
  47. package/config/features.js +1 -0
  48. package/config/labels-annotations.js +3 -0
  49. package/config/product/manager.js +6 -0
  50. package/config/secret.ts +10 -0
  51. package/config/settings.ts +6 -2
  52. package/config/table-headers.js +5 -2
  53. package/config/types.js +7 -0
  54. package/core/__tests__/plugin-products-apply.test.ts +787 -0
  55. package/core/__tests__/plugin-products-extend.test.ts +189 -0
  56. package/core/__tests__/plugin-products-helpers.test.ts +81 -78
  57. package/core/__tests__/plugin-products-new.test.ts +389 -0
  58. package/core/__tests__/plugin-products-scenarios.test.ts +980 -0
  59. package/core/__tests__/plugin-products-side-menu.test.ts +1935 -0
  60. package/core/plugin-products-base.ts +201 -93
  61. package/core/plugin-products-extending.ts +1 -1
  62. package/core/plugin-products-external.ts +380 -0
  63. package/core/plugin-products-helpers.ts +28 -24
  64. package/core/plugin-products-internal.ts +207 -0
  65. package/core/plugin-products-top-level.ts +10 -5
  66. package/core/plugin-products-type-guards.ts +17 -6
  67. package/core/plugin-products.ts +5 -7
  68. package/core/plugin-types.ts +1 -389
  69. package/core/plugin.ts +10 -11
  70. package/core/types.ts +6 -202
  71. package/detail/__tests__/provisioning.cattle.io.cluster.test.ts +52 -0
  72. package/detail/pod.vue +1 -1
  73. package/detail/provisioning.cattle.io.cluster.vue +79 -5
  74. package/dialog/RotateEncryptionKeyDialog.vue +33 -9
  75. package/dialog/__tests__/RotateEncryptionKeyDialog.test.ts +78 -0
  76. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +92 -0
  77. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +101 -0
  78. package/edit/__tests__/management.cattle.io.setting.test.ts +2 -1
  79. package/edit/auth/AuthProviderWarningBanners.vue +12 -0
  80. package/edit/auth/__tests__/AuthProviderWarningBanners.test.ts +10 -1
  81. package/edit/auth/__tests__/azuread.test.ts +1 -0
  82. package/edit/auth/__tests__/github.test.ts +1 -0
  83. package/edit/auth/__tests__/oidc.test.ts +1 -0
  84. package/edit/auth/__tests__/saml.test.ts +1 -0
  85. package/edit/auth/ldap/__tests__/index.test.ts +1 -0
  86. package/edit/compliance.cattle.io.clusterscanprofile.vue +39 -41
  87. package/edit/fleet.cattle.io.gitrepo.vue +70 -16
  88. package/edit/fleet.cattle.io.helmop.vue +51 -5
  89. package/edit/helm.cattle.io.projecthelmchart.vue +1 -0
  90. package/edit/{management.cattle.io.setting.vue → management.cattle.io.setting/index.vue} +32 -9
  91. package/edit/management.cattle.io.setting/system-default-registry-pull-secrets.vue +81 -0
  92. package/edit/provisioning.cattle.io.cluster/SelectCredential.vue +3 -12
  93. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +18 -0
  94. package/edit/provisioning.cattle.io.cluster/index.vue +1 -1
  95. package/edit/provisioning.cattle.io.cluster/rke2.vue +6 -2
  96. package/edit/provisioning.cattle.io.cluster/tabs/etcd/index.vue +0 -1
  97. package/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue +2 -2
  98. package/edit/provisioning.cattle.io.cluster/tabs/registries/index.vue +14 -55
  99. package/list/management.cattle.io.user.vue +24 -5
  100. package/list/utils/management.cattle.io.cluster.utils.ts +1 -1
  101. package/machine-config/__tests__/generic.test.ts +163 -0
  102. package/machine-config/components/EC2Networking.vue +31 -7
  103. package/machine-config/components/__tests__/EC2Networking.test.ts +38 -15
  104. package/machine-config/generic.vue +32 -5
  105. package/mixins/resource-fetch.js +1 -1
  106. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +156 -0
  107. package/models/__tests__/rke-machine.cattle.io.vmwarevspheremachinetemplate.test.ts +22 -0
  108. package/models/__tests__/secret.test.ts +68 -1
  109. package/models/management.cattle.io.cluster.js +21 -3
  110. package/models/pod.js +25 -12
  111. package/models/provisioning.cattle.io.cluster.js +59 -9
  112. package/models/rke-machine.cattle.io.vmwarevspheremachinetemplate.js +9 -0
  113. package/models/rke.cattle.io.etcdsnapshot.js +17 -9
  114. package/models/secret.js +19 -0
  115. package/models/workload.js +12 -7
  116. package/package.json +1 -1
  117. package/pages/auth/login.vue +1 -1
  118. package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +569 -81
  119. package/pages/c/_cluster/apps/charts/install.vue +143 -28
  120. package/pages/c/_cluster/auth/config/index.vue +11 -4
  121. package/pages/c/_cluster/monitoring/index.vue +6 -1
  122. package/pkg/require-asset.lib.js +25 -0
  123. package/pkg/vue.config.js +7 -0
  124. package/plugins/dashboard-store/__tests__/resource-class.test.ts +84 -0
  125. package/plugins/dashboard-store/getters.js +0 -1
  126. package/plugins/dashboard-store/resource-class.js +62 -15
  127. package/plugins/steve/__tests__/steve-class-resource-api.test.ts +147 -0
  128. package/plugins/steve/steve-class.js +43 -0
  129. package/plugins/steve/subscribe.js +11 -0
  130. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +115 -0
  131. package/rancher-components/Form/LabeledInput/LabeledInput.vue +83 -3
  132. package/rancher-components/Form/TextArea/TextAreaAutoGrow.vue +30 -0
  133. package/rancher-components/Form/TextArea/__tests__/TextAreaAutoGrow.test.ts +95 -0
  134. package/rancher-components/RcButton/index.ts +1 -1
  135. package/rancher-components/RcDropdown/RcDropdownTrigger.vue +6 -1
  136. package/store/__tests__/auth.test.ts +18 -0
  137. package/store/__tests__/features.test.ts +131 -0
  138. package/store/__tests__/growl.test.ts +374 -0
  139. package/store/__tests__/modal.test.ts +131 -0
  140. package/store/__tests__/notifications.test.ts +466 -1
  141. package/store/__tests__/slideInPanel.test.ts +88 -0
  142. package/store/__tests__/type-map.utils.test.ts +433 -0
  143. package/store/__tests__/ui-context.test.ts +255 -0
  144. package/store/auth.js +11 -1
  145. package/store/features.js +5 -0
  146. package/store/plugins.js +6 -0
  147. package/types/rancher/steve.api.ts +2 -2
  148. package/types/shell/index.d.ts +120 -0
  149. package/types/store/dashboard-store.types.ts +2 -2
  150. package/types/store/type-map.ts +262 -1
  151. package/utils/__tests__/operation-cr.test.ts +34 -0
  152. package/utils/async.ts +2 -0
  153. package/utils/grafana.js +2 -17
  154. package/utils/monitoring.js +38 -1
  155. package/utils/operation-cr.js +19 -0
  156. package/utils/require-asset.ts +7 -0
  157. package/utils/validators/__tests__/private-registry.test.ts +27 -15
  158. package/utils/validators/private-registry.ts +15 -4
  159. package/core/__tests__/plugin-products.test.ts +0 -4694
@@ -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];
@@ -1,24 +1,185 @@
1
1
  <script setup lang="ts">
2
- import { ref, watch } from 'vue';
2
+ import { ref, watch, onMounted, computed } from 'vue';
3
+ import { useStore } from 'vuex';
4
+ import { useI18n } from '@shell/composables/useI18n';
3
5
  import Banner from '@components/Banner/Banner.vue';
4
6
  import { Checkbox } from '@components/Form/Checkbox';
5
7
  import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
8
+ import SelectOrCreateAuthSecret from '@shell/components/form/SelectOrCreateAuthSecret.vue';
9
+ import { MANAGEMENT } from '@shell/config/types';
10
+ import { SETTING } from '@shell/config/settings';
11
+ import { PRIVATE_REGISTRY_CONTEXT } from '@shell/components/form/PrivateRegistry.constants';
12
+ import type { PrivateRegistryContext } from '@shell/components/form/PrivateRegistry.constants';
6
13
 
7
- const props = defineProps<{
14
+ const props = withDefaults(defineProps<{
8
15
  value?: string | null;
9
16
  enabled?: boolean;
10
17
  mode?: string;
11
18
  rules?: Function[];
12
19
  checkboxTestId?: string;
13
20
  inputTestId?: string;
14
- }>();
21
+ pullSecret?: string;
22
+ registerBeforeHook?: Function;
23
+ context?: PrivateRegistryContext;
24
+ defaultRegistry?: string;
25
+ namespace?: string;
26
+ inStore?: string;
27
+ showPullSecrets?: boolean;
28
+ noneLabel?: string | null;
29
+ skipPullSecrets?: boolean;
30
+ repoDefaultPullSecrets?: string[];
31
+ existingValuesPullSecrets?: string[];
32
+ }>(), {
33
+ value: undefined,
34
+ enabled: false,
35
+ mode: 'edit',
36
+ rules: () => [],
37
+ checkboxTestId: undefined,
38
+ inputTestId: undefined,
39
+ pullSecret: undefined,
40
+ registerBeforeHook: undefined,
41
+ context: PRIVATE_REGISTRY_CONTEXT.PROVISIONING,
42
+ defaultRegistry: undefined,
43
+ namespace: 'fleet-default',
44
+ inStore: 'management',
45
+ showPullSecrets: true,
46
+ noneLabel: undefined,
47
+ skipPullSecrets: false,
48
+ repoDefaultPullSecrets: () => [],
49
+ existingValuesPullSecrets: () => [],
50
+ });
15
51
 
16
52
  const emit = defineEmits<{
17
- 'update:value': [val: string | null];
53
+ 'update:value': [val: string | undefined];
18
54
  'update:enabled': [val: boolean];
55
+ 'update:pullSecret': [val: string | undefined];
56
+ 'update:skipPullSecrets': [val: boolean];
19
57
  }>();
20
58
 
59
+ const store = useStore();
60
+ const { t } = useI18n(store);
61
+
21
62
  const showInput = ref(!!props.value);
63
+ const globalRegistry = ref('');
64
+ const defaultPullSecrets = ref<string[]>([]);
65
+ const localSkipPullSecrets = ref(props.skipPullSecrets);
66
+
67
+ const isCharts = computed(() => props.context === PRIVATE_REGISTRY_CONTEXT.CHARTS);
68
+ const isImporting = computed(() => props.context === PRIVATE_REGISTRY_CONTEXT.IMPORTING);
69
+
70
+ const descriptionKey = computed(() => {
71
+ if (isCharts.value) {
72
+ return 'catalog.chart.registry.tooltip';
73
+ }
74
+ if (isImporting.value) {
75
+ return 'cluster.privateRegistry.importedDescription';
76
+ }
77
+
78
+ return 'cluster.privateRegistry.description';
79
+ });
80
+
81
+ const checkboxLabelKey = computed(() => {
82
+ if (isCharts.value) {
83
+ return 'catalog.chart.registry.custom.checkBoxLabel';
84
+ }
85
+
86
+ return 'cluster.privateRegistry.label';
87
+ });
88
+
89
+ const checkboxTooltipKey = computed(() => {
90
+ if (isCharts.value) {
91
+ return 'catalog.chart.registry.tooltip';
92
+ }
93
+
94
+ return undefined;
95
+ });
96
+
97
+ onMounted(() => {
98
+ if (props.defaultRegistry) {
99
+ globalRegistry.value = props.defaultRegistry;
100
+ } else {
101
+ const registrySetting = store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.SYSTEM_DEFAULT_REGISTRY);
102
+
103
+ globalRegistry.value = registrySetting?.value || registrySetting?.defaultValue;
104
+ }
105
+
106
+ const pullSecretsSetting = store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.SYSTEM_DEFAULT_REGISTRY_PULL_SECRETS);
107
+
108
+ if (props.repoDefaultPullSecrets?.length) {
109
+ defaultPullSecrets.value = props.repoDefaultPullSecrets;
110
+ } else if (pullSecretsSetting?.value ) {
111
+ defaultPullSecrets.value = pullSecretsSetting?.value.split(',').map((s: string) => s.trim());
112
+ }
113
+ });
114
+
115
+ const hasMultipleDefaultSecrets = computed(() => {
116
+ return defaultPullSecrets.value?.length > 1;
117
+ });
118
+
119
+ /**
120
+ * On upgrade, if the chart values already contain multiple imagePullSecrets,
121
+ * the dropdown cannot represent them. Show a banner instead and let the
122
+ * user edit the values YAML directly.
123
+ */
124
+ const hasMultipleExistingPullSecrets = computed(() => {
125
+ return (props.existingValuesPullSecrets?.length ?? 0) > 1;
126
+ });
127
+
128
+ /**
129
+ * Format a list of names with commas and "and" before the last item.
130
+ * e.g. ["a"] => "<code>a</code>"
131
+ * ["a", "b"] => "<code>a</code> and <code>b</code>"
132
+ * ["a", "b", "c"] => "<code>a</code>, <code>b</code>, and <code>c</code>"
133
+ */
134
+ function formatSecretList(names: string[]): string {
135
+ const wrapped = names.map((n) => `<code>${ n }</code>`);
136
+
137
+ const and = t('generic.and');
138
+
139
+ if (wrapped.length <= 1) {
140
+ return wrapped[0] || '';
141
+ }
142
+ if (wrapped.length === 2) {
143
+ return `${ wrapped[0] }${ and }${ wrapped[1] }`;
144
+ }
145
+
146
+ return `${ wrapped.slice(0, -1).join(', ') },${ and }${ wrapped[wrapped.length - 1] }`;
147
+ }
148
+
149
+ /**
150
+ * If there is exactly one default pull secret configured, we can show that it is the default in the secret dropdown.
151
+ * If there is more than one, UI can't be certain which secret will be used
152
+ * so a banner listing all secrets is shown instead.
153
+ */
154
+ const defaultPullSecretLabel = computed(() => {
155
+ if (props.noneLabel) {
156
+ return props.noneLabel;
157
+ }
158
+ if (!defaultPullSecrets.value?.length) {
159
+ return null;
160
+ }
161
+ if (defaultPullSecrets.value?.length === 1) {
162
+ return t('catalog.chart.registry.pullSecret.defaultLabel', { name: defaultPullSecrets.value[0] });
163
+ }
164
+
165
+ return t('catalog.chart.registry.pullSecret.defaultLabelGeneric');
166
+ });
167
+
168
+ const existingValuesBannerHtml = computed(() => {
169
+ if (!props.existingValuesPullSecrets?.length) {
170
+ return '';
171
+ }
172
+
173
+ return t('catalog.chart.registry.pullSecret.existingValuesBanner', { secrets: formatSecretList(props.existingValuesPullSecrets) }, true);
174
+ });
175
+
176
+ const defaultSecretsBannerHtml = computed(() => {
177
+ if (!defaultPullSecrets.value?.length) {
178
+ return '';
179
+ }
180
+
181
+ return t('catalog.chart.registry.pullSecret.defaultSecretsBanner', { secrets: formatSecretList(defaultPullSecrets.value) }, true);
182
+ });
22
183
 
23
184
  watch(() => props.enabled, (neu) => {
24
185
  if (typeof neu === 'boolean' && neu !== showInput.value) {
@@ -31,7 +192,8 @@ watch(showInput, (neu, old) => {
31
192
  emit('update:enabled', neu);
32
193
  }
33
194
  if (!neu && old && props.value) {
34
- emit('update:value', null);
195
+ emit('update:value', undefined);
196
+ emit('update:pullSecret', undefined);
35
197
  }
36
198
  });
37
199
 
@@ -40,30 +202,103 @@ watch(() => props.value, (neu) => {
40
202
  showInput.value = true;
41
203
  }
42
204
  });
205
+
206
+ watch(() => props.pullSecret, (neu, old) => {
207
+ if (neu && !props.value) {
208
+ emit('update:value', globalRegistry.value);
209
+ }
210
+ if (!neu && old && props.value === globalRegistry.value) {
211
+ emit('update:value', undefined);
212
+ }
213
+ });
214
+
215
+ watch(localSkipPullSecrets, (neu) => {
216
+ emit('update:skipPullSecrets', neu);
217
+ if (neu) {
218
+ emit('update:pullSecret', undefined);
219
+ }
220
+ });
221
+
222
+ watch(() => props.skipPullSecrets, (neu) => {
223
+ if (neu !== localSkipPullSecrets.value) {
224
+ localSkipPullSecrets.value = neu;
225
+ }
226
+ });
227
+
43
228
  </script>
44
229
 
45
230
  <template>
46
231
  <Banner
47
232
  color="info"
48
233
  class="mt-0"
49
- label-key="cluster.privateRegistry.importedDescription"
234
+ :label-key="descriptionKey"
50
235
  />
51
236
  <Checkbox
52
237
  v-model:value="showInput"
53
238
  class="mb-20"
54
239
  :mode="mode"
55
- :label="t('cluster.privateRegistry.label')"
240
+ :label="t(checkboxLabelKey)"
241
+ :tooltip="checkboxTooltipKey ? t(checkboxTooltipKey) : undefined"
56
242
  :data-testid="checkboxTestId"
57
243
  />
58
- <LabeledInput
59
- v-if="showInput"
60
- :value="value as string"
61
- :mode="mode"
62
- :rules="rules"
63
- :required="true"
64
- label-key="catalog.chart.registry.custom.inputLabel"
65
- :data-testid="inputTestId"
66
- :placeholder="t('catalog.chart.registry.custom.placeholder')"
67
- @update:value="(val) => emit('update:value', val)"
68
- />
244
+ <template v-if="showInput">
245
+ <div class="row">
246
+ <div class="col span-6">
247
+ <LabeledInput
248
+ :value="value || globalRegistry"
249
+ :mode="mode"
250
+ :rules="rules"
251
+ :required="!globalRegistry"
252
+ label-key="catalog.chart.registry.custom.inputLabel"
253
+ :data-testid="inputTestId"
254
+ :placeholder="t('catalog.chart.registry.custom.placeholder')"
255
+ @update:value="(val) => emit('update:value', val)"
256
+ />
257
+ </div>
258
+ </div>
259
+ <template v-if="showPullSecrets">
260
+ <Checkbox
261
+ v-if="isCharts"
262
+ v-model:value="localSkipPullSecrets"
263
+ class="mb-10 mt-10"
264
+ :mode="mode"
265
+ :label="t('catalog.chart.registry.pullSecret.skipOption')"
266
+ data-testid="registry-skip-pull-secrets-checkbox"
267
+ />
268
+ <Banner
269
+ v-if="hasMultipleExistingPullSecrets && !localSkipPullSecrets"
270
+ color="info"
271
+ >
272
+ <span v-clean-html="existingValuesBannerHtml" />
273
+ </Banner>
274
+ <Banner
275
+ v-if="!hasMultipleExistingPullSecrets && hasMultipleDefaultSecrets && !localSkipPullSecrets"
276
+ color="info"
277
+ >
278
+ <span v-clean-html="defaultSecretsBannerHtml" />
279
+ </Banner>
280
+ <div :class="{'col span-6': !isCharts}">
281
+ <SelectOrCreateAuthSecret
282
+ v-if="!localSkipPullSecrets && !hasMultipleExistingPullSecrets"
283
+ :value="pullSecret"
284
+ :namespace="namespace"
285
+ :allow-rke="!isCharts"
286
+ :vertical="!isCharts"
287
+ :in-store="inStore"
288
+ limit-to-namespace
289
+ fixed-image-pull-secret
290
+ :none-label="defaultPullSecretLabel"
291
+ :image-pull-secret-docker-json-url-config="value || globalRegistry"
292
+ :register-before-hook="registerBeforeHook"
293
+ @update:value="(val) => emit('update:pullSecret', val)"
294
+ />
295
+ </div>
296
+ </template>
297
+ </template>
69
298
  </template>
299
+
300
+ <style lang="scss" scoped>
301
+ :deep(.banner__content) {
302
+ display: block;
303
+ }
304
+ </style>