@rancher/shell 3.0.9 → 3.0.10
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 +2 -5
- package/components/CopyToClipboard.vue +28 -0
- package/components/CopyToClipboardText.vue +4 -0
- package/components/CruResource.vue +1 -0
- package/components/GlobalRoleBindings.vue +1 -5
- package/components/ResourceDetail/index.vue +0 -21
- package/components/__tests__/CruResource.test.ts +35 -1
- package/composables/useIsNewDetailPageEnabled.test.ts +98 -0
- package/composables/useIsNewDetailPageEnabled.ts +12 -0
- package/config/product/explorer.js +11 -1
- package/config/table-headers.js +0 -9
- package/config/types.js +0 -1
- package/edit/auth/github-app-steps.vue +2 -0
- package/edit/auth/github-steps.vue +2 -0
- package/edit/management.cattle.io.user.vue +60 -35
- package/edit/token.vue +29 -68
- package/models/token.js +0 -4
- package/package.json +8 -8
- package/pages/account/index.vue +67 -96
- package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +66 -9
- package/pages/c/_cluster/explorer/index.vue +2 -19
- 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 -4
- package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.vue +7 -7
- package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +5 -2
- 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 +5 -4
- package/types/shell/index.d.ts +92 -108
- package/utils/style.ts +17 -0
- package/utils/units.js +14 -5
- package/models/ext.cattle.io.token.js +0 -48
package/edit/token.vue
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { mapGetters } from 'vuex';
|
|
3
3
|
import day from 'dayjs';
|
|
4
4
|
import sortBy from 'lodash/sortBy';
|
|
5
|
-
import { MANAGEMENT,
|
|
5
|
+
import { MANAGEMENT, NORMAN } from '@shell/config/types';
|
|
6
6
|
import { Banner } from '@components/Banner';
|
|
7
7
|
import DetailText from '@shell/components/DetailText';
|
|
8
8
|
import Footer from '@shell/components/form/Footer';
|
|
@@ -14,7 +14,6 @@ import CreateEditView from '@shell/mixins/create-edit-view';
|
|
|
14
14
|
import { diffFrom } from '@shell/utils/time';
|
|
15
15
|
import { filterHiddenLocalCluster, filterOnlyKubernetesClusters } from '@shell/utils/cluster';
|
|
16
16
|
import { SETTING } from '@shell/config/settings';
|
|
17
|
-
import Checkbox from '@components/Form/Checkbox/Checkbox.vue';
|
|
18
17
|
|
|
19
18
|
export default {
|
|
20
19
|
components: {
|
|
@@ -25,7 +24,6 @@ export default {
|
|
|
25
24
|
LabeledSelect,
|
|
26
25
|
RadioGroup,
|
|
27
26
|
Select,
|
|
28
|
-
Checkbox,
|
|
29
27
|
},
|
|
30
28
|
|
|
31
29
|
mixins: [CreateEditView],
|
|
@@ -43,11 +41,7 @@ export default {
|
|
|
43
41
|
|
|
44
42
|
return {
|
|
45
43
|
errors: null,
|
|
46
|
-
user: null,
|
|
47
44
|
form: {
|
|
48
|
-
enabled: true,
|
|
49
|
-
description: '',
|
|
50
|
-
clusterName: '',
|
|
51
45
|
expiryType: 'never',
|
|
52
46
|
customExpiry: 0,
|
|
53
47
|
customExpiryUnits: 'minute',
|
|
@@ -57,13 +51,11 @@ export default {
|
|
|
57
51
|
accessKey: '',
|
|
58
52
|
secretKey: '',
|
|
59
53
|
maxTTL,
|
|
60
|
-
ttl: ''
|
|
61
54
|
};
|
|
62
55
|
},
|
|
63
56
|
|
|
64
57
|
computed: {
|
|
65
58
|
...mapGetters({ t: 'i18n/t' }),
|
|
66
|
-
|
|
67
59
|
scopes() {
|
|
68
60
|
const all = this.$store.getters['management/all'](MANAGEMENT.CLUSTER);
|
|
69
61
|
const kubeClusters = filterHiddenLocalCluster(filterOnlyKubernetesClusters(all, this.$store), this.$store);
|
|
@@ -79,20 +71,16 @@ export default {
|
|
|
79
71
|
const options = ['never', 'day', 'month', 'year', 'custom'];
|
|
80
72
|
let opts = options.map((opt) => ({ value: opt, label: this.t(`accountAndKeys.apiKeys.add.expiry.options.${ opt }`) }));
|
|
81
73
|
|
|
82
|
-
// When the TTL is
|
|
74
|
+
// When the TTL is anything other than 0, present only two options
|
|
83
75
|
// (1) The maximum allowed
|
|
84
76
|
// (2) Custom
|
|
85
|
-
if (this.maxTTL
|
|
77
|
+
if (this.maxTTL !== 0 ) {
|
|
86
78
|
const now = day();
|
|
87
79
|
const expiry = now.add(this.maxTTL, 'minute');
|
|
88
80
|
const max = diffFrom(expiry, now, this.t);
|
|
89
81
|
|
|
90
82
|
opts = opts.filter((opt) => opt.value === 'custom');
|
|
91
83
|
opts.unshift({ value: 'max', label: this.t('accountAndKeys.apiKeys.add.expiry.options.maximum', { value: max.string }) });
|
|
92
|
-
} else {
|
|
93
|
-
// maxTTL <= 0 means there is no maximum, so we can show the 'never' option which results in an infinite TTL
|
|
94
|
-
// OR if we set a positive TTL, then it assumes that value
|
|
95
|
-
opts = opts.filter((opt) => opt.value === 'never' || opt.value === 'custom');
|
|
96
84
|
}
|
|
97
85
|
|
|
98
86
|
return opts;
|
|
@@ -103,9 +91,6 @@ export default {
|
|
|
103
91
|
|
|
104
92
|
return filtered.map((opt) => ({ value: opt, label: this.t(`accountAndKeys.apiKeys.add.customExpiry.options.${ opt }`) }));
|
|
105
93
|
},
|
|
106
|
-
hasNeverOption() {
|
|
107
|
-
return this.expiryOptions?.filter((opt) => opt.value === 'never')?.length === 1;
|
|
108
|
-
}
|
|
109
94
|
},
|
|
110
95
|
|
|
111
96
|
mounted() {
|
|
@@ -130,33 +115,31 @@ export default {
|
|
|
130
115
|
});
|
|
131
116
|
},
|
|
132
117
|
|
|
133
|
-
async actuallySave() {
|
|
134
|
-
// update expiration value before save
|
|
118
|
+
async actuallySave(url) {
|
|
135
119
|
this.updateExpiry();
|
|
136
|
-
|
|
137
120
|
if ( this.isCreate ) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
ttl: this.ttl
|
|
147
|
-
// userID: not needed as it will be set by the backend to the current user
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
const steveTokenSaved = await steveToken.save();
|
|
152
|
-
|
|
153
|
-
this.created = steveTokenSaved;
|
|
154
|
-
this.ttlLimited = this.created?.spec?.ttl !== this.ttl;
|
|
155
|
-
const token = this.created?.status?.bearerToken?.split(':');
|
|
121
|
+
// Description is a bit weird, so need to clone and set this
|
|
122
|
+
// rather than use this.value - need to find a way to set this if we ever
|
|
123
|
+
// want to allow edit (which I don't think we do)
|
|
124
|
+
const res = await this.value.save();
|
|
125
|
+
|
|
126
|
+
this.created = res;
|
|
127
|
+
this.ttlLimited = res.ttl !== this.value.ttl;
|
|
128
|
+
const token = this.created.token.split(':');
|
|
156
129
|
|
|
157
130
|
this.accessKey = token[0];
|
|
158
131
|
this.secretKey = (token.length > 1) ? token[1] : '';
|
|
159
|
-
this.token = this.created
|
|
132
|
+
this.token = this.created.token;
|
|
133
|
+
|
|
134
|
+
// Force a refresh of the token so we get the expiry date correctly
|
|
135
|
+
await this.$store.dispatch('rancher/find', {
|
|
136
|
+
type: NORMAN.TOKEN,
|
|
137
|
+
id: res.id,
|
|
138
|
+
opt: { force: true }
|
|
139
|
+
}, { root: true });
|
|
140
|
+
} else {
|
|
141
|
+
// Note: update of existing key not supported currently
|
|
142
|
+
await this.value.save();
|
|
160
143
|
}
|
|
161
144
|
},
|
|
162
145
|
|
|
@@ -176,9 +159,7 @@ export default {
|
|
|
176
159
|
const units = (v === 'custom') ? this.form.customExpiryUnits : v;
|
|
177
160
|
let ttl = 0;
|
|
178
161
|
|
|
179
|
-
if (
|
|
180
|
-
ttl = -1;
|
|
181
|
-
} else if (units === 'max') {
|
|
162
|
+
if (units === 'max') {
|
|
182
163
|
ttl = this.maxTTL * 60 * 1000;
|
|
183
164
|
} else if ( units !== 'never' ) {
|
|
184
165
|
const now = day();
|
|
@@ -186,8 +167,7 @@ export default {
|
|
|
186
167
|
|
|
187
168
|
ttl = expiry.diff(now);
|
|
188
169
|
}
|
|
189
|
-
|
|
190
|
-
this.ttl = ttl;
|
|
170
|
+
this.value.ttl = ttl;
|
|
191
171
|
}
|
|
192
172
|
}
|
|
193
173
|
};
|
|
@@ -198,7 +178,7 @@ export default {
|
|
|
198
178
|
<div class="pl-10 pr-10">
|
|
199
179
|
<LabeledInput
|
|
200
180
|
key="description"
|
|
201
|
-
v-model:value="
|
|
181
|
+
v-model:value="value.description"
|
|
202
182
|
:placeholder="t('accountAndKeys.apiKeys.add.description.placeholder')"
|
|
203
183
|
label-key="accountAndKeys.apiKeys.add.description.label"
|
|
204
184
|
mode="edit"
|
|
@@ -206,30 +186,13 @@ export default {
|
|
|
206
186
|
/>
|
|
207
187
|
|
|
208
188
|
<LabeledSelect
|
|
209
|
-
v-model:value="
|
|
189
|
+
v-model:value="value.clusterId"
|
|
210
190
|
class="mt-20 scope-select"
|
|
211
191
|
label-key="accountAndKeys.apiKeys.add.scope"
|
|
212
192
|
:options="scopes"
|
|
213
193
|
/>
|
|
214
194
|
|
|
215
|
-
<
|
|
216
|
-
v-model:value="form.enabled"
|
|
217
|
-
class="mt-20 mb-20"
|
|
218
|
-
:mode="mode"
|
|
219
|
-
label-key="accountAndKeys.apiKeys.add.enabled"
|
|
220
|
-
/>
|
|
221
|
-
|
|
222
|
-
<Banner
|
|
223
|
-
v-if="hasNeverOption"
|
|
224
|
-
color="warning"
|
|
225
|
-
class="mt-20"
|
|
226
|
-
>
|
|
227
|
-
<div>
|
|
228
|
-
{{ t('accountAndKeys.apiKeys.info.expiryOptionsWithNever') }}
|
|
229
|
-
</div>
|
|
230
|
-
</Banner>
|
|
231
|
-
|
|
232
|
-
<h5 class="mb-20">
|
|
195
|
+
<h5 class="pt-20">
|
|
233
196
|
{{ t('accountAndKeys.apiKeys.add.expiry.label') }}
|
|
234
197
|
</h5>
|
|
235
198
|
|
|
@@ -241,9 +204,7 @@ export default {
|
|
|
241
204
|
class="mr-20"
|
|
242
205
|
name="expiryGroup"
|
|
243
206
|
/>
|
|
244
|
-
<div
|
|
245
|
-
class="ml-20 mt-10 expiry"
|
|
246
|
-
>
|
|
207
|
+
<div class="ml-20 mt-10 expiry">
|
|
247
208
|
<input
|
|
248
209
|
v-model="form.customExpiry"
|
|
249
210
|
:disabled="form.expiryType !== 'custom'"
|
package/models/token.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rancher/shell",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.10",
|
|
4
4
|
"description": "Rancher Dashboard Shell",
|
|
5
5
|
"repository": "https://github.com/rancher/dashboard",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"private": false,
|
|
9
9
|
"types": "types/shell/index.d.ts",
|
|
10
10
|
"engines": {
|
|
11
|
-
"node": ">=
|
|
11
|
+
"node": ">=24.0.0"
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
14
|
"**/*"
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"@popperjs/core": "2.11.8",
|
|
42
42
|
"@rancher/icons": "2.0.55",
|
|
43
43
|
"@types/is-url": "1.2.30",
|
|
44
|
-
"@types/node": "
|
|
44
|
+
"@types/node": "25.3.3",
|
|
45
45
|
"@types/semver": "^7.5.8",
|
|
46
46
|
"@typescript-eslint/eslint-plugin": "5.62.0",
|
|
47
47
|
"@typescript-eslint/parser": "5.62.0",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"color": "5.0.3",
|
|
66
66
|
"cookie-universal": "2.2.2",
|
|
67
67
|
"cookie": "0.7.0",
|
|
68
|
-
"core-js": "3.
|
|
68
|
+
"core-js": "3.48.0",
|
|
69
69
|
"cron-validator": "1.4.0",
|
|
70
70
|
"cronstrue": "3.9.0",
|
|
71
71
|
"cross-env": "7.0.3",
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
"papaparse": "5.3.0",
|
|
120
120
|
"portal-vue": "~3.0.0",
|
|
121
121
|
"sass-loader": "12.6.0",
|
|
122
|
-
"sass": "1.
|
|
122
|
+
"sass": "1.97.3",
|
|
123
123
|
"serve-static": "1.14.1",
|
|
124
124
|
"set-cookie-parser": "2.4.6",
|
|
125
125
|
"shell-quote": "1.7.3",
|
|
@@ -131,10 +131,10 @@
|
|
|
131
131
|
"ufo": "0.7.11",
|
|
132
132
|
"unfetch": "4.2.0",
|
|
133
133
|
"url-parse": "1.5.10",
|
|
134
|
-
"vue-router": "4.
|
|
134
|
+
"vue-router": "4.6.4",
|
|
135
135
|
"vue-select": "4.0.0-beta.6",
|
|
136
136
|
"vue-server-renderer": "2.7.16",
|
|
137
|
-
"vue": "3.5.
|
|
137
|
+
"vue": "3.5.29",
|
|
138
138
|
"vue3-resize": "0.2.0",
|
|
139
139
|
"vue3-virtual-scroll-list": "0.2.1",
|
|
140
140
|
"vuedraggable": "4.1.0",
|
|
@@ -164,7 +164,7 @@
|
|
|
164
164
|
"roarr": "7.0.4",
|
|
165
165
|
"semver": "7.5.4",
|
|
166
166
|
"@types/lodash": "4.17.5",
|
|
167
|
-
"@types/node": "
|
|
167
|
+
"@types/node": "25.3.3",
|
|
168
168
|
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
|
|
169
169
|
},
|
|
170
170
|
"nyc": {
|
package/pages/account/index.vue
CHANGED
|
@@ -9,107 +9,94 @@ import { mapGetters } from 'vuex';
|
|
|
9
9
|
|
|
10
10
|
import { Banner } from '@components/Banner';
|
|
11
11
|
import ResourceTable from '@shell/components/ResourceTable';
|
|
12
|
+
import CopyToClipboardText from '@shell/components/CopyToClipboardText';
|
|
12
13
|
import TabTitle from '@shell/components/TabTitle';
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
import {
|
|
17
|
-
ACCESS_KEY, DESCRIPTION, EXPIRES, EXPIRY_STATE,
|
|
18
|
-
LAST_USED, AGE_NORMAN, SCOPE_NORMAN, NORMAN_KEY_DEPRECATION
|
|
19
|
-
} from '@shell/config/table-headers';
|
|
20
|
-
import { FilterArgs, PaginationParamFilter } from '@shell/types/store/pagination.types';
|
|
15
|
+
const API_ENDPOINT = '/v3';
|
|
21
16
|
|
|
22
17
|
export default {
|
|
23
18
|
components: {
|
|
24
|
-
BackLink, Banner, Loading, ResourceTable, Principal, TabTitle
|
|
19
|
+
CopyToClipboardText, BackLink, Banner, Loading, ResourceTable, Principal, TabTitle
|
|
25
20
|
},
|
|
26
21
|
mixins: [BackRoute],
|
|
27
22
|
async fetch() {
|
|
28
|
-
const hashedRequests = {};
|
|
29
|
-
|
|
30
23
|
this.canChangePassword = await this.calcCanChangePassword();
|
|
31
24
|
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
if (this.apiKeySchema) {
|
|
26
|
+
this.rows = await this.$store.dispatch('rancher/findAll', { type: NORMAN.TOKEN });
|
|
27
|
+
}
|
|
34
28
|
|
|
35
|
-
|
|
29
|
+
// Get all settings - the API host setting may not be set, so this avoids a 404 request if we look for the specific setting
|
|
30
|
+
const allSettings = await this.$store.dispatch('management/findAll', { type: MANAGEMENT.SETTING });
|
|
31
|
+
const apiHostSetting = allSettings.find((i) => i.id === SETTING.API_HOST);
|
|
32
|
+
const serverUrlSetting = allSettings.find((i) => i.id === SETTING.SERVER_URL);
|
|
36
33
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
34
|
+
this.apiHostSetting = apiHostSetting?.value;
|
|
35
|
+
this.serverUrlSetting = serverUrlSetting?.value;
|
|
40
36
|
|
|
41
|
-
|
|
42
|
-
this.filterByUserTokens = this.$store.getters[`management/paginationEnabled`](EXT.TOKEN);
|
|
43
|
-
|
|
44
|
-
if (this.filterByUserTokens && selfUser?.status?.userID) {
|
|
45
|
-
// Only get associated with the current user
|
|
46
|
-
const opt = { // Of type ActionFindPageArgs
|
|
47
|
-
pagination: new FilterArgs({
|
|
48
|
-
filters: PaginationParamFilter.createSingleField({
|
|
49
|
-
field: 'metadata.fields.1',
|
|
50
|
-
value: selfUser.status?.userID,
|
|
51
|
-
})
|
|
52
|
-
})
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
hashedRequests.steveTokens = this.$store.dispatch(`management/findPage`, { type: EXT.TOKEN, opt });
|
|
56
|
-
} else {
|
|
57
|
-
hashedRequests.steveTokens = this.$store.dispatch('management/findAll', { type: EXT.TOKEN });
|
|
58
|
-
}
|
|
59
|
-
}
|
|
37
|
+
const selfUser = await this.$store.dispatch('auth/getSelfUser');
|
|
60
38
|
|
|
61
39
|
if (selfUser?.canGetUser && selfUser.status?.userID) {
|
|
62
40
|
// Fetch the user info for ChangePassword (ChangePasswordDialog needs the user info for the user whose password is being changed)
|
|
63
|
-
|
|
41
|
+
this.user = await this.$store.dispatch('management/find', {
|
|
64
42
|
type: MANAGEMENT.USER,
|
|
65
43
|
id: selfUser.status?.userID
|
|
66
44
|
});
|
|
45
|
+
} else {
|
|
46
|
+
throw new Error(this.t('changePassword.errors.cannotFetchSelf'));
|
|
67
47
|
}
|
|
68
|
-
|
|
69
|
-
// Get all settings - the API host setting may not be set, so this avoids a 404 request if we look for the specific setting
|
|
70
|
-
hashedRequests.allSettings = this.$store.dispatch('management/findAll', { type: MANAGEMENT.SETTING });
|
|
71
|
-
|
|
72
|
-
const {
|
|
73
|
-
normanTokens, steveTokens, allSettings, user
|
|
74
|
-
} = await allHash(hashedRequests);
|
|
75
|
-
|
|
76
|
-
this.normanTokens = normanTokens;
|
|
77
|
-
this.steveTokens = steveTokens;
|
|
78
|
-
this.user = user;
|
|
79
|
-
|
|
80
|
-
const apiHostSetting = allSettings.find((i) => i.id === SETTING.API_HOST);
|
|
81
|
-
const serverUrlSetting = allSettings.find((i) => i.id === SETTING.SERVER_URL);
|
|
82
|
-
|
|
83
|
-
this.apiHostSetting = apiHostSetting?.value;
|
|
84
|
-
this.serverUrlSetting = serverUrlSetting?.value;
|
|
85
48
|
},
|
|
86
49
|
data() {
|
|
87
50
|
return {
|
|
88
|
-
normanTokenSchema: undefined,
|
|
89
|
-
steveTokenSchema: undefined,
|
|
90
51
|
apiHostSetting: null,
|
|
91
52
|
serverUrlSetting: null,
|
|
92
53
|
rows: null,
|
|
93
54
|
canChangePassword: false,
|
|
94
|
-
user: null
|
|
95
|
-
normanTokens: null,
|
|
96
|
-
steveTokens: null,
|
|
55
|
+
user: null
|
|
97
56
|
};
|
|
98
57
|
},
|
|
99
58
|
computed: {
|
|
100
59
|
...mapGetters({ t: 'i18n/t' }),
|
|
101
60
|
|
|
102
61
|
apiKeyheaders() {
|
|
103
|
-
return [
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
62
|
+
return this.apiKeySchema ? this.$store.getters['type-map/headersFor'](this.apiKeySchema) : [];
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// Port of Ember code for API Url - see: https://github.com/rancher/ui/blob/8e07c492673171731f3b26af14c978bc103d1828/lib/shared/addon/endpoint/service.js#L58
|
|
66
|
+
apiUrlBase() {
|
|
67
|
+
let setting = this.apiHostSetting;
|
|
68
|
+
|
|
69
|
+
if (setting && setting.indexOf('http') !== 0) {
|
|
70
|
+
setting = `http://${ setting }`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Use Server Setting URL if the api host setting is not set
|
|
74
|
+
let url = setting || this.serverUrlSetting;
|
|
75
|
+
|
|
76
|
+
// If the URL is relative, add on the current base URL from the browser
|
|
77
|
+
if ( url.indexOf('http') !== 0 ) {
|
|
78
|
+
url = `${ window.location.origin }/${ url.replace(/^\/+/, '') }`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// URL must end in a single slash
|
|
82
|
+
url = `${ url.replace(/\/+$/, '') }/`;
|
|
83
|
+
|
|
84
|
+
return url;
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
apiUrl() {
|
|
88
|
+
const base = this.apiUrlBase;
|
|
89
|
+
const path = API_ENDPOINT.replace(/^\/+/, '');
|
|
90
|
+
|
|
91
|
+
return `${ base }${ path }`;
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
apiKeySchema() {
|
|
95
|
+
try {
|
|
96
|
+
return this.$store.getters[`rancher/schemaFor`](NORMAN.TOKEN);
|
|
97
|
+
} catch (e) {}
|
|
98
|
+
|
|
99
|
+
return null;
|
|
113
100
|
},
|
|
114
101
|
|
|
115
102
|
principal() {
|
|
@@ -123,7 +110,7 @@ export default {
|
|
|
123
110
|
return principal || {};
|
|
124
111
|
},
|
|
125
112
|
|
|
126
|
-
|
|
113
|
+
apiKeys() {
|
|
127
114
|
// Filter out tokens that are not API Keys and are not expired UI Sessions
|
|
128
115
|
const isApiKey = (key) => {
|
|
129
116
|
const labels = key.labels;
|
|
@@ -133,24 +120,7 @@ export default {
|
|
|
133
120
|
return ( !expired || !labels || !labels['ui-session'] ) && !current;
|
|
134
121
|
};
|
|
135
122
|
|
|
136
|
-
return !this.
|
|
137
|
-
},
|
|
138
|
-
|
|
139
|
-
filteredNewTokens() {
|
|
140
|
-
// Filter out tokens that are not API Keys and are not expired UI Sessions
|
|
141
|
-
const isApiKey = (key) => {
|
|
142
|
-
const labels = key.metadata?.labels;
|
|
143
|
-
const expired = key.status?.expired;
|
|
144
|
-
const current = key.status?.current;
|
|
145
|
-
|
|
146
|
-
return ( !expired || !labels || !labels['ui-session'] ) && !current;
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
return !this.steveTokens ? [] : this.steveTokens.filter(isApiKey);
|
|
150
|
-
},
|
|
151
|
-
|
|
152
|
-
apiKeys() {
|
|
153
|
-
return (this.filteredNormanTokens || []).concat(this.filteredNewTokens || []);
|
|
123
|
+
return !this.rows ? [] : this.rows.filter(isApiKey);
|
|
154
124
|
}
|
|
155
125
|
},
|
|
156
126
|
|
|
@@ -221,9 +191,16 @@ export default {
|
|
|
221
191
|
<div class="keys-header">
|
|
222
192
|
<div>
|
|
223
193
|
<h2 v-t="'accountAndKeys.apiKeys.title'" />
|
|
194
|
+
<div class="api-url">
|
|
195
|
+
<span>{{ t("accountAndKeys.apiKeys.apiEndpoint") }}</span>
|
|
196
|
+
<CopyToClipboardText
|
|
197
|
+
:aria-label="t('accountAndKeys.apiKeys.copyApiEnpoint')"
|
|
198
|
+
:text="apiUrl"
|
|
199
|
+
/>
|
|
200
|
+
</div>
|
|
224
201
|
</div>
|
|
225
202
|
<button
|
|
226
|
-
v-if="
|
|
203
|
+
v-if="apiKeySchema"
|
|
227
204
|
role="button"
|
|
228
205
|
:aria-label="t('accountAndKeys.apiKeys.add.label')"
|
|
229
206
|
class="btn role-primary add mb-20"
|
|
@@ -234,17 +211,11 @@ export default {
|
|
|
234
211
|
</button>
|
|
235
212
|
</div>
|
|
236
213
|
<div
|
|
237
|
-
v-if="
|
|
214
|
+
v-if="apiKeySchema"
|
|
238
215
|
class="keys"
|
|
239
216
|
>
|
|
240
|
-
<Banner
|
|
241
|
-
v-if="filteredNormanTokens.length"
|
|
242
|
-
color="warning"
|
|
243
|
-
class="mb-20"
|
|
244
|
-
:label="t('accountAndKeys.apiKeys.normanTokenDeprecation')"
|
|
245
|
-
/>
|
|
246
217
|
<ResourceTable
|
|
247
|
-
:schema="
|
|
218
|
+
:schema="apiKeySchema"
|
|
248
219
|
:rows="apiKeys"
|
|
249
220
|
:headers="apiKeyheaders"
|
|
250
221
|
key-field="id"
|
|
@@ -1,27 +1,78 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { reactive } from 'vue';
|
|
2
3
|
import { RcItemCardAction } from '@components/RcItemCard';
|
|
3
4
|
import { RcButton } from '@components/RcButton';
|
|
4
|
-
import {
|
|
5
|
+
import { isTruncated } from '@shell/utils/style';
|
|
6
|
+
import RcIcon from '@components/RcIcon/RcIcon.vue';
|
|
7
|
+
import type { RcIconType } from '@components/RcIcon/types';
|
|
5
8
|
|
|
6
9
|
interface FooterItem {
|
|
7
|
-
icon?:
|
|
8
|
-
iconTooltip?:
|
|
10
|
+
icon?: RcIconType;
|
|
11
|
+
iconTooltip?: { key?: string; text?: string };
|
|
9
12
|
labels: string[];
|
|
10
13
|
labelTooltip?: string;
|
|
11
14
|
type?: string;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
|
-
const emit = defineEmits<{(e: 'click:item', type: string, label: string): void
|
|
17
|
+
const emit = defineEmits<{(e: 'click:item', type: string, label: string): void }>();
|
|
15
18
|
|
|
16
19
|
defineProps<{
|
|
17
20
|
items: FooterItem[];
|
|
18
21
|
clickable?: boolean;
|
|
19
22
|
}>();
|
|
20
23
|
|
|
21
|
-
|
|
24
|
+
const tooltipOverrides = reactive<Record<string, string | undefined>>({});
|
|
25
|
+
const labelElements: Record<string, HTMLElement | null> = {};
|
|
26
|
+
|
|
27
|
+
function onClickItem(type: string, label: string): void {
|
|
22
28
|
emit('click:item', type, label);
|
|
23
29
|
}
|
|
24
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Creates a unique key for identifying a label element.
|
|
33
|
+
* @param itemIndex - Index of the footer item
|
|
34
|
+
* @param labelIndex - Index of the label within the footer item
|
|
35
|
+
*/
|
|
36
|
+
function createLabelKey(itemIndex: number, labelIndex: number): string {
|
|
37
|
+
return `${ itemIndex }-${ labelIndex }`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Registers a label element reference for truncation detection.
|
|
42
|
+
* @param key - Unique identifier for the label
|
|
43
|
+
* @param el - The HTML element or null
|
|
44
|
+
*/
|
|
45
|
+
function registerLabelRef(key: string, el: HTMLElement | null): void {
|
|
46
|
+
labelElements[key] = el;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Updates the tooltip content based on whether the label text is truncated.
|
|
51
|
+
* If truncated, prepends the full label text to the base tooltip.
|
|
52
|
+
* @param key - Unique identifier for the label
|
|
53
|
+
* @param label - The label text content
|
|
54
|
+
* @param baseTooltip - The original tooltip content
|
|
55
|
+
*/
|
|
56
|
+
function updateTooltipOnHover(key: string, label: string, baseTooltip?: string): void {
|
|
57
|
+
if (!baseTooltip) {
|
|
58
|
+
tooltipOverrides[key] = undefined;
|
|
59
|
+
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const element = labelElements[key];
|
|
64
|
+
|
|
65
|
+
tooltipOverrides[key] = isTruncated(element) ? `${ label }. ${ baseTooltip }` : baseTooltip;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Returns the tooltip to display for a given label.
|
|
70
|
+
* @param key - Unique identifier for the label
|
|
71
|
+
* @param fallback - Fallback tooltip if no override exists
|
|
72
|
+
*/
|
|
73
|
+
function getTooltip(key: string, fallback?: string): string | undefined {
|
|
74
|
+
return tooltipOverrides[key] ?? fallback;
|
|
75
|
+
}
|
|
25
76
|
</script>
|
|
26
77
|
|
|
27
78
|
<template>
|
|
@@ -34,7 +85,7 @@ function onClickItem(type: string, label: string) {
|
|
|
34
85
|
>
|
|
35
86
|
<RcIcon
|
|
36
87
|
v-if="footerItem.icon"
|
|
37
|
-
v-clean-tooltip="footerItem.iconTooltip?.key ? t(footerItem.iconTooltip
|
|
88
|
+
v-clean-tooltip="footerItem.iconTooltip?.key ? t(footerItem.iconTooltip.key) : undefined"
|
|
38
89
|
class="app-chart-card-footer-item-icon"
|
|
39
90
|
:type="footerItem.icon"
|
|
40
91
|
/>
|
|
@@ -47,14 +98,18 @@ function onClickItem(type: string, label: string) {
|
|
|
47
98
|
class="app-chart-card-footer-item-action"
|
|
48
99
|
>
|
|
49
100
|
<rc-button
|
|
50
|
-
v-clean-tooltip="footerItem.labelTooltip"
|
|
101
|
+
v-clean-tooltip="getTooltip(createLabelKey(i, j), footerItem.labelTooltip)"
|
|
51
102
|
variant="ghost"
|
|
52
103
|
class="app-chart-card-footer-button secondary-text-link"
|
|
53
104
|
data-testid="app-chart-card-footer-item-text"
|
|
54
105
|
:aria-label="t('catalog.charts.appChartCard.footerItem.ariaLabel', { filter: label })"
|
|
55
106
|
@click="onClickItem(footerItem.type, label)"
|
|
107
|
+
@mouseenter="updateTooltipOnHover(createLabelKey(i, j), label, footerItem.labelTooltip)"
|
|
56
108
|
>
|
|
57
|
-
<span
|
|
109
|
+
<span
|
|
110
|
+
:ref="(el) => registerLabelRef(createLabelKey(i, j), el as HTMLElement)"
|
|
111
|
+
class="app-chart-card-footer-button-label"
|
|
112
|
+
>{{ label }}</span>
|
|
58
113
|
</rc-button>
|
|
59
114
|
<span
|
|
60
115
|
v-if="footerItem.labels.length > 1 && j !== footerItem.labels.length - 1"
|
|
@@ -63,9 +118,11 @@ function onClickItem(type: string, label: string) {
|
|
|
63
118
|
</rc-item-card-action>
|
|
64
119
|
<span
|
|
65
120
|
v-else
|
|
66
|
-
|
|
121
|
+
:ref="(el) => registerLabelRef(createLabelKey(i, j), el as HTMLElement)"
|
|
122
|
+
v-clean-tooltip="getTooltip(createLabelKey(i, j), footerItem.labelTooltip)"
|
|
67
123
|
class="app-chart-card-footer-item-text"
|
|
68
124
|
data-testid="app-chart-card-footer-item-text"
|
|
125
|
+
@mouseenter="updateTooltipOnHover(createLabelKey(i, j), label, footerItem.labelTooltip)"
|
|
69
126
|
>
|
|
70
127
|
{{ label }}
|
|
71
128
|
<span
|
|
@@ -394,29 +394,12 @@ export default {
|
|
|
394
394
|
},
|
|
395
395
|
|
|
396
396
|
metricAggregations() {
|
|
397
|
-
let checkNodes = this.nodes;
|
|
398
|
-
|
|
399
|
-
// Special case local cluster
|
|
400
|
-
if (this.currentCluster.isLocal) {
|
|
401
|
-
const nodeNames = this.nodes.reduce((acc, n) => {
|
|
402
|
-
acc[n.id] = n;
|
|
403
|
-
|
|
404
|
-
return acc;
|
|
405
|
-
}, {});
|
|
406
|
-
|
|
407
|
-
checkNodes = this.mgmtNodes.filter((n) => {
|
|
408
|
-
const nodeName = n.metadata?.labels?.['management.cattle.io/nodename'] || n.id;
|
|
409
|
-
|
|
410
|
-
return !!nodeNames[nodeName];
|
|
411
|
-
});
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
const someNonWorkerRoles = checkNodes.some((node) => node.hasARole && !node.isWorker);
|
|
415
397
|
const metrics = this.nodeMetrics.filter((nodeMetrics) => {
|
|
416
398
|
const node = this.nodes.find((nd) => nd.id === nodeMetrics.id);
|
|
417
399
|
|
|
418
|
-
return node
|
|
400
|
+
return node;
|
|
419
401
|
});
|
|
402
|
+
|
|
420
403
|
const initialAggregation = {
|
|
421
404
|
cpu: 0,
|
|
422
405
|
memory: 0
|