@rancher/shell 3.0.10 → 3.0.11
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/translations/en-us.yaml +7 -5
- 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/ExplorerProjectsNamespaces.vue +1 -4
- package/components/LazyImage.vue +2 -1
- package/components/Resource/Detail/Card/Scaler.vue +4 -4
- package/components/Tabbed/index.vue +6 -0
- package/components/__tests__/ConsumptionGauge.test.ts +31 -0
- package/components/form/ProjectMemberEditor.vue +0 -10
- package/components/nav/TopLevelMenu.helper.ts +7 -79
- package/components/nav/__tests__/TopLevelMenu.helper.test.ts +2 -53
- package/config/private-label.js +2 -1
- package/config/product/apps.js +1 -0
- package/core/__tests__/extension-manager-impl.test.js +187 -2
- package/core/extension-manager-impl.js +4 -2
- package/core/plugin-helpers.ts +31 -0
- package/detail/__tests__/node.test.ts +83 -0
- package/detail/management.cattle.io.oidcclient.vue +2 -1
- package/detail/node.vue +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/index.vue +5 -4
- package/edit/provisioning.cattle.io.cluster/shared.ts +4 -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 -49
- package/mixins/brand.js +2 -1
- 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/provisioning.cattle.io.cluster.js +2 -2
- package/package.json +5 -5
- 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/performance.vue +0 -5
- package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +2 -1
- package/pages/c/_cluster/uiplugins/index.vue +2 -1
- package/plugins/steve/steve-pagination-utils.ts +1 -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/types/shell/index.d.ts +1 -0
- package/utils/__tests__/require-asset.test.ts +98 -0
- package/utils/async.ts +1 -5
- package/utils/brand.ts +3 -1
- package/utils/favicon.js +4 -3
- package/utils/require-asset.ts +95 -0
- package/vue.config.js +4 -3
- package/components/HarvesterServiceAddOnConfig.vue +0 -207
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* A split button that combines a primary action button with a dropdown trigger
|
|
4
|
+
* for secondary actions.
|
|
5
|
+
*
|
|
6
|
+
* Example:
|
|
7
|
+
*
|
|
8
|
+
* <rc-button-split variant="primary" @click="doAction" @update:open="onOpen">
|
|
9
|
+
* Save
|
|
10
|
+
* <template #dropdownCollection>
|
|
11
|
+
* <rc-dropdown-item @click="doOtherAction">Save as Draft</rc-dropdown-item>
|
|
12
|
+
* </template>
|
|
13
|
+
* </rc-button-split>
|
|
14
|
+
*/
|
|
15
|
+
import { RcButton } from '@components/RcButton';
|
|
16
|
+
import { RcDropdown, RcDropdownItem, RcDropdownTrigger } from '@components/RcDropdown';
|
|
17
|
+
import RcIcon from '@components/RcIcon/RcIcon.vue';
|
|
18
|
+
import { ButtonVariant, ButtonSize, IconProps } from '@components/RcButton/types';
|
|
19
|
+
import type { Placement } from 'floating-vue';
|
|
20
|
+
|
|
21
|
+
type RcButtonSplitVariant = Exclude<ButtonVariant, 'link' | 'ghost' | 'multiAction'>;
|
|
22
|
+
|
|
23
|
+
type RcButtonSplitItem = {
|
|
24
|
+
id: string;
|
|
25
|
+
label: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type RcButtonSplitProps = {
|
|
29
|
+
disabled?: boolean;
|
|
30
|
+
variant?: RcButtonSplitVariant;
|
|
31
|
+
size?: ButtonSize;
|
|
32
|
+
ariaLabel?: string;
|
|
33
|
+
ariaLabelTrigger?: string;
|
|
34
|
+
ariaLabelDropdown?: string;
|
|
35
|
+
placement?: Placement;
|
|
36
|
+
distance?: number;
|
|
37
|
+
items?: RcButtonSplitItem[];
|
|
38
|
+
} & IconProps;
|
|
39
|
+
|
|
40
|
+
withDefaults(
|
|
41
|
+
defineProps<RcButtonSplitProps>(),
|
|
42
|
+
{
|
|
43
|
+
disabled: false,
|
|
44
|
+
variant: 'primary',
|
|
45
|
+
size: 'medium',
|
|
46
|
+
ariaLabel: undefined,
|
|
47
|
+
ariaLabelTrigger: undefined,
|
|
48
|
+
ariaLabelDropdown: undefined,
|
|
49
|
+
placement: 'bottom-end',
|
|
50
|
+
distance: undefined,
|
|
51
|
+
items: undefined,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const emit = defineEmits<{
|
|
55
|
+
click: [event: MouseEvent];
|
|
56
|
+
'update:open': [open: boolean];
|
|
57
|
+
select: [id: string];
|
|
58
|
+
}>();
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<template>
|
|
62
|
+
<RcDropdown
|
|
63
|
+
:aria-label="ariaLabelDropdown"
|
|
64
|
+
:placement="placement"
|
|
65
|
+
:distance="distance"
|
|
66
|
+
@update:open="emit('update:open', $event)"
|
|
67
|
+
>
|
|
68
|
+
<div class="rc-button-split">
|
|
69
|
+
<RcButton
|
|
70
|
+
class="rc-button-split-action"
|
|
71
|
+
:aria-label="ariaLabel"
|
|
72
|
+
:disabled="disabled"
|
|
73
|
+
:variant="variant"
|
|
74
|
+
:size="size"
|
|
75
|
+
:left-icon="leftIcon"
|
|
76
|
+
:right-icon="rightIcon"
|
|
77
|
+
@click="emit('click', $event)"
|
|
78
|
+
>
|
|
79
|
+
<template
|
|
80
|
+
v-if="$slots.before"
|
|
81
|
+
#before
|
|
82
|
+
>
|
|
83
|
+
<slot name="before" />
|
|
84
|
+
</template>
|
|
85
|
+
<slot />
|
|
86
|
+
<template
|
|
87
|
+
v-if="$slots.after"
|
|
88
|
+
#after
|
|
89
|
+
>
|
|
90
|
+
<slot name="after" />
|
|
91
|
+
</template>
|
|
92
|
+
</RcButton>
|
|
93
|
+
|
|
94
|
+
<RcDropdownTrigger
|
|
95
|
+
class="rc-button-split-trigger"
|
|
96
|
+
:aria-label="ariaLabelTrigger"
|
|
97
|
+
:disabled="disabled"
|
|
98
|
+
:variant="variant"
|
|
99
|
+
:size="size"
|
|
100
|
+
>
|
|
101
|
+
<RcIcon
|
|
102
|
+
type="chevron-down"
|
|
103
|
+
size="inherit"
|
|
104
|
+
/>
|
|
105
|
+
</RcDropdownTrigger>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<template #dropdownCollection>
|
|
109
|
+
<RcDropdownItem
|
|
110
|
+
v-for="item in items"
|
|
111
|
+
:key="item.id"
|
|
112
|
+
@click="emit('select', item.id)"
|
|
113
|
+
>
|
|
114
|
+
{{ item.label }}
|
|
115
|
+
</RcDropdownItem>
|
|
116
|
+
<slot name="dropdownCollection" />
|
|
117
|
+
</template>
|
|
118
|
+
</RcDropdown>
|
|
119
|
+
</template>
|
|
120
|
+
|
|
121
|
+
<style lang="scss" scoped>
|
|
122
|
+
.rc-button-split {
|
|
123
|
+
display: inline-flex;
|
|
124
|
+
|
|
125
|
+
// Round only the outer left edge of the main button
|
|
126
|
+
:deep(.rc-button-split-action) {
|
|
127
|
+
border-top-right-radius: 0;
|
|
128
|
+
border-bottom-right-radius: 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Round only the outer right edge of the trigger button; narrow padding
|
|
132
|
+
:deep(button.rc-button-split-trigger) {
|
|
133
|
+
border-top-left-radius: 0;
|
|
134
|
+
border-bottom-left-radius: 0;
|
|
135
|
+
padding-left: 8px;
|
|
136
|
+
padding-right: 8px;
|
|
137
|
+
min-width: unset;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
:deep(button.btn-small.rc-button-split-trigger) {
|
|
141
|
+
padding-left: 4px;
|
|
142
|
+
padding-right: 4px;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Primary: semi-transparent right border as separator
|
|
146
|
+
:deep(.rc-button-split-trigger.variant-primary),
|
|
147
|
+
:deep(.rc-button-split-trigger.variant-secondary),
|
|
148
|
+
:deep(.rc-button-split-trigger.variant-tertiary) {
|
|
149
|
+
border-left: 1px solid rgba(255, 255, 255, 0.3);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Link/Ghost: subtle separator
|
|
153
|
+
:deep(.rc-button-split-action.variant-link),
|
|
154
|
+
:deep(.rc-button-split-action.variant-ghost) {
|
|
155
|
+
border-right: 1px solid var(--border);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
</style>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as RcButtonSplit } from './RcButtonSplit.vue';
|
|
@@ -107,7 +107,7 @@ createTestComponent() {
|
|
|
107
107
|
|
|
108
108
|
# Publish shell pkg (tag is needed as publish-shell is optimized to work with release-shell-pkg workflow)
|
|
109
109
|
echo "Publishing Shell package to local registry"
|
|
110
|
-
yarn install
|
|
110
|
+
yarn install --frozen-lockfile
|
|
111
111
|
export TAG="shell-pkg-v${SHELL_VERSION}"
|
|
112
112
|
${SHELL_DIR}/scripts/publish-shell.sh
|
|
113
113
|
|
|
@@ -139,7 +139,7 @@ if [ "${SKIP_STANDALONE}" == "false" ]; then
|
|
|
139
139
|
|
|
140
140
|
pushd test-app > /dev/null
|
|
141
141
|
|
|
142
|
-
yarn install
|
|
142
|
+
yarn install --frozen-lockfile
|
|
143
143
|
# this is the "same" as doing a yarn dev (in a build sense)
|
|
144
144
|
# it's to make sure the dev environment is running properly
|
|
145
145
|
FORCE_COLOR=true yarn build | cat
|
|
@@ -165,7 +165,7 @@ pushd $BASE_DIR
|
|
|
165
165
|
# Now try a plugin within the dashboard codebase
|
|
166
166
|
echo "Validating in-tree package"
|
|
167
167
|
|
|
168
|
-
yarn install
|
|
168
|
+
yarn install --frozen-lockfile
|
|
169
169
|
|
|
170
170
|
if [ "${TEST_PERSIST_BUILD}" != "true" ]; then
|
|
171
171
|
echo "Removing folder ./pkg/test-pkg"
|
|
@@ -202,7 +202,7 @@ function clone_repo_test_extension_build() {
|
|
|
202
202
|
pushd ${BASE_DIR}/$REPO_NAME
|
|
203
203
|
|
|
204
204
|
echo -e "\nInstalling dependencies for $REPO_NAME\n"
|
|
205
|
-
yarn install
|
|
205
|
+
yarn install --frozen-lockfile
|
|
206
206
|
|
|
207
207
|
# set registry to local verdaccio (to install new shell)
|
|
208
208
|
yarn config set registry ${VERDACCIO_NPM_REGISTRY}
|
package/types/shell/index.d.ts
CHANGED
|
@@ -2818,6 +2818,7 @@ export default class ClusterRepo {
|
|
|
2818
2818
|
get isSuseAppCollection(): any;
|
|
2819
2819
|
get typeDisplay(): "oci" | "SUSE AppCo" | "git" | "http" | "?";
|
|
2820
2820
|
get nameDisplay(): any;
|
|
2821
|
+
detailPageHeaderActionOverride(realMode: any): any;
|
|
2821
2822
|
get urlDisplay(): any;
|
|
2822
2823
|
get branchDisplay(): any;
|
|
2823
2824
|
get details(): ({
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { toContextKey, requireAsset, requireJson, _setContexts } from '@shell/utils/require-asset';
|
|
2
|
+
|
|
3
|
+
describe('fx: toContextKey', () => {
|
|
4
|
+
it.each([
|
|
5
|
+
['~shell/assets/images/providers/aws.svg', './images/providers/aws.svg'],
|
|
6
|
+
['@shell/assets/images/providers/aws.svg', './images/providers/aws.svg'],
|
|
7
|
+
['~shell/assets/brand/suse/metadata.json', './brand/suse/metadata.json'],
|
|
8
|
+
['@shell/assets/images/pl/dark/logo.svg', './images/pl/dark/logo.svg'],
|
|
9
|
+
])('should convert %s to %s', (input, expected) => {
|
|
10
|
+
expect(toContextKey(input)).toStrictEqual(expected);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should prepend ./ to paths without shell prefix', () => {
|
|
14
|
+
expect(toContextKey('images/providers/aws.svg')).toStrictEqual('./images/providers/aws.svg');
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('fx: requireAsset', () => {
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
_setContexts(null, null);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should return the resolved asset URL from the image context', () => {
|
|
24
|
+
const mockImgCtx = jest.fn().mockReturnValue('/static/images/aws.svg');
|
|
25
|
+
|
|
26
|
+
_setContexts(mockImgCtx, null);
|
|
27
|
+
|
|
28
|
+
const result = requireAsset('~shell/assets/images/providers/aws.svg');
|
|
29
|
+
|
|
30
|
+
expect(result).toStrictEqual('/static/images/aws.svg');
|
|
31
|
+
expect(mockImgCtx).toHaveBeenCalledWith('./images/providers/aws.svg');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should throw when the image context is not available', () => {
|
|
35
|
+
_setContexts(null, null);
|
|
36
|
+
|
|
37
|
+
expect(() => requireAsset('~shell/assets/images/foo.svg'))
|
|
38
|
+
.toThrow('Asset context not available for: ~shell/assets/images/foo.svg');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should propagate errors from the context function for missing assets', () => {
|
|
42
|
+
const mockImgCtx = jest.fn().mockImplementation(() => {
|
|
43
|
+
throw new Error('Cannot find module');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
_setContexts(mockImgCtx, null);
|
|
47
|
+
|
|
48
|
+
expect(() => requireAsset('~shell/assets/images/missing.svg'))
|
|
49
|
+
.toThrow('Cannot find module');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('fx: requireJson', () => {
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
_setContexts(null, null);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should return mod.default when available', () => {
|
|
59
|
+
const jsonData = { vendor: 'suse' };
|
|
60
|
+
const mockJsonCtx = jest.fn().mockReturnValue({ default: jsonData });
|
|
61
|
+
|
|
62
|
+
_setContexts(null, mockJsonCtx);
|
|
63
|
+
|
|
64
|
+
const result = requireJson('~shell/assets/brand/suse/metadata.json');
|
|
65
|
+
|
|
66
|
+
expect(result).toStrictEqual(jsonData);
|
|
67
|
+
expect(mockJsonCtx).toHaveBeenCalledWith('./brand/suse/metadata.json');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should return mod directly when no default export', () => {
|
|
71
|
+
const jsonData = { vendor: 'rancher' };
|
|
72
|
+
const mockJsonCtx = jest.fn().mockReturnValue(jsonData);
|
|
73
|
+
|
|
74
|
+
_setContexts(null, mockJsonCtx);
|
|
75
|
+
|
|
76
|
+
const result = requireJson('~shell/assets/brand/rancher/metadata.json');
|
|
77
|
+
|
|
78
|
+
expect(result).toStrictEqual(jsonData);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should throw when the JSON context is not available', () => {
|
|
82
|
+
_setContexts(null, null);
|
|
83
|
+
|
|
84
|
+
expect(() => requireJson('~shell/assets/brand/suse/metadata.json'))
|
|
85
|
+
.toThrow('JSON context not available for: ~shell/assets/brand/suse/metadata.json');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should propagate errors from the context function for missing files', () => {
|
|
89
|
+
const mockJsonCtx = jest.fn().mockImplementation(() => {
|
|
90
|
+
throw new Error('Cannot find module');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
_setContexts(null, mockJsonCtx);
|
|
94
|
+
|
|
95
|
+
expect(() => requireJson('~shell/assets/brand/missing/metadata.json'))
|
|
96
|
+
.toThrow('Cannot find module');
|
|
97
|
+
});
|
|
98
|
+
});
|
package/utils/async.ts
CHANGED
|
@@ -10,11 +10,7 @@ export const waitFor = (testFn: Function, msg = '', timeoutMs = 3000000, interva
|
|
|
10
10
|
gatedLog('Wait for', msg, 'timed out');
|
|
11
11
|
clearInterval(interval);
|
|
12
12
|
clearTimeout(timeout);
|
|
13
|
-
|
|
14
|
-
reject(new Error(`Failed waiting for: ${ msg }`));
|
|
15
|
-
} else {
|
|
16
|
-
throw new Error(`waitFor timed out after ${ timeoutMs / 1000 } seconds`);
|
|
17
|
-
}
|
|
13
|
+
reject(new Error(msg ? `Failed waiting for: ${ msg }` : `waitFor timed out after ${ timeoutMs / 1000 } seconds`));
|
|
18
14
|
}, timeoutMs);
|
|
19
15
|
const interval = setInterval(() => {
|
|
20
16
|
if ( testFn() ) {
|
package/utils/brand.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { requireJson } from '@shell/utils/require-asset';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Brand/Theme metadata
|
|
3
5
|
*/
|
|
@@ -21,7 +23,7 @@ export function getBrandMeta(brand: string): BrandMeta {
|
|
|
21
23
|
|
|
22
24
|
if (brand) {
|
|
23
25
|
try {
|
|
24
|
-
brandMeta =
|
|
26
|
+
brandMeta = requireJson(`~shell/assets/brand/${ brand }/metadata.json`) as BrandMeta;
|
|
25
27
|
} catch {}
|
|
26
28
|
}
|
|
27
29
|
|
package/utils/favicon.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SETTING } from '@shell/config/settings';
|
|
2
2
|
import { MANAGEMENT } from '@shell/config/types';
|
|
3
|
+
import { requireAsset } from '@shell/utils/require-asset';
|
|
3
4
|
|
|
4
5
|
let favIconSet = false;
|
|
5
6
|
|
|
@@ -16,11 +17,11 @@ export function setFavIcon(store) {
|
|
|
16
17
|
let brandImage;
|
|
17
18
|
|
|
18
19
|
if (brandSetting === 'suse') {
|
|
19
|
-
brandImage =
|
|
20
|
+
brandImage = requireAsset('~shell/assets/brand/suse/favicon.png');
|
|
20
21
|
} else if (brandSetting === 'csp') {
|
|
21
|
-
brandImage =
|
|
22
|
+
brandImage = requireAsset('~shell/assets/brand/csp/favicon.png');
|
|
22
23
|
} else if (brandSetting === 'harvester') {
|
|
23
|
-
brandImage =
|
|
24
|
+
brandImage = requireAsset('~shell/assets/brand/harvester/favicon.png');
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
link.href = res?.value || brandImage || defaultFavIcon;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility to replace dynamic require() calls for image/asset imports.
|
|
3
|
+
*
|
|
4
|
+
* Uses webpack's require.context to resolve assets at compile time,
|
|
5
|
+
* then provides lookup functions by path.
|
|
6
|
+
*
|
|
7
|
+
* When migrating to Vite, replace the require.context calls below with:
|
|
8
|
+
*
|
|
9
|
+
* const imgCtx = import.meta.glob(
|
|
10
|
+
* '@shell/assets/**\/*.{svg,png,jpg,jpeg,gif,ico,webp}',
|
|
11
|
+
* { eager: true, query: '?url', import: 'default' }
|
|
12
|
+
* );
|
|
13
|
+
*
|
|
14
|
+
* const jsonCtx = import.meta.glob(
|
|
15
|
+
* '@shell/assets/**\/*.json',
|
|
16
|
+
* { eager: true }
|
|
17
|
+
* );
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// --- Webpack: require.context type declarations ---
|
|
21
|
+
|
|
22
|
+
interface WebpackRequireContext {
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
24
|
+
(key: string): any;
|
|
25
|
+
keys(): string[];
|
|
26
|
+
resolve(key: string): string;
|
|
27
|
+
id: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// --- Webpack: require.context (compile-time transform) ---
|
|
31
|
+
|
|
32
|
+
let imgCtx: WebpackRequireContext | null = null;
|
|
33
|
+
let jsonCtx: WebpackRequireContext | null = null;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
// @ts-expect-error — require.context is a webpack compile-time transform, not visible to TypeScript
|
|
37
|
+
imgCtx = require.context('@shell/assets', true, /\.(svg|png|jpe?g|gif|ico|webp)$/);
|
|
38
|
+
} catch (e) {}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
// @ts-expect-error — require.context is a webpack compile-time transform, not visible to TypeScript
|
|
42
|
+
jsonCtx = require.context('@shell/assets', true, /\.json$/);
|
|
43
|
+
} catch (e) {}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Convert an asset path to a require.context key.
|
|
47
|
+
*
|
|
48
|
+
* Input: '~shell/assets/images/providers/aws.svg' or '@shell/assets/images/providers/aws.svg'
|
|
49
|
+
* Output: './images/providers/aws.svg'
|
|
50
|
+
*/
|
|
51
|
+
export function toContextKey(path: string): string {
|
|
52
|
+
return `./${ path.replace(/^[~@]shell\/assets\//, '') }`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Look up an image asset URL by path, similar to Webpack's require() for images.
|
|
57
|
+
*
|
|
58
|
+
* Accepts paths with ~shell/ or @shell/ prefix.
|
|
59
|
+
* Throws if the asset is not found (matching the original require() behavior),
|
|
60
|
+
* so callers can use try/catch for fallback logic.
|
|
61
|
+
*/
|
|
62
|
+
export function requireAsset(path: string): string {
|
|
63
|
+
if (!imgCtx) {
|
|
64
|
+
// Throw to match original require() behavior — callers rely on try/catch for fallback logic
|
|
65
|
+
throw new Error(`Asset context not available for: ${ path }`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const key = toContextKey(path);
|
|
69
|
+
|
|
70
|
+
return imgCtx(key);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Load a JSON file from @shell/assets.
|
|
75
|
+
*
|
|
76
|
+
* Throws if the JSON file is not found (matching the original require() behavior),
|
|
77
|
+
* so callers can use try/catch for fallback logic.
|
|
78
|
+
*/
|
|
79
|
+
export function requireJson(path: string): object {
|
|
80
|
+
if (!jsonCtx) {
|
|
81
|
+
// Throw to match original require() behavior — callers rely on try/catch for fallback logic
|
|
82
|
+
throw new Error(`JSON context not available for: ${ path }`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const key = toContextKey(path);
|
|
86
|
+
const mod = jsonCtx(key);
|
|
87
|
+
|
|
88
|
+
return mod.default || mod;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Exported for testing — allows injecting mock contexts
|
|
92
|
+
export function _setContexts(img: WebpackRequireContext | null, json: WebpackRequireContext | null): void {
|
|
93
|
+
imgCtx = img;
|
|
94
|
+
jsonCtx = json;
|
|
95
|
+
}
|
package/vue.config.js
CHANGED
|
@@ -143,7 +143,7 @@ const instrumentCode = (config) => {
|
|
|
143
143
|
}
|
|
144
144
|
};
|
|
145
145
|
|
|
146
|
-
const getLoaders = (SHELL_ABS) => [
|
|
146
|
+
const getLoaders = (SHELL_ABS, dir) => [
|
|
147
147
|
// no fallback for pre-2013 browsers https://caniuse.com/webworkers
|
|
148
148
|
{
|
|
149
149
|
test: /web-worker.[a-z-]+.js/i,
|
|
@@ -199,7 +199,8 @@ const getLoaders = (SHELL_ABS) => [
|
|
|
199
199
|
appendTsxSuffixTo: [
|
|
200
200
|
'\\.vue$'
|
|
201
201
|
],
|
|
202
|
-
configFile:
|
|
202
|
+
configFile: path.join(SHELL_ABS, 'tsconfig.json'),
|
|
203
|
+
compilerOptions: { rootDir: dir }
|
|
203
204
|
}
|
|
204
205
|
}
|
|
205
206
|
]
|
|
@@ -580,7 +581,7 @@ module.exports = function(dir, appConfig = {}) {
|
|
|
580
581
|
|
|
581
582
|
config.resolve.symlinks = false;
|
|
582
583
|
processShellFiles(config, SHELL_ABS);
|
|
583
|
-
config.module.rules.push(...getLoaders(SHELL_ABS));
|
|
584
|
+
config.module.rules.push(...getLoaders(SHELL_ABS, dir));
|
|
584
585
|
instrumentCode(config);
|
|
585
586
|
preserveWhitespace(config);
|
|
586
587
|
},
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
<script>
|
|
2
|
-
import { mapGetters } from 'vuex';
|
|
3
|
-
import semver from 'semver';
|
|
4
|
-
|
|
5
|
-
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
6
|
-
import { _CREATE } from '@shell/config/query-params';
|
|
7
|
-
import { get } from '@shell/utils/object';
|
|
8
|
-
import { HCI as HCI_LABELS_ANNOTATIONS } from '@shell/config/labels-annotations';
|
|
9
|
-
import { SERVICE } from '@shell/config/types';
|
|
10
|
-
import { allHash } from '@shell/utils/promise';
|
|
11
|
-
|
|
12
|
-
const HARVESTER_ADD_ON_CONFIG = [{
|
|
13
|
-
variableName: 'ipam',
|
|
14
|
-
key: HCI_LABELS_ANNOTATIONS.CLOUD_PROVIDER_IPAM,
|
|
15
|
-
default: 'dhcp'
|
|
16
|
-
}, {
|
|
17
|
-
variableName: 'sharedService',
|
|
18
|
-
key: HCI_LABELS_ANNOTATIONS.PRIMARY_SERVICE,
|
|
19
|
-
default: ''
|
|
20
|
-
}];
|
|
21
|
-
|
|
22
|
-
export default {
|
|
23
|
-
components: { LabeledSelect },
|
|
24
|
-
|
|
25
|
-
props: {
|
|
26
|
-
mode: {
|
|
27
|
-
type: String,
|
|
28
|
-
default: _CREATE,
|
|
29
|
-
},
|
|
30
|
-
|
|
31
|
-
value: {
|
|
32
|
-
type: Object,
|
|
33
|
-
default: () => {
|
|
34
|
-
return {};
|
|
35
|
-
}
|
|
36
|
-
},
|
|
37
|
-
|
|
38
|
-
registerBeforeHook: {
|
|
39
|
-
type: Function,
|
|
40
|
-
default: null
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
|
|
44
|
-
created() {
|
|
45
|
-
if (this.registerBeforeHook) {
|
|
46
|
-
this.registerBeforeHook(this.willSave, 'harvesterWillSave');
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
|
|
50
|
-
async fetch() {
|
|
51
|
-
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
52
|
-
|
|
53
|
-
const hash = {
|
|
54
|
-
rke2Versions: this.$store.dispatch('management/request', { url: '/v1-rke2-release/releases' }),
|
|
55
|
-
services: this.$store.dispatch(`${ inStore }/findAll`, { type: SERVICE }),
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const res = await allHash(hash);
|
|
59
|
-
|
|
60
|
-
this.rke2Versions = res.rke2Versions;
|
|
61
|
-
},
|
|
62
|
-
|
|
63
|
-
data() {
|
|
64
|
-
const harvesterAddOnConfig = {};
|
|
65
|
-
|
|
66
|
-
HARVESTER_ADD_ON_CONFIG.forEach((c) => {
|
|
67
|
-
harvesterAddOnConfig[c.variableName] = this.value.metadata.annotations[c.key] || c.default;
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
let showShareIP;
|
|
71
|
-
|
|
72
|
-
if (this.value.metadata.annotations[HCI_LABELS_ANNOTATIONS.PRIMARY_SERVICE]) {
|
|
73
|
-
showShareIP = true;
|
|
74
|
-
} else {
|
|
75
|
-
showShareIP = false;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return {
|
|
79
|
-
...harvesterAddOnConfig,
|
|
80
|
-
showShareIP,
|
|
81
|
-
rke2Versions: {},
|
|
82
|
-
};
|
|
83
|
-
},
|
|
84
|
-
|
|
85
|
-
computed: {
|
|
86
|
-
...mapGetters(['allowedNamespaces', 'namespaces', 'currentCluster']),
|
|
87
|
-
|
|
88
|
-
ipamOptions() {
|
|
89
|
-
return [{
|
|
90
|
-
label: 'DHCP',
|
|
91
|
-
value: 'dhcp',
|
|
92
|
-
}, {
|
|
93
|
-
label: 'Pool',
|
|
94
|
-
value: 'pool',
|
|
95
|
-
}];
|
|
96
|
-
},
|
|
97
|
-
|
|
98
|
-
portOptions() {
|
|
99
|
-
const ports = this.value?.spec?.ports || [];
|
|
100
|
-
|
|
101
|
-
return ports.filter((p) => p.port && p.protocol === 'TCP').map((p) => p.port) || [];
|
|
102
|
-
},
|
|
103
|
-
|
|
104
|
-
serviceOptions() {
|
|
105
|
-
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
106
|
-
const services = this.$store.getters[`${ inStore }/all`](SERVICE);
|
|
107
|
-
|
|
108
|
-
const namespaces = this.namespaces();
|
|
109
|
-
|
|
110
|
-
const out = services.filter((s) => {
|
|
111
|
-
const ingress = s?.status?.loadBalancer?.ingress || [];
|
|
112
|
-
|
|
113
|
-
return ingress.length > 0 &&
|
|
114
|
-
!s?.metadata?.annotations?.['cloudprovider.harvesterhci.io/primary-service'] &&
|
|
115
|
-
s.spec?.type === 'LoadBalancer' &&
|
|
116
|
-
namespaces[s.metadata.namespace];
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
return out.map((s) => s.id);
|
|
120
|
-
},
|
|
121
|
-
|
|
122
|
-
shareIPEnabled() {
|
|
123
|
-
const kubernetesVersion = this.currentCluster.kubernetesVersion || '';
|
|
124
|
-
const kubernetesVersionExtension = this.currentCluster.kubernetesVersionExtension;
|
|
125
|
-
|
|
126
|
-
if (kubernetesVersionExtension.startsWith('+rke2')) {
|
|
127
|
-
const charts = ((this.rke2Versions?.data || []).find((v) => v.id === kubernetesVersion) || {}).charts;
|
|
128
|
-
let ccmVersion = charts?.['harvester-cloud-provider']?.version || '';
|
|
129
|
-
|
|
130
|
-
if (ccmVersion.endsWith('00')) {
|
|
131
|
-
ccmVersion = ccmVersion.slice(0, -2);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return semver.satisfies(ccmVersion, '>=0.2.0');
|
|
135
|
-
} else {
|
|
136
|
-
return true;
|
|
137
|
-
}
|
|
138
|
-
},
|
|
139
|
-
},
|
|
140
|
-
|
|
141
|
-
methods: {
|
|
142
|
-
willSave() {
|
|
143
|
-
const errors = [];
|
|
144
|
-
|
|
145
|
-
if (this.showShareIP) {
|
|
146
|
-
if (!this.sharedService) {
|
|
147
|
-
errors.push(this.t('validation.required', { key: this.t('servicesPage.harvester.shareIP.label') }, true));
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (errors.length > 0) {
|
|
152
|
-
return Promise.reject(errors);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
HARVESTER_ADD_ON_CONFIG.forEach((c) => {
|
|
156
|
-
this.value.metadata.annotations[c.key] = String(get(this, c.variableName));
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
if (this.showShareIP) {
|
|
160
|
-
delete this.value.metadata.annotations[HCI_LABELS_ANNOTATIONS.CLOUD_PROVIDER_IPAM];
|
|
161
|
-
} else {
|
|
162
|
-
delete this.value.metadata.annotations[HCI_LABELS_ANNOTATIONS.PRIMARY_SERVICE];
|
|
163
|
-
}
|
|
164
|
-
},
|
|
165
|
-
|
|
166
|
-
toggleShareIP() {
|
|
167
|
-
this.showShareIP = !this.showShareIP;
|
|
168
|
-
},
|
|
169
|
-
},
|
|
170
|
-
};
|
|
171
|
-
</script>
|
|
172
|
-
|
|
173
|
-
<template>
|
|
174
|
-
<div>
|
|
175
|
-
<div class="row mt-30">
|
|
176
|
-
<div class="col span-6">
|
|
177
|
-
<LabeledSelect
|
|
178
|
-
v-if="showShareIP"
|
|
179
|
-
v-model:value="sharedService"
|
|
180
|
-
:mode="mode"
|
|
181
|
-
:options="serviceOptions"
|
|
182
|
-
:label="t('servicesPage.harvester.shareIP.label')"
|
|
183
|
-
:disabled="mode === 'edit'"
|
|
184
|
-
/>
|
|
185
|
-
<LabeledSelect
|
|
186
|
-
v-else
|
|
187
|
-
v-model:value="ipam"
|
|
188
|
-
:mode="mode"
|
|
189
|
-
:options="ipamOptions"
|
|
190
|
-
:label="t('servicesPage.harvester.ipam.label')"
|
|
191
|
-
:disabled="mode === 'edit'"
|
|
192
|
-
/>
|
|
193
|
-
<div
|
|
194
|
-
v-if="mode === 'create'"
|
|
195
|
-
class="mt-10"
|
|
196
|
-
>
|
|
197
|
-
<a
|
|
198
|
-
role="button"
|
|
199
|
-
@click="toggleShareIP"
|
|
200
|
-
>
|
|
201
|
-
{{ showShareIP ? t('servicesPage.harvester.useIpam.label') : t('servicesPage.harvester.useShareIP.label') }}
|
|
202
|
-
</a>
|
|
203
|
-
</div>
|
|
204
|
-
</div>
|
|
205
|
-
</div>
|
|
206
|
-
</div>
|
|
207
|
-
</template>
|