@rancher/shell 3.0.11 → 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 +5 -4
- package/assets/translations/zh-hans.yaml +0 -3
- package/components/EmptyProductPage.vue +76 -0
- 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/__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/__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.vue +15 -1
- package/config/pagination-table-headers.js +8 -1
- package/config/product/apps.js +2 -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__/plugin-products-helpers.test.ts +454 -0
- package/core/__tests__/plugin-products.test.ts +3219 -0
- package/core/extension-manager-impl.js +30 -1
- 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/fleet.cattle.io.bundle.vue +21 -34
- 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/provisioning.cattle.io.cluster/__tests__/Ingress.test.ts +176 -0
- package/edit/provisioning.cattle.io.cluster/rke2.vue +4 -1
- package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +6 -0
- package/edit/provisioning.cattle.io.cluster/tabs/Ingress.vue +7 -2
- package/list/provisioning.cattle.io.cluster.vue +0 -1
- package/list/workload.vue +11 -4
- package/mixins/resource-fetch.js +12 -3
- package/models/pod.js +18 -0
- package/models/workload.js +20 -2
- package/package.json +1 -2
- package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +0 -1
- package/pages/c/_cluster/settings/brand.vue +4 -4
- package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +231 -13
- package/pages/c/_cluster/uiplugins/index.vue +143 -37
- 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 +7 -0
- 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 -2909
- package/types/store/dashboard-store.types.ts +5 -0
- package/types/store/pagination.types.ts +6 -0
- package/utils/axios.js +1 -4
- package/utils/dynamic-importer.js +3 -2
- package/utils/pagination-utils.ts +1 -1
- package/utils/uiplugins.ts +12 -16
- package/utils/validators/__tests__/private-registry.test.ts +76 -0
- package/utils/validators/private-registry.ts +28 -0
|
@@ -161,3 +161,34 @@
|
|
|
161
161
|
// we need to use !important because it needs to superseed other classes that might impact outlines
|
|
162
162
|
outline: 2px solid var(--primary-keyboard-focus);
|
|
163
163
|
}
|
|
164
|
+
|
|
165
|
+
// -------------------------------------------------------------------------------------------------
|
|
166
|
+
// Extension dialog styles
|
|
167
|
+
|
|
168
|
+
@mixin extension-dialog {
|
|
169
|
+
padding: 8px 16px 16px 16px;
|
|
170
|
+
|
|
171
|
+
h4 {
|
|
172
|
+
font-weight: bold;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.dialog-panel {
|
|
176
|
+
display: flex;
|
|
177
|
+
flex-direction: column;
|
|
178
|
+
min-height: 96px;
|
|
179
|
+
|
|
180
|
+
.dialog-info {
|
|
181
|
+
flex: 1;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.dialog-buttons {
|
|
186
|
+
display: flex;
|
|
187
|
+
justify-content: flex-end;
|
|
188
|
+
margin-top: 24px;
|
|
189
|
+
|
|
190
|
+
> *:not(:last-child) {
|
|
191
|
+
margin-right: 8px;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -74,6 +74,7 @@ $contrasted-light: $lightest !default;
|
|
|
74
74
|
$selected: rgba(#3D98D3, .5);
|
|
75
75
|
|
|
76
76
|
$drag-over: #DCDEE7;
|
|
77
|
+
$body-bg : $lightest;
|
|
77
78
|
|
|
78
79
|
BODY, .theme-light {
|
|
79
80
|
|
|
@@ -418,7 +419,7 @@ BODY, .theme-light {
|
|
|
418
419
|
}
|
|
419
420
|
|
|
420
421
|
|
|
421
|
-
--body-bg : #{$
|
|
422
|
+
--body-bg : #{$body-bg};
|
|
422
423
|
--body-text : #{$darkest};
|
|
423
424
|
--body-text-hover : var(--body-text);
|
|
424
425
|
--scrollbar-thumb : #{$dark};
|
|
@@ -724,8 +725,8 @@ BODY, .theme-light {
|
|
|
724
725
|
--rc-disabled-background: #{$gray001};
|
|
725
726
|
--rc-disabled-text-color: #{$gray004};
|
|
726
727
|
|
|
727
|
-
--rc-section-background-primary: #{$
|
|
728
|
-
--rc-section-background-secondary: #{$
|
|
728
|
+
--rc-section-background-primary: #{$body-bg};
|
|
729
|
+
--rc-section-background-secondary: #{$grey-5};
|
|
729
730
|
--rc-section-action-color: #{$gray009};
|
|
730
731
|
|
|
731
732
|
--rc-image-bg: #{$lightest};
|
|
@@ -1072,8 +1073,8 @@ BODY, .theme-dark {
|
|
|
1072
1073
|
--rc-disabled-background: #{$gray005};
|
|
1073
1074
|
--rc-disabled-text-color: #{$gray004};
|
|
1074
1075
|
|
|
1075
|
-
--rc-section-background-primary: #{$
|
|
1076
|
-
--rc-section-background-secondary: #{$
|
|
1076
|
+
--rc-section-background-primary: #{$body-bg};
|
|
1077
|
+
--rc-section-background-secondary: #{$darkest};
|
|
1077
1078
|
--rc-section-action-color: #{$gray010};
|
|
1078
1079
|
|
|
1079
1080
|
--rc-image-bg: #{$lightest};
|
|
@@ -2989,7 +2989,7 @@ fleet:
|
|
|
2989
2989
|
add: Add Selector
|
|
2990
2990
|
tolerations:
|
|
2991
2991
|
label: Tolerations
|
|
2992
|
-
description: "List of node taints to tolerate
|
|
2992
|
+
description: "List of node taints to tolerate."
|
|
2993
2993
|
add: Add Toleration
|
|
2994
2994
|
priorityClassName:
|
|
2995
2995
|
label: Priority Class Name
|
|
@@ -5578,6 +5578,9 @@ plugins:
|
|
|
5578
5578
|
prompt: "Are you sure that you want to install this extension?"
|
|
5579
5579
|
version: Version
|
|
5580
5580
|
warnNotCertified: Please ensure that you are aware of the risks of installing Extensions from untrusted authors
|
|
5581
|
+
alreadyInstalledTitle: This extension is already installed from another source
|
|
5582
|
+
alreadyInstalledPrompt: To install it from this source, you need to uninstall the existing version first and reload the page (required). Would you like to continue?
|
|
5583
|
+
uninstallExisting: Uninstall existing version
|
|
5581
5584
|
upgrade:
|
|
5582
5585
|
label: Upgrade
|
|
5583
5586
|
title: Upgrade extension {name}
|
|
@@ -6988,6 +6991,7 @@ tableHeaders:
|
|
|
6988
6991
|
progress: Progress
|
|
6989
6992
|
podImages: Image
|
|
6990
6993
|
podRestarts: Restarts
|
|
6994
|
+
podLastRestart: Last Restart
|
|
6991
6995
|
pods: Pods
|
|
6992
6996
|
pod-Selector: Pod-Selector
|
|
6993
6997
|
providers: Providers
|
|
@@ -8045,10 +8049,7 @@ typeDescription:
|
|
|
8045
8049
|
monitoring.coreos.com.prometheus: A Prometheus server is a Prometheus deployment whose scrape configuration and rules are determined by selected ServiceMonitors, PodMonitors, and PrometheusRules and whose alerts will be sent to all selected Alertmanagers with the custom resource's configuration.
|
|
8046
8050
|
monitoring.coreos.com.alertmanager: An alert manager is deployment whose configuration will be specified by a secret in the same namespace, which determines which alerts should go to which receiver.
|
|
8047
8051
|
node: The base Kubernetes Node resource represents a virtual or physical machine which hosts deployments. To manage the machine lifecycle, if available, go to Cluster Management.
|
|
8048
|
-
catalog.cattle.io.clusterrepo: 'A chart repository is a Helm repository or {vendor} git based application catalog. It provides the list of available charts in the cluster.'
|
|
8049
|
-
catalog.cattle.io.clusterrepo.local: ' A chart repository is a Helm repository or {vendor} git based application catalog. It provides the list of available charts in the cluster. Cluster Templates are deployed via Helm charts.'
|
|
8050
8052
|
catalog.cattle.io.operation: An operation is the list of recent Helm operations that have been applied to the cluster.
|
|
8051
|
-
catalog.cattle.io.app: An installed application is a Helm 3 chart that was installed either via our charts or through the Helm CLI.
|
|
8052
8053
|
logging.banzaicloud.io.clusterflow: Logs from the cluster will be collected and logged to the selected Cluster Output.
|
|
8053
8054
|
logging.banzaicloud.io.clusteroutput: A cluster output defines which logging providers that logs can be sent to and is only effective when deployed in the namespace that the logging operator is in.
|
|
8054
8055
|
logging.banzaicloud.io.flow: A flow defines which logs to collect and filter as well as which output to send the logs. The flow is a namespaced resource, which means logs will only be collected from the namespace that the flow is deployed in.
|
|
@@ -6339,10 +6339,7 @@ typeDescription:
|
|
|
6339
6339
|
monitoring.coreos.com.prometheus: Prometheus server 是一个 Prometheus deployment,其抓取的配置和规则由选定的 ServiceMonitor、PodMonitor 和 PrometheusRule 决定。它将其告警信息发送给所有选择的具有定制资源配置的 AlertManager。
|
|
6340
6340
|
monitoring.coreos.com.alertmanager: Alertmanager 是一个 deployment。其配置由同一命名空间中的密文指定,该密文决定了告警的接收器。
|
|
6341
6341
|
node: Kubernetes 节点资源展示了承载 Deployment 的虚拟机或物理机。请进入"集群管理"页面管理可用节点的生命周期。
|
|
6342
|
-
catalog.cattle.io.clusterrepo: 'Chart 仓库是一个 Helm 仓库或 {vendor} 基于 Git 的应用商店。此处列出了集群中可用的 Chart。'
|
|
6343
|
-
catalog.cattle.io.clusterrepo.local: ' Chart 仓库是一个 Helm 仓库或 {vendor} 基于 Git 的应用商店。此处列出了集群中可用的 Chart。集群模板是通过 Helm Chart 部署的。'
|
|
6344
6342
|
catalog.cattle.io.operation: 最近的操作指的是最近应用于集群的一系列 Helm 操作。
|
|
6345
|
-
catalog.cattle.io.app: 已安装的应用指的是通过我们的 Chart 或 Helm CLI 安装的 Helm 3 Chart。
|
|
6346
6343
|
logging.banzaicloud.io.clusterflow: 集群日志将被收集并投递到选定的 ClusterOutput 中。
|
|
6347
6344
|
logging.banzaicloud.io.clusteroutput: ClusterOutput 定义了日志可以发送到哪些日志提供程序。只有部署在 Logging operator 所在的命名空间中时,ClusterOutput 才生效。
|
|
6348
6345
|
logging.banzaicloud.io.flow: Flow 定义了要收集和过滤的日志,以及日志的输出目标。Flow 是一个命名空间资源。换言之,只有部署了该 Flow 的命名空间日志才能被该 Flow 收集。
|
|
@@ -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>
|
|
@@ -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;
|
|
@@ -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
|
});
|