@rancher/shell 0.3.1 → 0.3.2

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 (44) hide show
  1. package/assets/styles/global/_gauges.scss +1 -1
  2. package/assets/styles/global/_layout.scss +4 -0
  3. package/assets/styles/themes/_dark.scss +1 -0
  4. package/assets/translations/en-us.yaml +4 -0
  5. package/assets/translations/zh-hans.yaml +175 -44
  6. package/components/ActionMenu.vue +28 -7
  7. package/components/DetailTop.vue +14 -1
  8. package/components/ExtensionPanel.vue +42 -0
  9. package/components/IconOrSvg.vue +31 -2
  10. package/components/ResourceDetail/Masthead.vue +16 -3
  11. package/components/ResourceList/index.vue +15 -2
  12. package/components/ResourceTable.vue +3 -1
  13. package/components/SortableTable/THead.vue +6 -9
  14. package/components/SortableTable/filtering.js +1 -1
  15. package/components/SortableTable/selection.js +15 -3
  16. package/components/Tabbed/Tab.vue +1 -1
  17. package/components/form/ResourceTabs/index.vue +23 -0
  18. package/components/nav/Header.vue +69 -5
  19. package/config/product/backup.js +1 -1
  20. package/config/query-params.js +1 -0
  21. package/config/uiplugins.js +3 -3
  22. package/core/plugin-helpers.js +171 -0
  23. package/core/plugin.ts +61 -1
  24. package/core/plugins.js +33 -0
  25. package/core/types.ts +128 -2
  26. package/edit/catalog.cattle.io.clusterrepo.vue +3 -0
  27. package/edit/monitoring.coreos.com.alertmanagerconfig/receiverConfig.vue +14 -2
  28. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +3 -0
  29. package/edit/provisioning.cattle.io.cluster/rke2.vue +21 -18
  30. package/package.json +1 -1
  31. package/pages/c/_cluster/apps/charts/index.vue +17 -0
  32. package/pages/c/_cluster/explorer/index.vue +39 -0
  33. package/pages/c/_cluster/uiplugins/DeveloperInstallDialog.vue +2 -0
  34. package/pages/c/_cluster/uiplugins/InstallDialog.vue +3 -0
  35. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +8 -1
  36. package/pages/c/_cluster/uiplugins/RemoveUIPlugins.vue +2 -0
  37. package/pages/c/_cluster/uiplugins/SetupUIPlugins.vue +1 -0
  38. package/pages/c/_cluster/uiplugins/UninstallDialog.vue +2 -0
  39. package/pages/c/_cluster/uiplugins/index.vue +14 -1
  40. package/plugins/dashboard-store/resource-class.js +16 -1
  41. package/rancher-components/components/Banner/Banner.vue +1 -0
  42. package/store/action-menu.js +4 -3
  43. package/store/type-map.js +26 -0
  44. package/types/shell/index.d.ts +1 -0
@@ -0,0 +1,171 @@
1
+ import { ActionLocation, CardLocation, ExtensionPoint } from '@shell/core/types';
2
+ import { isMac } from '@shell/utils/platform';
3
+ import { ucFirst, randomStr } from '@shell/utils/string';
4
+ import { _EDIT, _CONFIG, _DETAIL, _LIST } from '@shell/config/query-params';
5
+ import { getProductFromRoute } from '@shell/middleware/authenticated';
6
+
7
+ function checkRouteProduct({ name, params, query }, locationConfigParam) {
8
+ const product = getProductFromRoute({
9
+ name, params, query
10
+ });
11
+
12
+ // alias for the homepage
13
+ if (locationConfigParam === 'home' && name === 'home') {
14
+ return true;
15
+ } else if (locationConfigParam === product) {
16
+ return true;
17
+ }
18
+
19
+ return false;
20
+ }
21
+
22
+ function checkRouteMode({ name, query }, locationConfigParam) {
23
+ if (locationConfigParam === _EDIT && query.mode && query.mode === _EDIT) {
24
+ return true;
25
+ } else if (locationConfigParam === _CONFIG && query.as && query.as === _CONFIG) {
26
+ return true;
27
+ } else if (locationConfigParam === _DETAIL && name.includes('-id')) {
28
+ return true;
29
+ // alias to target all list views
30
+ } else if (locationConfigParam === _LIST && !name.includes('-id') && name.includes('-resource')) {
31
+ return true;
32
+ }
33
+
34
+ return false;
35
+ }
36
+
37
+ function checkExtensionRouteBinding($route, locationConfig) {
38
+ // if no configuration is passed, consider it as global
39
+ if (!Object.keys(locationConfig).length) {
40
+ return true;
41
+ }
42
+
43
+ const { params } = $route;
44
+
45
+ // "params" to be checked based on the locationConfig
46
+ const paramsToCheck = [
47
+ 'product',
48
+ 'resource',
49
+ 'namespace',
50
+ 'cluster',
51
+ 'id',
52
+ 'mode'
53
+ ];
54
+
55
+ let res = false;
56
+
57
+ for (let i = 0; i < paramsToCheck.length; i++) {
58
+ const param = paramsToCheck[i];
59
+
60
+ if (locationConfig[param]) {
61
+ for (let x = 0; x < locationConfig[param].length; x++) {
62
+ const locationConfigParam = locationConfig[param][x];
63
+
64
+ if (locationConfigParam) {
65
+ // handle "product" in a separate way...
66
+ if (param === 'product') {
67
+ res = checkRouteProduct($route, locationConfigParam);
68
+ // also handle "mode" in a separate way because it mainly depends on query params
69
+ } else if (param === 'mode') {
70
+ res = checkRouteMode($route, locationConfigParam);
71
+ } else if (locationConfigParam === params[param]) {
72
+ res = true;
73
+ } else {
74
+ res = false;
75
+ }
76
+ }
77
+
78
+ // If a single location config param is good then this is an param (aka ['pods', 'configmap'] = pods or configmaps)
79
+ if (res) {
80
+ break;
81
+ }
82
+ }
83
+
84
+ // If a single param (set of location config params) is bad then this is not an acceptable location
85
+ if (!res) {
86
+ break;
87
+ }
88
+ }
89
+ }
90
+
91
+ return res;
92
+ }
93
+
94
+ export function getApplicableExtensionEnhancements(pluginCtx, actionType, uiArea, currRoute, translationCtx = pluginCtx) {
95
+ const extensionEnhancements = [];
96
+
97
+ const actions = pluginCtx.$plugin.getUIConfig(actionType, uiArea);
98
+
99
+ actions.forEach((action, i) => {
100
+ if (checkExtensionRouteBinding(currRoute, action.locationConfig)) {
101
+ // ADD CARD PLUGIN UI ENHANCEMENT
102
+ if (actionType === ExtensionPoint.CARD) {
103
+ // intercept to apply translation
104
+ if (uiArea === CardLocation.CLUSTER_DASHBOARD_CARD && action.labelKey) {
105
+ actions[i].label = translationCtx.t(action.labelKey);
106
+ }
107
+
108
+ // ADD ACTION PLUGIN UI ENHANCEMENT
109
+ } else if (actionType === ExtensionPoint.ACTION) {
110
+ // TABLE ACTION
111
+ if (uiArea === ActionLocation.TABLE) {
112
+ // intercept to apply translation
113
+ if (action.labelKey) {
114
+ actions[i].label = translationCtx.t(action.labelKey);
115
+ }
116
+
117
+ // sets the enabled flag to true if omitted on the config
118
+ if (!Object.keys(action).includes('enabled')) {
119
+ actions[i].enabled = true;
120
+ }
121
+
122
+ // bulkable flag
123
+ actions[i].bulkable = actions[i].multiple || actions[i].bulkable;
124
+
125
+ // populate action identifier to prevent errors
126
+ if (!actions[i].action) {
127
+ actions[i].action = `custom-table-action-${ randomStr(10).toLowerCase() }`;
128
+ }
129
+ }
130
+
131
+ // extract simplified shortcut definition on plugin - HEADER ACTION
132
+ if (uiArea === ActionLocation.HEADER && action.shortcut) {
133
+ // if it's a string, then assume CTRL for windows and META for mac
134
+ if (typeof action.shortcut === 'string') {
135
+ actions[i].shortcutLabel = () => {
136
+ return isMac ? `(\u2318-${ action.shortcut.toUpperCase() })` : `(Ctrl-${ action.shortcut.toUpperCase() })`;
137
+ };
138
+ actions[i].shortcutKey = { windows: ['ctrl', action.shortcut], mac: ['meta', action.shortcut] };
139
+ // correct check for an Object type in JS... handle the object passed
140
+ } else if (typeof action.shortcut === 'object' && !Array.isArray(action.shortcut) && action.shortcut !== null) {
141
+ actions[i].shortcutKey = action.shortcut;
142
+ const keyboardCombo = isMac ? actions[i].shortcut.mac : actions[i].shortcut.windows ? actions[i].shortcut.windows : [];
143
+ let scLabel = '';
144
+
145
+ keyboardCombo.forEach((key, i) => {
146
+ if (i < keyboardCombo.length - 1) {
147
+ if (key === 'meta') {
148
+ key = '\u2318';
149
+ } else {
150
+ key = ucFirst(key);
151
+ }
152
+ scLabel += `${ key }`;
153
+ scLabel += '-';
154
+ } else {
155
+ scLabel += `${ key.toUpperCase() }`;
156
+ }
157
+ });
158
+
159
+ actions[i].shortcutLabel = () => {
160
+ return `(${ scLabel })`;
161
+ };
162
+ }
163
+ }
164
+ }
165
+
166
+ extensionEnhancements.push(actions[i]);
167
+ }
168
+ });
169
+
170
+ return extensionEnhancements;
171
+ }
package/core/plugin.ts CHANGED
@@ -1,6 +1,16 @@
1
1
  import { RouteConfig } from 'vue-router';
2
2
  import { DSL as STORE_DSL } from '@shell/store/type-map';
3
- import { CoreStoreInit, IPlugin } from './types';
3
+ import {
4
+ CoreStoreInit,
5
+ Action,
6
+ Tab,
7
+ Card,
8
+ Panel,
9
+ TableColumn,
10
+ IPlugin,
11
+ LocationConfig,
12
+ ExtensionPoint,
13
+ } from './types';
4
14
  import coreStore, { coreStoreModule, coreStoreState } from '@shell/plugins/dashboard-store';
5
15
  import {
6
16
  PluginRouteConfig, RegisterStore, UnregisterStore, CoreStoreSpecifics, CoreStoreConfig, OnNavToPackage, OnNavAwayFromPackage, OnLogOut
@@ -20,6 +30,8 @@ export class Plugin implements IPlugin {
20
30
  public onLeave: OnNavAwayFromPackage = () => Promise.resolve();
21
31
  public _onLogOut: OnLogOut = () => Promise.resolve();
22
32
 
33
+ public uiConfig: { [key: string]: any } = {};
34
+
23
35
  // Plugin metadata (plugin package.json)
24
36
  public _metadata: any = {};
25
37
 
@@ -34,6 +46,11 @@ export class Plugin implements IPlugin {
34
46
  constructor(id: string) {
35
47
  this.id = id;
36
48
  this.name = id;
49
+
50
+ // Initialize uiConfig for all of the possible enum values
51
+ Object.values(ExtensionPoint).forEach((v) => {
52
+ this.uiConfig[v] = {};
53
+ });
37
54
  }
38
55
 
39
56
  get metadata() {
@@ -108,6 +125,49 @@ export class Plugin implements IPlugin {
108
125
  this.routes.push({ parent, route });
109
126
  }
110
127
 
128
+ private _addUIConfig(type: string, where: string, when: LocationConfig | string, config: any) {
129
+ // For convenience 'when' can be a string to indicate a resource, so convert it to the LocationConfig format
130
+ const locationConfig = (typeof when === 'string') ? { resource: when } : when;
131
+
132
+ this.uiConfig[type][where] = this.uiConfig[type][where] || [];
133
+ this.uiConfig[type][where].push({ ...config, locationConfig });
134
+ }
135
+
136
+ /**
137
+ * Adds an action/button to the UI
138
+ */
139
+ addAction(where: string, when: LocationConfig | string, action: Action): void {
140
+ this._addUIConfig(ExtensionPoint.ACTION, where, when, action);
141
+ }
142
+
143
+ /**
144
+ * Adds a tab to the UI
145
+ */
146
+ addTab(where: string, when: LocationConfig | string, tab: Tab): void {
147
+ this._addUIConfig(ExtensionPoint.TAB, where, when, tab);
148
+ }
149
+
150
+ /**
151
+ * Adds a panel/component to the UI
152
+ */
153
+ addPanel(where: string, when: LocationConfig | string, panel: Panel): void {
154
+ this._addUIConfig(ExtensionPoint.PANEL, where, when, panel);
155
+ }
156
+
157
+ /**
158
+ * Adds a card to the to the UI
159
+ */
160
+ addCard( where: string, when: LocationConfig | string, card: Card): void {
161
+ this._addUIConfig(ExtensionPoint.CARD, where, when, card);
162
+ }
163
+
164
+ /**
165
+ * Adds a new column to a table on the UI
166
+ */
167
+ addTableColumn(where: string, when: LocationConfig | string, column: TableColumn): void {
168
+ this._addUIConfig(ExtensionPoint.TABLE_COL, where, when, column);
169
+ }
170
+
111
171
  setHomePage(component: any) {
112
172
  this.addRoute({
113
173
  name: 'home',
package/core/plugins.js CHANGED
@@ -3,6 +3,7 @@ import { clearModelCache } from '@shell/plugins/dashboard-store/model-loader';
3
3
  import { Plugin } from './plugin';
4
4
  import { PluginRoutes } from './plugin-routes';
5
5
  import { UI_PLUGIN_BASE_URL } from '@shell/config/uiplugins';
6
+ import { ExtensionPoint } from './types';
6
7
 
7
8
  const MODEL_TYPE = 'models';
8
9
 
@@ -21,6 +22,12 @@ export default function({
21
22
 
22
23
  const pluginRoutes = new PluginRoutes(app.router);
23
24
 
25
+ const uiConfig = {};
26
+
27
+ for (const ep in ExtensionPoint) {
28
+ uiConfig[ExtensionPoint[ep]] = {};
29
+ }
30
+
24
31
  inject('plugin', {
25
32
  // Plugins should not use these - but we will pass them in for now as a 2nd argument
26
33
  // in case there are use cases not covered that require direct access - we may remove access later
@@ -251,6 +258,18 @@ export default function({
251
258
  });
252
259
  });
253
260
 
261
+ // UI Configuration - copy UI config from a plugin into the global uiConfig object
262
+ Object.keys(plugin.uiConfig).forEach((actionType) => {
263
+ Object.keys(plugin.uiConfig[actionType]).forEach((actionLocation) => {
264
+ plugin.uiConfig[actionType][actionLocation].forEach((action) => {
265
+ if (!uiConfig[actionType][actionLocation]) {
266
+ uiConfig[actionType][actionLocation] = [];
267
+ }
268
+ uiConfig[actionType][actionLocation].push(action);
269
+ });
270
+ });
271
+ });
272
+
254
273
  // l10n
255
274
  Object.keys(plugin.l10n).forEach((name) => {
256
275
  plugin.l10n[name].forEach((fn) => {
@@ -334,6 +353,20 @@ export default function({
334
353
  return validators[name];
335
354
  },
336
355
 
356
+ /**
357
+ * Return the UI configuration for the given type and location
358
+ */
359
+ getUIConfig(type, uiArea) {
360
+ return uiConfig[type][uiArea] || [];
361
+ },
362
+
363
+ /**
364
+ * Returns all UI Configuration (useful for debugging)
365
+ */
366
+ getAllUIConfig() {
367
+ return uiConfig;
368
+ },
369
+
337
370
  // Timestamp that a UI package was last loaded
338
371
  // Typically used to invalidate caches (e.g. i18n) when new plugins are loaded
339
372
  get lastLoad() {
package/core/types.ts CHANGED
@@ -31,10 +31,111 @@ export type OnEnterLeavePackageConfig = {
31
31
  isExt: string,
32
32
  oldIsExt: string
33
33
  }
34
+
34
35
  export type OnNavToPackage = (store: any, config: OnEnterLeavePackageConfig) => Promise<void>;
35
36
  export type OnNavAwayFromPackage = (store: any, config: OnEnterLeavePackageConfig) => Promise<void>;
36
37
  export type OnLogOut = (store: any) => Promise<void>;
37
38
 
39
+ /** Enum regarding the extensionable areas/places of the UI */
40
+ export enum ExtensionPoint {
41
+ ACTION = 'Action', // eslint-disable-line no-unused-vars
42
+ TAB = 'Tab', // eslint-disable-line no-unused-vars
43
+ PANEL = 'Panel', // eslint-disable-line no-unused-vars
44
+ CARD = 'Card', // eslint-disable-line no-unused-vars
45
+ TABLE_COL = 'TableColumn', // eslint-disable-line no-unused-vars
46
+ }
47
+
48
+ /** Enum regarding action locations that are extensionable in the UI */
49
+ export enum ActionLocation {
50
+ HEADER = 'header-action', // eslint-disable-line no-unused-vars
51
+ TABLE = 'table-action', // eslint-disable-line no-unused-vars
52
+ }
53
+
54
+ /** Enum regarding panel locations that are extensionable in the UI */
55
+ export enum PanelLocation {
56
+ DETAILS_MASTHEAD = 'details-masthead', // eslint-disable-line no-unused-vars
57
+ DETAIL_TOP = 'detail-top', // eslint-disable-line no-unused-vars
58
+ RESOURCE_LIST = 'resource-list', // eslint-disable-line no-unused-vars
59
+ }
60
+
61
+ /** Enum regarding tab locations that are extensionable in the UI */
62
+ export enum TabLocation {
63
+ RESOURCE_DETAIL = 'tab', // eslint-disable-line no-unused-vars
64
+ }
65
+
66
+ /** Enum regarding card locations that are extensionable in the UI */
67
+ export enum CardLocation {
68
+ CLUSTER_DASHBOARD_CARD = 'cluster-dashboard-card', // eslint-disable-line no-unused-vars
69
+ }
70
+
71
+ /** Enum regarding table col locations that are extensionable in the UI */
72
+ export enum TableColumnLocation {
73
+ RESOURCE = 'resource-list', // eslint-disable-line no-unused-vars
74
+ }
75
+
76
+ /** Definition of the shortcut object (keyboard shortcuts) */
77
+ export type ShortCutKey = {
78
+ windows?: string[];
79
+ mac?: string[];
80
+ };
81
+
82
+ /** Definition of the action options (table actions) */
83
+ export type ActionOpts = {
84
+ event: any;
85
+ isAlt: boolean;
86
+ action: any;
87
+ };
88
+
89
+ /** Definition of an extension action (options that can be passed when setting an extension action) */
90
+ export type Action = {
91
+ label?: string;
92
+ labelKey?: string;
93
+ tooltipKey?: string;
94
+ tooltip?: string;
95
+ shortcut?: string | ShortCutKey;
96
+ svg?: Function;
97
+ icon?: string;
98
+ multiple?: boolean;
99
+ enabled?: (ctx: any) => boolean;
100
+ invoke: (opts: ActionOpts, resources: any[]) => void | boolean | Promise<boolean>;
101
+ };
102
+
103
+ /** Definition of a panel (options that can be passed when defining an extension panel enhancement) */
104
+ export type Panel = {
105
+ component: Function;
106
+ };
107
+
108
+ /** Definition of a card (options that can be passed when defining an extension card enhancement) */
109
+ export type Card = {
110
+ label?: string;
111
+ labelKey?: string;
112
+ component: Function;
113
+ };
114
+
115
+ export type TableColumn = any;
116
+
117
+ /** Definition of a tab (options that can be passed when defining an extension tab enhancement) */
118
+ export type Tab = {
119
+ name: string;
120
+ label?: string;
121
+ labelKey?: string;
122
+ tooltipKey?: string;
123
+ tooltip?: string;
124
+ showHeader?: boolean;
125
+ weight?: number;
126
+ component: Function;
127
+ };
128
+
129
+ /** Definition of the locationConfig object (used in extensions) */
130
+ export type LocationConfig = {
131
+ product?: string[],
132
+ resource?: string[],
133
+ namespace?: string[],
134
+ cluster?: string[],
135
+ id?: string[],
136
+ mode?: string[]
137
+ };
138
+
38
139
  /**
39
140
  * Interface for a Dashboard plugin
40
141
  */
@@ -63,7 +164,7 @@ export interface IPlugin {
63
164
  validators: {[key: string]: Function};
64
165
 
65
166
  /**
66
- * Add a module contains localisations for a specific locale
167
+ * Add a module containing localisations for a specific locale
67
168
  */
68
169
  addL10n(locale: string, fn: Function): void;
69
170
 
@@ -73,6 +174,31 @@ export interface IPlugin {
73
174
  addRoute(route: RouteConfig): void;
74
175
  addRoute(parent: string, route: RouteConfig): void;
75
176
 
177
+ /**
178
+ * Adds an action/button to the UI
179
+ */
180
+ addAction(where: ActionLocation | string, when: LocationConfig | string, action: Action): void;
181
+
182
+ /**
183
+ * Adds a tab to the UI (ResourceTabs component)
184
+ */
185
+ addTab(where: TabLocation | string, when: LocationConfig | string, action: Tab): void;
186
+
187
+ /**
188
+ * Adds a panel/component to the UI
189
+ */
190
+ addPanel(where: PanelLocation | string, when: LocationConfig | string, action: Panel): void;
191
+
192
+ /**
193
+ * Adds a card to the UI
194
+ */
195
+ addCard(where: CardLocation | string, when: LocationConfig | string, action: Card): void;
196
+
197
+ /**
198
+ * Adds a new column to the SortableTable component
199
+ */
200
+ addTableColumn(where: TableColumnLocation | string, when: LocationConfig | string, action: TableColumn): void;
201
+
76
202
  /**
77
203
  * Set the component to use for the landing home page
78
204
  * @param component Home page component
@@ -116,7 +242,7 @@ export interface IPlugin {
116
242
  onLogOut?: OnLogOut
117
243
  ): void;
118
244
 
119
- /**
245
+ /**
120
246
  * Register 'something' that can be dynamically loaded - e.g. model, edit, create, list, i18n
121
247
  * @param {String} type type of thing to register, e.g. 'edit'
122
248
  * @param {String} name unique name of 'something'
@@ -59,6 +59,7 @@ export default {
59
59
  :options="[false, true]"
60
60
  :labels="[t('catalog.repo.target.http'), t('catalog.repo.target.git')]"
61
61
  :mode="mode"
62
+ data-testid="clusterrepo-radio-input"
62
63
  />
63
64
  </div>
64
65
  </div>
@@ -74,6 +75,7 @@ export default {
74
75
  :label="t('catalog.repo.gitRepo.label')"
75
76
  :placeholder="t('catalog.repo.gitRepo.placeholder', null, true)"
76
77
  :mode="mode"
78
+ data-testid="clusterrepo-git-repo-input"
77
79
  />
78
80
  </div>
79
81
  <div class="col span-6">
@@ -83,6 +85,7 @@ export default {
83
85
  :label="t('catalog.repo.gitBranch.label')"
84
86
  :placeholder="t('catalog.repo.gitBranch.placeholder', null, true)"
85
87
  :mode="mode"
88
+ data-testid="clusterrepo-git-branch-input"
86
89
  />
87
90
  </div>
88
91
  </div>
@@ -154,7 +154,7 @@ export default {
154
154
  view: _VIEW,
155
155
  yamlError: '',
156
156
  fvFormRuleSets: [
157
- { path: 'name', rules: ['required'] }
157
+ { path: 'name', rules: ['required', 'duplicateName'] }
158
158
  ],
159
159
  fvReportedValidationPaths: ['value']
160
160
  };
@@ -183,7 +183,19 @@ export default {
183
183
  receiverNameDisabled() {
184
184
  return this.$route.query.mode === _VIEW;
185
185
  },
186
-
186
+ fvExtraRules() {
187
+ return {
188
+ duplicateName: () => {
189
+ const receiversArray = this.alertmanagerConfigResource.spec.receivers;
190
+ const receiverNamesArray = receiversArray.map(R => R.name);
191
+ const receiversSet = new Set(receiverNamesArray);
192
+
193
+ if (receiversArray.length !== receiversSet.size) {
194
+ return this.$store.getters['i18n/t']('monitoring.alerting.validation.duplicatedReceiverName', { name: this.value.name });
195
+ }
196
+ }
197
+ };
198
+ }
187
199
  },
188
200
 
189
201
  watch: {
@@ -6,6 +6,9 @@ describe('component: rke2', () => {
6
6
  'v1.25.0+rke2r1',
7
7
  'v1.24.0+rke2r1',
8
8
  'v1.23.0+rke2r1',
9
+ 'v1.25.0+k3s1',
10
+ 'v1.24.0+k3s1',
11
+ 'v1.23.0+k3s1',
9
12
  ])('should display PSA option', () => {
10
13
  const label = 'whatever';
11
14
  const option = { label, value: label };
@@ -314,6 +314,7 @@ export default {
314
314
  harvesterVersionRange: {},
315
315
  lastDefaultPodSecurityPolicyTemplateName,
316
316
  previousKubernetesVersion,
317
+ harvesterVersion: ''
317
318
  };
318
319
  },
319
320
 
@@ -356,11 +357,10 @@ export default {
356
357
  */
357
358
  needsPSA() {
358
359
  const release = this.value?.spec?.kubernetesVersion || '';
359
- const isRKE2 = release.includes('rke2');
360
360
  const version = release.match(/\d+/g);
361
361
  const isRequiredVersion = version?.length ? +version[0] > 1 || +version[1] >= 23 : false;
362
362
 
363
- return isRKE2 && isRequiredVersion;
363
+ return isRequiredVersion;
364
364
  },
365
365
 
366
366
  /**
@@ -903,6 +903,7 @@ export default {
903
903
  },
904
904
 
905
905
  isHarvesterIncompatible() {
906
+ const CompareVersion = '<v1.2';
906
907
  let ccmRke2Version = (this.chartVersions['harvester-cloud-provider'] || {})['version'];
907
908
  let csiRke2Version = (this.chartVersions['harvester-csi-driver'] || {})['version'];
908
909
 
@@ -918,8 +919,12 @@ export default {
918
919
  }
919
920
 
920
921
  if (ccmVersion && csiVersion) {
921
- if (semver.satisfies(ccmRke2Version, ccmVersion) &&
922
- semver.satisfies(csiRke2Version, csiVersion)) {
922
+ if (semver.satisfies(this.harvesterVersion, CompareVersion) || !(this.harvesterVersion || '').startsWith('v')) {
923
+ // When harveste version is less than `CompareVersion`, compatibility is not determined,
924
+ // At the same time, version numbers like this will not be checked: master-14bbee2c-head
925
+ return false;
926
+ } else if (semver.satisfies(ccmRke2Version, ccmVersion) &&
927
+ semver.satisfies(csiRke2Version, csiVersion)) {
923
928
  return false;
924
929
  } else {
925
930
  return true;
@@ -1752,7 +1757,10 @@ export default {
1752
1757
  const res = await this.$store.dispatch('cluster/request', { url: `${ url }/${ HCI.SETTING }s` });
1753
1758
 
1754
1759
  const version = (res?.data || []).find(s => s.id === 'harvester-csi-ccm-versions');
1760
+ // get harvester server-version
1761
+ const serverVersion = (res?.data || []).find(s => s.id === 'server-version');
1755
1762
 
1763
+ this.harvesterVersion = serverVersion.value;
1756
1764
  if (version) {
1757
1765
  this.harvesterVersionRange = JSON.parse(version.value || version.default || '{}');
1758
1766
  } else {
@@ -1791,32 +1799,27 @@ export default {
1791
1799
  const major = parseInt(version?.[0] || 0);
1792
1800
  const minor = parseInt(version?.[1] || 0);
1793
1801
 
1794
- // If the new version is 1.25 or greater, set the PSP Policy to 'RKE2 Default' (empty string)
1795
- if (major === 1 && minor >= 25) {
1802
+ // Reset PSA if not RKE2
1803
+ if (!value.includes('rke2')) {
1796
1804
  set(this.value.spec, 'defaultPodSecurityPolicyTemplateName', '');
1797
1805
  } else {
1798
- const previous = VERSION.parse(this.previousKubernetesVersion);
1799
- const major = parseInt(previous?.[0] || 0);
1800
- const minor = parseInt(previous?.[1] || 0);
1801
-
1806
+ // Reset PSP if it's legacy due k8s version 1.25+
1802
1807
  if (major === 1 && minor >= 25) {
1803
- // Previous value was 1.25 or greater, so reset back
1808
+ set(this.value.spec, 'defaultPodSecurityPolicyTemplateName', '');
1809
+ } else {
1804
1810
  set(this.value.spec, 'defaultPodSecurityPolicyTemplateName', this.lastDefaultPodSecurityPolicyTemplateName);
1805
1811
  }
1806
- }
1807
1812
 
1808
- this.previousKubernetesVersion = value;
1809
- set(this.value.spec, 'defaultPodSecurityAdmissionConfigurationTemplateName', '');
1813
+ this.previousKubernetesVersion = value;
1814
+ }
1810
1815
  }
1811
1816
  },
1812
1817
 
1813
1818
  /**
1814
- * Handle PSP changes side effects, like PSA resets
1819
+ * Keep last PSP value
1815
1820
  */
1816
1821
  handlePspChange(value) {
1817
- if (value) {
1818
- this.lastDefaultPodSecurityPolicyTemplateName = value;
1819
- }
1822
+ this.lastDefaultPodSecurityPolicyTemplateName = value;
1820
1823
  },
1821
1824
 
1822
1825
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rancher/shell",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Rancher Dashboard Shell",
5
5
  "repository": "https://github.com/rancherlabs/dashboard",
6
6
  "license": "Apache-2.0",
@@ -21,6 +21,7 @@ import { CATALOG } from '@shell/config/labels-annotations';
21
21
  import { isUIPlugin } from '@shell/config/uiplugins';
22
22
 
23
23
  export default {
24
+ name: 'Charts',
24
25
  components: {
25
26
  AsyncButton,
26
27
  Banner,
@@ -53,6 +54,7 @@ export default {
53
54
  searchQuery: null,
54
55
  showDeprecated: null,
55
56
  showHidden: null,
57
+ isPspLegacy: false,
56
58
  chartOptions: [
57
59
  {
58
60
  label: 'Browse',
@@ -239,6 +241,14 @@ export default {
239
241
  }
240
242
  },
241
243
 
244
+ created() {
245
+ const release = this.currentCluster?.status?.version.gitVersion || '';
246
+ const isRKE2 = release.includes('rke2');
247
+ const version = release.match(/\d+/g);
248
+
249
+ this.isPspLegacy = version?.length ? isRKE2 && (+version[0] === 1 && +version[1] < 25) : false;
250
+ },
251
+
242
252
  methods: {
243
253
  colorForChart(chart) {
244
254
  const repos = this.repoOptions;
@@ -363,6 +373,13 @@ export default {
363
373
  @clicked="(row) => selectChart(row)"
364
374
  />
365
375
  </div>
376
+
377
+ <Banner
378
+ v-if="isPspLegacy"
379
+ color="warning"
380
+ :label="t('catalog.chart.banner.legacy')"
381
+ />
382
+
366
383
  <TypeDescription resource="chart" />
367
384
  <div class="left-right-split">
368
385
  <Select