@rancher/shell 0.3.11 → 0.3.13

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 (95) 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/__tests__/namespace.test.ts +46 -0
  35. package/edit/autoscaling.horizontalpodautoscaler/metric-target.vue +0 -2
  36. package/edit/logging.banzaicloud.io.output/__tests__/logging.banzaicloud.io.output.test.ts +43 -0
  37. package/edit/logging.banzaicloud.io.output/index.vue +8 -5
  38. package/edit/logging.banzaicloud.io.output/providers/__tests__/loki.test.ts +13 -0
  39. package/edit/logging.banzaicloud.io.output/providers/loki.vue +1 -0
  40. package/edit/monitoring.coreos.com.alertmanagerconfig/receiverConfig.vue +0 -2
  41. package/edit/monitoring.coreos.com.receiver/index.vue +32 -1
  42. package/edit/monitoring.coreos.com.receiver/types/email.vue +12 -4
  43. package/edit/namespace.vue +2 -1
  44. package/edit/provisioning.cattle.io.cluster/MachinePool.vue +36 -6
  45. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +2 -2
  46. package/edit/provisioning.cattle.io.cluster/rke2.vue +58 -13
  47. package/middleware/authenticated.js +1 -0
  48. package/models/__tests__/batch.cronjob.test.ts +88 -0
  49. package/models/cluster/node.js +8 -0
  50. package/models/management.cattle.io.clusterroletemplatebinding.js +5 -1
  51. package/models/projectroletemplatebinding.js +9 -1
  52. package/models/workload.js +1 -1
  53. package/package.json +1 -1
  54. package/pages/__tests__/prefs.test.ts +96 -0
  55. package/pages/auth/setup.vue +13 -13
  56. package/pages/c/_cluster/apps/charts/chart.vue +1 -1
  57. package/pages/c/_cluster/apps/charts/install.vue +5 -2
  58. package/pages/c/_cluster/monitoring/index.vue +10 -5
  59. package/pages/c/_cluster/settings/performance.vue +2 -0
  60. package/pages/c/_cluster/uiplugins/CatalogList/CatalogLoadDialog.vue +601 -0
  61. package/pages/c/_cluster/uiplugins/CatalogList/index.vue +183 -0
  62. package/pages/c/_cluster/uiplugins/UninstallDialog.vue +50 -9
  63. package/pages/c/_cluster/uiplugins/index.vue +329 -224
  64. package/pages/fail-whale.vue +1 -1
  65. package/pages/home.vue +11 -0
  66. package/pages/prefs.vue +20 -1
  67. package/plugins/plugin.js +1 -1
  68. package/public/index.html +6 -1
  69. package/rancher-components/components/Card/Card.vue +1 -0
  70. package/rancher-components/components/Form/Radio/RadioGroup.vue +1 -0
  71. package/scripts/extension/bundle +20 -4
  72. package/scripts/extension/helm/charts/ui-plugin-server/.helmignore +23 -0
  73. package/scripts/extension/helm/charts/ui-plugin-server/Chart.yaml +20 -0
  74. package/scripts/extension/helm/charts/ui-plugin-server/templates/_helpers.tpl +52 -0
  75. package/scripts/extension/helm/charts/ui-plugin-server/templates/cr.yaml +12 -0
  76. package/scripts/extension/helm/charts/ui-plugin-server/values.yaml +6 -0
  77. package/scripts/extension/helm/package/Dockerfile +27 -0
  78. package/scripts/extension/helm/package/nginx.conf +17 -0
  79. package/scripts/extension/helm/scripts/package +23 -0
  80. package/scripts/extension/helm/scripts/patch +101 -0
  81. package/scripts/extension/helm/scripts/version +31 -0
  82. package/scripts/extension/helmpatch +3 -25
  83. package/scripts/extension/publish +50 -33
  84. package/types/shell/index.d.ts +39 -33
  85. package/utils/__tests__/grafana.test.ts +2 -2
  86. package/utils/error.js +11 -0
  87. package/utils/grafana.js +5 -4
  88. package/vue.config.js +3 -17
  89. package/components/.DS_Store +0 -0
  90. package/components/__tests__/.DS_Store +0 -0
  91. package/creators/pkg/package-lock.json +0 -37
  92. package/rancher-components/StringList/StringList.test.ts +0 -80
  93. package/rancher-components/StringList/StringList.vue +0 -593
  94. package/rancher-components/StringList/index.ts +0 -1
  95. package/yarn-error.log +0 -196
@@ -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>