@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
@@ -31,11 +31,18 @@ export default {
31
31
  perfConfig = DEFAULT_PERF_SETTING;
32
32
  }
33
33
 
34
+ // Normally owner components supply `resource` and `inStore` as part of their data, however these are needed here before parent data runs
35
+ // So set up both here
36
+ const params = { ...this.$route.params };
37
+ const resource = params.resource || this.schema?.id; // Resource can either be on a page showing single list, or a page of a resource showing a list of another resource
38
+ const inStore = this.$store.getters['currentStore'](resource);
39
+
34
40
  return {
41
+ inStore,
35
42
  perfConfig,
36
43
  init: false,
37
44
  multipleResources: [],
38
- loadResources: [this.resource],
45
+ loadResources: [resource],
39
46
  // manual refresh vars
40
47
  hasManualRefresh: false,
41
48
  watch: true,
@@ -60,17 +67,47 @@ export default {
60
67
  }
61
68
  },
62
69
 
70
+ props: {
71
+ /**
72
+ * Add additional filtering to the rows
73
+ *
74
+ * Should only be used when we have all results, otherwise we're filtering a page which already has been filtered...
75
+ */
76
+ localFilter: {
77
+ type: Function,
78
+ default: null,
79
+ },
80
+
81
+ /**
82
+ * Add additional filtering to the pagination api request
83
+ */
84
+ apiFilter: {
85
+ type: Function,
86
+ default: null,
87
+ },
88
+ },
89
+
63
90
  computed: {
64
91
  ...mapGetters({ refreshFlag: 'resource-fetch/refreshFlag' }),
92
+
65
93
  rows() {
66
94
  const currResource = this.fetchedResourceType.find((item) => item.type === this.resource);
67
95
 
68
96
  if (currResource) {
69
- return this.$store.getters[`${ currResource.currStore }/all`](this.resource);
70
- } else {
71
- return [];
97
+ const rows = this.$store.getters[`${ currResource.currStore }/all`](this.resource);
98
+
99
+ if (this.canPaginate) {
100
+ if (this.havePaginated) {
101
+ return rows;
102
+ }
103
+ } else {
104
+ return this.localFilter ? this.localFilter(rows) : rows;
105
+ }
72
106
  }
107
+
108
+ return [];
73
109
  },
110
+
74
111
  loading() {
75
112
  if (this.canPaginate) {
76
113
  return this.paginating;
@@ -86,7 +123,9 @@ export default {
86
123
  if (this.init && neu) {
87
124
  await this.$fetch();
88
125
  if (this.canPaginate && this.fetchPageSecondaryResources) {
89
- this.fetchPageSecondaryResources(true);
126
+ this.fetchPageSecondaryResources({
127
+ canPaginate: this.canPaginate, force: true, page: this.rows, pagResult: this.paginationResult
128
+ });
90
129
  }
91
130
  }
92
131
  }
@@ -140,6 +179,10 @@ export default {
140
179
  force: this.paginating !== null // Fix for manual refresh (before ripped out).
141
180
  };
142
181
 
182
+ if (this.apiFilter) {
183
+ opt.paginating = this.apiFilter(opt.pagination);
184
+ }
185
+
143
186
  this['paginating'] = true;
144
187
 
145
188
  const that = this;
@@ -4,11 +4,12 @@ import HybridModel from '@shell/plugins/steve/hybrid-class';
4
4
  import { notOnlyOfRole } from '@shell/models/cluster.x-k8s.io.machine';
5
5
 
6
6
  export default class MgmtNodePool extends HybridModel {
7
- get nodeTemplate() {
8
- const id = (this.spec?.nodeTemplateName || '').replace(/:/, '/');
9
- const template = this.$getters['byId'](MANAGEMENT.NODE_TEMPLATE, id);
7
+ get nodeTemplateId() {
8
+ return (this.spec?.nodeTemplateName || '').replace(/:/, '/');
9
+ }
10
10
 
11
- return template;
11
+ get nodeTemplate() {
12
+ return this.$getters['byId'](MANAGEMENT.NODE_TEMPLATE, this.nodeTemplateId);
12
13
  }
13
14
 
14
15
  get provider() {
@@ -349,19 +349,11 @@ export default class ProvCluster extends SteveModel {
349
349
  }
350
350
 
351
351
  get mgmtClusterId() {
352
- return this.mgmt?.id || this.id?.replace(`${ this.metadata.namespace }/`, '');
352
+ return this.status?.clusterName;
353
353
  }
354
354
 
355
355
  get mgmt() {
356
- const name = this.status?.clusterName;
357
-
358
- if ( !name ) {
359
- return null;
360
- }
361
-
362
- const out = this.$rootGetters['management/byId'](MANAGEMENT.CLUSTER, name);
363
-
364
- return out;
356
+ return this.$rootGetters['management/byId'](MANAGEMENT.CLUSTER, this.mgmtClusterId);
365
357
  }
366
358
 
367
359
  get isReady() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rancher/shell",
3
- "version": "3.0.1-rc.4",
3
+ "version": "3.0.1",
4
4
  "description": "Rancher Dashboard Shell",
5
5
  "repository": "https://github.com/rancherlabs/dashboard",
6
6
  "license": "Apache-2.0",
@@ -39,7 +39,7 @@
39
39
  "@popperjs/core": "2.4.4",
40
40
  "@rancher/icons": "2.0.29",
41
41
  "@types/is-url": "1.2.30",
42
- "@types/node": "16.4.3",
42
+ "@types/node": "20.10.8",
43
43
  "@types/semver": "^7.5.8",
44
44
  "@typescript-eslint/eslint-plugin": "~5.4.0",
45
45
  "@typescript-eslint/parser": "~5.4.0",
@@ -60,7 +60,7 @@
60
60
  "color": "4.2.3",
61
61
  "codemirror": ">=5.64.0 <6",
62
62
  "codemirror-editor-vue3": "2.7.1",
63
- "cookie": "0.5.0",
63
+ "cookie": "0.7.0",
64
64
  "cookie-universal": "2.2.2",
65
65
  "core-js": "3.25.3",
66
66
  "cron-validator": "1.3.1",
@@ -104,7 +104,7 @@
104
104
  "js-yaml": "4.1.0",
105
105
  "js-yaml-loader": "1.2.2",
106
106
  "jsdiff": "1.1.1",
107
- "jsonpath-plus": "10.0.0",
107
+ "jsonpath-plus": "10.0.7",
108
108
  "jsrsasign": "10.5.25",
109
109
  "jszip": "3.8.0",
110
110
  "lodash": "4.17.21",
@@ -123,7 +123,7 @@
123
123
  "start-server-and-test": "1.13.1",
124
124
  "style-loader": "1.2.1",
125
125
  "ts-node": "8.10.2",
126
- "typescript": "4.5.5",
126
+ "typescript": "5.6.3",
127
127
  "ufo": "0.7.11",
128
128
  "unfetch": "4.2.0",
129
129
  "url-parse": "1.5.10",
@@ -160,7 +160,7 @@
160
160
  "roarr": "7.0.4",
161
161
  "semver": "7.5.4",
162
162
  "@types/lodash": "4.17.5",
163
- "@types/node": "~20.10.0",
163
+ "@types/node": "20.10.8",
164
164
  "@vue/cli-service/html-webpack-plugin": "^5.0.0"
165
165
  },
166
166
  "nyc": {
package/pages/about.vue CHANGED
@@ -108,6 +108,9 @@ export default {
108
108
  :to="{ name: 'diagnostic' }"
109
109
  class="btn role-primary"
110
110
  data-testid="about__diagnostics_button"
111
+ role="button"
112
+ :aria-label="t('about.diagnostic.title')"
113
+ @keyup.space="$router.push({ name: 'diagnostic' })"
111
114
  >
112
115
  {{ t('about.diagnostic.title') }}
113
116
  </router-link>
@@ -126,6 +129,8 @@ export default {
126
129
  href="https://github.com/rancher/rancher"
127
130
  target="_blank"
128
131
  rel="nofollow noopener noreferrer"
132
+ role="link"
133
+ :aria-label="t('about.versions.githubRepo', {name: t(`about.versions.rancher`) })"
129
134
  >
130
135
  {{ t("about.versions.rancher") }}
131
136
  </a>
@@ -137,6 +142,8 @@ export default {
137
142
  href="https://github.com/rancher/dashboard"
138
143
  target="_blank"
139
144
  rel="nofollow noopener noreferrer"
145
+ role="link"
146
+ :aria-label="t('about.versions.githubRepo', {name: t(`generic.dashboard`)})"
140
147
  >
141
148
  {{ t("generic.dashboard") }}
142
149
  </a>
@@ -148,6 +155,8 @@ export default {
148
155
  href="https://github.com/rancher/cli"
149
156
  target="_blank"
150
157
  rel="nofollow noopener noreferrer"
158
+ role="link"
159
+ :aria-label="t('about.versions.githubRepo', {name: t(`about.versions.cli`) })"
151
160
  >
152
161
  {{ appName }} {{ t("about.versions.cli") }}
153
162
  </a>
@@ -159,6 +168,8 @@ export default {
159
168
  href="https://github.com/rancher/helm"
160
169
  target="_blank"
161
170
  rel="nofollow noopener noreferrer"
171
+ role="link"
172
+ :aria-label="t('about.versions.githubRepo', {name: t(`about.versions.helm`) })"
162
173
  >
163
174
  {{ t("about.versions.helm") }}
164
175
  </a>
@@ -170,6 +181,8 @@ export default {
170
181
  href="https://github.com/rancher/machine"
171
182
  target="_blank"
172
183
  rel="nofollow noopener noreferrer"
184
+ role="link"
185
+ :aria-label="t('about.versions.githubRepo', {name: t(`about.versions.machine`) })"
173
186
  >
174
187
  {{ t("about.versions.machine") }}
175
188
  </a>
@@ -178,9 +191,12 @@ export default {
178
191
  </table>
179
192
  <p class="pt-20">
180
193
  <a
194
+ class="release-notes-link"
181
195
  :href="releaseNotesUrl"
182
196
  target="_blank"
183
197
  rel="nofollow noopener noreferrer"
198
+ role="link"
199
+ :aria-label="t('about.versions.releaseNotes')"
184
200
  >
185
201
  {{ t('about.versions.releaseNotes') }}
186
202
  </a>
@@ -202,8 +218,12 @@ export default {
202
218
  <td>
203
219
  <a
204
220
  v-if="d.imageList"
221
+ tabindex="0"
205
222
  :data-testid="`image_list_download_link__${d.label}`"
223
+ role="link"
224
+ :aria-label="t('about.versions.downloadImages', { listName: t(d.label) })"
206
225
  @click="d.imageList"
226
+ @keyup.enter="d.imageList"
207
227
  >
208
228
  {{ t('asyncButton.download.action') }}
209
229
  </a>
@@ -230,6 +250,8 @@ export default {
230
250
  <a
231
251
  v-if="d.cliLink"
232
252
  :href="d.cliLink"
253
+ role="link"
254
+ :aria-label="t('about.versions.downloadCli', { os: t(d.label) })"
233
255
  >{{ d.cliFile }}</a>
234
256
  </td>
235
257
  </tr>
@@ -99,7 +99,7 @@ describe('page: cluster dashboard', () => {
99
99
  [STATES_ENUM.HEALTHY, 'icon-checkmark', true, false, false, [{ status: 'True' }], 1, 0],
100
100
  ]]
101
101
  ])('%p cluster - %p agent health box :', (_, agentId, isLocal, agentResources, statuses) => {
102
- it.each(statuses)('should NOT show %p status due to missing canList permissions', (status, iconClass, isLoaded, disconnected, error, conditions, readyReplicas, unavailableReplicas) => {
102
+ it.each(statuses)('should NOT show %p status due to missing canList permissions', (status, iconClass, isLoaded, disconnected, error, conditions, readyReplicas, unavailableReplicas) => {
103
103
  const options = clone(mountOptions);
104
104
 
105
105
  options.global.mocks.$store.getters.currentCluster.isLocal = isLocal;
@@ -138,41 +138,48 @@ describe('page: cluster dashboard', () => {
138
138
 
139
139
  describe.each([
140
140
  ['local', 'fleet', true, ['fleetDeployment', 'fleetStatefulSet'], [
141
- [STATES_ENUM.IN_PROGRESS, 'icon-spinner', false, false, false, '', 0, 0],
142
- [STATES_ENUM.UNHEALTHY, 'icon-warning', true, false, false, [{ status: 'False' }], 0, 0],
143
- [STATES_ENUM.UNHEALTHY, 'icon-warning', true, false, true, [{ status: 'True' }], 0, 0],
144
- [STATES_ENUM.WARNING, 'icon-warning', true, true, false, [{ status: 'True' }], 0, 0],
145
- [STATES_ENUM.WARNING, 'icon-warning', true, false, false, [{ status: 'True' }], 0, 0],
146
- [STATES_ENUM.WARNING, 'icon-warning', true, false, false, [{ status: 'True' }], 0, 1],
147
- [STATES_ENUM.HEALTHY, 'icon-checkmark', true, false, false, [{ status: 'True' }], 1, 0],
141
+ [STATES_ENUM.IN_PROGRESS, 'icon-spinner', false, false, false, false, '', 0, 0],
142
+ [STATES_ENUM.UNHEALTHY, 'icon-warning', true, true, false, false, [{ status: 'False' }], 0, 0],
143
+ [STATES_ENUM.UNHEALTHY, 'icon-warning', true, true, false, true, [{ status: 'True' }], 0, 0],
144
+ [STATES_ENUM.WARNING, 'icon-warning', true, true, true, false, [{ status: 'True' }], 0, 0],
145
+ [STATES_ENUM.WARNING, 'icon-warning', true, true, false, false, [{ status: 'True' }], 0, 0],
146
+ [STATES_ENUM.WARNING, 'icon-warning', true, true, false, false, [{ status: 'True' }], 0, 1],
147
+ [STATES_ENUM.HEALTHY, 'icon-checkmark', false, true, false, false, [{ status: 'True' }], 1, 0],
148
148
  ]],
149
149
  ['downstream RKE2', 'fleet', false, ['fleetStatefulSet'], [
150
- [STATES_ENUM.IN_PROGRESS, 'icon-spinner', false, false, false, '', 0, 0],
151
- [STATES_ENUM.UNHEALTHY, 'icon-warning', true, false, false, [{ status: 'False' }], 0, 0],
152
- [STATES_ENUM.UNHEALTHY, 'icon-warning', true, false, true, [{ status: 'True' }], 0, 0],
153
- [STATES_ENUM.WARNING, 'icon-warning', true, true, false, [{ status: 'True' }], 0, 0],
154
- [STATES_ENUM.WARNING, 'icon-warning', true, false, false, [{ status: 'True' }], 0, 0],
155
- [STATES_ENUM.WARNING, 'icon-warning', true, false, false, [{ status: 'True' }], 0, 1],
156
- [STATES_ENUM.HEALTHY, 'icon-checkmark', true, false, false, [{ status: 'True' }], 1, 0],
150
+ [STATES_ENUM.IN_PROGRESS, 'icon-spinner', false, false, false, false, '', 0, 0],
151
+ [STATES_ENUM.UNHEALTHY, 'icon-warning', true, true, false, false, [{ status: 'False' }], 0, 0],
152
+ [STATES_ENUM.UNHEALTHY, 'icon-warning', true, true, false, true, [{ status: 'True' }], 0, 0],
153
+ [STATES_ENUM.WARNING, 'icon-warning', true, true, true, false, [{ status: 'True' }], 0, 0],
154
+ [STATES_ENUM.WARNING, 'icon-warning', true, true, false, false, [{ status: 'True' }], 0, 0],
155
+ [STATES_ENUM.WARNING, 'icon-warning', true, true, false, false, [{ status: 'True' }], 0, 1],
156
+ [STATES_ENUM.HEALTHY, 'icon-checkmark', false, true, false, false, [{ status: 'True' }], 1, 0],
157
157
  ]],
158
158
  ['downstream RKE2', 'cattle', false, ['cattleDeployment'], [
159
- [STATES_ENUM.IN_PROGRESS, 'icon-spinner', false, false, false, '', 0, 0],
160
- [STATES_ENUM.UNHEALTHY, 'icon-warning', true, false, false, [{ status: 'False' }], 0, 0],
161
- [STATES_ENUM.UNHEALTHY, 'icon-warning', true, true, false, [{ status: 'True' }], 0, 0],
162
- [STATES_ENUM.UNHEALTHY, 'icon-warning', true, false, true, [{ status: 'True' }], 0, 0],
163
- [STATES_ENUM.WARNING, 'icon-warning', true, false, false, [{ status: 'True' }], 0, 0],
164
- [STATES_ENUM.WARNING, 'icon-warning', true, false, false, [{ status: 'True' }], 0, 1],
165
- [STATES_ENUM.HEALTHY, 'icon-checkmark', true, false, false, [{ status: 'True' }], 1, 0],
159
+ [STATES_ENUM.IN_PROGRESS, 'icon-spinner', false, false, false, false, '', 0, 0],
160
+ [STATES_ENUM.UNHEALTHY, 'icon-warning', true, true, false, false, [{ status: 'False' }], 0, 0],
161
+ [STATES_ENUM.UNHEALTHY, 'icon-warning', true, true, true, false, [{ status: 'True' }], 0, 0],
162
+ [STATES_ENUM.UNHEALTHY, 'icon-warning', true, true, false, true, [{ status: 'True' }], 0, 0],
163
+ [STATES_ENUM.WARNING, 'icon-warning', true, true, false, false, [{ status: 'True' }], 0, 0],
164
+ [STATES_ENUM.WARNING, 'icon-warning', true, true, false, false, [{ status: 'True' }], 0, 1],
165
+ [STATES_ENUM.HEALTHY, 'icon-checkmark', false, true, false, false, [{ status: 'True' }], 1, 0],
166
166
  ]]
167
167
  ])('%p cluster - %p agent health box ::', (_, agentId, isLocal, agentResources, statuses) => {
168
- it.each(statuses)('should show %p status', (status, iconClass, isLoaded, disconnected, error, conditions, readyReplicas, unavailableReplicas) => {
168
+ it.each(statuses)('should show %p status', async(status, iconClass, clickable, isLoaded, disconnected, error, conditions, readyReplicas, unavailableReplicas) => {
169
+ let agentRoute = null;
170
+
169
171
  const options = clone(mountOptions);
170
172
 
171
173
  options.global.mocks.$store.getters.currentCluster.isLocal = isLocal;
172
174
 
173
- // let's pass the canList now
174
175
  options.global.mocks.$store.getters['cluster/canList'] = (type: string) => !!(type === WORKLOAD_TYPES.DEPLOYMENT) || !!(type === WORKLOAD_TYPES.STATEFUL_SET);
175
176
 
177
+ options.global.mocks.$router = {
178
+ push: (route: any) => {
179
+ agentRoute = route;
180
+ }
181
+ };
182
+
176
183
  const resources = agentResources.reduce((acc, r) => {
177
184
  const agent = {
178
185
  metadata: { state: { error } },
@@ -204,7 +211,12 @@ describe('page: cluster dashboard', () => {
204
211
 
205
212
  expect(box.element).toBeDefined();
206
213
  expect(box.element.classList).toContain(status);
214
+ expect(!!(box.element as any).$_popper).toBe(clickable);
207
215
  expect(icon.element.classList).toContain(iconClass);
216
+
217
+ await box.trigger('click');
218
+
219
+ expect(!!agentRoute).toBe(clickable);
208
220
  });
209
221
  });
210
222
 
@@ -125,6 +125,13 @@ export default {
125
125
  ROLES,
126
126
  ];
127
127
 
128
+ const clusterServiceIcons = {
129
+ [STATES_ENUM.IN_PROGRESS]: 'icon-spinner icon-spin',
130
+ [STATES_ENUM.HEALTHY]: 'icon-checkmark',
131
+ [STATES_ENUM.WARNING]: 'icon-warning',
132
+ [STATES_ENUM.UNHEALTHY]: 'icon-warning',
133
+ };
134
+
128
135
  return {
129
136
  nodeHeaders,
130
137
  constraints: [],
@@ -148,6 +155,7 @@ export default {
148
155
  clusterCounts,
149
156
  selectedTab: 'cluster-events',
150
157
  extensionCards: getApplicableExtensionEnhancements(this, ExtensionPoint.CARD, CardLocation.CLUSTER_DASHBOARD_CARD, this.$route),
158
+ clusterServiceIcons,
151
159
  };
152
160
  },
153
161
 
@@ -283,52 +291,51 @@ export default {
283
291
  clusterServices() {
284
292
  const services = [];
285
293
 
286
- CLUSTER_COMPONENTS.forEach((cs) => {
294
+ CLUSTER_COMPONENTS.forEach((name) => {
295
+ const component = this.getComponentStatus(name);
296
+
287
297
  services.push({
288
- name: cs,
289
- status: this.getComponentStatus(cs),
290
- labelKey: `clusterIndexPage.sections.componentStatus.${ cs }`,
298
+ name,
299
+ status: component.state,
300
+ labelKey: `clusterIndexPage.sections.componentStatus.component.${ name }.label`,
301
+ icon: this.clusterServiceIcons[component.state],
302
+ tooltip: component.tooltip,
303
+ goTo: () => null,
291
304
  });
292
305
  });
293
306
 
294
307
  if (this.cattleAgentNamespace) {
295
308
  services.push({
296
309
  name: 'cattle',
297
- status: this.cattleStatus,
298
- labelKey: 'clusterIndexPage.sections.componentStatus.cattle',
310
+ status: this.cattleAgent.state,
311
+ labelKey: 'clusterIndexPage.sections.componentStatus.component.cattle.label',
312
+ icon: this.clusterServiceIcons[this.cattleAgent.state],
313
+ tooltip: this.cattleAgent.tooltip,
314
+ goTo: () => this.goToClusterService(this.cattleAgent),
299
315
  });
300
316
  }
301
317
 
302
318
  if (this.fleetAgentNamespace) {
303
319
  services.push({
304
320
  name: 'fleet',
305
- status: this.fleetStatus,
306
- labelKey: 'clusterIndexPage.sections.componentStatus.fleet',
321
+ status: this.fleetAgent.state,
322
+ labelKey: 'clusterIndexPage.sections.componentStatus.component.fleet.label',
323
+ icon: this.clusterServiceIcons[this.fleetAgent.state],
324
+ tooltip: this.fleetAgent.tooltip,
325
+ goTo: () => this.goToClusterService(this.fleetAgent),
307
326
  });
308
327
  }
309
328
 
310
329
  return services;
311
330
  },
312
331
 
313
- cattleStatus() {
314
- const resource = this.cattleDeployment;
315
-
316
- if (resource === 'loading') {
317
- return STATES_ENUM.IN_PROGRESS;
318
- }
319
-
320
- if (!resource || this.disconnected || resource.status.conditions?.find((c) => c.status !== 'True') || resource.metadata.state?.error) {
321
- return STATES_ENUM.UNHEALTHY;
322
- }
323
-
324
- if (resource.spec.replicas !== resource.status.readyReplicas || resource.status.unavailableReplicas > 0) {
325
- return STATES_ENUM.WARNING;
326
- }
332
+ cattleAgent() {
333
+ const resources = [this.cattleDeployment];
327
334
 
328
- return STATES_ENUM.HEALTHY;
335
+ return this.getAgentStatus(resources, { checkDisconnected: true });
329
336
  },
330
337
 
331
- fleetStatus() {
338
+ fleetAgent() {
332
339
  const resources = this.currentCluster.isLocal ? [
333
340
  /**
334
341
  * 'fleetStatefulSet' could take a while to be created by rancher.
@@ -339,23 +346,7 @@ export default {
339
346
  this.fleetStatefulSet
340
347
  ];
341
348
 
342
- if (resources.find((r) => r === 'loading')) {
343
- return STATES_ENUM.IN_PROGRESS;
344
- }
345
-
346
- for (const resource of resources) {
347
- if (!resource || resource.status.conditions?.find((c) => c.status !== 'True') || resource.metadata.state?.error) {
348
- return STATES_ENUM.UNHEALTHY;
349
- }
350
- }
351
-
352
- for (const resource of resources) {
353
- if (resource.spec.replicas !== resource.status.readyReplicas || resource.status.unavailableReplicas > 0) {
354
- return STATES_ENUM.WARNING;
355
- }
356
- }
357
-
358
- return STATES_ENUM.HEALTHY;
349
+ return this.getAgentStatus(resources);
359
350
  },
360
351
 
361
352
  totalCountGaugeInput() {
@@ -532,25 +523,61 @@ export default {
532
523
  }
533
524
  },
534
525
 
526
+ getAgentStatus(resources, opt = { checkDisconnected: false }) {
527
+ if (resources.find((resource) => resource === 'loading')) {
528
+ return { state: STATES_ENUM.IN_PROGRESS };
529
+ }
530
+
531
+ for (const resource of resources) {
532
+ if (
533
+ !resource ||
534
+ (opt.checkDisconnected && this.disconnected) || // cattle
535
+ resource.status.conditions?.find((c) => c.status !== 'True') ||
536
+ resource.metadata.state?.error
537
+ ) {
538
+ return {
539
+ resource,
540
+ tooltip: resource?.stateDescription || this.t(`clusterIndexPage.sections.componentStatus.tooltip.disconnected`),
541
+ state: STATES_ENUM.UNHEALTHY,
542
+ };
543
+ }
544
+ }
545
+
546
+ for (const resource of resources) {
547
+ if (resource.spec.replicas !== resource.status.readyReplicas || resource.status.unavailableReplicas > 0) {
548
+ return {
549
+ resource,
550
+ tooltip: resource?.stateDescription || this.t(`clusterIndexPage.sections.componentStatus.tooltip.unavailableReplicas`),
551
+ state: STATES_ENUM.WARNING,
552
+ };
553
+ }
554
+ }
555
+
556
+ return { state: STATES_ENUM.HEALTHY };
557
+ },
558
+
535
559
  getComponentStatus(field) {
536
560
  const matching = (this.currentCluster?.status?.componentStatuses || []).filter((s) => s.name.startsWith(field));
537
561
 
538
562
  // If there's no matching component status, it's "healthy"
539
563
  if ( !matching.length ) {
540
- return STATES_ENUM.HEALTHY;
564
+ return { state: STATES_ENUM.HEALTHY };
541
565
  }
542
566
 
543
- const count = matching.reduce((acc, status) => {
544
- const conditions = status.conditions.find((c) => c.status !== 'True');
567
+ const errorConditions = matching.reduce((acc, status) => {
568
+ const condition = status.conditions.find((c) => c.status !== 'True');
545
569
 
546
- return !conditions ? acc : acc + 1;
547
- }, 0);
570
+ return !condition ? acc : [...acc, condition];
571
+ }, []);
548
572
 
549
- if (count > 0) {
550
- return STATES_ENUM.UNHEALTHY;
573
+ if (errorConditions.length > 0) {
574
+ return {
575
+ tooltip: errorConditions[0].message,
576
+ state: STATES_ENUM.UNHEALTHY
577
+ };
551
578
  }
552
579
 
553
- return STATES_ENUM.HEALTHY;
580
+ return { state: STATES_ENUM.HEALTHY };
554
581
  },
555
582
 
556
583
  showActions() {
@@ -578,6 +605,23 @@ export default {
578
605
  await provCluster.goToHarvesterCluster();
579
606
  } catch {
580
607
  }
608
+ },
609
+
610
+ goToClusterService(agent) {
611
+ if (!agent.resource || agent.state === STATES_ENUM.HEALTHY) {
612
+ return;
613
+ }
614
+
615
+ this.$router.push({
616
+ name: 'c-cluster-product-resource-namespace-id',
617
+ params: {
618
+ cluster: this.currentCluster.id,
619
+ product: 'explorer',
620
+ resource: agent.resource.type,
621
+ namespace: agent.resource.metadata.namespace,
622
+ id: agent.resource.metadata.name,
623
+ }
624
+ });
581
625
  }
582
626
  },
583
627
  };
@@ -722,21 +766,15 @@ export default {
722
766
  <div
723
767
  v-for="(service, i) in clusterServices"
724
768
  :key="i"
769
+ v-clean-tooltip="service.tooltip"
725
770
  class="k8s-service-status"
726
771
  :class="{[service.status]: true }"
727
772
  :data-testid="`k8s-service-${ service.name }`"
773
+ @click="service.goTo"
728
774
  >
729
775
  <i
730
- v-if="service.status === STATES_ENUM.IN_PROGRESS"
731
- class="icon icon-spinner icon-spin"
732
- />
733
- <i
734
- v-else-if="service.status === STATES_ENUM.HEALTHY"
735
- class="icon icon-checkmark"
736
- />
737
- <i
738
- v-else
739
- class="icon icon-warning"
776
+ class="icon"
777
+ :class="service.icon"
740
778
  />
741
779
  <div class="label">
742
780
  {{ t(service.labelKey) }}
@@ -948,6 +986,7 @@ export default {
948
986
 
949
987
  &.unhealthy {
950
988
  border-color: var(--error-border);
989
+ cursor: pointer;
951
990
 
952
991
  > I {
953
992
  color: var(--error)
@@ -955,6 +994,8 @@ export default {
955
994
  }
956
995
 
957
996
  &.warning {
997
+ cursor: pointer;
998
+
958
999
  > I {
959
1000
  color: var(--warning)
960
1001
  }