@rancher/shell 0.3.11 → 0.3.12

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 (87) hide show
  1. package/assets/translations/en-us.yaml +51 -5
  2. package/chart/monitoring/StorageClassSelector.vue +1 -0
  3. package/chart/monitoring/index.vue +4 -0
  4. package/chart/monitoring/prometheus/index.vue +6 -3
  5. package/components/ActionMenu.vue +1 -1
  6. package/components/DetailTop.vue +0 -2
  7. package/components/ExplorerMembers.vue +22 -10
  8. package/components/ExplorerProjectsNamespaces.vue +1 -0
  9. package/components/GrafanaDashboard.vue +2 -2
  10. package/components/Inactivity.vue +1 -0
  11. package/components/ModalWithCard.vue +1 -0
  12. package/components/Tabbed/index.vue +2 -0
  13. package/components/Wizard.vue +4 -3
  14. package/components/form/KeyValue.vue +12 -7
  15. package/components/form/NodeAffinity.vue +29 -7
  16. package/components/form/PodAffinity.vue +27 -7
  17. package/components/form/Taints.vue +6 -0
  18. package/components/formatter/ExtensionCache.vue +74 -0
  19. package/components/nav/Header.vue +1 -0
  20. package/components/nav/WindowManager/ContainerShell.vue +10 -0
  21. package/components/nav/WindowManager/index.vue +1 -0
  22. package/config/product/explorer.js +1 -10
  23. package/config/product/monitoring.js +2 -1
  24. package/config/router.js +3 -3
  25. package/config/table-headers.js +32 -24
  26. package/config/uiplugins.js +11 -0
  27. package/config/workload.ts +1 -0
  28. package/core/types.ts +25 -7
  29. package/creators/pkg/files/.github/workflows/build-container.yml +64 -0
  30. package/creators/pkg/init +13 -6
  31. package/detail/node.vue +2 -2
  32. package/detail/workload/index.vue +1 -1
  33. package/edit/__tests__/management.cattle.io.setting.test.ts +1 -1
  34. package/edit/autoscaling.horizontalpodautoscaler/metric-target.vue +0 -2
  35. package/edit/logging.banzaicloud.io.output/__tests__/logging.banzaicloud.io.output.test.ts +43 -0
  36. package/edit/logging.banzaicloud.io.output/index.vue +8 -5
  37. package/edit/logging.banzaicloud.io.output/providers/__tests__/loki.test.ts +13 -0
  38. package/edit/logging.banzaicloud.io.output/providers/loki.vue +1 -0
  39. package/edit/monitoring.coreos.com.alertmanagerconfig/receiverConfig.vue +0 -2
  40. package/edit/monitoring.coreos.com.receiver/index.vue +32 -1
  41. package/edit/monitoring.coreos.com.receiver/types/email.vue +12 -4
  42. package/edit/namespace.vue +1 -0
  43. package/edit/provisioning.cattle.io.cluster/MachinePool.vue +36 -6
  44. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +2 -2
  45. package/edit/provisioning.cattle.io.cluster/rke2.vue +58 -13
  46. package/middleware/authenticated.js +1 -0
  47. package/models/__tests__/batch.cronjob.test.ts +88 -0
  48. package/models/cluster/node.js +8 -0
  49. package/models/management.cattle.io.clusterroletemplatebinding.js +5 -1
  50. package/models/projectroletemplatebinding.js +9 -1
  51. package/models/workload.js +1 -1
  52. package/package.json +1 -1
  53. package/pages/__tests__/prefs.test.ts +96 -0
  54. package/pages/auth/setup.vue +13 -13
  55. package/pages/c/_cluster/apps/charts/chart.vue +1 -1
  56. package/pages/c/_cluster/apps/charts/install.vue +5 -2
  57. package/pages/c/_cluster/monitoring/index.vue +10 -5
  58. package/pages/c/_cluster/settings/performance.vue +2 -0
  59. package/pages/c/_cluster/uiplugins/CatalogList/CatalogLoadDialog.vue +601 -0
  60. package/pages/c/_cluster/uiplugins/CatalogList/index.vue +183 -0
  61. package/pages/c/_cluster/uiplugins/UninstallDialog.vue +50 -9
  62. package/pages/c/_cluster/uiplugins/index.vue +329 -224
  63. package/pages/fail-whale.vue +1 -1
  64. package/pages/home.vue +11 -0
  65. package/pages/prefs.vue +20 -1
  66. package/plugins/plugin.js +1 -1
  67. package/public/index.html +6 -1
  68. package/rancher-components/components/Card/Card.vue +1 -0
  69. package/rancher-components/components/Form/Radio/RadioGroup.vue +1 -0
  70. package/scripts/extension/bundle +20 -4
  71. package/scripts/extension/helm/charts/ui-plugin-server/.helmignore +23 -0
  72. package/scripts/extension/helm/charts/ui-plugin-server/Chart.yaml +20 -0
  73. package/scripts/extension/helm/charts/ui-plugin-server/templates/_helpers.tpl +52 -0
  74. package/scripts/extension/helm/charts/ui-plugin-server/templates/cr.yaml +12 -0
  75. package/scripts/extension/helm/charts/ui-plugin-server/values.yaml +6 -0
  76. package/scripts/extension/helm/package/Dockerfile +27 -0
  77. package/scripts/extension/helm/package/nginx.conf +17 -0
  78. package/scripts/extension/helm/scripts/package +23 -0
  79. package/scripts/extension/helm/scripts/patch +101 -0
  80. package/scripts/extension/helm/scripts/version +31 -0
  81. package/scripts/extension/helmpatch +3 -25
  82. package/scripts/extension/publish +47 -32
  83. package/types/shell/index.d.ts +30 -24
  84. package/utils/__tests__/grafana.test.ts +2 -2
  85. package/utils/error.js +11 -0
  86. package/utils/grafana.js +5 -4
  87. package/vue.config.js +3 -17
@@ -1120,6 +1120,7 @@ export default {
1120
1120
  Refer to the developer docs at docs/developer/helm-chart-apps.md
1121
1121
  for details on what values are injected and where they come from.
1122
1122
  */
1123
+
1123
1124
  this.addGlobalValuesTo(values);
1124
1125
 
1125
1126
  const form = JSON.parse(JSON.stringify(this.value));
@@ -1510,6 +1511,7 @@ export default {
1510
1511
  <div class="step__values__controls">
1511
1512
  <ButtonGroup
1512
1513
  v-model="preFormYamlOption"
1514
+ data-testid="btn-group-options-view"
1513
1515
  :options="formYamlOptions"
1514
1516
  inactive-class="bg-disabled btn-sm"
1515
1517
  active-class="bg-primary btn-sm"
@@ -1979,7 +1981,7 @@ export default {
1979
1981
  display: flex;
1980
1982
  flex-direction: column;
1981
1983
  padding: 0;
1982
- height: calc(100% - 112px);
1984
+ overflow: auto;
1983
1985
  }
1984
1986
 
1985
1987
  .header {
@@ -2052,10 +2054,11 @@ export default {
2052
2054
  }
2053
2055
 
2054
2056
  .os-label {
2055
- position: absolute;
2057
+ position: relative;
2056
2058
  background-color: var(--warning-banner-bg);
2057
2059
  color:var(--warning);
2058
2060
  margin-top: 5px;
2061
+ top: 21px;
2059
2062
  }
2060
2063
 
2061
2064
  </style>
@@ -4,10 +4,10 @@ import isEmpty from 'lodash/isEmpty';
4
4
  import InstallRedirect from '@shell/utils/install-redirect';
5
5
  import AlertTable from '@shell/components/AlertTable';
6
6
  import { NAME, CHART_NAME } from '@shell/config/product/monitoring';
7
- import { ENDPOINTS, MONITORING } from '@shell/config/types';
7
+ import { CATALOG, ENDPOINTS, MONITORING } from '@shell/config/types';
8
8
  import { allHash } from '@shell/utils/promise';
9
9
  import { findBy } from '@shell/utils/array';
10
-
10
+ import { getClusterPrefix } from '@shell/utils/grafana';
11
11
  import { Banner } from '@components/Banner';
12
12
  import LazyImage from '@shell/components/LazyImage';
13
13
  import SimpleBox from '@shell/components/SimpleBox';
@@ -58,7 +58,7 @@ export default {
58
58
  iconSrc: grafanaSrc,
59
59
  label: 'monitoring.overview.linkedList.grafana.label',
60
60
  description: 'monitoring.overview.linkedList.grafana.description',
61
- link: `/k8s/clusters/${ currentCluster.id }/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy`,
61
+ link: '',
62
62
  },
63
63
  {
64
64
  enabled: false,
@@ -94,10 +94,13 @@ export default {
94
94
  methods: {
95
95
  async fetchDeps() {
96
96
  const { $store, externalLinks } = this;
97
+ const currentCluster = this.$store.getters['currentCluster'];
97
98
 
98
99
  this.v1Installed = await haveV1MonitoringWorkloads($store);
99
-
100
- const hash = await allHash({ endpoints: $store.dispatch('cluster/findAll', { type: ENDPOINTS }) });
100
+ const hash = await allHash({
101
+ endpoints: $store.dispatch('cluster/findAll', { type: ENDPOINTS }),
102
+ app: $store.dispatch(`cluster/find`, { type: CATALOG.APP, id: 'cattle-monitoring-system/rancher-monitoring' })
103
+ });
101
104
 
102
105
  if (!isEmpty(hash.endpoints)) {
103
106
  const amMatch = findBy(externalLinks, 'group', 'alertmanager');
@@ -105,6 +108,8 @@ export default {
105
108
  const promeMatch = externalLinks.filter(
106
109
  el => el.group === 'prometheus'
107
110
  );
111
+
112
+ grafanaMatch.link = `${ getClusterPrefix(hash.app?.currentVersion || '', currentCluster.id) }/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/`;
108
113
  const alertmanager = findBy(
109
114
  hash.endpoints,
110
115
  'id',
@@ -174,6 +174,7 @@ export default {
174
174
  <div class="ml-20">
175
175
  <LabeledInput
176
176
  v-model="value.inactivity.threshold"
177
+ data-testid="inactivity-threshold"
177
178
  :mode="mode"
178
179
  :label="t('performance.inactivity.inputLabel')"
179
180
  :disabled="!value.inactivity.enabled"
@@ -382,6 +383,7 @@ export default {
382
383
  </template>
383
384
  <div v-if="mode === 'edit'">
384
385
  <AsyncButton
386
+ data-testid="performance__save-btn"
385
387
  class="pull-right mt-20"
386
388
  mode="apply"
387
389
  :disabled="!canSave"
@@ -0,0 +1,601 @@
1
+ <script>
2
+ import { mapGetters } from 'vuex';
3
+ import isEmpty from 'lodash/isEmpty';
4
+
5
+ import {
6
+ CATALOG, SECRET, SERVICE, UI_PLUGIN, WORKLOAD_TYPES
7
+ } from '@shell/config/types';
8
+ import { UI_PLUGIN_LABELS, UI_PLUGIN_NAMESPACE } from '@shell/config/uiplugins';
9
+ import { TYPES as SECRET_TYPES } from '@shell/models/secret';
10
+ import { allHash } from '@shell/utils/promise';
11
+
12
+ import ResourceManager from '@shell/mixins/resource-manager';
13
+
14
+ import AsyncButton from '@shell/components/AsyncButton';
15
+ import LabeledSelect from '@shell/components/form/LabeledSelect';
16
+ import Loading from '@shell/components/Loading.vue';
17
+ import { Banner } from '@components/Banner';
18
+ import { LabeledInput } from '@components/Form/LabeledInput';
19
+
20
+ const DEFAULT_DEPLOYMENT = {
21
+ type: WORKLOAD_TYPES.DEPLOYMENT,
22
+ metadata: {
23
+ name: '',
24
+ namespace: UI_PLUGIN_NAMESPACE,
25
+ labels: {}
26
+ },
27
+ spec: {
28
+ replicas: 1,
29
+ selector: { matchLabels: {} },
30
+ template: {
31
+ metadata: {
32
+ namespace: UI_PLUGIN_NAMESPACE,
33
+ labels: {}
34
+ },
35
+ spec: {
36
+ containers: [
37
+ {
38
+ image: '',
39
+ imagePullPolicy: 'Always',
40
+ name: 'container-0'
41
+ }
42
+ ],
43
+ imagePullSecrets: [],
44
+ restartPolicy: 'Always'
45
+ }
46
+ }
47
+ }
48
+ };
49
+
50
+ const DEFAULT_SERVICE = {
51
+ type: SERVICE,
52
+ metadata: {
53
+ name: '',
54
+ namespace: UI_PLUGIN_NAMESPACE,
55
+ labels: { [UI_PLUGIN_LABELS.CATALOG_IMAGE]: '' }
56
+ },
57
+ spec: {
58
+ ports: [
59
+ {
60
+ name: '',
61
+ port: 8080,
62
+ protocol: 'TCP',
63
+ targetPort: 8080
64
+ }
65
+ ],
66
+ selector: { [UI_PLUGIN_LABELS.CATALOG_IMAGE]: '' },
67
+ type: 'ClusterIP'
68
+ }
69
+ };
70
+
71
+ const DEFAULT_REPO = {
72
+ type: CATALOG.CLUSTER_REPO,
73
+ metadata: {
74
+ name: '',
75
+ labels: { [UI_PLUGIN_LABELS.CATALOG_IMAGE]: '' }
76
+ },
77
+ spec: { url: null }
78
+ };
79
+
80
+ const initialState = () => {
81
+ const deploymentValues = structuredClone(DEFAULT_DEPLOYMENT);
82
+ const serviceValues = structuredClone(DEFAULT_SERVICE);
83
+ const repoValues = structuredClone(DEFAULT_REPO);
84
+
85
+ return {
86
+ deploymentValues,
87
+ serviceValues,
88
+ repoValues,
89
+ canModifyName: true,
90
+ canModifyImage: true,
91
+ imagePullSecrets: [],
92
+ imagePullNamespacedSecrets: [],
93
+ extensionUrl: null,
94
+ extensionDeployment: null,
95
+ extensionSvc: null,
96
+ extensionRepo: null,
97
+ extensionCrd: null
98
+ };
99
+ };
100
+
101
+ export default {
102
+ components: {
103
+ AsyncButton, Banner, LabeledInput, Loading, LabeledSelect
104
+ },
105
+
106
+ mixins: [ResourceManager],
107
+
108
+ async fetch() {
109
+ const hash = {};
110
+
111
+ if ( this.$store.getters['management/canList'](WORKLOAD_TYPES.DEPLOYMENT) ) {
112
+ hash.deployments = this.$store.dispatch('management/findAll', { type: WORKLOAD_TYPES.DEPLOYMENT });
113
+ }
114
+
115
+ if ( this.$store.getters['management/canList'](SERVICE) ) {
116
+ hash.services = this.$store.dispatch('management/findAll', { type: SERVICE });
117
+ }
118
+
119
+ await allHash(hash);
120
+
121
+ this.secondaryResourceData = this.secondaryResourceDataConfig();
122
+ this.resourceManagerFetchSecondaryResources(this.secondaryResourceData);
123
+ },
124
+
125
+ data() {
126
+ return {
127
+ ...initialState(),
128
+ secondaryResourceData: null
129
+ };
130
+ },
131
+
132
+ computed: {
133
+ ...mapGetters({ allRepos: 'catalog/repos' }),
134
+
135
+ namespacedDeployments() {
136
+ return this.$store.getters['management/all'](WORKLOAD_TYPES.DEPLOYMENT).filter(dep => dep.metadata.namespace === UI_PLUGIN_NAMESPACE);
137
+ },
138
+
139
+ namespacedServices() {
140
+ return this.$store.getters['management/all'](SERVICE).filter(svc => svc.metadata.namespace === UI_PLUGIN_NAMESPACE);
141
+ }
142
+ },
143
+
144
+ methods: {
145
+ secondaryResourceDataConfig() {
146
+ return {
147
+ namespace: UI_PLUGIN_NAMESPACE,
148
+ data: {
149
+ [SECRET]: {
150
+ applyTo: [
151
+ {
152
+ var: 'imagePullNamespacedSecrets',
153
+ parsingFunc: (data) => {
154
+ return data.filter(secret => (secret._type === SECRET_TYPES.DOCKER || secret._type === SECRET_TYPES.DOCKER_JSON));
155
+ }
156
+ }
157
+ ]
158
+ }
159
+ }
160
+ };
161
+ },
162
+
163
+ showDialog() {
164
+ this.$modal.show('catalogLoadDialog');
165
+ },
166
+
167
+ closeDialog(result) {
168
+ this.$modal.hide('catalogLoadDialog');
169
+ this.$emit('closed', result);
170
+
171
+ // Reset state
172
+ Object.assign(this.$data, initialState());
173
+ this.secondaryResourceData = this.secondaryResourceDataConfig();
174
+ this.resourceManagerFetchSecondaryResources(this.secondaryResourceData);
175
+ },
176
+
177
+ async loadImage(btnCb) {
178
+ try {
179
+ if (!isEmpty(this.deploymentValues.spec.template.spec.containers[0].image)) {
180
+ const image = this.deploymentValues.spec.template.spec.containers[0].image;
181
+ const name = this.extractImageName(image);
182
+
183
+ if (name) {
184
+ // Create deployment
185
+ await this.loadDeployment(image, name, btnCb);
186
+
187
+ if (this.extensionDeployment) {
188
+ // Create service
189
+ await this.loadService(name, btnCb);
190
+ }
191
+
192
+ if (this.extensionSvc) {
193
+ // Create helm repo
194
+ await this.loadRepo(name, btnCb);
195
+ }
196
+
197
+ if (this.extensionRepo) {
198
+ // Create uiplugin crd
199
+ await this.loadPlugin(name, this.extensionUrl, image);
200
+ }
201
+
202
+ btnCb(true);
203
+ } else {
204
+ throw new Error('Unable to determine image name');
205
+ }
206
+ }
207
+ } catch (e) {
208
+ this.handleGrowlError(e, true);
209
+ btnCb(false);
210
+ }
211
+ },
212
+
213
+ async loadDeployment(image, name, btnCb) {
214
+ const exists = this.namespacedDeployments.find(dep => dep.spec.template.spec.containers[0].image === image);
215
+
216
+ if (!exists) {
217
+ // Sets deploymentValues with name, labels, and imagePullSecrets
218
+ const deploymentValues = this.parseDeploymentValues(name);
219
+
220
+ this.extensionDeployment = await this.$store.dispatch('management/create', deploymentValues);
221
+
222
+ try {
223
+ await this.extensionDeployment.save();
224
+ } catch (e) {
225
+ this.handleGrowlError(e, true);
226
+ btnCb(false);
227
+ }
228
+ } else {
229
+ const error = {
230
+ _statusText: this.t('plugins.manageCatalog.imageLoad.error.exists.deployment.title'),
231
+ message: this.t('plugins.manageCatalog.imageLoad.error.exists.deployment.message', { image })
232
+ };
233
+
234
+ this.handleGrowlError(error);
235
+ btnCb(false);
236
+ }
237
+ },
238
+
239
+ async loadService(name, btnCb) {
240
+ const serviceName = `${ name }-svc`;
241
+ const exists = this.namespacedServices.find(svc => svc.metadata.name === serviceName);
242
+
243
+ if (exists) {
244
+ const error = {
245
+ _statusText: this.t('plugins.manageCatalog.imageLoad.error.exists.service.title'),
246
+ message: this.t('plugins.manageCatalog.imageLoad.error.exists.service.message', { svc: serviceName })
247
+ };
248
+
249
+ this.handleGrowlError(error, true);
250
+ btnCb(false);
251
+
252
+ return;
253
+ }
254
+
255
+ // Sets serviceValues with name, label, and selector
256
+ const serviceValues = this.parseServiceValues(name, serviceName);
257
+ const serviceResource = await this.$store.dispatch('management/create', serviceValues);
258
+
259
+ try {
260
+ await serviceResource.save();
261
+ } catch (e) {
262
+ this.handleGrowlError(e, true);
263
+ btnCb(false);
264
+
265
+ return;
266
+ }
267
+
268
+ try {
269
+ this.extensionSvc = await this.$store.dispatch('management/find', {
270
+ type: SERVICE,
271
+ id: `${ UI_PLUGIN_NAMESPACE }/${ serviceResource.metadata.name }`,
272
+ namespace: UI_PLUGIN_NAMESPACE
273
+ });
274
+
275
+ if (this.extensionSvc) {
276
+ this.extensionUrl = `http://${ this.extensionSvc.spec.clusterIP }:${ this.extensionSvc.spec.ports[0].port }`;
277
+ } else {
278
+ throw new Error('Error fetching extension service');
279
+ }
280
+ } catch (e) {
281
+ this.handleGrowlError(e, true);
282
+ btnCb(false);
283
+ }
284
+ },
285
+
286
+ async loadRepo(name, btnCb) {
287
+ const chartName = `${ name }-charts`;
288
+ const exists = this.allRepos.find(repo => repo.metadata.name === chartName);
289
+
290
+ if (exists) {
291
+ const error = {
292
+ _statusText: this.t('plugins.manageCatalog.imageLoad.error.exists.repo.title'),
293
+ message: this.t('plugins.manageCatalog.imageLoad.error.exists.repo.message', { repo: chartName })
294
+ };
295
+
296
+ this.handleGrowlError(error);
297
+ btnCb(false);
298
+ this.clean();
299
+
300
+ return;
301
+ }
302
+
303
+ // Set repoValues with name, label, and url
304
+ const repoValues = this.parseRepoValues(chartName);
305
+
306
+ this.extensionRepo = await this.$store.dispatch('management/create', repoValues);
307
+
308
+ try {
309
+ await this.extensionRepo.save();
310
+ } catch (e) {
311
+ this.handleGrowlError(e, true);
312
+ btnCb(false);
313
+ }
314
+ },
315
+
316
+ async loadPlugin(name, url, image, btnCb) {
317
+ // Try and parse version number from the image
318
+ const version = this.extractImageVersion(image) || 'latest';
319
+
320
+ if (!this.extractImageVersion(image)) {
321
+ this.$store.dispatch('growl/warning', {
322
+ title: this.t('plugins.manageCatalog.imageLoad.imageVersion.title'),
323
+ message: this.t('plugins.manageCatalog.imageLoad.imageVersion.message', { image }),
324
+ timeout: 4000,
325
+ }, { root: true });
326
+ }
327
+
328
+ let crdName = name;
329
+
330
+ const parts = name.split('-');
331
+
332
+ if (parts.length >= 2) {
333
+ crdName = parts.join('-');
334
+ }
335
+
336
+ this.extensionCrd = await this.$store.dispatch('management/create', {
337
+ type: UI_PLUGIN,
338
+ metadata: {
339
+ name,
340
+ namespace: UI_PLUGIN_NAMESPACE,
341
+ labels: {
342
+ [UI_PLUGIN_LABELS.CATALOG_IMAGE]: name,
343
+ [UI_PLUGIN_LABELS.REPOSITORY]: this.extensionRepo.metadata.name
344
+ }
345
+ },
346
+ spec: {
347
+ plugin: {
348
+ name: crdName,
349
+ version,
350
+ endpoint: url,
351
+ noCache: false,
352
+ metadata: { [UI_PLUGIN_LABELS.CATALOG]: 'true' }
353
+ }
354
+ }
355
+ });
356
+
357
+ try {
358
+ await this.extensionCrd.save({ url: `/v1/${ UI_PLUGIN }`, method: 'POST' });
359
+
360
+ this.closeDialog();
361
+ this.$store.dispatch('growl/success', {
362
+ title: this.t('plugins.manageCatalog.imageLoad.success.title', { name }),
363
+ message: this.t('plugins.manageCatalog.imageLoad.success.message'),
364
+ timeout: 4000,
365
+ }, { root: true });
366
+ } catch (e) {
367
+ this.handleGrowlError(e, true);
368
+ btnCb(false);
369
+ }
370
+ },
371
+
372
+ parseDeploymentValues(name) {
373
+ let out = {};
374
+
375
+ this.$set(this.deploymentValues.metadata, 'name', name);
376
+
377
+ const addLabel = { [UI_PLUGIN_LABELS.CATALOG_IMAGE]: name };
378
+ const addTo = ['metadata.labels', 'spec.selector.matchLabels', 'spec.template.metadata.labels'];
379
+
380
+ // Populates workloadselector labels
381
+ out = this.assignLabels(this.deploymentValues, addLabel, addTo);
382
+
383
+ if (this.imagePullSecrets.length) {
384
+ out.spec.template.spec.imagePullSecrets = this.imagePullSecrets.map((secret) => {
385
+ return { name: secret };
386
+ });
387
+ }
388
+
389
+ return out;
390
+ },
391
+
392
+ parseServiceValues(name, serviceName) {
393
+ const out = this.serviceValues;
394
+
395
+ out.metadata.name = serviceName;
396
+ out.metadata.labels[UI_PLUGIN_LABELS.CATALOG_IMAGE] = name;
397
+ out.spec.selector[UI_PLUGIN_LABELS.CATALOG_IMAGE] = name;
398
+
399
+ return out;
400
+ },
401
+
402
+ parseRepoValues(chartName) {
403
+ const out = this.repoValues;
404
+
405
+ out.metadata.name = chartName;
406
+ out.metadata.labels[UI_PLUGIN_LABELS.CATALOG_IMAGE] = this.deploymentValues.metadata.name;
407
+ out.spec.url = this.extensionUrl;
408
+
409
+ return out;
410
+ },
411
+
412
+ assignLabels(source, labels, args) {
413
+ for (let i = 0; i < args.length; i++) {
414
+ const path = args[i].split('.');
415
+ let currentObj = source;
416
+
417
+ for (let j = 0; j < path.length - 1; j++) {
418
+ currentObj = currentObj[path[j]];
419
+ }
420
+
421
+ currentObj[path[path.length - 1]] = labels;
422
+ }
423
+
424
+ return source;
425
+ },
426
+
427
+ extractImageVersion(image) {
428
+ // Returns the version number with optional pre-release identifiers
429
+ const regex = /:(\d+\.\d+\.\d+([-\w\d]+)*)$/;
430
+ const matches = regex.exec(image);
431
+
432
+ if (matches) {
433
+ return matches[1];
434
+ }
435
+
436
+ return null;
437
+ },
438
+
439
+ extractImageName(image) {
440
+ // Returns the name within the image that prefixes the version number
441
+ const regex = /\/([^/:]+)(?::\d+\.\d+\.\d+(-[a-zA-Z0-9]+)?|$)/;
442
+ const matches = regex.exec(image);
443
+
444
+ if (matches) {
445
+ return matches[1];
446
+ }
447
+
448
+ return null;
449
+ },
450
+
451
+ clean() {
452
+ // Remove failed resources
453
+ if (this.extensionDeployment) {
454
+ this.extensionDeployment.remove();
455
+ }
456
+ if (this.extensionSvc) {
457
+ this.extensionSvc.remove();
458
+ }
459
+ if (this.extensionRepo) {
460
+ this.extensionRepo.remove();
461
+ }
462
+ if (this.extensionCrd) {
463
+ this.extensionCrd.remove();
464
+ }
465
+ },
466
+
467
+ handleGrowlError(e, clean = false) {
468
+ const error = e?.data || e;
469
+
470
+ this.$store.dispatch('growl/error', {
471
+ title: error._statusText,
472
+ message: error.message,
473
+ timeout: 5000,
474
+ }, { root: true });
475
+
476
+ if (clean) {
477
+ this.clean();
478
+ }
479
+ }
480
+ }
481
+ };
482
+ </script>
483
+
484
+ <template>
485
+ <modal
486
+ name="catalogLoadDialog"
487
+ height="auto"
488
+ :scrollable="true"
489
+ @closed="closeDialog()"
490
+ >
491
+ <Loading
492
+ v-if="$fetchState.loading"
493
+ mode="relative"
494
+ />
495
+ <div
496
+ v-else
497
+ class="plugin-install-dialog"
498
+ >
499
+ <template>
500
+ <div>
501
+ <h4>
502
+ {{ t('plugins.manageCatalog.imageLoad.load') }}
503
+ </h4>
504
+ <p>
505
+ {{ t('plugins.manageCatalog.imageLoad.prompt') }}
506
+ </p>
507
+
508
+ <div class="custom mt-10">
509
+ <Banner
510
+ color="info"
511
+ :label="t('plugins.manageCatalog.imageLoad.banner')"
512
+ class="mt-10"
513
+ />
514
+ </div>
515
+
516
+ <div class="custom mt-10">
517
+ <div class="fields">
518
+ <LabeledInput
519
+ v-model.trim="deploymentValues.spec.template.spec.containers[0].image"
520
+ label-key="plugins.manageCatalog.imageLoad.fields.image.label"
521
+ placeholder-key="plugins.manageCatalog.imageLoad.fields.image.placeholder"
522
+ />
523
+ </div>
524
+ </div>
525
+ <div class="custom mt-10">
526
+ <div class="fields">
527
+ <LabeledSelect
528
+ v-model="imagePullSecrets"
529
+ :label="t('workload.container.imagePullSecrets')"
530
+ :multiple="true"
531
+ :taggable="true"
532
+ :options="imagePullNamespacedSecrets"
533
+ option-label="metadata.name"
534
+ :reduce="service => service.metadata.name"
535
+ />
536
+ <Banner
537
+ color="warning"
538
+ class="mt-10"
539
+ >
540
+ <span v-clean-html="t('plugins.manageCatalog.imageLoad.fields.secrets.banner', {}, true)" />
541
+ </Banner>
542
+ </div>
543
+ </div>
544
+ </div>
545
+ </template>
546
+
547
+ <div class="custom mt-10">
548
+ <div class="fields">
549
+ <div class="dialog-buttons mt-20">
550
+ <button
551
+ class="btn role-secondary"
552
+ data-testid="image-load-ext-modal-cancel-btn"
553
+ @click="closeDialog()"
554
+ >
555
+ {{ t('generic.cancel') }}
556
+ </button>
557
+ <AsyncButton
558
+ mode="load"
559
+ data-testid="image-load-ext-modal-install-btn"
560
+ @click="loadImage"
561
+ />
562
+ </div>
563
+ </div>
564
+ </div>
565
+ </div>
566
+ </modal>
567
+ </template>
568
+
569
+ <style lang="scss" scoped>
570
+ .plugin-install-dialog {
571
+ padding: 10px;
572
+
573
+ h4 {
574
+ font-weight: bold;
575
+ }
576
+
577
+ .dialog-panel {
578
+ display: flex;
579
+ flex-direction: column;
580
+ min-height: 100px;
581
+
582
+ p {
583
+ margin-bottom: 5px;
584
+ }
585
+
586
+ .dialog-info {
587
+ flex: 1;
588
+ }
589
+ }
590
+
591
+ .dialog-buttons {
592
+ display: flex;
593
+ justify-content: flex-end;
594
+ margin-top: 10px;
595
+
596
+ > *:not(:last-child) {
597
+ margin-right: 10px;
598
+ }
599
+ }
600
+ }
601
+ </style>