@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
@@ -0,0 +1,165 @@
1
+ import _actions from '@shell/plugins/dashboard-store/actions';
2
+
3
+ const { findAll } = _actions;
4
+
5
+ describe('dashboard-store: actions', () => {
6
+ const setupContext = () => {
7
+ const commit = jest.fn();
8
+ const dispatch = jest.fn((...args) => {
9
+ switch (args[0]) {
10
+ case 'request':
11
+ return { data: ['requestData'] };
12
+ }
13
+ });
14
+ const state = { config: { namespace: 'unitTest' } };
15
+ const getters = {
16
+ normalizeType: jest.fn(() => 'getters.normalizeType'),
17
+ typeRegistered: jest.fn(() => false),
18
+ haveAll: jest.fn(() => false),
19
+ haveAllNamespace: jest.fn(() => false),
20
+ all: jest.fn(() => 'getters.all'),
21
+ urlFor: jest.fn(() => 'getters.urlFor'), // we're not testing the urlFor getter so we don't need to do anything with opt here
22
+ };
23
+ const rootGetters = {
24
+ 'type-map/optionsFor': jest.fn(),
25
+ 'auth/fromHeader': 'foo'
26
+ };
27
+
28
+ // we're not testing function output based off of state or getter inputs here since they are dependencies and should be tested independently
29
+ return {
30
+ state,
31
+ getters,
32
+ rootGetters,
33
+ commit,
34
+ dispatch
35
+ };
36
+ };
37
+
38
+ const standardAssertions = {
39
+ returnsPromise:
40
+ {
41
+ assertionLabel: 'returns a promise',
42
+ valueGetter: ({ findAllPromise }) => typeof findAllPromise.then,
43
+ valueExpected: 'function'
44
+ },
45
+ callsAll: {
46
+ assertionLabel: 'calls the "all" getter with the normalizedType',
47
+ valueGetter: ({ getters }) => getters.all.mock.calls[0][0],
48
+ valueExpected: 'getters.normalizeType'
49
+ },
50
+ returnsFromAll: {
51
+ assertionLabel: 'returns the value expected from the "all" getter',
52
+ valueGetter: ({ findAllReturnValue }) => findAllReturnValue,
53
+ valueExpected: 'getters.all'
54
+ },
55
+ firstDispatchAction: {
56
+ assertionLabel: 'first dispatch should be the "request" action',
57
+ valueGetter: ({ dispatch }) => dispatch.mock.calls[0][0],
58
+ valueExpected: 'request'
59
+ },
60
+ firstDispatchParams: {
61
+ assertionLabel: 'first dispatch parameters should be provided a normalized type and a url, streaming, and "metadata.managedFields" excluded under opt',
62
+ valueGetter: ({ dispatch }) => dispatch.mock.calls[0][1],
63
+ valueExpected: {
64
+ type: 'getters.normalizeType',
65
+ opt: {
66
+ url: 'getters.urlFor',
67
+ stream: true
68
+ }
69
+ },
70
+ assertionMethod: 'toMatchObject'
71
+ },
72
+ secondDispatchAction: {
73
+ assertionLabel: 'second dispatch should be the "watch" action',
74
+ valueGetter: ({ dispatch }) => dispatch.mock.calls[1][0],
75
+ valueExpected: 'watch'
76
+ },
77
+ secondDispatchParams: {
78
+ assertionLabel: 'second dispatch parameters should have a normalized type and force set to false',
79
+ valueGetter: ({ dispatch }) => dispatch.mock.calls[1][1],
80
+ valueExpected: { type: 'getters.normalizeType', force: false },
81
+ assertionMethod: 'toMatchObject'
82
+ },
83
+ countDispatches: {
84
+ assertionLabel: 'should only make two dispatches',
85
+ valueGetter: ({ dispatch }) => dispatch.mock.calls,
86
+ valueExpected: 2,
87
+ assertionMethod: 'toHaveLength'
88
+ },
89
+ firstCommitMutation: {
90
+ assertionLabel: 'first commit should be the "registerType" mutation',
91
+ valueGetter: ({ commit }) => commit.mock.calls[0][0],
92
+ valueExpected: 'registerType'
93
+ },
94
+ firstCommitParams: {
95
+ assertionLabel: 'first commit parameter should be a normalized type',
96
+ valueGetter: ({ commit }) => commit.mock.calls[0][1],
97
+ valueExpected: 'getters.normalizeType'
98
+ },
99
+ secondCommitMutation: {
100
+ assertionLabel: 'second commit should be the "loadAll" mutation',
101
+ valueGetter: ({ commit }) => commit.mock.calls[1][0],
102
+ valueExpected: 'loadAll'
103
+ },
104
+ secondCommitParams: {
105
+ assertionLabel: 'second commit parameters should have a normalized type, ctx.state.config.namespace, data returned by request, and skipHaveAll set to false',
106
+ valueGetter: ({ commit }) => commit.mock.calls[1][1],
107
+ valueExpected: {
108
+ type: 'getters.normalizeType',
109
+ ctx: { state: { config: { namespace: 'unitTest' } } },
110
+ data: ['requestData'],
111
+ skipHaveAll: false,
112
+ },
113
+ assertionMethod: 'toMatchObject'
114
+ },
115
+ countCommits: {
116
+ assertionLabel: 'should only make two commits',
117
+ valueGetter: ({ commit }) => commit.mock.calls,
118
+ valueExpected: 2,
119
+ assertionMethod: 'toHaveLength'
120
+ }
121
+
122
+ };
123
+
124
+ describe('dashboard-store > actions > findAll', () => {
125
+ describe('called without a cache for the type in the second param', () => {
126
+ const {
127
+ dispatch, commit, getters, rootGetters, state
128
+ } = setupContext();
129
+
130
+ const findAllPromise = findAll(
131
+ {
132
+ dispatch, commit, getters, rootGetters, state
133
+ },
134
+ { type: 'type' }
135
+ );
136
+
137
+ const assertionChain = [
138
+ standardAssertions.returnsPromise,
139
+ standardAssertions.callsAll,
140
+ standardAssertions.returnsFromAll,
141
+ standardAssertions.firstDispatchAction,
142
+ standardAssertions.firstDispatchParams,
143
+ standardAssertions.secondDispatchAction,
144
+ standardAssertions.secondDispatchParams,
145
+ standardAssertions.countDispatches,
146
+ standardAssertions.firstCommitMutation,
147
+ standardAssertions.firstCommitParams,
148
+ standardAssertions.secondCommitMutation,
149
+ standardAssertions.secondCommitParams,
150
+ standardAssertions.countCommits
151
+ ];
152
+
153
+ it.each(assertionChain)(
154
+ '$assertionLabel',
155
+ async({ valueGetter, valueExpected, assertionMethod = 'toBe' }) => {
156
+ const findAllReturnValue = await findAllPromise;
157
+
158
+ expect(valueGetter({
159
+ findAllPromise, findAllReturnValue, getters, dispatch, commit
160
+ }))[assertionMethod](valueExpected);
161
+ }
162
+ );
163
+ });
164
+ });
165
+ });
@@ -0,0 +1,100 @@
1
+ import getters, { urlFor } from '@shell/plugins/dashboard-store/getters';
2
+
3
+ const { urlOptions } = getters;
4
+
5
+ describe('dashboard-store: getters', () => {
6
+ describe('dashboard-store > getters > exported function: urlFor', () => {
7
+ // we're not testing function output based off of state or getter inputs here since they are dependencies
8
+ const state = { config: { baseUrl: 'protocol' } };
9
+ const getters = {
10
+ normalizeType: (type) => type,
11
+ schemaFor: (type) => {
12
+ if (type === 'typeFoo') {
13
+ return { links: { collection: 'urlFoo' } };
14
+ }
15
+ },
16
+ // this has its own tests so it just returns the input string
17
+ urlOptions: (string) => string
18
+ };
19
+
20
+ const urlForGetter = urlFor(state, getters);
21
+
22
+ it('expects urlFor to return a function', () => {
23
+ expect(typeof urlFor(state, getters)).toBe('function');
24
+ });
25
+
26
+ it('expects function returned by urlFor to return a string', () => {
27
+ expect(urlForGetter('typeFoo')).toBe('protocol/urlFoo');
28
+ });
29
+
30
+ it('expects function returned by urlFor to return a the url supplied as opt.url directly', () => {
31
+ expect(urlForGetter('typeFoo', null, { url: 'urlBar' })).toBe('protocol/urlBar');
32
+ });
33
+
34
+ it('expects function returned by urlFor to return a the url to not receive an additional protocol if relative url', () => {
35
+ expect(urlForGetter('typeFoo', null, { url: '/urlBar' })).toBe('/urlBar');
36
+ });
37
+
38
+ it('expects function returned by urlFor to return a the url to to not receive an additional protocol if the url starts with "http"', () => {
39
+ expect(urlForGetter('typeFoo', null, { url: 'http urlBar' })).toBe('http urlBar');
40
+ });
41
+
42
+ it('expects function returned by urlFor to throw an error if passed an invalid type', () => {
43
+ expect(() => urlForGetter('typeBaz')).toThrow('Unknown schema for type: typeBaz');
44
+ });
45
+
46
+ it('expects function returned by urlFor to append an id to the url if one is supplied', () => {
47
+ expect(urlForGetter('typeFoo', 'idBar')).toBe('protocol/urlFoo/idBar');
48
+ });
49
+ });
50
+ describe('dashboard-store > getters > urlOptions', () => {
51
+ // we're not testing function output based off of state or getter inputs here since they are dependencies
52
+ const state = { config: { baseUrl: 'protocol' } };
53
+ const getters = {
54
+ normalizeType: (type) => type,
55
+ schemaFor: (type) => {
56
+ if (type === 'typeFoo') {
57
+ return { links: { collection: 'urlFoo' } };
58
+ }
59
+ },
60
+ // this has its own tests so it just returns the input string
61
+ urlOptions: (string) => string
62
+ };
63
+
64
+ const urlOptionsGetter = urlOptions();
65
+
66
+ it('expects urlOptions to return a function', () => {
67
+ expect(typeof urlOptions(state, getters)).toBe('function');
68
+ });
69
+ it('returns undefined when called without params', () => {
70
+ expect(urlOptionsGetter()).toBeUndefined();
71
+ });
72
+ it('returns an unmodified string when called without options', () => {
73
+ expect(urlOptionsGetter('foo')).toBe('foo');
74
+ });
75
+ it('returns an unmodified string when called with options that are not accounted for', () => {
76
+ expect(urlOptionsGetter('foo', { bar: 'baz' })).toBe('foo');
77
+ });
78
+ it('returns an unmodified stringif a single filter statement is applied', () => {
79
+ expect(urlOptionsGetter('foo', { filter: { bar: 'baz' } })).toBe('foo');
80
+ });
81
+ it('returns an unmodified string if a single filter statement is applied', () => {
82
+ expect(urlOptionsGetter('foo', { filter: { bar: 'baz', far: 'faz' } })).toBe('foo');
83
+ });
84
+ it('returns an unmodified string if excludeFields is a single element array with the string "bar"', () => {
85
+ expect(urlOptionsGetter('/v1/foo', { excludeFields: ['bar'] })).toBe('/v1/foo');
86
+ });
87
+ it('returns an unmodified string if excludeFields is an array but the URL doesnt include the "/v1/ string"', () => {
88
+ expect(urlOptionsGetter('foo', { excludeFields: ['bar'] })).toBe('foo');
89
+ });
90
+ it('returns an unmodified string if a limit is provided', () => {
91
+ expect(urlOptionsGetter('foo', { limit: 10 })).toBe('foo');
92
+ });
93
+ it('returns an unmodified string if the sort option is provided', () => {
94
+ expect(urlOptionsGetter('foo', { sortBy: 'bar' })).toBe('foo');
95
+ });
96
+ it('returns an unmodified string if the sort option is provided and an order if sortOrder is provided', () => {
97
+ expect(urlOptionsGetter('foo', { sortBy: 'bar', sortOrder: 'baz' })).toBe('foo');
98
+ });
99
+ });
100
+ });
@@ -256,8 +256,8 @@ describe('dashboard-store: mutations', () => {
256
256
  {
257
257
  ctx,
258
258
  batch: {
259
- [POD]: { [pod.id]: pod },
260
- [ WORKLOAD_TYPES.DEPLOYMENT]: { [deployment.id]: deployment }
259
+ [POD]: { [pod.id]: pod },
260
+ [WORKLOAD_TYPES.DEPLOYMENT]: { [deployment.id]: deployment }
261
261
  }
262
262
  }
263
263
  ],
@@ -310,7 +310,7 @@ export default {
310
310
  data: out.data,
311
311
  revision: out.revision,
312
312
  skipHaveAll,
313
- namespace: opt.namespaced,
313
+ namespace: opt.namespaced
314
314
  });
315
315
  }
316
316
  }
@@ -518,6 +518,14 @@ export function stateSort(color, display) {
518
518
  return `${ SORT_ORDER[color] || SORT_ORDER['other'] } ${ display }`;
519
519
  }
520
520
 
521
+ export function isConditionReadyAndWaiting(condition) {
522
+ if (!condition) {
523
+ return false;
524
+ }
525
+
526
+ return condition?.type?.toLowerCase() === 'ready' && condition?.reason?.toLowerCase() === 'waiting';
527
+ }
528
+
521
529
  function maybeFn(val) {
522
530
  if ( isFunction(val) ) {
523
531
  return val(this);
@@ -858,7 +866,7 @@ export default class Resource {
858
866
  const currentRoute = this.currentRouter().app._route;
859
867
  const extensionMenuActions = getApplicableExtensionEnhancements(this.$rootState, ExtensionPoint.ACTION, ActionLocation.TABLE, currentRoute, this);
860
868
 
861
- let all = [
869
+ const all = [
862
870
  { divider: true },
863
871
  {
864
872
  action: this.canUpdate ? 'goToEdit' : 'goToViewConfig',
@@ -910,7 +918,32 @@ export default class Resource {
910
918
  if (extensionMenuActions.length) {
911
919
  // Add a divider first
912
920
  all.push({ divider: true });
913
- all = all.concat(extensionMenuActions);
921
+
922
+ extensionMenuActions.forEach((action) => {
923
+ const newActionInstance = { ...action };
924
+
925
+ const enabledFn = newActionInstance.enabled;
926
+ const typeofEnabled = typeof enabledFn;
927
+
928
+ switch (typeofEnabled) {
929
+ case 'undefined':
930
+ newActionInstance.enabled = true;
931
+ break;
932
+ case 'function':
933
+ Object.defineProperty(newActionInstance, 'enabled', { get: () => enabledFn(this) });
934
+ break;
935
+ case 'boolean':
936
+ // no op, just use it directly
937
+ break;
938
+ default:
939
+ // unsupported value
940
+ console.warn(`Unsupported 'enabled' property type for action: ${ action.label || action.labelKey }` ); // eslint-disable-line no-console
941
+ delete newActionInstance.enabled;
942
+ break;
943
+ }
944
+
945
+ all.push(newActionInstance);
946
+ });
914
947
  }
915
948
 
916
949
  return all;
@@ -1431,6 +1464,10 @@ export default class Resource {
1431
1464
  }
1432
1465
 
1433
1466
  async saveYaml(yaml) {
1467
+ this._saveYaml(yaml);
1468
+ }
1469
+
1470
+ async _saveYaml(yaml) {
1434
1471
  /* Multipart support, but need to know the right cluster and work for management store
1435
1472
  and "apply" seems to only work for create, not update.
1436
1473
 
package/plugins/plugin.js CHANGED
@@ -12,9 +12,17 @@ export default async function(context) {
12
12
  // Provide a mechanism to load the UI without the plugins loaded - in case there is a problem
13
13
  let loadPlugins = true;
14
14
 
15
- if (context.route?.path.endsWith('/safeMode')) {
15
+ const queryKeys = Object.keys(context.route?.query || {}).map((q) => q.toLowerCase());
16
+
17
+ if (queryKeys.includes('safemode')) {
16
18
  loadPlugins = false;
17
19
  console.warn('Safe Mode - plugins will not be loaded'); // eslint-disable-line no-console
20
+ setTimeout(() => {
21
+ context.store.dispatch('growl/success', {
22
+ title: context.store.getters['i18n/t']('plugins.safeMode.title'),
23
+ message: context.store.getters['i18n/t']('plugins.safeMode.message')
24
+ }, { root: true });
25
+ }, 1000);
18
26
  }
19
27
 
20
28
  if (loadPlugins) {
@@ -0,0 +1,93 @@
1
+ import _getters from '@shell/plugins/steve/getters';
2
+
3
+ const { urlFor, urlOptions } = _getters;
4
+
5
+ describe('steve: getters', () => {
6
+ describe('steve > getters > urlFor', () => {
7
+ // we're not testing function output based off of state or getter inputs here since they are dependencies
8
+ const state = { config: { baseUrl: 'protocol' } };
9
+ const getters = {
10
+ normalizeType: (type) => type,
11
+ schemaFor: (type) => {
12
+ if (type === 'typeFoo') {
13
+ return { links: { collection: 'urlFoo' } };
14
+ }
15
+ },
16
+ // this has its own tests so it just returns the input string
17
+ urlOptions: (string) => string
18
+ };
19
+
20
+ const urlForGetter = urlFor(state, getters);
21
+
22
+ // most tests for this getter will go through the dashboard-store getters test spec, this only tests logic specific to the steve variant
23
+
24
+ it('expects urlFor to return a function', () => {
25
+ expect(typeof urlFor(state, getters)).toBe('function');
26
+ });
27
+
28
+ it('expects function returned by urlFor to return a string a type', () => {
29
+ expect(urlForGetter('typeFoo')).toBe('protocol/urlFoo');
30
+ });
31
+
32
+ it('expects function returned by urlFor to return a string containing a namespace when provided with a type and a single namespace string', () => {
33
+ expect(urlForGetter('typeFoo', undefined, { namespaced: 'nsBar' })).toBe('protocol/urlFoo/nsBar');
34
+ });
35
+
36
+ it('expects function returned by urlFor to return a string not containing a namespace when provided with a type and a multiple namespaces string', () => {
37
+ expect(urlForGetter('typeFoo', undefined, { namespaced: ['nsBar', 'nsBaz'] })).toBe('protocol/urlFoo');
38
+ });
39
+ });
40
+ describe('steve > getters > urlOptions', () => {
41
+ // we're not testing function output based off of state or getter inputs here since they are dependencies
42
+ const state = { config: { baseUrl: 'protocol' } };
43
+ const getters = {
44
+ normalizeType: (type) => type,
45
+ schemaFor: (type) => {
46
+ if (type === 'typeFoo') {
47
+ return { links: { collection: 'urlFoo' } };
48
+ }
49
+ },
50
+ // this has its own tests so it just returns the input string
51
+ urlOptions: (string) => string
52
+ };
53
+
54
+ const urlOptionsGetter = urlOptions();
55
+
56
+ it('expects urlOptions to return a function', () => {
57
+ expect(typeof urlOptions(state, getters)).toBe('function');
58
+ });
59
+ it('returns undefined when called without params', () => {
60
+ expect(urlOptionsGetter()).toBeUndefined();
61
+ });
62
+ it('returns an unmodified string when called without options', () => {
63
+ expect(urlOptionsGetter('foo')).toBe('foo');
64
+ });
65
+ it('returns an unmodified string when called with options that are not accounted for', () => {
66
+ expect(urlOptionsGetter('foo', { bar: 'baz' })).toBe('foo');
67
+ });
68
+ it('returns a string with a single filter statement applied if a single filter statement is applied', () => {
69
+ expect(urlOptionsGetter('foo', { filter: { bar: 'baz' } })).toBe('foo?bar=baz');
70
+ });
71
+ it('returns a string with a multiple filter statements applied if a single filter statement is applied', () => {
72
+ expect(urlOptionsGetter('foo', { filter: { bar: 'baz', far: 'faz' } })).toBe('foo?bar=baz&far=faz');
73
+ });
74
+ it('returns a string with an exclude statement for "bar" and "metadata.managedFields" if excludeFields is a single element array with the string "bar" and the url starts with "/v1/"', () => {
75
+ expect(urlOptionsGetter('/v1/foo', { excludeFields: ['bar'] })).toBe('/v1/foo?exclude=bar&exclude=metadata.managedFields');
76
+ });
77
+ it('returns a string without an exclude statement if excludeFields is but the url does not start with "/v1/"', () => {
78
+ expect(urlOptionsGetter('foo', { excludeFields: ['bar'] })).toBe('foo');
79
+ });
80
+ it('returns a string without an exclude statement if excludeFields is an array but the URL doesnt include the "/v1/ string"', () => {
81
+ expect(urlOptionsGetter('foo', { excludeFields: ['bar'] })).toBe('foo');
82
+ });
83
+ it('returns a string with a limit applied if a limit is provided', () => {
84
+ expect(urlOptionsGetter('foo', { limit: 10 })).toBe('foo?limit=10');
85
+ });
86
+ it('returns a string with a sorting criteria if the sort option is provided', () => {
87
+ expect(urlOptionsGetter('foo', { sortBy: 'bar' })).toBe('foo?sort=bar');
88
+ });
89
+ it('returns a string with a sorting criteria if the sort option is provided and an order if sortOrder is provided', () => {
90
+ expect(urlOptionsGetter('foo', { sortBy: 'bar', sortOrder: 'baz' })).toBe('foo?sort=bar&order=baz');
91
+ });
92
+ });
93
+ });
@@ -9,6 +9,7 @@ import NormanModel from './norman-class';
9
9
  import { urlFor } from '@shell/plugins/dashboard-store/getters';
10
10
  import { normalizeType } from '@shell/plugins/dashboard-store/normalize';
11
11
  import pAndNFiltering from '@shell/utils/projectAndNamespaceFiltering.utils';
12
+ import { parse } from '@shell/utils/url';
12
13
 
13
14
  export const STEVE_MODEL_TYPES = {
14
15
  NORMAN: 'norman',
@@ -26,8 +27,11 @@ const GC_IGNORE_TYPES = {
26
27
  export default {
27
28
  urlOptions: () => (url, opt) => {
28
29
  opt = opt || {};
30
+ const parsedUrl = parse(url);
31
+ const isSteve = parsedUrl.path.startsWith('/v1');
29
32
 
30
33
  // Filter
34
+ // Steve's filter options work differently nowadays (https://github.com/rancher/steve#filter) #9341
31
35
  if ( opt.filter ) {
32
36
  const keys = Object.keys(opt.filter);
33
37
 
@@ -54,6 +58,21 @@ export default {
54
58
  }
55
59
  // End: Filter
56
60
 
61
+ // Exclude
62
+ // excludeFields should be an array of strings representing the paths of the fields to exclude
63
+ // only works on Steve but is ignored without error by Norman
64
+ if (isSteve) {
65
+ if (Array.isArray(opt?.excludeFields)) {
66
+ opt.excludeFields = [...opt.excludeFields, 'metadata.managedFields'];
67
+ } else {
68
+ opt.excludeFields = ['metadata.managedFields'];
69
+ }
70
+ const excludeParamsString = opt.excludeFields.map((field) => `exclude=${ field }`).join('&');
71
+
72
+ url += `${ url.includes('?') ? '&' : '?' }${ excludeParamsString }`;
73
+ }
74
+ // End: Exclude
75
+
57
76
  // Limit
58
77
  const limit = opt.limit;
59
78
 
@@ -63,6 +82,7 @@ export default {
63
82
  // End: Limit
64
83
 
65
84
  // Sort
85
+ // Steve's sort options work differently nowadays (https://github.com/rancher/steve#sort) #9341
66
86
  const sortBy = opt.sortBy;
67
87
 
68
88
  if ( sortBy ) {
@@ -85,7 +105,7 @@ export default {
85
105
  // `namespaced` is either
86
106
  // - a string representing a single namespace - add restriction to the url
87
107
  // - an array of namespaces or projects - add restriction as a param
88
- if (opt.namespaced && !pAndNFiltering.isApplicable(opt)) {
108
+ if (opt?.namespaced && !pAndNFiltering.isApplicable(opt)) {
89
109
  const parts = url.split('/');
90
110
 
91
111
  url = `${ parts.join('/') }/${ opt.namespaced }`;
@@ -32,9 +32,7 @@ import { keyForSubscribe } from '@shell/plugins/steve/resourceWatcher';
32
32
  import { waitFor } from '@shell/utils/async';
33
33
  import { WORKER_MODES } from './worker';
34
34
  import pAndNFiltering from '@shell/utils/projectAndNamespaceFiltering.utils';
35
-
36
- import { BLANK_CLUSTER } from '@shell/store/index.js';
37
- import { STORE } from '@shell/store/store-types';
35
+ import { BLANK_CLUSTER, STORE } from '@shell/store/store-types.js';
38
36
 
39
37
  // minimum length of time a disconnect notification is shown
40
38
  const MINIMUM_TIME_NOTIFIED = 3000;
@@ -60,7 +60,11 @@ export default Vue.extend({
60
60
 
61
61
  <template>
62
62
  <span :class="{'badge-state': true, [bg]: true}">
63
- <i v-if="icon" class="icon" :class="{[icon]: true, 'mr-5': !!msg}" />{{ msg }}
63
+ <i
64
+ v-if="icon"
65
+ class="icon"
66
+ :class="{[icon]: true, 'mr-5': !!msg}"
67
+ />{{ msg }}
64
68
  </span>
65
69
  </template>
66
70
 
@@ -1,13 +1,63 @@
1
1
  import { mount } from '@vue/test-utils';
2
2
  import { Banner } from './index';
3
+ import { cleanHtmlDirective } from '@shell/plugins/clean-html-directive';
3
4
 
4
5
  describe('component: Banner', () => {
5
6
  it('should display text based on label', () => {
6
7
  const label = 'test';
7
- const wrapper = mount(Banner, { propsData: { label } });
8
+ const wrapper = mount(
9
+ Banner,
10
+ {
11
+ directives: { cleanHtmlDirective },
12
+ propsData: { label }
13
+ });
8
14
 
9
15
  const element = wrapper.find('span').element;
10
16
 
11
17
  expect(element.textContent).toBe(label);
12
18
  });
19
+
20
+ it('should display an icon', () => {
21
+ const icon = 'my-icon';
22
+ const wrapper = mount(Banner, { propsData: { icon } });
23
+
24
+ const element = wrapper.find(`.${ icon }`).element;
25
+
26
+ expect(element.classList).toContain(icon);
27
+ });
28
+
29
+ it('should not display an icon', () => {
30
+ const wrapper = mount(Banner);
31
+
32
+ const element = wrapper.find(`[data-testid="banner-icon"]`).element;
33
+
34
+ expect(element).not.toBeDefined();
35
+ });
36
+
37
+ it('should emit close event', () => {
38
+ const wrapper = mount(Banner, { propsData: { closable: true } });
39
+ const element = wrapper.find(`[data-testid="banner-close"]`).element;
40
+
41
+ element.click();
42
+
43
+ expect(wrapper.emitted('close')).toHaveLength(1);
44
+ });
45
+
46
+ it('should add the right color', () => {
47
+ const color = 'red';
48
+ const wrapper = mount(Banner, { propsData: { color } });
49
+
50
+ const element = wrapper.element;
51
+
52
+ expect(element.classList).toContain(color);
53
+ });
54
+
55
+ it('should stack the banner messages', () => {
56
+ const stacked = true;
57
+ const wrapper = mount(Banner, { propsData: { stacked } });
58
+
59
+ const element = wrapper.find(`[data-testid="banner-content"]`).element;
60
+
61
+ expect(element.classList).toContain('stacked');
62
+ });
13
63
  });