@rancher/shell 3.0.8-rc.2 → 3.0.8-rc.3
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/dark/rancher-logo.svg +64 -1
- package/assets/brand/suse/rancher-logo.svg +1 -1
- package/assets/styles/global/_cards.scss +0 -3
- package/assets/styles/themes/_modern.scss +9 -1
- package/assets/styles/themes/_suse.scss +81 -24
- package/assets/translations/en-us.yaml +68 -3
- package/components/AutoscalerCard.vue +113 -0
- package/components/AutoscalerTab.vue +94 -0
- package/components/ClusterIconMenu.vue +1 -1
- package/components/ClusterProviderIcon.vue +1 -1
- package/components/IconOrSvg.vue +2 -2
- package/components/PopoverCard.vue +192 -0
- package/components/Resource/Detail/FetchLoader/composables.ts +18 -4
- package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/identifying-fields.test.ts +1 -1
- package/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields.ts +4 -0
- package/components/Resource/Detail/ResourcePopover/ResourcePopoverCard.vue +2 -19
- package/components/Resource/Detail/ResourcePopover/__tests__/ResourcePopoverCard.test.ts +0 -29
- package/components/Resource/Detail/ResourcePopover/__tests__/index.test.ts +132 -150
- package/components/Resource/Detail/ResourcePopover/index.vue +54 -159
- package/components/ResourceDetail/Masthead/latest.vue +29 -0
- package/components/ResourceList/Masthead.vue +1 -1
- package/components/__tests__/AutoscalerCard.test.ts +154 -0
- package/components/__tests__/AutoscalerTab.test.ts +125 -0
- package/components/__tests__/PopoverCard.test.ts +204 -0
- package/components/formatter/Autoscaler.vue +97 -0
- package/components/formatter/InternalExternalIP.vue +195 -24
- package/components/formatter/__tests__/Autoscaler.test.ts +156 -0
- package/components/formatter/__tests__/InternalExternalIP.test.ts +133 -0
- package/components/nav/Group.vue +12 -3
- package/components/nav/TopLevelMenu.vue +2 -2
- package/composables/useInterval.ts +15 -0
- package/config/labels-annotations.js +8 -1
- package/config/product/manager.js +20 -9
- package/config/router/routes.js +4 -0
- package/config/settings.ts +2 -1
- package/config/table-headers.js +8 -0
- package/config/types.js +2 -0
- package/core/types-provisioning.ts +3 -0
- package/detail/provisioning.cattle.io.cluster.vue +12 -1
- package/directives/ui-context.ts +8 -2
- package/edit/auth/github.vue +5 -0
- package/edit/cloudcredential.vue +1 -1
- package/edit/fleet.cattle.io.gitrepo.vue +0 -10
- package/edit/provisioning.cattle.io.cluster/CustomCommand.vue +32 -5
- package/edit/provisioning.cattle.io.cluster/__tests__/CustomCommand.test.ts +35 -0
- package/edit/provisioning.cattle.io.cluster/__tests__/Networking.test.ts +132 -0
- package/edit/provisioning.cattle.io.cluster/index.vue +18 -12
- package/edit/provisioning.cattle.io.cluster/rke2.vue +39 -8
- package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +107 -5
- package/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue +90 -3
- package/initialize/install-plugins.js +3 -1
- package/list/provisioning.cattle.io.cluster.vue +15 -2
- package/machine-config/amazonec2.vue +36 -135
- package/machine-config/components/EC2Networking.vue +474 -0
- package/machine-config/components/__tests__/EC2Networking.test.ts +94 -0
- package/machine-config/components/__tests__/utils/vpcSubnetMockData.js +294 -0
- package/machine-config/digitalocean.vue +11 -0
- package/models/cluster/node.js +13 -6
- package/models/cluster.x-k8s.io.machine.js +10 -20
- package/models/cluster.x-k8s.io.machinedeployment.js +5 -1
- package/models/management.cattle.io.kontainerdriver.js +1 -0
- package/models/provisioning.cattle.io.cluster.js +223 -2
- package/package.json +1 -1
- package/pages/c/_cluster/apps/charts/install.vue +1 -1
- package/pages/c/_cluster/manager/hostedprovider/index.vue +209 -0
- package/plugins/dynamic-content.js +13 -0
- package/rancher-components/Form/Checkbox/Checkbox.vue +1 -1
- package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +8 -0
- package/store/features.js +1 -0
- package/store/notifications.ts +32 -1
- package/store/plugins.js +7 -3
- package/store/prefs.js +1 -0
- package/types/notifications/index.ts +24 -3
- package/types/shell/index.d.ts +26 -1
- package/utils/__tests__/object.test.ts +19 -0
- package/utils/autoscaler-utils.ts +7 -0
- package/utils/dynamic-content/__tests__/announcement.test.ts +498 -0
- package/utils/dynamic-content/announcement.ts +112 -0
- package/utils/dynamic-content/example.json +40 -0
- package/utils/dynamic-content/index.ts +6 -2
- package/utils/dynamic-content/new-release.ts +1 -1
- package/utils/dynamic-content/notification-handler.ts +48 -0
- package/utils/dynamic-content/types.d.ts +33 -1
- package/utils/object.js +20 -2
- package/utils/scroll.js +7 -0
- package/utils/settings.ts +15 -0
- package/utils/validators/machine-pool.ts +13 -3
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
import { isV4Format, isV6Format } from 'ip';
|
|
3
3
|
import CopyToClipboard from '@shell/components/CopyToClipboard';
|
|
4
4
|
import { mapGetters } from 'vuex';
|
|
5
|
+
import RcStatusBadge from '@components/Pill/RcStatusBadge/RcStatusBadge';
|
|
6
|
+
|
|
5
7
|
export default {
|
|
6
|
-
components: { CopyToClipboard },
|
|
8
|
+
components: { CopyToClipboard, RcStatusBadge },
|
|
7
9
|
props: {
|
|
8
10
|
row: {
|
|
9
11
|
type: Object,
|
|
@@ -11,10 +13,49 @@ export default {
|
|
|
11
13
|
},
|
|
12
14
|
},
|
|
13
15
|
computed: {
|
|
16
|
+
...mapGetters({ t: 'i18n/t' }),
|
|
17
|
+
filteredExternalIps() {
|
|
18
|
+
return this.row.externalIps?.filter((ip) => this.isIp(ip)) || [];
|
|
19
|
+
},
|
|
20
|
+
filteredInternalIps() {
|
|
21
|
+
return this.row.internalIps?.filter((ip) => this.isIp(ip)) || [];
|
|
22
|
+
},
|
|
14
23
|
internalSameAsExternal() {
|
|
15
|
-
return this.
|
|
24
|
+
return this.externalIp && this.internalIp && this.externalIp === this.internalIp;
|
|
25
|
+
},
|
|
26
|
+
showPopover() {
|
|
27
|
+
return this.filteredExternalIps.length > 1 || this.filteredInternalIps.length > 1;
|
|
16
28
|
},
|
|
17
|
-
|
|
29
|
+
externalIp() {
|
|
30
|
+
return this.filteredExternalIps[0] || null;
|
|
31
|
+
},
|
|
32
|
+
internalIp() {
|
|
33
|
+
return this.filteredInternalIps[0] || null;
|
|
34
|
+
},
|
|
35
|
+
remainingIpCount() {
|
|
36
|
+
let count = 0;
|
|
37
|
+
|
|
38
|
+
if (this.filteredExternalIps.length > 1) {
|
|
39
|
+
count += this.filteredExternalIps.length - 1;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!this.internalSameAsExternal && this.filteredInternalIps.length > 1) {
|
|
43
|
+
count += this.filteredInternalIps.length - 1;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return count;
|
|
47
|
+
},
|
|
48
|
+
tooltipContent() {
|
|
49
|
+
const count = this.remainingIpCount;
|
|
50
|
+
|
|
51
|
+
return this.t('internalExternalIP.clickToShowMoreIps', { count });
|
|
52
|
+
},
|
|
53
|
+
remainingExternalIps() {
|
|
54
|
+
return this.filteredExternalIps.slice(1);
|
|
55
|
+
},
|
|
56
|
+
remainingInternalIps() {
|
|
57
|
+
return this.filteredInternalIps.slice(1);
|
|
58
|
+
}
|
|
18
59
|
},
|
|
19
60
|
methods: {
|
|
20
61
|
isIp(ip) {
|
|
@@ -25,40 +66,170 @@ export default {
|
|
|
25
66
|
</script>
|
|
26
67
|
|
|
27
68
|
<template>
|
|
28
|
-
<
|
|
29
|
-
<template v-if="
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
69
|
+
<div class="ip-container">
|
|
70
|
+
<template v-if="externalIp">
|
|
71
|
+
<span data-testid="external-ip">
|
|
72
|
+
{{ externalIp }}
|
|
73
|
+
<CopyToClipboard
|
|
74
|
+
:aria-label="t('internalExternalIP.copyExternalIp')"
|
|
75
|
+
label-as="tooltip"
|
|
76
|
+
:text="externalIp"
|
|
77
|
+
class="icon-btn"
|
|
78
|
+
action-color="bg-transparent"
|
|
79
|
+
/>
|
|
80
|
+
</span>
|
|
37
81
|
</template>
|
|
38
82
|
<template v-else>
|
|
39
83
|
-
|
|
40
84
|
</template>
|
|
41
|
-
|
|
42
|
-
<template v-if="internalSameAsExternal
|
|
85
|
+
<span class="separator">/</span>
|
|
86
|
+
<template v-if="internalSameAsExternal">
|
|
43
87
|
{{ t('tableHeaders.internalIpSameAsExternal') }}
|
|
44
88
|
</template>
|
|
45
|
-
<template v-else-if="
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
89
|
+
<template v-else-if="internalIp">
|
|
90
|
+
<span data-testid="internal-ip">
|
|
91
|
+
{{ internalIp }}
|
|
92
|
+
<CopyToClipboard
|
|
93
|
+
:aria-label="t('internalExternalIP.copyInternalIp')"
|
|
94
|
+
label-as="tooltip"
|
|
95
|
+
:text="internalIp"
|
|
96
|
+
class="icon-btn"
|
|
97
|
+
action-color="bg-transparent"
|
|
98
|
+
/>
|
|
99
|
+
</span>
|
|
53
100
|
</template>
|
|
54
101
|
<template v-else>
|
|
55
102
|
-
|
|
56
103
|
</template>
|
|
57
|
-
|
|
104
|
+
<v-dropdown
|
|
105
|
+
v-if="showPopover"
|
|
106
|
+
ref="dropdown"
|
|
107
|
+
placement="bottom-start"
|
|
108
|
+
>
|
|
109
|
+
<template #default>
|
|
110
|
+
<RcStatusBadge
|
|
111
|
+
v-clean-tooltip="tooltipContent"
|
|
112
|
+
status="info"
|
|
113
|
+
data-testid="plus-more"
|
|
114
|
+
@click.stop
|
|
115
|
+
>
|
|
116
|
+
{{ t('generic.plusMore', {n: remainingIpCount}) }}
|
|
117
|
+
</RcStatusBadge>
|
|
118
|
+
</template>
|
|
119
|
+
<template #popper>
|
|
120
|
+
<div
|
|
121
|
+
class="ip-addresses-popover"
|
|
122
|
+
data-testid="ip-addresses-popover"
|
|
123
|
+
>
|
|
124
|
+
<button
|
|
125
|
+
class="btn btn-sm close-button"
|
|
126
|
+
@click="$refs.dropdown.hide()"
|
|
127
|
+
>
|
|
128
|
+
<i class="icon icon-close" />
|
|
129
|
+
</button>
|
|
130
|
+
<div
|
|
131
|
+
v-if="remainingExternalIps.length"
|
|
132
|
+
class="ip-list"
|
|
133
|
+
data-testid="external-ip-list"
|
|
134
|
+
>
|
|
135
|
+
<h5>{{ t('generic.externalIps') }}</h5>
|
|
136
|
+
<div
|
|
137
|
+
v-for="ip in remainingExternalIps"
|
|
138
|
+
:key="ip"
|
|
139
|
+
class="ip-address"
|
|
140
|
+
>
|
|
141
|
+
<span>{{ ip }}</span>
|
|
142
|
+
<CopyToClipboard
|
|
143
|
+
:text="ip"
|
|
144
|
+
label-as="tooltip"
|
|
145
|
+
class="icon-btn"
|
|
146
|
+
action-color="bg-transparent"
|
|
147
|
+
/>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
<div
|
|
151
|
+
v-if="remainingInternalIps.length"
|
|
152
|
+
class="ip-list"
|
|
153
|
+
data-testid="internal-ip-list"
|
|
154
|
+
>
|
|
155
|
+
<h5>{{ t('generic.internalIps') }}</h5>
|
|
156
|
+
<div
|
|
157
|
+
v-for="ip in remainingInternalIps"
|
|
158
|
+
:key="ip"
|
|
159
|
+
class="ip-address"
|
|
160
|
+
>
|
|
161
|
+
<span>{{ ip }}</span>
|
|
162
|
+
<CopyToClipboard
|
|
163
|
+
:text="ip"
|
|
164
|
+
label-as="tooltip"
|
|
165
|
+
class="icon-btn"
|
|
166
|
+
action-color="bg-transparent"
|
|
167
|
+
/>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</template>
|
|
172
|
+
</v-dropdown>
|
|
173
|
+
</div>
|
|
58
174
|
</template>
|
|
59
175
|
|
|
60
176
|
<style lang='scss' scoped>
|
|
177
|
+
.ip-container {
|
|
178
|
+
display: flex;
|
|
179
|
+
flex-wrap: wrap;
|
|
180
|
+
align-items: center;
|
|
181
|
+
gap: 4px;
|
|
182
|
+
margin: 8px 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
61
185
|
.icon-btn {
|
|
62
|
-
|
|
186
|
+
padding: 2px;
|
|
187
|
+
min-height: 24px;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.rc-status-badge {
|
|
191
|
+
cursor: pointer;
|
|
192
|
+
padding: 0 4px;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.ip-addresses-popover {
|
|
196
|
+
display: flex;
|
|
197
|
+
flex-direction: column;
|
|
198
|
+
min-width: 120px;
|
|
199
|
+
padding: 8px;
|
|
200
|
+
gap: 16px;
|
|
201
|
+
|
|
202
|
+
.ip-list {
|
|
203
|
+
display: flex;
|
|
204
|
+
flex-direction: column;
|
|
205
|
+
gap: 4px;
|
|
206
|
+
margin-top: 8px;
|
|
207
|
+
|
|
208
|
+
h5 {
|
|
209
|
+
margin-bottom: 4px;
|
|
210
|
+
font-weight: 600;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.ip-address {
|
|
215
|
+
display: flex;
|
|
216
|
+
align-items: center;
|
|
217
|
+
gap: 4px;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.close-button {
|
|
221
|
+
position: absolute;
|
|
222
|
+
top: -6px;
|
|
223
|
+
right: -6px;
|
|
224
|
+
padding: 8px;
|
|
225
|
+
display: flex;
|
|
226
|
+
align-items: center;
|
|
227
|
+
justify-content: center;
|
|
228
|
+
background: transparent;
|
|
229
|
+
|
|
230
|
+
&:hover .icon-close{
|
|
231
|
+
color: var(--primary);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
63
234
|
}
|
|
64
235
|
</style>
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
|
|
2
|
+
import { mount } from '@vue/test-utils';
|
|
3
|
+
import Autoscaler from '@shell/components/formatter/Autoscaler.vue';
|
|
4
|
+
import { createStore } from 'vuex';
|
|
5
|
+
|
|
6
|
+
describe('component: formatter/Autoscaler.vue', () => {
|
|
7
|
+
const mockToggleRunner = jest.fn();
|
|
8
|
+
const mockClose = jest.fn();
|
|
9
|
+
|
|
10
|
+
const PopoverCardStub = {
|
|
11
|
+
name: 'PopoverCard',
|
|
12
|
+
template: `
|
|
13
|
+
<div>
|
|
14
|
+
<slot />
|
|
15
|
+
<slot name="heading-action" :close="close" />
|
|
16
|
+
<slot name="card-body" />
|
|
17
|
+
</div>
|
|
18
|
+
`,
|
|
19
|
+
props: ['cardTitle', 'fallbackFocus'],
|
|
20
|
+
setup() {
|
|
21
|
+
return { close: mockClose };
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const createWrapper = (props: any) => {
|
|
26
|
+
return mount(Autoscaler, {
|
|
27
|
+
props,
|
|
28
|
+
global: {
|
|
29
|
+
plugins: [createStore({})],
|
|
30
|
+
stubs: {
|
|
31
|
+
PopoverCard: PopoverCardStub,
|
|
32
|
+
RcButton: { template: '<button><slot /></button>' },
|
|
33
|
+
AutoscalerCard: {
|
|
34
|
+
name: 'AutoscalerCard',
|
|
35
|
+
props: ['value'],
|
|
36
|
+
template: '<div></div>'
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
jest.clearAllMocks();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('unchecked state', () => {
|
|
48
|
+
it.each([
|
|
49
|
+
[false],
|
|
50
|
+
['false'],
|
|
51
|
+
[''],
|
|
52
|
+
])('should render a dash when value is %p', (value) => {
|
|
53
|
+
const wrapper = createWrapper({ value, row: {} });
|
|
54
|
+
|
|
55
|
+
expect(wrapper.text()).toBe('—');
|
|
56
|
+
expect(wrapper.find('.text-muted').exists()).toBe(true);
|
|
57
|
+
expect(wrapper.findComponent({ name: 'PopoverCard' }).exists()).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('checked state', () => {
|
|
62
|
+
it.each([
|
|
63
|
+
[true],
|
|
64
|
+
['true'],
|
|
65
|
+
[undefined],
|
|
66
|
+
])('should render popover when value is %p', (value) => {
|
|
67
|
+
const wrapper = createWrapper({ value, row: {} });
|
|
68
|
+
|
|
69
|
+
expect(wrapper.findComponent({ name: 'PopoverCard' }).exists()).toBe(true);
|
|
70
|
+
expect(wrapper.find('.icon-checkmark').exists()).toBe(true);
|
|
71
|
+
expect(wrapper.text()).not.toBe('—');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should stop click propagation', async() => {
|
|
75
|
+
const mockStopPropagation = jest.fn();
|
|
76
|
+
const wrapper = createWrapper({ value: true, row: {} });
|
|
77
|
+
|
|
78
|
+
await wrapper.find('.autoscaler').trigger('click', { stopPropagation: mockStopPropagation });
|
|
79
|
+
expect(mockStopPropagation).toHaveBeenCalledWith();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should pass correct props to PopoverCard', () => {
|
|
83
|
+
const wrapper = createWrapper({ value: true, row: {} });
|
|
84
|
+
const popover = wrapper.findComponent(PopoverCardStub);
|
|
85
|
+
|
|
86
|
+
expect(popover.props('cardTitle')).toBe('autoscaler.card.title');
|
|
87
|
+
expect(popover.props('fallbackFocus')).toBe('.autoscaler .action');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should render AutoscalerCard with correct row data', () => {
|
|
91
|
+
const rowData = { id: 'test-row' };
|
|
92
|
+
const wrapper = createWrapper({ value: true, row: rowData });
|
|
93
|
+
const card = wrapper.findComponent({ name: 'AutoscalerCard' });
|
|
94
|
+
|
|
95
|
+
expect(card.exists()).toBe(true);
|
|
96
|
+
expect(card.props('value')).toStrictEqual(rowData);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('heading action button', () => {
|
|
101
|
+
it('should NOT render if canExplore is false', () => {
|
|
102
|
+
const rowData = { canExplore: false };
|
|
103
|
+
const wrapper = createWrapper({ value: true, row: rowData });
|
|
104
|
+
|
|
105
|
+
expect(wrapper.find('button').exists()).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should render "Pause" button if autoscaler is running', () => {
|
|
109
|
+
const rowData = {
|
|
110
|
+
canExplore: true, isAutoscalerPaused: false, canPauseResumeAutoscaler: true
|
|
111
|
+
};
|
|
112
|
+
const wrapper = createWrapper({
|
|
113
|
+
value: true, row: rowData, canPauseResumeAutoscaler: true
|
|
114
|
+
});
|
|
115
|
+
const button = wrapper.find('button');
|
|
116
|
+
|
|
117
|
+
expect(button.exists()).toBe(true);
|
|
118
|
+
expect(button.text()).toBe('autoscaler.card.pause');
|
|
119
|
+
expect(wrapper.find('.icon-pause').exists()).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should render "Resume" button if autoscaler is paused', () => {
|
|
123
|
+
const rowData = {
|
|
124
|
+
canExplore: true, isAutoscalerPaused: true, canPauseResumeAutoscaler: true
|
|
125
|
+
};
|
|
126
|
+
const wrapper = createWrapper({ value: true, row: rowData });
|
|
127
|
+
const button = wrapper.find('button');
|
|
128
|
+
|
|
129
|
+
expect(button.exists()).toBe(true);
|
|
130
|
+
expect(button.text()).toBe('autoscaler.card.resume');
|
|
131
|
+
expect(wrapper.find('.icon-play').exists()).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should hide "Resume" button if canPauseResumeAutoscaler is false', () => {
|
|
135
|
+
const rowData = {
|
|
136
|
+
canExplore: true, isAutoscalerPaused: true, canPauseResumeAutoscaler: false
|
|
137
|
+
};
|
|
138
|
+
const wrapper = createWrapper({ value: true, row: rowData });
|
|
139
|
+
const button = wrapper.find('button');
|
|
140
|
+
|
|
141
|
+
expect(button.exists()).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should call toggleAutoscalerRunner and close on click', async() => {
|
|
145
|
+
const rowData = {
|
|
146
|
+
canExplore: true, toggleAutoscalerRunner: mockToggleRunner, canPauseResumeAutoscaler: true
|
|
147
|
+
};
|
|
148
|
+
const wrapper = createWrapper({ value: true, row: rowData });
|
|
149
|
+
|
|
150
|
+
wrapper.find('button').trigger('click');
|
|
151
|
+
|
|
152
|
+
expect(mockToggleRunner).toHaveBeenCalledTimes(1);
|
|
153
|
+
expect(mockClose).toHaveBeenCalledTimes(1);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { mount, VueWrapper } from '@vue/test-utils';
|
|
2
|
+
import InternalExternalIP from '@shell/components/formatter/InternalExternalIP.vue';
|
|
3
|
+
|
|
4
|
+
jest.mock('clipboard-polyfill', () => ({ writeText: jest.fn() }));
|
|
5
|
+
|
|
6
|
+
describe('component: InternalExternalIP', () => {
|
|
7
|
+
const mockGetters = {
|
|
8
|
+
'i18n/t': (key: string, args: any) => {
|
|
9
|
+
if (key === 'generic.plusMore') {
|
|
10
|
+
return `+${ args.n } more`;
|
|
11
|
+
}
|
|
12
|
+
if (key === 'internalExternalIP.clickToShowMoreIps') {
|
|
13
|
+
return `Click to show ${ args.count } more IP(s)`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return key;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const mountComponent = (props: any): VueWrapper<any> => {
|
|
21
|
+
return mount(InternalExternalIP, {
|
|
22
|
+
props,
|
|
23
|
+
global: {
|
|
24
|
+
components: { 'v-dropdown': { name: 'v-dropdown', template: '<div><slot /><slot name="popper" /></div>' } },
|
|
25
|
+
directives: {
|
|
26
|
+
'clean-tooltip': (el, binding) => {
|
|
27
|
+
el.setAttribute('v-clean-tooltip', binding.value);
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
mocks: { $store: { getters: mockGetters } }
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
describe('no IPs', () => {
|
|
36
|
+
it('should display placeholders', () => {
|
|
37
|
+
const wrapper = mountComponent({ row: { externalIps: [], internalIps: ['1.1.1.1'] } });
|
|
38
|
+
|
|
39
|
+
expect(wrapper.text()).toStrictEqual('- /1.1.1.1');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('single IPs', () => {
|
|
44
|
+
it('should display a single external IP', () => {
|
|
45
|
+
const wrapper = mountComponent({ row: { externalIps: ['1.1.1.1'], internalIps: [] } });
|
|
46
|
+
|
|
47
|
+
expect(wrapper.find('[data-testid="external-ip"]').text()).toStrictEqual('1.1.1.1');
|
|
48
|
+
expect(wrapper.find('[data-testid="plus-more"]').exists()).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should display a single internal IP', () => {
|
|
52
|
+
const wrapper = mountComponent({ row: { externalIps: [], internalIps: ['2.2.2.2'] } });
|
|
53
|
+
|
|
54
|
+
expect(wrapper.find('[data-testid="internal-ip"]').text()).toStrictEqual('2.2.2.2');
|
|
55
|
+
expect(wrapper.find('[data-testid="plus-more"]').exists()).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should display both external and internal IPs', () => {
|
|
59
|
+
const wrapper = mountComponent({ row: { externalIps: ['1.1.1.1'], internalIps: ['2.2.2.2'] } });
|
|
60
|
+
|
|
61
|
+
expect(wrapper.find('[data-testid="external-ip"]').text()).toStrictEqual('1.1.1.1');
|
|
62
|
+
expect(wrapper.find('[data-testid="internal-ip"]').text()).toStrictEqual('2.2.2.2');
|
|
63
|
+
expect(wrapper.find('[data-testid="plus-more"]').exists()).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should display "Same as external" when IPs are the same', () => {
|
|
67
|
+
const wrapper = mountComponent({ row: { externalIps: ['1.1.1.1'], internalIps: ['1.1.1.1'] } });
|
|
68
|
+
|
|
69
|
+
expect(wrapper.text()).toContain('tableHeaders.internalIpSameAsExternal');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('multiple IPs', () => {
|
|
74
|
+
it('should show popover with remaining external IPs', async() => {
|
|
75
|
+
const wrapper = mountComponent({ row: { externalIps: ['1.1.1.1', '3.3.3.3'], internalIps: ['2.2.2.2'] } });
|
|
76
|
+
|
|
77
|
+
expect(wrapper.find('[data-testid="plus-more"]').text()).toStrictEqual('+1 more');
|
|
78
|
+
|
|
79
|
+
const popover = wrapper.find('[data-testid="ip-addresses-popover"]');
|
|
80
|
+
const ipSpans = popover.findAll('.ip-address span');
|
|
81
|
+
|
|
82
|
+
expect(ipSpans).toHaveLength(1);
|
|
83
|
+
expect(ipSpans[0].text()).toStrictEqual('3.3.3.3');
|
|
84
|
+
expect(popover.find('[data-testid="internal-ip-list"]').exists()).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should show popover with remaining internal IPs', async() => {
|
|
88
|
+
const wrapper = mountComponent({ row: { externalIps: ['1.1.1.1'], internalIps: ['2.2.2.2', '4.4.4.4'] } });
|
|
89
|
+
|
|
90
|
+
expect(wrapper.find('[data-testid="plus-more"]').text()).toStrictEqual('+1 more');
|
|
91
|
+
|
|
92
|
+
const popover = wrapper.find('[data-testid="ip-addresses-popover"]');
|
|
93
|
+
const ipSpans = popover.findAll('.ip-address span');
|
|
94
|
+
|
|
95
|
+
expect(ipSpans).toHaveLength(1);
|
|
96
|
+
expect(ipSpans[0].text()).toStrictEqual('4.4.4.4');
|
|
97
|
+
expect(popover.find('[data-testid="external-ip-list"]').exists()).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should show popover with remaining external and internal IPs', async() => {
|
|
101
|
+
const wrapper = mountComponent({ row: { externalIps: ['1.1.1.1', '3.3.3.3'], internalIps: ['2.2.2.2', '4.4.4.4'] } });
|
|
102
|
+
|
|
103
|
+
expect(wrapper.find('[data-testid="plus-more"]').text()).toStrictEqual('+2 more');
|
|
104
|
+
|
|
105
|
+
const popover = wrapper.find('[data-testid="ip-addresses-popover"]');
|
|
106
|
+
const externalIpSpans = popover.find('[data-testid="external-ip-list"]').findAll('.ip-address span');
|
|
107
|
+
const internalIpSpans = popover.find('[data-testid="internal-ip-list"]').findAll('.ip-address span');
|
|
108
|
+
|
|
109
|
+
expect(externalIpSpans).toHaveLength(1);
|
|
110
|
+
expect(externalIpSpans[0].text()).toStrictEqual('3.3.3.3');
|
|
111
|
+
expect(internalIpSpans).toHaveLength(1);
|
|
112
|
+
expect(internalIpSpans[0].text()).toStrictEqual('4.4.4.4');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('invalid IPs', () => {
|
|
117
|
+
it('should filter invalid IPs', () => {
|
|
118
|
+
const wrapper = mountComponent({ row: { externalIps: ['1.1.1.1', 'not-an-ip'], internalIps: ['2.2.2.2'] } });
|
|
119
|
+
|
|
120
|
+
expect(wrapper.find('[data-testid="external-ip"]').text()).toStrictEqual('1.1.1.1');
|
|
121
|
+
expect(wrapper.find('[data-testid="plus-more"]').exists()).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('tooltip', () => {
|
|
126
|
+
it('should display the correct tooltip text', () => {
|
|
127
|
+
const wrapper = mountComponent({ row: { externalIps: ['1.1.1.1', '3.3.3.3'], internalIps: ['2.2.2.2', '4.4.4.4'] } });
|
|
128
|
+
const badge = wrapper.find('[data-testid="plus-more"]');
|
|
129
|
+
|
|
130
|
+
expect(badge.attributes('v-clean-tooltip')).toBe('Click to show 2 more IP(s)');
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
});
|
package/components/nav/Group.vue
CHANGED
|
@@ -130,14 +130,23 @@ export default {
|
|
|
130
130
|
index = (found === -1) ? 0 : found;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
const
|
|
133
|
+
const item = items[index];
|
|
134
|
+
const route = item.route;
|
|
134
135
|
|
|
135
136
|
if (route) {
|
|
136
137
|
this.$router.replace(route);
|
|
138
|
+
} else if (item) {
|
|
139
|
+
this.routeToFirstChild(item);
|
|
137
140
|
}
|
|
138
141
|
}
|
|
139
142
|
},
|
|
140
143
|
|
|
144
|
+
routeToFirstChild(item) {
|
|
145
|
+
if (item.children.length && item.children[0].route) {
|
|
146
|
+
this.$router.replace(item.children[0].route);
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
|
|
141
150
|
selectType() {
|
|
142
151
|
this.groupSelected();
|
|
143
152
|
this.close();
|
|
@@ -455,7 +464,7 @@ export default {
|
|
|
455
464
|
}
|
|
456
465
|
|
|
457
466
|
&.depth-1 {
|
|
458
|
-
> .header {
|
|
467
|
+
> .accordion-item > .header {
|
|
459
468
|
padding-left: 20px;
|
|
460
469
|
> H6 {
|
|
461
470
|
line-height: 18px;
|
|
@@ -474,7 +483,7 @@ export default {
|
|
|
474
483
|
}
|
|
475
484
|
|
|
476
485
|
&:not(.depth-0) {
|
|
477
|
-
> .header {
|
|
486
|
+
> .accordion-item > .header {
|
|
478
487
|
> H6 {
|
|
479
488
|
// Child groups that aren't linked themselves
|
|
480
489
|
display: inline-block;
|
|
@@ -1086,7 +1086,7 @@ export default {
|
|
|
1086
1086
|
align-items: center;
|
|
1087
1087
|
cursor: pointer;
|
|
1088
1088
|
display: flex;
|
|
1089
|
-
color: var(--link);
|
|
1089
|
+
color: var(--on-tertiary, var(--link));
|
|
1090
1090
|
font-size: 14px;
|
|
1091
1091
|
height: $option-height;
|
|
1092
1092
|
white-space: nowrap;
|
|
@@ -1177,7 +1177,7 @@ export default {
|
|
|
1177
1177
|
.rancher-provider-icon,
|
|
1178
1178
|
svg {
|
|
1179
1179
|
margin-right: 16px;
|
|
1180
|
-
fill: var(--link);
|
|
1180
|
+
fill: var(--on-tertiary, var(--link));
|
|
1181
1181
|
}
|
|
1182
1182
|
|
|
1183
1183
|
.top-menu-icon {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
|
2
|
+
|
|
3
|
+
export const useInterval = (fn: Function, delay: number) => {
|
|
4
|
+
const interval = ref<any>(null);
|
|
5
|
+
|
|
6
|
+
onMounted(() => {
|
|
7
|
+
interval.value = setInterval(fn, delay);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
onBeforeUnmount(() => {
|
|
11
|
+
if (interval.value) {
|
|
12
|
+
clearInterval(interval.value);
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
};
|
|
@@ -69,7 +69,14 @@ export const CAPI = {
|
|
|
69
69
|
/**
|
|
70
70
|
* Annotation for overriding the cluster provider,
|
|
71
71
|
*/
|
|
72
|
-
UI_CUSTOM_PROVIDER: 'ui.rancher/provider'
|
|
72
|
+
UI_CUSTOM_PROVIDER: 'ui.rancher/provider',
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Annotations for autoscaler
|
|
76
|
+
*/
|
|
77
|
+
AUTOSCALER_CLUSTER_PAUSE: 'provisioning.cattle.io/cluster-autoscaler-paused',
|
|
78
|
+
AUTOSCALER_MACHINE_POOL_MIN_SIZE: 'cluster.x-k8s.io/cluster-api-autoscaler-node-group-min-size',
|
|
79
|
+
AUTOSCALER_MACHINE_POOL_MAX_SIZE: 'cluster.x-k8s.io/cluster-api-autoscaler-node-group-max-size'
|
|
73
80
|
};
|
|
74
81
|
|
|
75
82
|
export const CATALOG = {
|