dashboard-shell-shell 1.0.1000000117 → 1.0.1000000118

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 (124) hide show
  1. package/assets/styles/base/_functions.scss +0 -0
  2. package/assets/styles/base/_mixins.scss +1 -1
  3. package/assets/styles/global/_button.scss +10 -17
  4. package/assets/styles/global/_form.scss +2 -2
  5. package/assets/styles/global/_labeled-input.scss +2 -6
  6. package/assets/styles/global/_select.scss +7 -6
  7. package/assets/styles/global/_table.scss +2 -3
  8. package/assets/styles/global/_tooltip.scss +1 -8
  9. package/assets/styles/themes/_dark.scss +0 -2
  10. package/assets/styles/themes/_light.scss +2 -5
  11. package/assets/styles/vendor/vue-select.scss +1 -2
  12. package/assets/translations/en-us.yaml +3 -1
  13. package/assets/translations/zh-hans.yaml +28 -51
  14. package/components/ActionDropdown.vue +0 -1
  15. package/components/ActionMenuShell.vue +3 -6
  16. package/components/BrandImage.vue +0 -22
  17. package/components/ClusterIconMenu.vue +1 -1
  18. package/components/CodeMirror.vue +0 -1
  19. package/components/CruResource.vue +1 -1
  20. package/components/CruResourceFooter.vue +1 -1
  21. package/components/ExplorerProjectsNamespaces.vue +24 -4
  22. package/components/GlobalRoleBindings.vue +48 -112
  23. package/components/IndentedPanel.vue +10 -4
  24. package/components/PromptRemove.vue +3 -3
  25. package/components/ResourceDetail/Masthead.vue +242 -190
  26. package/components/ResourceDetail/index.vue +5 -20
  27. package/components/ResourceList/Masthead.vue +84 -146
  28. package/components/ResourceList/ResourceLoadingIndicator.vue +2 -5
  29. package/components/ResourceTable.vue +1 -76
  30. package/components/SideNav.vue +29 -66
  31. package/components/SortableTable/THead.vue +0 -6
  32. package/components/SortableTable/index.vue +388 -481
  33. package/components/Tabbed/index.vue +5 -4
  34. package/components/auth/Principal.vue +2 -3
  35. package/components/auth/RoleDetailEdit.vue +5 -58
  36. package/components/auth/SelectPrincipal.vue +0 -1
  37. package/components/form/BannerSettings.vue +16 -18
  38. package/components/form/ChangePassword.vue +4 -4
  39. package/components/form/ColorInput.vue +8 -32
  40. package/components/form/Footer.vue +1 -1
  41. package/components/form/InputWithSelect.vue +0 -2
  42. package/components/form/KeyValue.vue +7 -31
  43. package/components/form/LabeledSelect.vue +178 -178
  44. package/components/form/Members/ClusterPermissionsEditor.vue +2 -1
  45. package/components/form/Members/MembershipEditor.vue +1 -1
  46. package/components/form/NameNsDescription.vue +11 -24
  47. package/components/form/Password.vue +2 -6
  48. package/components/form/ResourceQuota/Namespace.vue +1 -1
  49. package/components/form/ResourceQuota/NamespaceRow.vue +10 -13
  50. package/components/form/ResourceQuota/ProjectRow.vue +1 -0
  51. package/components/form/Select.vue +2 -2
  52. package/components/nav/Favorite.vue +1 -5
  53. package/components/nav/Group.vue +23 -69
  54. package/components/nav/Header.vue +17 -82
  55. package/components/nav/HeaderPageActionMenu.vue +0 -1
  56. package/components/nav/NamespaceFilter.vue +3 -0
  57. package/components/nav/TopLevelMenu.vue +119 -182
  58. package/components/nav/Type.vue +11 -48
  59. package/components/rancherResourceDetail/Masthead.vue +769 -0
  60. package/components/rancherResourceDetail/__tests__/Masthead.test.ts +65 -0
  61. package/components/rancherResourceDetail/index.vue +591 -0
  62. package/components/rancherResourceList/Masthead.vue +375 -0
  63. package/components/rancherResourceList/ResourceLoadingIndicator.vue +140 -0
  64. package/components/rancherResourceList/index.vue +307 -0
  65. package/components/rancherResourceList/resource-list.config.js +7 -0
  66. package/components/rancherResourceTable.vue +783 -0
  67. package/components/rancherSortableTable/THead.vue +561 -0
  68. package/components/rancherSortableTable/actions.js +153 -0
  69. package/components/rancherSortableTable/advanced-filtering.js +272 -0
  70. package/components/rancherSortableTable/debug.js +117 -0
  71. package/components/rancherSortableTable/filtering.js +290 -0
  72. package/components/rancherSortableTable/grouping.js +48 -0
  73. package/components/rancherSortableTable/index.vue +2712 -0
  74. package/components/rancherSortableTable/paging.js +155 -0
  75. package/components/rancherSortableTable/selection.js +629 -0
  76. package/components/rancherSortableTable/sortable-config.ts +4 -0
  77. package/components/rancherSortableTable/sorting.js +129 -0
  78. package/composables/useClickOutside.ts +1 -1
  79. package/config/product/auth.js +7 -16
  80. package/config/product/explorer.js +1 -1
  81. package/config/product/settings.js +8 -17
  82. package/config/settings.ts +0 -28
  83. package/edit/management.cattle.io.user.vue +4 -17
  84. package/edit/networking.k8s.io.ingress/RulePath.vue +1 -1
  85. package/edit/token.vue +1 -1
  86. package/list/harvesterhci.io.management.cluster.vue +0 -17
  87. package/list/management.cattle.io.setting.vue +13 -22
  88. package/list/management.cattle.io.user.vue +14 -25
  89. package/list/provisioning.cattle.io.cluster.vue +7 -6
  90. package/mixins/brand.js +0 -17
  91. package/package.json +1 -1
  92. package/pages/auth/login.vue +29 -84
  93. package/pages/c/_cluster/auth/roles/index.vue +14 -61
  94. package/pages/c/_cluster/settings/banners.vue +101 -174
  95. package/pages/c/_cluster/settings/brand.vue +301 -348
  96. package/pages/c/_cluster/settings/performance.vue +38 -61
  97. package/pages/home.vue +21 -70
  98. package/pages/prefs.vue +23 -25
  99. package/pkg/tsconfig.json +9 -9
  100. package/pkg/vue.config.js +1 -1
  101. package/promptRemove/mixin/roleDeletionCheck.js +2 -2
  102. package/scripts/clean +0 -0
  103. package/scripts/extension/bundle +0 -0
  104. package/scripts/extension/helm/scripts/package +0 -0
  105. package/scripts/extension/helm/scripts/patch +0 -0
  106. package/scripts/extension/helm/scripts/version +0 -0
  107. package/scripts/extension/helmpatch +0 -0
  108. package/scripts/extension/parse-tag-name +0 -0
  109. package/scripts/extension/publish +0 -0
  110. package/scripts/publish-shell.sh +60 -86
  111. package/scripts/serve-pkgs +0 -0
  112. package/scripts/sync-shell-deps +0 -0
  113. package/scripts/typegen.sh +28 -44
  114. package/store/i18n.js +5 -5
  115. package/store/prefs.js +5 -17
  116. package/store/type-map.js +1 -2
  117. package/types/cloud-shell/index.d.ts +11014 -0
  118. package/types/shell/index.d.ts +1 -1
  119. package/utils/error.js +0 -4
  120. package/utils/router.js +3 -3
  121. package/vue.config.js +6 -1
  122. package/assets/images/action.svg +0 -6
  123. package/assets/images/pl/logo.png +0 -0
  124. /package/components/{ResourceList → rancherResourceList}/Masthead-btn.vue +0 -0
@@ -0,0 +1,65 @@
1
+ import { mount, RouterLinkStub } from '@vue/test-utils';
2
+ import { _VIEW } from '@shell/config/query-params';
3
+ import Masthead from '@shell/components/ResourceDetail/Masthead.vue';
4
+ import { createStore } from 'vuex';
5
+
6
+ const mockedStore = () => {
7
+ return {
8
+ getters: {
9
+ currentStore: () => 'current_store',
10
+ currentProduct: { inStore: 'cluster' },
11
+ isExplorer: false,
12
+ currentCluster: {},
13
+ 'type-map/labelFor': jest.fn(),
14
+ 'type-map/optionsFor': jest.fn(),
15
+ 'current_store/schemaFor': jest.fn(),
16
+ },
17
+ };
18
+ };
19
+
20
+ const requiredSetup = () => {
21
+ const store = createStore({ getters: { 'management/byId': () => jest.fn() } });
22
+
23
+ return {
24
+ stubs: {
25
+ 'router-link': RouterLinkStub,
26
+ LiveDate: true
27
+ },
28
+ provide: { store },
29
+ mocks: { $store: mockedStore() }
30
+ };
31
+ };
32
+
33
+ describe('component: Masthead', () => {
34
+ it.each([
35
+ ['hidden', '', false, { displayName: 'admin', location: { id: 'resource-id' } }, false, false],
36
+ ['plain-text', 'admin', true, { displayName: 'admin', location: null }, false, true],
37
+ ['link', 'foo', true, { displayName: 'foo', location: { id: 'resource-id' } }, true, false],
38
+ ])('"Created By" should be %p, with text: %p', (
39
+ _,
40
+ text,
41
+ showCreatedBy,
42
+ createdBy,
43
+ showLink,
44
+ showPlainText,
45
+ ) => {
46
+ const wrapper = mount(Masthead, {
47
+ props: {
48
+ mode: _VIEW,
49
+ value: {
50
+ showCreatedBy,
51
+ createdBy,
52
+ },
53
+ },
54
+ global: { ...requiredSetup() }
55
+ });
56
+
57
+ const container = wrapper.find('[data-testid="masthead-subheader-createdBy"]');
58
+ const link = wrapper.find('[data-testid="masthead-subheader-createdBy-link"]');
59
+ const plainText = wrapper.find('[data-testid="masthead-subheader-createdBy_plain-text"]');
60
+
61
+ expect(link.exists()).toBe(showLink);
62
+ expect(plainText.exists()).toBe(showPlainText);
63
+ expect(showLink || showPlainText ? container.element.textContent : '').toContain(text);
64
+ });
65
+ });
@@ -0,0 +1,591 @@
1
+ <script>
2
+ import CreateEditView from '@shell/mixins/create-edit-view/impl';
3
+ import Loading from '@shell/components/Loading';
4
+ import ResourceYaml from '@shell/components/ResourceYaml';
5
+ import {
6
+ _VIEW, _EDIT, _CLONE, _IMPORT, _STAGE, _CREATE,
7
+ AS, _YAML, _DETAIL, _CONFIG, _GRAPH, PREVIEW, MODE,
8
+ } from '@shell/config/query-params';
9
+ import { FLEET, SCHEMA } from '@shell/config/types';
10
+ import { createYaml } from '@shell/utils/create-yaml';
11
+ import Masthead from '@shell/components/ResourceDetail/Masthead';
12
+ import DetailTop from '@shell/components/DetailTop';
13
+ import { clone, diff } from '@shell/utils/object';
14
+ import IconMessage from '@shell/components/IconMessage';
15
+ import ForceDirectedTreeChart from '@shell/components/fleet/ForceDirectedTreeChart';
16
+ import { checkSchemasForFindAllHash } from '@shell/utils/auth';
17
+ import { stringify } from '@shell/utils/error';
18
+ import { Banner } from '@components/Banner';
19
+ import { harvesterhci2cloud, cloud2harvesterhci } from '@shell/utils/router'
20
+
21
+ function modeFor(route) {
22
+ if ( route.query?.mode === _IMPORT ) {
23
+ return _IMPORT;
24
+ }
25
+
26
+ if ( route.params?.id ) {
27
+ return route.query.mode || _VIEW;
28
+ } else {
29
+ return _CREATE;
30
+ }
31
+ }
32
+
33
+ async function getYaml(store, model) {
34
+ let yaml;
35
+ const opt = { headers: { accept: 'application/yaml' } };
36
+
37
+ if ( model.hasLink('view') ) {
38
+ yaml = (await model.followLink('view', opt)).data;
39
+ }
40
+
41
+ return model.cleanForDownload(yaml);
42
+ }
43
+
44
+ export default {
45
+ emits: ['input'],
46
+
47
+ components: {
48
+ Loading,
49
+ DetailTop,
50
+ ForceDirectedTreeChart,
51
+ ResourceYaml,
52
+ Masthead,
53
+ IconMessage,
54
+ Banner
55
+ },
56
+
57
+ mixins: [CreateEditView],
58
+
59
+ props: {
60
+ storeOverride: {
61
+ type: String,
62
+ default: null,
63
+ },
64
+
65
+ resourceOverride: {
66
+ type: String,
67
+ default: null,
68
+ },
69
+
70
+ parentRouteOverride: {
71
+ type: String,
72
+ default: null,
73
+ },
74
+
75
+ flexContent: {
76
+ type: Boolean,
77
+ default: false,
78
+ },
79
+
80
+ /**
81
+ * Inherited global identifier prefix for tests
82
+ * Define a term based on the parent component to avoid conflicts on multiple components
83
+ */
84
+ componentTestid: {
85
+ type: String,
86
+ default: 'resource-details'
87
+ },
88
+ errorsMap: {
89
+ type: Object,
90
+ default: null
91
+ },
92
+ },
93
+
94
+ async fetch() {
95
+ const store = this.$store;
96
+ const route = this.$route;
97
+ const params = route.params;
98
+ const cloneParams = clone(params)
99
+ let resourceType = this.resourceOverride || cloneParams.resource;
100
+
101
+ const inStore = this.storeOverride || store.getters['currentStore'](resourceType);
102
+ const realMode = this.realMode;
103
+
104
+ // eslint-disable-next-line prefer-const
105
+ let { namespace, id } = params;
106
+
107
+ // There are 6 "real" modes that can be put into the query string
108
+ // These are mapped down to the 3 regular page "mode"s that create-edit-view components
109
+ // know about: view, edit, create (stage, import and clone become "create")
110
+ const mode = ([_CLONE, _IMPORT, _STAGE].includes(realMode) ? _CREATE : realMode);
111
+
112
+ const getGraphConfig = store.getters['type-map/hasGraph'](resourceType);
113
+ const hasGraph = !!getGraphConfig;
114
+ const hasCustomDetail = store.getters['type-map/hasCustomDetail'](resourceType, id);
115
+ const hasCustomEdit = store.getters['type-map/hasCustomEdit'](resourceType, id);
116
+
117
+ const schemas = store.getters[`${ inStore }/all`](SCHEMA);
118
+
119
+ // As determines what component will be rendered
120
+ const requested = route.query[AS];
121
+ let as;
122
+ let notFound = false;
123
+
124
+ if ( mode === _VIEW && hasCustomDetail && (!requested || requested === _DETAIL) ) {
125
+ as = _DETAIL;
126
+ } else if ( mode === _VIEW && hasGraph && requested === _GRAPH) {
127
+ as = _GRAPH;
128
+ } else if ( hasCustomEdit && (!requested || requested === _CONFIG) ) {
129
+ as = _CONFIG;
130
+ } else {
131
+ as = _YAML;
132
+ }
133
+
134
+ this.as = as;
135
+
136
+ const options = store.getters[`type-map/optionsFor`](resourceType);
137
+
138
+ this.showMasthead = [_CREATE, _EDIT].includes(mode) ? options.resourceEditMasthead : true;
139
+ const canViewYaml = options.canYaml;
140
+
141
+ if ( options.resource ) {
142
+ resourceType = options.resource;
143
+ }
144
+
145
+ const schema = store.getters[`${ inStore }/schemaFor`](resourceType);
146
+ let model, initialModel, liveModel, yaml;
147
+
148
+ if ( realMode === _CREATE || realMode === _IMPORT ) {
149
+ if ( !namespace ) {
150
+ namespace = store.getters['defaultNamespace'];
151
+ }
152
+
153
+ const data = { type: resourceType };
154
+
155
+ if ( schema?.attributes?.namespaced ) {
156
+ data.metadata = { namespace };
157
+ }
158
+
159
+ liveModel = await store.dispatch(`${ inStore }/create`, data);
160
+ initialModel = await store.dispatch(`${ inStore }/clone`, { resource: liveModel });
161
+ model = await store.dispatch(`${ inStore }/clone`, { resource: liveModel });
162
+
163
+ if (model.forceYaml === true) {
164
+ as = _YAML;
165
+ this.as = as;
166
+ }
167
+
168
+ if ( as === _YAML ) {
169
+ if (schema?.fetchResourceFields) {
170
+ // fetch resourceFields for createYaml
171
+ await schema.fetchResourceFields();
172
+ }
173
+
174
+ yaml = createYaml(schemas, resourceType, data);
175
+ }
176
+ } else {
177
+ if ( as === _GRAPH ) {
178
+ const graphSchema = await checkSchemasForFindAllHash({
179
+ cluster: {
180
+ inStoreType: 'management',
181
+ type: FLEET.CLUSTER
182
+ },
183
+ bundle: {
184
+ inStoreType: 'management',
185
+ type: FLEET.BUNDLE,
186
+ opt: { excludeFields: ['metadata.managedFields', 'spec.resources'] },
187
+ },
188
+
189
+ bundleDeployment: {
190
+ inStoreType: 'management',
191
+ type: FLEET.BUNDLE_DEPLOYMENT
192
+ }
193
+
194
+ }, this.$store);
195
+
196
+ this.canViewChart = graphSchema.cluster && graphSchema.bundle && graphSchema.bundleDeployment;
197
+ }
198
+
199
+ let fqid = id;
200
+
201
+ if ( schema.attributes?.namespaced && namespace ) {
202
+ fqid = `${ namespace }/${ fqid }`;
203
+ }
204
+
205
+ try {
206
+ liveModel = await store.dispatch(`${ inStore }/find`, {
207
+ type: resourceType,
208
+ id: fqid,
209
+ opt: { watch: true }
210
+ });
211
+ } catch (e) {
212
+ if (e.status === 404 || e.status === 403) {
213
+ store.dispatch('loadingError', new Error(this.t('nav.failWhale.resourceIdNotFound', { resource: resourceType, fqid }, true)));
214
+ }
215
+ liveModel = {};
216
+ notFound = fqid;
217
+ }
218
+
219
+ try {
220
+ if (realMode === _VIEW) {
221
+ model = liveModel;
222
+ } else {
223
+ model = await store.dispatch(`${ inStore }/clone`, { resource: liveModel });
224
+ }
225
+ initialModel = await store.dispatch(`${ inStore }/clone`, { resource: liveModel });
226
+
227
+ if ( as === _YAML ) {
228
+ yaml = await getYaml(this.$store, liveModel);
229
+ }
230
+ } catch (e) {
231
+ this.errors.push(e);
232
+ }
233
+ if ( as === _YAML ) {
234
+ try {
235
+ yaml = await getYaml(this.$store, liveModel);
236
+ } catch (e) {
237
+ this.errors.push(e);
238
+ }
239
+ }
240
+
241
+ if ( as === _GRAPH ) {
242
+ this.chartData = liveModel;
243
+ }
244
+
245
+ if ( [_CLONE, _IMPORT, _STAGE].includes(realMode) ) {
246
+ model.cleanForNew();
247
+ yaml = model.cleanYaml(yaml, realMode);
248
+ }
249
+ }
250
+
251
+ // Ensure common properties exists
252
+ try {
253
+ model = await store.dispatch(`${ inStore }/cleanForDetail`, model);
254
+ } catch (e) {
255
+ this.errors.push(e);
256
+ }
257
+
258
+ const out = {
259
+ hasGraph,
260
+ getGraphConfig,
261
+ hasCustomDetail,
262
+ hasCustomEdit,
263
+ canViewYaml,
264
+ resourceType,
265
+ as,
266
+ yaml,
267
+ initialModel,
268
+ liveModel,
269
+ mode,
270
+ value: model,
271
+ notFound,
272
+ };
273
+
274
+ for ( const key in out ) {
275
+ this[key] = out[key];
276
+ }
277
+
278
+ if ( this.mode === _CREATE ) {
279
+ this.value.applyDefaults(this, realMode);
280
+ }
281
+ },
282
+ data() {
283
+ return {
284
+ chartData: null,
285
+ resourceSubtype: null,
286
+
287
+ // Set by fetch
288
+ hasGraph: null,
289
+ hasCustomDetail: null,
290
+ hasCustomEdit: null,
291
+ resourceType: null,
292
+ asYaml: null,
293
+ yaml: null,
294
+ liveModel: null,
295
+ initialModel: null,
296
+ mode: null,
297
+ as: null,
298
+ value: null,
299
+ model: null,
300
+ notFound: null,
301
+ canViewChart: true,
302
+ canViewYaml: null,
303
+ errors: []
304
+ };
305
+ },
306
+
307
+ computed: {
308
+ realMode() {
309
+ // There are 5 "real" modes that you can start in: view, edit, create, stage, clone
310
+ const realMode = modeFor(this.$route);
311
+
312
+ return realMode;
313
+ },
314
+
315
+ isView() {
316
+ return this.mode === _VIEW;
317
+ },
318
+
319
+ isYaml() {
320
+ return this.as === _YAML;
321
+ },
322
+
323
+ isDetail() {
324
+ return this.as === _DETAIL;
325
+ },
326
+
327
+ isGraph() {
328
+ return this.as === _GRAPH;
329
+ },
330
+
331
+ offerPreview() {
332
+ return this.as === _YAML && [_EDIT, _CLONE, _IMPORT, _STAGE].includes(this.mode);
333
+ },
334
+
335
+ showComponent() {
336
+ switch ( this.as ) {
337
+ case _DETAIL: return this.detailComponent;
338
+ case _CONFIG: return this.editComponent;
339
+ }
340
+
341
+ return null;
342
+ },
343
+ hasErrors() {
344
+ return this.errors?.length && Array.isArray(this.errors);
345
+ },
346
+ mappedErrors() {
347
+ return !this.errors ? {} : this.errorsMap || this.errors.reduce((acc, error) => ({
348
+ ...acc,
349
+ [error]: {
350
+ message: error?.data?.message || error,
351
+ icon: null
352
+ }
353
+ }), {});
354
+ },
355
+ },
356
+
357
+ watch: {
358
+ '$route'(current, prev) {
359
+ if (current.name !== prev.name) {
360
+ return;
361
+ }
362
+ const neu = clone(current.query);
363
+ const old = clone(prev.query);
364
+
365
+ delete neu[PREVIEW];
366
+ delete old[PREVIEW];
367
+
368
+ if ( !this.isView ) {
369
+ delete neu[AS];
370
+ delete old[AS];
371
+ }
372
+
373
+ const queryDiff = Object.keys(diff(neu, old));
374
+
375
+ if (queryDiff.includes(MODE) || queryDiff.includes(AS)) {
376
+ this.$fetch();
377
+ }
378
+ },
379
+
380
+ // Auto refresh YAML when the model changes
381
+ async 'value.metadata.resourceVersion'(a, b) {
382
+ if ( this.mode === _VIEW && this.as === _YAML && a && b && a !== b) {
383
+ this.yaml = await getYaml(this.$store, this.liveModel);
384
+ }
385
+ }
386
+ },
387
+
388
+ created() {
389
+ this.configureResource();
390
+ },
391
+
392
+ methods: {
393
+ stringify,
394
+ setSubtype(subtype) {
395
+ this.resourceSubtype = subtype;
396
+ },
397
+
398
+ keyAction(act) {
399
+ const m = this.liveModel;
400
+
401
+ if ( m?.[act] ) {
402
+ m[act]();
403
+ }
404
+ },
405
+ closeError(index) {
406
+ this.errors = this.errors.filter((_, i) => i !== index);
407
+ },
408
+ /**
409
+ * Initializes the resource components based on the provided user and
410
+ * resource override.
411
+ *
412
+ * Configures the detail and edit components for a resource based on the
413
+ * user's ID and the specified resource.
414
+ *
415
+ * @param {Object} user - The user object containing user-specific
416
+ * information.
417
+ * @param {string|null} resourceOverride - An optional resource override
418
+ * string. If not provided, the method will use the default resource from
419
+ * the route parameters or the instance's resourceOverride property.
420
+ */
421
+ configureResource(userId = '', resourceOverride = null) {
422
+ const id = userId || this.$route.params.id;
423
+ // const paramsResource = this.$route.params.resource
424
+ const routerResource = clone(this.$route.params.resource)
425
+ const resource = resourceOverride || this.resourceOverride || routerResource;
426
+ const options = this.$store.getters[`type-map/optionsFor`](resource);
427
+
428
+ const detailResource = options.resourceDetail || options.resource || resource;
429
+ const editResource = options.resourceEdit || options.resource || resource;
430
+
431
+ // FIXME: These aren't right... signature is (rawType, subType).. not (rawType, resourceId)
432
+ // Remove id? How does subtype get in (cluster/node)
433
+ this.detailComponent = this.$store.getters['type-map/importDetail'](detailResource, id);
434
+ this.editComponent = this.$store.getters['type-map/importEdit'](editResource, id);
435
+ },
436
+ /**
437
+ * Sets the mode and initializes the resource components.
438
+ *
439
+ * This method sets the mode of the component and configures the resource
440
+ * components based on the provided user and resource.
441
+ *
442
+ * @param {Object} payload - An object containing the mode, user, and
443
+ * resource properties.
444
+ * @param {string} payload.mode - The mode to set.
445
+ * @param {Object} payload.user - The user object containing user-specific
446
+ * information.
447
+ * @param {string} payload.resource - The resource string to use for
448
+ * initialization.
449
+ */
450
+ setMode({ mode, userId, resource }) {
451
+ this.mode = mode;
452
+ this.value.id = userId;
453
+ this.configureResource(userId, resource);
454
+ }
455
+ }
456
+ };
457
+ </script>
458
+
459
+ <template>
460
+
461
+ <!-- 如果数据还在加载中,或者资源未找到,则显示 Loading 组件 -->
462
+ <Loading v-if="$fetchState.pending || notFound" />
463
+
464
+ <!-- 数据加载完成且资源存在时 -->
465
+ <div style="padding: 20px 0 20px 20px;height: 100%;" v-else>
466
+
467
+ <!-- 顶部 Masthead 区域 -->
468
+ <Masthead
469
+ v-if="showMasthead"
470
+ :resource="resourceType"
471
+ :value="liveModel"
472
+ :mode="mode"
473
+ :real-mode="realMode"
474
+ :as="as"
475
+ :has-graph="hasGraph"
476
+ :has-detail="hasCustomDetail"
477
+ :has-edit="hasCustomEdit"
478
+ :can-view-yaml="canViewYaml"
479
+ :resource-subtype="resourceSubtype"
480
+ :parent-route-override="parentRouteOverride"
481
+ :store-override="storeOverride"
482
+ >
483
+
484
+ <!-- 详情顶部信息,仅在查看模式且是详情页面时显示 -->
485
+ <!-- <DetailTop
486
+ v-if="isView && isDetail"
487
+ :value="liveModel"
488
+ /> -->
489
+ </Masthead>
490
+
491
+
492
+ <!-- 错误信息显示区域 -->
493
+ <div
494
+ v-if="hasErrors"
495
+ id="cru-errors"
496
+ class="cru__errors"
497
+ >
498
+
499
+ <!-- 循环渲染错误 Banner -->
500
+ <Banner
501
+ v-for="(err, i) in errors"
502
+ :key="i"
503
+ color="error"
504
+ :data-testid="`error-banner${i}`"
505
+ :label="stringify(mappedErrors[err].message)"
506
+ :icon="mappedErrors[err].icon"
507
+ :closable="true"
508
+ @close="closeError(i)"
509
+ />
510
+ </div>
511
+
512
+ <!-- 力导向图模式 -->
513
+ <ForceDirectedTreeChart
514
+ v-if="isGraph && canViewChart"
515
+ :data="chartData"
516
+ :fdc-config="getGraphConfig"
517
+ />
518
+
519
+ <!-- YAML 查看/编辑模式 -->
520
+ <ResourceYaml
521
+ v-else-if="isYaml"
522
+ ref="resourceyaml"
523
+ :value="value"
524
+ :mode="mode"
525
+ :yaml="yaml"
526
+ :offer-preview="offerPreview"
527
+ :done-route="doneRoute"
528
+ :done-override="value ? value.doneOverride : null"
529
+ @update:value="$emit('input', $event)"
530
+ @error="e=>errors.push(e)"
531
+ />
532
+
533
+ <!-- 动态组件渲染,根据 showComponent 动态选择组件 -->
534
+ <component
535
+ :is="showComponent"
536
+ v-else
537
+ ref="comp"
538
+ v-model:value="value"
539
+ v-bind="$data"
540
+ :done-params="doneParams"
541
+ :done-route="doneRoute"
542
+ :mode="mode"
543
+ :initial-value="initialModel"
544
+ :live-value="liveModel"
545
+ :real-mode="realMode"
546
+ :class="{'flex-content': flexContent}"
547
+ @update:value="$emit('input', $event)"
548
+ @update:mode="setMode"
549
+ @set-subtype="setSubtype"
550
+ />
551
+
552
+
553
+ <!-- 快捷键按钮(隐藏) -->
554
+ <button
555
+ v-if="isView"
556
+ v-shortkey.once="['shift','d']"
557
+ :data-testid="componentTestid + '-detail'"
558
+ class="hide"
559
+ @shortkey="keyAction('goToDetail')"
560
+ />
561
+ <button
562
+ v-if="isView"
563
+ v-shortkey.once="['shift','c']"
564
+ :data-testid="componentTestid + '-config'"
565
+ class="hide"
566
+ @shortkey="keyAction('goToViewConfig')"
567
+ />
568
+ <button
569
+ v-if="isView"
570
+ v-shortkey.once="['shift','y']"
571
+ :data-testid="componentTestid + '-yaml'"
572
+ class="hide"
573
+ @shortkey="keyAction('goToViewYaml')"
574
+ />
575
+ <button
576
+ v-if="isView"
577
+ v-shortkey.once="['shift','e']"
578
+ :data-testid="componentTestid + '-edit'"
579
+ class="hide"
580
+ @shortkey="keyAction('goToEdit')"
581
+ />
582
+ </div>
583
+ </template>
584
+
585
+ <style lang='scss' scoped>
586
+ .flex-content {
587
+ display: flex;
588
+ flex-direction: column;
589
+ flex-grow: 1;
590
+ }
591
+ </style>