@rancher/shell 3.0.1-rc.4 → 3.0.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.
Files changed (64) hide show
  1. package/assets/data/aws-regions.json +1 -0
  2. package/assets/styles/base/_basic.scss +5 -0
  3. package/assets/styles/base/_mixins.scss +8 -0
  4. package/assets/styles/global/_button.scss +5 -0
  5. package/assets/styles/themes/_dark.scss +2 -0
  6. package/assets/styles/themes/_light.scss +2 -0
  7. package/assets/translations/en-us.yaml +27 -11
  8. package/assets/translations/zh-hans.yaml +1 -1
  9. package/chart/monitoring/StorageClassSelector.vue +1 -1
  10. package/components/AssignTo.vue +1 -0
  11. package/components/AsyncButton.vue +1 -0
  12. package/components/BackLink.vue +8 -2
  13. package/components/PaginatedResourceTable.vue +135 -0
  14. package/components/ResourceList/index.vue +0 -1
  15. package/components/ResourceTable.vue +6 -1
  16. package/components/SortableTable/index.vue +8 -6
  17. package/components/Tabbed/index.vue +35 -2
  18. package/components/form/ResourceLabeledSelect.vue +2 -2
  19. package/components/form/ResourceTabs/index.vue +0 -23
  20. package/components/form/Taints.vue +1 -1
  21. package/components/nav/TopLevelMenu.helper.ts +546 -0
  22. package/components/nav/TopLevelMenu.vue +124 -159
  23. package/components/nav/__tests__/TopLevelMenu.test.ts +338 -326
  24. package/config/pagination-table-headers.js +4 -4
  25. package/config/product/explorer.js +2 -0
  26. package/config/router/routes.js +1 -1
  27. package/config/settings.ts +13 -1
  28. package/core/plugin.ts +8 -1
  29. package/core/types-provisioning.ts +5 -0
  30. package/core/types.ts +26 -1
  31. package/dialog/DrainNode.vue +6 -6
  32. package/edit/catalog.cattle.io.clusterrepo.vue +95 -52
  33. package/edit/provisioning.cattle.io.cluster/index.vue +8 -3
  34. package/list/node.vue +8 -5
  35. package/mixins/resource-fetch-api-pagination.js +40 -5
  36. package/mixins/resource-fetch.js +48 -5
  37. package/models/management.cattle.io.nodepool.js +5 -4
  38. package/models/provisioning.cattle.io.cluster.js +2 -10
  39. package/package.json +6 -6
  40. package/pages/about.vue +22 -0
  41. package/pages/c/_cluster/explorer/__tests__/index.test.ts +36 -24
  42. package/pages/c/_cluster/explorer/index.vue +100 -59
  43. package/pages/home.vue +308 -123
  44. package/plugins/dashboard-store/__tests__/mutations.test.ts +2 -0
  45. package/plugins/dashboard-store/actions.js +29 -19
  46. package/plugins/dashboard-store/getters.js +5 -2
  47. package/plugins/dashboard-store/mutations.js +4 -2
  48. package/plugins/steve/__tests__/mutations.test.ts +2 -1
  49. package/plugins/steve/steve-pagination-utils.ts +25 -2
  50. package/plugins/steve/subscribe.js +22 -8
  51. package/scripts/extension/parse-tag-name +2 -0
  52. package/scripts/test-plugins-build.sh +1 -0
  53. package/store/index.js +31 -9
  54. package/tsconfig.json +7 -1
  55. package/types/resources/settings.d.ts +1 -1
  56. package/types/shell/index.d.ts +1107 -1276
  57. package/types/store/dashboard-store.types.ts +4 -0
  58. package/types/store/pagination.types.ts +13 -0
  59. package/types/store/vuex.d.ts +8 -0
  60. package/types/vue-shim.d.ts +6 -31
  61. package/utils/cluster.js +92 -1
  62. package/utils/pagination-utils.ts +17 -8
  63. package/utils/pagination-wrapper.ts +70 -0
  64. package/utils/uiplugins.ts +18 -4
@@ -0,0 +1,546 @@
1
+ import { CAPI, MANAGEMENT } from '@shell/config/types';
2
+ import { PaginationParam, PaginationParamFilter, PaginationSort } from '@shell/types/store/pagination.types';
3
+ import { VuexStore } from '@shell/types/store/vuex';
4
+ import { filterHiddenLocalCluster, filterOnlyKubernetesClusters, paginationFilterClusters } from '@shell/utils/cluster';
5
+ import PaginationWrapper from '@shell/utils/pagination-wrapper';
6
+ import { allHash } from '@shell/utils/promise';
7
+ import { sortBy } from '@shell/utils/sort';
8
+ import { LocationAsRelativeRaw } from 'vue-router';
9
+
10
+ interface TopLevelMenuCluster {
11
+ id: string,
12
+ label: string,
13
+ ready: boolean
14
+ providerNavLogo: string,
15
+ badge: string,
16
+ isLocal: boolean,
17
+ pinned: boolean,
18
+ description: string,
19
+ pin: () => void,
20
+ unpin: () => void,
21
+ clusterRoute: LocationAsRelativeRaw,
22
+ }
23
+
24
+ interface UpdateArgs {
25
+ searchTerm: string,
26
+ pinnedIds: string[],
27
+ unPinnedMax?: number,
28
+ }
29
+
30
+ type MgmtCluster = {
31
+ [key: string]: any
32
+ }
33
+
34
+ type ProvCluster = {
35
+ [key: string]: any
36
+ }
37
+
38
+ /**
39
+ * Order
40
+ * 1. local cluster - https://github.com/rancher/dashboard/issues/10975
41
+ * 2. working clusters
42
+ * 3. name
43
+ */
44
+ const DEFAULT_SORT: Array<PaginationSort> = [
45
+ {
46
+ asc: false,
47
+ field: 'spec.internal',
48
+ },
49
+ // {
50
+ // asc: true,
51
+ // field: 'status.conditions[0].status' // Pending API changes https://github.com/rancher/rancher/issues/48092
52
+ // },
53
+ {
54
+ asc: true,
55
+ field: 'spec.displayName',
56
+ },
57
+ ];
58
+
59
+ export interface TopLevelMenuHelper {
60
+ /**
61
+ * Filter mgmt clusters by
62
+ * 1. If harvester or not (filterOnlyKubernetesClusters)
63
+ * 2. If local or not (filterHiddenLocalCluster)
64
+ * 3. Is pinned
65
+ *
66
+ * Sort By
67
+ * 1. is local cluster (appears at top)
68
+ * 2. ready
69
+ * 3. name
70
+ */
71
+ clustersPinned: Array<TopLevelMenuCluster>;
72
+
73
+ /**
74
+ * Filter mgmt clusters by
75
+ * 1. If harvester or not (filterOnlyKubernetesClusters)
76
+ * 2. If local or not (filterHiddenLocalCluster)
77
+ * 3.
78
+ * a) if search term, filter on it
79
+ * b) if no search term, filter on pinned
80
+ *
81
+ * Sort By
82
+ * 1. is local cluster (appears at top)
83
+ * 2. ready
84
+ * 3. name
85
+ */
86
+ clustersOthers: Array<TopLevelMenuCluster>;
87
+
88
+ update: (args: UpdateArgs) => Promise<void>
89
+ }
90
+
91
+ export abstract class BaseTopLevelMenuHelper {
92
+ protected $store: VuexStore;
93
+ protected hasProvCluster: boolean;
94
+
95
+ /**
96
+ * Filter mgmt clusters by
97
+ * 1. If harvester or not (filterOnlyKubernetesClusters)
98
+ * 2. If local or not (filterHiddenLocalCluster)
99
+ * 3. Is pinned
100
+ *
101
+ * Why aren't we filtering these by search term? Because we don't show pinned when filtering on search term
102
+ *
103
+ * Sort By
104
+ * 1. is local cluster (appears at top)
105
+ * 2. ready
106
+ * 3. name
107
+ */
108
+ public clustersPinned: Array<TopLevelMenuCluster> = [];
109
+
110
+ /**
111
+ * Filter mgmt clusters by
112
+ * 1. If harvester or not (filterOnlyKubernetesClusters)
113
+ * 2. If local or not (filterHiddenLocalCluster)
114
+ * 3.
115
+ * a) if search term, filter on it
116
+ * b) if no search term, filter on pinned
117
+ *
118
+ * Sort By
119
+ * 1. is local cluster (appears at top)
120
+ * 2. ready
121
+ * 3. name
122
+ */
123
+ public clustersOthers: Array<TopLevelMenuCluster> = [];
124
+
125
+ constructor({ $store }: {
126
+ $store: VuexStore,
127
+ }) {
128
+ this.$store = $store;
129
+
130
+ this.hasProvCluster = this.$store.getters[`management/schemaFor`](CAPI.RANCHER_CLUSTER);
131
+ }
132
+
133
+ protected convertToCluster(mgmtCluster: MgmtCluster, provCluster: ProvCluster): TopLevelMenuCluster {
134
+ return {
135
+ id: mgmtCluster.id,
136
+ label: mgmtCluster.nameDisplay,
137
+ ready: mgmtCluster.isReady, // && !provCluster?.hasError,
138
+ providerNavLogo: mgmtCluster.providerMenuLogo,
139
+ badge: mgmtCluster.badge,
140
+ isLocal: mgmtCluster.isLocal,
141
+ pinned: mgmtCluster.pinned,
142
+ description: provCluster?.description || mgmtCluster.description,
143
+ pin: () => mgmtCluster.pin(),
144
+ unpin: () => mgmtCluster.unpin(),
145
+ clusterRoute: { name: 'c-cluster-explorer', params: { cluster: mgmtCluster.id } }
146
+ };
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Helper designed to supply paginated results for the top level menu cluster resources
152
+ */
153
+ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper implements TopLevelMenuHelper {
154
+ private args?: UpdateArgs;
155
+
156
+ private clustersPinnedWrapper: PaginationWrapper;
157
+ private clustersOthersWrapper: PaginationWrapper;
158
+ private provClusterWrapper: PaginationWrapper;
159
+
160
+ private commonClusterFilters: PaginationParam[];
161
+
162
+ constructor({ $store }: {
163
+ $store: VuexStore,
164
+ }) {
165
+ super({ $store });
166
+
167
+ this.commonClusterFilters = paginationFilterClusters({ getters: this.$store.getters });
168
+
169
+ this.clustersPinnedWrapper = new PaginationWrapper({
170
+ $store,
171
+ onUpdate: () => {
172
+ // trigger on websocket update (only need 1 trigger for this cluster type)
173
+ // https://github.com/rancher/rancher/issues/40773 / https://github.com/rancher/dashboard/issues/12734
174
+ if (this.args) {
175
+ this.update(this.args);
176
+ }
177
+ },
178
+ enabledFor: {
179
+ store: 'management',
180
+ resource: {
181
+ id: MANAGEMENT.CLUSTER,
182
+ context: 'side-bar',
183
+ }
184
+ }
185
+ });
186
+ this.clustersOthersWrapper = new PaginationWrapper({
187
+ $store,
188
+ onUpdate: (res) => {
189
+ // trigger on websocket update (only need 1 trigger for this cluster type)
190
+ // https://github.com/rancher/rancher/issues/40773 / https://github.com/rancher/dashboard/issues/12734
191
+ if (this.args) {
192
+ this.update(this.args);
193
+ }
194
+ },
195
+ enabledFor: {
196
+ store: 'management',
197
+ resource: {
198
+ id: MANAGEMENT.CLUSTER,
199
+ context: 'side-bar',
200
+ }
201
+ }
202
+ });
203
+ this.provClusterWrapper = new PaginationWrapper({
204
+ $store,
205
+ onUpdate: (res) => {
206
+ // trigger on websocket update (only need 1 trigger for this cluster type)
207
+ // https://github.com/rancher/rancher/issues/40773 / https://github.com/rancher/dashboard/issues/12734
208
+ if (this.args) {
209
+ this.update(this.args);
210
+ }
211
+ },
212
+ enabledFor: {
213
+ store: 'management',
214
+ resource: {
215
+ id: CAPI.RANCHER_CLUSTER,
216
+ context: 'side-bar',
217
+ }
218
+ }
219
+ });
220
+ }
221
+
222
+ // ---------- requests ----------
223
+ async update(args: UpdateArgs) {
224
+ if (!this.hasProvCluster) {
225
+ // We're filtering out mgmt clusters without prov clusters, so if the user can't see any prov clusters at all
226
+ // exit early
227
+ return;
228
+ }
229
+
230
+ this.args = args;
231
+ const promises = {
232
+ pinned: this.updatePinned(args),
233
+ notPinned: this.updateOthers(args)
234
+ };
235
+
236
+ const res: {
237
+ pinned: MgmtCluster[],
238
+ notPinned: MgmtCluster[]
239
+ } = await allHash(promises) as any;
240
+ const provClusters = await this.updateProvCluster(res.notPinned, res.pinned);
241
+ const provClustersByMgmtId = provClusters.reduce((res: { [mgmtId: string]: ProvCluster}, provCluster: ProvCluster) => {
242
+ if (provCluster.mgmtClusterId) {
243
+ res[provCluster.mgmtClusterId] = provCluster;
244
+ }
245
+
246
+ return res;
247
+ }, {} as { [mgmtId: string]: ProvCluster});
248
+
249
+ const _clustersNotPinned = res.notPinned
250
+ .filter((mgmtCluster) => !!provClustersByMgmtId[mgmtCluster.id])
251
+ .map((mgmtCluster) => this.convertToCluster(mgmtCluster, provClustersByMgmtId[mgmtCluster.id]));
252
+ const _clustersPinned = res.pinned
253
+ .filter((mgmtCluster) => !!provClustersByMgmtId[mgmtCluster.id])
254
+ .map((mgmtCluster) => this.convertToCluster(mgmtCluster, provClustersByMgmtId[mgmtCluster.id]));
255
+
256
+ this.clustersPinned.length = 0;
257
+ this.clustersOthers.length = 0;
258
+
259
+ this.clustersPinned.push(..._clustersPinned);
260
+ this.clustersOthers.push(..._clustersNotPinned);
261
+ }
262
+
263
+ private constructParams({
264
+ pinnedIds,
265
+ searchTerm,
266
+ includeLocal,
267
+ includeSearchTerm,
268
+ includePinned,
269
+ excludePinned,
270
+ }: {
271
+ pinnedIds?: string[],
272
+ searchTerm?: string,
273
+ includeLocal?: boolean,
274
+ includeSearchTerm?: boolean,
275
+ includePinned?: boolean,
276
+ excludePinned?: boolean,
277
+ }): PaginationParam[] {
278
+ const filters: PaginationParam[] = [...this.commonClusterFilters];
279
+
280
+ if (pinnedIds) {
281
+ if (includePinned) {
282
+ // cluster id is 1 OR 2 OR 3 OR 4...
283
+ filters.push(PaginationParamFilter.createMultipleFields(
284
+ pinnedIds.map((id) => ({
285
+ field: 'id', value: id, equals: true, exact: true
286
+ }))
287
+ ));
288
+ }
289
+
290
+ if (excludePinned) {
291
+ // cluster id is NOT 1 AND NOT 2 AND NOT 3 AND NOT 4...
292
+ filters.push(...pinnedIds.map((id) => PaginationParamFilter.createSingleField({
293
+ field: 'id', equals: false, value: id
294
+ })));
295
+ }
296
+ }
297
+
298
+ if (searchTerm && includeSearchTerm) {
299
+ filters.push(PaginationParamFilter.createSingleField({
300
+ field: 'spec.displayName', exact: false, value: searchTerm
301
+ }));
302
+ }
303
+
304
+ if (includeLocal) {
305
+ filters.push(PaginationParamFilter.createSingleField({ field: 'id', value: 'local' }));
306
+ }
307
+
308
+ return filters;
309
+ }
310
+
311
+ /**
312
+ * See `clustersPinned` description for details
313
+ */
314
+ private async updatePinned(args: UpdateArgs): Promise<MgmtCluster[]> {
315
+ if (args.pinnedIds?.length < 1) {
316
+ // Return early, otherwise we're fetching all clusters...
317
+ return Promise.resolve([]);
318
+ }
319
+
320
+ return this.clustersPinnedWrapper.request({
321
+ pagination: {
322
+ filters: this.constructParams({
323
+ pinnedIds: args.pinnedIds,
324
+ includePinned: true,
325
+ }),
326
+ page: 1,
327
+ sort: DEFAULT_SORT,
328
+ projectsOrNamespaces: []
329
+ },
330
+ classify: true,
331
+ }).then((r) => r.data);
332
+ }
333
+
334
+ /**
335
+ * See `clustersOthers` description for details
336
+ */
337
+ private async updateOthers(args: UpdateArgs): Promise<MgmtCluster[]> {
338
+ return this.clustersOthersWrapper.request({
339
+ pagination: {
340
+ filters: this.constructParams({
341
+ searchTerm: args.searchTerm,
342
+ includeSearchTerm: !!args.searchTerm,
343
+ pinnedIds: args.pinnedIds,
344
+ excludePinned: !args.searchTerm,
345
+ }),
346
+ page: 1,
347
+ pageSize: args.unPinnedMax,
348
+ sort: DEFAULT_SORT,
349
+ projectsOrNamespaces: []
350
+ },
351
+ classify: true,
352
+ }).then((r) => r.data);
353
+ }
354
+
355
+ /**
356
+ * Find all provisioning clusters associated with the displayed mgmt clusters
357
+ */
358
+ private async updateProvCluster(notPinned: MgmtCluster[], pinned: MgmtCluster[]): Promise<ProvCluster[]> {
359
+ return this.provClusterWrapper.request({
360
+ pagination: {
361
+
362
+ filters: [
363
+ PaginationParamFilter.createMultipleFields(
364
+ [...notPinned, ...pinned]
365
+ .map((mgmtCluster) => ({
366
+ field: 'status.clusterName', value: mgmtCluster.id, equals: true, exact: true
367
+ }))
368
+ )
369
+ ],
370
+
371
+ page: 1,
372
+ sort: [],
373
+ projectsOrNamespaces: []
374
+ },
375
+ classify: true,
376
+ }).then((r) => r.data);
377
+ }
378
+ }
379
+
380
+ /**
381
+ * Helper designed to supply non-pagainted results for the top level menu cluster resources
382
+ */
383
+ export class TopLevelMenuHelperLegacy extends BaseTopLevelMenuHelper implements TopLevelMenuHelper {
384
+ constructor({ $store }: {
385
+ $store: VuexStore,
386
+ }) {
387
+ super({ $store });
388
+
389
+ if (this.hasProvCluster) {
390
+ $store.dispatch('management/findAll', { type: CAPI.RANCHER_CLUSTER });
391
+ }
392
+ }
393
+
394
+ async update(args: UpdateArgs) {
395
+ const clusters = this.updateClusters();
396
+ const _clustersNotPinned = this.clustersFiltered(clusters, args);
397
+ const _clustersPinned = this.pinFiltered(clusters, args);
398
+
399
+ this.clustersPinned.length = 0;
400
+ this.clustersOthers.length = 0;
401
+
402
+ this.clustersPinned.push(..._clustersPinned);
403
+ this.clustersOthers.push(..._clustersNotPinned);
404
+ }
405
+
406
+ /**
407
+ * Filter mgmt clusters by
408
+ * 1. Harvester type 1 (filterOnlyKubernetesClusters)
409
+ * 2. Harvester type 2 (filterHiddenLocalCluster)
410
+ * 3. There's a matching prov cluster
411
+ *
412
+ * Convert remaining clusters to special format
413
+ */
414
+ private updateClusters(): TopLevelMenuCluster[] {
415
+ if (!this.hasProvCluster) {
416
+ // We're filtering out mgmt clusters without prov clusters, so if the user can't see any prov clusters at all
417
+ // exit early
418
+ return [];
419
+ }
420
+
421
+ const all = this.$store.getters['management/all'](MANAGEMENT.CLUSTER);
422
+ const mgmtClusters = filterHiddenLocalCluster(filterOnlyKubernetesClusters(all, this.$store), this.$store);
423
+ const provClusters = this.$store.getters['management/all'](CAPI.RANCHER_CLUSTER);
424
+ const provClustersByMgmtId = provClusters.reduce((res: any, provCluster: ProvCluster) => {
425
+ if (provCluster.mgmt?.id) {
426
+ res[provCluster.mgmt.id] = provCluster;
427
+ }
428
+
429
+ return res;
430
+ }, {});
431
+
432
+ return (mgmtClusters || []).reduce((res: any, mgmtCluster: MgmtCluster) => {
433
+ // Filter to only show mgmt clusters that exist for the available provisioning clusters
434
+ // Addresses issue where a mgmt cluster can take some time to get cleaned up after the corresponding
435
+ // provisioning cluster has been deleted
436
+ if (!provClustersByMgmtId[mgmtCluster.id]) {
437
+ return res;
438
+ }
439
+
440
+ res.push(this.convertToCluster(mgmtCluster, provClustersByMgmtId[mgmtCluster.id]));
441
+
442
+ return res;
443
+ }, []);
444
+ }
445
+
446
+ /**
447
+ * Filter clusters by
448
+ * 1. Not pinned
449
+ * 2. Includes search term
450
+ *
451
+ * Sort remaining clusters
452
+ *
453
+ * Reduce number of clusters if too many too show
454
+ *
455
+ * Important! This is used to show unpinned clusters OR results of search
456
+ */
457
+ private clustersFiltered(clusters: TopLevelMenuCluster[], args: UpdateArgs): TopLevelMenuCluster[] {
458
+ const clusterFilter = args.searchTerm;
459
+ const maxClustersToShow = args.unPinnedMax || 10;
460
+
461
+ const search = (clusterFilter || '').toLowerCase();
462
+ let localCluster: MgmtCluster | null = null;
463
+
464
+ const filtered = clusters.filter((c) => {
465
+ // If we're searching we don't care if pinned or not
466
+ if (search) {
467
+ if (!c.label?.toLowerCase().includes(search)) {
468
+ return false;
469
+ }
470
+ } else if (c.pinned) {
471
+ // Not searching, not pinned, don't care
472
+ return false;
473
+ }
474
+
475
+ if (!localCluster && c.id === 'local') {
476
+ // Local cluster is a special case, we're inserting it at top so don't include in the middle
477
+ localCluster = c;
478
+
479
+ return false;
480
+ }
481
+
482
+ return true;
483
+ });
484
+
485
+ const sorted = sortBy(filtered, ['ready:desc', 'label']);
486
+
487
+ // put local cluster on top of list always - https://github.com/rancher/dashboard/issues/10975
488
+ if (localCluster) {
489
+ sorted.unshift(localCluster);
490
+ }
491
+
492
+ if (search) {
493
+ // this.showPinClusters = false;
494
+ // this.searchActive = !sorted.length > 0;
495
+
496
+ return sorted;
497
+ }
498
+ // this.showPinClusters = true;
499
+ // this.searchActive = false;
500
+
501
+ if (sorted.length >= maxClustersToShow) {
502
+ return sorted.slice(0, maxClustersToShow);
503
+ }
504
+
505
+ return sorted;
506
+ }
507
+
508
+ /**
509
+ * Filter clusters by
510
+ * 1. Not pinned
511
+ * 2. Includes search term
512
+ *
513
+ * Sort remaining clusters
514
+ *
515
+ * Reduce number of clusters if too many too show
516
+ *
517
+ * Important! This is hidden if there's a filter (user searching)
518
+ */
519
+ private pinFiltered(clusters: TopLevelMenuCluster[], args: UpdateArgs): TopLevelMenuCluster[] {
520
+ let localCluster = null;
521
+ const filtered = clusters.filter((c) => {
522
+ if (!c.pinned) {
523
+ // We only care about pinned clusters
524
+ return false;
525
+ }
526
+
527
+ if (c.id === 'local') {
528
+ // Special case, we're going to add this at the start so filter out
529
+ localCluster = c;
530
+
531
+ return false;
532
+ }
533
+
534
+ return true;
535
+ });
536
+
537
+ const sorted = sortBy(filtered, ['ready:desc', 'label']);
538
+
539
+ // put local cluster on top of list always - https://github.com/rancher/dashboard/issues/10975
540
+ if (localCluster) {
541
+ sorted.unshift(localCluster);
542
+ }
543
+
544
+ return sorted;
545
+ }
546
+ }