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.
- package/assets/styles/base/_functions.scss +0 -0
- package/assets/styles/base/_mixins.scss +1 -1
- package/assets/styles/global/_button.scss +10 -17
- package/assets/styles/global/_form.scss +2 -2
- package/assets/styles/global/_labeled-input.scss +2 -6
- package/assets/styles/global/_select.scss +7 -6
- package/assets/styles/global/_table.scss +2 -3
- package/assets/styles/global/_tooltip.scss +1 -8
- package/assets/styles/themes/_dark.scss +0 -2
- package/assets/styles/themes/_light.scss +2 -5
- package/assets/styles/vendor/vue-select.scss +1 -2
- package/assets/translations/en-us.yaml +3 -1
- package/assets/translations/zh-hans.yaml +28 -51
- package/components/ActionDropdown.vue +0 -1
- package/components/ActionMenuShell.vue +3 -6
- package/components/BrandImage.vue +0 -22
- package/components/ClusterIconMenu.vue +1 -1
- package/components/CodeMirror.vue +0 -1
- package/components/CruResource.vue +1 -1
- package/components/CruResourceFooter.vue +1 -1
- package/components/ExplorerProjectsNamespaces.vue +24 -4
- package/components/GlobalRoleBindings.vue +48 -112
- package/components/IndentedPanel.vue +10 -4
- package/components/PromptRemove.vue +3 -3
- package/components/ResourceDetail/Masthead.vue +242 -190
- package/components/ResourceDetail/index.vue +5 -20
- package/components/ResourceList/Masthead.vue +84 -146
- package/components/ResourceList/ResourceLoadingIndicator.vue +2 -5
- package/components/ResourceTable.vue +1 -76
- package/components/SideNav.vue +29 -66
- package/components/SortableTable/THead.vue +0 -6
- package/components/SortableTable/index.vue +388 -481
- package/components/Tabbed/index.vue +5 -4
- package/components/auth/Principal.vue +2 -3
- package/components/auth/RoleDetailEdit.vue +5 -58
- package/components/auth/SelectPrincipal.vue +0 -1
- package/components/form/BannerSettings.vue +16 -18
- package/components/form/ChangePassword.vue +4 -4
- package/components/form/ColorInput.vue +8 -32
- package/components/form/Footer.vue +1 -1
- package/components/form/InputWithSelect.vue +0 -2
- package/components/form/KeyValue.vue +7 -31
- package/components/form/LabeledSelect.vue +178 -178
- package/components/form/Members/ClusterPermissionsEditor.vue +2 -1
- package/components/form/Members/MembershipEditor.vue +1 -1
- package/components/form/NameNsDescription.vue +11 -24
- package/components/form/Password.vue +2 -6
- package/components/form/ResourceQuota/Namespace.vue +1 -1
- package/components/form/ResourceQuota/NamespaceRow.vue +10 -13
- package/components/form/ResourceQuota/ProjectRow.vue +1 -0
- package/components/form/Select.vue +2 -2
- package/components/nav/Favorite.vue +1 -5
- package/components/nav/Group.vue +23 -69
- package/components/nav/Header.vue +17 -82
- package/components/nav/HeaderPageActionMenu.vue +0 -1
- package/components/nav/NamespaceFilter.vue +3 -0
- package/components/nav/TopLevelMenu.vue +119 -182
- package/components/nav/Type.vue +11 -48
- package/components/rancherResourceDetail/Masthead.vue +769 -0
- package/components/rancherResourceDetail/__tests__/Masthead.test.ts +65 -0
- package/components/rancherResourceDetail/index.vue +591 -0
- package/components/rancherResourceList/Masthead.vue +375 -0
- package/components/rancherResourceList/ResourceLoadingIndicator.vue +140 -0
- package/components/rancherResourceList/index.vue +307 -0
- package/components/rancherResourceList/resource-list.config.js +7 -0
- package/components/rancherResourceTable.vue +783 -0
- package/components/rancherSortableTable/THead.vue +561 -0
- package/components/rancherSortableTable/actions.js +153 -0
- package/components/rancherSortableTable/advanced-filtering.js +272 -0
- package/components/rancherSortableTable/debug.js +117 -0
- package/components/rancherSortableTable/filtering.js +290 -0
- package/components/rancherSortableTable/grouping.js +48 -0
- package/components/rancherSortableTable/index.vue +2712 -0
- package/components/rancherSortableTable/paging.js +155 -0
- package/components/rancherSortableTable/selection.js +629 -0
- package/components/rancherSortableTable/sortable-config.ts +4 -0
- package/components/rancherSortableTable/sorting.js +129 -0
- package/composables/useClickOutside.ts +1 -1
- package/config/product/auth.js +7 -16
- package/config/product/explorer.js +1 -1
- package/config/product/settings.js +8 -17
- package/config/settings.ts +0 -28
- package/edit/management.cattle.io.user.vue +4 -17
- package/edit/networking.k8s.io.ingress/RulePath.vue +1 -1
- package/edit/token.vue +1 -1
- package/list/harvesterhci.io.management.cluster.vue +0 -17
- package/list/management.cattle.io.setting.vue +13 -22
- package/list/management.cattle.io.user.vue +14 -25
- package/list/provisioning.cattle.io.cluster.vue +7 -6
- package/mixins/brand.js +0 -17
- package/package.json +1 -1
- package/pages/auth/login.vue +29 -84
- package/pages/c/_cluster/auth/roles/index.vue +14 -61
- package/pages/c/_cluster/settings/banners.vue +101 -174
- package/pages/c/_cluster/settings/brand.vue +301 -348
- package/pages/c/_cluster/settings/performance.vue +38 -61
- package/pages/home.vue +21 -70
- package/pages/prefs.vue +23 -25
- package/pkg/tsconfig.json +9 -9
- package/pkg/vue.config.js +1 -1
- package/promptRemove/mixin/roleDeletionCheck.js +2 -2
- package/scripts/clean +0 -0
- package/scripts/extension/bundle +0 -0
- package/scripts/extension/helm/scripts/package +0 -0
- package/scripts/extension/helm/scripts/patch +0 -0
- package/scripts/extension/helm/scripts/version +0 -0
- package/scripts/extension/helmpatch +0 -0
- package/scripts/extension/parse-tag-name +0 -0
- package/scripts/extension/publish +0 -0
- package/scripts/publish-shell.sh +60 -86
- package/scripts/serve-pkgs +0 -0
- package/scripts/sync-shell-deps +0 -0
- package/scripts/typegen.sh +28 -44
- package/store/i18n.js +5 -5
- package/store/prefs.js +5 -17
- package/store/type-map.js +1 -2
- package/types/cloud-shell/index.d.ts +11014 -0
- package/types/shell/index.d.ts +1 -1
- package/utils/error.js +0 -4
- package/utils/router.js +3 -3
- package/vue.config.js +6 -1
- package/assets/images/action.svg +0 -6
- package/assets/images/pl/logo.png +0 -0
- /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>
|