@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.
Files changed (64) hide show
  1. package/assets/translations/en-us.yaml +7 -5
  2. package/chart/__tests__/rancher-backup-index.test.ts +248 -0
  3. package/chart/rancher-backup/index.vue +41 -2
  4. package/components/BrandImage.vue +6 -5
  5. package/components/ConsumptionGauge.vue +12 -4
  6. package/components/DynamicContent/DynamicContentIcon.vue +3 -2
  7. package/components/ExplorerProjectsNamespaces.vue +1 -4
  8. package/components/LazyImage.vue +2 -1
  9. package/components/Resource/Detail/Card/Scaler.vue +4 -4
  10. package/components/Tabbed/index.vue +6 -0
  11. package/components/__tests__/ConsumptionGauge.test.ts +31 -0
  12. package/components/form/ProjectMemberEditor.vue +0 -10
  13. package/components/nav/TopLevelMenu.helper.ts +7 -79
  14. package/components/nav/__tests__/TopLevelMenu.helper.test.ts +2 -53
  15. package/config/private-label.js +2 -1
  16. package/config/product/apps.js +1 -0
  17. package/core/__tests__/extension-manager-impl.test.js +187 -2
  18. package/core/extension-manager-impl.js +4 -2
  19. package/core/plugin-helpers.ts +31 -0
  20. package/detail/__tests__/node.test.ts +83 -0
  21. package/detail/management.cattle.io.oidcclient.vue +2 -1
  22. package/detail/node.vue +1 -0
  23. package/edit/catalog.cattle.io.clusterrepo.vue +17 -3
  24. package/edit/cloudcredential.vue +2 -1
  25. package/edit/monitoring.coreos.com.alertmanagerconfig/receiverConfig.vue +11 -6
  26. package/edit/provisioning.cattle.io.cluster/index.vue +5 -4
  27. package/edit/provisioning.cattle.io.cluster/shared.ts +4 -2
  28. package/edit/secret/generic.vue +1 -0
  29. package/edit/secret/index.vue +2 -1
  30. package/edit/service.vue +2 -14
  31. package/list/management.cattle.io.feature.vue +7 -1
  32. package/list/provisioning.cattle.io.cluster.vue +0 -49
  33. package/mixins/brand.js +2 -1
  34. package/models/catalog.cattle.io.clusterrepo.js +9 -0
  35. package/models/cluster.x-k8s.io.machinedeployment.js +8 -3
  36. package/models/management.cattle.io.authconfig.js +2 -1
  37. package/models/management.cattle.io.cluster.js +4 -3
  38. package/models/monitoring.coreos.com.receiver.js +11 -6
  39. package/models/provisioning.cattle.io.cluster.js +2 -2
  40. package/package.json +5 -5
  41. package/pages/c/_cluster/apps/charts/index.vue +3 -8
  42. package/pages/c/_cluster/apps/charts/install.vue +8 -9
  43. package/pages/c/_cluster/istio/index.vue +4 -2
  44. package/pages/c/_cluster/longhorn/index.vue +2 -1
  45. package/pages/c/_cluster/monitoring/index.vue +2 -2
  46. package/pages/c/_cluster/neuvector/index.vue +2 -1
  47. package/pages/c/_cluster/settings/performance.vue +0 -5
  48. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +2 -1
  49. package/pages/c/_cluster/uiplugins/index.vue +2 -1
  50. package/plugins/steve/steve-pagination-utils.ts +1 -2
  51. package/plugins/steve/subscribe.js +29 -4
  52. package/rancher-components/RcButton/RcButton.vue +3 -3
  53. package/rancher-components/RcButtonSplit/RcButtonSplit.test.ts +253 -0
  54. package/rancher-components/RcButtonSplit/RcButtonSplit.vue +158 -0
  55. package/rancher-components/RcButtonSplit/index.ts +1 -0
  56. package/scripts/test-plugins-build.sh +4 -4
  57. package/types/shell/index.d.ts +1 -0
  58. package/utils/__tests__/require-asset.test.ts +98 -0
  59. package/utils/async.ts +1 -5
  60. package/utils/brand.ts +3 -1
  61. package/utils/favicon.js +4 -3
  62. package/utils/require-asset.ts +95 -0
  63. package/vue.config.js +4 -3
  64. 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}
@@ -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
- if (msg) {
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 = require(`~shell/assets/brand/${ brand }/metadata.json`);
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 = require('~shell/assets/brand/suse/favicon.png');
20
+ brandImage = requireAsset('~shell/assets/brand/suse/favicon.png');
20
21
  } else if (brandSetting === 'csp') {
21
- brandImage = require('~shell/assets/brand/csp/favicon.png');
22
+ brandImage = requireAsset('~shell/assets/brand/csp/favicon.png');
22
23
  } else if (brandSetting === 'harvester') {
23
- brandImage = require('~shell/assets/brand/harvester/favicon.png');
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: path.join(SHELL_ABS, 'tsconfig.json')
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>