@rancher/shell 3.0.0-rc.7 → 3.0.0-rc.8

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 (37) hide show
  1. package/assets/styles/global/_tooltip.scss +1 -12
  2. package/assets/translations/en-us.yaml +13 -1
  3. package/components/CodeMirror.vue +18 -15
  4. package/components/PromptRemove.vue +2 -2
  5. package/components/Questions/index.vue +2 -2
  6. package/components/ResourceDetail/Masthead.vue +1 -0
  7. package/components/ResourceDetail/index.vue +6 -7
  8. package/components/auth/RoleDetailEdit.vue +1 -6
  9. package/components/auth/__tests__/RoleDetailEdit.test.ts +53 -16
  10. package/components/form/ArrayList.vue +7 -3
  11. package/components/formatter/CloudCredExpired.vue +69 -0
  12. package/components/formatter/Date.vue +1 -1
  13. package/components/nav/TopLevelMenu.vue +115 -51
  14. package/components/nav/__tests__/TopLevelMenu.test.ts +49 -23
  15. package/config/labels-annotations.js +9 -5
  16. package/core/types.ts +1 -1
  17. package/detail/provisioning.cattle.io.cluster.vue +0 -4
  18. package/edit/management.cattle.io.project.vue +4 -1
  19. package/edit/provisioning.cattle.io.cluster/index.vue +7 -2
  20. package/edit/provisioning.cattle.io.cluster/rke2.vue +28 -5
  21. package/list/provisioning.cattle.io.cluster.vue +57 -10
  22. package/machine-config/vmwarevsphere.vue +133 -95
  23. package/mixins/vue-select-overrides.js +0 -1
  24. package/models/catalog.cattle.io.app.js +4 -3
  25. package/models/cloudcredential.js +158 -2
  26. package/models/management.cattle.io.globalrole.js +6 -0
  27. package/models/management.cattle.io.roletemplate.js +6 -0
  28. package/models/nodedriver.js +5 -0
  29. package/models/provisioning.cattle.io.cluster.js +35 -1
  30. package/package.json +1 -1
  31. package/pages/c/_cluster/apps/charts/index.vue +0 -6
  32. package/pages/c/_cluster/manager/cloudCredential/index.vue +68 -4
  33. package/pages/home.vue +1 -0
  34. package/plugins/dashboard-store/mutations.js +24 -3
  35. package/plugins/dashboard-store/resource-class.js +6 -0
  36. package/store/type-map.js +4 -2
  37. package/types/shell/index.d.ts +12 -1
@@ -15,10 +15,7 @@
15
15
  }
16
16
 
17
17
  .v-popper__arrow-container {
18
- width: 0;
19
- height: 0;
20
18
  border: 0 solid transparent;
21
- position: absolute;
22
19
  z-index: 1;
23
20
 
24
21
  .v-popper__arrow-inner {
@@ -30,9 +27,7 @@
30
27
  .v-popper__arrow-container {
31
28
 
32
29
  .v-popper__arrow-outer {
33
- border-bottom-width: 0;
34
30
  border-top-color: var(--tooltip-bg);
35
- left: -$triangle-inner-size;
36
31
  }
37
32
  }
38
33
  }
@@ -42,9 +37,7 @@
42
37
  .v-popper__arrow-container {
43
38
 
44
39
  .v-popper__arrow-outer {
45
- border-top-width: 0;
46
40
  border-bottom-color: var(--tooltip-bg);
47
- left: -$triangle-inner-size;
48
41
  background: transparent;
49
42
  }
50
43
  }
@@ -55,8 +48,6 @@
55
48
 
56
49
  .v-popper__arrow-outer {
57
50
  border-right-color: var(--tooltip-bg);
58
- top: -$triangle-inner-size;
59
- border-left-width: 0;
60
51
  }
61
52
  }
62
53
  }
@@ -65,9 +56,7 @@
65
56
  .v-popper__arrow-container {
66
57
 
67
58
  .v-popper__arrow-outer {
68
- border-right-width: 0;
69
59
  border-left-color: var(--tooltip-bg);
70
- top: -$triangle-inner-size;
71
60
  }
72
61
  }
73
62
  }
@@ -173,4 +162,4 @@
173
162
  //icon tooltip
174
163
  .icon-info.v-popper--has-tooltip {
175
164
  font-size: 14px;
176
- }
165
+ }
@@ -1822,6 +1822,13 @@ cluster:
1822
1822
  other { {pool_name}: The provided values for {fields} were not found in the list of expected values. This can happen with clusters provisioned outside of Rancher or when options for the provider have changed. }
1823
1823
  }
1824
1824
  rkeTemplateUpgrade: Template revision {name} available for upgrade
1825
+ cloudCredentials:
1826
+ renew: Renew Cloud Credentials
1827
+ expired: Cloud Credential expired, please Renew Cloud Credentials
1828
+ expiring: Cloud Credential will expire on { expires }, please Renew Cloud Credentials
1829
+ banners:
1830
+ expiring: "{count} {count, plural, =1 { Cluster has a Cloud Credential that expires soon} other { Clusters have Cloud Credentials that expire soon}}, please Renew Cloud {count, plural, =1 { Credential} other { Credentials}}"
1831
+ expired: "{count} {count, plural, =1 { Cluster has a Cloud Credential that has expired} other { Clusters have Cloud Credentials that have expired}}, please Renew Cloud {count, plural, =1 { Credential} other { Credentials}}"
1825
1832
 
1826
1833
  architecture:
1827
1834
  label:
@@ -5655,7 +5662,6 @@ tableHeaders:
5655
5662
  createdAt: Created At
5656
5663
  customVerbs: Custom Verbs
5657
5664
  description: Description
5658
- expires: Expires
5659
5665
  cpu: CPU
5660
5666
  currentReplicas: Current Replicas
5661
5667
  date: Date
@@ -5672,6 +5678,7 @@ tableHeaders:
5672
5678
  distinguisherMethod: Distinguisher Method
5673
5679
  effect: Effect
5674
5680
  endpoints: Endpoints
5681
+ expires: Expires
5675
5682
  firstSeen: First Seen
5676
5683
  fleetBundleType: Type
5677
5684
  flow: Flow
@@ -7751,6 +7758,11 @@ volumeClaimTemplate:
7751
7758
  manager:
7752
7759
  cloudCredentials:
7753
7760
  label: Cloud Credentials
7761
+ renew: Renew
7762
+ expired: Expired
7763
+ banners:
7764
+ expiring: "{count} Cloud {count, plural, =1 { Credential expires soon} other { Credentials expire soon}}, please Renew"
7765
+ expired: "{count} Cloud {count, plural, =1 { Credential has expired} other { Credentials have expired}}, please Renew"
7754
7766
  drivers:
7755
7767
  label: Drivers
7756
7768
  rkeTemplates:
@@ -292,11 +292,24 @@ export default {
292
292
  }
293
293
  }
294
294
 
295
- .code-mirror .codemirror-container {
296
- z-index: 0;
297
- font-size: inherit !important;
295
+ .code-mirror {
296
+ position: relative;
297
+
298
+ .codemirror-container {
299
+ z-index: 0;
300
+ font-size: inherit !important;
301
+
302
+ //rm no longer extant selector
303
+ .CodeMirror {
304
+ height: initial;
305
+ background: none
306
+ }
307
+
308
+ .CodeMirror-gutters {
309
+ background: inherit;
310
+ }
311
+ }
298
312
 
299
- // Keyboard mapping overlap
300
313
  .keymap.overlay {
301
314
  position: absolute;
302
315
  display: flex;
@@ -352,16 +365,6 @@ export default {
352
365
  }
353
366
  }
354
367
  }
355
-
356
- //rm no longer extant selector
357
- .CodeMirror {
358
- height: initial;
359
- background: none
360
- }
361
-
362
- .CodeMirror-gutters {
363
- background: inherit;
364
- }
365
-
366
368
  }
369
+
367
370
  </style>
@@ -105,8 +105,8 @@ export default {
105
105
  return null;
106
106
  }
107
107
 
108
- if (this.toRemove[0].doneLocationRemove) {
109
- return this.toRemove[0].doneLocationRemove;
108
+ if (this.toRemove[0].doneOverride) {
109
+ return this.toRemove[0].doneOverride;
110
110
  }
111
111
 
112
112
  const currentRoute = this.toRemove[0].currentRoute();
@@ -45,9 +45,9 @@ export function componentForQuestion(q) {
45
45
 
46
46
  if ( knownTypes[type] ) {
47
47
  return type;
48
- } else if ( type.startsWith('array[') ) { // This only really works for array[string|multiline], but close enough for now.
48
+ } else if ( type.startsWith('array') ) { // This only really works for array[string|multiline], but close enough for now.
49
49
  return ArrayType;
50
- } else if ( type.startsWith('map[') ) { // Same, only works with map[string|multiline]
50
+ } else if ( type.startsWith('map') ) { // Same, only works with map[string|multiline]
51
51
  return MapType;
52
52
  } else if ( type.startsWith('reference[') ) { // Same, only works with map[string|multiline]
53
53
  return ReferenceType;
@@ -521,6 +521,7 @@ export default {
521
521
  <button
522
522
  v-if="isView"
523
523
  ref="actions"
524
+ data-testid="mathead-action-menu"
524
525
  aria-haspopup="true"
525
526
  type="button"
526
527
  class="btn role-multi-action actions"
@@ -318,9 +318,12 @@ export default {
318
318
  },
319
319
 
320
320
  watch: {
321
- '$route.query'(inNeu, inOld) {
322
- const neu = clone(inNeu);
323
- const old = clone(inOld);
321
+ '$route'(current, prev) {
322
+ if (current.name !== prev.name) {
323
+ return;
324
+ }
325
+ const neu = clone(current.query);
326
+ const old = clone(prev.query);
324
327
 
325
328
  delete neu[PREVIEW];
326
329
  delete old[PREVIEW];
@@ -332,10 +335,6 @@ export default {
332
335
 
333
336
  const queryDiff = Object.keys(diff(neu, old));
334
337
 
335
- if (Object.keys(neu).length <= 0) {
336
- return;
337
- }
338
-
339
338
  if (queryDiff.includes(MODE) || queryDiff.includes(AS)) {
340
339
  this.$fetch();
341
340
  }
@@ -127,12 +127,6 @@ export default {
127
127
 
128
128
  created() {
129
129
  this.value['rules'] = this.value.rules || [];
130
- this.value.rules.forEach((rule) => {
131
- if (rule.verbs[0] === '*') {
132
- rule['verbs'] = [...VERBS];
133
- }
134
- });
135
-
136
130
  const query = { ...this.$route.query };
137
131
  const { roleContext } = query;
138
132
 
@@ -701,6 +695,7 @@ export default {
701
695
  <template #columns="props">
702
696
  <div class="columns row mr-20">
703
697
  <div :class="ruleClass">
698
+ <!-- Select verbs -->
704
699
  <Select
705
700
  :value="props.row.value.verbs"
706
701
  class="lg"
@@ -2,22 +2,20 @@ import { mount } from '@vue/test-utils';
2
2
  import RoleDetailEdit from '@shell/components/auth/RoleDetailEdit.vue';
3
3
  import { SUBTYPE_MAPPING } from '@shell/models/management.cattle.io.roletemplate';
4
4
 
5
- const role = {
6
- apiVersion: 'management.cattle.io/v3',
7
- kind: 'GlobalRole',
8
- metadata: { name: 'global-role-with-inherited' },
9
- inheritedClusterRoles: ['cluster-admin'],
10
- rules:
11
- [{
12
- verbs: ['get', 'list'],
13
- resources: ['pods'],
14
- apiGroups: ['']
15
- }],
16
- subtype: SUBTYPE_MAPPING.GLOBAL.id
17
- };
18
-
19
5
  describe('component: RoleDetailEdit', () => {
20
6
  it('does not have validation errors when the role has no displayName', () => {
7
+ const role = {
8
+ apiVersion: 'management.cattle.io/v3',
9
+ kind: 'GlobalRole',
10
+ metadata: { name: 'global-role-with-inherited' },
11
+ inheritedClusterRoles: ['cluster-admin'],
12
+ rules: [{
13
+ verbs: ['get', 'list'],
14
+ resources: ['pods'],
15
+ apiGroups: ['']
16
+ }],
17
+ subtype: SUBTYPE_MAPPING.GLOBAL.id
18
+ };
21
19
  const wrapper = mount(RoleDetailEdit, {
22
20
  props: { value: role },
23
21
 
@@ -28,14 +26,16 @@ describe('component: RoleDetailEdit', () => {
28
26
  $store: {
29
27
  dispatch: jest.fn(),
30
28
  getters: {
31
- currentStore: () => 'store', 'i18n/t': jest.fn(), 'store/schemaFor': jest.fn(), 'store/customisation/': jest.fn()
29
+ currentStore: () => 'store',
30
+ 'i18n/t': jest.fn(),
31
+ 'store/schemaFor': jest.fn(),
32
+ 'store/customisation/': jest.fn()
32
33
  }
33
34
  }
34
35
  },
35
36
 
36
37
  stubs: {
37
38
  CruResource: { template: '<div><slot></slot></div>' },
38
- // NameNsDescription: true,
39
39
  Tab: { template: '<div><slot></slot></div>' },
40
40
  },
41
41
  },
@@ -43,4 +43,41 @@ describe('component: RoleDetailEdit', () => {
43
43
 
44
44
  expect((wrapper.vm as any).fvFormIsValid).toBe(true);
45
45
  });
46
+
47
+ it.each([
48
+ [['*']],
49
+ [['create', 'delete', 'get', 'list', 'patch', 'update', 'watch']],
50
+ ])('should display the verbs %p', (verbs: string[]) => {
51
+ const wrapper = mount(RoleDetailEdit, {
52
+ props: {
53
+ value: {
54
+ rules: [{ verbs }],
55
+ subtype: 'GLOBAL'
56
+ },
57
+ },
58
+
59
+ global: {
60
+ mocks: {
61
+ $fetchState: { pending: false },
62
+ $route: { name: 'anything' },
63
+ $store: {
64
+ dispatch: jest.fn(),
65
+ getters: {
66
+ currentStore: () => 'store',
67
+ 'i18n/t': jest.fn(),
68
+ 'store/schemaFor': jest.fn(),
69
+ 'store/customisation/': jest.fn()
70
+ }
71
+ }
72
+ },
73
+
74
+ stubs: {
75
+ CruResource: { template: '<div><slot></slot></div>' },
76
+ Tab: { template: '<div><slot></slot></div>' },
77
+ },
78
+ },
79
+ });
80
+
81
+ expect(wrapper.vm.value.rules[0].verbs).toStrictEqual(verbs);
82
+ });
46
83
  });
@@ -138,10 +138,14 @@ export default {
138
138
  }
139
139
  },
140
140
  watch: {
141
- value() {
142
- this.lastUpdateWasFromValue = true;
143
- this.rows = (this.value || []).map((v) => ({ value: v }));
141
+ value: {
142
+ deep: true,
143
+ handler() {
144
+ this.lastUpdateWasFromValue = true;
145
+ this.rows = (this.value || []).map((v) => ({ value: v }));
146
+ }
144
147
  },
148
+
145
149
  rows: {
146
150
  deep: true,
147
151
  handler(newValue, oldValue) {
@@ -0,0 +1,69 @@
1
+ <script>
2
+
3
+ export default {
4
+ props: {
5
+ value: {
6
+ type: [Number, String],
7
+ required: true
8
+ },
9
+
10
+ row: {
11
+ type: Object,
12
+ default: () => {
13
+ return {};
14
+ }
15
+ },
16
+
17
+ verbose: {
18
+ type: Boolean,
19
+ default: false,
20
+ }
21
+
22
+ },
23
+
24
+ computed: {
25
+ outputString() {
26
+ return this.verbose ? this.verboseOutputString : this.row.expiresString;
27
+ },
28
+
29
+ verboseOutputString() {
30
+ const expireData = this.row?.expireData;
31
+
32
+ if (expireData?.expired) {
33
+ return this.t('cluster.cloudCredentials.expired');
34
+ } else if (expireData?.expiring) {
35
+ return this.t('cluster.cloudCredentials.expiring', { expires: this.row.expiresString });
36
+ }
37
+
38
+ return null;
39
+ }
40
+ }
41
+ };
42
+ </script>
43
+
44
+ <template>
45
+ <div
46
+ v-if="outputString"
47
+ class="cloud-cred-expired"
48
+ :class="{ 'text-error': row.expireData.expired, 'text-warning': row.expireData.expiring}"
49
+ >
50
+ <div class="token-icon mr-5">
51
+ <i
52
+ class="icon"
53
+ :class="{'icon-error': row.expireData.expired, 'icon-warning': row.expireData.expiring}"
54
+ />
55
+ </div>
56
+ {{ outputString }}
57
+ </div>
58
+ </template>
59
+
60
+ <style lang="scss" scoped>
61
+ .cloud-cred-expired {
62
+ display: flex;
63
+ align-items: center;
64
+ .token-icon {
65
+ display: flex;
66
+ align-items: center;
67
+ }
68
+ }
69
+ </style>
@@ -11,7 +11,7 @@ export default {
11
11
  },
12
12
 
13
13
  value: {
14
- type: [String, Date],
14
+ type: [String, Date, Number],
15
15
  default: ''
16
16
  },
17
17
 
@@ -76,58 +76,102 @@ export default {
76
76
  return this.clusters.length > this.maxClustersToShow;
77
77
  },
78
78
 
79
+ /**
80
+ * Filter mgmt clusters by
81
+ * 1. Harvester type 1 (filterOnlyKubernetesClusters)
82
+ * 2. Harvester type 2 (filterHiddenLocalCluster)
83
+ * 3. There's a matching prov cluster
84
+ *
85
+ * Convert remaining clusters to special format
86
+ */
79
87
  clusters() {
80
- const all = this.$store.getters['management/all'](MANAGEMENT.CLUSTER);
81
- let kubeClusters = filterHiddenLocalCluster(filterOnlyKubernetesClusters(all, this.$store), this.$store);
82
- let pClusters = null;
88
+ if (!this.hasProvCluster) {
89
+ // We're filtering out mgmt clusters without prov clusters, so if the user can't see any prov clusters at all
90
+ // exit early
91
+ return [];
92
+ }
83
93
 
84
- if (this.hasProvCluster) {
85
- pClusters = this.$store.getters['management/all'](CAPI.RANCHER_CLUSTER);
86
- const available = pClusters.reduce((p, c) => {
87
- p[c.mgmt] = true;
94
+ const all = this.$store.getters['management/all'](MANAGEMENT.CLUSTER);
95
+ const mgmtClusters = filterHiddenLocalCluster(filterOnlyKubernetesClusters(all, this.$store), this.$store);
96
+ const provClusters = this.$store.getters['management/all'](CAPI.RANCHER_CLUSTER);
97
+ const provClustersByMgmtId = provClusters.reduce((res, provCluster) => {
98
+ if (provCluster.mgmt?.id) {
99
+ res[provCluster.mgmt.id] = provCluster;
100
+ }
88
101
 
89
- return p;
90
- }, {});
102
+ return res;
103
+ }, {});
91
104
 
105
+ return (mgmtClusters || []).reduce((res, mgmtCluster) => {
92
106
  // Filter to only show mgmt clusters that exist for the available provisioning clusters
93
107
  // Addresses issue where a mgmt cluster can take some time to get cleaned up after the corresponding
94
108
  // provisioning cluster has been deleted
95
- kubeClusters = kubeClusters.filter((c) => !!available[c]);
96
- }
109
+ if (!provClustersByMgmtId[mgmtCluster.id]) {
110
+ return res;
111
+ }
97
112
 
98
- return kubeClusters?.map((x) => {
99
- const pCluster = pClusters?.find((c) => c.mgmt?.id === x.id);
113
+ const pCluster = provClustersByMgmtId[mgmtCluster.id];
114
+
115
+ res.push({
116
+ id: mgmtCluster.id,
117
+ label: mgmtCluster.nameDisplay,
118
+ ready: mgmtCluster.isReady && !pCluster?.hasError,
119
+ osLogo: mgmtCluster.providerOsLogo,
120
+ providerNavLogo: mgmtCluster.providerMenuLogo,
121
+ badge: mgmtCluster.badge,
122
+ isLocal: mgmtCluster.isLocal,
123
+ isHarvester: mgmtCluster.isHarvester,
124
+ pinned: mgmtCluster.pinned,
125
+ description: pCluster?.description || mgmtCluster.description,
126
+ pin: () => mgmtCluster.pin(),
127
+ unpin: () => mgmtCluster.unpin(),
128
+ clusterRoute: { name: 'c-cluster-explorer', params: { cluster: mgmtCluster.id } }
129
+ });
100
130
 
101
- return {
102
- id: x.id,
103
- label: x.nameDisplay,
104
- ready: x.isReady && !pCluster?.hasError,
105
- osLogo: x.providerOsLogo,
106
- providerNavLogo: x.providerMenuLogo,
107
- badge: x.badge,
108
- isLocal: x.isLocal,
109
- isHarvester: x.isHarvester,
110
- pinned: x.pinned,
111
- description: pCluster?.description || x.description,
112
- pin: () => x.pin(),
113
- unpin: () => x.unpin(),
114
- clusterRoute: { name: 'c-cluster-explorer', params: { cluster: x.id } }
115
- };
116
- }) || [];
131
+ return res;
132
+ }, []);
117
133
  },
118
134
 
135
+ /**
136
+ * Filter clusters by
137
+ * 1. Not pinned
138
+ * 2. Includes search term
139
+ *
140
+ * Sort remaining clusters
141
+ *
142
+ * Reduce number of clusters if too many too show
143
+ *
144
+ * Important! This is used to show unpinned clusters OR results of search
145
+ */
119
146
  clustersFiltered() {
120
147
  const search = (this.clusterFilter || '').toLowerCase();
121
- const out = search ? this.clusters.filter((item) => item.label?.toLowerCase().includes(search)) : this.clusters;
122
- const sorted = sortBy(out, ['ready:desc', 'label']);
148
+ let localCluster = null;
123
149
 
124
- // put local cluster on top of list always
125
- // https://github.com/rancher/dashboard/issues/10975
126
- if (sorted.findIndex((c) => c.id === 'local') > 0) {
127
- const localCluster = sorted.find((c) => c.id === 'local');
128
- const localIndex = sorted.findIndex((c) => c.id === 'local');
150
+ const filtered = this.clusters.filter((c) => {
151
+ // If we're searching we don't care if pinned or not
152
+ if (search) {
153
+ if (!c.label?.toLowerCase().includes(search)) {
154
+ return false;
155
+ }
156
+ } else if (c.pinned) {
157
+ // Not searching, not pinned, don't care
158
+ return false;
159
+ }
160
+
161
+ if (!localCluster && c.id === 'local') {
162
+ // Local cluster is a special case, we're inserting it at top so don't include in the middle
163
+ localCluster = c;
164
+
165
+ return false;
166
+ }
129
167
 
130
- sorted.splice(localIndex, 1);
168
+ return true;
169
+ });
170
+
171
+ const sorted = sortBy(filtered, ['ready:desc', 'label']);
172
+
173
+ // put local cluster on top of list always - https://github.com/rancher/dashboard/issues/10975
174
+ if (localCluster) {
131
175
  sorted.unshift(localCluster);
132
176
  }
133
177
 
@@ -141,25 +185,45 @@ export default {
141
185
  this.searchActive = false;
142
186
 
143
187
  if (sorted.length >= this.maxClustersToShow) {
144
- const sortedPinOut = sorted.filter((item) => !item.pinned).slice(0, this.maxClustersToShow);
145
-
146
- return sortedPinOut;
147
- } else {
148
- return sorted.filter((item) => !item.pinned);
188
+ return sorted.slice(0, this.maxClustersToShow);
149
189
  }
190
+
191
+ return sorted;
150
192
  },
151
193
 
194
+ /**
195
+ * Filter clusters by
196
+ * 1. Not pinned
197
+ * 2. Includes search term
198
+ *
199
+ * Sort remaining clusters
200
+ *
201
+ * Reduce number of clusters if too many too show
202
+ *
203
+ * Important! This is hidden if there's a filter (user searching)
204
+ */
152
205
  pinFiltered() {
153
- const out = this.clusters.filter((item) => item.pinned);
154
- const sorted = sortBy(out, ['ready:desc', 'label']);
206
+ let localCluster = null;
207
+ const filtered = this.clusters.filter((c) => {
208
+ if (!c.pinned) {
209
+ // We only care about pinned clusters
210
+ return false;
211
+ }
212
+
213
+ if (c.id === 'local') {
214
+ // Special case, we're going to add this at the start so filter out
215
+ localCluster = c;
216
+
217
+ return false;
218
+ }
219
+
220
+ return true;
221
+ });
155
222
 
156
- // put local cluster on top of list always
157
- // https://github.com/rancher/dashboard/issues/10975
158
- if (sorted.findIndex((c) => c.id === 'local') > 0) {
159
- const localCluster = sorted.find((c) => c.id === 'local');
160
- const localIndex = sorted.findIndex((c) => c.id === 'local');
223
+ const sorted = sortBy(filtered, ['ready:desc', 'label']);
161
224
 
162
- sorted.splice(localIndex, 1);
225
+ // put local cluster on top of list always - https://github.com/rancher/dashboard/issues/10975
226
+ if (localCluster) {
163
227
  sorted.unshift(localCluster);
164
228
  }
165
229
 
@@ -167,7 +231,7 @@ export default {
167
231
  },
168
232
 
169
233
  pinnedClustersHeight() {
170
- const pinCount = this.clusters.filter((item) => item.pinned).length;
234
+ const pinCount = this.pinFiltered.length;
171
235
  const height = pinCount > 2 ? (pinCount * 43) : 90;
172
236
 
173
237
  return `min-height: ${ height }px`;