@rancher/shell 3.0.10 → 3.0.12-rc.1
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/_mixins.scss +31 -0
- package/assets/styles/base/_variables.scss +2 -0
- package/assets/styles/themes/_modern.scss +6 -5
- package/assets/translations/en-us.yaml +12 -9
- package/assets/translations/zh-hans.yaml +0 -3
- 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/DynamicContent/DynamicContentIcon.vue +3 -2
- package/components/EmptyProductPage.vue +76 -0
- package/components/ExplorerProjectsNamespaces.vue +1 -4
- package/components/LazyImage.vue +2 -1
- package/components/Resource/Detail/Card/Scaler.vue +4 -4
- package/components/Resource/Detail/CopyToClipboard.vue +1 -2
- package/components/Resource/Detail/Metadata/KeyValueRow.vue +9 -3
- package/components/Resource/Detail/TitleBar/__tests__/__snapshots__/index.test.ts.snap +31 -0
- package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +45 -1
- package/components/Resource/Detail/TitleBar/index.vue +1 -1
- package/components/Resource/Detail/ViewOptions/__tests__/__snapshots__/index.test.ts.snap +9 -0
- package/components/Resource/Detail/ViewOptions/__tests__/index.test.ts +62 -0
- package/components/Resource/Detail/ViewOptions/index.vue +2 -1
- package/components/ResourceList/Masthead.vue +25 -2
- package/components/SideNav.vue +13 -0
- package/components/Tabbed/index.vue +6 -0
- package/components/__tests__/ConsumptionGauge.test.ts +31 -0
- package/components/__tests__/PromptModal.test.ts +2 -0
- package/components/fleet/FleetClusters.vue +1 -0
- package/components/fleet/__tests__/FleetClusters.test.ts +71 -0
- package/components/form/NodeScheduling.vue +17 -3
- package/components/form/PrivateRegistry.vue +69 -0
- package/components/form/ProjectMemberEditor.vue +0 -10
- package/components/form/__tests__/PrivateRegistry.test.ts +133 -0
- package/components/formatter/WorkloadHealthScale.vue +3 -1
- package/components/nav/Group.vue +26 -3
- package/components/nav/Header.vue +32 -7
- package/components/nav/TopLevelMenu.helper.ts +7 -79
- package/components/nav/TopLevelMenu.vue +15 -1
- package/components/nav/__tests__/TopLevelMenu.helper.test.ts +2 -53
- package/config/pagination-table-headers.js +8 -1
- package/config/private-label.js +2 -1
- package/config/product/apps.js +3 -1
- package/config/product/auth.js +1 -0
- package/config/product/backup.js +1 -0
- package/config/product/compliance.js +1 -1
- package/config/product/explorer.js +25 -6
- package/config/product/fleet.js +1 -0
- package/config/product/gatekeeper.js +1 -0
- package/config/product/istio.js +1 -0
- package/config/product/logging.js +1 -0
- package/config/product/longhorn.js +2 -1
- package/config/product/manager.js +1 -0
- package/config/product/monitoring.js +1 -0
- package/config/product/navlinks.js +1 -0
- package/config/product/neuvector.js +2 -1
- package/config/product/settings.js +1 -0
- package/config/product/uiplugins.js +1 -0
- package/core/__tests__/extension-manager-impl.test.js +187 -2
- package/core/__tests__/plugin-products-helpers.test.ts +454 -0
- package/core/__tests__/plugin-products.test.ts +3219 -0
- package/core/extension-manager-impl.js +34 -3
- package/core/plugin-helpers.ts +31 -0
- package/core/plugin-products-base.ts +375 -0
- package/core/plugin-products-extending.ts +44 -0
- package/core/plugin-products-helpers.ts +262 -0
- package/core/plugin-products-top-level.ts +66 -0
- package/core/plugin-products-type-guards.ts +33 -0
- package/core/plugin-products.ts +50 -0
- package/core/plugin-types.ts +222 -0
- package/core/plugin.ts +45 -10
- package/core/productDebugger.js +48 -0
- package/core/types.ts +95 -11
- package/detail/__tests__/__snapshots__/fleet.cattle.io.bundle.test.ts.snap +52 -0
- package/detail/__tests__/fleet.cattle.io.bundle.test.ts +171 -0
- package/detail/__tests__/node.test.ts +83 -0
- package/detail/fleet.cattle.io.bundle.vue +21 -34
- package/detail/management.cattle.io.oidcclient.vue +2 -1
- package/detail/node.vue +1 -0
- package/dialog/ExtensionCatalogInstallDialog.vue +1 -1
- package/dialog/InstallExtensionDialog.vue +6 -27
- package/dialog/UninstallExistingExtensionDialog.vue +141 -0
- package/dialog/UninstallExtensionDialog.vue +4 -26
- package/dialog/__tests__/UninstallExistingExtensionDialog.test.ts +114 -0
- package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +1 -0
- package/edit/catalog.cattle.io.clusterrepo.vue +17 -3
- package/edit/cloudcredential.vue +2 -1
- package/edit/monitoring.coreos.com.alertmanagerconfig/receiverConfig.vue +11 -6
- package/edit/provisioning.cattle.io.cluster/__tests__/Ingress.test.ts +176 -0
- package/edit/provisioning.cattle.io.cluster/index.vue +5 -4
- package/edit/provisioning.cattle.io.cluster/rke2.vue +4 -1
- package/edit/provisioning.cattle.io.cluster/shared.ts +4 -2
- package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +6 -0
- package/edit/provisioning.cattle.io.cluster/tabs/Ingress.vue +7 -2
- package/edit/secret/generic.vue +1 -0
- package/edit/secret/index.vue +2 -1
- package/edit/service.vue +2 -14
- package/list/management.cattle.io.feature.vue +7 -1
- package/list/provisioning.cattle.io.cluster.vue +0 -50
- package/list/workload.vue +11 -4
- package/mixins/brand.js +2 -1
- package/mixins/resource-fetch.js +12 -3
- 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/pod.js +18 -0
- package/models/provisioning.cattle.io.cluster.js +2 -2
- package/models/workload.js +20 -2
- package/package.json +5 -6
- package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +0 -1
- package/pages/c/_cluster/apps/charts/index.vue +3 -8
- package/pages/c/_cluster/apps/charts/install.vue +8 -9
- 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/brand.vue +4 -4
- package/pages/c/_cluster/settings/performance.vue +0 -5
- package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +2 -1
- package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +231 -13
- package/pages/c/_cluster/uiplugins/index.vue +145 -38
- package/plugins/dashboard-store/__tests__/resource-class.test.ts +1 -0
- package/plugins/dashboard-store/actions.js +3 -2
- package/plugins/dashboard-store/resource-class.js +62 -6
- package/plugins/plugin.js +16 -0
- package/plugins/steve/steve-pagination-utils.ts +8 -2
- package/plugins/steve/subscribe.js +29 -4
- 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/scripts/test-plugins-build.sh +4 -4
- package/scripts/typegen.sh +13 -1
- package/store/__tests__/type-map.test.ts +84 -24
- package/store/type-map.js +42 -3
- package/tsconfig.paths.json +1 -0
- package/types/resources/pod.ts +18 -0
- package/types/shell/index.d.ts +8506 -2908
- package/types/store/dashboard-store.types.ts +5 -0
- package/types/store/pagination.types.ts +6 -0
- package/utils/__tests__/require-asset.test.ts +98 -0
- package/utils/async.ts +1 -5
- package/utils/axios.js +1 -4
- package/utils/brand.ts +3 -1
- package/utils/dynamic-importer.js +3 -2
- package/utils/favicon.js +4 -3
- package/utils/pagination-utils.ts +1 -1
- package/utils/require-asset.ts +95 -0
- package/utils/uiplugins.ts +12 -16
- package/utils/validators/__tests__/private-registry.test.ts +76 -0
- package/utils/validators/private-registry.ts +28 -0
- package/vue.config.js +4 -3
- package/components/HarvesterServiceAddOnConfig.vue +0 -207
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
export default {
|
|
3
|
+
name: 'EmptyProductPage',
|
|
4
|
+
layout: 'plain',
|
|
5
|
+
data() {
|
|
6
|
+
const err = this.$route.meta?.pageError;
|
|
7
|
+
|
|
8
|
+
let msg;
|
|
9
|
+
|
|
10
|
+
switch (err) {
|
|
11
|
+
case 'no-nav':
|
|
12
|
+
msg = [
|
|
13
|
+
'When a component is not provided for a product, the layout with side navigation is used',
|
|
14
|
+
'No child items were specified, so this "Default" empty view has been added',
|
|
15
|
+
'Please add child items to this product'
|
|
16
|
+
];
|
|
17
|
+
break;
|
|
18
|
+
default:
|
|
19
|
+
msg = ['No component defined for this extension product... Define a component or child pages so it can be rendered here.'];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
img: require('~shell/assets/images/generic-plugin.svg'),
|
|
24
|
+
msg,
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<div class="empty-product-page">
|
|
32
|
+
<img
|
|
33
|
+
:src="img"
|
|
34
|
+
alt="Extension Product Error"
|
|
35
|
+
>
|
|
36
|
+
<div class="err-messages">
|
|
37
|
+
<p
|
|
38
|
+
v-for="(m, index) in msg"
|
|
39
|
+
:key="index"
|
|
40
|
+
>
|
|
41
|
+
{{ m }}
|
|
42
|
+
</p>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</template>
|
|
46
|
+
|
|
47
|
+
<style lang="scss" scoped>
|
|
48
|
+
.empty-product-page {
|
|
49
|
+
align-items: center;
|
|
50
|
+
display: flex;
|
|
51
|
+
justify-content: center;
|
|
52
|
+
opacity: 0.75;
|
|
53
|
+
|
|
54
|
+
> img {
|
|
55
|
+
width: 128px;
|
|
56
|
+
margin-bottom: 20px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.err-messages {
|
|
60
|
+
display: flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
text-align: center;
|
|
63
|
+
flex-direction: column;
|
|
64
|
+
|
|
65
|
+
> * {
|
|
66
|
+
margin-bottom: 8px;
|
|
67
|
+
font-size: 16px;
|
|
68
|
+
|
|
69
|
+
&:last-child {
|
|
70
|
+
font-weight: bold;
|
|
71
|
+
color: var(--error);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
</style>
|
|
@@ -593,15 +593,12 @@ export default {
|
|
|
593
593
|
|
|
594
594
|
.project-namespaces {
|
|
595
595
|
& :deep() {
|
|
596
|
-
.project-namespaces-table table {
|
|
597
|
-
table-layout: fixed;
|
|
598
|
-
}
|
|
599
|
-
|
|
600
596
|
.project-name {
|
|
601
597
|
line-height: 30px;
|
|
602
598
|
}
|
|
603
599
|
|
|
604
600
|
.project-bar {
|
|
601
|
+
contain: inline-size;
|
|
605
602
|
display: flex;
|
|
606
603
|
flex-direction: row;
|
|
607
604
|
justify-content: space-between;
|
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;
|
|
@@ -70,8 +70,7 @@ const onClick = (ev: MouseEvent) => {
|
|
|
70
70
|
border-color: var(--success-border);
|
|
71
71
|
color: var(--success-text);
|
|
72
72
|
|
|
73
|
-
transition:
|
|
74
|
-
transition-timing-function: ease;
|
|
73
|
+
transition: background-color 0.25s ease, border-color 0.25s ease, color 0.25s ease;
|
|
75
74
|
}
|
|
76
75
|
|
|
77
76
|
&:focus-visible {
|
|
@@ -77,12 +77,15 @@ const previewId = randomStr();
|
|
|
77
77
|
position: relative;
|
|
78
78
|
padding: 0;
|
|
79
79
|
|
|
80
|
+
$topShift: -6px;
|
|
81
|
+
$topShiftHidden: -100vh; //100% of the viewport
|
|
82
|
+
|
|
80
83
|
.copy-to-clipboard {
|
|
81
84
|
position: fixed;
|
|
82
85
|
|
|
83
86
|
right: -20px;
|
|
84
|
-
top:
|
|
85
|
-
z-index:
|
|
87
|
+
top: $topShiftHidden;
|
|
88
|
+
z-index: z-index('copyToClipboard');
|
|
86
89
|
}
|
|
87
90
|
|
|
88
91
|
&, .btn, .rc-tag {
|
|
@@ -126,13 +129,16 @@ const previewId = randomStr();
|
|
|
126
129
|
}
|
|
127
130
|
|
|
128
131
|
& + .copy-to-clipboard {
|
|
132
|
+
// This is how we "hide" the component but still allow it to be visible to accessibility (tab focus)
|
|
129
133
|
position: absolute;
|
|
134
|
+
top: $topShift;
|
|
130
135
|
}
|
|
131
136
|
}
|
|
132
137
|
|
|
133
138
|
.copy-to-clipboard:focus-visible, .copy-to-clipboard:hover {
|
|
139
|
+
// This is how we "hide" the component but still allow it to be visible to accessibility (tab focus)
|
|
134
140
|
position: absolute;
|
|
135
|
-
|
|
141
|
+
top: $topShift;
|
|
136
142
|
}
|
|
137
143
|
|
|
138
144
|
.btn:has(+ .copy-to-clipboard:focus-visible), .btn:has(+ .copy-to-clipboard:hover) {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`component: TitleBar/index should match the full component snapshot 1`] = `
|
|
4
|
+
<div class="title-bar">
|
|
5
|
+
<div class="top">
|
|
6
|
+
<h1 class="title">
|
|
7
|
+
<!----><a class="resource-link">RESOURCE_TYPE_LABEL: </a><span class="resource-name masthead-resource-title">RESOURCE_NAME</span><span class="badge-state bg-success badge-state"><!--v-if--><span class="msg">Active</span></span>
|
|
8
|
+
</h1>
|
|
9
|
+
<div class="actions"><button role="button" class="rc-button btn variant-primary btn-medium">
|
|
10
|
+
<!--v-if-->Deploy
|
|
11
|
+
<!--v-if-->
|
|
12
|
+
</button><button role="button" class="rc-button btn variant-secondary btn-large">
|
|
13
|
+
<!--v-if-->Rollback
|
|
14
|
+
<!--v-if-->
|
|
15
|
+
</button><button role="button" class="rc-button btn variant-primary btn-large show-configuration" data-testid="show-configuration-cta" aria-label="component.resource.detail.titleBar.ariaLabel.showConfiguration-{"resource":"RESOURCE_NAME"}">
|
|
16
|
+
<!--v-if--><i class="icon icon-document" aria-hidden="true"></i> component.resource.detail.titleBar.showConfiguration
|
|
17
|
+
<!--v-if-->
|
|
18
|
+
</button>
|
|
19
|
+
<div class="v-popper v-popper--theme-dropdown"><button role="button" class="rc-button btn variant-multi-action btn-medium" aria-haspopup="menu" aria-expanded="false" data-testid="masthead-action-menu" aria-label="component.resource.detail.titleBar.ariaLabel.actionMenu-{"resource":"RESOURCE_NAME"}">
|
|
20
|
+
<!--v-if--><i class="icon icon-actions"></i>
|
|
21
|
+
<!--v-if-->
|
|
22
|
+
</button></div>
|
|
23
|
+
<div class="popperContainer">
|
|
24
|
+
<!--Empty container for mounting popper content-->
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="bottom description text-deemphasized">A test description</div>
|
|
29
|
+
<!--v-if-->
|
|
30
|
+
</div>
|
|
31
|
+
`;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { mount, RouterLinkStub } from '@vue/test-utils';
|
|
2
|
-
import TitleBar from '@shell/components/Resource/Detail/TitleBar/index.vue';
|
|
2
|
+
import TitleBar, { AdditionalActionButton } from '@shell/components/Resource/Detail/TitleBar/index.vue';
|
|
3
3
|
import ActionMenu from '@shell/components/ActionMenuShell.vue';
|
|
4
4
|
import { createStore } from 'vuex';
|
|
5
5
|
import { defineComponent, h } from 'vue';
|
|
@@ -240,5 +240,49 @@ describe('component: TitleBar/index', () => {
|
|
|
240
240
|
expect(wrapper.find('.slot-button').exists()).toBeTruthy();
|
|
241
241
|
expect(wrapper.find('.slot-button').text()).toBe('Slot Button');
|
|
242
242
|
});
|
|
243
|
+
|
|
244
|
+
it('should render the actions container correctly when additional-actions slot contains nested buttons', async() => {
|
|
245
|
+
const wrapper = mount(TitleBar, {
|
|
246
|
+
props: { resourceTypeLabel, resourceName },
|
|
247
|
+
slots: { 'additional-actions': '<div class="btn-group"><button class="nested-btn">A</button><button class="nested-btn">B</button></div>' },
|
|
248
|
+
global: { stubs: { 'router-link': RouterLinkStub }, provide: { store } }
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const actions = wrapper.find('.actions');
|
|
252
|
+
|
|
253
|
+
expect(actions.find('.btn-group').exists()).toBeTruthy();
|
|
254
|
+
expect(actions.findAll('.btn-group .nested-btn')).toHaveLength(2);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should match the full component snapshot', () => {
|
|
259
|
+
const additionalActions: AdditionalActionButton[] = [
|
|
260
|
+
{
|
|
261
|
+
label: 'Deploy', variant: 'primary', onClick: jest.fn()
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
label: 'Rollback', variant: 'secondary', size: 'large', onClick: jest.fn()
|
|
265
|
+
},
|
|
266
|
+
];
|
|
267
|
+
|
|
268
|
+
const wrapper = mount(TitleBar, {
|
|
269
|
+
props: {
|
|
270
|
+
resource: {},
|
|
271
|
+
resourceTypeLabel,
|
|
272
|
+
resourceName,
|
|
273
|
+
resourceTo,
|
|
274
|
+
description: 'A test description',
|
|
275
|
+
badge: { color: 'bg-success', label: 'Active' },
|
|
276
|
+
additionalActions,
|
|
277
|
+
actionMenuResource: { resource: 'test-menu' },
|
|
278
|
+
onShowConfiguration() {},
|
|
279
|
+
},
|
|
280
|
+
global: {
|
|
281
|
+
stubs: { 'router-link': RouterLinkStub },
|
|
282
|
+
provide: { store }
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
243
287
|
});
|
|
244
288
|
});
|
|
@@ -178,7 +178,7 @@ const showAdditionalActionButtons = computed(() => isArray(additionalActions));
|
|
|
178
178
|
align-items: center;
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
.show-configuration, &:deep() .actions button {
|
|
181
|
+
.show-configuration, &:deep() .actions > button {
|
|
182
182
|
margin-left: 16px;
|
|
183
183
|
}
|
|
184
184
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`component: ViewOptions/index should match the snapshot 1`] = `
|
|
4
|
+
<div class="btn-group"><button data-testid="button-group-child-0" type="button" class="btn bg-primary" role="button" aria-label="%resourceDetail.masthead.detail%" aria-pressed="true">
|
|
5
|
+
<!--v-if--><span k="resourceDetail.masthead.detail"></span>
|
|
6
|
+
</button><button data-testid="button-group-child-1" type="button" class="btn bg-disabled" role="button" aria-label="%resourceDetail.masthead.graph%" aria-pressed="false">
|
|
7
|
+
<!--v-if--><span k="resourceDetail.masthead.graph"></span>
|
|
8
|
+
</button></div>
|
|
9
|
+
`;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils';
|
|
2
|
+
import ViewOptions from '@shell/components/Resource/Detail/ViewOptions/index.vue';
|
|
3
|
+
import ButtonGroup from '@shell/components/ButtonGroup.vue';
|
|
4
|
+
import { _CONFIG, _GRAPH } from '@shell/config/query-params';
|
|
5
|
+
|
|
6
|
+
const mockPush = jest.fn();
|
|
7
|
+
const mockQuery = { view: _CONFIG };
|
|
8
|
+
|
|
9
|
+
jest.mock('vue-router', () => ({
|
|
10
|
+
useRouter: () => ({ push: mockPush }),
|
|
11
|
+
useRoute: () => ({ query: mockQuery }),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
describe('component: ViewOptions/index', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
jest.clearAllMocks();
|
|
17
|
+
mockQuery.view = _CONFIG;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const createWrapper = () => {
|
|
21
|
+
return mount(ViewOptions);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
it('should render a ButtonGroup component', () => {
|
|
25
|
+
const wrapper = createWrapper();
|
|
26
|
+
|
|
27
|
+
expect(wrapper.findComponent(ButtonGroup).exists()).toBeTruthy();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should provide two view options: detail and graph', () => {
|
|
31
|
+
const wrapper = createWrapper();
|
|
32
|
+
const buttonGroup = wrapper.findComponent(ButtonGroup);
|
|
33
|
+
const options = buttonGroup.props('options');
|
|
34
|
+
|
|
35
|
+
expect(options).toHaveLength(2);
|
|
36
|
+
expect(options[0].value).toStrictEqual(_CONFIG);
|
|
37
|
+
expect(options[1].value).toStrictEqual(_GRAPH);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should set the initial view from the route query', () => {
|
|
41
|
+
const wrapper = createWrapper();
|
|
42
|
+
const buttonGroup = wrapper.findComponent(ButtonGroup);
|
|
43
|
+
|
|
44
|
+
expect(buttonGroup.props('value')).toStrictEqual(_CONFIG);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should push to router when view changes', async() => {
|
|
48
|
+
const wrapper = createWrapper();
|
|
49
|
+
const buttons = wrapper.findAll('.btn-group button');
|
|
50
|
+
const graphButton = buttons[1];
|
|
51
|
+
|
|
52
|
+
await graphButton.trigger('click');
|
|
53
|
+
|
|
54
|
+
expect(mockPush).toHaveBeenCalledWith({ query: { view: _GRAPH } });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should match the snapshot', () => {
|
|
58
|
+
const wrapper = createWrapper();
|
|
59
|
+
|
|
60
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -14,7 +14,8 @@ const view = ref(currentView.value);
|
|
|
14
14
|
const viewOptions = computed(() => {
|
|
15
15
|
return [
|
|
16
16
|
{
|
|
17
|
-
labelKey: 'resourceDetail.masthead.
|
|
17
|
+
labelKey: 'resourceDetail.masthead.detail',
|
|
18
|
+
// _CONFIG is the default when there is no query on the router
|
|
18
19
|
value: _CONFIG,
|
|
19
20
|
},
|
|
20
21
|
{
|
|
@@ -85,7 +85,29 @@ export default {
|
|
|
85
85
|
data() {
|
|
86
86
|
const params = { ...this.$route.params };
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
// Determine if the current product has a topLevelProduct defined, and if so,
|
|
89
|
+
// use that for the formRoute instead of the current route's product.
|
|
90
|
+
// This allows resources from extensions (new product registration) to use the correct route for creation,
|
|
91
|
+
// which may be different from the route of the resource list.
|
|
92
|
+
let currPluginName = '';
|
|
93
|
+
let formRoute;
|
|
94
|
+
let overrideCreateLocationByExtension = false;
|
|
95
|
+
const plugins = this.$extension.getPlugins();
|
|
96
|
+
|
|
97
|
+
Object.keys(plugins).forEach((key) => {
|
|
98
|
+
if (plugins[key].productNames.includes(this.$store.getters['productId'])) {
|
|
99
|
+
currPluginName = key;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (currPluginName && plugins[currPluginName]?.topLevelProduct) {
|
|
104
|
+
// override create route for extension resource lists
|
|
105
|
+
formRoute = { name: `${ this.$route.name }-create`, params: { ...params, product: this.$store.getters['productId'] } };
|
|
106
|
+
overrideCreateLocationByExtension = true;
|
|
107
|
+
} else {
|
|
108
|
+
// this was the original logic before the topLevelProduct override was added
|
|
109
|
+
formRoute = { name: `${ this.$route.name }-create`, params };
|
|
110
|
+
}
|
|
89
111
|
|
|
90
112
|
const hasEditComponent = this.$store.getters['type-map/hasCustomEdit'](this.resource);
|
|
91
113
|
|
|
@@ -96,6 +118,7 @@ export default {
|
|
|
96
118
|
};
|
|
97
119
|
|
|
98
120
|
return {
|
|
121
|
+
overrideCreateLocationByExtension,
|
|
99
122
|
formRoute,
|
|
100
123
|
yamlRoute,
|
|
101
124
|
hasEditComponent,
|
|
@@ -149,7 +172,7 @@ export default {
|
|
|
149
172
|
},
|
|
150
173
|
|
|
151
174
|
_createLocation() {
|
|
152
|
-
return this.createLocation || this.formRoute;
|
|
175
|
+
return this.overrideCreateLocationByExtension ? this.formRoute : this.createLocation || this.formRoute;
|
|
153
176
|
},
|
|
154
177
|
|
|
155
178
|
_yamlCreateLocation() {
|
package/components/SideNav.vue
CHANGED
|
@@ -229,6 +229,19 @@ export default {
|
|
|
229
229
|
|
|
230
230
|
this.getExplorerGroups(out);
|
|
231
231
|
|
|
232
|
+
// If there's a root group, pull its children up to the top level
|
|
233
|
+
// so that we can order them alongside group items in the nav
|
|
234
|
+
const rootGroupIndex = out.findIndex((g) => g.name.toLowerCase() === 'root');
|
|
235
|
+
const rootGroup = out[rootGroupIndex];
|
|
236
|
+
|
|
237
|
+
if (rootGroup && rootGroup.children?.length) {
|
|
238
|
+
out.splice(rootGroupIndex, 1);
|
|
239
|
+
|
|
240
|
+
rootGroup.children.forEach((child) => {
|
|
241
|
+
addObject(out, { ...child, children: [] });
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
232
245
|
replaceWith(this.groups, ...sortBy(out, ['weight:desc', 'label']));
|
|
233
246
|
|
|
234
247
|
this.gettingGroups = false;
|
|
@@ -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'
|
|
@@ -24,6 +24,7 @@ import DeveloperLoadExtensionDialog from '@shell/dialog/DeveloperLoadExtensionDi
|
|
|
24
24
|
import AddExtensionReposDialog from '@shell/dialog/AddExtensionReposDialog.vue';
|
|
25
25
|
import InstallExtensionDialog from '@shell/dialog/InstallExtensionDialog.vue';
|
|
26
26
|
import UninstallExtensionDialog from '@shell/dialog/UninstallExtensionDialog.vue';
|
|
27
|
+
import UninstallExistingExtensionDialog from '@shell/dialog/UninstallExistingExtensionDialog.vue';
|
|
27
28
|
import KnownHostsEditDialog from '@shell/dialog/KnownHostsEditDialog.vue';
|
|
28
29
|
import ImportDialog from '@shell/dialog/ImportDialog.vue';
|
|
29
30
|
import SearchDialog from '@shell/dialog/SearchDialog.vue';
|
|
@@ -110,6 +111,7 @@ describe('component: PromptModal', () => {
|
|
|
110
111
|
['AddExtensionReposDialog', AddExtensionReposDialog],
|
|
111
112
|
['InstallExtensionDialog', InstallExtensionDialog],
|
|
112
113
|
['UninstallExtensionDialog', UninstallExtensionDialog],
|
|
114
|
+
['UninstallExistingExtensionDialog', UninstallExistingExtensionDialog],
|
|
113
115
|
['KnownHostsEditDialog', KnownHostsEditDialog],
|
|
114
116
|
['ImportDialog', ImportDialog],
|
|
115
117
|
['SearchDialog', SearchDialog],
|
|
@@ -573,4 +573,75 @@ describe('component: FleetClusters', () => {
|
|
|
573
573
|
expect(additionalSubRow.exists()).toBe(false);
|
|
574
574
|
});
|
|
575
575
|
});
|
|
576
|
+
|
|
577
|
+
describe('labels visibility regardless of error state', () => {
|
|
578
|
+
it('should pass sub-rows prop as true to ResourceTable so labels always render', () => {
|
|
579
|
+
const wrapper = createWrapper();
|
|
580
|
+
|
|
581
|
+
// sub-rows=true ensures SortableTable.showSubRow() returns true,
|
|
582
|
+
// which makes the #additional-sub-row slot render regardless of stateDescription.
|
|
583
|
+
// Without this, labels only appear when there is an error (stateDescription).
|
|
584
|
+
const resourceTableStub = wrapper.findComponent('.resource-table') as any;
|
|
585
|
+
|
|
586
|
+
expect(resourceTableStub.props('subRows')).toBe(true);
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it('should render labels when cluster has no stateDescription (no error)', () => {
|
|
590
|
+
const rows = [{
|
|
591
|
+
customLabels: ['env:prod', 'team:backend'],
|
|
592
|
+
displayCustomLabels: false,
|
|
593
|
+
stateDescription: undefined,
|
|
594
|
+
}];
|
|
595
|
+
|
|
596
|
+
const wrapper = createWrapper({ rows });
|
|
597
|
+
const tags = wrapper.findAll('.tag');
|
|
598
|
+
|
|
599
|
+
expect(tags).toHaveLength(2);
|
|
600
|
+
expect(tags[0].text()).toBe('env:prod');
|
|
601
|
+
expect(tags[1].text()).toBe('team:backend');
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it('should render labels when cluster has a stateDescription (error)', () => {
|
|
605
|
+
const rows = [{
|
|
606
|
+
customLabels: ['env:prod', 'team:backend'],
|
|
607
|
+
displayCustomLabels: false,
|
|
608
|
+
stateDescription: 'Something went wrong',
|
|
609
|
+
}];
|
|
610
|
+
|
|
611
|
+
const wrapper = createWrapper({ rows });
|
|
612
|
+
const tags = wrapper.findAll('.tag');
|
|
613
|
+
|
|
614
|
+
expect(tags).toHaveLength(2);
|
|
615
|
+
expect(tags[0].text()).toBe('env:prod');
|
|
616
|
+
expect(tags[1].text()).toBe('team:backend');
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it('should render labels when stateDescription is empty string', () => {
|
|
620
|
+
const rows = [{
|
|
621
|
+
customLabels: ['env:staging'],
|
|
622
|
+
displayCustomLabels: false,
|
|
623
|
+
stateDescription: '',
|
|
624
|
+
}];
|
|
625
|
+
|
|
626
|
+
const wrapper = createWrapper({ rows });
|
|
627
|
+
const tags = wrapper.findAll('.tag');
|
|
628
|
+
|
|
629
|
+
expect(tags).toHaveLength(1);
|
|
630
|
+
expect(tags[0].text()).toBe('env:staging');
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it('should render labels when stateDescription is null', () => {
|
|
634
|
+
const rows = [{
|
|
635
|
+
customLabels: ['region:eu-west'],
|
|
636
|
+
displayCustomLabels: false,
|
|
637
|
+
stateDescription: null,
|
|
638
|
+
}];
|
|
639
|
+
|
|
640
|
+
const wrapper = createWrapper({ rows });
|
|
641
|
+
const tags = wrapper.findAll('.tag');
|
|
642
|
+
|
|
643
|
+
expect(tags).toHaveLength(1);
|
|
644
|
+
expect(tags[0].text()).toBe('region:eu-west');
|
|
645
|
+
});
|
|
646
|
+
});
|
|
576
647
|
});
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { mapGetters } from 'vuex';
|
|
3
3
|
import { RadioGroup } from '@components/Form/Radio';
|
|
4
4
|
import ResourceLabeledSelect from '@shell/components/form/ResourceLabeledSelect.vue';
|
|
5
|
+
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
|
5
6
|
import NodeAffinity from '@shell/components/form/NodeAffinity.vue';
|
|
6
7
|
import { HARVESTER_NAME as VIRTUAL } from '@shell/config/features';
|
|
7
8
|
import { _VIEW } from '@shell/config/query-params';
|
|
@@ -18,6 +19,7 @@ const parseNode = (node: string | KubeNode) => typeof node === 'string' ? node :
|
|
|
18
19
|
export default {
|
|
19
20
|
components: {
|
|
20
21
|
RadioGroup,
|
|
22
|
+
LabeledSelect,
|
|
21
23
|
ResourceLabeledSelect,
|
|
22
24
|
NodeAffinity,
|
|
23
25
|
},
|
|
@@ -35,7 +37,7 @@ export default {
|
|
|
35
37
|
*/
|
|
36
38
|
nodes: {
|
|
37
39
|
type: Array,
|
|
38
|
-
default: () =>
|
|
40
|
+
default: () => null
|
|
39
41
|
},
|
|
40
42
|
|
|
41
43
|
mode: {
|
|
@@ -218,14 +220,14 @@ export default {
|
|
|
218
220
|
handler(nodeSelector) {
|
|
219
221
|
// Harvester specific code should not live in rancher/dashboard components
|
|
220
222
|
// This was brought into harvester/dashboard via https://github.com/harvester/dashboard/pull/342
|
|
221
|
-
// rancher/dashboard via https://github.com/rancher/dashboard/pull/6310
|
|
223
|
+
// and then rancher/dashboard via https://github.com/rancher/dashboard/pull/6310
|
|
222
224
|
if (this.isHarvester && nodeSelector?.[HOSTNAME]) {
|
|
223
225
|
this.selectNode = 'nodeSelector';
|
|
224
226
|
const nodeName = nodeSelector[HOSTNAME];
|
|
225
227
|
|
|
226
228
|
this.nodeName = nodeName;
|
|
227
229
|
|
|
228
|
-
const array = this.nodes
|
|
230
|
+
const array = this.nodes?.map((n) => n.value) || [];
|
|
229
231
|
|
|
230
232
|
if (nodeName && !array.includes(nodeName)) {
|
|
231
233
|
this.$store.dispatch('growl/error', {
|
|
@@ -259,7 +261,19 @@ export default {
|
|
|
259
261
|
<template v-if="selectNode === 'nodeSelector'">
|
|
260
262
|
<div class="row">
|
|
261
263
|
<div class="col span-6">
|
|
264
|
+
<LabeledSelect
|
|
265
|
+
v-if="nodes"
|
|
266
|
+
v-model:value="nodeName"
|
|
267
|
+
:label="t('workload.scheduling.affinity.nodeName')"
|
|
268
|
+
:options="nodes || []"
|
|
269
|
+
:mode="mode"
|
|
270
|
+
:multiple="false"
|
|
271
|
+
:loading="loading"
|
|
272
|
+
:data-testid="'node-scheduling-nodeSelector'"
|
|
273
|
+
@update:value="update"
|
|
274
|
+
/>
|
|
262
275
|
<ResourceLabeledSelect
|
|
276
|
+
v-else
|
|
263
277
|
v-model:value="nodeName"
|
|
264
278
|
:label="t('workload.scheduling.affinity.nodeName')"
|
|
265
279
|
:resource-type="NODE"
|