@rancher/shell 3.0.8-rc.6 → 3.0.8-rc.7
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/brand/suse/favicon.png +0 -0
- package/assets/images/content/README.md +5 -0
- package/assets/images/content/cloud-native.svg +84 -0
- package/assets/images/content/dark/cloud-native.svg +21 -0
- package/assets/images/content/dark/shield.svg +59 -0
- package/assets/images/content/dark/suse.svg +10 -0
- package/assets/images/content/shield.svg +59 -0
- package/assets/images/content/suse.svg +10 -0
- package/assets/styles/themes/_dark.scss +1 -1
- package/assets/styles/themes/_light.scss +1 -1
- package/assets/styles/themes/_modern.scss +2 -2
- package/assets/styles/themes/_suse.scss +4 -3
- package/assets/translations/en-us.yaml +6 -0
- package/components/BannerGraphic.vue +4 -4
- package/components/DynamicContent/DynamicContentBanner.vue +102 -0
- package/components/DynamicContent/DynamicContentCloseButton.vue +42 -0
- package/components/DynamicContent/DynamicContentIcon.vue +132 -0
- package/components/DynamicContent/DynamicContentPanel.vue +112 -0
- package/components/DynamicContent/content.ts +78 -0
- package/components/Resource/Detail/CopyToClipboard.vue +3 -0
- package/components/Resource/Detail/Metadata/KeyValueRow.vue +1 -1
- package/components/nav/Header.vue +1 -1
- package/components/nav/NamespaceFilter.vue +13 -1
- package/components/nav/NotificationCenter/index.vue +2 -1
- package/components/templates/plain.vue +30 -4
- package/core/plugin-helpers.ts +2 -0
- package/edit/auth/__tests__/oidc.test.ts +26 -0
- package/edit/auth/oidc.vue +5 -1
- package/edit/provisioning.cattle.io.cluster/__tests__/Networking.test.ts +24 -1
- package/edit/provisioning.cattle.io.cluster/index.vue +7 -3
- package/edit/provisioning.cattle.io.cluster/rke2.vue +7 -5
- package/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue +3 -2
- package/edit/service.vue +8 -4
- package/machine-config/amazonec2.vue +7 -1
- package/machine-config/components/EC2Networking.vue +32 -19
- package/machine-config/components/__tests__/EC2Networking.test.ts +30 -0
- package/models/provisioning.cattle.io.cluster.js +5 -1
- package/package.json +1 -1
- package/pages/c/_cluster/manager/hostedprovider/index.vue +5 -0
- package/pages/home.vue +9 -1
- package/rancher-components/RcDropdown/RcDropdownItem.vue +1 -0
- package/store/notifications.ts +21 -5
- package/types/notifications/index.ts +3 -0
- package/types/window-manager.ts +2 -0
- package/utils/dynamic-content/announcement.ts +71 -41
- package/utils/dynamic-content/types.d.ts +21 -1
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Icon component to render an icon from the icon data in an announcement
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useStore } from 'vuex';
|
|
7
|
+
import { computed } from 'vue';
|
|
8
|
+
|
|
9
|
+
import { AnnouncementNotificationIconData } from '@shell/utils/dynamic-content/types';
|
|
10
|
+
|
|
11
|
+
type KeyValues = {
|
|
12
|
+
[key: string]: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const ICON_FORMAT = /(@|#|>)/;
|
|
16
|
+
|
|
17
|
+
const props = defineProps<{icon: AnnouncementNotificationIconData}>();
|
|
18
|
+
const store = useStore();
|
|
19
|
+
const theme = computed(() => store.getters['prefs/theme']);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get the correct url to use for light/dark mode
|
|
23
|
+
*/
|
|
24
|
+
const url = computed(() => {
|
|
25
|
+
const darkTheme = theme.value === 'dark';
|
|
26
|
+
|
|
27
|
+
return darkTheme ? props.icon?.dark || props.icon?.light : props.icon?.light;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const iconName = computed(() => {
|
|
31
|
+
const decodedIcon = url.value.split(ICON_FORMAT);
|
|
32
|
+
|
|
33
|
+
if (decodedIcon[0].startsWith('!')) {
|
|
34
|
+
return `icon-${ decodedIcon[0].substring(1) }`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return undefined;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get the src value for an image tag
|
|
42
|
+
*/
|
|
43
|
+
const src = computed(() => {
|
|
44
|
+
const decodedIcon = url.value.split(ICON_FORMAT);
|
|
45
|
+
|
|
46
|
+
// If the icon name starts with ~, then it references a built-in icon/image
|
|
47
|
+
if (decodedIcon[0].startsWith('~')) {
|
|
48
|
+
const img = decodedIcon[0].substring(1);
|
|
49
|
+
const themePrefix = theme.value === 'dark' ? 'dark/' : '';
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
return require(`~shell/assets/images/content/${ themePrefix }${ img }`);
|
|
53
|
+
} catch {
|
|
54
|
+
return require(`~shell/assets/images/content/${ img }`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Regular URL, use it directly
|
|
59
|
+
return decodedIcon[0];
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Icon value can include some custom style information:
|
|
64
|
+
*
|
|
65
|
+
* '@wxh' (or @w) To change the width/height
|
|
66
|
+
* '>x' to set the padding to x px
|
|
67
|
+
* '<x' to set the margin to x px
|
|
68
|
+
* '#rrggbb' to set the color
|
|
69
|
+
*
|
|
70
|
+
*/
|
|
71
|
+
const style = computed(() => {
|
|
72
|
+
const decodedIcon = props.icon.light.split(ICON_FORMAT).slice(1);
|
|
73
|
+
const OPTIONS: { [key: string]: (v: string, result: KeyValues) => void } = {
|
|
74
|
+
'@': (v: string, result: KeyValues) => {
|
|
75
|
+
const wh = v.split('x');
|
|
76
|
+
|
|
77
|
+
result.width = `${ wh[0] }px`;
|
|
78
|
+
result.height = (wh.length === 2) ? `${ wh[1] }px` : `${ wh[0] }px`;
|
|
79
|
+
result.fontSize = result.width;
|
|
80
|
+
},
|
|
81
|
+
'#': (v: string, result: KeyValues) => {
|
|
82
|
+
result.color = `#${ v }`;
|
|
83
|
+
},
|
|
84
|
+
'>': (v: string, result: KeyValues) => {
|
|
85
|
+
result.padding = `${ v }px`;
|
|
86
|
+
},
|
|
87
|
+
'<': (v: string, result: KeyValues) => {
|
|
88
|
+
result.margin = `${ v }px`;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const pairs = Math.floor(decodedIcon.length / 2);
|
|
93
|
+
const result = {};
|
|
94
|
+
|
|
95
|
+
for (let i = 0; i < pairs; i++) {
|
|
96
|
+
const index = 2 * i;
|
|
97
|
+
|
|
98
|
+
if (OPTIONS[decodedIcon[index]]) {
|
|
99
|
+
const handler = OPTIONS[decodedIcon[index]];
|
|
100
|
+
const value = decodedIcon[index + 1];
|
|
101
|
+
|
|
102
|
+
if (handler) {
|
|
103
|
+
handler(value, result);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return result;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
</script>
|
|
112
|
+
<template>
|
|
113
|
+
<i
|
|
114
|
+
v-if="iconName"
|
|
115
|
+
class="icon"
|
|
116
|
+
:style="style"
|
|
117
|
+
:class="iconName"
|
|
118
|
+
/>
|
|
119
|
+
<img
|
|
120
|
+
v-else
|
|
121
|
+
:style="style"
|
|
122
|
+
:src="src"
|
|
123
|
+
class="dc-icon"
|
|
124
|
+
>
|
|
125
|
+
</template>
|
|
126
|
+
|
|
127
|
+
<style lang="scss" scoped>
|
|
128
|
+
.dc-icon {
|
|
129
|
+
width: 48px;
|
|
130
|
+
height: 48px;
|
|
131
|
+
}
|
|
132
|
+
</style>
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Component top render an announcement as a side panel
|
|
4
|
+
*/
|
|
5
|
+
import DynamicContentIcon from './DynamicContentIcon.vue';
|
|
6
|
+
import DynamicContentCloseButton from './DynamicContentCloseButton.vue';
|
|
7
|
+
import Markdown from '@shell/components/Markdown.vue';
|
|
8
|
+
import { useDynamicContent, DynamicInputProps } from './content';
|
|
9
|
+
|
|
10
|
+
const props = defineProps<DynamicInputProps>();
|
|
11
|
+
const {
|
|
12
|
+
dynamicContent,
|
|
13
|
+
invokeAction,
|
|
14
|
+
primaryButtonStyle,
|
|
15
|
+
} = useDynamicContent(props, 'rhs');
|
|
16
|
+
|
|
17
|
+
</script>
|
|
18
|
+
<template>
|
|
19
|
+
<div
|
|
20
|
+
v-if="dynamicContent"
|
|
21
|
+
:compact="true"
|
|
22
|
+
:can-close="true"
|
|
23
|
+
class="dc-side-panel mt-10"
|
|
24
|
+
>
|
|
25
|
+
<div class="dc-title-block">
|
|
26
|
+
<DynamicContentIcon
|
|
27
|
+
v-if="dynamicContent.data.icon"
|
|
28
|
+
:icon="dynamicContent.data.icon"
|
|
29
|
+
:class="{'mr-10': dynamicContent.data.icon }"
|
|
30
|
+
/>
|
|
31
|
+
<div class="dc-title">
|
|
32
|
+
{{ dynamicContent.title }}
|
|
33
|
+
</div>
|
|
34
|
+
<DynamicContentCloseButton
|
|
35
|
+
:id="dynamicContent.id"
|
|
36
|
+
class="dc-close-button"
|
|
37
|
+
/>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="dc-content">
|
|
40
|
+
<div class="dc-message">
|
|
41
|
+
<Markdown
|
|
42
|
+
v-if="dynamicContent.message"
|
|
43
|
+
v-model:value="dynamicContent.message"
|
|
44
|
+
/>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="dc-actions">
|
|
48
|
+
<button
|
|
49
|
+
v-if="dynamicContent.primaryAction"
|
|
50
|
+
role="button"
|
|
51
|
+
class="btn btn-sm"
|
|
52
|
+
:aria-label="t('dynamicContent.action.openPrimary')"
|
|
53
|
+
:class="primaryButtonStyle"
|
|
54
|
+
@click.stop.prevent="invokeAction(dynamicContent.primaryAction)"
|
|
55
|
+
>
|
|
56
|
+
{{ dynamicContent.primaryAction.label }}
|
|
57
|
+
</button>
|
|
58
|
+
<button
|
|
59
|
+
v-if="dynamicContent.secondaryAction"
|
|
60
|
+
role="button"
|
|
61
|
+
:aria-label="t('dynamicContent.action.openSecondary')"
|
|
62
|
+
class="btn btn-sm role-secondary"
|
|
63
|
+
@click.stop.prevent="invokeAction(dynamicContent.secondaryAction)"
|
|
64
|
+
>
|
|
65
|
+
{{ dynamicContent.secondaryAction.label }}
|
|
66
|
+
</button>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</template>
|
|
70
|
+
|
|
71
|
+
<style lang="scss" scoped>
|
|
72
|
+
$dc-padding: 8px;
|
|
73
|
+
|
|
74
|
+
.dc-side-panel {
|
|
75
|
+
border: 1px solid var(--border);
|
|
76
|
+
display: flex;
|
|
77
|
+
flex-direction: column;
|
|
78
|
+
|
|
79
|
+
.dc-title-block {
|
|
80
|
+
display: flex;
|
|
81
|
+
flex: 1;
|
|
82
|
+
align-items: center;
|
|
83
|
+
border-bottom: 1px solid var(--border);
|
|
84
|
+
padding: 0 $dc-padding;
|
|
85
|
+
|
|
86
|
+
.dc-title {
|
|
87
|
+
flex: 1;
|
|
88
|
+
font-weight: bold;
|
|
89
|
+
font-size: 14px;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.dc-content {
|
|
94
|
+
display: flex;
|
|
95
|
+
flex-direction: column;
|
|
96
|
+
flex: 1;
|
|
97
|
+
padding: $dc-padding;
|
|
98
|
+
|
|
99
|
+
.dc-message {
|
|
100
|
+
font-size: 1em;
|
|
101
|
+
line-height: 1.3em;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.dc-actions {
|
|
106
|
+
display: flex;
|
|
107
|
+
justify-content: flex-end;
|
|
108
|
+
padding: $dc-padding;
|
|
109
|
+
gap: 10px;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
</style>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composable to provide access to an announcement
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { computed, ComputedRef } from 'vue';
|
|
6
|
+
import { useStore } from 'vuex';
|
|
7
|
+
import { Notification, StoredNotification, NotificationAction } from '@shell/types/notifications';
|
|
8
|
+
import { useRouter } from 'vue-router';
|
|
9
|
+
|
|
10
|
+
export type Styles = { [key: string]: string };
|
|
11
|
+
|
|
12
|
+
export interface UseDynamicInput {
|
|
13
|
+
dynamicContent: ComputedRef<Notification | undefined>;
|
|
14
|
+
primaryButtonStyle: ComputedRef<string>;
|
|
15
|
+
styles: ComputedRef<Styles>;
|
|
16
|
+
invokeAction: (action: NotificationAction) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface DynamicInputProps {
|
|
20
|
+
location?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const useDynamicContent = (props: DynamicInputProps, defaultLocation: string): UseDynamicInput => {
|
|
24
|
+
const store = useStore();
|
|
25
|
+
const router = useRouter();
|
|
26
|
+
|
|
27
|
+
// Return the first un-read hidden notification for the given location
|
|
28
|
+
const dynamicContent = computed(() => {
|
|
29
|
+
const location = props.location || defaultLocation;
|
|
30
|
+
const hiddenUnreadNotificationsForLocation: Notification[] = store.getters['notifications/hidden'].filter((n: StoredNotification) => !n.read && n.data?.location === location);
|
|
31
|
+
|
|
32
|
+
return hiddenUnreadNotificationsForLocation.length > 0 ? hiddenUnreadNotificationsForLocation[0] : undefined;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const styles = computed(() => {
|
|
36
|
+
const parts = dynamicContent?.value?.data?.style?.trim().split(',') || [];
|
|
37
|
+
const res: Styles = {};
|
|
38
|
+
|
|
39
|
+
parts.forEach((part: string) => {
|
|
40
|
+
const kv = part.split(':');
|
|
41
|
+
|
|
42
|
+
if (kv.length === 2) {
|
|
43
|
+
res[kv[0].trim()] = kv[1].trim();
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return res;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const primaryButtonStyle = computed(() => {
|
|
51
|
+
const buttonStyle = styles.value.btn === 'link' ? 'tertiary' : styles.value.btn || 'primary';
|
|
52
|
+
|
|
53
|
+
return `role-${ buttonStyle }`;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Invoke action (typically from either the primary or secondary buttons of a notification)
|
|
57
|
+
// This can open a URL in a new tab OR navigate to an application route
|
|
58
|
+
const invokeAction = (action: NotificationAction) => {
|
|
59
|
+
if (action.target) {
|
|
60
|
+
window.open(action.target, '_blank');
|
|
61
|
+
} else if (action.route) {
|
|
62
|
+
try {
|
|
63
|
+
router.push(action.route);
|
|
64
|
+
} catch (e) {
|
|
65
|
+
console.error('Error navigating to route for the notification action', e); // eslint-disable-line no-console
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
console.error('Notification action must either specify a "target" or a "route"'); // eslint-disable-line no-console
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
dynamicContent,
|
|
74
|
+
invokeAction,
|
|
75
|
+
primaryButtonStyle,
|
|
76
|
+
styles
|
|
77
|
+
};
|
|
78
|
+
};
|
|
@@ -673,9 +673,16 @@ export default {
|
|
|
673
673
|
},
|
|
674
674
|
|
|
675
675
|
removeOption(ns, event) {
|
|
676
|
-
this.selectOption(ns);
|
|
677
676
|
event.preventDefault();
|
|
678
677
|
event.stopPropagation();
|
|
678
|
+
|
|
679
|
+
this.selectOption(ns);
|
|
680
|
+
|
|
681
|
+
if (event.type !== 'keydown' || this.value.length !== 0) {
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
this.$refs.namespaceFilterInput.focus();
|
|
679
686
|
},
|
|
680
687
|
|
|
681
688
|
defaultOption() {
|
|
@@ -782,6 +789,7 @@ export default {
|
|
|
782
789
|
ghost
|
|
783
790
|
class="ns-chip-button"
|
|
784
791
|
:data-testid="`namespaces-values-close-${j}`"
|
|
792
|
+
:aria-label="t('namespaceFilter.removeNamespace', { name: ns.label })"
|
|
785
793
|
@click="removeOption(ns, $event)"
|
|
786
794
|
@keydown.enter.space.stop="removeOption(ns, $event)"
|
|
787
795
|
@mousedown="handleValueMouseDown(ns, $event)"
|
|
@@ -951,6 +959,10 @@ export default {
|
|
|
951
959
|
display: inline-block;
|
|
952
960
|
border-radius: var(--border-radius);
|
|
953
961
|
|
|
962
|
+
&:focus, &.focused {
|
|
963
|
+
@include focus-outline;
|
|
964
|
+
}
|
|
965
|
+
|
|
954
966
|
.ns-glass {
|
|
955
967
|
top: 0;
|
|
956
968
|
bottom: 0;
|
|
@@ -13,7 +13,8 @@ import {
|
|
|
13
13
|
} from '@components/RcDropdown';
|
|
14
14
|
|
|
15
15
|
const store = useStore();
|
|
16
|
-
|
|
16
|
+
// We don't want any hidden notifications showing in the notification center (these are shown elsewhere, e.g. home page dynamic content announcements)
|
|
17
|
+
const allNotifications = computed(() => store.getters['notifications/visible']);
|
|
17
18
|
const unreadLevelClass = computed(() => {
|
|
18
19
|
return store.getters['notifications/unreadCount'] === 0 ? '' : 'unread';
|
|
19
20
|
});
|
|
@@ -15,6 +15,8 @@ import BrowserTabVisibility from '@shell/mixins/browser-tab-visibility';
|
|
|
15
15
|
import Inactivity from '@shell/components/Inactivity';
|
|
16
16
|
import { mapGetters } from 'vuex';
|
|
17
17
|
import PromptModal from '@shell/components/PromptModal';
|
|
18
|
+
import WindowManager from '@shell/components/nav/WindowManager';
|
|
19
|
+
import { Layout } from '@shell/types/window-manager';
|
|
18
20
|
|
|
19
21
|
export default {
|
|
20
22
|
|
|
@@ -30,7 +32,8 @@ export default {
|
|
|
30
32
|
SlideInPanelManager,
|
|
31
33
|
AwsComplianceBanner,
|
|
32
34
|
AzureWarning,
|
|
33
|
-
Inactivity
|
|
35
|
+
Inactivity,
|
|
36
|
+
WindowManager
|
|
34
37
|
},
|
|
35
38
|
|
|
36
39
|
mixins: [Brand, BrowserTabVisibility],
|
|
@@ -40,6 +43,7 @@ export default {
|
|
|
40
43
|
// Assume home pages have routes where the name is the key to use for string lookup
|
|
41
44
|
name: this.$route.name,
|
|
42
45
|
noLocaleShortcut: process.env.dev || false,
|
|
46
|
+
layout: Layout.plain,
|
|
43
47
|
};
|
|
44
48
|
},
|
|
45
49
|
|
|
@@ -97,6 +101,7 @@ export default {
|
|
|
97
101
|
@shortkey="toggleNoneLocale()"
|
|
98
102
|
/>
|
|
99
103
|
</main>
|
|
104
|
+
<WindowManager :layout="layout" />
|
|
100
105
|
</div>
|
|
101
106
|
|
|
102
107
|
<FixedBanner :footer="true" />
|
|
@@ -117,10 +122,10 @@ export default {
|
|
|
117
122
|
flex-grow: 1;
|
|
118
123
|
|
|
119
124
|
grid-template-areas:
|
|
120
|
-
"header"
|
|
121
|
-
"main";
|
|
125
|
+
"header header header"
|
|
126
|
+
"wm-vl main wm-vr";
|
|
122
127
|
|
|
123
|
-
grid-template-columns: auto;
|
|
128
|
+
grid-template-columns: var(--wm-vl-width, 0px) auto var(--wm-vr-width, 0px);
|
|
124
129
|
grid-template-rows: var(--header-height) auto;
|
|
125
130
|
|
|
126
131
|
> HEADER {
|
|
@@ -128,6 +133,27 @@ export default {
|
|
|
128
133
|
}
|
|
129
134
|
}
|
|
130
135
|
|
|
136
|
+
.wm {
|
|
137
|
+
grid-area: wm;
|
|
138
|
+
overflow-y: hidden;
|
|
139
|
+
z-index: z-index('windowsManager');
|
|
140
|
+
position: relative;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.wm-vr {
|
|
144
|
+
grid-area: wm-vr;
|
|
145
|
+
overflow-y: hidden;
|
|
146
|
+
z-index: z-index('windowsManager');
|
|
147
|
+
position: relative;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.wm-vl {
|
|
151
|
+
grid-area: wm-vl;
|
|
152
|
+
overflow-y: hidden;
|
|
153
|
+
z-index: z-index('windowsManager');
|
|
154
|
+
position: relative;
|
|
155
|
+
}
|
|
156
|
+
|
|
131
157
|
MAIN {
|
|
132
158
|
grid-area: main;
|
|
133
159
|
overflow: auto;
|
package/core/plugin-helpers.ts
CHANGED
|
@@ -221,5 +221,31 @@ describe('oidc.vue', () => {
|
|
|
221
221
|
expect(issuer).toBe('');
|
|
222
222
|
expect(endpoint).toBe('');
|
|
223
223
|
});
|
|
224
|
+
|
|
225
|
+
it('custom claims fields should not appear in UI if Amazon cognito', async() => {
|
|
226
|
+
wrapper.vm.model.id = 'cognito';
|
|
227
|
+
|
|
228
|
+
const nameClaim = wrapper.find('[data-testid="input-name-claim"]');
|
|
229
|
+
const groupsClaim = wrapper.find('[data-testid="input-groups-claim"]');
|
|
230
|
+
const emailClaim = wrapper.find('[data-testid="input-email-claim"]');
|
|
231
|
+
|
|
232
|
+
expect(nameClaim.exists()).toBe(false);
|
|
233
|
+
expect(groupsClaim.exists()).toBe(false);
|
|
234
|
+
expect(emailClaim.exists()).toBe(false);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('custom claims fields should appear in UI if genericoidc', async() => {
|
|
238
|
+
wrapper.vm.model.id = 'genericoidc';
|
|
239
|
+
wrapper.vm.addCustomClaims = true;
|
|
240
|
+
await nextTick();
|
|
241
|
+
|
|
242
|
+
const nameClaim = wrapper.find('[data-testid="input-name-claim"]');
|
|
243
|
+
const groupsClaim = wrapper.find('[data-testid="input-groups-claim"]');
|
|
244
|
+
const emailClaim = wrapper.find('[data-testid="input-email-claim"]');
|
|
245
|
+
|
|
246
|
+
expect(nameClaim.exists()).toBe(true);
|
|
247
|
+
expect(groupsClaim.exists()).toBe(true);
|
|
248
|
+
expect(emailClaim.exists()).toBe(true);
|
|
249
|
+
});
|
|
224
250
|
});
|
|
225
251
|
});
|
package/edit/auth/oidc.vue
CHANGED
|
@@ -442,12 +442,14 @@ export default {
|
|
|
442
442
|
</div>
|
|
443
443
|
</template>
|
|
444
444
|
|
|
445
|
-
|
|
445
|
+
<!-- Custom Claims -->
|
|
446
|
+
<template v-if="addCustomClaims && isGenericOidc">
|
|
446
447
|
<h4>{{ t('authConfig.oidc.customClaims.label') }}</h4>
|
|
447
448
|
<div class="row mb-20">
|
|
448
449
|
<div class="col span-6">
|
|
449
450
|
<LabeledInput
|
|
450
451
|
v-model:value="model.nameClaim"
|
|
452
|
+
data-testid="input-name-claim"
|
|
451
453
|
:label="t(`authConfig.oidc.customClaims.nameClaim.label`)"
|
|
452
454
|
:mode="mode"
|
|
453
455
|
/>
|
|
@@ -455,6 +457,7 @@ export default {
|
|
|
455
457
|
<div class="col span-6">
|
|
456
458
|
<LabeledInput
|
|
457
459
|
v-model:value="model.groupsClaim"
|
|
460
|
+
data-testid="input-groups-claim"
|
|
458
461
|
:label="t(`authConfig.oidc.customClaims.groupsClaim.label`)"
|
|
459
462
|
:mode="mode"
|
|
460
463
|
/>
|
|
@@ -464,6 +467,7 @@ export default {
|
|
|
464
467
|
<div class="col span-6">
|
|
465
468
|
<LabeledInput
|
|
466
469
|
v-model:value="model.emailClaim"
|
|
470
|
+
data-testid="input-email-claim"
|
|
467
471
|
:label="t(`authConfig.oidc.customClaims.emailClaim.label`)"
|
|
468
472
|
:mode="mode"
|
|
469
473
|
/>
|
|
@@ -47,7 +47,7 @@ describe('component: RKE2Networking', () => {
|
|
|
47
47
|
expect(dropdown.props('options')).toHaveLength(3);
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
it('should show an error when an ipv6 pool is present and the user selects the ipv4-only stack preference', async() => {
|
|
50
|
+
it('should show an error when an ipv6 pool is present and the user selects the ipv4-only stack preference when creating a new cluster', async() => {
|
|
51
51
|
const spec = { ...defaultSpec, rkeConfig: { ...defaultSpec.rkeConfig, networking: { stackPreference: 'ipv4' } } };
|
|
52
52
|
const wrapper = mount(Networking, {
|
|
53
53
|
propsData: {
|
|
@@ -129,4 +129,27 @@ describe('component: RKE2Networking', () => {
|
|
|
129
129
|
|
|
130
130
|
expect(banner.exists()).toBe(false);
|
|
131
131
|
});
|
|
132
|
+
|
|
133
|
+
it('should not automatically update stack preference or validate it when editing an existing cluster even if its set to ipv4 and the user appears to have ipv6 pools', async() => {
|
|
134
|
+
const spec = { ...defaultSpec, rkeConfig: { ...defaultSpec.rkeConfig, networking: { stackPreference: 'ipv4' } } };
|
|
135
|
+
const wrapper = mount(Networking, {
|
|
136
|
+
propsData: {
|
|
137
|
+
mode: 'edit',
|
|
138
|
+
value: { spec },
|
|
139
|
+
selectedVersion: { serverArgs: mockServerArgs },
|
|
140
|
+
hasSomeIpv6Pools: true,
|
|
141
|
+
},
|
|
142
|
+
global: {
|
|
143
|
+
mocks: {
|
|
144
|
+
...defaultMocks,
|
|
145
|
+
$store: { getters: defaultGetters },
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
await wrapper.vm.$nextTick();
|
|
151
|
+
|
|
152
|
+
expect(wrapper.emitted('validationChanged')?.[0]?.[0]).toBe(true);
|
|
153
|
+
expect(wrapper.emitted('stack-preference-changed')).toBeUndefined();
|
|
154
|
+
});
|
|
132
155
|
});
|
|
@@ -163,9 +163,8 @@ export default {
|
|
|
163
163
|
};
|
|
164
164
|
|
|
165
165
|
this.extensions = this.$extension.getProviders(context);
|
|
166
|
-
},
|
|
167
166
|
|
|
168
|
-
|
|
167
|
+
// At this point, we know we definitely have the mgmt cluster, so we can access `isImported` and `isLocal`
|
|
169
168
|
let subType = null;
|
|
170
169
|
|
|
171
170
|
subType = this.$route.query[SUB_TYPE] || null;
|
|
@@ -176,6 +175,11 @@ export default {
|
|
|
176
175
|
} else if (this.value.isLocal) {
|
|
177
176
|
subType = LOCAL;
|
|
178
177
|
}
|
|
178
|
+
|
|
179
|
+
this.subType = subType;
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
data() {
|
|
179
183
|
const rkeType = this.$route.query[RKE_TYPE] || null;
|
|
180
184
|
const chart = this.$route.query[CHART] || null;
|
|
181
185
|
const isImport = this.realMode === _IMPORT;
|
|
@@ -184,7 +188,7 @@ export default {
|
|
|
184
188
|
nodeDrivers: [],
|
|
185
189
|
kontainerDrivers: [],
|
|
186
190
|
extensions: [],
|
|
187
|
-
subType,
|
|
191
|
+
subType: null,
|
|
188
192
|
rkeType,
|
|
189
193
|
chart,
|
|
190
194
|
isImport,
|
|
@@ -990,15 +990,16 @@ export default {
|
|
|
990
990
|
},
|
|
991
991
|
|
|
992
992
|
hasSomeIpv6Pools(neu) {
|
|
993
|
-
|
|
993
|
+
if (this.isCreate && this.localValue.spec.rkeConfig.networking.stackPreference !== STACK_PREFS.IPV6) { // if stack pref is ipv6, the user has manually configured that and we shouldn't change it
|
|
994
|
+
if (neu) {
|
|
995
|
+
this.localValue.spec.rkeConfig.networking.stackPreference = STACK_PREFS.DUAL;
|
|
996
|
+
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
994
999
|
|
|
995
|
-
if (neu && (stackPreference === STACK_PREFS.IPV4 || !stackPreference)) { // localValue.spec.rkeConfig.networking is initialized in the beforeCreate hook
|
|
996
|
-
this.localValue.spec.rkeConfig.networking.stackPreference = STACK_PREFS.DUAL;
|
|
997
|
-
} else if (stackPreference === STACK_PREFS.DUAL) {
|
|
998
1000
|
this.localValue.spec.rkeConfig.networking.stackPreference = STACK_PREFS.IPV4;
|
|
999
1001
|
}
|
|
1000
1002
|
},
|
|
1001
|
-
|
|
1002
1003
|
},
|
|
1003
1004
|
|
|
1004
1005
|
created() {
|
|
@@ -2256,6 +2257,7 @@ export default {
|
|
|
2256
2257
|
handleTabChange(data) {
|
|
2257
2258
|
this.activeTab = data;
|
|
2258
2259
|
},
|
|
2260
|
+
|
|
2259
2261
|
}
|
|
2260
2262
|
};
|
|
2261
2263
|
</script>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
3
3
|
import { Banner } from '@components/Banner';
|
|
4
4
|
import { Checkbox } from '@components/Form/Checkbox';
|
|
5
|
-
import { _EDIT, _VIEW } from '@shell/config/query-params';
|
|
5
|
+
import { _EDIT, _VIEW, _CREATE } from '@shell/config/query-params';
|
|
6
6
|
import ArrayList from '@shell/components/form/ArrayList';
|
|
7
7
|
import ACE from '@shell/edit/provisioning.cattle.io.cluster/tabs/networking/ACE';
|
|
8
8
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
@@ -128,11 +128,12 @@ export default {
|
|
|
128
128
|
methods: {
|
|
129
129
|
// if ipv6 pools are detected, we enforce dual-stack or ipv6 stack prefs.
|
|
130
130
|
// If ipv6 pools are not detected we don't know for sure they aren't there so we don't validate the input
|
|
131
|
+
// also not validating the input when editing existing clusters to ensure we don't prevent editing clusters using dual-stack VPCs provisioned before the ipv6 feature was added
|
|
131
132
|
stackPreferenceValidator(val) {
|
|
132
133
|
const value = val?.value || val;
|
|
133
134
|
let isValid;
|
|
134
135
|
|
|
135
|
-
if (this.hasSomeIpv6Pools) {
|
|
136
|
+
if (this.hasSomeIpv6Pools && this.mode === _CREATE) {
|
|
136
137
|
isValid = value !== STACK_PREFS.IPV4;
|
|
137
138
|
|
|
138
139
|
return isValid ? null : this.t('cluster.rke2.stackPreference.errorNeedsIpv6');
|