@rancher/shell 0.3.16 → 0.3.18

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 (174) hide show
  1. package/assets/images/wechat-qr-code.jpg +0 -0
  2. package/assets/translations/en-us.yaml +75 -16
  3. package/assets/translations/zh-hans.yaml +151 -15
  4. package/chart/__tests__/S3.test.ts +50 -0
  5. package/chart/rancher-backup/S3.vue +21 -0
  6. package/chart/rancher-backup/index.vue +4 -0
  7. package/components/AsyncButton.vue +1 -1
  8. package/components/CommunityLinks.vue +1 -0
  9. package/components/FileDiff.vue +92 -85
  10. package/components/Inactivity.vue +10 -0
  11. package/components/LazyImage.vue +2 -2
  12. package/components/PromptRestore.vue +7 -5
  13. package/components/ResourceDetail/Masthead.vue +1 -1
  14. package/components/ResourceDetail/index.vue +8 -14
  15. package/components/ResourceList/index.vue +1 -1
  16. package/components/ResourceTable.vue +50 -2
  17. package/components/YamlEditor.vue +1 -0
  18. package/components/__tests__/PromptRestore.test.ts +72 -0
  19. package/components/auth/AzureWarning.vue +1 -1
  20. package/components/auth/RoleDetailEdit.vue +1 -0
  21. package/components/fleet/FleetResources.vue +3 -64
  22. package/components/form/FileImageSelector.vue +9 -0
  23. package/components/form/FileSelector.vue +2 -1
  24. package/components/form/MatchExpressions.vue +1 -3
  25. package/components/form/NameNsDescription.vue +28 -12
  26. package/components/form/NodeAffinity.vue +2 -2
  27. package/components/form/PodAffinity.vue +2 -2
  28. package/components/form/ResourceTabs/index.vue +8 -2
  29. package/components/form/Select.vue +16 -0
  30. package/components/form/__tests__/FileImageSelector.test.ts +42 -0
  31. package/components/form/__tests__/FileSelector.test.ts +76 -0
  32. package/components/form/__tests__/NodeAffinity.test.ts +38 -0
  33. package/components/form/__tests__/PodAffinity.test.ts +46 -0
  34. package/components/formatter/ClusterLink.vue +8 -4
  35. package/components/formatter/ClusterProvider.vue +3 -1
  36. package/components/formatter/ImageName.vue +23 -0
  37. package/components/formatter/PodImages.vue +7 -1
  38. package/components/formatter/__tests__/ClusterLink.test.ts +101 -0
  39. package/components/formatter/__tests__/ClusterProvider.test.ts +24 -0
  40. package/components/nav/Header.vue +2 -2
  41. package/components/nav/WindowManager/ContainerShell.vue +60 -36
  42. package/components/nav/WindowManager/__tests__/ContainerShell.test.ts +561 -0
  43. package/config/__test__/home-links.test.ts +62 -0
  44. package/config/home-links.js +15 -3
  45. package/config/labels-annotations.js +7 -2
  46. package/config/persistentVolume.ts +108 -0
  47. package/config/product/manager.js +5 -1
  48. package/config/router.js +0 -4
  49. package/config/settings.ts +4 -0
  50. package/config/table-headers.js +6 -5
  51. package/config/types.js +2 -0
  52. package/config/uiplugins.js +50 -5
  53. package/core/plugin-helpers.js +39 -15
  54. package/core/plugin.ts +9 -0
  55. package/core/plugins.js +1 -1
  56. package/core/types-provisioning.ts +253 -0
  57. package/core/types.ts +21 -3
  58. package/detail/autoscaling.horizontalpodautoscaler/index.vue +50 -1
  59. package/detail/fleet.cattle.io.gitrepo.vue +10 -2
  60. package/detail/node.vue +6 -6
  61. package/detail/pod.vue +38 -9
  62. package/detail/provisioning.cattle.io.cluster.vue +46 -7
  63. package/detail/workload/index.vue +49 -18
  64. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +62 -0
  65. package/edit/__tests__/ui.cattle.io.navlink.test.ts +110 -0
  66. package/edit/auth/github.vue +1 -0
  67. package/edit/autoscaling.horizontalpodautoscaler/hpa-scaling-rule.vue +130 -0
  68. package/edit/autoscaling.horizontalpodautoscaler/index.vue +79 -0
  69. package/edit/fleet.cattle.io.clustergroup.vue +14 -3
  70. package/edit/fleet.cattle.io.gitrepo.vue +18 -1
  71. package/edit/namespace.vue +9 -1
  72. package/edit/networking.k8s.io.ingress/RulePath.vue +0 -2
  73. package/edit/persistentvolume/__tests__/persistentvolume.test.ts +82 -0
  74. package/edit/persistentvolume/index.vue +2 -1
  75. package/edit/persistentvolume/plugins/csi.vue +3 -1
  76. package/edit/persistentvolume/plugins/longhorn.vue +12 -12
  77. package/edit/provisioning.cattle.io.cluster/AgentConfiguration.vue +1 -30
  78. package/edit/provisioning.cattle.io.cluster/RegistryConfigs.vue +15 -11
  79. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +79 -1
  80. package/edit/provisioning.cattle.io.cluster/index.vue +53 -1
  81. package/edit/provisioning.cattle.io.cluster/rke2.vue +335 -151
  82. package/edit/storage.k8s.io.storageclass/index.vue +1 -2
  83. package/edit/ui.cattle.io.navlink.vue +213 -186
  84. package/initialize/App.js +3 -13
  85. package/initialize/layouts.ts +26 -0
  86. package/layouts/default.vue +1 -1
  87. package/list/group.principal.vue +1 -1
  88. package/list/provisioning.cattle.io.cluster.vue +8 -1
  89. package/middleware/authenticated.js +101 -5
  90. package/mixins/brand.js +39 -3
  91. package/mixins/child-hook.js +2 -2
  92. package/mixins/create-edit-view/impl.js +4 -4
  93. package/models/chart.js +1 -1
  94. package/models/fleet.cattle.io.cluster.js +33 -4
  95. package/models/fleet.cattle.io.gitrepo.js +113 -38
  96. package/models/management.cattle.io.kontainerdriver.js +14 -0
  97. package/models/persistentvolume.js +2 -111
  98. package/models/pod.js +30 -0
  99. package/models/provisioning.cattle.io.cluster.js +9 -1
  100. package/models/rke.cattle.io.etcdsnapshot.js +10 -7
  101. package/package.json +2 -2
  102. package/pages/about.vue +8 -2
  103. package/pages/auth/login.vue +1 -1
  104. package/pages/auth/logout.vue +11 -3
  105. package/pages/c/_cluster/apps/charts/index.vue +5 -2
  106. package/pages/c/_cluster/apps/charts/install.vue +5 -0
  107. package/pages/c/_cluster/auth/group.principal/assign-edit.vue +1 -1
  108. package/pages/c/_cluster/auth/roles/index.vue +1 -1
  109. package/pages/c/_cluster/explorer/index.vue +2 -11
  110. package/pages/c/_cluster/manager/cloudCredential/_id.vue +0 -1
  111. package/pages/c/_cluster/manager/cloudCredential/create.vue +0 -1
  112. package/pages/c/_cluster/settings/brand.vue +11 -8
  113. package/pages/c/_cluster/uiplugins/AddExtensionRepos.vue +177 -0
  114. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +19 -3
  115. package/pages/c/_cluster/uiplugins/RemoveUIPlugins.vue +90 -21
  116. package/pages/c/_cluster/uiplugins/SetupUIPlugins.vue +107 -37
  117. package/pages/c/_cluster/uiplugins/index.vue +160 -44
  118. package/pages/docs/_doc.vue +9 -3
  119. package/pages/home.vue +6 -6
  120. package/pages/support/index.vue +10 -4
  121. package/pkg/auto-import.js +1 -1
  122. package/plugins/clean-tooltip-directive.js +1 -1
  123. package/plugins/dashboard-store/__tests__/actions.spec.ts +165 -0
  124. package/plugins/dashboard-store/__tests__/getters.spec.ts +100 -0
  125. package/plugins/dashboard-store/__tests__/{mutations.spec.js → mutations.spec.ts} +2 -2
  126. package/plugins/dashboard-store/actions.js +1 -1
  127. package/plugins/dashboard-store/resource-class.js +39 -2
  128. package/plugins/plugin.js +9 -1
  129. package/plugins/steve/__tests__/getters.spec.ts +93 -0
  130. package/plugins/steve/getters.js +21 -1
  131. package/plugins/steve/subscribe.js +1 -3
  132. package/rancher-components/BadgeState/BadgeState.vue +5 -1
  133. package/rancher-components/Banner/Banner.test.ts +51 -1
  134. package/rancher-components/Banner/Banner.vue +134 -53
  135. package/rancher-components/Card/Card.test.ts +37 -0
  136. package/rancher-components/Card/Card.vue +24 -7
  137. package/rancher-components/Form/Checkbox/Checkbox.test.ts +20 -29
  138. package/rancher-components/Form/Checkbox/Checkbox.vue +45 -20
  139. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +2 -8
  140. package/rancher-components/Form/LabeledInput/LabeledInput.vue +22 -10
  141. package/rancher-components/Form/Radio/RadioButton.test.ts +31 -0
  142. package/rancher-components/Form/Radio/RadioButton.vue +30 -13
  143. package/rancher-components/Form/Radio/RadioGroup.vue +26 -7
  144. package/rancher-components/Form/TextArea/TextAreaAutoGrow.vue +7 -6
  145. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.test.ts +25 -38
  146. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +23 -11
  147. package/rancher-components/LabeledTooltip/LabeledTooltip.vue +19 -5
  148. package/rancher-components/StringList/StringList.test.ts +453 -49
  149. package/rancher-components/StringList/StringList.vue +44 -26
  150. package/scripts/extension/publish +2 -2
  151. package/scripts/typegen.sh +11 -2
  152. package/server/server-middleware.js +4 -12
  153. package/store/index.js +14 -3
  154. package/store/prefs.js +0 -3
  155. package/store/store-types.js +2 -0
  156. package/store/type-map.js +17 -29
  157. package/types/api.d.ts +1 -0
  158. package/types/fleet.d.ts +1 -0
  159. package/types/shell/index.d.ts +931 -85
  160. package/types/userPreferences.d.ts +1 -1
  161. package/utils/__mocks__/socket.js +21 -0
  162. package/utils/grafana.js +23 -11
  163. package/utils/kube.js +9 -0
  164. package/utils/object.js +27 -0
  165. package/utils/selector.js +2 -1
  166. package/utils/settings.ts +2 -2
  167. package/utils/validators/formRules/index.ts +3 -3
  168. package/vue.config.js +3 -2
  169. package/components/.DS_Store +0 -0
  170. package/components/__tests__/.DS_Store +0 -0
  171. package/creators/pkg/package-lock.json +0 -37
  172. package/pages/safeMode.vue +0 -17
  173. package/plugins/steve/urloptions.js +0 -47
  174. package/yarn-error.log +0 -196
@@ -26,6 +26,16 @@ const getPackageFromRoute = (route) => {
26
26
  return arraySafe.find((m) => !!m.pkg)?.pkg;
27
27
  };
28
28
 
29
+ const getResourceFromRoute = (to) => {
30
+ let resource = to.params?.resource;
31
+
32
+ if (!resource) {
33
+ resource = findMeta(to, 'resource');
34
+ }
35
+
36
+ return resource;
37
+ };
38
+
29
39
  let beforeEachSetup = false;
30
40
 
31
41
  function findMeta(route, key) {
@@ -71,9 +81,18 @@ export function getProductFromRoute(to) {
71
81
  return product;
72
82
  }
73
83
 
74
- function setProduct(store, to) {
84
+ function setProduct(store, to, redirect) {
75
85
  let product = getProductFromRoute(to);
76
86
 
87
+ // since all products are hardcoded as routes (ex: c-local-explorer), if we match the wildcard route it means that the product does not exist
88
+ if ((product && (!to.matched.length || (to.matched.length && to.matched[0].path === '/c/:cluster/:product'))) ||
89
+ // if the product grabbed from the route is not registered, then we don't have it!
90
+ (product && !store.getters['type-map/isProductRegistered'](product))) {
91
+ store.dispatch('loadingError', new Error(store.getters['i18n/t']('nav.failWhale.productNotFound', { productNotFound: product }, true)));
92
+
93
+ return () => redirect(302, '/fail-whale');
94
+ }
95
+
77
96
  if ( !product ) {
78
97
  product = EXPLORER;
79
98
  }
@@ -92,6 +111,50 @@ function setProduct(store, to) {
92
111
  // There might be management catalog items in it vs cluster.
93
112
  store.commit('catalog/reset');
94
113
  }
114
+
115
+ return false;
116
+ }
117
+
118
+ /**
119
+ * Check that the resource is valid, if not redirect to fail whale
120
+ *
121
+ * This requires that
122
+ * - product is set
123
+ * - product's store is set and setup (so we can check schema's within it)
124
+ * - product's store has the schemaFor getter (extension stores might not have it)
125
+ * - there's a resource associated with route (meta or param)
126
+ */
127
+ function invalidResource(store, to, redirect) {
128
+ const product = store.getters['currentProduct'];
129
+ const resource = getResourceFromRoute(to);
130
+
131
+ // In order to check a resource is valid we need these
132
+ if (!product || !resource) {
133
+ return false;
134
+ }
135
+
136
+ // Note - don't use the current products store... because products can override stores for resources with `typeStoreMap`
137
+ const inStore = store.getters['currentStore'](resource);
138
+ // There's a chance we're in an extension's product who's store could be anything, so confirm schemaFor exists
139
+ const schemaFor = store.getters[`${ inStore }/schemaFor`];
140
+
141
+ // In order to check a resource is valid we need these
142
+ if (!inStore || !schemaFor) {
143
+ return false;
144
+ }
145
+
146
+ // Resource is valid if a schema exists for it (standard resource, spoofed resource) or it's a virtual resource
147
+ const validResource = schemaFor(resource) || store.getters['type-map/isVirtual'](resource);
148
+
149
+ if (validResource) {
150
+ return false;
151
+ }
152
+
153
+ // Unknown resource, redirect to fail whale
154
+
155
+ store.dispatch('loadingError', new Error(store.getters['i18n/t']('nav.failWhale.resourceNotFound', { resource }, true)));
156
+
157
+ return () => redirect(302, '/fail-whale');
95
158
  }
96
159
 
97
160
  export default async function({
@@ -281,19 +344,39 @@ export default async function({
281
344
  store.dispatch('gcRouteChanged', route);
282
345
 
283
346
  // Load stuff
347
+ let localCheckResource = false;
348
+
284
349
  await applyProducts(store, $plugin);
350
+
285
351
  // Setup a beforeEach hook once to keep track of the current product
286
352
  if ( !beforeEachSetup ) {
287
353
  beforeEachSetup = true;
354
+ // This only needs to happen when beforeEach hook hasn't run (the initial load)
355
+ localCheckResource = true;
288
356
 
289
357
  store.app.router.beforeEach((to, from, next) => {
290
358
  // NOTE - This beforeEach runs AFTER this middleware. So anything in this middleware that requires it must set it manually
291
- setProduct(store, to);
359
+ let redirected = setProduct(store, to, redirect);
360
+
361
+ if (redirected) {
362
+ return redirected();
363
+ }
364
+
365
+ redirected = invalidResource(store, to, redirect);
366
+
367
+ if (redirected) {
368
+ return redirected();
369
+ }
370
+
292
371
  next();
293
372
  });
294
373
 
295
374
  // Call it for the initial pageload
296
- setProduct(store, route);
375
+ const redirected = setProduct(store, route, redirect);
376
+
377
+ if (redirected) {
378
+ return redirected();
379
+ }
297
380
 
298
381
  if (process.client) {
299
382
  store.app.router.afterEach((to, from) => {
@@ -376,7 +459,12 @@ export default async function({
376
459
  // When fleet moves to it's own package this should be moved to pkg onEnter/onLeave
377
460
  if ((oldProduct === FLEET_NAME || product === FLEET_NAME) && oldProduct !== product) {
378
461
  // See note above for store.app.router.beforeEach, need to setProduct manually, for the moment do this in a targeted way
379
- setProduct(store, route);
462
+ const redirected = setProduct(store, route, redirect);
463
+
464
+ if (redirected) {
465
+ return redirected();
466
+ }
467
+
380
468
  store.commit('updateWorkspace', {
381
469
  value: store.getters['prefs/get'](WORKSPACE) || DEFAULT_WORKSPACE,
382
470
  getters: store.getters
@@ -397,6 +485,14 @@ export default async function({
397
485
  })
398
486
  ]);
399
487
 
488
+ if (localCheckResource) {
489
+ const redirected = invalidResource(store, route, redirect);
490
+
491
+ if (redirected) {
492
+ return redirected();
493
+ }
494
+ }
495
+
400
496
  if (!clusterId) {
401
497
  clusterId = store.getters['defaultClusterId']; // This needs the cluster list, so no parallel
402
498
  const isSingleProduct = store.getters['isSingleProduct'];
@@ -422,7 +518,7 @@ export default async function({
422
518
  return redirect(302, '/home');
423
519
  } else {
424
520
  // Sets error 500 if lost connection to API
425
- store.commit('setError', { error: e, locationError: new Error('Auth Middleware') });
521
+ store.commit('setError', { error: e, locationError: new Error(store.getters['i18n/t']('nav.failWhale.authMiddleware')) });
426
522
 
427
523
  return redirect(302, '/fail-whale');
428
524
  }
package/mixins/brand.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { mapGetters } from 'vuex';
1
2
  import { CATALOG, MANAGEMENT } from '@shell/config/types';
2
3
  import { getVendor } from '@shell/config/private-label';
3
4
  import { SETTING } from '@shell/config/settings';
@@ -5,6 +6,12 @@ import { findBy } from '@shell/utils/array';
5
6
  import { createCssVars } from '@shell/utils/color';
6
7
  import { _ALL_IF_AUTHED } from '@shell/plugins/dashboard-store/actions';
7
8
 
9
+ const cspAdaptorApp = ['rancher-csp-adapter', 'rancher-csp-billing-adapter'];
10
+
11
+ export const hasCspAdapter = (apps) => {
12
+ return apps?.find((a) => cspAdaptorApp.includes(a.metadata?.name));
13
+ };
14
+
8
15
  export default {
9
16
  async fetch() {
10
17
  // For the login page, the schemas won't be loaded - we don't need the apps in this case
@@ -23,13 +30,19 @@ export default {
23
30
  }
24
31
  });
25
32
  } catch (e) {}
33
+
34
+ // Setting this up front will remove `computed` churn, and we only care that we've initialised them
35
+ this.haveAppsAndSettings = !!this.apps && !!this.globalSettings;
26
36
  },
27
37
 
28
38
  data() {
29
- return { apps: [], globalSettings: [] };
39
+ return {
40
+ apps: null, globalSettings: null, haveAppsAndSettings: null
41
+ };
30
42
  },
31
43
 
32
44
  computed: {
45
+ ...mapGetters({ loggedIn: 'auth/loggedIn' }),
33
46
 
34
47
  brand() {
35
48
  const setting = findBy(this.globalSettings, 'id', SETTING.BRAND);
@@ -61,7 +74,23 @@ export default {
61
74
  },
62
75
 
63
76
  cspAdapter() {
64
- return findBy(this.apps, 'metadata.name', 'rancher-csp-adapter' );
77
+ if (!this.canCalcCspAdapter) {
78
+ // We only have a watch on cspAdapter to kick off persisting the brand setting.
79
+ // So we need to ensure we don't return an undefined here... which would match the undefined gave if no csp app was found...
80
+ // .. and wouldn't kick off the watcher
81
+ return '';
82
+ }
83
+
84
+ // Note! this used to be `findBy(this.app)` however for that case we lost reactivity on the collection
85
+ // (computed fires before fetch, fetch happens and update apps, computed would not fire again - even with vue.set)
86
+ // So use `.find` in method instead
87
+ return hasCspAdapter(this.apps);
88
+ },
89
+
90
+ canCalcCspAdapter() {
91
+ // We need to take consider the loggedIn state, as the brand mixin is used in the logout page where we can be in a mixed state
92
+ // (things in store but user has no auth to make changes)
93
+ return this.loggedIn && this.haveAppsAndSettings;
65
94
  }
66
95
  },
67
96
 
@@ -90,7 +119,13 @@ export default {
90
119
  },
91
120
 
92
121
  cspAdapter(neu) {
122
+ if (!this.canCalcCspAdapter) {
123
+ return;
124
+ }
125
+
126
+ // The brand setting will only get updated if...
93
127
  if (neu && !this.brand) {
128
+ // 1) There should be a brand... but there's no brand setting
94
129
  const brandSetting = findBy(this.globalSettings, 'id', SETTING.BRAND);
95
130
 
96
131
  if (brandSetting) {
@@ -109,7 +144,8 @@ export default {
109
144
  } else if (!neu) {
110
145
  const brandSetting = findBy(this.globalSettings, 'id', SETTING.BRAND);
111
146
 
112
- if (brandSetting) {
147
+ if (brandSetting && brandSetting.value !== '') {
148
+ // 2) There should not be a brand... but there is a brand setting
113
149
  brandSetting.value = '';
114
150
  brandSetting.save();
115
151
  }
@@ -20,8 +20,8 @@ export default {
20
20
  });
21
21
  },
22
22
 
23
- registerAfterHook(boundFn, name, priority) {
24
- this._registerHook(AFTER_SAVE_HOOKS, boundFn, name, priority);
23
+ registerAfterHook(boundFn, name, priority = 99, boundFnContext) {
24
+ this._registerHook(AFTER_SAVE_HOOKS, boundFn, name, priority, boundFnContext);
25
25
  },
26
26
 
27
27
  async applyHooks(key, ...args) {
@@ -65,9 +65,9 @@ export default {
65
65
 
66
66
  let name = this.$route.name;
67
67
 
68
- if ( name.endsWith('-id') ) {
68
+ if ( name?.endsWith('-id') ) {
69
69
  name = name.replace(/(-namespace)?-id$/, '');
70
- } else if ( name.endsWith('-create') ) {
70
+ } else if ( name?.endsWith('-create') ) {
71
71
  name = name.replace(/-create$/, '');
72
72
  }
73
73
 
@@ -124,7 +124,7 @@ export default {
124
124
  }
125
125
 
126
126
  try {
127
- await this.applyHooks(BEFORE_SAVE_HOOKS);
127
+ await this.applyHooks(BEFORE_SAVE_HOOKS, this.value);
128
128
 
129
129
  // Remove the labels map if it's empty
130
130
  if ( this.value?.metadata?.labels && Object.keys(this.value.metadata.labels || {}).length === 0 ) {
@@ -152,7 +152,7 @@ export default {
152
152
  await this.$store.dispatch('cluster/findAll', { type: this.value.type, opt: { force: true } }, { root: true });
153
153
  }
154
154
 
155
- await this.applyHooks(AFTER_SAVE_HOOKS);
155
+ await this.applyHooks(AFTER_SAVE_HOOKS, this.value);
156
156
  buttonDone && buttonDone(true);
157
157
 
158
158
  this.done();
package/models/chart.js CHANGED
@@ -2,7 +2,7 @@ import { compatibleVersionsFor } from '@shell/store/catalog';
2
2
  import {
3
3
  REPO_TYPE, REPO, CHART, VERSION, _FLAGGED, HIDE_SIDE_NAV
4
4
  } from '@shell/config/query-params';
5
- import { BLANK_CLUSTER } from '@shell/store';
5
+ import { BLANK_CLUSTER } from '@shell/store/store-types.js';
6
6
  import SteveModel from '@shell/plugins/steve/steve-class';
7
7
 
8
8
  export default class Chart extends SteveModel {
@@ -4,6 +4,7 @@ import { _RKE2 } from '@shell/store/prefs';
4
4
  import SteveModel from '@shell/plugins/steve/steve-class';
5
5
  import { escapeHtml } from '@shell/utils/string';
6
6
  import { insertAt } from '@shell/utils/array';
7
+ import jsyaml from 'js-yaml';
7
8
 
8
9
  export default class FleetCluster extends SteveModel {
9
10
  get _availableActions() {
@@ -89,7 +90,7 @@ export default class FleetCluster extends SteveModel {
89
90
  }
90
91
 
91
92
  get state() {
92
- if ( this.spec?.paused === true ) {
93
+ if (this.spec?.paused === true) {
93
94
  return 'paused';
94
95
  }
95
96
 
@@ -124,19 +125,47 @@ export default class FleetCluster extends SteveModel {
124
125
  return mgmt;
125
126
  }
126
127
 
127
- get norman() {
128
- const norman = this.$rootGetters['rancher/byId'](NORMAN.CLUSTER, this.metadata.labels?.[FLEET_LABELS.CLUSTER_NAME]);
128
+ get basicNorman() {
129
+ const norman = this.$rootGetters['rancher/byId'](NORMAN.CLUSTER, this.metadata?.labels?.[FLEET_LABELS.CLUSTER_NAME]);
129
130
 
130
131
  return norman;
131
132
  }
132
133
 
134
+ get norman() {
135
+ if (this.basicNorman) {
136
+ return this.basicNorman;
137
+ }
138
+
139
+ // If navigate to YAML view directly, norman is not loaded yet
140
+ return this.$dispatch('rancher/find', { type: NORMAN.CLUSTER, id: this.metadata.labels[FLEET_LABELS.CLUSTER_NAME] }, { root: true });
141
+ }
142
+
143
+ async normanClone() {
144
+ const norman = await this.norman;
145
+
146
+ return this.$dispatch('rancher/clone', { resource: norman }, { root: true });
147
+ }
148
+
133
149
  get groupByLabel() {
134
150
  const name = this.metadata.namespace;
135
151
 
136
- if ( name ) {
152
+ if (name) {
137
153
  return this.$rootGetters['i18n/t']('resourceTable.groupLabel.workspace', { name: escapeHtml(name) });
138
154
  } else {
139
155
  return this.$rootGetters['i18n/t']('resourceTable.groupLabel.notInAWorkspace');
140
156
  }
141
157
  }
158
+
159
+ async saveYaml(yaml) {
160
+ await this._saveYaml(yaml);
161
+
162
+ const parsed = jsyaml.load(yaml);
163
+
164
+ const norman = await this.normanClone();
165
+
166
+ norman.setLabels(parsed.metadata.labels);
167
+ norman.setAnnotations(parsed.metadata.annotations);
168
+
169
+ await norman.save();
170
+ }
142
171
  }
@@ -1,14 +1,16 @@
1
1
  import { convert, matching, convertSelectorObj } from '@shell/utils/selector';
2
2
  import jsyaml from 'js-yaml';
3
- import { escapeHtml } from '@shell/utils/string';
3
+ import { escapeHtml, randomStr } from '@shell/utils/string';
4
4
  import { FLEET } from '@shell/config/types';
5
5
  import { FLEET as FLEET_ANNOTATIONS } from '@shell/config/labels-annotations';
6
6
  import { addObject, addObjects, findBy, insertAt } from '@shell/utils/array';
7
7
  import { set } from '@shell/utils/object';
8
8
  import SteveModel from '@shell/plugins/steve/steve-class';
9
+ import { STATES_ENUM, colorForState, stateDisplay, stateSort } from '@shell/plugins/dashboard-store/resource-class';
10
+ import { NAME } from '@shell/config/product/explorer';
9
11
 
10
12
  function quacksLikeAHash(str) {
11
- if ( str.match(/^[a-f0-9]{40,}$/i) ) {
13
+ if (str.match(/^[a-f0-9]{40,}$/i)) {
12
14
  return true;
13
15
  }
14
16
 
@@ -24,12 +26,13 @@ export default class GitRepo extends SteveModel {
24
26
 
25
27
  spec.repo = spec.repo || '';
26
28
 
27
- if ( !spec.branch && !spec.revision ) {
29
+ if (!spec.branch && !spec.revision) {
28
30
  spec.branch = 'master';
29
31
  }
30
32
 
31
33
  spec.paths = spec.paths || [];
32
34
  spec.clientSecretName = spec.clientSecretName || null;
35
+ spec.correctDrift = { enabled: false };
33
36
 
34
37
  set(this, 'spec', spec);
35
38
  set(this, 'metadata', meta);
@@ -85,7 +88,7 @@ export default class GitRepo extends SteveModel {
85
88
  }
86
89
 
87
90
  get state() {
88
- if ( this.spec?.paused === true ) {
91
+ if (this.spec?.paused === true) {
89
92
  return 'paused';
90
93
  }
91
94
 
@@ -97,10 +100,10 @@ export default class GitRepo extends SteveModel {
97
100
  const clusters = workspace?.clusters || [];
98
101
  const groups = workspace?.clusterGroups || [];
99
102
 
100
- if ( workspace?.id === 'fleet-local' ) {
103
+ if (workspace?.id === 'fleet-local') {
101
104
  const local = findBy(groups, 'id', 'fleet-local/default');
102
105
 
103
- if ( local ) {
106
+ if (local) {
104
107
  return local.targetClusters;
105
108
  }
106
109
 
@@ -113,30 +116,30 @@ export default class GitRepo extends SteveModel {
113
116
 
114
117
  const out = [];
115
118
 
116
- for ( const tgt of this.spec.targets ) {
117
- if ( tgt.clusterName ) {
119
+ for (const tgt of this.spec.targets) {
120
+ if (tgt.clusterName) {
118
121
  const cluster = findBy(clusters, 'metadata.name', tgt.clusterName);
119
122
 
120
- if ( cluster ) {
123
+ if (cluster) {
121
124
  addObject(out, cluster);
122
125
  }
123
- } else if ( tgt.clusterGroup ) {
126
+ } else if (tgt.clusterGroup) {
124
127
  const group = findBy(groups, {
125
128
  'metadata.namespace': this.metadata.namespace,
126
129
  'metadata.name': tgt.clusterGroup,
127
130
  });
128
131
 
129
- if ( group ) {
132
+ if (group) {
130
133
  addObjects(out, group.targetClusters);
131
134
  }
132
- } else if ( tgt.clusterGroupSelector ) {
135
+ } else if (tgt.clusterGroupSelector) {
133
136
  const expressions = convertSelectorObj(tgt.clusterGroupSelector);
134
137
  const matchingGroups = matching(groups, expressions);
135
138
 
136
- for ( const group of matchingGroups ) {
139
+ for (const group of matchingGroups) {
137
140
  addObjects(out, group.targetClusters);
138
141
  }
139
- } else if ( tgt.clusterSelector ) {
142
+ } else if (tgt.clusterSelector) {
140
143
  const expressions = convertSelectorObj(tgt.clusterSelector);
141
144
  const matchingClusters = matching(clusters, expressions);
142
145
 
@@ -150,7 +153,7 @@ export default class GitRepo extends SteveModel {
150
153
  get github() {
151
154
  const match = this.spec.repo.match(/^https?:\/\/github\.com\/(.*?)(\.git)?\/*$/);
152
155
 
153
- if ( match ) {
156
+ if (match) {
154
157
  return match[1];
155
158
  }
156
159
 
@@ -158,7 +161,7 @@ export default class GitRepo extends SteveModel {
158
161
  }
159
162
 
160
163
  get repoIcon() {
161
- if ( this.github ) {
164
+ if (this.github) {
162
165
  return 'icon icon-github';
163
166
  }
164
167
 
@@ -172,7 +175,7 @@ export default class GitRepo extends SteveModel {
172
175
  repo = repo.replace(/^https:\/\//, '');
173
176
  repo = repo.replace(/\/+$/, '');
174
177
 
175
- if ( this.github ) {
178
+ if (this.github) {
176
179
  return this.github;
177
180
  }
178
181
 
@@ -183,15 +186,15 @@ export default class GitRepo extends SteveModel {
183
186
  const spec = this.spec;
184
187
  const hash = this.status?.commit?.substr(0, 7);
185
188
 
186
- if ( !spec || !spec.repo ) {
189
+ if (!spec || !spec.repo) {
187
190
  return null;
188
191
  }
189
192
 
190
- if ( spec.revision && quacksLikeAHash(spec.revision) ) {
193
+ if (spec.revision && quacksLikeAHash(spec.revision)) {
191
194
  return spec.revision.substr(0, 7);
192
- } else if ( spec.revision ) {
195
+ } else if (spec.revision) {
193
196
  return spec.revision;
194
- } else if ( spec.branch ) {
197
+ } else if (spec.branch) {
195
198
  return spec.branch + (hash ? ` @ ${ hash }` : '');
196
199
  }
197
200
 
@@ -219,7 +222,7 @@ export default class GitRepo extends SteveModel {
219
222
 
220
223
  advanced = jsyaml.dump(targets);
221
224
 
222
- if ( advanced === '[]\n' ) {
225
+ if (advanced === '[]\n') {
223
226
  advanced = `# - name:
224
227
  # clusterSelector:
225
228
  # matchLabels:
@@ -239,39 +242,39 @@ export default class GitRepo extends SteveModel {
239
242
  `;
240
243
  }
241
244
 
242
- if ( this.metadata.namespace === 'fleet-local' ) {
245
+ if (this.metadata.namespace === 'fleet-local') {
243
246
  mode = 'local';
244
- } else if ( !targets.length ) {
247
+ } else if (!targets.length) {
245
248
  mode = 'none';
246
- } else if ( targets.length === 1) {
249
+ } else if (targets.length === 1) {
247
250
  const target = targets[0];
248
251
 
249
252
  if (Object.keys(target).length > 1) {
250
253
  // There are multiple properties in a single target, so use the 'advanced' mode
251
254
  // (otherwise any existing content is nuked for what we provide)
252
255
  mode = 'advanced';
253
- } else if ( target.clusterGroup ) {
256
+ } else if (target.clusterGroup) {
254
257
  clusterGroup = target.clusterGroup;
255
258
 
256
- if ( !mode ) {
259
+ if (!mode) {
257
260
  mode = 'clusterGroup';
258
261
  }
259
- } else if ( target.clusterName ) {
262
+ } else if (target.clusterName) {
260
263
  mode = 'cluster';
261
264
  cluster = target.clusterName;
262
- } else if ( target.clusterSelector ) {
263
- if ( Object.keys(target.clusterSelector).length === 0 ) {
265
+ } else if (target.clusterSelector) {
266
+ if (Object.keys(target.clusterSelector).length === 0) {
264
267
  mode = 'all';
265
268
  } else {
266
269
  const expressions = convert(target.clusterSelector.matchLabels, target.clusterSelector.matchExpressions);
267
270
 
268
- if ( expressions.length === 1 &&
269
- expressions[0].key === FLEET_ANNOTATIONS.CLUSTER_NAME &&
270
- expressions[0].operator === 'In' &&
271
- expressions[0].values.length === 1
271
+ if (expressions.length === 1 &&
272
+ expressions[0].key === FLEET_ANNOTATIONS.CLUSTER_NAME &&
273
+ expressions[0].operator === 'In' &&
274
+ expressions[0].values.length === 1
272
275
  ) {
273
276
  cluster = expressions[0].values[0];
274
- if ( !mode ) {
277
+ if (!mode) {
275
278
  mode = 'cluster';
276
279
  }
277
280
  }
@@ -279,7 +282,7 @@ export default class GitRepo extends SteveModel {
279
282
  }
280
283
  }
281
284
 
282
- if ( !mode ) {
285
+ if (!mode) {
283
286
  mode = 'advanced';
284
287
  }
285
288
 
@@ -295,7 +298,7 @@ export default class GitRepo extends SteveModel {
295
298
  get groupByLabel() {
296
299
  const name = this.metadata.namespace;
297
300
 
298
- if ( name ) {
301
+ if (name) {
299
302
  return this.$rootGetters['i18n/t']('resourceTable.groupLabel.workspace', { name: escapeHtml(name) });
300
303
  } else {
301
304
  return this.$rootGetters['i18n/t']('resourceTable.groupLabel.notInAWorkspace');
@@ -305,7 +308,7 @@ export default class GitRepo extends SteveModel {
305
308
  get bundles() {
306
309
  const all = this.$getters['all'](FLEET.BUNDLE);
307
310
 
308
- return all.filter((bundle) => bundle.name.startsWith(`${ this.name }-`) &&
311
+ return all.filter((bundle) => bundle.repoName === this.name &&
309
312
  bundle.namespace === this.namespace &&
310
313
  bundle.namespacedName.startsWith(`${ this.namespace }:${ this.name }`));
311
314
  }
@@ -324,6 +327,78 @@ export default class GitRepo extends SteveModel {
324
327
  return bds.filter((bd) => bd.metadata?.labels?.['fleet.cattle.io/repo-name'] === this.name);
325
328
  }
326
329
 
330
+ get resourcesStatuses() {
331
+ const clusters = this.targetClusters || [];
332
+ const resources = this.status?.resources || [];
333
+ const conditions = this.status?.conditions || [];
334
+
335
+ const out = [];
336
+
337
+ for (const c of clusters) {
338
+ const clusterBundleDeploymentResources = this.bundleDeployments
339
+ .find((bd) => bd.metadata?.labels?.[FLEET_ANNOTATIONS.CLUSTER] === c.metadata.name)
340
+ ?.status?.resources || [];
341
+
342
+ resources.forEach((r, i) => {
343
+ let namespacedName = r.name;
344
+
345
+ if (r.namespace) {
346
+ namespacedName = `${ r.namespace }:${ r.name }`;
347
+ }
348
+
349
+ let state = r.state;
350
+ const perEntry = r.perClusterState?.find((x) => x.clusterId === c.id);
351
+ const tooMany = r.perClusterState?.length >= 10 || false;
352
+
353
+ if (perEntry) {
354
+ state = perEntry.state;
355
+ } else if (tooMany) {
356
+ state = STATES_ENUM.UNKNOWN;
357
+ } else {
358
+ state = STATES_ENUM.READY;
359
+ }
360
+
361
+ const color = colorForState(state).replace('text-', 'bg-');
362
+ const display = stateDisplay(state);
363
+
364
+ const detailLocation = {
365
+ name: `c-cluster-product-resource${ r.namespace ? '-namespace' : '' }-id`,
366
+ params: {
367
+ product: NAME,
368
+ cluster: c.metadata.labels[FLEET_ANNOTATIONS.CLUSTER_NAME],
369
+ resource: r.type,
370
+ namespace: r.namespace,
371
+ id: r.name,
372
+ }
373
+ };
374
+
375
+ out.push({
376
+ key: `${ r.id }-${ c.id }-${ r.type }-${ r.namespace }-${ r.name }`,
377
+ tableKey: `${ r.id }-${ c.id }-${ r.type }-${ r.namespace }-${ r.name }-${ randomStr(8) }`,
378
+ kind: r.kind,
379
+ apiVersion: r.apiVersion,
380
+ type: r.type,
381
+ id: r.id,
382
+ namespace: r.namespace,
383
+ name: r.name,
384
+ clusterId: c.id,
385
+ clusterName: c.nameDisplay,
386
+ state,
387
+ stateBackground: color,
388
+ stateDisplay: display,
389
+ stateSort: stateSort(color, display),
390
+ namespacedName,
391
+ detailLocation,
392
+ conditions: conditions[i],
393
+ bundleDeploymentStatus: clusterBundleDeploymentResources?.[i],
394
+ creationTimestamp: clusterBundleDeploymentResources?.[i]?.createdAt
395
+ });
396
+ });
397
+ }
398
+
399
+ return out;
400
+ }
401
+
327
402
  get clustersList() {
328
403
  return this.$getters['all'](FLEET.CLUSTER);
329
404
  }