@rancher/shell 3.0.9-rc.3 → 3.0.9-rc.4
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/translations/en-us.yaml +14 -2
- package/components/formatter/KubeconfigClusters.vue +74 -0
- package/components/formatter/__tests__/KubeconfigClusters.test.ts +125 -0
- package/config/product/manager.js +29 -2
- package/config/router/routes.js +4 -1
- package/edit/provisioning.cattle.io.cluster/defaults.ts +1 -0
- package/edit/provisioning.cattle.io.cluster/rke2.vue +2 -1
- package/edit/provisioning.cattle.io.cluster/tabs/etcd/S3Config.vue +57 -2
- package/edit/provisioning.cattle.io.cluster/tabs/etcd/__tests__/S3Config.test.ts +109 -0
- package/edit/provisioning.cattle.io.cluster/tabs/etcd/index.vue +1 -0
- package/list/ext.cattle.io.kubeconfig.vue +118 -0
- package/mixins/__tests__/chart.test.ts +147 -0
- package/mixins/chart.js +10 -8
- package/models/__tests__/ext.cattle.io.kubeconfig.test.ts +364 -0
- package/models/__tests__/secret.test.ts +55 -0
- package/models/ext.cattle.io.kubeconfig.ts +97 -0
- package/models/secret.js +1 -1
- package/package.json +2 -2
- package/pages/about.vue +3 -2
- package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +38 -14
- package/pages/c/_cluster/apps/charts/index.vue +1 -0
- package/rancher-components/RcItemCard/RcItemCard.test.ts +4 -2
- package/rancher-components/RcItemCard/RcItemCard.vue +27 -10
- package/types/shell/index.d.ts +14 -1
- package/utils/__tests__/chart.test.ts +96 -0
- package/utils/__tests__/version.test.ts +1 -19
- package/utils/chart.js +64 -0
- package/utils/version.js +5 -17
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { CAPI, MANAGEMENT } from '@shell/config/types';
|
|
2
|
+
import SteveModel from '@shell/plugins/steve/steve-class';
|
|
3
|
+
import { Location } from 'vue-router';
|
|
4
|
+
|
|
5
|
+
interface ReferencedCluster {
|
|
6
|
+
label: string;
|
|
7
|
+
location: Location | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default class Kubeconfig extends SteveModel {
|
|
11
|
+
declare spec: {
|
|
12
|
+
clusters?: string[];
|
|
13
|
+
ttl?: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
declare metadata: {
|
|
17
|
+
name?: string;
|
|
18
|
+
creationTimestamp?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
get _availableActions(): object[] {
|
|
22
|
+
const out = super._availableActions;
|
|
23
|
+
|
|
24
|
+
// Remove element at index 1 (the first divider), the actions that don't make sense.
|
|
25
|
+
return out.filter((action: { action: string }, index: number) => index !== 1 && !['goToEdit', 'goToEditYaml', 'cloneYaml', 'download'].includes(action.action));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Calculates the expiry timestamp from creationTimestamp + ttl.
|
|
30
|
+
* Returns an ISO date string for use with LiveDate formatter.
|
|
31
|
+
*/
|
|
32
|
+
get expiresAt(): string | null {
|
|
33
|
+
const ttlSeconds = this.spec?.ttl;
|
|
34
|
+
const creationTimestamp = this.metadata?.creationTimestamp;
|
|
35
|
+
|
|
36
|
+
if (!ttlSeconds || !creationTimestamp) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const createdAt = new Date(creationTimestamp);
|
|
41
|
+
const expiresAt = new Date(createdAt.getTime() + (ttlSeconds * 1000));
|
|
42
|
+
|
|
43
|
+
return expiresAt.toISOString();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Returns cluster information for display and linking.
|
|
48
|
+
* Each object contains {label, location} where location is null if cluster doesn't exist.
|
|
49
|
+
*/
|
|
50
|
+
get referencedClusters(): ReferencedCluster[] {
|
|
51
|
+
const clusterIds = this.spec?.clusters || [];
|
|
52
|
+
const provClusters = this.$rootGetters['management/all'](CAPI.RANCHER_CLUSTER) || [];
|
|
53
|
+
const mgmtClusters = this.$rootGetters['management/all'](MANAGEMENT.CLUSTER) || [];
|
|
54
|
+
|
|
55
|
+
return clusterIds.map((id: string) => {
|
|
56
|
+
const provCluster = provClusters.find((c: any) => c.mgmt?.id === id || c.status?.clusterName === id);
|
|
57
|
+
const mgmtCluster = mgmtClusters.find((c: any) => c.id === id);
|
|
58
|
+
const cluster = provCluster || mgmtCluster;
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
label: cluster?.nameDisplay || this.t('"ext.cattle.io.kubeconfig".deleted', { name: id }),
|
|
62
|
+
location: provCluster?.detailLocation || mgmtCluster?.detailLocation || null
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Returns referenced clusters sorted: existing clusters first (by name), then deleted clusters.
|
|
69
|
+
*/
|
|
70
|
+
get sortedReferencedClusters(): ReferencedCluster[] {
|
|
71
|
+
return this.referencedClusters.slice().sort((a, b) => {
|
|
72
|
+
const aExists = a.location !== null;
|
|
73
|
+
const bExists = b.location !== null;
|
|
74
|
+
|
|
75
|
+
if (aExists && !bExists) {
|
|
76
|
+
return -1;
|
|
77
|
+
}
|
|
78
|
+
if (!aExists && bExists) {
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const aName = a.label.toLowerCase();
|
|
83
|
+
const bName = b.label.toLowerCase();
|
|
84
|
+
|
|
85
|
+
return aName.localeCompare(bName, undefined, { numeric: true });
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Returns a sortable string for the clusters column.
|
|
91
|
+
*/
|
|
92
|
+
get referencedClustersSortable(): string {
|
|
93
|
+
return this.sortedReferencedClusters
|
|
94
|
+
.map((c) => c.label.toLowerCase())
|
|
95
|
+
.join(',');
|
|
96
|
+
}
|
|
97
|
+
}
|
package/models/secret.js
CHANGED
|
@@ -573,7 +573,7 @@ export default class Secret extends SteveModel {
|
|
|
573
573
|
const id = this.id?.replace(/.*\//, '');
|
|
574
574
|
|
|
575
575
|
return {
|
|
576
|
-
name: `c-cluster-product-
|
|
576
|
+
name: `c-cluster-product-${ VIRTUAL_TYPES.PROJECT_SECRETS }-namespace-id`,
|
|
577
577
|
params: {
|
|
578
578
|
product: this.$rootGetters['productId'],
|
|
579
579
|
cluster: this.$rootGetters['clusterId'],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rancher/shell",
|
|
3
|
-
"version": "3.0.9-rc.
|
|
3
|
+
"version": "3.0.9-rc.4",
|
|
4
4
|
"description": "Rancher Dashboard Shell",
|
|
5
5
|
"repository": "https://github.com/rancher/dashboard",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"@babel/preset-typescript": "7.16.7",
|
|
40
40
|
"@novnc/novnc": "1.2.0",
|
|
41
41
|
"@popperjs/core": "2.11.8",
|
|
42
|
-
"@rancher/icons": "2.0.
|
|
42
|
+
"@rancher/icons": "2.0.55",
|
|
43
43
|
"@types/is-url": "1.2.30",
|
|
44
44
|
"@types/node": "20.10.8",
|
|
45
45
|
"@types/semver": "^7.5.8",
|
package/pages/about.vue
CHANGED
|
@@ -9,6 +9,7 @@ import { mapGetters } from 'vuex';
|
|
|
9
9
|
import TabTitle from '@shell/components/TabTitle';
|
|
10
10
|
import { PanelLocation, ExtensionPoint } from '@shell/core/types';
|
|
11
11
|
import ExtensionPanel from '@shell/components/ExtensionPanel';
|
|
12
|
+
import { getVersionInfo } from '@shell/utils/version';
|
|
12
13
|
|
|
13
14
|
export default {
|
|
14
15
|
components: {
|
|
@@ -30,7 +31,7 @@ export default {
|
|
|
30
31
|
computed: {
|
|
31
32
|
...mapGetters(['releaseNotesUrl']),
|
|
32
33
|
rancherVersion() {
|
|
33
|
-
return this
|
|
34
|
+
return getVersionInfo(this.$store).fullVersion;
|
|
34
35
|
},
|
|
35
36
|
appName() {
|
|
36
37
|
return getVendor();
|
|
@@ -124,7 +125,7 @@ export default {
|
|
|
124
125
|
>
|
|
125
126
|
{{ t("about.versions.rancher") }}
|
|
126
127
|
</a>
|
|
127
|
-
</td><td>{{ rancherVersion
|
|
128
|
+
</td><td>{{ rancherVersion }}</td>
|
|
128
129
|
</tr>
|
|
129
130
|
<tr v-if="dashboardVersion">
|
|
130
131
|
<td>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { RcItemCardAction } from '@components/RcItemCard';
|
|
3
|
+
import { RcButton } from '@components/RcButton';
|
|
3
4
|
|
|
4
5
|
interface FooterItem {
|
|
5
6
|
icon?: string;
|
|
@@ -30,26 +31,34 @@ function onClickItem(type: string, label: string) {
|
|
|
30
31
|
class="app-chart-card-footer-item"
|
|
31
32
|
data-testid="app-chart-card-footer-item"
|
|
32
33
|
>
|
|
33
|
-
<i
|
|
34
|
-
v-if="footerItem.icon"
|
|
35
|
-
v-clean-tooltip="t(footerItem.iconTooltip?.key)"
|
|
36
|
-
:class="['icon', 'app-chart-card-footer-item-icon', footerItem.icon]"
|
|
37
|
-
/>
|
|
38
34
|
<template
|
|
39
35
|
v-for="(label, j) in footerItem.labels"
|
|
40
36
|
:key="j"
|
|
41
37
|
>
|
|
42
38
|
<rc-item-card-action
|
|
43
39
|
v-if="clickable && footerItem.type"
|
|
44
|
-
|
|
45
|
-
class="app-chart-card-footer-item-text secondary-text-link"
|
|
46
|
-
data-testid="app-chart-card-footer-item-text"
|
|
47
|
-
tabindex="0"
|
|
48
|
-
:aria-label="t('catalog.charts.appChartCard.footerItem.ariaLabel')"
|
|
49
|
-
@click="onClickItem(footerItem.type, label)"
|
|
40
|
+
class="app-chart-card-footer-item-text"
|
|
50
41
|
>
|
|
51
|
-
|
|
52
|
-
|
|
42
|
+
<rc-button
|
|
43
|
+
v-clean-tooltip="footerItem.labelTooltip"
|
|
44
|
+
variant="ghost"
|
|
45
|
+
class="app-chart-card-footer-button secondary-text-link"
|
|
46
|
+
data-testid="app-chart-card-footer-item-text"
|
|
47
|
+
:aria-label="t('catalog.charts.appChartCard.footerItem.ariaLabel', { filter: label })"
|
|
48
|
+
@click="onClickItem(footerItem.type, label)"
|
|
49
|
+
>
|
|
50
|
+
<template
|
|
51
|
+
v-if="footerItem.icon"
|
|
52
|
+
#before
|
|
53
|
+
>
|
|
54
|
+
<i
|
|
55
|
+
v-clean-tooltip="t(footerItem.iconTooltip?.key)"
|
|
56
|
+
:class="['icon', 'app-chart-card-footer-item-icon', footerItem.icon]"
|
|
57
|
+
/>
|
|
58
|
+
</template>
|
|
59
|
+
{{ label }}
|
|
60
|
+
<span v-if="footerItem.labels.length > 1 && j !== footerItem.labels.length - 1">, </span>
|
|
61
|
+
</rc-button>
|
|
53
62
|
</rc-item-card-action>
|
|
54
63
|
<span
|
|
55
64
|
v-else
|
|
@@ -78,7 +87,6 @@ function onClickItem(type: string, label: string) {
|
|
|
78
87
|
margin-right: 8px;
|
|
79
88
|
|
|
80
89
|
&-text {
|
|
81
|
-
text-transform: capitalize;
|
|
82
90
|
margin-right: 8px;
|
|
83
91
|
display: -webkit-box;
|
|
84
92
|
-webkit-line-clamp: 1;
|
|
@@ -98,5 +106,21 @@ function onClickItem(type: string, label: string) {
|
|
|
98
106
|
margin-right: 8px;
|
|
99
107
|
}
|
|
100
108
|
}
|
|
109
|
+
|
|
110
|
+
&-button {
|
|
111
|
+
text-transform: capitalize;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
button.variant-ghost.app-chart-card-footer-button {
|
|
116
|
+
padding: 0;
|
|
117
|
+
gap: 0;
|
|
118
|
+
min-height: 20px;
|
|
119
|
+
|
|
120
|
+
&:focus-visible {
|
|
121
|
+
border-color: var(--primary);
|
|
122
|
+
@include focus-outline;
|
|
123
|
+
outline-offset: -2px;
|
|
124
|
+
}
|
|
101
125
|
}
|
|
102
126
|
</style>
|
|
@@ -124,7 +124,7 @@ describe('rcItemCard', () => {
|
|
|
124
124
|
}
|
|
125
125
|
});
|
|
126
126
|
|
|
127
|
-
const root = wrapper.get(`[data-testid="
|
|
127
|
+
const root = wrapper.get(`[data-testid="card-header-left"]`);
|
|
128
128
|
|
|
129
129
|
expect(root.attributes('role')).toBe('button');
|
|
130
130
|
expect(root.attributes('tabindex')).toBe('0');
|
|
@@ -152,7 +152,9 @@ describe('rcItemCard', () => {
|
|
|
152
152
|
}
|
|
153
153
|
});
|
|
154
154
|
|
|
155
|
-
|
|
155
|
+
const clickTarget = wrapper.find('.item-card-header-left');
|
|
156
|
+
|
|
157
|
+
await clickTarget.trigger('keydown.enter');
|
|
156
158
|
expect(wrapper.emitted('card-click')).toBeTruthy();
|
|
157
159
|
});
|
|
158
160
|
|
|
@@ -112,6 +112,8 @@ interface RcItemCardProps {
|
|
|
112
112
|
|
|
113
113
|
/** Makes the card clickable and emits 'card-click' on click/enter/space */
|
|
114
114
|
clickable?: boolean;
|
|
115
|
+
|
|
116
|
+
role?: 'link' | 'button' | undefined;
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
const props = defineProps<RcItemCardProps>();
|
|
@@ -161,27 +163,23 @@ const statusTooltips = computed(() => props.header.statuses?.map((status) => lab
|
|
|
161
163
|
const cardMeta = computed(() => ({
|
|
162
164
|
ariaLabel: props.clickable ? t('itemCard.ariaLabel.clickable', { cardTitle: labelText(props.header.title) }) : undefined,
|
|
163
165
|
tabIndex: props.clickable ? '0' : undefined,
|
|
164
|
-
role: props.clickable ? 'button' : undefined,
|
|
165
|
-
actionMenuLabel: props.actions && t('itemCard.actionMenu.label', { cardTitle: labelText(props.header.title) })
|
|
166
|
+
role: props.role ?? (props.clickable ? 'button' : undefined),
|
|
167
|
+
actionMenuLabel: props.actions && t('itemCard.actionMenu.label', { cardTitle: labelText(props.header.title) }),
|
|
166
168
|
}));
|
|
167
169
|
|
|
170
|
+
const cursorValue = computed(() => props.clickable ? 'pointer' : 'auto');
|
|
168
171
|
</script>
|
|
169
172
|
|
|
170
173
|
<template>
|
|
171
174
|
<div
|
|
172
175
|
ref="cardEl"
|
|
173
176
|
class="item-card"
|
|
177
|
+
:data-testid="`item-card-${id}`"
|
|
174
178
|
:class="{
|
|
175
179
|
'clickable':
|
|
176
180
|
clickable
|
|
177
181
|
}"
|
|
178
|
-
:role="cardMeta.role"
|
|
179
|
-
:tabindex="cardMeta.tabIndex"
|
|
180
|
-
:aria-label="cardMeta.ariaLabel"
|
|
181
|
-
:data-testid="`item-card-${id}`"
|
|
182
182
|
@click="_handleCardClick"
|
|
183
|
-
@keydown.enter="_handleCardClick"
|
|
184
|
-
@keydown.space.prevent="_handleCardClick"
|
|
185
183
|
>
|
|
186
184
|
<div :class="['item-card-body', variant]">
|
|
187
185
|
<template v-if="variant !== 'small'">
|
|
@@ -214,7 +212,16 @@ const cardMeta = computed(() => ({
|
|
|
214
212
|
|
|
215
213
|
<div :class="['item-card-body-details', variant]">
|
|
216
214
|
<div :class="['item-card-header', variant]">
|
|
217
|
-
<div
|
|
215
|
+
<div
|
|
216
|
+
class="item-card-header-left"
|
|
217
|
+
:data-testid="`card-header-left`"
|
|
218
|
+
:role="cardMeta.role"
|
|
219
|
+
:tabindex="cardMeta.tabIndex"
|
|
220
|
+
:aria-label="cardMeta.ariaLabel"
|
|
221
|
+
@click.self="_handleCardClick"
|
|
222
|
+
@keydown.enter="_handleCardClick"
|
|
223
|
+
@keydown.space.prevent="_handleCardClick"
|
|
224
|
+
>
|
|
218
225
|
<template v-if="variant === 'small'">
|
|
219
226
|
<slot name="item-card-image">
|
|
220
227
|
<div
|
|
@@ -315,16 +322,22 @@ $image-medium-box-width: 48px;
|
|
|
315
322
|
border-radius: var(--border-radius-md);
|
|
316
323
|
border: 1px solid var(--border);
|
|
317
324
|
background: var(--body-bg);
|
|
325
|
+
cursor: v-bind(cursorValue);
|
|
318
326
|
|
|
319
327
|
&.clickable:hover {
|
|
320
328
|
border-color: var(--primary);
|
|
321
329
|
}
|
|
322
330
|
|
|
323
|
-
&:focus-visible {
|
|
331
|
+
&:has(.item-card-header-left:focus-visible) {
|
|
332
|
+
border-color: var(--primary);
|
|
324
333
|
@include focus-outline;
|
|
325
334
|
outline-offset: -2px;
|
|
326
335
|
}
|
|
327
336
|
|
|
337
|
+
&:focus-visible {
|
|
338
|
+
outline: none;
|
|
339
|
+
}
|
|
340
|
+
|
|
328
341
|
&-image {
|
|
329
342
|
width: $image-medium-box-width;
|
|
330
343
|
height: $image-medium-box-width;
|
|
@@ -358,6 +371,10 @@ $image-medium-box-width: 48px;
|
|
|
358
371
|
&-left {
|
|
359
372
|
flex-grow: 1;
|
|
360
373
|
min-width: 0;
|
|
374
|
+
|
|
375
|
+
&:focus-visible {
|
|
376
|
+
outline: none;
|
|
377
|
+
}
|
|
361
378
|
}
|
|
362
379
|
|
|
363
380
|
&-title {
|
package/types/shell/index.d.ts
CHANGED
|
@@ -4089,6 +4089,20 @@ export function overlayIndividualBanners(parsedBanner: any, banners: any): void;
|
|
|
4089
4089
|
// @shell/utils/chart
|
|
4090
4090
|
|
|
4091
4091
|
declare module '@shell/utils/chart' {
|
|
4092
|
+
/**
|
|
4093
|
+
* Compares two chart versions using SemVer logic, with special handling for Rancher's "up" build metadata.
|
|
4094
|
+
*
|
|
4095
|
+
* It uses `semver.compare` for the primary comparison. If versions are considered equal (SemVer ignores build metadata),
|
|
4096
|
+
* it checks if both versions have build metadata starting with "up". If so, it strips the "up" prefix and compares the remaining strings as versions.
|
|
4097
|
+
*
|
|
4098
|
+
* If the "up" logic doesn't apply or results in equality, it falls back to `semver.compareBuild` to handle
|
|
4099
|
+
* other build metadata differences (e.g. sorting alphabetically).
|
|
4100
|
+
*
|
|
4101
|
+
* @param {string} v1 - The first version string.
|
|
4102
|
+
* @param {string} v2 - The second version string.
|
|
4103
|
+
* @returns {number} - 0 if equal, -1 if v1 < v2, 1 if v1 > v2.
|
|
4104
|
+
*/
|
|
4105
|
+
export function compareChartVersions(v1: string, v2: string): number;
|
|
4092
4106
|
/**
|
|
4093
4107
|
* Get the latest chart version that is compatible with the cluster's OS and user's pre-release preference.
|
|
4094
4108
|
* @param {Object} chart - The chart object.
|
|
@@ -5408,7 +5422,6 @@ export function parse(str: any): any;
|
|
|
5408
5422
|
export function sortable(str: any): any;
|
|
5409
5423
|
export function compare(in1: any, in2: any): any;
|
|
5410
5424
|
export function isPrerelease(version?: string): boolean;
|
|
5411
|
-
export function isUpgradeFromPreToStable(currentVersion: any, targetVersion: any): any;
|
|
5412
5425
|
export function isDevBuild(version: any): boolean;
|
|
5413
5426
|
export function getVersionInfo(store: any): {
|
|
5414
5427
|
displayVersion: any;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { compareChartVersions } from '@shell/utils/chart';
|
|
2
|
+
|
|
3
|
+
describe('compareChartVersions', () => {
|
|
4
|
+
describe('standard SemVer Comparison', () => {
|
|
5
|
+
it('should correctly compare standard versions', () => {
|
|
6
|
+
expect(compareChartVersions('1.0.0', '2.0.0')).toBe(-1);
|
|
7
|
+
expect(compareChartVersions('2.0.0', '1.0.0')).toBe(1);
|
|
8
|
+
expect(compareChartVersions('1.0.0', '1.0.0')).toBe(0);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should compare minor and patch versions correctly', () => {
|
|
12
|
+
expect(compareChartVersions('1.0.0', '1.1.0')).toBe(-1);
|
|
13
|
+
expect(compareChartVersions('1.0.0', '1.0.1')).toBe(-1);
|
|
14
|
+
expect(compareChartVersions('1.1.0', '1.0.1')).toBe(1);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should handle loose parsing (v-prefix)', () => {
|
|
18
|
+
expect(compareChartVersions('v1.0.0', '1.0.0')).toBe(0);
|
|
19
|
+
expect(compareChartVersions('v1.0.0', 'v2.0.0')).toBe(-1);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('rancher "up" Build Metadata Logic', () => {
|
|
24
|
+
it('should compare inner versions when both have "up" prefix', () => {
|
|
25
|
+
// 1.0.0 vs 2.0.0 inside the metadata
|
|
26
|
+
expect(compareChartVersions('1.0.0+up1.0.0', '1.0.0+up2.0.0')).toBe(-1);
|
|
27
|
+
expect(compareChartVersions('1.0.0+up2.0.0', '1.0.0+up1.0.0')).toBe(1);
|
|
28
|
+
// Equal inner versions
|
|
29
|
+
expect(compareChartVersions('1.0.0+up1.0.0', '1.0.0+up1.0.0')).toBe(0);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should handle pre-releases within "up" metadata correctly', () => {
|
|
33
|
+
// Crucial test: semver logic ensures 1.0.0-rc.1 < 1.0.0
|
|
34
|
+
// Standard string sort would often fail here depending on the string
|
|
35
|
+
expect(compareChartVersions('1.0.0+up1.0.0-rc.1', '1.0.0+up1.0.0')).toBe(-1);
|
|
36
|
+
expect(compareChartVersions('1.0.0+up1.0.0', '1.0.0+up1.0.0-rc.1')).toBe(1);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should compare different inner major/minor/patch versions', () => {
|
|
40
|
+
expect(compareChartVersions('0.0.1+up1.0.0', '0.0.1+up0.1.0')).toBe(1);
|
|
41
|
+
expect(compareChartVersions('0.0.1+up0.1.0', '0.0.1+up1.0.0')).toBe(-1);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should prioritize valid inner semver over invalid inner semver', () => {
|
|
45
|
+
// Valid "up" version > Invalid "up" version
|
|
46
|
+
expect(compareChartVersions('1.0.0+up1.0.0', '1.0.0+upInvalid')).toBe(1);
|
|
47
|
+
expect(compareChartVersions('1.0.0+upInvalid', '1.0.0+up1.0.0')).toBe(-1);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should fall back to lexical sort if both "up" suffixes are invalid semver', () => {
|
|
51
|
+
// Both are "up..." but not valid semver, so it falls back to semver.compareBuild (lexical)
|
|
52
|
+
expect(compareChartVersions('1.0.0+upA', '1.0.0+upB')).toBe(-1);
|
|
53
|
+
expect(compareChartVersions('1.0.0+upB', '1.0.0+upA')).toBe(1);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('standard Build Metadata Fallback', () => {
|
|
58
|
+
it('should correctly compare versions with standard build metadata (lexicographical)', () => {
|
|
59
|
+
// 1.0.0+a vs 1.0.0+b -> -1
|
|
60
|
+
expect(compareChartVersions('1.0.0+a', '1.0.0+b')).toBe(-1);
|
|
61
|
+
expect(compareChartVersions('1.0.0+b', '1.0.0+a')).toBe(1);
|
|
62
|
+
// 1.0.0+1 vs 1.0.0+2 -> -1
|
|
63
|
+
expect(compareChartVersions('1.0.0+1', '1.0.0+2')).toBe(-1);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should use standard comparison if only one has "up" prefix', () => {
|
|
67
|
+
// "up" comes after "foo" lexically
|
|
68
|
+
expect(compareChartVersions('1.0.0+foo', '1.0.0+up1.0.0')).toBe(-1);
|
|
69
|
+
expect(compareChartVersions('1.0.0+up1.0.0', '1.0.0+foo')).toBe(1);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('edge Cases and Invalid Inputs', () => {
|
|
74
|
+
it('should handle null or undefined inputs safely', () => {
|
|
75
|
+
// Implementation behavior: fallback to utils/version compare
|
|
76
|
+
// which treats falsy as "high" (return 1) if first arg is null?
|
|
77
|
+
// Checking implementation of `compare` in shell/utils/version.js:
|
|
78
|
+
// if (!in1) return 1; if (!in2) return -1;
|
|
79
|
+
expect(compareChartVersions(null, '1.0.0')).toBe(1);
|
|
80
|
+
expect(compareChartVersions('1.0.0', null)).toBe(-1);
|
|
81
|
+
expect(compareChartVersions(undefined, '1.0.0')).toBe(1);
|
|
82
|
+
expect(compareChartVersions('1.0.0', undefined)).toBe(-1);
|
|
83
|
+
expect(compareChartVersions(null, null)).toBe(1); // First check is !in1 -> 1
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should handle completely invalid strings', () => {
|
|
87
|
+
// "invalid" is not valid semver, so it falls back to utils/version compare (string/numeric comparison)
|
|
88
|
+
// "invalid" vs "1.0.0"
|
|
89
|
+
// "invalid" is treated as string, "1.0.0" parsed as parts
|
|
90
|
+
// Effectively tests the fallback logic stability
|
|
91
|
+
expect(compareChartVersions('invalid', '1.0.0')).not.toBe(0);
|
|
92
|
+
expect(compareChartVersions('a', 'b')).toBe(-1);
|
|
93
|
+
expect(compareChartVersions('b', 'a')).toBe(1);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isDevBuild,
|
|
1
|
+
import { isDevBuild, getReleaseNotesURL } from '@shell/utils/version';
|
|
2
2
|
|
|
3
3
|
describe('fx: isDevBuild', () => {
|
|
4
4
|
it.each([
|
|
@@ -17,24 +17,6 @@ describe('fx: isDevBuild', () => {
|
|
|
17
17
|
);
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
-
describe('fx: isUpgradeFromPreToStable', () => {
|
|
21
|
-
it('should be true when going from pre-release to stable of same version', () => {
|
|
22
|
-
expect(isUpgradeFromPreToStable('1.0.0-rc1', '1.0.0')).toBe(true);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('should be false when going from stable to pre-release', () => {
|
|
26
|
-
expect(isUpgradeFromPreToStable('1.0.0', '1.0.0-rc1')).toBe(false );
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('should be false for stable to stable', () => {
|
|
30
|
-
expect(isUpgradeFromPreToStable('1.0.0', '1.1.0')).toBe(false);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('should be false for pre-release to pre-release', () => {
|
|
34
|
-
expect(isUpgradeFromPreToStable('1.0.0-rc1', '1.0.0-rc2')).toBe(false);
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
|
|
38
20
|
describe('fx: getReleaseNotesURL', () => {
|
|
39
21
|
describe('when version is not provided', () => {
|
|
40
22
|
it('should return the community dev URL', () => {
|
package/utils/chart.js
CHANGED
|
@@ -1,5 +1,69 @@
|
|
|
1
|
+
import semver from 'semver';
|
|
2
|
+
import { compare } from '@shell/utils/version';
|
|
1
3
|
import { compatibleVersionsFor } from '@shell/store/catalog';
|
|
2
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Compares two chart versions using SemVer logic, with special handling for Rancher's "up" build metadata.
|
|
7
|
+
*
|
|
8
|
+
* It uses `semver.compare` for the primary comparison. If versions are considered equal (SemVer ignores build metadata),
|
|
9
|
+
* it checks if both versions have build metadata starting with "up". If so, it strips the "up" prefix and compares the remaining strings as versions.
|
|
10
|
+
*
|
|
11
|
+
* If the "up" logic doesn't apply or results in equality, it falls back to `semver.compareBuild` to handle
|
|
12
|
+
* other build metadata differences (e.g. sorting alphabetically).
|
|
13
|
+
*
|
|
14
|
+
* @param {string} v1 - The first version string.
|
|
15
|
+
* @param {string} v2 - The second version string.
|
|
16
|
+
* @returns {number} - 0 if equal, -1 if v1 < v2, 1 if v1 > v2.
|
|
17
|
+
*/
|
|
18
|
+
export function compareChartVersions(v1, v2) {
|
|
19
|
+
const v1Valid = semver.valid(v1, { loose: true });
|
|
20
|
+
const v2Valid = semver.valid(v2, { loose: true });
|
|
21
|
+
|
|
22
|
+
if (!v1Valid || !v2Valid) {
|
|
23
|
+
return compare(v1, v2);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// semver.compare ignores build metadata (e.g., 1.0.0+1 == 1.0.0+2)
|
|
27
|
+
let diff = semver.compare(v1, v2, { loose: true });
|
|
28
|
+
|
|
29
|
+
if (diff === 0) {
|
|
30
|
+
const parsedV1 = semver.parse(v1, { loose: true });
|
|
31
|
+
const parsedV2 = semver.parse(v2, { loose: true });
|
|
32
|
+
const buildV1 = parsedV1.build.join('.');
|
|
33
|
+
const buildV2 = parsedV2.build.join('.');
|
|
34
|
+
|
|
35
|
+
// Special logic for Rancher charts where "up" prefix in build metadata contains version info.
|
|
36
|
+
// E.g. 108.0.0+up0.25.0-rc.4 vs 108.0.0+up0.25.0
|
|
37
|
+
// Standard semver.compareBuild would sort ASCII: "up...-rc" > "up..." (incorrect for RC)
|
|
38
|
+
// We strip "up" and compare the rest as versions to properly handle pre-releases (RC < Stable).
|
|
39
|
+
if (buildV1.startsWith('up') && buildV2.startsWith('up')) {
|
|
40
|
+
const subV1 = buildV1.substring(2);
|
|
41
|
+
const subV2 = buildV2.substring(2);
|
|
42
|
+
const subV1Valid = semver.valid(subV1, { loose: true });
|
|
43
|
+
const subV2Valid = semver.valid(subV2, { loose: true });
|
|
44
|
+
|
|
45
|
+
if (subV1Valid && subV2Valid) {
|
|
46
|
+
// Both "up" metadata parts are valid semver: compare them semantically.
|
|
47
|
+
diff = semver.compare(subV1, subV2, { loose: true });
|
|
48
|
+
} else if (subV1Valid && !subV2Valid) {
|
|
49
|
+
// Only v1 has valid "up" metadata: prefer v1 over v2.
|
|
50
|
+
diff = 1;
|
|
51
|
+
} else if (!subV1Valid && subV2Valid) {
|
|
52
|
+
// Only v2 has valid "up" metadata: prefer v2 over v1.
|
|
53
|
+
diff = -1;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Fallback to standard build comparison for other cases (e.g. 1.0.0+1 vs 1.0.0+2).
|
|
58
|
+
// semver.compareBuild sorts build metadata lexicographically.
|
|
59
|
+
if (diff === 0) {
|
|
60
|
+
diff = semver.compareBuild(v1, v2, { loose: true });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return diff;
|
|
65
|
+
}
|
|
66
|
+
|
|
3
67
|
/**
|
|
4
68
|
* Get the latest chart version that is compatible with the cluster's OS and user's pre-release preference.
|
|
5
69
|
* @param {Object} chart - The chart object.
|
package/utils/version.js
CHANGED
|
@@ -3,6 +3,7 @@ import semver from 'semver';
|
|
|
3
3
|
import { MANAGEMENT } from '@shell/config/types';
|
|
4
4
|
import { READ_WHATS_NEW, SEEN_WHATS_NEW } from '@shell/store/prefs';
|
|
5
5
|
import { SETTING } from '@shell/config/settings';
|
|
6
|
+
import { getVersionData } from '@shell/config/version';
|
|
6
7
|
|
|
7
8
|
export function parse(str) {
|
|
8
9
|
str = `${ str }`;
|
|
@@ -74,21 +75,6 @@ export function isPrerelease(version = '') {
|
|
|
74
75
|
return !!semver.prerelease(version);
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
export function isUpgradeFromPreToStable(currentVersion, targetVersion) {
|
|
78
|
-
if (!isPrerelease(currentVersion) || isPrerelease(targetVersion)) {
|
|
79
|
-
return false;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const cVersion = semver.clean(currentVersion, { loose: true });
|
|
83
|
-
const tVersion = semver.clean(targetVersion, { loose: true });
|
|
84
|
-
|
|
85
|
-
if (cVersion && tVersion && semver.valid(cVersion) && semver.valid(tVersion)) {
|
|
86
|
-
return semver.lt(cVersion, tVersion);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
78
|
export function isDevBuild(version) {
|
|
93
79
|
if ( ['dev', 'master', 'head'].includes(version) || version.endsWith('-head') || version.match(/-rc\d+$/) || version.match(/-alpha\d+$/) ) {
|
|
94
80
|
return true;
|
|
@@ -98,8 +84,10 @@ export function isDevBuild(version) {
|
|
|
98
84
|
}
|
|
99
85
|
|
|
100
86
|
export function getVersionInfo(store) {
|
|
101
|
-
const
|
|
102
|
-
|
|
87
|
+
const fullVersion = store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.VERSION_RANCHER)?.value ??
|
|
88
|
+
getVersionData()?.Version ??
|
|
89
|
+
'unknown';
|
|
90
|
+
|
|
103
91
|
let displayVersion = fullVersion;
|
|
104
92
|
|
|
105
93
|
const match = fullVersion.match(/^(.*)-([0-9a-f]{40})-(.*)$/);
|