@rancher/shell 3.0.9-rc.3 → 3.0.9-rc.5
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/brand/suse/metadata.json +2 -1
- package/assets/translations/en-us.yaml +105 -5
- package/components/ActionMenuShell.vue +1 -1
- package/components/Inactivity.vue +2 -2
- package/components/Resource/Detail/Card/ExtrasCard.vue +49 -15
- package/components/Resource/Detail/Card/__tests__/ExtrasCard.test.ts +111 -0
- package/components/Resource/Detail/Masthead/__tests__/index.test.ts +0 -17
- package/components/Resource/Detail/Masthead/index.vue +11 -4
- package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +3 -1
- package/components/Resource/Detail/Metadata/index.vue +1 -1
- package/components/Resource/Detail/ResourceRow.vue +1 -1
- package/components/ResourceDetail/Masthead/latest.vue +12 -2
- package/components/ResourceList/index.vue +9 -0
- package/components/ResourceTable.vue +38 -4
- package/components/Tabbed/Tab.vue +4 -0
- package/components/Tabbed/index.vue +4 -1
- package/components/__tests__/ProjectRow.test.ts +60 -0
- package/components/form/ChangePassword.vue +41 -35
- package/components/form/ResourceQuota/Project.vue +42 -1
- package/components/form/ResourceQuota/ProjectRow.vue +71 -4
- package/components/form/ResourceQuota/__tests__/Project.test.ts +63 -0
- package/components/form/SelectOrCreateAuthSecret.vue +6 -1
- package/components/form/__tests__/SelectOrCreateAuthSecret.test.ts +35 -0
- package/components/formatter/KubeconfigClusters.vue +74 -0
- package/components/formatter/MachineSummaryGraph.vue +10 -2
- package/components/formatter/__tests__/KubeconfigClusters.test.ts +125 -0
- package/components/nav/TopLevelMenu.helper.ts +50 -2
- package/components/nav/TopLevelMenu.vue +14 -0
- package/components/nav/Type.vue +5 -0
- package/components/nav/__tests__/TopLevelMenu.test.ts +3 -3
- package/components/nav/__tests__/Type.test.ts +6 -4
- package/config/product/explorer.js +4 -3
- package/config/product/manager.js +47 -3
- package/config/router/navigation-guards/authentication.js +8 -9
- package/config/router/routes.js +4 -1
- package/config/types.js +10 -2
- package/detail/auditlog.cattle.io.auditpolicy.vue +19 -0
- package/detail/management.cattle.io.user.vue +1 -2
- package/detail/node.vue +0 -1
- package/detail/provisioning.cattle.io.cluster.vue +2 -1
- package/dialog/ChangePasswordDialog.vue +8 -0
- package/dialog/GenericPrompt.vue +20 -3
- package/dialog/ScaleMachineDownDialog.vue +65 -15
- package/dialog/SearchDialog.vue +10 -2
- package/dialog/__tests__/ScaleMachineDownDialog.test.ts +184 -0
- package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +89 -0
- package/edit/__tests__/management.cattle.io.project.test.js +56 -1
- package/edit/auditlog.cattle.io.auditpolicy/AdditionalRedactions.vue +114 -0
- package/edit/auditlog.cattle.io.auditpolicy/Filters.vue +119 -0
- package/edit/auditlog.cattle.io.auditpolicy/General.vue +180 -0
- package/edit/auditlog.cattle.io.auditpolicy/__tests__/AdditionalRedactions.test.ts +327 -0
- package/edit/auditlog.cattle.io.auditpolicy/__tests__/Filters.test.ts +449 -0
- package/edit/auditlog.cattle.io.auditpolicy/__tests__/General.test.ts +472 -0
- package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/AdditionalRedactions.test.ts.snap +27 -0
- package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/Filters.test.ts.snap +39 -0
- package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/General.test.ts.snap +174 -0
- package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/index.test.ts.snap +29 -0
- package/edit/auditlog.cattle.io.auditpolicy/__tests__/index.test.ts +215 -0
- package/edit/auditlog.cattle.io.auditpolicy/index.vue +104 -0
- package/edit/auditlog.cattle.io.auditpolicy/types.ts +28 -0
- package/edit/fleet.cattle.io.gitrepo.vue +16 -1
- package/edit/management.cattle.io.project.vue +8 -2
- package/edit/management.cattle.io.user.vue +29 -34
- package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +178 -0
- package/edit/provisioning.cattle.io.cluster/rke2.vue +22 -2
- package/edit/provisioning.cattle.io.cluster/shared.ts +4 -0
- package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +1 -0
- 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/auditlog.cattle.io.auditpolicy.vue +63 -0
- package/list/ext.cattle.io.kubeconfig.vue +118 -0
- package/list/group.principal.vue +11 -15
- package/list/management.cattle.io.user.vue +11 -21
- package/machine-config/azure.vue +14 -0
- package/mixins/__tests__/chart.test.ts +147 -0
- package/mixins/browser-tab-visibility.js +5 -4
- package/mixins/chart.js +10 -8
- package/mixins/fetch.client.js +6 -0
- package/models/__tests__/auditlog.cattle.io.auditpolicy.test.ts +117 -0
- package/models/__tests__/ext.cattle.io.kubeconfig.test.ts +364 -0
- package/models/__tests__/secret.test.ts +55 -0
- package/models/__tests__/workload.test.ts +49 -6
- package/models/auditlog.cattle.io.auditpolicy.js +46 -0
- package/models/cluster.x-k8s.io.machine.js +1 -1
- package/models/cluster.x-k8s.io.machinedeployment.js +5 -5
- package/models/event.js +5 -0
- package/models/ext.cattle.io.groupmembershiprefreshrequest.js +15 -0
- package/models/ext.cattle.io.kubeconfig.ts +97 -0
- package/models/ext.cattle.io.passwordchangerequest.js +15 -0
- package/models/ext.cattle.io.selfuser.js +15 -0
- package/models/fleet-application.js +17 -7
- package/models/management.cattle.io.user.js +28 -31
- package/models/schema.js +18 -0
- package/models/secret.js +28 -25
- package/models/steve-schema.ts +39 -2
- package/models/workload.js +3 -2
- package/package.json +2 -2
- package/pages/about.vue +3 -2
- package/pages/account/index.vue +23 -16
- package/pages/auth/login.vue +15 -8
- package/pages/auth/setup.vue +52 -15
- package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +38 -14
- package/pages/c/_cluster/apps/charts/index.vue +1 -0
- package/pages/home.vue +9 -3
- package/plugins/dashboard-store/__tests__/resource-class.test.ts +1 -3
- package/plugins/dashboard-store/actions.js +7 -0
- package/plugins/dashboard-store/getters.js +23 -1
- package/plugins/dashboard-store/index.js +3 -2
- package/plugins/dashboard-store/mutations.js +4 -0
- package/plugins/dashboard-store/resource-class.js +12 -5
- package/plugins/steve/__tests__/steve-class.test.ts +167 -0
- package/plugins/steve/schema.d.ts +5 -0
- package/plugins/steve/steve-class.js +19 -0
- package/plugins/steve/steve-pagination-utils.ts +2 -1
- package/rancher-components/RcItemCard/RcItemCard.test.ts +4 -2
- package/rancher-components/RcItemCard/RcItemCard.vue +27 -10
- package/store/auth.js +57 -19
- package/store/notifications.ts +1 -1
- package/store/type-map.js +12 -1
- package/types/shell/index.d.ts +24 -15
- package/types/store/dashboard-store.types.ts +7 -0
- package/utils/__tests__/chart.test.ts +96 -0
- package/utils/__tests__/version.test.ts +1 -19
- package/utils/chart.js +64 -0
- package/utils/pagination-wrapper.ts +11 -3
- package/utils/version.js +5 -17
- package/vue.config.js +26 -13
package/pages/auth/setup.vue
CHANGED
|
@@ -4,7 +4,7 @@ import { LabeledInput } from '@components/Form/LabeledInput';
|
|
|
4
4
|
import CopyToClipboard from '@shell/components/CopyToClipboard';
|
|
5
5
|
import AsyncButton from '@shell/components/AsyncButton';
|
|
6
6
|
import { LOGGED_OUT, SETUP } from '@shell/config/query-params';
|
|
7
|
-
import { NORMAN, MANAGEMENT } from '@shell/config/types';
|
|
7
|
+
import { NORMAN, MANAGEMENT, EXT } from '@shell/config/types';
|
|
8
8
|
import { findBy } from '@shell/utils/array';
|
|
9
9
|
import { Checkbox } from '@components/Form/Checkbox';
|
|
10
10
|
import { getVendor, getProduct, setVendor } from '@shell/config/private-label';
|
|
@@ -21,6 +21,7 @@ import FormValidation from '@shell/mixins/form-validation';
|
|
|
21
21
|
import isUrl from 'is-url';
|
|
22
22
|
import { isLocalhost } from '@shell/utils/validators/setting';
|
|
23
23
|
import Loading from '@shell/components/Loading';
|
|
24
|
+
import { getBrandMeta } from '@shell/utils/brand';
|
|
24
25
|
|
|
25
26
|
const calcIsFirstLogin = (store) => {
|
|
26
27
|
const firstLoginSetting = store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.FIRST_LOGIN);
|
|
@@ -31,7 +32,7 @@ const calcIsFirstLogin = (store) => {
|
|
|
31
32
|
const calcMustChangePassword = async(store) => {
|
|
32
33
|
await store.dispatch('auth/getUser');
|
|
33
34
|
|
|
34
|
-
const out = store.getters['auth/
|
|
35
|
+
const out = store.getters['auth/user']?.mustChangePassword;
|
|
35
36
|
|
|
36
37
|
return out;
|
|
37
38
|
};
|
|
@@ -62,7 +63,7 @@ export default {
|
|
|
62
63
|
current: null,
|
|
63
64
|
password: randomStr(),
|
|
64
65
|
confirm: null,
|
|
65
|
-
|
|
66
|
+
user: null,
|
|
66
67
|
serverUrl: null,
|
|
67
68
|
mcmEnabled: null,
|
|
68
69
|
eula: false,
|
|
@@ -124,7 +125,7 @@ export default {
|
|
|
124
125
|
const me = findBy(principals, 'me', true);
|
|
125
126
|
|
|
126
127
|
const current = this.$route.query[SETUP] || this.$store.getters['auth/initialPass'];
|
|
127
|
-
const
|
|
128
|
+
const user = this.$store.getters['auth/user'] ?? {};
|
|
128
129
|
|
|
129
130
|
const mcmFeature = await this.$store.dispatch('management/find', {
|
|
130
131
|
type: MANAGEMENT.FEATURE, id: 'multi-cluster-management', opt: { url: `/v1/${ MANAGEMENT.FEATURE }/multi-cluster-management` }
|
|
@@ -149,7 +150,7 @@ export default {
|
|
|
149
150
|
this['isFirstLogin'] = isFirstLogin;
|
|
150
151
|
this['mustChangePassword'] = mustChangePassword;
|
|
151
152
|
this['current'] = current;
|
|
152
|
-
this['
|
|
153
|
+
this['user'] = user;
|
|
153
154
|
this['serverUrl'] = serverUrl;
|
|
154
155
|
this['mcmEnabled'] = mcmEnabled;
|
|
155
156
|
this['principals'] = principals;
|
|
@@ -188,6 +189,21 @@ export default {
|
|
|
188
189
|
|
|
189
190
|
showLocalhostWarning() {
|
|
190
191
|
return isLocalhost(this.serverUrl);
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
customizations() {
|
|
195
|
+
const brandMeta = getBrandMeta(this.$store.getters['management/brand']);
|
|
196
|
+
const login = brandMeta?.login || {};
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
setupLabelKey: 'setup.welcome',
|
|
200
|
+
logoClass: 'login-logo',
|
|
201
|
+
...login,
|
|
202
|
+
};
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
brandLogo() {
|
|
206
|
+
return this.customizations.logo;
|
|
191
207
|
}
|
|
192
208
|
},
|
|
193
209
|
|
|
@@ -213,19 +229,25 @@ export default {
|
|
|
213
229
|
await this.$store.dispatch('loadManagement');
|
|
214
230
|
|
|
215
231
|
if ( this.mustChangePassword ) {
|
|
216
|
-
await this.$store.dispatch('
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
232
|
+
const passwordChangeRequest = await this.$store.dispatch('management/create', { type: EXT.PASSWORD_CHANGE_REQUESTS });
|
|
233
|
+
|
|
234
|
+
if (!passwordChangeRequest?.canChangePassword) {
|
|
235
|
+
this.errors = exceptionToErrorsArray(this.t('changePassword.errors.cannotChange'));
|
|
236
|
+
throw new Error(this.t('changePassword.errors.cannotChange'));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
passwordChangeRequest.spec = {
|
|
240
|
+
currentPassword: this.current,
|
|
241
|
+
newPassword: this.password,
|
|
242
|
+
userID: this.user?.id
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
await passwordChangeRequest.save();
|
|
224
246
|
} else {
|
|
225
247
|
promises.push(setSetting(this.$store, SETTING.FIRST_LOGIN, 'false'));
|
|
226
248
|
}
|
|
227
249
|
|
|
228
|
-
const user = this.
|
|
250
|
+
const user = this.user;
|
|
229
251
|
|
|
230
252
|
user.mustChangePassword = false;
|
|
231
253
|
this.$store.dispatch('auth/gotUser', user);
|
|
@@ -278,8 +300,18 @@ export default {
|
|
|
278
300
|
|
|
279
301
|
</div>
|
|
280
302
|
<div>
|
|
303
|
+
<div
|
|
304
|
+
v-if="brandLogo"
|
|
305
|
+
class="brand-logo"
|
|
306
|
+
>
|
|
307
|
+
<BrandImage
|
|
308
|
+
:class="{[customizations.logoClass]: !!customizations.logoClass}"
|
|
309
|
+
:file-name="brandLogo"
|
|
310
|
+
:alt="t('setup.setup')"
|
|
311
|
+
/>
|
|
312
|
+
</div>
|
|
281
313
|
<h1 class="text-center">
|
|
282
|
-
{{ t(
|
|
314
|
+
{{ t(customizations.setupLabelKey, {product}) }}
|
|
283
315
|
</h1>
|
|
284
316
|
|
|
285
317
|
<template v-if="mustChangePassword">
|
|
@@ -467,6 +499,11 @@ export default {
|
|
|
467
499
|
.setup {
|
|
468
500
|
overflow: hidden;
|
|
469
501
|
|
|
502
|
+
.brand-logo {
|
|
503
|
+
display: flex;
|
|
504
|
+
justify-content: center;
|
|
505
|
+
}
|
|
506
|
+
|
|
470
507
|
.row {
|
|
471
508
|
& .checkbox {
|
|
472
509
|
margin: auto
|
|
@@ -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>
|
package/pages/home.vue
CHANGED
|
@@ -10,7 +10,7 @@ import SingleClusterInfo from '@shell/components/SingleClusterInfo.vue';
|
|
|
10
10
|
import DynamicContentBanner from '@shell/components/DynamicContent/DynamicContentBanner.vue';
|
|
11
11
|
import DynamicContentPanel from '@shell/components/DynamicContent/DynamicContentPanel.vue';
|
|
12
12
|
import { mapGetters, mapState } from 'vuex';
|
|
13
|
-
import { MANAGEMENT, CAPI, COUNT } from '@shell/config/types';
|
|
13
|
+
import { MANAGEMENT, CAPI, COUNT, SAVED_COUNTS } from '@shell/config/types';
|
|
14
14
|
import { NAME as MANAGER } from '@shell/config/product/manager';
|
|
15
15
|
import { AGE, STATE } from '@shell/config/table-headers';
|
|
16
16
|
import { MODE, _IMPORT } from '@shell/config/query-params';
|
|
@@ -256,8 +256,14 @@ export default defineComponent({
|
|
|
256
256
|
*/
|
|
257
257
|
altClusterList() {
|
|
258
258
|
return this.tooManyClusters && !this.altClusterListDisabled;
|
|
259
|
-
}
|
|
259
|
+
},
|
|
260
260
|
|
|
261
|
+
clusterCountDisplay() {
|
|
262
|
+
// If we have the cluster count from the store, use that instead
|
|
263
|
+
const savedCount = this.$store.getters['management/getSavedCount'](SAVED_COUNTS.K8S_CLUSTERS);
|
|
264
|
+
|
|
265
|
+
return typeof savedCount !== 'undefined' ? savedCount : this.clusterCount;
|
|
266
|
+
}
|
|
261
267
|
},
|
|
262
268
|
|
|
263
269
|
watch: {
|
|
@@ -806,7 +812,7 @@ export default defineComponent({
|
|
|
806
812
|
</h1>
|
|
807
813
|
<BadgeState
|
|
808
814
|
v-if="clusterCount && !tooManyClusters"
|
|
809
|
-
:label="
|
|
815
|
+
:label="clusterCountDisplay.toString()"
|
|
810
816
|
color="bg-info ml-20 mr-20"
|
|
811
817
|
/>
|
|
812
818
|
</div>
|
|
@@ -397,9 +397,7 @@ describe('class: Resource', () => {
|
|
|
397
397
|
|
|
398
398
|
const cards = resource.cards;
|
|
399
399
|
|
|
400
|
-
expect(cards).toHaveLength(
|
|
401
|
-
expect(cards[0]).toHaveProperty('component');
|
|
402
|
-
expect(cards[0]).toHaveProperty('props');
|
|
400
|
+
expect(cards).toHaveLength(0);
|
|
403
401
|
});
|
|
404
402
|
});
|
|
405
403
|
|
|
@@ -515,6 +515,13 @@ export default {
|
|
|
515
515
|
});
|
|
516
516
|
}
|
|
517
517
|
|
|
518
|
+
if (opt.saveCountAs) {
|
|
519
|
+
commit('setSavedCount', {
|
|
520
|
+
name: opt.saveCountAs,
|
|
521
|
+
count: out.count,
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
518
525
|
if ( !opt.transient && opt.watch !== false ) {
|
|
519
526
|
dispatch('watch', watchArgs);
|
|
520
527
|
}
|
|
@@ -351,10 +351,22 @@ export default {
|
|
|
351
351
|
return out;
|
|
352
352
|
},
|
|
353
353
|
|
|
354
|
+
/**
|
|
355
|
+
* Can the user GET a resource of the given type
|
|
356
|
+
*/
|
|
357
|
+
canGet: (state, getters) => (type) => {
|
|
358
|
+
const schema = getters.schemaFor(type);
|
|
359
|
+
|
|
360
|
+
return schema?.canGet;
|
|
361
|
+
},
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Can the user LIST a resource of the given type
|
|
365
|
+
*/
|
|
354
366
|
canList: (state, getters) => (type) => {
|
|
355
367
|
const schema = getters.schemaFor(type);
|
|
356
368
|
|
|
357
|
-
return schema
|
|
369
|
+
return schema?.canList;
|
|
358
370
|
},
|
|
359
371
|
|
|
360
372
|
typeRegistered: (state, getters) => (type) => {
|
|
@@ -559,4 +571,14 @@ export default {
|
|
|
559
571
|
* Can be used to change behaviour given steve cache api functionality
|
|
560
572
|
*/
|
|
561
573
|
isSteveCacheUrl: (state) => () => false,
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Get the saved count for the given name
|
|
577
|
+
*
|
|
578
|
+
* @param {string} name Name of the saved count
|
|
579
|
+
* @returns {number|undefined} The saved count or undefined if not found
|
|
580
|
+
*/
|
|
581
|
+
getSavedCount: (state) => (name) => {
|
|
582
|
+
return state.savedCounts[name];
|
|
583
|
+
}
|
|
562
584
|
};
|
|
@@ -23,8 +23,9 @@ export const coreStoreState = (namespace, baseUrl, isClusterStore) => ({
|
|
|
23
23
|
namespace,
|
|
24
24
|
isClusterStore
|
|
25
25
|
},
|
|
26
|
-
types:
|
|
27
|
-
|
|
26
|
+
types: {},
|
|
27
|
+
savedCounts: {}, // Saved counts for resource types (from paginated API called where marked)
|
|
28
|
+
$ctx: markRaw({}),
|
|
28
29
|
});
|
|
29
30
|
|
|
30
31
|
export default (vuexModule, config, init) => {
|
|
@@ -1284,9 +1284,15 @@ export default class Resource {
|
|
|
1284
1284
|
|
|
1285
1285
|
// Steve sometimes returns Table responses instead of the resource you just saved.. ignore
|
|
1286
1286
|
if ( res && res.kind !== 'Table') {
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1287
|
+
const keyField = this.$getters.keyFieldForType(this.type);
|
|
1288
|
+
const id = res[keyField];
|
|
1289
|
+
|
|
1290
|
+
// only items with ID will be added to the store, this prevents "new" resources that return an empty body OR no ID from being added to the store with an ID of "undefined"
|
|
1291
|
+
if (id) {
|
|
1292
|
+
await this.$dispatch('load', {
|
|
1293
|
+
data: res, existing: (forNew ? this : undefined ), invalidatePageCache
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1290
1296
|
}
|
|
1291
1297
|
} catch (e) {
|
|
1292
1298
|
if ( this.type && this.id && e?._status === 409) {
|
|
@@ -2164,7 +2170,7 @@ export default class Resource {
|
|
|
2164
2170
|
get insightCardProps() {
|
|
2165
2171
|
const rows = [
|
|
2166
2172
|
useResourceCardRow(this.t('component.resource.detail.card.insightsCard.rows.conditions'), this.resourceConditions, undefined, 'condition', '#conditions'),
|
|
2167
|
-
useResourceCardRow(this.t('component.resource.detail.card.insightsCard.rows.events'), this.resourceEvents,
|
|
2173
|
+
useResourceCardRow(this.t('component.resource.detail.card.insightsCard.rows.events'), this.resourceEvents, 'insightsColor', 'eventType', '#events'),
|
|
2168
2174
|
];
|
|
2169
2175
|
|
|
2170
2176
|
return {
|
|
@@ -2181,7 +2187,8 @@ export default class Resource {
|
|
|
2181
2187
|
}
|
|
2182
2188
|
|
|
2183
2189
|
get _cards() {
|
|
2184
|
-
|
|
2190
|
+
// All cards are opt in, we're leaving the insights card as part of the base resource since it should proliferate to most resources
|
|
2191
|
+
return [];
|
|
2185
2192
|
}
|
|
2186
2193
|
|
|
2187
2194
|
get cards() {
|
|
@@ -56,5 +56,172 @@ describe('class: Steve', () => {
|
|
|
56
56
|
expect({ ...steve }).toStrictEqual(customResource);
|
|
57
57
|
});
|
|
58
58
|
});
|
|
59
|
+
|
|
60
|
+
describe('method: processSaveResponse', () => {
|
|
61
|
+
it('should call parent processSaveResponse', () => {
|
|
62
|
+
const mockDispatch = jest.fn();
|
|
63
|
+
const mockRootGetters = { 'i18n/t': jest.fn().mockReturnValue('Resource created: test-id') };
|
|
64
|
+
const steve = new Steve(customResource, {
|
|
65
|
+
getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
|
|
66
|
+
dispatch: mockDispatch,
|
|
67
|
+
rootGetters: mockRootGetters,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Mock the parent processSaveResponse method
|
|
71
|
+
const parentProcessSaveResponse = jest.spyOn(Object.getPrototypeOf(Object.getPrototypeOf(steve)), 'processSaveResponse');
|
|
72
|
+
|
|
73
|
+
const response = { _status: 200 };
|
|
74
|
+
|
|
75
|
+
steve.processSaveResponse(response);
|
|
76
|
+
|
|
77
|
+
expect(parentProcessSaveResponse).toHaveBeenCalledWith(response);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should show growl notification for autogenerated names on 201 status', () => {
|
|
81
|
+
const mockDispatch = jest.fn();
|
|
82
|
+
const mockT = jest.fn()
|
|
83
|
+
.mockReturnValueOnce('CustomResourceDefinition')
|
|
84
|
+
.mockReturnValueOnce('CustomResourceDefinition created')
|
|
85
|
+
.mockReturnValueOnce('Resource test-generated-abc123 created successfully');
|
|
86
|
+
const mockRootGetters = { 'i18n/t': mockT };
|
|
87
|
+
const steve = new Steve(customResource, {
|
|
88
|
+
getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
|
|
89
|
+
dispatch: mockDispatch,
|
|
90
|
+
rootGetters: mockRootGetters,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const response = {
|
|
94
|
+
_status: 201,
|
|
95
|
+
metadata: { generateName: 'test-generated-' },
|
|
96
|
+
id: 'default/test-generated-abc123'
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
steve.processSaveResponse(response);
|
|
100
|
+
|
|
101
|
+
expect(mockT).toHaveBeenCalledWith(`typeLabel."${ customResource.type }"`, { count: 1 });
|
|
102
|
+
expect(mockT).toHaveBeenCalledWith('generic.autogeneratedCreated.title', { resource: 'CustomResourceDefinition' });
|
|
103
|
+
expect(mockT).toHaveBeenCalledWith('generic.autogeneratedCreated.message', { id: 'test-generated-abc123' });
|
|
104
|
+
expect(mockDispatch).toHaveBeenCalledWith(
|
|
105
|
+
'growl/success',
|
|
106
|
+
{
|
|
107
|
+
title: 'CustomResourceDefinition created',
|
|
108
|
+
message: 'Resource test-generated-abc123 created successfully',
|
|
109
|
+
timeout: 3000
|
|
110
|
+
},
|
|
111
|
+
{ root: true }
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should show growl notification for autogenerated names without namespace', () => {
|
|
116
|
+
const mockDispatch = jest.fn();
|
|
117
|
+
const mockT = jest.fn()
|
|
118
|
+
.mockReturnValueOnce('CustomResourceDefinition')
|
|
119
|
+
.mockReturnValueOnce('CustomResourceDefinition created')
|
|
120
|
+
.mockReturnValueOnce('Resource simple-id created successfully');
|
|
121
|
+
const mockRootGetters = { 'i18n/t': mockT };
|
|
122
|
+
const steve = new Steve(customResource, {
|
|
123
|
+
getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
|
|
124
|
+
dispatch: mockDispatch,
|
|
125
|
+
rootGetters: mockRootGetters,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const response = {
|
|
129
|
+
_status: 201,
|
|
130
|
+
metadata: { generateName: 'simple-' },
|
|
131
|
+
id: 'simple-id'
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
steve.processSaveResponse(response);
|
|
135
|
+
|
|
136
|
+
expect(mockT).toHaveBeenCalledWith(`typeLabel."${ customResource.type }"`, { count: 1 });
|
|
137
|
+
expect(mockT).toHaveBeenCalledWith('generic.autogeneratedCreated.title', { resource: 'CustomResourceDefinition' });
|
|
138
|
+
expect(mockT).toHaveBeenCalledWith('generic.autogeneratedCreated.message', { id: 'simple-id' });
|
|
139
|
+
expect(mockDispatch).toHaveBeenCalledWith(
|
|
140
|
+
'growl/success',
|
|
141
|
+
{
|
|
142
|
+
title: 'CustomResourceDefinition created',
|
|
143
|
+
message: 'Resource simple-id created successfully',
|
|
144
|
+
timeout: 3000
|
|
145
|
+
},
|
|
146
|
+
{ root: true }
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should not show growl notification for non-201 status', () => {
|
|
151
|
+
const mockDispatch = jest.fn();
|
|
152
|
+
const mockT = jest.fn();
|
|
153
|
+
const mockRootGetters = { 'i18n/t': mockT };
|
|
154
|
+
const steve = new Steve(customResource, {
|
|
155
|
+
getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
|
|
156
|
+
dispatch: mockDispatch,
|
|
157
|
+
rootGetters: mockRootGetters,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const response = {
|
|
161
|
+
_status: 200,
|
|
162
|
+
metadata: { generateName: 'test-generated-' },
|
|
163
|
+
id: 'default/test-generated-abc123'
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
steve.processSaveResponse(response);
|
|
167
|
+
|
|
168
|
+
expect(mockT).not.toHaveBeenCalled();
|
|
169
|
+
expect(mockDispatch).not.toHaveBeenCalledWith(
|
|
170
|
+
'growl/success',
|
|
171
|
+
expect.any(Object),
|
|
172
|
+
{ root: true }
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should not show growl notification without generateName', () => {
|
|
177
|
+
const mockDispatch = jest.fn();
|
|
178
|
+
const mockT = jest.fn();
|
|
179
|
+
const mockRootGetters = { 'i18n/t': mockT };
|
|
180
|
+
const steve = new Steve(customResource, {
|
|
181
|
+
getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
|
|
182
|
+
dispatch: mockDispatch,
|
|
183
|
+
rootGetters: mockRootGetters,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const response = {
|
|
187
|
+
_status: 201,
|
|
188
|
+
id: 'default/test-regular-name'
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
steve.processSaveResponse(response);
|
|
192
|
+
|
|
193
|
+
expect(mockT).not.toHaveBeenCalled();
|
|
194
|
+
expect(mockDispatch).not.toHaveBeenCalledWith(
|
|
195
|
+
'growl/success',
|
|
196
|
+
expect.any(Object),
|
|
197
|
+
{ root: true }
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should not show growl notification without id', () => {
|
|
202
|
+
const mockDispatch = jest.fn();
|
|
203
|
+
const mockT = jest.fn();
|
|
204
|
+
const mockRootGetters = { 'i18n/t': mockT };
|
|
205
|
+
const steve = new Steve(customResource, {
|
|
206
|
+
getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
|
|
207
|
+
dispatch: mockDispatch,
|
|
208
|
+
rootGetters: mockRootGetters,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const response = {
|
|
212
|
+
_status: 201,
|
|
213
|
+
metadata: { generateName: 'test-generated-' }
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
steve.processSaveResponse(response);
|
|
217
|
+
|
|
218
|
+
expect(mockT).not.toHaveBeenCalled();
|
|
219
|
+
expect(mockDispatch).not.toHaveBeenCalledWith(
|
|
220
|
+
'growl/success',
|
|
221
|
+
expect.any(Object),
|
|
222
|
+
{ root: true }
|
|
223
|
+
);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
59
226
|
});
|
|
60
227
|
});
|
|
@@ -8,12 +8,17 @@ export interface SchemaAttributeColumn {
|
|
|
8
8
|
type: string,
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
export type SchemaAttributeVerbs = 'get' | 'patch' | 'list' | 'update'
|
|
12
|
+
|
|
11
13
|
export interface SchemaAttribute {
|
|
12
14
|
columns: SchemaAttributeColumn[],
|
|
13
15
|
namespaced: boolean
|
|
16
|
+
verbs: SchemaAttributeVerbs[]
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
/**
|
|
20
|
+
* Interface for a steve schema, represents raw json
|
|
21
|
+
*
|
|
17
22
|
* At some point this will be properly typed, until then...
|
|
18
23
|
*/
|
|
19
24
|
export interface Schema {
|
|
@@ -63,4 +63,23 @@ export default class SteveModel extends HybridModel {
|
|
|
63
63
|
paginationEnabled() {
|
|
64
64
|
return this.$getters['paginationEnabled'](this.type);
|
|
65
65
|
}
|
|
66
|
+
|
|
67
|
+
processSaveResponse(res) {
|
|
68
|
+
super.processSaveResponse(res);
|
|
69
|
+
|
|
70
|
+
// Conditionally show the growl for autogenerated names
|
|
71
|
+
if (res && res._status === 201 && res.metadata?.generateName && res.id) {
|
|
72
|
+
// Split to remove the namespace if present (default/generated-xxx)
|
|
73
|
+
const nameOnly = res.id.split('/').pop();
|
|
74
|
+
|
|
75
|
+
// Avoid showing the growl without the ID.
|
|
76
|
+
if (nameOnly.length > 0) {
|
|
77
|
+
this.$dispatch('growl/success', {
|
|
78
|
+
title: this.t('generic.autogeneratedCreated.title', { resource: this.t(`typeLabel."${ this.type }"`, { count: 1 }) }),
|
|
79
|
+
message: this.t('generic.autogeneratedCreated.message', { id: nameOnly }),
|
|
80
|
+
timeout: 3000
|
|
81
|
+
}, { root: true });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
66
85
|
}
|
|
@@ -764,7 +764,8 @@ export const PAGINATION_SETTINGS_STORE_DEFAULTS: PaginationSettingsStores = {
|
|
|
764
764
|
{ resource: CAPI.RANCHER_CLUSTER, context: ['side-bar'] },
|
|
765
765
|
{ resource: MANAGEMENT.CLUSTER, context: ['side-bar'] },
|
|
766
766
|
{ resource: CATALOG.APP, context: ['branding'] },
|
|
767
|
-
SECRET
|
|
767
|
+
SECRET,
|
|
768
|
+
CAPI.MACHINE_SET
|
|
768
769
|
],
|
|
769
770
|
generic: false,
|
|
770
771
|
}
|
|
@@ -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
|
|