@rancher/shell 3.0.9 → 3.0.11
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.
- package/assets/styles/base/_color.scss +4 -0
- package/assets/styles/themes/_light.scss +6 -6
- package/assets/styles/themes/_modern.scss +14 -6
- package/assets/translations/en-us.yaml +9 -10
- package/chart/__tests__/rancher-backup-index.test.ts +248 -0
- package/chart/rancher-backup/index.vue +41 -2
- package/components/BrandImage.vue +6 -5
- package/components/ConsumptionGauge.vue +12 -4
- package/components/CopyToClipboard.vue +28 -0
- package/components/CopyToClipboardText.vue +4 -0
- package/components/CruResource.vue +1 -0
- package/components/DynamicContent/DynamicContentIcon.vue +3 -2
- package/components/ExplorerProjectsNamespaces.vue +1 -4
- package/components/GlobalRoleBindings.vue +1 -5
- package/components/LazyImage.vue +2 -1
- package/components/Resource/Detail/Card/Scaler.vue +4 -4
- package/components/ResourceDetail/index.vue +0 -21
- package/components/Tabbed/index.vue +6 -0
- package/components/__tests__/ConsumptionGauge.test.ts +31 -0
- package/components/__tests__/CruResource.test.ts +35 -1
- package/components/form/ProjectMemberEditor.vue +0 -10
- package/components/nav/TopLevelMenu.helper.ts +7 -79
- package/components/nav/__tests__/TopLevelMenu.helper.test.ts +2 -53
- package/composables/useIsNewDetailPageEnabled.test.ts +98 -0
- package/composables/useIsNewDetailPageEnabled.ts +12 -0
- package/config/private-label.js +2 -1
- package/config/product/apps.js +1 -0
- package/config/product/explorer.js +11 -1
- package/config/table-headers.js +0 -9
- package/config/types.js +0 -1
- package/core/__tests__/extension-manager-impl.test.js +187 -2
- package/core/extension-manager-impl.js +4 -2
- package/core/plugin-helpers.ts +31 -0
- package/detail/__tests__/node.test.ts +83 -0
- package/detail/management.cattle.io.oidcclient.vue +2 -1
- package/detail/node.vue +1 -0
- package/edit/auth/github-app-steps.vue +2 -0
- package/edit/auth/github-steps.vue +2 -0
- package/edit/catalog.cattle.io.clusterrepo.vue +17 -3
- package/edit/cloudcredential.vue +2 -1
- package/edit/management.cattle.io.user.vue +60 -35
- package/edit/monitoring.coreos.com.alertmanagerconfig/receiverConfig.vue +11 -6
- package/edit/provisioning.cattle.io.cluster/index.vue +5 -4
- package/edit/provisioning.cattle.io.cluster/shared.ts +4 -2
- package/edit/secret/generic.vue +1 -0
- package/edit/secret/index.vue +2 -1
- package/edit/service.vue +2 -14
- package/edit/token.vue +29 -68
- package/list/management.cattle.io.feature.vue +7 -1
- package/list/provisioning.cattle.io.cluster.vue +0 -49
- package/mixins/brand.js +2 -1
- package/models/catalog.cattle.io.clusterrepo.js +9 -0
- package/models/cluster.x-k8s.io.machinedeployment.js +8 -3
- package/models/management.cattle.io.authconfig.js +2 -1
- package/models/management.cattle.io.cluster.js +4 -3
- package/models/monitoring.coreos.com.receiver.js +11 -6
- package/models/provisioning.cattle.io.cluster.js +2 -2
- package/models/token.js +0 -4
- package/package.json +12 -12
- package/pages/account/index.vue +67 -96
- package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +66 -9
- package/pages/c/_cluster/apps/charts/index.vue +3 -8
- package/pages/c/_cluster/apps/charts/install.vue +8 -9
- package/pages/c/_cluster/explorer/index.vue +2 -19
- package/pages/c/_cluster/istio/index.vue +4 -2
- package/pages/c/_cluster/longhorn/index.vue +2 -1
- package/pages/c/_cluster/monitoring/index.vue +2 -2
- package/pages/c/_cluster/neuvector/index.vue +2 -1
- package/pages/c/_cluster/settings/performance.vue +0 -5
- package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +2 -1
- package/pages/c/_cluster/uiplugins/index.vue +2 -1
- package/pkg/auto-import.js +41 -0
- package/plugins/dashboard-store/resource-class.js +2 -2
- package/plugins/steve/__tests__/steve-class.test.ts +1 -1
- package/plugins/steve/steve-class.js +3 -3
- package/plugins/steve/steve-pagination-utils.ts +2 -5
- package/plugins/steve/subscribe.js +29 -4
- package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.vue +7 -7
- package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +5 -2
- package/rancher-components/RcButton/RcButton.vue +3 -3
- package/rancher-components/RcButtonSplit/RcButtonSplit.test.ts +253 -0
- package/rancher-components/RcButtonSplit/RcButtonSplit.vue +158 -0
- package/rancher-components/RcButtonSplit/index.ts +1 -0
- package/rancher-components/RcIcon/types.ts +2 -2
- package/rancher-components/RcSection/RcSection.test.ts +323 -0
- package/rancher-components/RcSection/RcSection.vue +252 -0
- package/rancher-components/RcSection/RcSectionActions.test.ts +212 -0
- package/rancher-components/RcSection/RcSectionActions.vue +85 -0
- package/rancher-components/RcSection/RcSectionBadges.test.ts +149 -0
- package/rancher-components/RcSection/RcSectionBadges.vue +29 -0
- package/rancher-components/RcSection/index.ts +12 -0
- package/rancher-components/RcSection/types.ts +86 -0
- package/scripts/test-plugins-build.sh +9 -8
- package/types/shell/index.d.ts +93 -108
- package/utils/__tests__/require-asset.test.ts +98 -0
- package/utils/async.ts +1 -5
- package/utils/brand.ts +3 -1
- package/utils/favicon.js +4 -3
- package/utils/require-asset.ts +95 -0
- package/utils/style.ts +17 -0
- package/utils/units.js +14 -5
- package/vue.config.js +4 -3
- package/components/HarvesterServiceAddOnConfig.vue +0 -207
- package/models/ext.cattle.io.token.js +0 -48
|
@@ -38,8 +38,30 @@ jest.mock('@shell/core/plugin', () => {
|
|
|
38
38
|
};
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
// Mock ExtensionPoint
|
|
42
|
-
|
|
41
|
+
// Mock ExtensionPoint — only EDIT_YAML, simulating an older dashboard (e.g. 2.13)
|
|
42
|
+
// that does not know about newer ExtensionPoints like 'Table'.
|
|
43
|
+
// The ensureUIConfigCompat tests override this to include TABLE for plugin-helpers,
|
|
44
|
+
// simulating an extension shipping a newer shell.
|
|
45
|
+
jest.mock('@shell/core/types', () => ({
|
|
46
|
+
ExtensionPoint: { EDIT_YAML: 'edit-yaml' },
|
|
47
|
+
ActionLocation: { TABLE: 'table-action' },
|
|
48
|
+
CardLocation: {},
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
jest.mock('@shell/utils/platform', () => ({ isMac: false }));
|
|
52
|
+
jest.mock('@shell/utils/string', () => ({
|
|
53
|
+
ucFirst: jest.fn((s) => s),
|
|
54
|
+
randomStr: jest.fn(() => 'abc123'),
|
|
55
|
+
}));
|
|
56
|
+
jest.mock('@shell/config/query-params', () => ({
|
|
57
|
+
_EDIT: 'edit',
|
|
58
|
+
_CONFIG: 'config',
|
|
59
|
+
_DETAIL: 'detail',
|
|
60
|
+
_LIST: 'list',
|
|
61
|
+
_CREATE: 'create',
|
|
62
|
+
}));
|
|
63
|
+
jest.mock('@shell/utils/router', () => ({ getProductFromRoute: jest.fn() }));
|
|
64
|
+
jest.mock('@shell/utils/object', () => ({ isEqual: jest.fn() }));
|
|
43
65
|
|
|
44
66
|
// Mock PluginRoutes
|
|
45
67
|
jest.mock('@shell/core/plugin-routes', () => {
|
|
@@ -299,6 +321,28 @@ describe('extension Manager', () => {
|
|
|
299
321
|
delete window[pluginId];
|
|
300
322
|
});
|
|
301
323
|
|
|
324
|
+
it('surfaces the plugin id and original error message when initialization fails', async() => {
|
|
325
|
+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
326
|
+
const pluginId = 'surface-error-plugin';
|
|
327
|
+
const originalError = new Error('something went wrong');
|
|
328
|
+
const mockPluginInit = jest.fn().mockImplementation(() => {
|
|
329
|
+
throw originalError;
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
window[pluginId] = { default: mockPluginInit };
|
|
333
|
+
|
|
334
|
+
const loadPromise = manager.loadAsync(pluginId, 'test.js');
|
|
335
|
+
const script = document.head.querySelector(`script[id="${ pluginId }"]`);
|
|
336
|
+
|
|
337
|
+
script.onload();
|
|
338
|
+
|
|
339
|
+
await expect(loadPromise).rejects.toThrow(`Could not initialize plugin ${ pluginId } - ${ originalError.message }`);
|
|
340
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(`Could not initialize plugin ${ pluginId }`, originalError);
|
|
341
|
+
|
|
342
|
+
consoleErrorSpy.mockRestore();
|
|
343
|
+
delete window[pluginId];
|
|
344
|
+
});
|
|
345
|
+
|
|
302
346
|
it('rejects if script load fails', async() => {
|
|
303
347
|
const pluginId = 'fail-plugin';
|
|
304
348
|
const loadPromise = manager.loadAsync(pluginId, 'bad-url.js');
|
|
@@ -433,5 +477,146 @@ describe('extension Manager', () => {
|
|
|
433
477
|
|
|
434
478
|
expect(config).toStrictEqual([]);
|
|
435
479
|
});
|
|
480
|
+
|
|
481
|
+
it('returns empty array for non-existent type', () => {
|
|
482
|
+
// Simulates an extension using a newer ExtensionPoint (e.g. 'Table')
|
|
483
|
+
// that the running dashboard does not have in its uiConfig
|
|
484
|
+
const config = manager.getUIConfig('unknown-type', 'some-area');
|
|
485
|
+
|
|
486
|
+
expect(config).toStrictEqual([]);
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
describe('ensureUIConfigCompat (via plugin-helpers)', () => {
|
|
491
|
+
// These tests verify that getApplicableExtensionEnhancements tracks missing
|
|
492
|
+
// ExtensionPoint keys and exits early when accessing them.
|
|
493
|
+
// This is the forwards-compatibility mechanism for extensions running on
|
|
494
|
+
// older dashboards that don't know about newer ExtensionPoints (e.g. 'Table').
|
|
495
|
+
|
|
496
|
+
const mockRoute = {
|
|
497
|
+
name: 'test',
|
|
498
|
+
params: {},
|
|
499
|
+
query: {},
|
|
500
|
+
meta: {},
|
|
501
|
+
hash: '',
|
|
502
|
+
path: '/test',
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
// Each test needs a fresh plugin-helpers module to reset the _uiConfigPatched tracking.
|
|
506
|
+
// The extension-manager is created with the "old" ExtensionPoint (only EDIT_YAML),
|
|
507
|
+
// then plugin-helpers is re-required with an "upgraded" ExtensionPoint that includes TABLE,
|
|
508
|
+
// simulating an extension built with a newer shell running on an older dashboard.
|
|
509
|
+
let freshGetApplicable;
|
|
510
|
+
let freshManager;
|
|
511
|
+
|
|
512
|
+
beforeEach(() => {
|
|
513
|
+
freshManager = createExtensionManager(context);
|
|
514
|
+
|
|
515
|
+
// Reset modules so plugin-helpers._uiConfigPatched starts as empty object
|
|
516
|
+
jest.resetModules();
|
|
517
|
+
|
|
518
|
+
// Override the types mock so plugin-helpers sees a newer ExtensionPoint with TABLE
|
|
519
|
+
jest.doMock('@shell/core/types', () => ({
|
|
520
|
+
ExtensionPoint: {
|
|
521
|
+
EDIT_YAML: 'edit-yaml',
|
|
522
|
+
TABLE: 'Table',
|
|
523
|
+
},
|
|
524
|
+
ActionLocation: { TABLE: 'table-action' },
|
|
525
|
+
CardLocation: {},
|
|
526
|
+
}));
|
|
527
|
+
|
|
528
|
+
freshGetApplicable = require('@shell/core/plugin-helpers').getApplicableExtensionEnhancements;
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('returns empty array for missing ExtensionPoint and does not mutate uiConfig', () => {
|
|
532
|
+
// Verify 'Table' is NOT in uiConfig before the call
|
|
533
|
+
const uiConfig = freshManager.getAllUIConfig();
|
|
534
|
+
|
|
535
|
+
expect(uiConfig['Table']).toBeUndefined();
|
|
536
|
+
|
|
537
|
+
// Call getApplicableExtensionEnhancements which triggers ensureUIConfigCompat
|
|
538
|
+
const result = freshGetApplicable({ $extension: freshManager }, 'Table', 'some-area', mockRoute);
|
|
539
|
+
|
|
540
|
+
// Should return empty array for missing extension point
|
|
541
|
+
expect(result).toStrictEqual([]);
|
|
542
|
+
|
|
543
|
+
// uiConfig should NOT be mutated - 'Table' should remain undefined
|
|
544
|
+
expect(uiConfig['Table']).toBeUndefined();
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it('does not affect existing ExtensionPoint keys', () => {
|
|
548
|
+
const mockAction = { label: 'Test', locationConfig: {} };
|
|
549
|
+
const plugin = {
|
|
550
|
+
types: {},
|
|
551
|
+
uiConfig: { 'edit-yaml': { header: [mockAction] } },
|
|
552
|
+
l10n: {},
|
|
553
|
+
modelExtensions: {},
|
|
554
|
+
stores: [],
|
|
555
|
+
locales: [],
|
|
556
|
+
routes: [],
|
|
557
|
+
validators: {},
|
|
558
|
+
productNames: [],
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
freshManager.applyPlugin(plugin);
|
|
562
|
+
|
|
563
|
+
const result = freshGetApplicable({ $extension: freshManager }, 'edit-yaml', 'header', mockRoute);
|
|
564
|
+
|
|
565
|
+
// Should return the actual action
|
|
566
|
+
expect(result).toHaveLength(1);
|
|
567
|
+
expect(result[0].label).toBe('Test');
|
|
568
|
+
|
|
569
|
+
// Existing config should be untouched
|
|
570
|
+
const uiConfig = freshManager.getAllUIConfig();
|
|
571
|
+
|
|
572
|
+
expect(uiConfig['edit-yaml'].header).toHaveLength(1);
|
|
573
|
+
expect(uiConfig['edit-yaml'].header[0]).toBe(mockAction);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it('logs a warning for missing ExtensionPoints', () => {
|
|
577
|
+
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
578
|
+
|
|
579
|
+
freshGetApplicable({ $extension: freshManager }, 'Table', 'some-area', mockRoute);
|
|
580
|
+
|
|
581
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
582
|
+
expect.stringContaining('Table')
|
|
583
|
+
);
|
|
584
|
+
|
|
585
|
+
consoleWarnSpy.mockRestore();
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it('dashboard-side getUIConfig calls remain safe with optional chaining', () => {
|
|
589
|
+
// getUIConfig('Table', ...) should not crash even though 'Table' is missing
|
|
590
|
+
// Returns empty array due to optional chaining in extension-manager
|
|
591
|
+
expect(freshManager.getUIConfig('Table', 'some-area')).toStrictEqual([]);
|
|
592
|
+
|
|
593
|
+
// Trigger ensureUIConfigCompat via getApplicableExtensionEnhancements
|
|
594
|
+
const result = freshGetApplicable({ $extension: freshManager }, 'Table', 'some-area', mockRoute);
|
|
595
|
+
|
|
596
|
+
// getApplicableExtensionEnhancements exits early and returns empty array
|
|
597
|
+
expect(result).toStrictEqual([]);
|
|
598
|
+
|
|
599
|
+
// uiConfig is NOT mutated - 'Table' key still doesn't exist
|
|
600
|
+
const uiConfig = freshManager.getAllUIConfig();
|
|
601
|
+
|
|
602
|
+
expect(uiConfig['Table']).toBeUndefined();
|
|
603
|
+
|
|
604
|
+
// Direct getUIConfig calls still work safely (optional chaining)
|
|
605
|
+
expect(freshManager.getUIConfig('Table', 'some-area')).toStrictEqual([]);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it('only logs warning once for each missing ExtensionPoint', () => {
|
|
609
|
+
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
610
|
+
|
|
611
|
+
// First call logs warning and tracks 'Table' as missing
|
|
612
|
+
freshGetApplicable({ $extension: freshManager }, 'Table', 'some-area', mockRoute);
|
|
613
|
+
// Second call should not log warning again (already tracked)
|
|
614
|
+
freshGetApplicable({ $extension: freshManager }, 'Table', 'some-area', mockRoute);
|
|
615
|
+
|
|
616
|
+
// Warning should only be logged once
|
|
617
|
+
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
|
|
618
|
+
|
|
619
|
+
consoleWarnSpy.mockRestore();
|
|
620
|
+
});
|
|
436
621
|
});
|
|
437
622
|
});
|
|
@@ -124,7 +124,9 @@ export const createExtensionManager = (context) => {
|
|
|
124
124
|
} catch (e) {
|
|
125
125
|
delete plugins[id];
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
console.error(`Could not initialize plugin ${ id }`, e); // eslint-disable-line no-console
|
|
128
|
+
|
|
129
|
+
return reject(new Error(`Could not initialize plugin ${ id } - ${ e?.message }`));
|
|
128
130
|
}
|
|
129
131
|
|
|
130
132
|
// Load all of the types etc from the plugin
|
|
@@ -426,7 +428,7 @@ export const createExtensionManager = (context) => {
|
|
|
426
428
|
* Return the UI configuration for the given type and location
|
|
427
429
|
*/
|
|
428
430
|
getUIConfig(type, uiArea) {
|
|
429
|
-
return uiConfig[type][uiArea] || [];
|
|
431
|
+
return uiConfig[type]?.[uiArea] || [];
|
|
430
432
|
},
|
|
431
433
|
|
|
432
434
|
/**
|
package/core/plugin-helpers.ts
CHANGED
|
@@ -137,6 +137,30 @@ function checkExtensionRouteBinding($route: any, locationConfig: any, context: a
|
|
|
137
137
|
return res;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
// Track which ExtensionPoint keys are missing from the extension manager's uiConfig.
|
|
141
|
+
// This handles forwards-compatibility when extensions ship a newer shell that defines
|
|
142
|
+
// ExtensionPoint values the running dashboard doesn't know about (e.g. 'Table' on 2.13).
|
|
143
|
+
const _uiConfigPatched: { [point: string]: boolean } = {};
|
|
144
|
+
|
|
145
|
+
function ensureUIConfigCompat(extensionManager: any) {
|
|
146
|
+
const uiConfig = extensionManager.getAllUIConfig?.();
|
|
147
|
+
|
|
148
|
+
if (uiConfig) {
|
|
149
|
+
const missingPoints: { [point: string]: boolean } = {};
|
|
150
|
+
|
|
151
|
+
Object.values(ExtensionPoint).forEach((ep) => {
|
|
152
|
+
if (!uiConfig[ep] && !_uiConfigPatched[ep]) {
|
|
153
|
+
missingPoints[ep] = true;
|
|
154
|
+
_uiConfigPatched[ep] = true;
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (Object.keys(missingPoints).length) {
|
|
159
|
+
console.warn(`[plugin-helpers] These ExtensionPoints aren't available for usage in this Rancher version: ${ Object.keys(missingPoints).join(', ') }`); // eslint-disable-line no-console
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
140
164
|
export function getApplicableExtensionEnhancements<T>(
|
|
141
165
|
pluginCtx: ComponentOptionsMixin,
|
|
142
166
|
actionType: ExtensionPoint,
|
|
@@ -148,6 +172,13 @@ export function getApplicableExtensionEnhancements<T>(
|
|
|
148
172
|
|
|
149
173
|
// gate it so that we prevent errors on older versions of dashboard
|
|
150
174
|
if (pluginCtx.$extension?.getUIConfig) {
|
|
175
|
+
ensureUIConfigCompat(pluginCtx.$extension);
|
|
176
|
+
|
|
177
|
+
// Exit early if actionType doesn't exist in the extension manager's uiConfig
|
|
178
|
+
if (_uiConfigPatched[actionType]) {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
|
|
151
182
|
const actions = pluginCtx.$extension.getUIConfig(actionType, uiArea);
|
|
152
183
|
|
|
153
184
|
actions.forEach((action: any, i: number) => {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { shallowMount } from '@vue/test-utils';
|
|
2
|
+
import node from '@shell/detail/node.vue';
|
|
3
|
+
import ConsumptionGauge from '@shell/components/ConsumptionGauge.vue';
|
|
4
|
+
|
|
5
|
+
describe('view: node detail', () => {
|
|
6
|
+
const mockStore = {
|
|
7
|
+
getters: {
|
|
8
|
+
'cluster/schemaFor': () => undefined,
|
|
9
|
+
'cluster/paginationEnabled': () => false,
|
|
10
|
+
'type-map/headersFor': jest.fn(),
|
|
11
|
+
'i18n/t': (key: string) => key,
|
|
12
|
+
currentCluster: { id: 'local' },
|
|
13
|
+
},
|
|
14
|
+
dispatch: jest.fn(),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const mocks = {
|
|
18
|
+
$store: mockStore,
|
|
19
|
+
$fetchState: { pending: false },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const defaultNodeValue = {
|
|
23
|
+
metadata: { name: 'test-node' },
|
|
24
|
+
status: {
|
|
25
|
+
nodeInfo: {}, images: [], conditions: []
|
|
26
|
+
},
|
|
27
|
+
spec: { taints: [] },
|
|
28
|
+
pods: [],
|
|
29
|
+
cpuCapacity: 4,
|
|
30
|
+
cpuUsage: 2,
|
|
31
|
+
ramReserved: 8000,
|
|
32
|
+
ramUsage: 4000,
|
|
33
|
+
podCapacity: 110,
|
|
34
|
+
podConsumed: 5,
|
|
35
|
+
isPidPressureOk: true,
|
|
36
|
+
isDiskPressureOk: true,
|
|
37
|
+
isMemoryPressureOk: true,
|
|
38
|
+
isKubeletOk: true,
|
|
39
|
+
internalIp: '10.0.0.1',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function createWrapper(nodeOverrides = {}) {
|
|
43
|
+
return shallowMount(node, {
|
|
44
|
+
props: { value: { ...defaultNodeValue, ...nodeOverrides } },
|
|
45
|
+
global: { mocks },
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function findGaugeByResource(wrapper: ReturnType<typeof createWrapper>, resourceKey: string) {
|
|
50
|
+
const gauges = wrapper.findAllComponents(ConsumptionGauge);
|
|
51
|
+
|
|
52
|
+
return gauges.find(
|
|
53
|
+
(g) => g.attributes('resourcename')?.includes(resourceKey)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
it('should pass the "running" translation key as usedlabel to the pods ConsumptionGauge', () => {
|
|
58
|
+
const wrapper = createWrapper();
|
|
59
|
+
|
|
60
|
+
const podsGauge = findGaugeByResource(wrapper, 'consumptionGauge.pods');
|
|
61
|
+
|
|
62
|
+
expect(podsGauge).toBeDefined();
|
|
63
|
+
expect(podsGauge!.attributes('usedlabel')).toContain('consumptionGauge.running');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should NOT pass a usedlabel to the CPU ConsumptionGauge', () => {
|
|
67
|
+
const wrapper = createWrapper();
|
|
68
|
+
|
|
69
|
+
const cpuGauge = findGaugeByResource(wrapper, 'consumptionGauge.cpu');
|
|
70
|
+
|
|
71
|
+
expect(cpuGauge).toBeDefined();
|
|
72
|
+
expect(cpuGauge!.attributes('usedlabel')).toBeUndefined();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should NOT pass a usedlabel to the Memory ConsumptionGauge', () => {
|
|
76
|
+
const wrapper = createWrapper();
|
|
77
|
+
|
|
78
|
+
const memoryGauge = findGaugeByResource(wrapper, 'consumptionGauge.memory');
|
|
79
|
+
|
|
80
|
+
expect(memoryGauge).toBeDefined();
|
|
81
|
+
expect(memoryGauge!.attributes('usedlabel')).toBeUndefined();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -9,6 +9,7 @@ import DateComponent from '@shell/components/formatter/Date.vue';
|
|
|
9
9
|
import { RcItemCard } from '@components/RcItemCard';
|
|
10
10
|
import ActionMenu, { type ActionMenuSelection } from '@shell/components/ActionMenuShell.vue';
|
|
11
11
|
import { Banner } from '@components/Banner';
|
|
12
|
+
import keySvg from '~shell/assets/images/key.svg';
|
|
12
13
|
|
|
13
14
|
type SecretActionType = 'create-secret' | 'regen-secret' | 'remove-secret'
|
|
14
15
|
interface ClientSecretData { createdAt: string, lastUsedAt: string, lastFiveCharacters: string }
|
|
@@ -228,7 +229,7 @@ export default defineComponent({
|
|
|
228
229
|
clientSecrets.push({
|
|
229
230
|
id: oidcSecretDataKey,
|
|
230
231
|
header: { title: { text: oidcSecretDataKey } },
|
|
231
|
-
image: { src:
|
|
232
|
+
image: { src: keySvg },
|
|
232
233
|
createdAt,
|
|
233
234
|
lastFiveCharacters: oidcSecretData.lastFiveCharacters,
|
|
234
235
|
lastUsedAt,
|
package/detail/node.vue
CHANGED
|
@@ -234,6 +234,7 @@ export default {
|
|
|
234
234
|
:resource-name="t('node.detail.glance.consumptionGauge.pods')"
|
|
235
235
|
:capacity="value.podCapacity"
|
|
236
236
|
:used="value.podConsumed"
|
|
237
|
+
:used-label="t('node.detail.glance.consumptionGauge.running')"
|
|
237
238
|
/>
|
|
238
239
|
</div>
|
|
239
240
|
<div class="spacer" />
|
|
@@ -49,6 +49,7 @@ defineProps<{
|
|
|
49
49
|
<CopyToClipboard
|
|
50
50
|
label-as="tooltip"
|
|
51
51
|
:text="tArgs.serverUrl"
|
|
52
|
+
:aria-label="t('generic.copyValueToClipboard', { value: tArgs.serverUrl })"
|
|
52
53
|
class="icon-btn"
|
|
53
54
|
action-color="bg-transparent"
|
|
54
55
|
/>
|
|
@@ -67,6 +68,7 @@ defineProps<{
|
|
|
67
68
|
<CopyToClipboard
|
|
68
69
|
:text="t(`authConfig.${name}.form.callback.value`, tArgs, true)"
|
|
69
70
|
label-as="tooltip"
|
|
71
|
+
:aria-label="t('generic.copyValueToClipboard', { value: t(`authConfig.${name}.form.callback.value`, tArgs, true) })"
|
|
70
72
|
class="icon-btn"
|
|
71
73
|
action-color="bg-transparent"
|
|
72
74
|
/>
|
|
@@ -40,6 +40,7 @@ defineProps<{
|
|
|
40
40
|
<b>{{ t(`authConfig.${name}.form.homepage.label`) }}</b>: {{ tArgs.serverUrl }} <CopyToClipboard
|
|
41
41
|
label-as="tooltip"
|
|
42
42
|
:text="tArgs.serverUrl"
|
|
43
|
+
:aria-label="t('generic.copyValueToClipboard', { value: tArgs.serverUrl })"
|
|
43
44
|
class="icon-btn"
|
|
44
45
|
action-color="bg-transparent"
|
|
45
46
|
/>
|
|
@@ -49,6 +50,7 @@ defineProps<{
|
|
|
49
50
|
<b>{{ t(`authConfig.${name}.form.callback.label`) }}</b>: {{ tArgs.serverUrl }} <CopyToClipboard
|
|
50
51
|
:text="tArgs.serverUrl"
|
|
51
52
|
label-as="tooltip"
|
|
53
|
+
:aria-label="t('generic.copyValueToClipboard', { value: tArgs.serverUrl })"
|
|
52
54
|
class="icon-btn"
|
|
53
55
|
action-color="bg-transparent"
|
|
54
56
|
/>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
|
3
|
+
import AsyncButton from '@shell/components/AsyncButton.vue';
|
|
3
4
|
import Footer from '@shell/components/form/Footer';
|
|
4
5
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
5
6
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
|
@@ -16,6 +17,7 @@ import { getVersionData } from '@shell/config/version';
|
|
|
16
17
|
import { RcItemCard } from '@components/RcItemCard';
|
|
17
18
|
import { _CREATE, _EDIT, TARGET, _VIEW } from '@shell/config/query-params';
|
|
18
19
|
import { RcIconType } from '@components/RcIcon/types';
|
|
20
|
+
import { requireAsset } from '@shell/utils/require-asset';
|
|
19
21
|
|
|
20
22
|
export default {
|
|
21
23
|
name: 'CruCatalogRepo',
|
|
@@ -23,6 +25,7 @@ export default {
|
|
|
23
25
|
emits: ['input'],
|
|
24
26
|
|
|
25
27
|
components: {
|
|
28
|
+
AsyncButton,
|
|
26
29
|
Footer,
|
|
27
30
|
LabeledInput,
|
|
28
31
|
NameNsDescription,
|
|
@@ -64,7 +67,7 @@ export default {
|
|
|
64
67
|
{
|
|
65
68
|
id: CLUSTER_REPO_TYPES.OCI_URL,
|
|
66
69
|
header: { title: { key: 'catalog.repo.target.oci.title' } },
|
|
67
|
-
image: { src:
|
|
70
|
+
image: { src: requireAsset('@shell/assets/images/providers/oci-open-containers.svg'), alt: { key: 'catalog.repo.target.oci.title' } },
|
|
68
71
|
content: { key: 'catalog.repo.target.oci.description' },
|
|
69
72
|
},
|
|
70
73
|
];
|
|
@@ -74,7 +77,7 @@ export default {
|
|
|
74
77
|
clusterRepoTargets.push({
|
|
75
78
|
id: CLUSTER_REPO_TYPES.SUSE_APP_COLLECTION,
|
|
76
79
|
header: { title: { key: 'catalog.repo.target.suseAppCollection.title' } },
|
|
77
|
-
image: { src:
|
|
80
|
+
image: { src: requireAsset('@shell/assets/images/content/suse.svg'), alt: { key: 'catalog.repo.target.suseAppCollection.title' } },
|
|
78
81
|
content: { key: 'catalog.repo.target.suseAppCollection.description' },
|
|
79
82
|
});
|
|
80
83
|
}
|
|
@@ -90,6 +93,7 @@ export default {
|
|
|
90
93
|
ociMaxRetries: this.value.spec.exponentialBackOffValues?.maxRetries,
|
|
91
94
|
getVersionData,
|
|
92
95
|
isView: this.mode === _VIEW,
|
|
96
|
+
isCreate: this.mode === _CREATE,
|
|
93
97
|
clusterRepoTargets,
|
|
94
98
|
previousName: '',
|
|
95
99
|
previousDescription: '',
|
|
@@ -450,7 +454,17 @@ export default {
|
|
|
450
454
|
:errors="errors"
|
|
451
455
|
@save="save"
|
|
452
456
|
@done="done"
|
|
453
|
-
|
|
457
|
+
>
|
|
458
|
+
<template
|
|
459
|
+
v-if="isCreate"
|
|
460
|
+
#save
|
|
461
|
+
>
|
|
462
|
+
<AsyncButton
|
|
463
|
+
:action-label="t('catalog.repo.add')"
|
|
464
|
+
@click="save"
|
|
465
|
+
/>
|
|
466
|
+
</template>
|
|
467
|
+
</Footer>
|
|
454
468
|
</form>
|
|
455
469
|
</template>
|
|
456
470
|
|
package/edit/cloudcredential.vue
CHANGED
|
@@ -3,6 +3,7 @@ import { SECRET_TYPES as TYPES } from '@shell/config/secret';
|
|
|
3
3
|
import { MANAGEMENT, NORMAN, SCHEMA, DEFAULT_WORKSPACE } from '@shell/config/types';
|
|
4
4
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
|
5
5
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
|
6
|
+
import { requireAsset } from '@shell/utils/require-asset';
|
|
6
7
|
import CruResource from '@shell/components/CruResource';
|
|
7
8
|
import { _CREATE, _EDIT } from '@shell/config/query-params';
|
|
8
9
|
import Loading from '@shell/components/Loading';
|
|
@@ -177,7 +178,7 @@ export default {
|
|
|
177
178
|
|
|
178
179
|
if (!bannerImage) {
|
|
179
180
|
try {
|
|
180
|
-
bannerImage =
|
|
181
|
+
bannerImage = requireAsset(`~shell/assets/images/providers/${ id }.svg`);
|
|
181
182
|
} catch (e) {
|
|
182
183
|
bannerImage = null;
|
|
183
184
|
bannerAbbrv = this.initialDisplayFor(id);
|
|
@@ -16,8 +16,6 @@ export default {
|
|
|
16
16
|
ChangePassword, GlobalRoleBindings, CruResource, LabeledInput, Loading
|
|
17
17
|
},
|
|
18
18
|
|
|
19
|
-
emits: ['update:mode'],
|
|
20
|
-
|
|
21
19
|
mixins: [
|
|
22
20
|
CreateEditView
|
|
23
21
|
],
|
|
@@ -42,8 +40,7 @@ export default {
|
|
|
42
40
|
password: false,
|
|
43
41
|
roles: !showGlobalRoles,
|
|
44
42
|
rolesChanged: false,
|
|
45
|
-
}
|
|
46
|
-
watchOverride: false,
|
|
43
|
+
}
|
|
47
44
|
};
|
|
48
45
|
},
|
|
49
46
|
|
|
@@ -110,10 +107,20 @@ export default {
|
|
|
110
107
|
if (this.isCreate) {
|
|
111
108
|
const user = await this.createUser();
|
|
112
109
|
|
|
113
|
-
await this.
|
|
110
|
+
await this.createSecret(user);
|
|
111
|
+
await this.updateRoles(user);
|
|
112
|
+
|
|
113
|
+
// Show success notification only after ALL operations complete
|
|
114
|
+
// this is a "clone" of steve-class "processSaveResponse" toast/growl
|
|
115
|
+
this.$store.dispatch('growl/success', {
|
|
116
|
+
title: this.t('generic.autogeneratedCreated.title', { resource: user.kind }),
|
|
117
|
+
message: this.t('generic.autogeneratedCreated.message', { id: user.username, resource: user.kind }),
|
|
118
|
+
timeout: 3000
|
|
119
|
+
}, { root: true });
|
|
114
120
|
} else {
|
|
115
|
-
await this.editUser();
|
|
116
|
-
|
|
121
|
+
const user = await this.editUser();
|
|
122
|
+
|
|
123
|
+
await this.updateRoles(user);
|
|
117
124
|
}
|
|
118
125
|
|
|
119
126
|
this.$router.replace({ name: this.doneRoute });
|
|
@@ -139,21 +146,10 @@ export default {
|
|
|
139
146
|
username: this.form.username
|
|
140
147
|
});
|
|
141
148
|
|
|
142
|
-
const userSaved = await user.save(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const secret = await this.$store.dispatch('management/create', {
|
|
147
|
-
type: SECRET,
|
|
148
|
-
metadata: {
|
|
149
|
-
namespace: 'cattle-local-user-passwords',
|
|
150
|
-
name: userSaved.id
|
|
151
|
-
},
|
|
152
|
-
data: { password: base64Encode(this.form.password.password) }
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
await secret.save();
|
|
156
|
-
}
|
|
149
|
+
const userSaved = await user.save({
|
|
150
|
+
// Don't show a success toast until the secret and GRB are also created
|
|
151
|
+
suppressSuccessToast: true,
|
|
152
|
+
});
|
|
157
153
|
|
|
158
154
|
return userSaved;
|
|
159
155
|
},
|
|
@@ -178,27 +174,57 @@ export default {
|
|
|
178
174
|
await wait(5000);
|
|
179
175
|
}
|
|
180
176
|
|
|
181
|
-
this.value.save();
|
|
177
|
+
const user = this.value.save();
|
|
178
|
+
|
|
179
|
+
return user;
|
|
182
180
|
},
|
|
183
181
|
|
|
184
|
-
async
|
|
182
|
+
async createSecret(user) {
|
|
183
|
+
if (this.form.password.password) {
|
|
184
|
+
try {
|
|
185
|
+
// create secret to hold user password
|
|
186
|
+
const secret = await this.$store.dispatch('management/create', {
|
|
187
|
+
type: SECRET,
|
|
188
|
+
metadata: {
|
|
189
|
+
namespace: 'cattle-local-user-passwords',
|
|
190
|
+
name: user.id
|
|
191
|
+
},
|
|
192
|
+
data: { password: base64Encode(this.form.password.password) }
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
await secret.save();
|
|
196
|
+
} catch (err) {
|
|
197
|
+
if (this.isCreate) {
|
|
198
|
+
try {
|
|
199
|
+
// If secret creation fails, attempt to clean up the user to maintain consistency
|
|
200
|
+
await user.remove();
|
|
201
|
+
} catch (cleanupErr) {
|
|
202
|
+
// Log cleanup error but prioritize original error for user feedback
|
|
203
|
+
console.error('Failed to clean up user after secret creation failure:', cleanupErr); // eslint-disable-line no-console
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
throw err;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
async updateRoles(user) {
|
|
185
213
|
if (!this.$refs.grb) {
|
|
186
214
|
return;
|
|
187
215
|
}
|
|
188
216
|
|
|
189
217
|
try {
|
|
190
|
-
await this.$refs.grb.save(
|
|
218
|
+
await this.$refs.grb.save(user.id);
|
|
191
219
|
} catch (err) {
|
|
192
220
|
if (this.isCreate) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
);
|
|
221
|
+
try {
|
|
222
|
+
// If GRB creation fails, clean up the user to maintain consistency
|
|
223
|
+
await user.remove();
|
|
224
|
+
} catch (cleanupErr) {
|
|
225
|
+
// Log cleanup error but prioritize original error for user feedback
|
|
226
|
+
console.error('Failed to clean up user after GRB creation failure:', cleanupErr); // eslint-disable-line no-console
|
|
227
|
+
}
|
|
202
228
|
}
|
|
203
229
|
throw err;
|
|
204
230
|
}
|
|
@@ -274,7 +300,6 @@ export default {
|
|
|
274
300
|
:user-id="value.id || liveValue.id"
|
|
275
301
|
:mode="mode"
|
|
276
302
|
:real-mode="realMode"
|
|
277
|
-
:watch-override="watchOverride"
|
|
278
303
|
type="user"
|
|
279
304
|
@hasChanges="validation.rolesChanged = $event"
|
|
280
305
|
@canLogIn="validation.roles = $event"
|