@rancher/shell 3.0.10 → 3.0.12-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.
- package/assets/styles/base/_mixins.scss +31 -0
- package/assets/styles/base/_variables.scss +2 -0
- package/assets/styles/themes/_modern.scss +6 -5
- package/assets/translations/en-us.yaml +12 -9
- package/assets/translations/zh-hans.yaml +0 -3
- package/chart/__tests__/rancher-backup-index.test.ts +248 -0
- package/chart/rancher-backup/index.vue +41 -2
- package/components/BrandImage.vue +6 -5
- package/components/ConsumptionGauge.vue +12 -4
- package/components/DynamicContent/DynamicContentIcon.vue +3 -2
- package/components/EmptyProductPage.vue +76 -0
- package/components/ExplorerProjectsNamespaces.vue +1 -4
- package/components/LazyImage.vue +2 -1
- package/components/Resource/Detail/Card/Scaler.vue +4 -4
- package/components/Resource/Detail/CopyToClipboard.vue +1 -2
- package/components/Resource/Detail/Metadata/KeyValueRow.vue +9 -3
- package/components/Resource/Detail/TitleBar/__tests__/__snapshots__/index.test.ts.snap +31 -0
- package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +45 -1
- package/components/Resource/Detail/TitleBar/index.vue +1 -1
- package/components/Resource/Detail/ViewOptions/__tests__/__snapshots__/index.test.ts.snap +9 -0
- package/components/Resource/Detail/ViewOptions/__tests__/index.test.ts +62 -0
- package/components/Resource/Detail/ViewOptions/index.vue +2 -1
- package/components/ResourceList/Masthead.vue +25 -2
- package/components/SideNav.vue +13 -0
- package/components/Tabbed/index.vue +6 -0
- package/components/__tests__/ConsumptionGauge.test.ts +31 -0
- package/components/__tests__/PromptModal.test.ts +2 -0
- package/components/fleet/FleetClusters.vue +1 -0
- package/components/fleet/__tests__/FleetClusters.test.ts +71 -0
- package/components/form/NodeScheduling.vue +17 -3
- package/components/form/PrivateRegistry.vue +69 -0
- package/components/form/ProjectMemberEditor.vue +0 -10
- package/components/form/__tests__/PrivateRegistry.test.ts +133 -0
- package/components/formatter/WorkloadHealthScale.vue +3 -1
- package/components/nav/Group.vue +26 -3
- package/components/nav/Header.vue +32 -7
- package/components/nav/TopLevelMenu.helper.ts +7 -79
- package/components/nav/TopLevelMenu.vue +15 -1
- package/components/nav/__tests__/TopLevelMenu.helper.test.ts +2 -53
- package/config/pagination-table-headers.js +8 -1
- package/config/private-label.js +2 -1
- package/config/product/apps.js +3 -1
- package/config/product/auth.js +1 -0
- package/config/product/backup.js +1 -0
- package/config/product/compliance.js +1 -1
- package/config/product/explorer.js +25 -6
- package/config/product/fleet.js +1 -0
- package/config/product/gatekeeper.js +1 -0
- package/config/product/istio.js +1 -0
- package/config/product/logging.js +1 -0
- package/config/product/longhorn.js +2 -1
- package/config/product/manager.js +1 -0
- package/config/product/monitoring.js +1 -0
- package/config/product/navlinks.js +1 -0
- package/config/product/neuvector.js +2 -1
- package/config/product/settings.js +1 -0
- package/config/product/uiplugins.js +1 -0
- package/core/__tests__/extension-manager-impl.test.js +187 -2
- package/core/__tests__/plugin-products-helpers.test.ts +454 -0
- package/core/__tests__/plugin-products.test.ts +3219 -0
- package/core/extension-manager-impl.js +34 -3
- package/core/plugin-helpers.ts +31 -0
- package/core/plugin-products-base.ts +375 -0
- package/core/plugin-products-extending.ts +44 -0
- package/core/plugin-products-helpers.ts +262 -0
- package/core/plugin-products-top-level.ts +66 -0
- package/core/plugin-products-type-guards.ts +33 -0
- package/core/plugin-products.ts +50 -0
- package/core/plugin-types.ts +222 -0
- package/core/plugin.ts +45 -10
- package/core/productDebugger.js +48 -0
- package/core/types.ts +95 -11
- package/detail/__tests__/__snapshots__/fleet.cattle.io.bundle.test.ts.snap +52 -0
- package/detail/__tests__/fleet.cattle.io.bundle.test.ts +171 -0
- package/detail/__tests__/node.test.ts +83 -0
- package/detail/fleet.cattle.io.bundle.vue +21 -34
- package/detail/management.cattle.io.oidcclient.vue +2 -1
- package/detail/node.vue +1 -0
- package/dialog/ExtensionCatalogInstallDialog.vue +1 -1
- package/dialog/InstallExtensionDialog.vue +6 -27
- package/dialog/UninstallExistingExtensionDialog.vue +141 -0
- package/dialog/UninstallExtensionDialog.vue +4 -26
- package/dialog/__tests__/UninstallExistingExtensionDialog.test.ts +114 -0
- package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +1 -0
- package/edit/catalog.cattle.io.clusterrepo.vue +17 -3
- package/edit/cloudcredential.vue +2 -1
- package/edit/monitoring.coreos.com.alertmanagerconfig/receiverConfig.vue +11 -6
- package/edit/provisioning.cattle.io.cluster/__tests__/Ingress.test.ts +176 -0
- package/edit/provisioning.cattle.io.cluster/index.vue +5 -4
- package/edit/provisioning.cattle.io.cluster/rke2.vue +4 -1
- package/edit/provisioning.cattle.io.cluster/shared.ts +4 -2
- package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +6 -0
- package/edit/provisioning.cattle.io.cluster/tabs/Ingress.vue +7 -2
- package/edit/secret/generic.vue +1 -0
- package/edit/secret/index.vue +2 -1
- package/edit/service.vue +2 -14
- package/list/management.cattle.io.feature.vue +7 -1
- package/list/provisioning.cattle.io.cluster.vue +0 -50
- package/list/workload.vue +11 -4
- package/mixins/brand.js +2 -1
- package/mixins/resource-fetch.js +12 -3
- package/models/catalog.cattle.io.clusterrepo.js +9 -0
- package/models/cluster.x-k8s.io.machinedeployment.js +8 -3
- package/models/management.cattle.io.authconfig.js +2 -1
- package/models/management.cattle.io.cluster.js +4 -3
- package/models/monitoring.coreos.com.receiver.js +11 -6
- package/models/pod.js +18 -0
- package/models/provisioning.cattle.io.cluster.js +2 -2
- package/models/workload.js +20 -2
- package/package.json +5 -6
- package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +0 -1
- package/pages/c/_cluster/apps/charts/index.vue +3 -8
- package/pages/c/_cluster/apps/charts/install.vue +8 -9
- package/pages/c/_cluster/istio/index.vue +4 -2
- package/pages/c/_cluster/longhorn/index.vue +2 -1
- package/pages/c/_cluster/monitoring/index.vue +2 -2
- package/pages/c/_cluster/neuvector/index.vue +2 -1
- package/pages/c/_cluster/settings/brand.vue +4 -4
- package/pages/c/_cluster/settings/performance.vue +0 -5
- package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +2 -1
- package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +231 -13
- package/pages/c/_cluster/uiplugins/index.vue +145 -38
- package/plugins/dashboard-store/__tests__/resource-class.test.ts +1 -0
- package/plugins/dashboard-store/actions.js +3 -2
- package/plugins/dashboard-store/resource-class.js +62 -6
- package/plugins/plugin.js +16 -0
- package/plugins/steve/steve-pagination-utils.ts +8 -2
- package/plugins/steve/subscribe.js +29 -4
- package/rancher-components/RcButton/RcButton.vue +3 -3
- package/rancher-components/RcButtonSplit/RcButtonSplit.test.ts +253 -0
- package/rancher-components/RcButtonSplit/RcButtonSplit.vue +158 -0
- package/rancher-components/RcButtonSplit/index.ts +1 -0
- package/scripts/test-plugins-build.sh +4 -4
- package/scripts/typegen.sh +13 -1
- package/store/__tests__/type-map.test.ts +84 -24
- package/store/type-map.js +42 -3
- package/tsconfig.paths.json +1 -0
- package/types/resources/pod.ts +18 -0
- package/types/shell/index.d.ts +8506 -2908
- package/types/store/dashboard-store.types.ts +5 -0
- package/types/store/pagination.types.ts +6 -0
- package/utils/__tests__/require-asset.test.ts +98 -0
- package/utils/async.ts +1 -5
- package/utils/axios.js +1 -4
- package/utils/brand.ts +3 -1
- package/utils/dynamic-importer.js +3 -2
- package/utils/favicon.js +4 -3
- package/utils/pagination-utils.ts +1 -1
- package/utils/require-asset.ts +95 -0
- package/utils/uiplugins.ts +12 -16
- package/utils/validators/__tests__/private-registry.test.ts +76 -0
- package/utils/validators/private-registry.ts +28 -0
- package/vue.config.js +4 -3
- package/components/HarvesterServiceAddOnConfig.vue +0 -207
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, watch } from 'vue';
|
|
3
|
+
import Banner from '@components/Banner/Banner.vue';
|
|
4
|
+
import { Checkbox } from '@components/Form/Checkbox';
|
|
5
|
+
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
value?: string | null;
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
mode?: string;
|
|
11
|
+
rules?: Function[];
|
|
12
|
+
checkboxTestId?: string;
|
|
13
|
+
inputTestId?: string;
|
|
14
|
+
}>();
|
|
15
|
+
|
|
16
|
+
const emit = defineEmits<{
|
|
17
|
+
'update:value': [val: string | null];
|
|
18
|
+
'update:enabled': [val: boolean];
|
|
19
|
+
}>();
|
|
20
|
+
|
|
21
|
+
const showInput = ref(!!props.value);
|
|
22
|
+
|
|
23
|
+
watch(() => props.enabled, (neu) => {
|
|
24
|
+
if (typeof neu === 'boolean' && neu !== showInput.value) {
|
|
25
|
+
showInput.value = neu;
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
watch(showInput, (neu, old) => {
|
|
30
|
+
if (neu !== props.enabled) {
|
|
31
|
+
emit('update:enabled', neu);
|
|
32
|
+
}
|
|
33
|
+
if (!neu && old && props.value) {
|
|
34
|
+
emit('update:value', null);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
watch(() => props.value, (neu) => {
|
|
39
|
+
if (!!neu && !showInput.value) {
|
|
40
|
+
showInput.value = true;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<template>
|
|
46
|
+
<Banner
|
|
47
|
+
color="info"
|
|
48
|
+
class="mt-0"
|
|
49
|
+
label-key="cluster.privateRegistry.importedDescription"
|
|
50
|
+
/>
|
|
51
|
+
<Checkbox
|
|
52
|
+
v-model:value="showInput"
|
|
53
|
+
class="mb-20"
|
|
54
|
+
:mode="mode"
|
|
55
|
+
:label="t('cluster.privateRegistry.label')"
|
|
56
|
+
:data-testid="checkboxTestId"
|
|
57
|
+
/>
|
|
58
|
+
<LabeledInput
|
|
59
|
+
v-if="showInput"
|
|
60
|
+
:value="value as string"
|
|
61
|
+
:mode="mode"
|
|
62
|
+
:rules="rules"
|
|
63
|
+
:required="true"
|
|
64
|
+
label-key="catalog.chart.registry.custom.inputLabel"
|
|
65
|
+
:data-testid="inputTestId"
|
|
66
|
+
:placeholder="t('catalog.chart.registry.custom.placeholder')"
|
|
67
|
+
@update:value="(val) => emit('update:value', val)"
|
|
68
|
+
/>
|
|
69
|
+
</template>
|
|
@@ -61,11 +61,6 @@ export default {
|
|
|
61
61
|
label: this.t('projectMembers.projectPermissions.ingressManage'),
|
|
62
62
|
value: false,
|
|
63
63
|
},
|
|
64
|
-
{
|
|
65
|
-
key: 'projectcatalogs-manage',
|
|
66
|
-
label: this.t('projectMembers.projectPermissions.projectcatalogsManage'),
|
|
67
|
-
value: false,
|
|
68
|
-
},
|
|
69
64
|
{
|
|
70
65
|
key: 'projectroletemplatebindings-manage',
|
|
71
66
|
label: this.t('projectMembers.projectPermissions.projectroletemplatebindingsManage'),
|
|
@@ -111,11 +106,6 @@ export default {
|
|
|
111
106
|
label: this.t('projectMembers.projectPermissions.monitoringUiView'),
|
|
112
107
|
value: false,
|
|
113
108
|
},
|
|
114
|
-
{
|
|
115
|
-
key: 'projectcatalogs-view',
|
|
116
|
-
label: this.t('projectMembers.projectPermissions.projectcatalogsView'),
|
|
117
|
-
value: false,
|
|
118
|
-
},
|
|
119
109
|
{
|
|
120
110
|
key: 'projectroletemplatebindings-view',
|
|
121
111
|
label: this.t('projectMembers.projectPermissions.projectroletemplatebindingsView'),
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { shallowMount } from '@vue/test-utils';
|
|
2
|
+
import PrivateRegistry from '@shell/components/form/PrivateRegistry.vue';
|
|
3
|
+
import { Checkbox } from '@components/Form/Checkbox';
|
|
4
|
+
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
|
|
5
|
+
|
|
6
|
+
const defaultMocks = {
|
|
7
|
+
$store: {
|
|
8
|
+
getters: {
|
|
9
|
+
'i18n/t': (text: string) => text,
|
|
10
|
+
t: (text: string) => text,
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const mountPrivateRegistry = (props = {}) => {
|
|
16
|
+
return shallowMount(PrivateRegistry, {
|
|
17
|
+
props: {
|
|
18
|
+
mode: 'edit',
|
|
19
|
+
...props
|
|
20
|
+
},
|
|
21
|
+
global: { mocks: defaultMocks }
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
describe('privateRegistry', () => {
|
|
26
|
+
it('should render the info banner', () => {
|
|
27
|
+
const wrapper = mountPrivateRegistry();
|
|
28
|
+
const banner = wrapper.find('[color="info"]');
|
|
29
|
+
|
|
30
|
+
expect(banner.exists()).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should render the enable checkbox', () => {
|
|
34
|
+
const wrapper = mountPrivateRegistry();
|
|
35
|
+
const checkbox = wrapper.findComponent(Checkbox);
|
|
36
|
+
|
|
37
|
+
expect(checkbox.exists()).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should not show the URL input when no value is provided', () => {
|
|
41
|
+
const wrapper = mountPrivateRegistry();
|
|
42
|
+
|
|
43
|
+
expect(wrapper.findComponent(LabeledInput).exists()).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should show the URL input when a value is provided', () => {
|
|
47
|
+
const wrapper = mountPrivateRegistry({ value: 'registry.example.com' });
|
|
48
|
+
|
|
49
|
+
expect(wrapper.findComponent(LabeledInput).exists()).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should show the URL input when checkbox is checked', async() => {
|
|
53
|
+
const wrapper = mountPrivateRegistry();
|
|
54
|
+
|
|
55
|
+
expect(wrapper.findComponent(LabeledInput).exists()).toBe(false);
|
|
56
|
+
|
|
57
|
+
const checkbox = wrapper.findComponent(Checkbox);
|
|
58
|
+
|
|
59
|
+
await checkbox.vm.$emit('update:value', true);
|
|
60
|
+
await wrapper.vm.$nextTick();
|
|
61
|
+
|
|
62
|
+
expect(wrapper.findComponent(LabeledInput).exists()).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should emit update:value with null when checkbox is unchecked', async() => {
|
|
66
|
+
const wrapper = mountPrivateRegistry({ value: 'registry.example.com' });
|
|
67
|
+
|
|
68
|
+
const checkbox = wrapper.findComponent(Checkbox);
|
|
69
|
+
|
|
70
|
+
await checkbox.vm.$emit('update:value', false);
|
|
71
|
+
await wrapper.vm.$nextTick();
|
|
72
|
+
|
|
73
|
+
expect(wrapper.emitted('update:value')).toHaveLength(1);
|
|
74
|
+
expect(wrapper.emitted('update:value')![0]).toStrictEqual([null]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should emit update:value when the URL input changes', async() => {
|
|
78
|
+
const wrapper = mountPrivateRegistry({ value: 'registry.example.com' });
|
|
79
|
+
const input = wrapper.findComponent(LabeledInput);
|
|
80
|
+
|
|
81
|
+
await input.vm.$emit('update:value', 'new-registry.example.com');
|
|
82
|
+
|
|
83
|
+
expect(wrapper.emitted('update:value')).toHaveLength(1);
|
|
84
|
+
expect(wrapper.emitted('update:value')![0]).toStrictEqual(['new-registry.example.com']);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should auto-enable the checkbox when value changes from null to a string', async() => {
|
|
88
|
+
const wrapper = mountPrivateRegistry();
|
|
89
|
+
|
|
90
|
+
expect(wrapper.findComponent(LabeledInput).exists()).toBe(false);
|
|
91
|
+
|
|
92
|
+
await wrapper.setProps({ value: 'registry.example.com' });
|
|
93
|
+
|
|
94
|
+
expect(wrapper.findComponent(LabeledInput).exists()).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should pass rules to the URL input', () => {
|
|
98
|
+
const mockRule = jest.fn();
|
|
99
|
+
const wrapper = mountPrivateRegistry({
|
|
100
|
+
value: 'registry.example.com',
|
|
101
|
+
rules: [mockRule]
|
|
102
|
+
});
|
|
103
|
+
const input = wrapper.findComponent(LabeledInput);
|
|
104
|
+
|
|
105
|
+
expect(input.attributes('rules')).toBeDefined();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should apply custom data-testid to checkbox when provided', () => {
|
|
109
|
+
const wrapper = mountPrivateRegistry({ checkboxTestId: 'my-checkbox' });
|
|
110
|
+
const checkbox = wrapper.findComponent(Checkbox);
|
|
111
|
+
|
|
112
|
+
expect(checkbox.attributes('data-testid')).toBe('my-checkbox');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should apply custom data-testid to input when provided', () => {
|
|
116
|
+
const wrapper = mountPrivateRegistry({
|
|
117
|
+
value: 'registry.example.com',
|
|
118
|
+
inputTestId: 'my-input'
|
|
119
|
+
});
|
|
120
|
+
const input = wrapper.findComponent(LabeledInput);
|
|
121
|
+
|
|
122
|
+
expect(input.attributes('data-testid')).toBe('my-input');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should not set data-testid when not provided', () => {
|
|
126
|
+
const wrapper = mountPrivateRegistry({ value: 'registry.example.com' });
|
|
127
|
+
const checkbox = wrapper.findComponent(Checkbox);
|
|
128
|
+
const input = wrapper.findComponent(LabeledInput);
|
|
129
|
+
|
|
130
|
+
expect(checkbox.attributes('data-testid')).toBeUndefined();
|
|
131
|
+
expect(input.attributes('data-testid')).toBeUndefined();
|
|
132
|
+
});
|
|
133
|
+
});
|
package/components/nav/Group.vue
CHANGED
|
@@ -269,8 +269,9 @@ export default {
|
|
|
269
269
|
@keyup.space="groupSelected()"
|
|
270
270
|
>
|
|
271
271
|
<slot name="header">
|
|
272
|
+
<!-- Group overview with link -->
|
|
272
273
|
<router-link
|
|
273
|
-
v-if="hasOverview"
|
|
274
|
+
v-if="hasOverview && hasChildren"
|
|
274
275
|
:to="headerRoute"
|
|
275
276
|
:exact="group.children[0].exact"
|
|
276
277
|
:tabindex="-1"
|
|
@@ -279,15 +280,32 @@ export default {
|
|
|
279
280
|
<span v-clean-html="group.labelDisplay || group.label" />
|
|
280
281
|
</h6>
|
|
281
282
|
</router-link>
|
|
283
|
+
<!-- Non-linked group header -->
|
|
282
284
|
<h6
|
|
283
|
-
v-else
|
|
285
|
+
v-else-if="hasChildren"
|
|
284
286
|
>
|
|
285
287
|
<span v-clean-html="group.labelDisplay || group.label" />
|
|
286
288
|
</h6>
|
|
289
|
+
<!-- Simple child (nav item) -->
|
|
290
|
+
<ul
|
|
291
|
+
v-else
|
|
292
|
+
class="list-unstyled body root-depth"
|
|
293
|
+
v-bind="$attrs"
|
|
294
|
+
>
|
|
295
|
+
<Type
|
|
296
|
+
|
|
297
|
+
:key="id+'_' + group.name + '_type'"
|
|
298
|
+
:is-root="depth == 0 && !showHeader"
|
|
299
|
+
:type="group"
|
|
300
|
+
:depth="depth"
|
|
301
|
+
:highlight-route="highlightRoute"
|
|
302
|
+
@selected="selectType($event)"
|
|
303
|
+
/>
|
|
304
|
+
</ul>
|
|
287
305
|
</slot>
|
|
288
306
|
</div>
|
|
289
307
|
<i
|
|
290
|
-
v-if="!onlyHasOverview && canCollapse"
|
|
308
|
+
v-if="!onlyHasOverview && canCollapse && hasChildren"
|
|
291
309
|
class="icon toggle toggle-accordion"
|
|
292
310
|
:class="{'icon-chevron-right': !isExpanded, 'icon-chevron-down': isExpanded}"
|
|
293
311
|
role="button"
|
|
@@ -377,6 +395,7 @@ export default {
|
|
|
377
395
|
display: block;
|
|
378
396
|
box-sizing:border-box;
|
|
379
397
|
height: 100%;
|
|
398
|
+
|
|
380
399
|
&:hover{
|
|
381
400
|
text-decoration: none;
|
|
382
401
|
}
|
|
@@ -484,6 +503,10 @@ export default {
|
|
|
484
503
|
}
|
|
485
504
|
}
|
|
486
505
|
}
|
|
506
|
+
|
|
507
|
+
.root-depth :deep() > .child.nav-type a {
|
|
508
|
+
padding-left: 14px;
|
|
509
|
+
}
|
|
487
510
|
}
|
|
488
511
|
|
|
489
512
|
&.depth-1 {
|
|
@@ -203,12 +203,6 @@ export default {
|
|
|
203
203
|
return !!this.currentCluster?.actions?.apply;
|
|
204
204
|
},
|
|
205
205
|
|
|
206
|
-
prod() {
|
|
207
|
-
const name = this.rootProduct.name;
|
|
208
|
-
|
|
209
|
-
return this.$store.getters['i18n/withFallback'](`product."${ name }"`, null, ucFirst(name));
|
|
210
|
-
},
|
|
211
|
-
|
|
212
206
|
showSearch() {
|
|
213
207
|
return this.rootProduct?.inStore === 'cluster';
|
|
214
208
|
},
|
|
@@ -239,6 +233,30 @@ export default {
|
|
|
239
233
|
isHarvester() {
|
|
240
234
|
return this.$store.getters['currentProduct'].inStore === HARVESTER;
|
|
241
235
|
},
|
|
236
|
+
|
|
237
|
+
productLabel() {
|
|
238
|
+
const name = this.rootProduct.name;
|
|
239
|
+
|
|
240
|
+
// single products do their own thing, which is the previous default behavior as per next line
|
|
241
|
+
if (this.isSingleProduct) {
|
|
242
|
+
return this.$store.getters['i18n/withFallback'](`product."${ name }"`, null, ucFirst(name));
|
|
243
|
+
} else {
|
|
244
|
+
if (this.rootProduct?.label) {
|
|
245
|
+
return this.rootProduct.label;
|
|
246
|
+
}
|
|
247
|
+
if (this.rootProduct?.labelKey) {
|
|
248
|
+
return this.$store.getters['i18n/t'](this.rootProduct.labelKey);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return this.$store.getters['i18n/withFallback'](`product."${ name }"`, null, ucFirst(name));
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
// Determine if we are on a route that shows the logo instead of the product label
|
|
256
|
+
// This is to enforce the logo display on certain routes like home, about, prefs, account, etc
|
|
257
|
+
isLogoRoute() {
|
|
258
|
+
return !this.$route.name.includes('c-cluster');
|
|
259
|
+
}
|
|
242
260
|
},
|
|
243
261
|
|
|
244
262
|
watch: {
|
|
@@ -518,7 +536,7 @@ export default {
|
|
|
518
536
|
:alt="t('branding.logos.label')"
|
|
519
537
|
>
|
|
520
538
|
<div class="product-name">
|
|
521
|
-
{{
|
|
539
|
+
{{ productLabel }}
|
|
522
540
|
</div>
|
|
523
541
|
</div>
|
|
524
542
|
</div>
|
|
@@ -534,6 +552,13 @@ export default {
|
|
|
534
552
|
{{ t(isSingleProduct.productNameKey) }}
|
|
535
553
|
</div>
|
|
536
554
|
|
|
555
|
+
<div
|
|
556
|
+
v-else-if="productLabel && !isLogoRoute"
|
|
557
|
+
class="product-name"
|
|
558
|
+
>
|
|
559
|
+
{{ productLabel }}
|
|
560
|
+
</div>
|
|
561
|
+
|
|
537
562
|
<div
|
|
538
563
|
v-else
|
|
539
564
|
class="side-menu-logo"
|
|
@@ -107,7 +107,6 @@ export interface TopLevelMenuHelper {
|
|
|
107
107
|
|
|
108
108
|
export abstract class BaseTopLevelMenuHelper {
|
|
109
109
|
protected $store: VuexStore;
|
|
110
|
-
protected hasProvCluster: boolean;
|
|
111
110
|
|
|
112
111
|
/**
|
|
113
112
|
* Filter mgmt clusters by
|
|
@@ -143,11 +142,9 @@ export abstract class BaseTopLevelMenuHelper {
|
|
|
143
142
|
$store: VuexStore,
|
|
144
143
|
}) {
|
|
145
144
|
this.$store = $store;
|
|
146
|
-
|
|
147
|
-
this.hasProvCluster = this.$store.getters[`management/schemaFor`](CAPI.RANCHER_CLUSTER);
|
|
148
145
|
}
|
|
149
146
|
|
|
150
|
-
protected convertToCluster(mgmtCluster: MgmtCluster, provCluster
|
|
147
|
+
protected convertToCluster(mgmtCluster: MgmtCluster, provCluster?: ProvCluster): TopLevelMenuCluster {
|
|
151
148
|
return {
|
|
152
149
|
id: mgmtCluster.id,
|
|
153
150
|
label: mgmtCluster.nameDisplay,
|
|
@@ -173,7 +170,6 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
|
|
|
173
170
|
|
|
174
171
|
private clustersPinnedWrapper: PaginationWrapper<any>;
|
|
175
172
|
private clustersOthersWrapper: PaginationWrapper<any>;
|
|
176
|
-
private provClusterWrapper: PaginationWrapper<any>;
|
|
177
173
|
|
|
178
174
|
private clusterCount = 0;
|
|
179
175
|
|
|
@@ -223,43 +219,10 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
|
|
|
223
219
|
},
|
|
224
220
|
formatResponse: { classify: true },
|
|
225
221
|
});
|
|
226
|
-
// Fetch all prov clusters for the mgmt clusters we have
|
|
227
|
-
this.provClusterWrapper = new PaginationWrapper({
|
|
228
|
-
$store,
|
|
229
|
-
id: 'top-level-menu-prov-clusters',
|
|
230
|
-
onChange: async({ forceWatch, revision }) => {
|
|
231
|
-
if (!this.args) {
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
try {
|
|
235
|
-
await this.update({
|
|
236
|
-
...this.args,
|
|
237
|
-
forceWatch,
|
|
238
|
-
provClusterRevision: revision,
|
|
239
|
-
});
|
|
240
|
-
} catch {
|
|
241
|
-
// Failures should be logged lower down, not much we can do here except catch to prevent whole ui page warnings in dev mode
|
|
242
|
-
}
|
|
243
|
-
},
|
|
244
|
-
enabledFor: {
|
|
245
|
-
store: STORE.MANAGEMENT,
|
|
246
|
-
resource: {
|
|
247
|
-
id: CAPI.RANCHER_CLUSTER,
|
|
248
|
-
context: 'side-bar',
|
|
249
|
-
}
|
|
250
|
-
},
|
|
251
|
-
formatResponse: { classify: true }
|
|
252
|
-
});
|
|
253
222
|
}
|
|
254
223
|
|
|
255
224
|
// ---------- requests ----------
|
|
256
225
|
async update(args: UpdateArgs) {
|
|
257
|
-
if (!this.hasProvCluster) {
|
|
258
|
-
// We're filtering out mgmt clusters without prov clusters, so if the user can't see any prov clusters at all
|
|
259
|
-
// exit early
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
226
|
this.args = args;
|
|
264
227
|
const promises = {
|
|
265
228
|
pinned: this.updatePinned(args),
|
|
@@ -271,22 +234,11 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
|
|
|
271
234
|
notPinned: MgmtCluster[]
|
|
272
235
|
} = await allHash(promises) as any;
|
|
273
236
|
|
|
274
|
-
const provClusters = await this.updateProvCluster(res.notPinned, res.pinned, args);
|
|
275
|
-
const provClustersByMgmtId = provClusters.reduce((res: { [mgmtId: string]: ProvCluster}, provCluster: ProvCluster) => {
|
|
276
|
-
if (provCluster.mgmtClusterId) {
|
|
277
|
-
res[provCluster.mgmtClusterId] = provCluster;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
return res;
|
|
281
|
-
}, {} as { [mgmtId: string]: ProvCluster});
|
|
282
|
-
|
|
283
237
|
// Filter out mgmt clusters that don't have matching prov cluster and convert remaining to required format
|
|
284
238
|
const _clustersNotPinned = res.notPinned
|
|
285
|
-
.
|
|
286
|
-
.map((mgmtCluster) => this.convertToCluster(mgmtCluster, provClustersByMgmtId[mgmtCluster.id]));
|
|
239
|
+
.map((mgmtCluster) => this.convertToCluster(mgmtCluster));
|
|
287
240
|
const _clustersPinned = res.pinned
|
|
288
|
-
.
|
|
289
|
-
.map((mgmtCluster) => this.convertToCluster(mgmtCluster, provClustersByMgmtId[mgmtCluster.id]));
|
|
241
|
+
.map((mgmtCluster) => this.convertToCluster(mgmtCluster));
|
|
290
242
|
|
|
291
243
|
this.clustersPinned.length = 0;
|
|
292
244
|
this.clustersOthers.length = 0;
|
|
@@ -298,7 +250,6 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
|
|
|
298
250
|
async destroy() {
|
|
299
251
|
this.clustersPinnedWrapper.onDestroy();
|
|
300
252
|
this.clustersOthersWrapper.onDestroy();
|
|
301
|
-
this.provClusterWrapper.onDestroy();
|
|
302
253
|
}
|
|
303
254
|
|
|
304
255
|
/**
|
|
@@ -445,41 +396,21 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
|
|
|
445
396
|
console.warn('Unable to set saved count for clusters', err); // eslint-disable-line no-console
|
|
446
397
|
}
|
|
447
398
|
}
|
|
448
|
-
|
|
449
|
-
/**
|
|
450
|
-
* Find all provisioning clusters associated with the displayed mgmt clusters
|
|
451
|
-
*/
|
|
452
|
-
private async updateProvCluster(notPinned: MgmtCluster[], pinned: MgmtCluster[], args: UpdateArgs): Promise<ProvCluster[]> {
|
|
453
|
-
return this.provClusterWrapper.request({
|
|
454
|
-
forceWatch: args.forceWatch,
|
|
455
|
-
pagination: {
|
|
456
|
-
filters: [
|
|
457
|
-
PaginationParamFilter.createMultipleFields(
|
|
458
|
-
[...notPinned, ...pinned]
|
|
459
|
-
.map((mgmtCluster) => ({
|
|
460
|
-
field: 'status.clusterName', value: mgmtCluster.id, equals: true, exact: true
|
|
461
|
-
}))
|
|
462
|
-
)
|
|
463
|
-
],
|
|
464
|
-
page: 1,
|
|
465
|
-
sort: [],
|
|
466
|
-
projectsOrNamespaces: []
|
|
467
|
-
},
|
|
468
|
-
revision: args.provClusterRevision
|
|
469
|
-
})
|
|
470
|
-
.then((r) => r.data);
|
|
471
|
-
}
|
|
472
399
|
}
|
|
473
400
|
|
|
474
401
|
/**
|
|
475
402
|
* Helper designed to supply non-paginated results for the top level menu cluster resources
|
|
476
403
|
*/
|
|
477
404
|
export class TopLevelMenuHelperLegacy extends BaseTopLevelMenuHelper implements TopLevelMenuHelper {
|
|
405
|
+
protected hasProvCluster: boolean;
|
|
406
|
+
|
|
478
407
|
constructor({ $store }: {
|
|
479
408
|
$store: VuexStore,
|
|
480
409
|
}) {
|
|
481
410
|
super({ $store });
|
|
482
411
|
|
|
412
|
+
this.hasProvCluster = this.$store.getters[`management/schemaFor`](CAPI.RANCHER_CLUSTER);
|
|
413
|
+
|
|
483
414
|
if (this.hasProvCluster) {
|
|
484
415
|
$store.dispatch('management/findAll', { type: CAPI.RANCHER_CLUSTER });
|
|
485
416
|
}
|
|
@@ -664,9 +595,6 @@ class TopLevelMenuHelperService {
|
|
|
664
595
|
const canPagination = $store.getters[`management/paginationEnabled`]({
|
|
665
596
|
id: MANAGEMENT.CLUSTER,
|
|
666
597
|
context: 'side-bar',
|
|
667
|
-
}) && $store.getters[`management/paginationEnabled`]({
|
|
668
|
-
id: CAPI.RANCHER_CLUSTER,
|
|
669
|
-
context: 'side-bar',
|
|
670
598
|
});
|
|
671
599
|
|
|
672
600
|
this._helper = canPagination ? new TopLevelMenuHelperPagination({ $store }) : new TopLevelMenuHelperLegacy({ $store });
|
|
@@ -186,8 +186,22 @@ export default {
|
|
|
186
186
|
to.params.product = p.name;
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
let label;
|
|
190
|
+
|
|
191
|
+
// Allow product to specify its label (old DSL product() did not have "label" or "labelKey")
|
|
192
|
+
// new extensions product registration supports both "label" and "labelKey" (with "labelKey" taking precedence if both are provided)
|
|
193
|
+
if (p.labelKey) {
|
|
194
|
+
label = this.$store.getters['i18n/t'](p.labelKey);
|
|
195
|
+
} else if (p.label) {
|
|
196
|
+
label = p.label;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (!label) {
|
|
200
|
+
label = this.$store.getters['i18n/withFallback'](`product.${ p.name }`, null, ucFirst(p.name));
|
|
201
|
+
}
|
|
202
|
+
|
|
189
203
|
return {
|
|
190
|
-
label
|
|
204
|
+
label,
|
|
191
205
|
icon: `icon-${ p.icon || 'copy' }`,
|
|
192
206
|
svg: p.svg,
|
|
193
207
|
value: p.name,
|
|
@@ -102,7 +102,7 @@ describe('topLevelMenu.helper', () => {
|
|
|
102
102
|
it('should initialize PaginationWrappers', () => {
|
|
103
103
|
mockStore.getters['management/schemaFor'].mockReturnValue(true);
|
|
104
104
|
new TopLevelMenuHelperPagination({ $store: mockStore });
|
|
105
|
-
expect(PaginationWrapper).toHaveBeenCalledTimes(
|
|
105
|
+
expect(PaginationWrapper).toHaveBeenCalledTimes(2);
|
|
106
106
|
});
|
|
107
107
|
|
|
108
108
|
it('should update clusters correctly', async() => {
|
|
@@ -113,19 +113,13 @@ describe('topLevelMenu.helper', () => {
|
|
|
113
113
|
const mgmtOthers = [{
|
|
114
114
|
id: 'c2', nameDisplay: 'Other', isReady: true, pinned: false, pin: jest.fn(), unpin: jest.fn()
|
|
115
115
|
}];
|
|
116
|
-
const provClusters = [
|
|
117
|
-
{ mgmtClusterId: 'c1' },
|
|
118
|
-
{ mgmtClusterId: 'c2' }
|
|
119
|
-
];
|
|
120
116
|
|
|
121
117
|
const mockRequestPinned = jest.fn().mockResolvedValue({ data: mgmtPinned });
|
|
122
118
|
const mockRequestOthers = jest.fn().mockResolvedValue({ data: mgmtOthers });
|
|
123
|
-
const mockRequestProv = jest.fn().mockResolvedValue({ data: provClusters });
|
|
124
119
|
|
|
125
120
|
(PaginationWrapper as unknown as jest.Mock)
|
|
126
121
|
.mockImplementationOnce(() => ({ request: mockRequestPinned, onDestroy: jest.fn() }))
|
|
127
|
-
.mockImplementationOnce(() => ({ request: mockRequestOthers, onDestroy: jest.fn() }))
|
|
128
|
-
.mockImplementationOnce(() => ({ request: mockRequestProv, onDestroy: jest.fn() }));
|
|
122
|
+
.mockImplementationOnce(() => ({ request: mockRequestOthers, onDestroy: jest.fn() }));
|
|
129
123
|
|
|
130
124
|
const helper = new TopLevelMenuHelperPagination({ $store: mockStore });
|
|
131
125
|
|
|
@@ -169,57 +163,12 @@ describe('topLevelMenu.helper', () => {
|
|
|
169
163
|
},
|
|
170
164
|
revision: undefined
|
|
171
165
|
});
|
|
172
|
-
expect(mockRequestProv).toHaveBeenCalledWith({
|
|
173
|
-
forceWatch: undefined,
|
|
174
|
-
pagination: {
|
|
175
|
-
filters: [{
|
|
176
|
-
equals: true,
|
|
177
|
-
fields: [{
|
|
178
|
-
equals: true, exact: true, field: 'status.clusterName', value: mgmtOthers[0].id
|
|
179
|
-
}, {
|
|
180
|
-
equals: true, exact: true, field: 'status.clusterName', value: mgmtPinned[0].id
|
|
181
|
-
}],
|
|
182
|
-
param: 'filter'
|
|
183
|
-
}],
|
|
184
|
-
page: 1,
|
|
185
|
-
projectsOrNamespaces: [],
|
|
186
|
-
sort: []
|
|
187
|
-
},
|
|
188
|
-
revision: undefined
|
|
189
|
-
});
|
|
190
166
|
|
|
191
167
|
expect(helper.clustersPinned).toHaveLength(1);
|
|
192
168
|
expect(helper.clustersPinned[0].id).toBe('c1');
|
|
193
169
|
expect(helper.clustersOthers).toHaveLength(1);
|
|
194
170
|
expect(helper.clustersOthers[0].id).toBe('c2');
|
|
195
171
|
});
|
|
196
|
-
|
|
197
|
-
it('should filter out mgmt clusters without matching prov clusters', async() => {
|
|
198
|
-
mockStore.getters['management/schemaFor'].mockReturnValue(true);
|
|
199
|
-
const mgmtOthers = [{
|
|
200
|
-
id: 'c2', nameDisplay: 'Other', isReady: true, pinned: false, pin: jest.fn(), unpin: jest.fn()
|
|
201
|
-
}];
|
|
202
|
-
// No prov cluster for c2
|
|
203
|
-
const provClusters: any[] = [];
|
|
204
|
-
|
|
205
|
-
const mockRequestPinned = jest.fn().mockResolvedValue({ data: [] });
|
|
206
|
-
const mockRequestOthers = jest.fn().mockResolvedValue({ data: mgmtOthers });
|
|
207
|
-
const mockRequestProv = jest.fn().mockResolvedValue({ data: provClusters });
|
|
208
|
-
|
|
209
|
-
(PaginationWrapper as unknown as jest.Mock)
|
|
210
|
-
.mockImplementationOnce(() => ({ request: mockRequestPinned, onDestroy: jest.fn() }))
|
|
211
|
-
.mockImplementationOnce(() => ({ request: mockRequestOthers, onDestroy: jest.fn() }))
|
|
212
|
-
.mockImplementationOnce(() => ({ request: mockRequestProv, onDestroy: jest.fn() }));
|
|
213
|
-
|
|
214
|
-
const helper = new TopLevelMenuHelperPagination({ $store: mockStore });
|
|
215
|
-
|
|
216
|
-
await helper.update({
|
|
217
|
-
searchTerm: '',
|
|
218
|
-
pinnedIds: [],
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
expect(helper.clustersOthers).toHaveLength(0);
|
|
222
|
-
});
|
|
223
172
|
});
|
|
224
173
|
|
|
225
174
|
describe('class: TopLevelMenuHelperService', () => {
|
|
@@ -4,7 +4,8 @@ import {
|
|
|
4
4
|
EVENT_LAST_SEEN_TIME,
|
|
5
5
|
EVENT_TYPE,
|
|
6
6
|
SECRET_ORIGIN,
|
|
7
|
-
EVENT_FIRST_SEEN_TIME
|
|
7
|
+
EVENT_FIRST_SEEN_TIME,
|
|
8
|
+
WORKLOAD_HEALTH_SCALE
|
|
8
9
|
} from '@shell/config/table-headers';
|
|
9
10
|
|
|
10
11
|
// This file contains table headers
|
|
@@ -95,3 +96,9 @@ export const STEVE_SECRET_ORIGIN = {
|
|
|
95
96
|
// So we sort by the 'UI_PROJECT_SECRET_COPY' annotation (management.cattle.io/project-scoped-secret-copy) which at least groups the copies.
|
|
96
97
|
sort: `metadata.annotations[${ UI_PROJECT_SECRET_COPY }]:desc`,
|
|
97
98
|
};
|
|
99
|
+
|
|
100
|
+
export const STEVE_WORKLOAD_HEALTH_SCALE = {
|
|
101
|
+
...WORKLOAD_HEALTH_SCALE,
|
|
102
|
+
sort: false,
|
|
103
|
+
search: false,
|
|
104
|
+
};
|
package/config/private-label.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SETTING } from './settings';
|
|
2
2
|
import { CURRENT_RANCHER_VERSION } from './version';
|
|
3
|
+
import { requireAsset } from '@shell/utils/require-asset';
|
|
3
4
|
|
|
4
5
|
export const ANY = 0;
|
|
5
6
|
export const STANDARD = 1;
|
|
@@ -78,7 +79,7 @@ export function setTitle() {
|
|
|
78
79
|
const v = getVendor();
|
|
79
80
|
|
|
80
81
|
if (v === 'Harvester') {
|
|
81
|
-
const ico =
|
|
82
|
+
const ico = requireAsset(`~shell/assets/images/pl/harvester.png`);
|
|
82
83
|
|
|
83
84
|
document.title = 'Harvester';
|
|
84
85
|
const link = document.createElement('link');
|