@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
|
@@ -49,10 +49,6 @@ export default {
|
|
|
49
49
|
userId: {
|
|
50
50
|
type: String,
|
|
51
51
|
default: ''
|
|
52
|
-
},
|
|
53
|
-
watchOverride: {
|
|
54
|
-
type: Boolean,
|
|
55
|
-
default: true,
|
|
56
52
|
}
|
|
57
53
|
},
|
|
58
54
|
async fetch() {
|
|
@@ -142,7 +138,7 @@ export default {
|
|
|
142
138
|
this.update();
|
|
143
139
|
},
|
|
144
140
|
userId(userId, oldUserId) {
|
|
145
|
-
if (userId === oldUserId
|
|
141
|
+
if (userId === oldUserId) {
|
|
146
142
|
return;
|
|
147
143
|
}
|
|
148
144
|
this.update();
|
package/components/LazyImage.vue
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { BLANK_IMAGE } from '@shell/utils/style';
|
|
3
|
+
import genericCatalogSvg from '@shell/assets/images/generic-catalog.svg';
|
|
3
4
|
|
|
4
5
|
export default {
|
|
5
6
|
props: {
|
|
@@ -10,7 +11,7 @@ export default {
|
|
|
10
11
|
|
|
11
12
|
errorSrc: {
|
|
12
13
|
type: String,
|
|
13
|
-
default:
|
|
14
|
+
default: genericCatalogSvg,
|
|
14
15
|
},
|
|
15
16
|
|
|
16
17
|
src: {
|
|
@@ -54,9 +54,9 @@ const i18n = useI18n(store);
|
|
|
54
54
|
.scaler {
|
|
55
55
|
display: inline-flex;
|
|
56
56
|
align-items: center;
|
|
57
|
-
background-color:
|
|
57
|
+
background-color: var(--accent-btn);
|
|
58
58
|
border-radius: var(--border-radius-md);
|
|
59
|
-
border:
|
|
59
|
+
border: solid thin var(--primary);
|
|
60
60
|
overflow: hidden;
|
|
61
61
|
|
|
62
62
|
button {
|
|
@@ -77,7 +77,7 @@ const i18n = useI18n(store);
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
&:hover {
|
|
80
|
-
background-color:
|
|
80
|
+
background-color: var(--accent-btn);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
&[disabled] {
|
|
@@ -88,7 +88,7 @@ const i18n = useI18n(store);
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
.value {
|
|
91
|
-
color:
|
|
91
|
+
color: var(--body-text);
|
|
92
92
|
cursor: default;
|
|
93
93
|
padding: 4px;
|
|
94
94
|
padding-top: 5px;
|
|
@@ -403,25 +403,6 @@ export default {
|
|
|
403
403
|
// Remove id? How does subtype get in (cluster/node)
|
|
404
404
|
this.detailComponent = this.$store.getters['type-map/importDetail'](detailResource, id);
|
|
405
405
|
this.editComponent = this.$store.getters['type-map/importEdit'](editResource, id);
|
|
406
|
-
},
|
|
407
|
-
/**
|
|
408
|
-
* Sets the mode and initializes the resource components.
|
|
409
|
-
*
|
|
410
|
-
* This method sets the mode of the component and configures the resource
|
|
411
|
-
* components based on the provided user and resource.
|
|
412
|
-
*
|
|
413
|
-
* @param {Object} payload - An object containing the mode, user, and
|
|
414
|
-
* resource properties.
|
|
415
|
-
* @param {string} payload.mode - The mode to set.
|
|
416
|
-
* @param {Object} payload.user - The user object containing user-specific
|
|
417
|
-
* information.
|
|
418
|
-
* @param {string} payload.resource - The resource string to use for
|
|
419
|
-
* initialization.
|
|
420
|
-
*/
|
|
421
|
-
setMode({ mode, userId, resource }) {
|
|
422
|
-
this.mode = mode;
|
|
423
|
-
this.value.id = userId;
|
|
424
|
-
this.configureResource(userId, resource);
|
|
425
406
|
}
|
|
426
407
|
}
|
|
427
408
|
};
|
|
@@ -444,7 +425,6 @@ export default {
|
|
|
444
425
|
:class="{'flex-content': flexContent}"
|
|
445
426
|
:resource-errors="errors"
|
|
446
427
|
@update:value="$emit('input', $event)"
|
|
447
|
-
@update:mode="setMode"
|
|
448
428
|
@set-subtype="setSubtype"
|
|
449
429
|
/>
|
|
450
430
|
<div v-else>
|
|
@@ -514,7 +494,6 @@ export default {
|
|
|
514
494
|
:real-mode="realMode"
|
|
515
495
|
:class="{'flex-content': flexContent}"
|
|
516
496
|
@update:value="$emit('input', $event)"
|
|
517
|
-
@update:mode="setMode"
|
|
518
497
|
@set-subtype="setSubtype"
|
|
519
498
|
/>
|
|
520
499
|
|
|
@@ -76,6 +76,12 @@ export default {
|
|
|
76
76
|
type: Boolean,
|
|
77
77
|
default: true,
|
|
78
78
|
},
|
|
79
|
+
|
|
80
|
+
extensionParams: {
|
|
81
|
+
type: Object,
|
|
82
|
+
default: null,
|
|
83
|
+
},
|
|
84
|
+
|
|
79
85
|
/**
|
|
80
86
|
* Inherited global identifier prefix for tests
|
|
81
87
|
* Define a term based on the parent component to avoid conflicts on multiple components
|
|
@@ -64,6 +64,37 @@ describe('component: ConsumptionGauge', () => {
|
|
|
64
64
|
expect(slotTitle.text()).toBe('some-resource-name');
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
+
it('should display the default "Used" label when usedLabel is not provided', () => {
|
|
68
|
+
const wrapper = mount(ConsumptionGauge, {
|
|
69
|
+
props: {
|
|
70
|
+
resourceName: 'some-resource-name',
|
|
71
|
+
capacity: 100,
|
|
72
|
+
used: 50,
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const usedSpan = wrapper.find('.consumption-gauge .numbers span:nth-child(1)');
|
|
77
|
+
|
|
78
|
+
expect(usedSpan.exists()).toBe(true);
|
|
79
|
+
expect(usedSpan.text()).toBe('%node.detail.glance.consumptionGauge.used%');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('usedLabel should override the default "Used" label text', () => {
|
|
83
|
+
const wrapper = mount(ConsumptionGauge, {
|
|
84
|
+
props: {
|
|
85
|
+
resourceName: 'some-resource-name',
|
|
86
|
+
capacity: 100,
|
|
87
|
+
used: 50,
|
|
88
|
+
usedLabel: 'Running'
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const usedSpan = wrapper.find('.consumption-gauge .numbers span:nth-child(1)');
|
|
93
|
+
|
|
94
|
+
expect(usedSpan.exists()).toBe(true);
|
|
95
|
+
expect(usedSpan.text()).toBe('Running');
|
|
96
|
+
});
|
|
97
|
+
|
|
67
98
|
it('passing slot TITLE should render correctly', () => {
|
|
68
99
|
const colorStops = {
|
|
69
100
|
0: '--success', 30: '--warning', 70: '--error'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mount } from '@vue/test-utils';
|
|
2
2
|
import CruResource from '@shell/components/CruResource.vue';
|
|
3
|
-
import { _EDIT, _YAML } from '@shell/config/query-params';
|
|
3
|
+
import { _CREATE, _EDIT, _VIEW, _YAML } from '@shell/config/query-params';
|
|
4
4
|
import TextAreaAutoGrow from '@components/Form/TextArea/TextAreaAutoGrow.vue';
|
|
5
5
|
|
|
6
6
|
describe('component: CruResource', () => {
|
|
@@ -171,6 +171,40 @@ describe('component: CruResource', () => {
|
|
|
171
171
|
expect(event.preventDefault).toHaveBeenCalledWith();
|
|
172
172
|
});
|
|
173
173
|
|
|
174
|
+
it.each([
|
|
175
|
+
[_EDIT, true],
|
|
176
|
+
[_CREATE, true],
|
|
177
|
+
[_VIEW, false],
|
|
178
|
+
])('should render CruResourceFooter when mode is %s: %s', (mode: string, shouldRender: boolean) => {
|
|
179
|
+
const wrapper = mount(CruResource, {
|
|
180
|
+
props: {
|
|
181
|
+
canYaml: false,
|
|
182
|
+
mode,
|
|
183
|
+
resource: {}
|
|
184
|
+
},
|
|
185
|
+
global: {
|
|
186
|
+
mocks: {
|
|
187
|
+
$store: {
|
|
188
|
+
getters: {
|
|
189
|
+
currentStore: () => 'current_store',
|
|
190
|
+
'current_store/schemaFor': jest.fn(),
|
|
191
|
+
'current_store/all': jest.fn(),
|
|
192
|
+
'i18n/t': jest.fn(),
|
|
193
|
+
'i18n/exists': jest.fn(),
|
|
194
|
+
},
|
|
195
|
+
dispatch: jest.fn(),
|
|
196
|
+
},
|
|
197
|
+
$route: { query: { AS: _YAML } },
|
|
198
|
+
$router: { applyQuery: jest.fn() },
|
|
199
|
+
},
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const footer = wrapper.find('.cru-resource-footer');
|
|
204
|
+
|
|
205
|
+
expect(footer.exists()).toBe(shouldRender);
|
|
206
|
+
});
|
|
207
|
+
|
|
174
208
|
it('should not prevent default events on keypress Enter', async() => {
|
|
175
209
|
const event = { preventDefault: jest.fn() };
|
|
176
210
|
const wrapper = mount(CruResource, {
|
|
@@ -61,11 +61,6 @@ export default {
|
|
|
61
61
|
label: this.t('projectMembers.projectPermissions.ingressManage'),
|
|
62
62
|
value: false,
|
|
63
63
|
},
|
|
64
|
-
{
|
|
65
|
-
key: 'projectcatalogs-manage',
|
|
66
|
-
label: this.t('projectMembers.projectPermissions.projectcatalogsManage'),
|
|
67
|
-
value: false,
|
|
68
|
-
},
|
|
69
64
|
{
|
|
70
65
|
key: 'projectroletemplatebindings-manage',
|
|
71
66
|
label: this.t('projectMembers.projectPermissions.projectroletemplatebindingsManage'),
|
|
@@ -111,11 +106,6 @@ export default {
|
|
|
111
106
|
label: this.t('projectMembers.projectPermissions.monitoringUiView'),
|
|
112
107
|
value: false,
|
|
113
108
|
},
|
|
114
|
-
{
|
|
115
|
-
key: 'projectcatalogs-view',
|
|
116
|
-
label: this.t('projectMembers.projectPermissions.projectcatalogsView'),
|
|
117
|
-
value: false,
|
|
118
|
-
},
|
|
119
109
|
{
|
|
120
110
|
key: 'projectroletemplatebindings-view',
|
|
121
111
|
label: this.t('projectMembers.projectPermissions.projectroletemplatebindingsView'),
|
|
@@ -107,7 +107,6 @@ export interface TopLevelMenuHelper {
|
|
|
107
107
|
|
|
108
108
|
export abstract class BaseTopLevelMenuHelper {
|
|
109
109
|
protected $store: VuexStore;
|
|
110
|
-
protected hasProvCluster: boolean;
|
|
111
110
|
|
|
112
111
|
/**
|
|
113
112
|
* Filter mgmt clusters by
|
|
@@ -143,11 +142,9 @@ export abstract class BaseTopLevelMenuHelper {
|
|
|
143
142
|
$store: VuexStore,
|
|
144
143
|
}) {
|
|
145
144
|
this.$store = $store;
|
|
146
|
-
|
|
147
|
-
this.hasProvCluster = this.$store.getters[`management/schemaFor`](CAPI.RANCHER_CLUSTER);
|
|
148
145
|
}
|
|
149
146
|
|
|
150
|
-
protected convertToCluster(mgmtCluster: MgmtCluster, provCluster
|
|
147
|
+
protected convertToCluster(mgmtCluster: MgmtCluster, provCluster?: ProvCluster): TopLevelMenuCluster {
|
|
151
148
|
return {
|
|
152
149
|
id: mgmtCluster.id,
|
|
153
150
|
label: mgmtCluster.nameDisplay,
|
|
@@ -173,7 +170,6 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
|
|
|
173
170
|
|
|
174
171
|
private clustersPinnedWrapper: PaginationWrapper<any>;
|
|
175
172
|
private clustersOthersWrapper: PaginationWrapper<any>;
|
|
176
|
-
private provClusterWrapper: PaginationWrapper<any>;
|
|
177
173
|
|
|
178
174
|
private clusterCount = 0;
|
|
179
175
|
|
|
@@ -223,43 +219,10 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
|
|
|
223
219
|
},
|
|
224
220
|
formatResponse: { classify: true },
|
|
225
221
|
});
|
|
226
|
-
// Fetch all prov clusters for the mgmt clusters we have
|
|
227
|
-
this.provClusterWrapper = new PaginationWrapper({
|
|
228
|
-
$store,
|
|
229
|
-
id: 'top-level-menu-prov-clusters',
|
|
230
|
-
onChange: async({ forceWatch, revision }) => {
|
|
231
|
-
if (!this.args) {
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
try {
|
|
235
|
-
await this.update({
|
|
236
|
-
...this.args,
|
|
237
|
-
forceWatch,
|
|
238
|
-
provClusterRevision: revision,
|
|
239
|
-
});
|
|
240
|
-
} catch {
|
|
241
|
-
// Failures should be logged lower down, not much we can do here except catch to prevent whole ui page warnings in dev mode
|
|
242
|
-
}
|
|
243
|
-
},
|
|
244
|
-
enabledFor: {
|
|
245
|
-
store: STORE.MANAGEMENT,
|
|
246
|
-
resource: {
|
|
247
|
-
id: CAPI.RANCHER_CLUSTER,
|
|
248
|
-
context: 'side-bar',
|
|
249
|
-
}
|
|
250
|
-
},
|
|
251
|
-
formatResponse: { classify: true }
|
|
252
|
-
});
|
|
253
222
|
}
|
|
254
223
|
|
|
255
224
|
// ---------- requests ----------
|
|
256
225
|
async update(args: UpdateArgs) {
|
|
257
|
-
if (!this.hasProvCluster) {
|
|
258
|
-
// We're filtering out mgmt clusters without prov clusters, so if the user can't see any prov clusters at all
|
|
259
|
-
// exit early
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
226
|
this.args = args;
|
|
264
227
|
const promises = {
|
|
265
228
|
pinned: this.updatePinned(args),
|
|
@@ -271,22 +234,11 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
|
|
|
271
234
|
notPinned: MgmtCluster[]
|
|
272
235
|
} = await allHash(promises) as any;
|
|
273
236
|
|
|
274
|
-
const provClusters = await this.updateProvCluster(res.notPinned, res.pinned, args);
|
|
275
|
-
const provClustersByMgmtId = provClusters.reduce((res: { [mgmtId: string]: ProvCluster}, provCluster: ProvCluster) => {
|
|
276
|
-
if (provCluster.mgmtClusterId) {
|
|
277
|
-
res[provCluster.mgmtClusterId] = provCluster;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
return res;
|
|
281
|
-
}, {} as { [mgmtId: string]: ProvCluster});
|
|
282
|
-
|
|
283
237
|
// Filter out mgmt clusters that don't have matching prov cluster and convert remaining to required format
|
|
284
238
|
const _clustersNotPinned = res.notPinned
|
|
285
|
-
.
|
|
286
|
-
.map((mgmtCluster) => this.convertToCluster(mgmtCluster, provClustersByMgmtId[mgmtCluster.id]));
|
|
239
|
+
.map((mgmtCluster) => this.convertToCluster(mgmtCluster));
|
|
287
240
|
const _clustersPinned = res.pinned
|
|
288
|
-
.
|
|
289
|
-
.map((mgmtCluster) => this.convertToCluster(mgmtCluster, provClustersByMgmtId[mgmtCluster.id]));
|
|
241
|
+
.map((mgmtCluster) => this.convertToCluster(mgmtCluster));
|
|
290
242
|
|
|
291
243
|
this.clustersPinned.length = 0;
|
|
292
244
|
this.clustersOthers.length = 0;
|
|
@@ -298,7 +250,6 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
|
|
|
298
250
|
async destroy() {
|
|
299
251
|
this.clustersPinnedWrapper.onDestroy();
|
|
300
252
|
this.clustersOthersWrapper.onDestroy();
|
|
301
|
-
this.provClusterWrapper.onDestroy();
|
|
302
253
|
}
|
|
303
254
|
|
|
304
255
|
/**
|
|
@@ -445,41 +396,21 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
|
|
|
445
396
|
console.warn('Unable to set saved count for clusters', err); // eslint-disable-line no-console
|
|
446
397
|
}
|
|
447
398
|
}
|
|
448
|
-
|
|
449
|
-
/**
|
|
450
|
-
* Find all provisioning clusters associated with the displayed mgmt clusters
|
|
451
|
-
*/
|
|
452
|
-
private async updateProvCluster(notPinned: MgmtCluster[], pinned: MgmtCluster[], args: UpdateArgs): Promise<ProvCluster[]> {
|
|
453
|
-
return this.provClusterWrapper.request({
|
|
454
|
-
forceWatch: args.forceWatch,
|
|
455
|
-
pagination: {
|
|
456
|
-
filters: [
|
|
457
|
-
PaginationParamFilter.createMultipleFields(
|
|
458
|
-
[...notPinned, ...pinned]
|
|
459
|
-
.map((mgmtCluster) => ({
|
|
460
|
-
field: 'status.clusterName', value: mgmtCluster.id, equals: true, exact: true
|
|
461
|
-
}))
|
|
462
|
-
)
|
|
463
|
-
],
|
|
464
|
-
page: 1,
|
|
465
|
-
sort: [],
|
|
466
|
-
projectsOrNamespaces: []
|
|
467
|
-
},
|
|
468
|
-
revision: args.provClusterRevision
|
|
469
|
-
})
|
|
470
|
-
.then((r) => r.data);
|
|
471
|
-
}
|
|
472
399
|
}
|
|
473
400
|
|
|
474
401
|
/**
|
|
475
402
|
* Helper designed to supply non-paginated results for the top level menu cluster resources
|
|
476
403
|
*/
|
|
477
404
|
export class TopLevelMenuHelperLegacy extends BaseTopLevelMenuHelper implements TopLevelMenuHelper {
|
|
405
|
+
protected hasProvCluster: boolean;
|
|
406
|
+
|
|
478
407
|
constructor({ $store }: {
|
|
479
408
|
$store: VuexStore,
|
|
480
409
|
}) {
|
|
481
410
|
super({ $store });
|
|
482
411
|
|
|
412
|
+
this.hasProvCluster = this.$store.getters[`management/schemaFor`](CAPI.RANCHER_CLUSTER);
|
|
413
|
+
|
|
483
414
|
if (this.hasProvCluster) {
|
|
484
415
|
$store.dispatch('management/findAll', { type: CAPI.RANCHER_CLUSTER });
|
|
485
416
|
}
|
|
@@ -664,9 +595,6 @@ class TopLevelMenuHelperService {
|
|
|
664
595
|
const canPagination = $store.getters[`management/paginationEnabled`]({
|
|
665
596
|
id: MANAGEMENT.CLUSTER,
|
|
666
597
|
context: 'side-bar',
|
|
667
|
-
}) && $store.getters[`management/paginationEnabled`]({
|
|
668
|
-
id: CAPI.RANCHER_CLUSTER,
|
|
669
|
-
context: 'side-bar',
|
|
670
598
|
});
|
|
671
599
|
|
|
672
600
|
this._helper = canPagination ? new TopLevelMenuHelperPagination({ $store }) : new TopLevelMenuHelperLegacy({ $store });
|
|
@@ -102,7 +102,7 @@ describe('topLevelMenu.helper', () => {
|
|
|
102
102
|
it('should initialize PaginationWrappers', () => {
|
|
103
103
|
mockStore.getters['management/schemaFor'].mockReturnValue(true);
|
|
104
104
|
new TopLevelMenuHelperPagination({ $store: mockStore });
|
|
105
|
-
expect(PaginationWrapper).toHaveBeenCalledTimes(
|
|
105
|
+
expect(PaginationWrapper).toHaveBeenCalledTimes(2);
|
|
106
106
|
});
|
|
107
107
|
|
|
108
108
|
it('should update clusters correctly', async() => {
|
|
@@ -113,19 +113,13 @@ describe('topLevelMenu.helper', () => {
|
|
|
113
113
|
const mgmtOthers = [{
|
|
114
114
|
id: 'c2', nameDisplay: 'Other', isReady: true, pinned: false, pin: jest.fn(), unpin: jest.fn()
|
|
115
115
|
}];
|
|
116
|
-
const provClusters = [
|
|
117
|
-
{ mgmtClusterId: 'c1' },
|
|
118
|
-
{ mgmtClusterId: 'c2' }
|
|
119
|
-
];
|
|
120
116
|
|
|
121
117
|
const mockRequestPinned = jest.fn().mockResolvedValue({ data: mgmtPinned });
|
|
122
118
|
const mockRequestOthers = jest.fn().mockResolvedValue({ data: mgmtOthers });
|
|
123
|
-
const mockRequestProv = jest.fn().mockResolvedValue({ data: provClusters });
|
|
124
119
|
|
|
125
120
|
(PaginationWrapper as unknown as jest.Mock)
|
|
126
121
|
.mockImplementationOnce(() => ({ request: mockRequestPinned, onDestroy: jest.fn() }))
|
|
127
|
-
.mockImplementationOnce(() => ({ request: mockRequestOthers, onDestroy: jest.fn() }))
|
|
128
|
-
.mockImplementationOnce(() => ({ request: mockRequestProv, onDestroy: jest.fn() }));
|
|
122
|
+
.mockImplementationOnce(() => ({ request: mockRequestOthers, onDestroy: jest.fn() }));
|
|
129
123
|
|
|
130
124
|
const helper = new TopLevelMenuHelperPagination({ $store: mockStore });
|
|
131
125
|
|
|
@@ -169,57 +163,12 @@ describe('topLevelMenu.helper', () => {
|
|
|
169
163
|
},
|
|
170
164
|
revision: undefined
|
|
171
165
|
});
|
|
172
|
-
expect(mockRequestProv).toHaveBeenCalledWith({
|
|
173
|
-
forceWatch: undefined,
|
|
174
|
-
pagination: {
|
|
175
|
-
filters: [{
|
|
176
|
-
equals: true,
|
|
177
|
-
fields: [{
|
|
178
|
-
equals: true, exact: true, field: 'status.clusterName', value: mgmtOthers[0].id
|
|
179
|
-
}, {
|
|
180
|
-
equals: true, exact: true, field: 'status.clusterName', value: mgmtPinned[0].id
|
|
181
|
-
}],
|
|
182
|
-
param: 'filter'
|
|
183
|
-
}],
|
|
184
|
-
page: 1,
|
|
185
|
-
projectsOrNamespaces: [],
|
|
186
|
-
sort: []
|
|
187
|
-
},
|
|
188
|
-
revision: undefined
|
|
189
|
-
});
|
|
190
166
|
|
|
191
167
|
expect(helper.clustersPinned).toHaveLength(1);
|
|
192
168
|
expect(helper.clustersPinned[0].id).toBe('c1');
|
|
193
169
|
expect(helper.clustersOthers).toHaveLength(1);
|
|
194
170
|
expect(helper.clustersOthers[0].id).toBe('c2');
|
|
195
171
|
});
|
|
196
|
-
|
|
197
|
-
it('should filter out mgmt clusters without matching prov clusters', async() => {
|
|
198
|
-
mockStore.getters['management/schemaFor'].mockReturnValue(true);
|
|
199
|
-
const mgmtOthers = [{
|
|
200
|
-
id: 'c2', nameDisplay: 'Other', isReady: true, pinned: false, pin: jest.fn(), unpin: jest.fn()
|
|
201
|
-
}];
|
|
202
|
-
// No prov cluster for c2
|
|
203
|
-
const provClusters: any[] = [];
|
|
204
|
-
|
|
205
|
-
const mockRequestPinned = jest.fn().mockResolvedValue({ data: [] });
|
|
206
|
-
const mockRequestOthers = jest.fn().mockResolvedValue({ data: mgmtOthers });
|
|
207
|
-
const mockRequestProv = jest.fn().mockResolvedValue({ data: provClusters });
|
|
208
|
-
|
|
209
|
-
(PaginationWrapper as unknown as jest.Mock)
|
|
210
|
-
.mockImplementationOnce(() => ({ request: mockRequestPinned, onDestroy: jest.fn() }))
|
|
211
|
-
.mockImplementationOnce(() => ({ request: mockRequestOthers, onDestroy: jest.fn() }))
|
|
212
|
-
.mockImplementationOnce(() => ({ request: mockRequestProv, onDestroy: jest.fn() }));
|
|
213
|
-
|
|
214
|
-
const helper = new TopLevelMenuHelperPagination({ $store: mockStore });
|
|
215
|
-
|
|
216
|
-
await helper.update({
|
|
217
|
-
searchTerm: '',
|
|
218
|
-
pinnedIds: [],
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
expect(helper.clustersOthers).toHaveLength(0);
|
|
222
|
-
});
|
|
223
172
|
});
|
|
224
173
|
|
|
225
174
|
describe('class: TopLevelMenuHelperService', () => {
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { useIsNewDetailPageEnabled } from '@shell/composables/useIsNewDetailPageEnabled';
|
|
2
|
+
|
|
3
|
+
const mockStore: any = { getters: {} };
|
|
4
|
+
const mockRoute: any = { query: {} };
|
|
5
|
+
|
|
6
|
+
jest.mock('vuex', () => ({ useStore: () => mockStore }));
|
|
7
|
+
jest.mock('vue-router', () => ({ useRoute: () => mockRoute }));
|
|
8
|
+
|
|
9
|
+
const mockGetVersionInfo = jest.fn(() => ({ fullVersion: '2.12.0' }));
|
|
10
|
+
|
|
11
|
+
jest.mock('@shell/utils/version', () => ({ getVersionInfo: (...args: any[]) => mockGetVersionInfo(...args) }));
|
|
12
|
+
|
|
13
|
+
describe('useIsNewDetailPageEnabled', () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
mockRoute.query = {};
|
|
16
|
+
mockGetVersionInfo.mockReturnValue({ fullVersion: '2.12.0' });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('version gating', () => {
|
|
20
|
+
it('should return false when version is below 2.12.0', () => {
|
|
21
|
+
mockGetVersionInfo.mockReturnValue({ fullVersion: '2.11.9' });
|
|
22
|
+
const result = useIsNewDetailPageEnabled();
|
|
23
|
+
|
|
24
|
+
expect(result.value).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should return false when version is 2.10.0', () => {
|
|
28
|
+
mockGetVersionInfo.mockReturnValue({ fullVersion: '2.10.0' });
|
|
29
|
+
const result = useIsNewDetailPageEnabled();
|
|
30
|
+
|
|
31
|
+
expect(result.value).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should return true when version is exactly 2.12.0 and no legacy query', () => {
|
|
35
|
+
mockGetVersionInfo.mockReturnValue({ fullVersion: '2.12.0' });
|
|
36
|
+
const result = useIsNewDetailPageEnabled();
|
|
37
|
+
|
|
38
|
+
expect(result.value).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should return true when version is above 2.12.0', () => {
|
|
42
|
+
mockGetVersionInfo.mockReturnValue({ fullVersion: '2.13.0' });
|
|
43
|
+
const result = useIsNewDetailPageEnabled();
|
|
44
|
+
|
|
45
|
+
expect(result.value).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should return false when version is undefined', () => {
|
|
49
|
+
mockGetVersionInfo.mockReturnValue({ fullVersion: undefined });
|
|
50
|
+
const result = useIsNewDetailPageEnabled();
|
|
51
|
+
|
|
52
|
+
expect(result.value).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should return false when version is null', () => {
|
|
56
|
+
mockGetVersionInfo.mockReturnValue({ fullVersion: null });
|
|
57
|
+
const result = useIsNewDetailPageEnabled();
|
|
58
|
+
|
|
59
|
+
expect(result.value).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should handle pre-release version strings', () => {
|
|
63
|
+
mockGetVersionInfo.mockReturnValue({ fullVersion: 'v2.12.1-rc1' });
|
|
64
|
+
const result = useIsNewDetailPageEnabled();
|
|
65
|
+
|
|
66
|
+
expect(result.value).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('legacy query param (with version >= 2.12.0)', () => {
|
|
71
|
+
it('should return true when no legacy query param is present', () => {
|
|
72
|
+
const result = useIsNewDetailPageEnabled();
|
|
73
|
+
|
|
74
|
+
expect(result.value).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should return false when legacy query param is "true"', () => {
|
|
78
|
+
mockRoute.query = { legacy: 'true' };
|
|
79
|
+
const result = useIsNewDetailPageEnabled();
|
|
80
|
+
|
|
81
|
+
expect(result.value).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should return true when legacy query param is "false"', () => {
|
|
85
|
+
mockRoute.query = { legacy: 'false' };
|
|
86
|
+
const result = useIsNewDetailPageEnabled();
|
|
87
|
+
|
|
88
|
+
expect(result.value).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should return true when legacy query param has an unexpected value', () => {
|
|
92
|
+
mockRoute.query = { legacy: 'something' };
|
|
93
|
+
const result = useIsNewDetailPageEnabled();
|
|
94
|
+
|
|
95
|
+
expect(result.value).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { useRoute } from 'vue-router';
|
|
2
2
|
import { LEGACY } from '@shell/config/query-params';
|
|
3
3
|
import { computed } from 'vue';
|
|
4
|
+
import { getVersionInfo } from '@shell/utils/version';
|
|
5
|
+
import semver from 'semver';
|
|
6
|
+
import { useStore } from 'vuex';
|
|
4
7
|
|
|
5
8
|
const enabledByDefault = true;
|
|
6
9
|
|
|
@@ -8,6 +11,15 @@ export const useIsNewDetailPageEnabled = () => {
|
|
|
8
11
|
const route = useRoute();
|
|
9
12
|
|
|
10
13
|
return computed(() => {
|
|
14
|
+
const store = useStore();
|
|
15
|
+
const { fullVersion } = getVersionInfo(store);
|
|
16
|
+
|
|
17
|
+
const coerced = semver.coerce(fullVersion) || { version: '0.0.0' };
|
|
18
|
+
|
|
19
|
+
if (!semver.gte(coerced.version, '2.12.0')) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
11
23
|
if (enabledByDefault) {
|
|
12
24
|
return route?.query?.[LEGACY] !== 'true';
|
|
13
25
|
}
|
package/config/private-label.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SETTING } from './settings';
|
|
2
2
|
import { CURRENT_RANCHER_VERSION } from './version';
|
|
3
|
+
import { requireAsset } from '@shell/utils/require-asset';
|
|
3
4
|
|
|
4
5
|
export const ANY = 0;
|
|
5
6
|
export const STANDARD = 1;
|
|
@@ -78,7 +79,7 @@ export function setTitle() {
|
|
|
78
79
|
const v = getVendor();
|
|
79
80
|
|
|
80
81
|
if (v === 'Harvester') {
|
|
81
|
-
const ico =
|
|
82
|
+
const ico = requireAsset(`~shell/assets/images/pl/harvester.png`);
|
|
82
83
|
|
|
83
84
|
document.title = 'Harvester';
|
|
84
85
|
const link = document.createElement('link');
|
package/config/product/apps.js
CHANGED
|
@@ -53,6 +53,7 @@ export function init(store) {
|
|
|
53
53
|
|
|
54
54
|
configureType(CATALOG.APP, { isCreatable: false, isEditable: false });
|
|
55
55
|
configureType(CATALOG.OPERATION, { isCreatable: false, isEditable: false });
|
|
56
|
+
configureType(CATALOG.CLUSTER_REPO, { listCreateButtonLabelKey: 'catalog.repo.add' });
|
|
56
57
|
|
|
57
58
|
const repoType = {
|
|
58
59
|
name: 'type',
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
USER_ID, USERNAME, USER_DISPLAY_NAME, USER_PROVIDER, USER_LAST_LOGIN, USER_DISABLED_IN, USER_DELETED_IN, WORKLOAD_ENDPOINTS, STORAGE_CLASS_DEFAULT,
|
|
20
20
|
STORAGE_CLASS_PROVISIONER, PERSISTENT_VOLUME_SOURCE,
|
|
21
21
|
HPA_REFERENCE, MIN_REPLICA, MAX_REPLICA, CURRENT_REPLICA,
|
|
22
|
-
DESCRIPTION, SUB_TYPE, PERSISTENT_VOLUME_CLAIM, RECLAIM_POLICY, PV_REASON, WORKLOAD_HEALTH_SCALE, POD_RESTARTS,
|
|
22
|
+
ACCESS_KEY, DESCRIPTION, EXPIRES, EXPIRY_STATE, LAST_USED, SUB_TYPE, AGE_NORMAN, SCOPE_NORMAN, PERSISTENT_VOLUME_CLAIM, RECLAIM_POLICY, PV_REASON, WORKLOAD_HEALTH_SCALE, POD_RESTARTS,
|
|
23
23
|
DURATION, MESSAGE, REASON, EVENT_TYPE, OBJECT, ROLE, ROLES, VERSION, INTERNAL_EXTERNAL_IP, KUBE_NODE_OS, CPU, RAM, SECRET_DATA,
|
|
24
24
|
EVENT_LAST_SEEN_TIME,
|
|
25
25
|
EVENT_FIRST_SEEN_TIME,
|
|
@@ -546,6 +546,16 @@ export function init(store) {
|
|
|
546
546
|
AGE
|
|
547
547
|
]);
|
|
548
548
|
|
|
549
|
+
headers(NORMAN.TOKEN, [
|
|
550
|
+
EXPIRY_STATE,
|
|
551
|
+
ACCESS_KEY,
|
|
552
|
+
DESCRIPTION,
|
|
553
|
+
SCOPE_NORMAN,
|
|
554
|
+
LAST_USED,
|
|
555
|
+
EXPIRES,
|
|
556
|
+
AGE_NORMAN
|
|
557
|
+
]);
|
|
558
|
+
|
|
549
559
|
virtualType({
|
|
550
560
|
label: store.getters['i18n/t']('clusterIndexPage.header'),
|
|
551
561
|
group: 'Root',
|
package/config/table-headers.js
CHANGED
|
@@ -1033,15 +1033,6 @@ export const SCOPE_NORMAN = {
|
|
|
1033
1033
|
sort: ['clusterId'],
|
|
1034
1034
|
};
|
|
1035
1035
|
|
|
1036
|
-
export const NORMAN_KEY_DEPRECATION = {
|
|
1037
|
-
name: 'isNormanKeyDeprecated',
|
|
1038
|
-
labelKey: 'tableHeaders.isLegacy',
|
|
1039
|
-
value: (row) => row.isDeprecated ? 'True' : undefined,
|
|
1040
|
-
sort: 'isDeprecated',
|
|
1041
|
-
align: 'left',
|
|
1042
|
-
dashIfEmpty: true,
|
|
1043
|
-
};
|
|
1044
|
-
|
|
1045
1036
|
export const EXPIRES = {
|
|
1046
1037
|
name: 'expires',
|
|
1047
1038
|
value: 'expiresAt',
|
package/config/types.js
CHANGED
|
@@ -269,7 +269,6 @@ export const EXT = {
|
|
|
269
269
|
GROUP_MEMBERSHIP_REFRESH_REQUESTS: 'ext.cattle.io.groupmembershiprefreshrequest',
|
|
270
270
|
PASSWORD_CHANGE_REQUESTS: 'ext.cattle.io.passwordchangerequest',
|
|
271
271
|
KUBECONFIG: 'ext.cattle.io.kubeconfig',
|
|
272
|
-
TOKEN: 'ext.cattle.io.token',
|
|
273
272
|
};
|
|
274
273
|
|
|
275
274
|
export const CAPI = {
|