@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.
Files changed (87) hide show
  1. package/assets/brand/suse/dark/rancher-logo.svg +64 -1
  2. package/assets/brand/suse/rancher-logo.svg +1 -1
  3. package/assets/styles/global/_cards.scss +0 -3
  4. package/assets/styles/themes/_modern.scss +9 -1
  5. package/assets/styles/themes/_suse.scss +81 -24
  6. package/assets/translations/en-us.yaml +68 -3
  7. package/components/AutoscalerCard.vue +113 -0
  8. package/components/AutoscalerTab.vue +94 -0
  9. package/components/ClusterIconMenu.vue +1 -1
  10. package/components/ClusterProviderIcon.vue +1 -1
  11. package/components/IconOrSvg.vue +2 -2
  12. package/components/PopoverCard.vue +192 -0
  13. package/components/Resource/Detail/FetchLoader/composables.ts +18 -4
  14. package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/identifying-fields.test.ts +1 -1
  15. package/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields.ts +4 -0
  16. package/components/Resource/Detail/ResourcePopover/ResourcePopoverCard.vue +2 -19
  17. package/components/Resource/Detail/ResourcePopover/__tests__/ResourcePopoverCard.test.ts +0 -29
  18. package/components/Resource/Detail/ResourcePopover/__tests__/index.test.ts +132 -150
  19. package/components/Resource/Detail/ResourcePopover/index.vue +54 -159
  20. package/components/ResourceDetail/Masthead/latest.vue +29 -0
  21. package/components/ResourceList/Masthead.vue +1 -1
  22. package/components/__tests__/AutoscalerCard.test.ts +154 -0
  23. package/components/__tests__/AutoscalerTab.test.ts +125 -0
  24. package/components/__tests__/PopoverCard.test.ts +204 -0
  25. package/components/formatter/Autoscaler.vue +97 -0
  26. package/components/formatter/InternalExternalIP.vue +195 -24
  27. package/components/formatter/__tests__/Autoscaler.test.ts +156 -0
  28. package/components/formatter/__tests__/InternalExternalIP.test.ts +133 -0
  29. package/components/nav/Group.vue +12 -3
  30. package/components/nav/TopLevelMenu.vue +2 -2
  31. package/composables/useInterval.ts +15 -0
  32. package/config/labels-annotations.js +8 -1
  33. package/config/product/manager.js +20 -9
  34. package/config/router/routes.js +4 -0
  35. package/config/settings.ts +2 -1
  36. package/config/table-headers.js +8 -0
  37. package/config/types.js +2 -0
  38. package/core/types-provisioning.ts +3 -0
  39. package/detail/provisioning.cattle.io.cluster.vue +12 -1
  40. package/directives/ui-context.ts +8 -2
  41. package/edit/auth/github.vue +5 -0
  42. package/edit/cloudcredential.vue +1 -1
  43. package/edit/fleet.cattle.io.gitrepo.vue +0 -10
  44. package/edit/provisioning.cattle.io.cluster/CustomCommand.vue +32 -5
  45. package/edit/provisioning.cattle.io.cluster/__tests__/CustomCommand.test.ts +35 -0
  46. package/edit/provisioning.cattle.io.cluster/__tests__/Networking.test.ts +132 -0
  47. package/edit/provisioning.cattle.io.cluster/index.vue +18 -12
  48. package/edit/provisioning.cattle.io.cluster/rke2.vue +39 -8
  49. package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +107 -5
  50. package/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue +90 -3
  51. package/initialize/install-plugins.js +3 -1
  52. package/list/provisioning.cattle.io.cluster.vue +15 -2
  53. package/machine-config/amazonec2.vue +36 -135
  54. package/machine-config/components/EC2Networking.vue +474 -0
  55. package/machine-config/components/__tests__/EC2Networking.test.ts +94 -0
  56. package/machine-config/components/__tests__/utils/vpcSubnetMockData.js +294 -0
  57. package/machine-config/digitalocean.vue +11 -0
  58. package/models/cluster/node.js +13 -6
  59. package/models/cluster.x-k8s.io.machine.js +10 -20
  60. package/models/cluster.x-k8s.io.machinedeployment.js +5 -1
  61. package/models/management.cattle.io.kontainerdriver.js +1 -0
  62. package/models/provisioning.cattle.io.cluster.js +223 -2
  63. package/package.json +1 -1
  64. package/pages/c/_cluster/apps/charts/install.vue +1 -1
  65. package/pages/c/_cluster/manager/hostedprovider/index.vue +209 -0
  66. package/plugins/dynamic-content.js +13 -0
  67. package/rancher-components/Form/Checkbox/Checkbox.vue +1 -1
  68. package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +8 -0
  69. package/store/features.js +1 -0
  70. package/store/notifications.ts +32 -1
  71. package/store/plugins.js +7 -3
  72. package/store/prefs.js +1 -0
  73. package/types/notifications/index.ts +24 -3
  74. package/types/shell/index.d.ts +26 -1
  75. package/utils/__tests__/object.test.ts +19 -0
  76. package/utils/autoscaler-utils.ts +7 -0
  77. package/utils/dynamic-content/__tests__/announcement.test.ts +498 -0
  78. package/utils/dynamic-content/announcement.ts +112 -0
  79. package/utils/dynamic-content/example.json +40 -0
  80. package/utils/dynamic-content/index.ts +6 -2
  81. package/utils/dynamic-content/new-release.ts +1 -1
  82. package/utils/dynamic-content/notification-handler.ts +48 -0
  83. package/utils/dynamic-content/types.d.ts +33 -1
  84. package/utils/object.js +20 -2
  85. package/utils/scroll.js +7 -0
  86. package/utils/settings.ts +15 -0
  87. 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.row.internalIp === this.row.externalIp;
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
- ...mapGetters({ t: 'i18n/t' })
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
- <span>
29
- <template v-if="isIp(row.externalIp)">
30
- {{ row.externalIp }} <CopyToClipboard
31
- :aria-label="t('internalExternalIP.copyExternalIp')"
32
- label-as="tooltip"
33
- :text="row.externalIp"
34
- class="icon-btn"
35
- action-color="bg-transparent"
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 && isIp(row.internalIp)">
85
+ <span class="separator">/</span>
86
+ <template v-if="internalSameAsExternal">
43
87
  {{ t('tableHeaders.internalIpSameAsExternal') }}
44
88
  </template>
45
- <template v-else-if="isIp(row.internalIp)">
46
- {{ row.internalIp }}<CopyToClipboard
47
- :aria-label="t('internalExternalIP.copyInternalIp')"
48
- label-as="tooltip"
49
- :text="row.internalIp"
50
- class="icon-btn"
51
- action-color="bg-transparent"
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
- </span>
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
- margin-left: 8px;
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
+ });
@@ -130,14 +130,23 @@ export default {
130
130
  index = (found === -1) ? 0 : found;
131
131
  }
132
132
 
133
- const route = items[index].route;
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 = {