@rancher/shell 3.0.0 → 3.0.1-rc.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 (83) hide show
  1. package/assets/brand/harvester/favicon.png +0 -0
  2. package/assets/brand/harvester/metadata.json +3 -0
  3. package/assets/images/pl/harvester.svg +1 -0
  4. package/assets/translations/en-us.yaml +25 -4
  5. package/assets/translations/zh-hans.yaml +1 -1
  6. package/components/BrandImage.vue +5 -1
  7. package/components/CopyToClipboardText.vue +2 -0
  8. package/components/CruResourceFooter.vue +1 -0
  9. package/components/DetailTop.vue +1 -1
  10. package/components/ExplorerMembers.vue +5 -1
  11. package/components/ExplorerProjectsNamespaces.vue +39 -15
  12. package/components/HardwareResourceGauge.vue +12 -2
  13. package/components/InputOrDisplay.vue +6 -2
  14. package/components/LandingPagePreference.vue +2 -2
  15. package/components/MessageLink.vue +1 -1
  16. package/components/ResourceDetail/Masthead.vue +22 -1
  17. package/components/ResourceDetail/index.vue +2 -8
  18. package/components/ResourceTable.vue +14 -6
  19. package/components/ResourceYaml.vue +1 -1
  20. package/components/SideNav.vue +1 -1
  21. package/components/TableDataUserIcon.vue +1 -1
  22. package/components/fleet/FleetRepos.vue +0 -7
  23. package/components/form/ArrayList.vue +5 -1
  24. package/components/form/ArrayListSelect.vue +5 -1
  25. package/components/form/KeyValue.vue +1 -1
  26. package/components/form/LabeledSelect.vue +26 -6
  27. package/components/form/Password.vue +7 -1
  28. package/components/form/UnitInput.vue +10 -1
  29. package/components/form/__tests__/UnitInput.test.ts +1 -0
  30. package/components/formatter/ClusterProvider.vue +3 -3
  31. package/components/formatter/ImagePercentageBar.vue +1 -1
  32. package/components/formatter/Si.vue +5 -1
  33. package/components/formatter/Translate.vue +1 -1
  34. package/components/nav/Header.vue +29 -5
  35. package/components/nav/NamespaceFilter.vue +5 -8
  36. package/components/nav/TopLevelMenu.vue +11 -11
  37. package/config/labels-annotations.js +2 -0
  38. package/config/table-headers.js +15 -0
  39. package/config/types.js +3 -0
  40. package/detail/fleet.cattle.io.bundle.vue +5 -68
  41. package/detail/fleet.cattle.io.gitrepo.vue +2 -1
  42. package/directives/clean-tooltip.js +4 -4
  43. package/edit/logging-flow/Match.vue +75 -42
  44. package/edit/logging-flow/index.vue +89 -10
  45. package/edit/logging.banzaicloud.io.output/index.vue +2 -2
  46. package/edit/management.cattle.io.project.vue +2 -2
  47. package/edit/namespace.vue +1 -1
  48. package/edit/provisioning.cattle.io.cluster/index.vue +2 -1
  49. package/edit/provisioning.cattle.io.cluster/rke2.vue +1 -3
  50. package/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryMirrors.vue +1 -1
  51. package/list/harvesterhci.io.management.cluster.vue +244 -0
  52. package/list/namespace.vue +16 -4
  53. package/models/__tests__/management.cattle.io.cluster.test.ts +45 -3
  54. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +0 -86
  55. package/models/fleet.cattle.io.bundle.js +3 -1
  56. package/models/fleet.cattle.io.gitrepo.js +43 -50
  57. package/models/k8s.cni.cncf.io.networkattachmentdefinition.js +88 -0
  58. package/models/management.cattle.io.cluster.js +26 -5
  59. package/models/management.cattle.io.setting.js +25 -0
  60. package/models/provisioning.cattle.io.cluster.js +5 -14
  61. package/models/storage.k8s.io.storageclass.js +15 -4
  62. package/package.json +3 -3
  63. package/pages/auth/login.vue +3 -2
  64. package/pages/auth/setup.vue +1 -1
  65. package/pages/c/_cluster/fleet/index.vue +2 -4
  66. package/pages/c/_cluster/settings/brand.vue +4 -1
  67. package/pages/prefs.vue +22 -10
  68. package/plugins/dashboard-store/resource-class.js +11 -3
  69. package/plugins/steve/steve-pagination-utils.ts +1 -1
  70. package/rancher-components/Form/LabeledInput/LabeledInput.vue +7 -2
  71. package/store/features.js +1 -0
  72. package/store/i18n.js +5 -1
  73. package/store/prefs.js +8 -0
  74. package/types/resources/fleet.d.ts +40 -0
  75. package/types/shell/index.d.ts +428 -395
  76. package/utils/auth.js +1 -1
  77. package/utils/create-yaml.js +1 -1
  78. package/utils/favicon.js +2 -0
  79. package/utils/fleet.ts +159 -0
  80. package/utils/gc/gc.ts +1 -1
  81. package/utils/v-sphere.ts +31 -0
  82. package/utils/validators/cron-schedule.js +1 -1
  83. package/utils/validators/formRules/index.ts +1 -1
@@ -2,6 +2,7 @@
2
2
  import KeyValue from '@shell/components/form/KeyValue';
3
3
  import Select from '@shell/components/form/Select';
4
4
  import LabeledSelect from '@shell/components/form/LabeledSelect';
5
+ import { HARVESTER_NAME as VIRTUAL } from '@shell/config/features';
5
6
 
6
7
  export default {
7
8
  emits: ['remove'],
@@ -39,6 +40,12 @@ export default {
39
40
  }
40
41
  },
41
42
 
43
+ computed: {
44
+ isHarvester() {
45
+ return this.$store.getters['currentProduct'].inStore === VIRTUAL;
46
+ },
47
+ },
48
+
42
49
  methods: {
43
50
  update() {},
44
51
 
@@ -51,19 +58,22 @@ export default {
51
58
 
52
59
  <template>
53
60
  <div>
54
- <KeyValue
55
- v-model:value="value.labels"
56
- :title="value.select ? t('logging.flow.matches.pods.title.include') : t('logging.flow.matches.pods.title.exclude')"
57
- :mode="mode"
58
- :initial-empty-row="true"
59
- :read-allowed="false"
60
- :title-add="true"
61
- protip=""
62
- :key-label="t('logging.flow.matches.pods.keyLabel')"
63
- :value-label="t('logging.flow.matches.pods.valueLabel')"
64
- :add-label="t('logging.flow.matches.pods.addLabel')"
65
- />
66
- <div class="spacer" />
61
+ <template v-if="!isHarvester">
62
+ <KeyValue
63
+ v-model:value="value.labels"
64
+ :title="value.select ? t('logging.flow.matches.pods.title.include') : t('logging.flow.matches.pods.title.exclude')"
65
+ :mode="mode"
66
+ :initial-empty-row="true"
67
+ :read-allowed="false"
68
+ :title-add="true"
69
+ protip=""
70
+ :key-label="t('logging.flow.matches.pods.keyLabel')"
71
+ :value-label="t('logging.flow.matches.pods.valueLabel')"
72
+ :add-label="t('logging.flow.matches.pods.addLabel')"
73
+ />
74
+ <div class="spacer" />
75
+ </template>
76
+
67
77
  <h3>
68
78
  {{ value.select ? t('logging.flow.matches.nodes.title.include') : t('logging.flow.matches.nodes.title.exclude') }}
69
79
  </h3>
@@ -83,48 +93,71 @@ export default {
83
93
  />
84
94
  </div>
85
95
  </div>
86
- <div class="spacer" />
87
- <h3>
88
- {{ value.select ? t('logging.flow.matches.containerNames.title.include') : t('logging.flow.matches.containerNames.title.exclude') }}
89
- </h3>
90
- <div class="row">
91
- <div class="col span-12">
92
- <LabeledSelect
93
- v-model:value="value.container_names"
94
- :mode="mode"
95
- :options="[]"
96
- :disabled="false"
97
- :placeholder="t('logging.flow.matches.containerNames.placeholder')"
98
- :multiple="true"
99
- :taggable="true"
100
- :clearable="true"
101
- :searchable="true"
102
- :close-on-select="false"
103
- no-options-label-key="logging.flow.matches.containerNames.enter"
104
- placement="top"
105
- />
106
- </div>
107
- </div>
108
- <div v-if="isClusterFlow">
96
+ <div v-if="!isHarvester">
109
97
  <div class="spacer" />
110
98
  <h3>
111
- {{ value.select ? t('logging.flow.matches.namespaces.title.include') : t('logging.flow.matches.namespaces.title.exclude') }}
99
+ {{ value.select ? t('logging.flow.matches.containerNames.title.include') : t('logging.flow.matches.containerNames.title.exclude') }}
112
100
  </h3>
113
101
  <div class="row">
114
102
  <div class="col span-12">
115
- <Select
116
- v-model:value="value.namespaces"
117
- class="lg"
118
- :options="namespaces"
119
- :placeholder="t('logging.flow.matches.namespaces.placeholder')"
103
+ <LabeledSelect
104
+ v-model:value="value.container_names"
105
+ :mode="mode"
106
+ :options="[]"
107
+ :disabled="false"
108
+ :placeholder="t('logging.flow.matches.containerNames.placeholder')"
120
109
  :multiple="true"
121
110
  :taggable="true"
122
111
  :clearable="true"
112
+ :searchable="true"
123
113
  :close-on-select="false"
114
+ no-options-label-key="logging.flow.matches.containerNames.enter"
124
115
  placement="top"
125
116
  />
126
117
  </div>
127
118
  </div>
119
+ <div v-if="isClusterFlow">
120
+ <div class="spacer" />
121
+ <h3>
122
+ {{ value.select ? t('logging.flow.matches.containerNames.title.include') : t('logging.flow.matches.containerNames.title.exclude') }}
123
+ </h3>
124
+ <div class="row">
125
+ <div class="col span-12">
126
+ <Select
127
+ v-model:value="value.namespaces"
128
+ class="lg"
129
+ :options="namespaces"
130
+ :placeholder="t('logging.flow.matches.namespaces.placeholder')"
131
+ :multiple="true"
132
+ :taggable="true"
133
+ :clearable="true"
134
+ :searchable="true"
135
+ :close-on-select="false"
136
+ no-options-label-key="logging.flow.matches.containerNames.enter"
137
+ placement="top"
138
+ />
139
+ </div>
140
+ </div>
141
+ <div class="spacer" />
142
+ <h3>
143
+ {{ value.select ? t('logging.flow.matches.namespaces.title.include') : t('logging.flow.matches.namespaces.title.exclude') }}
144
+ </h3>
145
+ <div class="row">
146
+ <div class="col span-12">
147
+ <Select
148
+ v-model="value.namespaces"
149
+ class="lg"
150
+ :options="namespaces"
151
+ :placeholder="t('logging.flow.matches.namespaces.placeholder')"
152
+ :multiple="true"
153
+ :taggable="true"
154
+ :clearable="true"
155
+ :close-on-select="false"
156
+ placement="top"
157
+ />
158
+ </div>
159
+ </div>
160
+ </div>
128
161
  </div>
129
162
  </div>
130
163
  </template>
@@ -6,20 +6,28 @@ import Loading from '@shell/components/Loading';
6
6
  import NameNsDescription from '@shell/components/form/NameNsDescription';
7
7
  import Tabbed from '@shell/components/Tabbed';
8
8
  import Tab from '@shell/components/Tabbed/Tab';
9
- import { LOGGING, NAMESPACE, NODE, SCHEMA } from '@shell/config/types';
9
+ import {
10
+ LOGGING, NAMESPACE, NODE, POD, SCHEMA
11
+ } from '@shell/config/types';
10
12
  import jsyaml from 'js-yaml';
11
13
  import { createYaml } from '@shell/utils/create-yaml';
12
14
  import YamlEditor, { EDITOR_MODES } from '@shell/components/YamlEditor';
13
15
  import { allHash } from '@shell/utils/promise';
14
- import { isArray } from '@shell/utils/array';
16
+ import { isArray, uniq } from '@shell/utils/array';
15
17
  import { matchRuleIsPopulated } from '@shell/models/logging.banzaicloud.io.flow';
16
18
  import LabeledSelect from '@shell/components/form/LabeledSelect';
17
19
  import { clone } from '@shell/utils/object';
18
20
  import isEmpty from 'lodash/isEmpty';
19
21
  import ArrayListGrouped from '@shell/components/form/ArrayListGrouped';
20
22
  import { exceptionToErrorsArray } from '@shell/utils/error';
23
+ import { HARVESTER_NAME as VIRTUAL } from '@shell/config/features';
21
24
  import Match from './Match';
22
25
 
26
+ const FLOW_LOGGING = 'Logging';
27
+ const FLOW_AUDIT = 'Audit';
28
+ const FLOW_EVENT = 'Event';
29
+ const FLOW_TYPE = [FLOW_LOGGING, FLOW_AUDIT, FLOW_EVENT];
30
+
23
31
  function emptyMatch(include = true) {
24
32
  const rule = {
25
33
  select: !!include,
@@ -53,14 +61,17 @@ export default {
53
61
  inheritAttrs: false,
54
62
 
55
63
  async fetch() {
56
- const hasAccessToClusterOutputs = this.$store.getters[`cluster/schemaFor`](LOGGING.CLUSTER_OUTPUT);
57
- const hasAccessToOutputs = this.$store.getters[`cluster/schemaFor`](LOGGING.OUTPUT);
64
+ const currentCluster = this.$store.getters['currentCluster'];
65
+ const inStore = currentCluster.isHarvester ? VIRTUAL : 'cluster';
66
+ const hasAccessToClusterOutputs = this.$store.getters[`${ inStore }/schemaFor`](LOGGING.CLUSTER_OUTPUT);
67
+ const hasAccessToOutputs = this.$store.getters[`${ inStore }/schemaFor`](LOGGING.OUTPUT);
58
68
  const hasAccessToNamespaces = this.$store.getters[`cluster/schemaFor`](NAMESPACE);
59
- const hasAccessToNodes = this.$store.getters[`cluster/schemaFor`](NODE);
69
+ const hasAccessToNodes = this.$store.getters[`${ inStore }/schemaFor`](NODE);
70
+ const hasAccessToPods = this.$store.getters[`${ inStore }/schemaFor`](POD);
60
71
  const isFlow = this.value.type === LOGGING.FLOW;
61
72
 
62
73
  const getAllOrDefault = (type, hasAccess) => {
63
- return hasAccess ? this.$store.dispatch('cluster/findAll', { type }) : Promise.resolve([]);
74
+ return hasAccess ? this.$store.dispatch(`${ inStore }/findAll`, { type }) : Promise.resolve([]);
64
75
  };
65
76
 
66
77
  const hash = await allHash({
@@ -68,6 +79,7 @@ export default {
68
79
  allClusterOutputs: getAllOrDefault(LOGGING.CLUSTER_OUTPUT, hasAccessToClusterOutputs),
69
80
  allNamespaces: getAllOrDefault(NAMESPACE, hasAccessToNamespaces),
70
81
  allNodes: getAllOrDefault(NODE, hasAccessToNodes),
82
+ allPods: getAllOrDefault(POD, hasAccessToPods),
71
83
  });
72
84
 
73
85
  for ( const k of Object.keys(hash) ) {
@@ -76,7 +88,9 @@ export default {
76
88
  },
77
89
 
78
90
  data() {
79
- const schemas = this.$store.getters['cluster/all'](SCHEMA);
91
+ const currentCluster = this.$store.getters['currentCluster'];
92
+ const inStore = currentCluster.isHarvester ? VIRTUAL : 'cluster';
93
+ const schemas = this.$store.getters[`${ inStore }/all`](SCHEMA);
80
94
  let filtersYaml;
81
95
 
82
96
  this.value.spec = this.value.spec || {};
@@ -124,7 +138,8 @@ export default {
124
138
  filtersYaml,
125
139
  initialFiltersYaml: filtersYaml,
126
140
  globalOutputRefs,
127
- localOutputRefs
141
+ localOutputRefs,
142
+ loggingType: clone(this.value.loggingType || FLOW_LOGGING)
128
143
  };
129
144
  },
130
145
 
@@ -150,7 +165,17 @@ export default {
150
165
  return true;
151
166
  }
152
167
 
153
- return output.namespace === this.value.namespace;
168
+ const isEqualNs = output.namespace === this.value.namespace;
169
+
170
+ if (!this.isHarvester) {
171
+ return isEqualNs;
172
+ }
173
+
174
+ if (this.loggingType === FLOW_AUDIT) {
175
+ return output.loggingType === FLOW_AUDIT && isEqualNs;
176
+ }
177
+
178
+ return output.loggingType !== FLOW_AUDIT && isEqualNs;
154
179
  }).map((x) => {
155
180
  return { label: x.metadata.name, value: x.metadata.name };
156
181
  });
@@ -165,7 +190,17 @@ export default {
165
190
 
166
191
  return this.allClusterOutputs
167
192
  .filter((clusterOutput) => {
168
- return clusterOutput.namespace === 'cattle-logging-system';
193
+ const isEqualNs = clusterOutput.namespace === 'cattle-logging-system';
194
+
195
+ if (!this.isHarvester) {
196
+ return isEqualNs;
197
+ }
198
+
199
+ if (this.loggingType === FLOW_AUDIT) {
200
+ return clusterOutput.loggingType === FLOW_AUDIT && isEqualNs;
201
+ }
202
+
203
+ return clusterOutput.loggingType !== FLOW_AUDIT && isEqualNs;
169
204
  })
170
205
  .map((clusterOutput) => {
171
206
  return { label: clusterOutput.metadata.name, value: clusterOutput.metadata.name };
@@ -204,6 +239,25 @@ export default {
204
239
  return out;
205
240
  },
206
241
 
242
+ containerChoices() {
243
+ const out = [];
244
+
245
+ for ( const pod of this.allPods ) {
246
+ for ( const c of (pod.spec?.containers || []) ) {
247
+ out.push(c.name);
248
+ }
249
+ }
250
+
251
+ return uniq(out).sort();
252
+ },
253
+
254
+ isHarvester() {
255
+ return this.$store.getters['currentProduct'].inStore === VIRTUAL;
256
+ },
257
+
258
+ flowTypeOptions() {
259
+ return FLOW_TYPE;
260
+ },
207
261
  },
208
262
 
209
263
  watch: {
@@ -312,6 +366,20 @@ export default {
312
366
  if (this.value.spec.match && this.isMatchEmpty(this.value.spec.match)) {
313
367
  delete this.value.spec['match'];
314
368
  }
369
+
370
+ if (this.loggingType === FLOW_AUDIT) {
371
+ this.value.spec['loggingRef'] = 'harvester-kube-audit-log-ref';
372
+ }
373
+
374
+ if (this.loggingType === FLOW_EVENT) {
375
+ const eventSelector = { select: { labels: { 'app.kubernetes.io/name': 'event-tailer' } } };
376
+
377
+ if (!this.value.spec.match) {
378
+ this.value.spec['match'] = [eventSelector];
379
+ } else {
380
+ this.value.spec.match.push(eventSelector);
381
+ }
382
+ }
315
383
  },
316
384
  onYamlEditorReady(cm) {
317
385
  cm.getMode().fold = 'yamlcomments';
@@ -359,10 +427,21 @@ export default {
359
427
  :weight="3"
360
428
  >
361
429
  <Banner
430
+ v-if="!isHarvester"
362
431
  color="info"
363
432
  class="mt-0"
364
433
  :label="t('logging.flow.matches.banner')"
365
434
  />
435
+ <div v-if="isHarvester">
436
+ <LabeledSelect
437
+ v-model:value="loggingType"
438
+ class="mb-20"
439
+ :options="flowTypeOptions"
440
+ :mode="mode"
441
+ :disabled="!isCreate"
442
+ :label="t('generic.type')"
443
+ />
444
+ </div>
366
445
  <ArrayListGrouped
367
446
  v-model:value="matches"
368
447
  :add-label="t('ingress.rules.addRule')"
@@ -199,13 +199,13 @@ export default {
199
199
  v-if="hasMultipleProvidersSelected"
200
200
  color="info"
201
201
  >
202
- This output is configured with multiple providers. We currently only support a single provider per output. You can view or edit the YAML.
202
+ {{ t('logging.output.tips.singleProvider') }}
203
203
  </Banner>
204
204
  <Banner
205
205
  v-else-if="!value.allProvidersSupported"
206
206
  color="info"
207
207
  >
208
- This output is configured with providers we don't support yet. You can view or edit the YAML.
208
+ {{ t('logging.output.tips.multipleProviders') }}
209
209
  </Banner>
210
210
  <Tabbed
211
211
  v-else
@@ -52,7 +52,7 @@ export default {
52
52
  };
53
53
  },
54
54
  computed: {
55
- ...mapGetters(['currentCluster']),
55
+ ...mapGetters(['currentCluster', 'isStandaloneHarvester']),
56
56
 
57
57
  canViewMembers() {
58
58
  return canViewProjectMembershipEditor(this.$store);
@@ -222,7 +222,7 @@ export default {
222
222
  <ResourceQuota
223
223
  :value="value"
224
224
  :mode="canEditTabElements"
225
- :types="isHarvester ? HARVESTER_TYPES : RANCHER_TYPES"
225
+ :types="isStandaloneHarvester ? HARVESTER_TYPES : RANCHER_TYPES"
226
226
  @remove="removeQuota"
227
227
  />
228
228
  </Tab>
@@ -15,9 +15,9 @@ import MoveModal from '@shell/components/MoveModal';
15
15
  import ResourceQuota from '@shell/components/form/ResourceQuota/Namespace';
16
16
  import Loading from '@shell/components/Loading';
17
17
  import { HARVESTER_TYPES, RANCHER_TYPES } from '@shell/components/form/ResourceQuota/shared';
18
- import { HARVESTER_NAME as HARVESTER } from '@shell/config/features';
19
18
  import Labels from '@shell/components/form/Labels';
20
19
  import { randomStr } from '@shell/utils/string';
20
+ import { HARVESTER_NAME as HARVESTER } from '@shell/config/features';
21
21
 
22
22
  export default {
23
23
  emits: ['input'],
@@ -211,8 +211,9 @@ export default {
211
211
 
212
212
  emberLink() {
213
213
  if (this.value) {
214
+ // set subtype if editing EKS/GKE/AKS cluster -- this ensures that the component provided by extension is loaded instead of iframing old ember ui
214
215
  if (this.value.provisioner) {
215
- const matchingSubtype = this.subTypes.find((st) => st.id.toLowerCase() === this.value.provisioner.toLowerCase() || DRIVER_TO_IMPORT[st.id.toLowerCase()] === this.value.provisioner.toLowerCase());
216
+ const matchingSubtype = this.subTypes.find((st) => DRIVER_TO_IMPORT[st.id.toLowerCase()] === this.value.provisioner.toLowerCase());
216
217
 
217
218
  if (matchingSubtype) {
218
219
  this.selectType(matchingSubtype.id, false);
@@ -65,14 +65,12 @@ import Advanced from '@shell/edit/provisioning.cattle.io.cluster/tabs/Advanced';
65
65
  import { DEFAULT_COMMON_BASE_PATH, DEFAULT_SUBDIRS } from '@shell/edit/provisioning.cattle.io.cluster/tabs/DirectoryConfig';
66
66
  import ClusterAppearance from '@shell/components/form/ClusterAppearance';
67
67
  import AddOnAdditionalManifest from '@shell/edit/provisioning.cattle.io.cluster/tabs/AddOnAdditionalManifest';
68
- import VsphereUtils from '@shell/utils/v-sphere';
68
+ import VsphereUtils, { VMWARE_VSPHERE } from '@shell/utils/v-sphere';
69
69
 
70
70
  const HARVESTER = 'harvester';
71
71
  const HARVESTER_CLOUD_PROVIDER = 'harvester-cloud-provider';
72
72
  const NETBIOS_TRUNCATION_LENGTH = 15;
73
73
 
74
- const VMWARE_VSPHERE = 'vmwarevsphere';
75
-
76
74
  /**
77
75
  * Classes to be adopted by the node badges in Machine pools
78
76
  */
@@ -90,7 +90,7 @@ export default {
90
90
  :default-add-value="defaultAddValue"
91
91
  :initial-empty-row="true"
92
92
  :mode="mode"
93
- @input="update"
93
+ @update:value="update"
94
94
  >
95
95
  <template #default="{row}">
96
96
  <div class="row">
@@ -0,0 +1,244 @@
1
+ <script>
2
+ import BrandImage from '@shell/components/BrandImage';
3
+ import TypeDescription from '@shell/components/TypeDescription';
4
+ import ResourceTable from '@shell/components/ResourceTable';
5
+ import Masthead from '@shell/components/ResourceList/Masthead';
6
+ import Loading from '@shell/components/Loading';
7
+ import { HARVESTER_NAME as VIRTUAL } from '@shell/config/features';
8
+ import { CAPI, HCI, MANAGEMENT } from '@shell/config/types';
9
+ import { isHarvesterCluster } from '@shell/utils/cluster';
10
+ import { allHash } from '@shell/utils/promise';
11
+
12
+ export default {
13
+ components: {
14
+ BrandImage,
15
+ ResourceTable,
16
+ Masthead,
17
+ TypeDescription,
18
+ Loading
19
+ },
20
+
21
+ props: {
22
+ schema: {
23
+ type: Object,
24
+ required: true,
25
+ },
26
+ useQueryParamsForSimpleFiltering: {
27
+ type: Boolean,
28
+ default: false
29
+ }
30
+ },
31
+
32
+ async fetch() {
33
+ const inStore = this.$store.getters['currentProduct'].inStore;
34
+
35
+ const hash = await allHash({
36
+ hciClusters: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER }),
37
+ mgmtClusters: this.$store.dispatch(`${ inStore }/findAll`, { type: MANAGEMENT.CLUSTER })
38
+ });
39
+
40
+ this.hciClusters = hash.hciClusters;
41
+ this.mgmtClusters = hash.mgmtClusters;
42
+ },
43
+
44
+ data() {
45
+ const resource = CAPI.RANCHER_CLUSTER;
46
+
47
+ return {
48
+ navigating: false,
49
+ VIRTUAL,
50
+ hciDashboard: HCI.DASHBOARD,
51
+ resource,
52
+ hResource: HCI.CLUSTER,
53
+ hciClusters: [],
54
+ mgmtClusters: []
55
+ };
56
+ },
57
+
58
+ computed: {
59
+ realSchema() {
60
+ return this.$store.getters['management/schemaFor'](CAPI.RANCHER_CLUSTER);
61
+ },
62
+
63
+ importLocation() {
64
+ return {
65
+ name: 'c-cluster-product-resource-create',
66
+ params: {
67
+ product: this.$store.getters['currentProduct'].name,
68
+ resource: this.schema.id,
69
+ },
70
+ };
71
+ },
72
+
73
+ canCreateCluster() {
74
+ const schema = this.$store.getters['management/schemaFor'](CAPI.RANCHER_CLUSTER);
75
+
76
+ return !!schema?.collectionMethods.find((x) => x.toLowerCase() === 'post');
77
+ },
78
+
79
+ rows() {
80
+ return this.hciClusters.filter((c) => {
81
+ const cluster = this.mgmtClusters.find((cluster) => cluster?.metadata?.name === c?.status?.clusterName);
82
+
83
+ return isHarvesterCluster(cluster);
84
+ });
85
+ },
86
+
87
+ typeDisplay() {
88
+ return this.t(`typeLabel."${ HCI.CLUSTER }"`, { count: this.row?.length || 0 });
89
+ },
90
+ },
91
+
92
+ methods: {
93
+ async goToCluster(row) {
94
+ const timeout = setTimeout(() => {
95
+ // Don't show loading indicator for quickly fetched plugins
96
+ this.navigating = row.id;
97
+ }, 1000);
98
+
99
+ try {
100
+ await row.goToCluster();
101
+
102
+ clearTimeout(timeout);
103
+ this.navigating = false;
104
+ } catch {
105
+ // The error handling is carried out within goToCluster, but just in case something happens before the promise chain can catch it...
106
+ clearTimeout(timeout);
107
+ this.navigating = false;
108
+ }
109
+ }
110
+ }
111
+ };
112
+ </script>
113
+
114
+ <template>
115
+ <Loading v-if="$fetchState.pending" />
116
+ <div v-else>
117
+ <Masthead
118
+ :schema="realSchema"
119
+ :resource="resource"
120
+ :is-creatable="false"
121
+ :type-display="typeDisplay"
122
+ >
123
+ <template #typeDescription>
124
+ <TypeDescription :resource="hResource" />
125
+ </template>
126
+
127
+ <template
128
+ v-if="canCreateCluster"
129
+ slot="extraActions"
130
+ >
131
+ <n-link
132
+ :to="importLocation"
133
+ class="btn role-primary"
134
+ >
135
+ {{ t('cluster.importAction') }}
136
+ </n-link>
137
+ </template>
138
+ </Masthead>
139
+
140
+ <ResourceTable
141
+ v-if="rows && rows.length"
142
+ :schema="schema"
143
+ :rows="rows"
144
+ :is-creatable="true"
145
+ :namespaced="false"
146
+ :use-query-params-for-simple-filtering="useQueryParamsForSimpleFiltering"
147
+ >
148
+ <template #col:name="{row}">
149
+ <td>
150
+ <span class="cluster-link">
151
+ <a
152
+ v-if="row.isReady"
153
+ class="link"
154
+ :disabled="navigating"
155
+ @click="goToCluster(row)"
156
+ >{{ row.nameDisplay }}</a>
157
+ <span v-else>
158
+ {{ row.nameDisplay }}
159
+ </span>
160
+ <i
161
+ class="icon icon-spinner icon-spin ml-5"
162
+ :class="{'navigating': navigating === row.id}"
163
+ />
164
+ </span>
165
+ </td>
166
+ </template>
167
+
168
+ <template #cell:harvester="{row}">
169
+ <n-link
170
+ class="btn btn-sm role-primary"
171
+ :to="row.detailLocation"
172
+ >
173
+ {{ t('harvesterManager.manage') }}
174
+ </n-link>
175
+ </template>
176
+ </ResourceTable>
177
+ <div v-else>
178
+ <div class="no-clusters">
179
+ {{ t('harvesterManager.cluster.none') }}
180
+ </div>
181
+ <hr class="info-section">
182
+ <div class="logo">
183
+ <BrandImage
184
+ file-name="harvester.png"
185
+ height="64"
186
+ />
187
+ </div>
188
+ <div class="tagline">
189
+ <div>{{ t('harvesterManager.cluster.description') }}</div>
190
+ </div>
191
+ <div class="tagline sub-tagline">
192
+ <div v-clean-html="t('harvesterManager.cluster.learnMore', {}, true)" />
193
+ </div>
194
+ </div>
195
+ </div>
196
+ </template>
197
+
198
+ <style lang="scss" scoped>
199
+ .cluster-link {
200
+ display: flex;
201
+ align-items: center;
202
+
203
+ .icon {
204
+ // Use visibility to avoid the columns re-adjusting when the icon is shown
205
+ visibility: hidden;
206
+
207
+ &.navigating {
208
+ visibility: visible;
209
+ }
210
+ }
211
+
212
+ }
213
+ .no-clusters {
214
+ text-align: center;
215
+ }
216
+
217
+ .info-section {
218
+ margin-top: 60px;
219
+ }
220
+
221
+ .logo {
222
+ display: flex;
223
+ justify-content: center;
224
+ margin: 60px 0 40px 0;
225
+ }
226
+
227
+ .tagline {
228
+ display: flex;
229
+ justify-content: center;
230
+ margin-top: 30px;
231
+
232
+ > div {
233
+ font-size: 16px;
234
+ line-height: 22px;
235
+ max-width: 80%;
236
+ text-align: center;
237
+ }
238
+ }
239
+
240
+ .link {
241
+ cursor: pointer;
242
+ }
243
+
244
+ </style>