@rancher/shell 3.0.6 → 3.0.7

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 (78) hide show
  1. package/assets/images/pl/dark/rancher-logo.svg +131 -44
  2. package/assets/images/pl/rancher-logo.svg +120 -44
  3. package/assets/styles/base/_basic.scss +2 -2
  4. package/assets/styles/base/_color-classic.scss +51 -0
  5. package/assets/styles/base/_color.scss +3 -3
  6. package/assets/styles/base/_mixins.scss +1 -1
  7. package/assets/styles/base/_variables-classic.scss +47 -0
  8. package/assets/styles/global/_button.scss +49 -17
  9. package/assets/styles/global/_form.scss +1 -1
  10. package/assets/styles/themes/_dark.scss +4 -0
  11. package/assets/styles/themes/_light.scss +3 -69
  12. package/assets/styles/themes/_modern.scss +194 -50
  13. package/assets/styles/vendor/vue-select.scss +1 -2
  14. package/assets/translations/en-us.yaml +33 -21
  15. package/components/ClusterIconMenu.vue +1 -1
  16. package/components/ClusterProviderIcon.vue +1 -1
  17. package/components/CodeMirror.vue +1 -1
  18. package/components/IconOrSvg.vue +40 -29
  19. package/components/ResourceDetail/index.vue +1 -0
  20. package/components/SortableTable/sorting.js +3 -1
  21. package/components/Tabbed/index.vue +5 -5
  22. package/components/form/ResourceTabs/index.vue +37 -18
  23. package/components/form/SecretSelector.vue +6 -2
  24. package/components/nav/Group.vue +29 -9
  25. package/components/nav/Header.vue +6 -8
  26. package/components/nav/NamespaceFilter.vue +1 -1
  27. package/components/nav/TopLevelMenu.helper.ts +47 -20
  28. package/components/nav/TopLevelMenu.vue +44 -14
  29. package/components/nav/Type.vue +0 -5
  30. package/components/nav/__tests__/TopLevelMenu.test.ts +2 -0
  31. package/config/pagination-table-headers.js +10 -2
  32. package/config/product/explorer.js +4 -3
  33. package/config/table-headers.js +9 -0
  34. package/core/plugin.ts +18 -6
  35. package/core/types.ts +8 -0
  36. package/detail/provisioning.cattle.io.cluster.vue +1 -0
  37. package/dialog/InstallExtensionDialog.vue +71 -45
  38. package/dialog/UninstallExtensionDialog.vue +2 -1
  39. package/dialog/__tests__/InstallExtensionDialog.test.ts +111 -0
  40. package/edit/auth/oidc.vue +86 -16
  41. package/mixins/__tests__/chart.test.ts +1 -1
  42. package/mixins/chart.js +1 -1
  43. package/models/event.js +7 -0
  44. package/models/provisioning.cattle.io.cluster.js +9 -0
  45. package/package.json +1 -1
  46. package/pages/c/_cluster/explorer/EventsTable.vue +3 -6
  47. package/pages/c/_cluster/settings/performance.vue +1 -1
  48. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +159 -62
  49. package/pages/c/_cluster/uiplugins/__tests__/PluginInfoPanel.test.ts +102 -0
  50. package/pages/c/_cluster/uiplugins/__tests__/{index.spec.ts → index.test.ts} +121 -55
  51. package/pages/c/_cluster/uiplugins/index.vue +110 -94
  52. package/plugins/__tests__/subscribe.events.test.ts +194 -0
  53. package/plugins/dashboard-store/actions.js +3 -0
  54. package/plugins/dashboard-store/getters.js +1 -1
  55. package/plugins/dashboard-store/resource-class.js +3 -3
  56. package/plugins/steve/__tests__/subscribe.spec.ts +27 -24
  57. package/plugins/steve/index.js +18 -10
  58. package/plugins/steve/mutations.js +2 -2
  59. package/plugins/steve/resourceWatcher.js +2 -2
  60. package/plugins/steve/steve-pagination-utils.ts +12 -9
  61. package/plugins/steve/subscribe.js +113 -85
  62. package/plugins/subscribe-events.ts +211 -0
  63. package/rancher-components/BadgeState/BadgeState.vue +8 -6
  64. package/rancher-components/Banner/Banner.vue +2 -1
  65. package/rancher-components/Form/Checkbox/Checkbox.vue +3 -3
  66. package/rancher-components/Form/Radio/RadioButton.vue +3 -3
  67. package/store/index.js +12 -22
  68. package/types/extension-manager.ts +8 -1
  69. package/types/resources/settings.d.ts +24 -17
  70. package/types/shell/index.d.ts +352 -335
  71. package/types/store/subscribe-events.types.ts +70 -0
  72. package/types/store/subscribe.types.ts +6 -22
  73. package/utils/pagination-utils.ts +87 -28
  74. package/utils/pagination-wrapper.ts +6 -8
  75. package/utils/sort.js +5 -0
  76. package/utils/unit-tests/pagination-utils.spec.ts +283 -0
  77. package/utils/validators/formRules/__tests__/index.test.ts +7 -0
  78. package/utils/validators/formRules/index.ts +2 -2
@@ -0,0 +1,211 @@
1
+ import { keyForSubscribe } from '@shell/plugins/steve/resourceWatcher';
2
+ import {
3
+ SubscribeEventListener, SubscribeEventCallbackArgs, SubscribeEventListenerArgs, SubscribeEventWatch, SubscribeEventWatchArgs,
4
+ STEVE_WATCH_EVENT_LISTENER_CALLBACK
5
+ } from '@shell/types/store/subscribe-events.types';
6
+ import { STEVE_WATCH_EVENT_TYPES, STEVE_WATCH_PARAMS } from '@shell/types/store/subscribe.types';
7
+
8
+ type SubscribeEventWatches = { [socketId: string]: SubscribeEventWatch};
9
+
10
+ /**
11
+ * For a specific resource watch, listen for a specific event type and trigger callback when received
12
+ *
13
+ * For example, listen for provisioning.cattle.io clusters messages of type resource.changes and trigger callback when received
14
+ *
15
+ * Watch - UI is watching a resource type restricted by nothing/id/namespace/selector. For example
16
+ * - watch all pods
17
+ * - watch specific pod
18
+ * - watch pods with specific labels
19
+ * Event - Rancher socket messages TO the ui. For example
20
+ * - resource.started
21
+ * - resource.change
22
+ * - resource.changes
23
+ * Listener - listen to events, trigger when received. For example
24
+ * - listen for resource.changes messages for the all pods watch
25
+ * Callback - triggered when a listener has heard something
26
+ * - watch for all pods receives a resource.changes message, it has a listener, listener executes it's callback
27
+ *
28
+ * Watch 0:M Events 0:M Listeners 0:M Callbacks
29
+ */
30
+ export class SteveWatchEventListenerManager {
31
+ private keyForSubscribe({ params }: {params: STEVE_WATCH_PARAMS}): string {
32
+ return keyForSubscribe(params);
33
+ }
34
+
35
+ /**
36
+ * collection of ui --> rancher watches. we keep state specific to this class here
37
+ */
38
+ private watches: SubscribeEventWatches = {};
39
+
40
+ /**
41
+ * Not all event types can be listened to are supported, only these
42
+ */
43
+ public readonly supportedEventTypes: STEVE_WATCH_EVENT_TYPES[] = [STEVE_WATCH_EVENT_TYPES.CHANGES];
44
+
45
+ /**
46
+ * Not all event types can be listened to are supported, check if one is
47
+ */
48
+ public isSupportedEventType(type: STEVE_WATCH_EVENT_TYPES): boolean {
49
+ return !!this.supportedEventTypes.includes(type);
50
+ }
51
+
52
+ /** **** Watches ***********************/
53
+
54
+ public getWatch({ params } : SubscribeEventWatchArgs): SubscribeEventWatch {
55
+ const socketId = this.keyForSubscribe({ params });
56
+
57
+ return this.watches[socketId];
58
+ }
59
+
60
+ private initialiseWatch({ params }: SubscribeEventWatchArgs): SubscribeEventWatch {
61
+ const socketId = this.keyForSubscribe({ params });
62
+
63
+ this.watches[socketId] = {
64
+ hasStandardWatch: false,
65
+ listeners: []
66
+ };
67
+
68
+ return this.watches[socketId];
69
+ }
70
+
71
+ /**
72
+ * This is just tidying the entry
73
+ *
74
+ * All watches associated with this type should be unwatched
75
+ */
76
+ private deleteWatch({ params } : SubscribeEventWatchArgs) {
77
+ const socketId = this.keyForSubscribe({ params });
78
+
79
+ delete this.watches[socketId];
80
+ }
81
+
82
+ /**
83
+ * Is there a standard non-listener watch for this this type
84
+ */
85
+ public hasStandardWatch({ params } : SubscribeEventWatchArgs): boolean {
86
+ const socketId = this.keyForSubscribe({ params });
87
+
88
+ return this.watches[socketId]?.hasStandardWatch;
89
+ }
90
+
91
+ /**
92
+ * Set if this type has a standard non-listener watch associated with it
93
+ */
94
+ public setStandardWatch({ standardWatch, args }: { standardWatch: boolean, args: SubscribeEventWatchArgs}) {
95
+ const { params } = args;
96
+
97
+ let watch = this.getWatch({ params });
98
+
99
+ if (!watch) {
100
+ if (!standardWatch) {
101
+ // no point setting a non-existent watch as not started
102
+ return;
103
+ }
104
+ watch = this.initialiseWatch({ params });
105
+ }
106
+
107
+ watch.hasStandardWatch = standardWatch;
108
+
109
+ // if we've just set this to false and there's no listeners, tidy up the entry
110
+ if (!watch.hasStandardWatch && watch.listeners.length === 0) {
111
+ this.deleteWatch({ params });
112
+ }
113
+ }
114
+
115
+ /** **** Listeners ***********************/
116
+
117
+ public hasEventListeners({ params }: SubscribeEventWatchArgs): boolean {
118
+ const socketId = this.keyForSubscribe({ params });
119
+ const watch = this.watches[socketId];
120
+ const listener = watch?.listeners.find((l) => Object.values(l.callbacks).length > 0);
121
+
122
+ return !!listener;
123
+ }
124
+
125
+ public getEventListener({ entryOnly, args }: { entryOnly?: boolean, args: SubscribeEventListenerArgs}): SubscribeEventListener | null {
126
+ const { params, event } = args;
127
+ const socketId = this.keyForSubscribe({ params });
128
+ const watch = this.watches[socketId];
129
+
130
+ if (watch) {
131
+ const listener = watch.listeners.find((w) => w.event === event);
132
+
133
+ if (listener && (entryOnly || !!Object.keys(listener?.callbacks || {}).length)) {
134
+ return listener;
135
+ }
136
+ }
137
+
138
+ return null;
139
+ }
140
+
141
+ public addEventListener({ event, params }: SubscribeEventListenerArgs): SubscribeEventListener {
142
+ if (!event) {
143
+ throw new Error(`Cannot add a socket watch event listener if there's no event to listen to`);
144
+ }
145
+
146
+ let watch = this.getWatch({ params });
147
+
148
+ if (!watch) {
149
+ watch = this.initialiseWatch({ params });
150
+ }
151
+
152
+ let listener = this.getEventListener({ entryOnly: true, args: { event, params } });
153
+
154
+ if (!listener) {
155
+ listener = {
156
+ event,
157
+ callbacks: { },
158
+ };
159
+ watch.listeners.push(listener);
160
+ }
161
+
162
+ return listener;
163
+ }
164
+
165
+ public triggerEventListener({ event, params }: SubscribeEventListenerArgs) {
166
+ const eventWatcher = this.getEventListener({ entryOnly: false, args: { event, params } });
167
+
168
+ if (eventWatcher) {
169
+ Object.values(eventWatcher.callbacks).forEach((cb) => {
170
+ cb();
171
+ });
172
+ }
173
+ }
174
+
175
+ public triggerAllEventListeners({ params }: SubscribeEventWatchArgs) {
176
+ const watch = this.getWatch({ params });
177
+
178
+ watch.listeners.forEach((l) => {
179
+ Object.values(l.callbacks || {}).forEach((cb) => cb());
180
+ });
181
+ }
182
+
183
+ /** **** Callbacks ***********************/
184
+
185
+ public addEventListenerCallback({ callback, args }: {
186
+ callback: STEVE_WATCH_EVENT_LISTENER_CALLBACK,
187
+ args: SubscribeEventCallbackArgs
188
+ }): SubscribeEventListener {
189
+ const { params, event, id } = args;
190
+ const eventWatcher = this.addEventListener({ event, params });
191
+
192
+ if (!eventWatcher.callbacks[id]) {
193
+ eventWatcher.callbacks[id] = callback;
194
+ }
195
+
196
+ return eventWatcher;
197
+ }
198
+
199
+ /**
200
+ * This is just tidying the entry
201
+ *
202
+ * All watches associated with this type should be unwatched
203
+ */
204
+ public removeEventListenerCallback({ event, params, id }: SubscribeEventCallbackArgs) {
205
+ const existing = this.getEventListener({ args: { event, params } });
206
+
207
+ if (existing) {
208
+ delete existing.callbacks[id];
209
+ }
210
+ }
211
+ }
@@ -79,22 +79,24 @@ export default defineComponent({
79
79
  border-radius: 20px;
80
80
 
81
81
  &.bg-info {
82
- border-color: var(--info);
82
+ color: var(--on-info-banner);
83
+ background: var(--info-badge, var(--info-banner));
83
84
  }
84
85
 
85
86
  &.bg-error {
86
- border-color: var(--error);
87
+ color: var(--on-error-banner);
88
+ background: var(--error-badge, var(--error-banner));
87
89
  }
88
90
 
89
91
  &.bg-warning {
90
- border-color: var(--warning);
92
+ color: var(--on-warning-banner);
93
+ background: var(--warning-badge, var(--warning-banner));
91
94
  }
92
95
 
93
96
  // Successful states are de-emphasized by using [text-]color instead of background-color
94
97
  &.bg-success {
95
- color: var(--success);
96
- background: transparent;
97
- border-color: var(--success);
98
+ color: var(--on-success-banner, var(--success-text));
99
+ background: var(--success-badge, var(--success));
98
100
  }
99
101
 
100
102
  // Added badge-disabled instead of bg-disabled since bg-disabled is used in other places with !important styling, an investigation is needed to make the naming consistent
@@ -231,12 +231,13 @@ $icon-size: 24px;
231
231
  .warning & {
232
232
  background: var(--warning-banner-bg);
233
233
  border-color: var(--warning);
234
+ color: var(--warning-banner-text, var(--body-text));
234
235
  }
235
236
 
236
237
  .error & {
237
238
  background: var(--error-banner-bg);
238
239
  border-color: var(--error);
239
- color: var(--error);
240
+ color: var(--error-banner-text, var(--error));
240
241
  }
241
242
 
242
243
  &.stacked {
@@ -416,7 +416,7 @@ $fontColor: var(--input-label);
416
416
  width: 14px;
417
417
  background-color: var(--body-bg);
418
418
  border-radius: var(--border-radius);
419
- border: 1px solid var(--border);
419
+ border: 1px solid var(--input-border);
420
420
  flex-shrink: 0;
421
421
 
422
422
  &:focus-visible {
@@ -440,12 +440,12 @@ $fontColor: var(--input-label);
440
440
  }
441
441
 
442
442
  input:checked ~ .checkbox-custom {
443
- background-color:var(--primary);
443
+ background-color: var(--active, var(--primary));
444
444
  -webkit-transform: rotate(0deg) scale(1);
445
445
  -ms-transform: rotate(0deg) scale(1);
446
446
  transform: rotate(0deg) scale(1);
447
447
  opacity:1;
448
- border: 1px solid var(--primary);
448
+ border: 1px solid var(--active, var(--primary));
449
449
  }
450
450
 
451
451
  // Custom Checkbox tick
@@ -274,7 +274,7 @@ $fontColor: var(--input-label);
274
274
  min-width: 14px;
275
275
  background-color: var(--input-bg);
276
276
  border-radius: 50%;
277
- border: 1.5px solid var(--border);
277
+ border: 1.5px solid var(--input-border);
278
278
  margin-top: 5px;
279
279
  }
280
280
 
@@ -284,12 +284,12 @@ $fontColor: var(--input-label);
284
284
 
285
285
  .radio-custom {
286
286
  &[aria-checked="true"] {
287
- background-color: var(--primary);
287
+ background-color: var(--active, var(--primary));
288
288
  -webkit-transform: rotate(0deg) scale(1);
289
289
  -ms-transform: rotate(0deg) scale(1);
290
290
  transform: rotate(0deg) scale(1);
291
291
  opacity:1;
292
- border: 1.5px solid var(--primary);
292
+ border: 1.5px solid var(--active, var(--primary));
293
293
 
294
294
  // Ensure that checked radio buttons are muted but still visibly selected when muted
295
295
  &.text-muted {
package/store/index.js CHANGED
@@ -40,6 +40,7 @@ import { isDevBuild } from '@shell/utils/version';
40
40
  import { markRaw } from 'vue';
41
41
  import paginationUtils from '@shell/utils/pagination-utils';
42
42
  import { addReleaseNotesNotification } from '@shell/utils/release-notes';
43
+ import sideNavService from '@shell/components/nav/TopLevelMenu.helper';
43
44
 
44
45
  // Disables strict mode for all store instances to prevent warning about changing state outside of mutations
45
46
  // because it's more efficient to do that sometimes.
@@ -230,8 +231,8 @@ const updateActiveNamespaceCache = (state, activeNamespaceCache) => {
230
231
  /**
231
232
  * Are we in the vai enabled world where mgmt clusters are paginated?
232
233
  */
233
- const paginateClusters = (rootGetters) => {
234
- return paginationUtils.isEnabled({ rootGetters }, { store: 'management', resource: { id: MANAGEMENT.CLUSTER, context: 'side-bar' } });
234
+ const paginateClusters = ({ rootGetters, state }) => {
235
+ return paginationUtils.isEnabled({ rootGetters, $plugin: state.$plugin }, { store: 'management', resource: { id: MANAGEMENT.CLUSTER, context: 'side-bar' } });
235
236
  };
236
237
 
237
238
  export const state = () => {
@@ -260,11 +261,8 @@ export const state = () => {
260
261
  $router: markRaw({}),
261
262
  $route: markRaw({}),
262
263
  $plugin: markRaw({}),
263
- /**
264
- * Cache state of side nav clusters. This avoids flickering when the user changes pages and the side nav component re-renders
265
- */
266
- sideNavCache: undefined,
267
264
  showWorkspaceSwitcher: true,
265
+
268
266
  };
269
267
  };
270
268
 
@@ -629,10 +627,6 @@ export const getters = {
629
627
  return `${ base }/latest`;
630
628
  },
631
629
 
632
- sideNavCache(state) {
633
- return state.sideNavCache;
634
- },
635
-
636
630
  ...gcGetters
637
631
  };
638
632
 
@@ -773,10 +767,6 @@ export const mutations = {
773
767
  state.$plugin = markRaw(pluginDefinition || {});
774
768
  },
775
769
 
776
- setSideNavCache(state, sideNavCache) {
777
- state.sideNavCache = sideNavCache;
778
- },
779
-
780
770
  showWorkspaceSwitcher(state, value) {
781
771
  state.showWorkspaceSwitcher = value;
782
772
  },
@@ -855,7 +845,7 @@ export const actions = {
855
845
 
856
846
  res = await allHash(promises);
857
847
 
858
- if (!res[MANAGEMENT.SETTING] || !paginateClusters(rootGetters)) {
848
+ if (!res[MANAGEMENT.SETTING] || !paginateClusters({ rootGetters, state })) {
859
849
  // This introduces a synchronous request, however we need settings to determine if SSP is enabled
860
850
  // Eventually it will be removed when SSP is always on
861
851
  res[MANAGEMENT.CLUSTER] = await dispatch('management/findAll', { type: MANAGEMENT.CLUSTER, opt: { watch: false } });
@@ -1033,7 +1023,7 @@ export const actions = {
1033
1023
  await dispatch('management/waitForSchema', { type: MANAGEMENT.CLUSTER });
1034
1024
 
1035
1025
  // If SSP is on we won't have requested all clusters
1036
- if (!paginateClusters(rootGetters)) {
1026
+ if (!paginateClusters({ rootGetters, state })) {
1037
1027
  await dispatch('management/waitForHaveAll', { type: MANAGEMENT.CLUSTER });
1038
1028
  }
1039
1029
 
@@ -1132,8 +1122,10 @@ export const actions = {
1132
1122
  commit('updateNamespaces', { filters: ids, getters });
1133
1123
  },
1134
1124
 
1135
- async cleanNamespaces({ getters, dispatch, rootGetters }) {
1136
- if (paginateClusters(rootGetters)) {
1125
+ async cleanNamespaces({
1126
+ getters, dispatch, rootGetters, state
1127
+ }) {
1128
+ if (paginateClusters({ rootGetters, state })) {
1137
1129
  // See https://github.com/rancher/dashboard/issues/12864
1138
1130
  // old world...
1139
1131
  // - loadManagement makes a request to fetch all mgmt clusters
@@ -1187,6 +1179,8 @@ export const actions = {
1187
1179
  }
1188
1180
  });
1189
1181
 
1182
+ sideNavService.reset();
1183
+
1190
1184
  await dispatch('management/unsubscribe');
1191
1185
  commit('managementChanged', { ready: false });
1192
1186
  commit('management/reset');
@@ -1308,10 +1302,6 @@ export const actions = {
1308
1302
  });
1309
1303
  },
1310
1304
 
1311
- setSideNavCache({ commit }, sideNavCache) {
1312
- commit('setSideNavCache', sideNavCache);
1313
- },
1314
-
1315
1305
  showWorkspaceSwitcher({ commit }, value) {
1316
1306
  commit('showWorkspaceSwitcher', value);
1317
1307
  },
@@ -1,4 +1,11 @@
1
+ import { EXT_IDS_VALUES } from '@shell/core/plugin';
1
2
  import { ClusterProvisionerContext } from '@shell/core/types';
3
+
4
+ type ExtensionManagerType = { [name: string]: Function, }
5
+ export type ExtensionManagerTypes =
6
+ { [type in EXT_IDS_VALUES]?: ExtensionManagerType } & // eslint-disable-line no-unused-vars
7
+ { [name: string]: ExtensionManagerType }
8
+
2
9
  export type ExtensionManager = {
3
10
  internal(): any;
4
11
  loadPluginAsync(plugin: any): Promise<void>;
@@ -12,7 +19,7 @@ export type ExtensionManager = {
12
19
  applyPlugin(plugin: any): void;
13
20
  register(type: string, name: string, fn: Function): void;
14
21
  unregister(type: string, name: string, fn: Function): void;
15
- getAll(): any;
22
+ getAll(): ExtensionManagerTypes;
16
23
  getPlugins(): any;
17
24
  getDynamic(typeName: string, name: string): any;
18
25
  getValidator(name: string): any;
@@ -1,25 +1,32 @@
1
-
2
1
  export interface PaginationSettingsStore {
3
- [name: string]: {
4
- resources: {
2
+ resources: {
3
+ /**
4
+ * Enable for all resources in this store
5
+ */
6
+ enableAll?: boolean,
7
+ /**
8
+ * Enable for only some resources in this store
9
+ */
10
+ enableSome?: {
5
11
  /**
6
- * Enable for all resources in this store
12
+ * Specific resource type to enable
7
13
  */
8
- enableAll: boolean,
9
- enableSome: {
10
- /**
11
- * Specific resource type to enable
12
- */
13
- enabled: (string | { resource: string, context: string[]})[],
14
- /**
15
- * There's no hardcoded headers or custom list for the resource type, headers will be generated from schema attributes.columns
16
- */
17
- generic: boolean,
18
- },
19
- }
14
+ enabled?: (string | { resource: string, context: string[]})[],
15
+ /**
16
+ * Additional resource types that do not have any custom pagination settings (headers, lists, etc) but can be generated automatically (headers from CRD additionalPrinterColumns) can be enabled
17
+ */
18
+ generic?: boolean,
19
+ },
20
20
  }
21
21
  }
22
22
 
23
+ /**
24
+ * Determine which resources can utilise server-side pagination
25
+ */
26
+ export interface PaginationSettingsStores {
27
+ [store: string]: PaginationSettingsStore
28
+ }
29
+
23
30
  export type PaginationFeature = 'listAutoRefreshToggle' | 'listManualRefresh'
24
31
 
25
32
  /**
@@ -33,7 +40,7 @@ export interface PaginationSettings {
33
40
  /**
34
41
  * Should pagination be enabled for resources in a store
35
42
  */
36
- stores?: PaginationSettingsStore,
43
+ stores?: PaginationSettingsStores,
37
44
 
38
45
  /**
39
46
  * List of specific features that can be enabled / disabled