@rancher/shell 3.0.5-rc.6 → 3.0.5-rc.8

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 (243) hide show
  1. package/assets/brand/classic/metadata.json +3 -0
  2. package/assets/styles/app.scss +1 -0
  3. package/assets/styles/base/_color.scss +16 -0
  4. package/assets/styles/base/_helpers.scss +10 -0
  5. package/assets/styles/base/_variables.scss +18 -12
  6. package/assets/styles/fonts/_icons.scss +1 -32
  7. package/assets/styles/global/_layout.scss +1 -1
  8. package/assets/styles/themes/_dark.scss +262 -258
  9. package/assets/styles/themes/_light.scss +538 -509
  10. package/assets/styles/themes/_modern.scss +914 -0
  11. package/assets/translations/en-us.yaml +110 -29
  12. package/chart/__tests__/S3.test.ts +2 -1
  13. package/cloud-credential/generic.vue +18 -10
  14. package/cloud-credential/harvester.vue +1 -9
  15. package/components/AdvancedSection.vue +8 -0
  16. package/components/ChartReadme.vue +17 -7
  17. package/components/CodeMirror.vue +1 -1
  18. package/components/Drawer/Chrome.vue +0 -1
  19. package/components/Drawer/ResourceDetailDrawer/__tests__/composables.test.ts +27 -28
  20. package/components/Drawer/ResourceDetailDrawer/composables.ts +4 -24
  21. package/components/Drawer/ResourceDetailDrawer/index.vue +18 -4
  22. package/components/InstallHelmCharts.vue +656 -0
  23. package/components/LazyImage.vue +60 -4
  24. package/components/Loading.vue +1 -1
  25. package/components/LocaleSelector.vue +7 -2
  26. package/components/Markdown.vue +4 -0
  27. package/components/PaginatedResourceTable.vue +46 -1
  28. package/components/PromptRestore.vue +22 -44
  29. package/components/Resource/Detail/Masthead/composable.ts +16 -0
  30. package/components/Resource/Detail/Masthead/index.vue +37 -0
  31. package/components/Resource/Detail/Metadata/IdentifyingInformation/composable.ts +10 -2
  32. package/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields.ts +26 -7
  33. package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +8 -1
  34. package/components/Resource/Detail/Metadata/KeyValue.vue +12 -10
  35. package/components/Resource/Detail/Metadata/Rectangle.vue +3 -1
  36. package/components/Resource/Detail/Metadata/__tests__/composables.test.ts +10 -17
  37. package/components/Resource/Detail/Metadata/composables.ts +9 -7
  38. package/components/Resource/Detail/Metadata/index.vue +17 -2
  39. package/components/Resource/Detail/Page.vue +35 -21
  40. package/components/Resource/Detail/SpacedRow.vue +1 -1
  41. package/components/Resource/Detail/TitleBar/__tests__/composables.test.ts +8 -9
  42. package/components/Resource/Detail/TitleBar/composables.ts +5 -5
  43. package/components/Resource/Detail/TitleBar/index.vue +12 -3
  44. package/components/ResourceDetail/Masthead/legacy.vue +1 -1
  45. package/components/ResourceDetail/index.vue +569 -72
  46. package/components/ResourceList/index.vue +1 -0
  47. package/components/ResourceTable.vue +6 -1
  48. package/components/ResourceYaml.vue +1 -1
  49. package/components/RichTranslation.vue +106 -0
  50. package/components/SlideInPanelManager.vue +13 -10
  51. package/components/SortableTable/index.vue +5 -5
  52. package/components/SortableTable/selection.js +0 -1
  53. package/components/Tabbed/index.vue +35 -4
  54. package/components/__tests__/LazyImage.spec.ts +121 -0
  55. package/components/__tests__/PromptRestore.test.ts +1 -65
  56. package/components/__tests__/RichTranslation.test.ts +115 -0
  57. package/components/fleet/FleetStatus.vue +4 -0
  58. package/components/fleet/dashboard/ResourcePanel.vue +2 -1
  59. package/components/form/ClusterAppearance.vue +5 -0
  60. package/components/form/FileImageSelector.vue +1 -1
  61. package/components/form/Members/ClusterPermissionsEditor.vue +1 -1
  62. package/components/form/NameNsDescription.vue +1 -0
  63. package/components/form/Networking.vue +24 -19
  64. package/components/form/ProjectMemberEditor.vue +1 -1
  65. package/components/form/ResourceLabeledSelect.vue +22 -8
  66. package/components/form/ResourceTabs/index.vue +20 -0
  67. package/components/form/SecretSelector.vue +9 -0
  68. package/components/form/SelectOrCreateAuthSecret.vue +6 -3
  69. package/components/form/__tests__/Networking.test.ts +116 -0
  70. package/components/form/labeled-select-utils/labeled-select-pagination.ts +3 -38
  71. package/components/formatter/FleetApplicationSource.vue +25 -17
  72. package/components/formatter/PodImages.vue +1 -1
  73. package/components/formatter/__tests__/LiveDate.test.ts +10 -2
  74. package/components/google/AccountAccess.vue +44 -46
  75. package/components/nav/Favorite.vue +4 -0
  76. package/components/nav/Group.vue +4 -1
  77. package/components/nav/NotificationCenter/Notification.vue +1 -27
  78. package/components/nav/WindowManager/index.vue +3 -3
  79. package/composables/resources.ts +2 -2
  80. package/config/labels-annotations.js +3 -2
  81. package/config/pagination-table-headers.js +8 -1
  82. package/config/product/explorer.js +27 -2
  83. package/config/product/manager.js +0 -1
  84. package/config/query-params.js +10 -0
  85. package/config/router/routes.js +21 -1
  86. package/config/system-namespaces.js +1 -1
  87. package/config/table-headers.js +30 -1
  88. package/config/types.js +1 -1
  89. package/config/version.js +1 -1
  90. package/detail/__tests__/provisioning.cattle.io.cluster.test.ts +11 -0
  91. package/detail/__tests__/workload.test.ts +164 -0
  92. package/detail/configmap.vue +33 -75
  93. package/detail/projectsecret.vue +11 -0
  94. package/detail/provisioning.cattle.io.cluster.vue +351 -369
  95. package/detail/secret.vue +49 -308
  96. package/detail/workload/index.vue +38 -21
  97. package/dialog/InstallExtensionDialog.vue +8 -5
  98. package/dialog/RotateEncryptionKeyDialog.vue +10 -30
  99. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +224 -0
  100. package/edit/auth/ldap/__tests__/config.test.ts +14 -0
  101. package/edit/auth/ldap/config.vue +24 -0
  102. package/edit/compliance.cattle.io.clusterscan.vue +1 -1
  103. package/edit/configmap.vue +4 -1
  104. package/edit/fleet.cattle.io.gitrepo.vue +5 -6
  105. package/edit/fleet.cattle.io.helmop.vue +78 -56
  106. package/edit/logging.banzaicloud.io.output/index.vue +1 -1
  107. package/edit/logging.banzaicloud.io.output/providers/awsElasticsearch.vue +5 -6
  108. package/edit/networking.k8s.io.ingress/Certificate.vue +20 -22
  109. package/edit/networking.k8s.io.ingress/DefaultBackend.vue +8 -3
  110. package/edit/networking.k8s.io.ingress/Rule.vue +2 -5
  111. package/edit/networking.k8s.io.ingress/RulePath.vue +17 -11
  112. package/edit/networking.k8s.io.ingress/__tests__/Certificate.test.ts +165 -0
  113. package/edit/networking.k8s.io.networkpolicy/PolicyRuleTarget.vue +11 -10
  114. package/edit/networking.k8s.io.networkpolicy/PolicyRules.vue +1 -3
  115. package/edit/networking.k8s.io.networkpolicy/index.vue +17 -17
  116. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +3 -2
  117. package/edit/provisioning.cattle.io.cluster/rke2.vue +123 -61
  118. package/edit/provisioning.cattle.io.cluster/tabs/AgentConfiguration.vue +9 -7
  119. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +22 -13
  120. package/edit/provisioning.cattle.io.cluster/tabs/DirectoryConfig.vue +10 -12
  121. package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +39 -38
  122. package/edit/provisioning.cattle.io.cluster/tabs/etcd/S3Config.vue +41 -19
  123. package/edit/provisioning.cattle.io.cluster/tabs/etcd/index.vue +16 -3
  124. package/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryConfigs.vue +32 -33
  125. package/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryMirrors.vue +9 -10
  126. package/edit/provisioning.cattle.io.cluster/tabs/registries/index.vue +1 -3
  127. package/edit/provisioning.cattle.io.cluster/tabs/upgrade/DrainOptions.vue +16 -9
  128. package/edit/secret/basic.vue +1 -0
  129. package/edit/secret/index.vue +126 -15
  130. package/edit/workload/index.vue +5 -14
  131. package/list/projectsecret.vue +345 -0
  132. package/list/provisioning.cattle.io.cluster.vue +1 -69
  133. package/list/secret.vue +109 -0
  134. package/machine-config/__tests__/vmwarevsphere.test.ts +5 -7
  135. package/machine-config/google.vue +9 -1
  136. package/machine-config/vmwarevsphere.vue +7 -17
  137. package/mixins/__tests__/brand.spec.ts +2 -2
  138. package/mixins/chart.js +0 -2
  139. package/mixins/create-edit-view/impl.js +10 -1
  140. package/mixins/resource-fetch-api-pagination.js +11 -12
  141. package/mixins/resource-fetch.js +3 -1
  142. package/models/__tests__/chart.test.ts +111 -80
  143. package/models/__tests__/fleet.cattle.io.helmop.test.ts +224 -0
  144. package/models/__tests__/node.test.ts +7 -63
  145. package/models/catalog.cattle.io.app.js +1 -1
  146. package/models/catalog.cattle.io.operation.js +1 -1
  147. package/models/chart.js +36 -20
  148. package/models/cloudcredential.js +2 -163
  149. package/models/cluster/node.js +7 -7
  150. package/models/cluster.x-k8s.io.machine.js +3 -3
  151. package/models/cluster.x-k8s.io.machinedeployment.js +11 -2
  152. package/models/compliance.cattle.io.clusterscan.js +2 -2
  153. package/models/configmap.js +4 -0
  154. package/models/constraints.gatekeeper.sh.constraint.js +1 -1
  155. package/models/fleet-application.js +0 -17
  156. package/models/fleet.cattle.io.cluster.js +2 -2
  157. package/models/fleet.cattle.io.gitrepo.js +15 -1
  158. package/models/fleet.cattle.io.helmop.js +26 -22
  159. package/models/management.cattle.io.setting.js +4 -0
  160. package/models/persistentvolumeclaim.js +1 -1
  161. package/models/pod.js +2 -2
  162. package/models/provisioning.cattle.io.cluster.js +39 -67
  163. package/models/rke.cattle.io.etcdsnapshot.js +1 -1
  164. package/models/secret.js +161 -2
  165. package/models/storage.k8s.io.storageclass.js +2 -2
  166. package/models/workload.js +3 -3
  167. package/package.json +11 -10
  168. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +1 -0
  169. package/pages/c/_cluster/apps/charts/AppChartCardSubHeader.vue +4 -1
  170. package/pages/c/_cluster/apps/charts/__tests__/AppChartCardFooter.spec.js +41 -0
  171. package/pages/c/_cluster/apps/charts/chart.vue +422 -174
  172. package/pages/c/_cluster/apps/charts/index.vue +46 -35
  173. package/pages/c/_cluster/apps/charts/install.vue +1 -1
  174. package/pages/c/_cluster/explorer/projectsecret.vue +24 -0
  175. package/pages/c/_cluster/fleet/__tests__/index.test.ts +608 -314
  176. package/pages/c/_cluster/fleet/index.vue +103 -45
  177. package/pages/c/_cluster/manager/cloudCredential/index.vue +2 -59
  178. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +10 -3
  179. package/pages/c/_cluster/uiplugins/index.vue +36 -25
  180. package/plugins/dashboard-store/__tests__/normalize.test.ts +223 -0
  181. package/plugins/dashboard-store/__tests__/resource-class.test.ts +191 -0
  182. package/plugins/dashboard-store/__tests__/utils/normalize-usecases.ts +1526 -0
  183. package/plugins/dashboard-store/actions.js +42 -22
  184. package/plugins/dashboard-store/normalize.js +29 -17
  185. package/plugins/dashboard-store/resource-class.js +83 -17
  186. package/plugins/steve/__tests__/getters.test.ts +1 -1
  187. package/plugins/steve/__tests__/subscribe.spec.ts +259 -1
  188. package/plugins/steve/getters.js +8 -2
  189. package/plugins/steve/resourceWatcher.js +10 -3
  190. package/plugins/steve/steve-pagination-utils.ts +14 -3
  191. package/plugins/steve/subscribe.js +192 -19
  192. package/plugins/steve/worker/web-worker.advanced.js +2 -0
  193. package/rancher-components/Card/Card.vue +0 -18
  194. package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.test.ts +15 -0
  195. package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +65 -0
  196. package/rancher-components/Pill/RcStatusBadge/index.ts +2 -0
  197. package/rancher-components/Pill/RcStatusBadge/types.ts +5 -0
  198. package/rancher-components/Pill/RcStatusIndicator/RcStatusIndicator.test.ts +33 -0
  199. package/rancher-components/Pill/RcStatusIndicator/RcStatusIndicator.vue +75 -0
  200. package/rancher-components/Pill/RcStatusIndicator/index.ts +2 -0
  201. package/rancher-components/Pill/RcStatusIndicator/types.ts +7 -0
  202. package/rancher-components/Pill/types.ts +2 -0
  203. package/rancher-components/RcButton/RcButton.vue +1 -1
  204. package/rancher-components/RcDropdown/RcDropdown.test.ts +98 -0
  205. package/rancher-components/RcDropdown/RcDropdown.vue +5 -0
  206. package/rancher-components/RcDropdown/RcDropdownItem.vue +7 -1
  207. package/rancher-components/RcDropdown/RcDropdownItemCheckbox.vue +2 -1
  208. package/rancher-components/RcDropdown/RcDropdownItemSelect.vue +2 -1
  209. package/rancher-components/RcDropdown/useDropdownContext.ts +21 -0
  210. package/rancher-components/RcDropdown/useDropdownItem.ts +30 -1
  211. package/rancher-components/RcItemCard/RcItemCard.test.ts +20 -0
  212. package/rancher-components/RcItemCard/RcItemCard.vue +40 -6
  213. package/store/__tests__/catalog.test.ts +93 -1
  214. package/store/aws.js +19 -8
  215. package/store/catalog.js +8 -3
  216. package/types/kube/kube-api.ts +12 -0
  217. package/types/resources/settings.d.ts +1 -1
  218. package/types/shell/index.d.ts +643 -585
  219. package/types/store/pagination.types.ts +16 -6
  220. package/types/uiplugins.ts +73 -0
  221. package/utils/__tests__/back-off.test.ts +354 -0
  222. package/utils/__tests__/create-yaml.test.ts +235 -0
  223. package/utils/__tests__/kontainer.test.ts +19 -0
  224. package/utils/__tests__/uiplugins.test.ts +84 -0
  225. package/utils/back-off.ts +176 -0
  226. package/utils/create-yaml.js +103 -9
  227. package/utils/dynamic-importer.js +8 -0
  228. package/utils/kontainer.ts +3 -5
  229. package/utils/pagination-utils.ts +18 -0
  230. package/utils/style.ts +3 -0
  231. package/utils/uiplugins.ts +29 -2
  232. package/utils/validators/__tests__/setting.test.js +92 -0
  233. package/utils/validators/formRules/__tests__/index.test.ts +88 -7
  234. package/utils/validators/formRules/index.ts +83 -8
  235. package/utils/validators/setting.js +17 -0
  236. package/cloud-credential/__tests__/harvester.test.ts +0 -18
  237. package/components/ResourceDetail/__tests__/index.test.ts +0 -135
  238. package/components/ResourceDetail/legacy.vue +0 -562
  239. package/components/formatter/CloudCredExpired.vue +0 -69
  240. package/models/etcdbackup.js +0 -45
  241. package/pages/explorer/resource/detail/configmap.vue +0 -42
  242. package/pages/explorer/resource/detail/secret.vue +0 -50
  243. package/utils/aws.js +0 -0
@@ -79,6 +79,29 @@ const findAllGetter = (getters, type, opt) => {
79
79
  return opt.namespaced ? getters.matching(type, null, opt.namespaced, { skipSelector: true }) : getters.all(type);
80
80
  };
81
81
 
82
+ const createFindWatchArg = ({
83
+ type, id, opt, res
84
+ }) => {
85
+ const revision = typeof opt.revision !== 'undefined' ? opt.revision : res?.metadata?.resourceVersion;
86
+ const watchMsg = {
87
+ type,
88
+ id,
89
+ // Although not used by sockets, we need this for when resyncWatch calls find... which needs namespace to construct the url
90
+ namespace: opt.namespaced,
91
+ revision: revision || '',
92
+ force: opt.forceWatch === true,
93
+ };
94
+
95
+ const idx = id.indexOf('/');
96
+
97
+ if ( idx > 0 ) {
98
+ watchMsg.namespace = id.substr(0, idx);
99
+ watchMsg.id = id.substr(idx + 1);
100
+ }
101
+
102
+ return watchMsg;
103
+ };
104
+
82
105
  export default {
83
106
  request() {
84
107
  throw new Error('Not Implemented');
@@ -657,6 +680,12 @@ export default {
657
680
  out = getters.byId(type, id);
658
681
 
659
682
  if ( out ) {
683
+ if ( opt.watch !== false ) {
684
+ dispatch('watch', createFindWatchArg({
685
+ type, id, opt, res: undefined
686
+ }));
687
+ }
688
+
660
689
  return out;
661
690
  }
662
691
  }
@@ -669,26 +698,9 @@ export default {
669
698
  await dispatch('load', { data: res });
670
699
 
671
700
  if ( opt.watch !== false ) {
672
- const watchMsg = {
673
- type,
674
- id,
675
- // Although not used by sockets, we need this for when resyncWatch calls find... which needs namespace to construct the url
676
- namespace: opt.namespaced,
677
- // Override the revision. Used in cases where we need to avoid using the resource's own revision which would be `too old`.
678
- // For the above case opt.revision will be `null`. If left as `undefined` the subscribe mechanism will try to determine a revision
679
- // from resources in store (which would be this one, with the too old revision)
680
- revision: typeof opt.revision !== 'undefined' ? opt.revision : res?.metadata?.resourceVersion,
681
- force: opt.forceWatch === true,
682
- };
683
-
684
- const idx = id.indexOf('/');
685
-
686
- if ( idx > 0 ) {
687
- watchMsg.namespace = id.substr(0, idx);
688
- watchMsg.id = id.substr(idx + 1);
689
- }
690
-
691
- dispatch('watch', watchMsg);
701
+ dispatch('watch', createFindWatchArg({
702
+ type, id, opt, res
703
+ }));
692
704
  }
693
705
 
694
706
  out = getters.byId(type, id);
@@ -808,13 +820,21 @@ export default {
808
820
  return classify(ctx, resource.toJSON(), true);
809
821
  },
810
822
 
811
- // Forget a type in the store
812
- // Remove all entries for that type and stop watching it
823
+ /**
824
+ * Remove all cached entries for a resource and stop watches
825
+ */
813
826
  forgetType({ commit, dispatch, state }, type, compareWatches) {
827
+ // Stop all known watches
814
828
  state.started
815
829
  .filter((entry) => compareWatches ? compareWatches(entry) : entry.type === type)
816
830
  .forEach((entry) => dispatch('unwatch', entry));
817
831
 
832
+ // Stop all known back-off watch processes for this type
833
+ dispatch('resetWatchBackOff', {
834
+ type, compareWatches, resetStarted: false
835
+ });
836
+
837
+ // Remove entries from store
818
838
  commit('forgetType', type);
819
839
  },
820
840
 
@@ -17,28 +17,40 @@ export function normalizeType(type) {
17
17
  return type;
18
18
  }
19
19
 
20
- // Detect and resolve conflicts from a 409 response.
21
- // If they are resolved, return a false-y value
22
- // Else they can't be resolved, return an array of errors to show to the user.
23
- export async function handleConflict(initialValueJSON, value, liveValue, rootGetters, store, storeNamespace) {
24
- const orig = await store.dispatch(`${ storeNamespace }/cleanForDiff`, initialValueJSON, { root: true });
25
- const user = await store.dispatch(`${ storeNamespace }/cleanForDiff`, value.toJSON(), { root: true });
26
- const cur = await store.dispatch(`${ storeNamespace }/cleanForDiff`, liveValue.toJSON(), { root: true });
27
-
28
- const bgChange = changeset(orig, cur);
29
- const userChange = changeset(orig, user);
30
- const actualConflicts = changesetConflicts(bgChange, userChange);
31
-
32
- console.log('Background Change', bgChange); // eslint-disable-line no-console
33
- console.log('User Change', userChange); // eslint-disable-line no-console
20
+ /**
21
+ * Detect and resolve conflicts from a 409 response.
22
+ *
23
+ * @param {*} initialValue the initial value before changes
24
+ * @param {*} userValue the value containing the local changes. this function will intentionally mutate this to contain changes made from the server
25
+ * @param {*} serverValue the very latest value from the server
26
+ * @returns If `value` has been successfully updated return a false-y value. Else they can't be resolved, return an array of errors to show the user.
27
+ */
28
+ export async function handleConflict(initialValue, userValue, serverValue, store, storeNamespace, toJSON = (x) => x.toJSON()) {
29
+ // initial value
30
+ const initial = await store.dispatch(`${ storeNamespace }/cleanForDiff`, toJSON(initialValue), { root: true });
31
+ // changed value (user edits)
32
+ const user = await store.dispatch(`${ storeNamespace }/cleanForDiff`, toJSON(userValue), { root: true });
33
+ // server value
34
+ const server = await store.dispatch(`${ storeNamespace }/cleanForDiff`, toJSON(serverValue), { root: true });
35
+
36
+ // changes made to the server value
37
+ const serverChanges = changeset(initial, server);
38
+ // changes made locally
39
+ const userChanges = changeset(initial, user);
40
+ // Any incompatibilities between changes made locally and the server?
41
+ const actualConflicts = changesetConflicts(serverChanges, userChanges);
42
+
43
+ console.log('Background Change', serverChanges); // eslint-disable-line no-console
44
+ console.log('User Change', userChanges); // eslint-disable-line no-console
34
45
  console.log('Conflicts', actualConflicts); // eslint-disable-line no-console
35
46
 
36
- value.metadata.resourceVersion = liveValue.metadata.resourceVersion;
37
- applyChangeset(value, bgChange);
47
+ userValue.metadata.resourceVersion = serverValue.metadata.resourceVersion;
48
+ // Apply changes made on the server to the changed (user) value
49
+ applyChangeset(userValue, serverChanges);
38
50
 
39
51
  if ( actualConflicts.length ) {
40
52
  // Stop the save and let the user inspect and continue editing
41
- const out = [rootGetters['i18n/t']('validation.conflict', { fields: actualConflicts.join(', '), fieldCount: actualConflicts.length })];
53
+ const out = [store.getters['i18n/t']('validation.conflict', { fields: actualConflicts.join(', '), fieldCount: actualConflicts.length })];
42
54
 
43
55
  return out;
44
56
  } else {
@@ -32,9 +32,11 @@ import isFunction from 'lodash/isFunction';
32
32
  import isString from 'lodash/isString';
33
33
  import { markRaw } from 'vue';
34
34
 
35
+ import { handleConflict } from '@shell/plugins/dashboard-store/normalize';
35
36
  import { ExtensionPoint, ActionLocation } from '@shell/core/types';
36
37
  import { getApplicableExtensionEnhancements } from '@shell/core/plugin-helpers';
37
38
  import { parse } from '@shell/utils/selector';
39
+ import { importDrawer } from '@shell/utils/dynamic-importer';
38
40
 
39
41
  export const DNS_LIKE_TYPES = ['dnsLabel', 'dnsLabelRestricted', 'hostname'];
40
42
 
@@ -858,6 +860,10 @@ export default class Resource {
858
860
 
859
861
  // ------------------------------------------------------------------
860
862
 
863
+ get canEdit() {
864
+ return this.canUpdate && this.canCustomEdit;
865
+ }
866
+
861
867
  get availableActions() {
862
868
  const all = this._availableActions;
863
869
 
@@ -897,6 +903,26 @@ export default class Resource {
897
903
  return out;
898
904
  }
899
905
 
906
+ showConfiguration(returnFocusSelector) {
907
+ const onClose = () => this.$ctx.commit('slideInPanel/close', undefined, { root: true });
908
+
909
+ this.$ctx.commit('slideInPanel/open', {
910
+ component: importDrawer('ResourceDetailDrawer'),
911
+ componentProps: {
912
+ resource: this,
913
+ onClose,
914
+ width: '73%',
915
+ // We want this to be full viewport height top to bottom
916
+ height: '100vh',
917
+ top: '0',
918
+ 'z-index': 101, // We want this to be above the main side menu
919
+ closeOnRouteChange: ['name', 'params', 'query'], // We want to ignore hash changes, tables in extensions can trigger the drawer to close while opening
920
+ triggerFocusTrap: true,
921
+ returnFocusSelector
922
+ }
923
+ }, { root: true });
924
+ }
925
+
900
926
  // You can add custom actions by overriding your own availableActions (and probably reading super._availableActions)
901
927
  get _availableActions() {
902
928
  // get menu actions available by plugins configuration
@@ -904,6 +930,12 @@ export default class Resource {
904
930
  const extensionMenuActions = getApplicableExtensionEnhancements(this.$rootState, ExtensionPoint.ACTION, ActionLocation.TABLE, currentRoute, this);
905
931
 
906
932
  const all = [
933
+ {
934
+ action: 'showConfiguration',
935
+ label: this.t('action.showConfiguration'),
936
+ icon: 'icon icon-document',
937
+ enabled: this.disableResourceDetailDrawer !== true && (this.canCustomEdit || this.canYaml), // If the resource can't show an edit or a yaml we don't want to show the configuration drawer
938
+ },
907
939
  { divider: true },
908
940
  {
909
941
  action: this.canUpdate ? 'goToEdit' : 'goToViewConfig',
@@ -1343,9 +1375,7 @@ export default class Resource {
1343
1375
  this.currentRouter().push(location);
1344
1376
  }
1345
1377
 
1346
- goToEdit(moreQuery = {}) {
1347
- const location = this.detailLocation;
1348
-
1378
+ goToEdit(moreQuery = {}, location = this.detailLocation) {
1349
1379
  location.query = {
1350
1380
  ...location.query,
1351
1381
  [MODE]: _EDIT,
@@ -1528,11 +1558,11 @@ export default class Resource {
1528
1558
  }
1529
1559
  }
1530
1560
 
1531
- async saveYaml(yaml) {
1532
- await this._saveYaml(yaml);
1561
+ async saveYaml(yaml, initialYaml) {
1562
+ await this._saveYaml(yaml, initialYaml);
1533
1563
  }
1534
1564
 
1535
- async _saveYaml(yaml) {
1565
+ async _saveYaml(yaml, initialYaml, depth = 0) {
1536
1566
  /* Multipart support, but need to know the right cluster and work for management store
1537
1567
  and "apply" seems to only work for create, not update.
1538
1568
 
@@ -1570,20 +1600,56 @@ export default class Resource {
1570
1600
  data: yaml
1571
1601
  });
1572
1602
  } else {
1573
- res = await this.followLink('update', {
1574
- method: 'PUT',
1575
- headers,
1576
- data: yaml
1577
- });
1603
+ try {
1604
+ res = await this.followLink('update', {
1605
+ method: 'PUT',
1606
+ headers,
1607
+ data: yaml
1608
+ });
1609
+ } catch (err) {
1610
+ const IS_ERR_409 = err.status === 409 || err._status === 409;
1611
+
1612
+ // Conflict, the resource being edited has changed since starting editing
1613
+ if (IS_ERR_409 && depth === 0 && initialYaml) {
1614
+ const inStore = this.$rootGetters['currentStore'](this.type);
1615
+
1616
+ const initialValue = jsyaml.load(initialYaml);
1617
+ const value = jsyaml.load(yaml);
1618
+ const liveValue = this.$rootGetters[`${ inStore }/byId`](this.type, this.id);
1619
+
1620
+ const handledConflictErr = await handleConflict(
1621
+ initialValue,
1622
+ value,
1623
+ liveValue,
1624
+ {
1625
+ dispatch: this.$dispatch,
1626
+ getters: this.$rootGetters
1627
+ },
1628
+ this.$rootGetters['currentStore'](this.type),
1629
+ (v) => v.toJSON ? v.toJSON() : v
1630
+ );
1631
+
1632
+ if (handledConflictErr === false) {
1633
+ // It was automatically figured out, save again
1634
+ await this._saveYaml(jsyaml.dump(value), null, depth + 1);
1635
+ } else {
1636
+ throw handledConflictErr;
1637
+ }
1638
+ } else {
1639
+ throw err;
1640
+ }
1641
+ }
1578
1642
  }
1579
1643
 
1580
- await this.$dispatch(`load`, {
1581
- data: res,
1582
- existing: (isCreate ? this : undefined)
1583
- });
1644
+ if (res) {
1645
+ await this.$dispatch(`load`, {
1646
+ data: res,
1647
+ existing: (isCreate ? this : undefined)
1648
+ });
1584
1649
 
1585
- if (this.isSpoofed) {
1586
- await this.$dispatch('cluster/findAll', { type: this.type, opt: { force: true } }, { root: true });
1650
+ if (this.isSpoofed) {
1651
+ await this.$dispatch('cluster/findAll', { type: this.type, opt: { force: true } }, { root: true });
1652
+ }
1587
1653
  }
1588
1654
  }
1589
1655
 
@@ -120,7 +120,7 @@ describe('steve: getters:', () => {
120
120
  expect(urlOptionsGetter('/v1/foo', { excludeFields: ['bar'] })).toBe('/v1/foo?exclude=bar');
121
121
  });
122
122
  it('returns a string without an exclude statement for "managedFields" if omitExcludeFields includes it and the url starts with "/v1/"', () => {
123
- expect(urlOptionsGetter('/v1/foo', { omitExcludeFields: ['metadata.managedFields'] })).toBe('/v1/foo?');
123
+ expect(urlOptionsGetter('/v1/foo', { omitExcludeFields: ['metadata.managedFields'] })).toBe('/v1/foo');
124
124
  });
125
125
  it('returns a string without an exclude statement if excludeFields is set but the url does not start with "/v1/"', () => {
126
126
  expect(urlOptionsGetter('foo', { excludeFields: ['bar'] })).toBe('foo');
@@ -1,4 +1,7 @@
1
- import { actions, getters } from '../subscribe';
1
+ import { actions, getters, mutations } from '../subscribe';
2
+ import { REVISION_TOO_OLD } from '../../../utils/socket';
3
+ import { STEVE_WATCH_EVENT } from '../../../types/store/subscribe.types';
4
+ import backOff from '../../../utils/back-off';
2
5
 
3
6
  describe('steve: subscribe', () => {
4
7
  describe('actions', () => {
@@ -171,4 +174,259 @@ describe('steve: subscribe', () => {
171
174
  });
172
175
  });
173
176
  });
177
+
178
+ describe('backoff', () => {
179
+ const waitForBackOff = async(advanceTimersByTime = 20000) => {
180
+ jest.advanceTimersByTime(advanceTimersByTime);
181
+ // jest.advanceTimersByTime(advanceTimersByTime);
182
+ await Promise.resolve();
183
+ await Promise.resolve();
184
+ await Promise.resolve();
185
+ };
186
+
187
+ describe('stale cache in replicate that handles watch', () => {
188
+ /**
189
+ 1. ui makes http request.
190
+ - it's handled by up-to-date replica A
191
+ - response contains an up-to-date revision X
192
+ 2. ui makes watch request referencing up-to-date revision X from A
193
+ - it's received by replica B with a stale cache which does not contain revision X.
194
+ - replicate B rejects watch with unknown revision message (i.e. 'too old')
195
+ 3. ui receives unknown revision and makes a new request
196
+ - this should backoff until eventually succeeding
197
+ */
198
+
199
+ const startWatch = ({
200
+ ctx,
201
+ obj, msg,
202
+ revision
203
+ }) => {
204
+ const {
205
+ state, dispatch, getters, rootGetters, commit
206
+ } = ctx;
207
+
208
+ // call watch
209
+ actions.watch({
210
+ state, dispatch, getters, rootGetters
211
+ }, {
212
+ ...obj,
213
+ revision,
214
+ mode: STEVE_WATCH_EVENT.CHANGES,
215
+ force: true,
216
+ });
217
+
218
+ expect(dispatch).toHaveBeenNthCalledWith(1, 'unwatchIncompatible', {
219
+ id: undefined, mode: STEVE_WATCH_EVENT.CHANGES, namespace: undefined, selector: undefined, type: obj.type
220
+ });
221
+
222
+ expect(dispatch).toHaveBeenNthCalledWith(2, 'send', {
223
+ debounceMs: 4000,
224
+ mode: 'resource.changes',
225
+ resourceType: obj.type,
226
+ resourceVersion: revision.toString(),
227
+ });
228
+
229
+ // Receive start from BE
230
+ actions['ws.resource.start']({
231
+ state, dispatch, getters, commit
232
+ }, { ...msg });
233
+
234
+ expect(dispatch).toHaveBeenCalledTimes(2);
235
+ dispatch.mockClear();
236
+ };
237
+
238
+ const errorWatch = ({
239
+ ctx,
240
+ obj, msg,
241
+ }) => {
242
+ const {
243
+ state, dispatch, getters, commit
244
+ } = ctx;
245
+
246
+ // Receive error from BE
247
+ actions['ws.resource.error']({
248
+ dispatch, getters, commit
249
+ }, {
250
+ ...msg,
251
+ data: { error: 'too old' }
252
+ });
253
+ expect(state.inError).toStrictEqual(
254
+ {
255
+ 'type=abc,namespace=,id=,selector=': {
256
+ obj: {
257
+ type: msg.resourceType,
258
+ mode: msg.mode,
259
+ },
260
+ reason: REVISION_TOO_OLD
261
+ }
262
+ }
263
+ );
264
+
265
+ // Receive stop from BE
266
+ actions['ws.resource.stop']({
267
+ state, dispatch, getters, commit
268
+ }, { ...msg });
269
+ // stop tries to watch again, however we're in error so will be ignored
270
+ expect(dispatch).toHaveBeenNthCalledWith(1, 'watch', {
271
+ id: undefined, mode: STEVE_WATCH_EVENT.CHANGES, namespace: undefined, selector: undefined, type: obj.type
272
+ });
273
+
274
+ dispatch.mockClear();
275
+ };
276
+
277
+ const cycleFail = async({
278
+ ctx,
279
+ obj, msg,
280
+ revision,
281
+ tooManyTries = false,
282
+ }) => {
283
+ const { dispatch } = ctx;
284
+
285
+ startWatch({
286
+ ctx, obj, msg, revision
287
+ });
288
+ errorWatch({
289
+ ctx, obj, msg
290
+ });
291
+
292
+ await waitForBackOff(50000);
293
+ await waitForBackOff(50000);
294
+
295
+ if (tooManyTries) {
296
+ expect(dispatch).toHaveBeenCalledTimes(0);
297
+ } else {
298
+ expect(dispatch).toHaveBeenCalledTimes(1);
299
+ expect(dispatch).toHaveBeenCalledWith('resyncWatch', {
300
+ ...msg,
301
+ data: { error: 'too old' }
302
+ });
303
+ }
304
+
305
+ await waitForBackOff();
306
+
307
+ if (tooManyTries) {
308
+ expect(dispatch).toHaveBeenCalledTimes(0);
309
+ } else {
310
+ expect(dispatch).toHaveBeenCalledTimes(1);
311
+ }
312
+
313
+ dispatch.mockClear();
314
+ };
315
+
316
+ const cycleSucceed = async({
317
+ ctx,
318
+ obj, msg,
319
+ revision
320
+ }) => {
321
+ const { dispatch } = ctx;
322
+
323
+ dispatch.mockImplementation(async(type: string) => {
324
+ if (type === 'resyncWatch') {
325
+ return Promise.resolve();
326
+ }
327
+ });
328
+
329
+ startWatch({
330
+ ctx, obj, msg, revision
331
+ });
332
+
333
+ await waitForBackOff();
334
+
335
+ expect(dispatch).toHaveBeenCalledTimes(0);
336
+
337
+ await waitForBackOff();
338
+
339
+ expect(dispatch).toHaveBeenCalledTimes(0);
340
+
341
+ dispatch.mockClear();
342
+ };
343
+
344
+ const dispatch = jest.fn();
345
+ const rootGetters = {
346
+ 'type-map/isSpoofed': () => false,
347
+ 'management/byId': () => ({ value: true })
348
+ };
349
+ const obj = { type: 'abc' };
350
+ const msg = {
351
+ resourceType: obj.type,
352
+ mode: STEVE_WATCH_EVENT.CHANGES,
353
+ };
354
+
355
+ const initStore = () => {
356
+ const state = { started: [], inError: {} };
357
+ const _getters = {
358
+ normalizeType: (type: string) => type,
359
+ schemaFor: () => ({}),
360
+ storeName: 'test',
361
+ inError: (...args) => getters.inError(state)(...args),
362
+ watchStarted: (...args) => getters.watchStarted(state)(...args),
363
+ backOffId: (...args) => getters.backOffId()(...args),
364
+ canBackoff: () => true,
365
+ };
366
+ const commit = (type, ...args) => mutations[type](state, ...args);
367
+
368
+ return {
369
+ state, dispatch, getters: _getters, rootGetters, commit
370
+ };
371
+ };
372
+
373
+ beforeAll(() => {
374
+ jest.useFakeTimers();
375
+ });
376
+
377
+ afterEach(() => {
378
+ backOff.resetAll();
379
+ dispatch.mockClear();
380
+ });
381
+
382
+ // eslint-disable-next-line jest/expect-expect
383
+ it('succeeds', async() => {
384
+ jest.useFakeTimers();
385
+
386
+ const ctx = initStore();
387
+
388
+ await cycleSucceed({
389
+ ctx, msg, obj, revision: 1
390
+ });
391
+ });
392
+
393
+ // eslint-disable-next-line jest/expect-expect
394
+ it('succeeds after a few failures', async() => {
395
+ jest.useFakeTimers();
396
+
397
+ const ctx = initStore();
398
+
399
+ await cycleFail({
400
+ ctx, msg, obj, revision: 1
401
+ });
402
+ await cycleFail({
403
+ ctx, msg, obj, revision: 1
404
+ });
405
+ await cycleFail({
406
+ ctx, msg, obj, revision: 1
407
+ });
408
+ await cycleFail({
409
+ ctx, msg, obj, revision: 1
410
+ });
411
+ await cycleSucceed({
412
+ ctx, msg, obj, revision: 1
413
+ });
414
+ });
415
+
416
+ // eslint-disable-next-line jest/expect-expect
417
+ it('never succeeds', async() => {
418
+ const ctx = initStore();
419
+
420
+ for (let i = 0; i < 10; i++) {
421
+ await cycleFail({
422
+ ctx, msg, obj, revision: 1
423
+ });
424
+ }
425
+
426
+ await cycleFail({
427
+ ctx, msg, obj, revision: 1, tooManyTries: true
428
+ });
429
+ });
430
+ });
431
+ });
174
432
  });
@@ -154,9 +154,15 @@ export default {
154
154
  opt.excludeFields = Array.isArray(opt?.omitExcludeFields) ? excludeFields.filter((f) => !f.includes(opt.omitExcludeFields)) : excludeFields;
155
155
  }
156
156
 
157
- const excludeParamsString = opt.excludeFields.map((field) => `exclude=${ field }`).join('&');
157
+ if (opt.excludeFields.length) {
158
+ const excludeParamsString = opt.excludeFields.map((field) => `exclude=${ field }`).join('&');
158
159
 
159
- url += `${ url.includes('?') ? '&' : '?' }${ excludeParamsString }`;
160
+ url += `${ url.includes('?') ? '&' : '?' }${ excludeParamsString }`;
161
+ }
162
+
163
+ if (opt.revision) {
164
+ url += `${ url.includes('?') ? '&' : '?' }${ `revision=${ opt.revision }` }`;
165
+ }
160
166
  }
161
167
  // End: Exclude
162
168
 
@@ -36,12 +36,19 @@ export const WATCH_STATUSES = {
36
36
  REMOVE_REQUESTED: 'removed_requested'
37
37
  };
38
38
 
39
+ /**
40
+ * Create a unique key for a specific resource watch's params
41
+ */
39
42
  export const keyForSubscribe = ({
40
43
  resourceType, type, namespace, id, selector
41
44
  } = {}) => {
42
- return [(resourceType || type), namespace, id, selector] // each watch param in an array
43
- .filter((param) => !!param) // filter out all the empty ones // the filter makes these keys neater
44
- .join('/'); // join into a string so we can use it as an object key
45
+ const keyMap = {
46
+ type: resourceType || type, namespace, id, selector
47
+ };
48
+
49
+ return Object.entries(keyMap)
50
+ .map(([prop, value]) => `${ prop }=${ value || '' }`)
51
+ .join(',');
45
52
  };
46
53
 
47
54
  export const watchKeyFromMessage = (msg) => {
@@ -15,7 +15,9 @@ import {
15
15
  HPA,
16
16
  SECRET
17
17
  } from '@shell/config/types';
18
- import { CAPI as CAPI_LAB_AND_ANO, CATTLE_PUBLIC_ENDPOINTS, STORAGE } from '@shell/config/labels-annotations';
18
+ import {
19
+ CAPI as CAPI_LAB_AND_ANO, CATTLE_PUBLIC_ENDPOINTS, STORAGE, UI_PROJECT_SECRET, UI_PROJECT_SECRET_COPY
20
+ } from '@shell/config/labels-annotations';
19
21
  import { Schema } from '@shell/plugins/steve/schema';
20
22
  import { PaginationSettingsStore } from '@shell/types/resources/settings';
21
23
  import paginationUtils from '@shell/utils/pagination-utils';
@@ -184,6 +186,10 @@ class StevePaginationUtils extends NamespaceProjectFilters {
184
186
  [CONFIG_MAP]: [
185
187
  { field: 'metadata.labels[harvesterhci.io/cloud-init-template]' }
186
188
  ],
189
+ [SECRET]: [
190
+ { field: `metadata.labels[${ UI_PROJECT_SECRET }]` },
191
+ { field: `metadata.annotations[${ UI_PROJECT_SECRET_COPY }]` },
192
+ ],
187
193
  [NAMESPACE]: [
188
194
  { field: 'metadata.labels[field.cattle.io/projectId]' }
189
195
  ],
@@ -499,7 +505,11 @@ class StevePaginationUtils extends NamespaceProjectFilters {
499
505
  // Check if the API supports filtering by this field
500
506
  this.validateField(validateFields, schema, field.field);
501
507
 
502
- const encodedValue = encodeURIComponent(field.value);
508
+ // we're just checking that the field exists, so there's no value
509
+ if (field.exists) {
510
+ return field.field;
511
+ }
512
+ const encodedValue = encodeURIComponent(field.value || '');
503
513
 
504
514
  // = exact match (equals + exact)
505
515
  // ~ partial match (equals + !exact)
@@ -508,7 +518,7 @@ class StevePaginationUtils extends NamespaceProjectFilters {
508
518
  const operator = `${ field.equals ? '' : '!' }${ field.exact ? '=' : '~' }`;
509
519
  let safeValue;
510
520
 
511
- if (StevePaginationUtils.VALID_FIELD_VALUE_REGEX.test(field.value)) {
521
+ if (StevePaginationUtils.VALID_FIELD_VALUE_REGEX.test(field.value || '')) {
512
522
  // Does not contain any protected characters, send as is
513
523
  safeValue = encodedValue;
514
524
  } else {
@@ -674,6 +684,7 @@ export const PAGINATION_SETTINGS_STORE_DEFAULTS: PaginationSettingsStore = {
674
684
  // { resource: CAPI.RANCHER_CLUSTER, context: ['home', 'side-bar'] }, // Disabled due to https://github.com/rancher/dashboard/issues/14493
675
685
  // { resource: MANAGEMENT.CLUSTER, context: ['side-bar'] }, // Disabled due to https://github.com/rancher/dashboard/issues/14493
676
686
  { resource: CATALOG.APP, context: ['branding'] },
687
+ SECRET
677
688
  ],
678
689
  generic: false,
679
690
  }