@rancher/shell 3.0.12-rc.5 → 3.0.12-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/apis/intf/resources-api/resource-base.ts +42 -2
- package/apis/intf/resources-api/resource-instance.ts +101 -0
- package/apis/intf/resources-api/resources-api.ts +177 -23
- package/apis/intf/resources.ts +8 -6
- package/apis/intf/shell-api/slide-in.ts +46 -17
- package/apis/resources/__tests__/resources-api-class.test.ts +309 -28
- package/apis/resources/resources-api-class.ts +232 -43
- package/apis/shell/__tests__/slide-in.test.ts +83 -2
- package/apis/shell/slide-in.ts +20 -0
- package/assets/images/providers/traefik.png +0 -0
- package/assets/translations/en-us.yaml +10 -4
- package/chart/__tests__/rancher-monitoring-dashboards-index.test.ts +269 -0
- package/chart/rancher-monitoring-dashboards/index.vue +155 -0
- package/cloud-credential/__tests__/generic.test.ts +132 -0
- package/cloud-credential/generic.vue +33 -3
- package/components/EtcdInfoBanner.vue +2 -12
- package/components/ExplorerProjectsNamespaces.vue +2 -2
- package/components/GrafanaDashboard.vue +2 -8
- package/components/Resource/Detail/Masthead/__tests__/index.test.ts +68 -14
- package/components/Resource/Detail/Masthead/index.vue +8 -3
- package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/identifying-fields.test.ts +9 -0
- package/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields.ts +5 -1
- package/components/SlideInPanelManager.vue +103 -25
- package/components/__tests__/SlideInPanelManager.spec.ts +153 -10
- package/components/auth/AuthBanner.vue +1 -1
- package/components/form/Security.vue +1 -1
- package/components/formatter/Translate.vue +22 -1
- package/components/nav/Group.vue +5 -10
- package/components/nav/TopLevelMenu.vue +1 -30
- package/components/nav/__tests__/Group.test.ts +61 -0
- package/config/labels-annotations.js +1 -0
- package/config/table-headers.js +5 -2
- package/core/__tests__/plugin-products-apply.test.ts +787 -0
- package/core/__tests__/plugin-products-extend.test.ts +189 -0
- package/core/__tests__/plugin-products-helpers.test.ts +81 -78
- package/core/__tests__/plugin-products-new.test.ts +389 -0
- package/core/__tests__/plugin-products-scenarios.test.ts +980 -0
- package/core/__tests__/plugin-products-side-menu.test.ts +1935 -0
- package/core/plugin-products-base.ts +201 -93
- package/core/plugin-products-extending.ts +1 -1
- package/core/plugin-products-external.ts +380 -0
- package/core/plugin-products-helpers.ts +28 -24
- package/core/plugin-products-internal.ts +207 -0
- package/core/plugin-products-top-level.ts +10 -5
- package/core/plugin-products-type-guards.ts +17 -6
- package/core/plugin-products.ts +5 -7
- package/core/plugin-types.ts +1 -389
- package/core/plugin.ts +10 -11
- package/core/plugins-loader.js +2 -0
- package/core/types.ts +6 -202
- package/detail/__tests__/provisioning.cattle.io.cluster.test.ts +52 -0
- package/detail/pod.vue +1 -1
- package/detail/provisioning.cattle.io.cluster.vue +0 -2
- package/edit/auth/AuthProviderWarningBanners.vue +12 -0
- package/edit/auth/__tests__/AuthProviderWarningBanners.test.ts +10 -1
- package/edit/auth/__tests__/azuread.test.ts +1 -0
- package/edit/auth/__tests__/github.test.ts +1 -0
- package/edit/auth/__tests__/oidc.test.ts +1 -0
- package/edit/auth/__tests__/saml.test.ts +1 -0
- package/edit/auth/ldap/__tests__/index.test.ts +1 -0
- package/edit/provisioning.cattle.io.cluster/index.vue +1 -1
- package/edit/provisioning.cattle.io.cluster/rke2.vue +1 -1
- package/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue +2 -2
- package/list/management.cattle.io.user.vue +24 -5
- package/list/utils/management.cattle.io.cluster.utils.ts +1 -1
- package/machine-config/__tests__/generic.test.ts +163 -0
- package/machine-config/components/EC2Networking.vue +31 -7
- package/machine-config/components/__tests__/EC2Networking.test.ts +38 -15
- package/machine-config/generic.vue +32 -5
- package/mixins/resource-fetch.js +1 -1
- package/models/__tests__/namespace.test.ts +133 -8
- package/models/__tests__/provisioning.cattle.io.cluster.test.ts +57 -1
- package/models/__tests__/rke-machine.cattle.io.vmwarevspheremachinetemplate.test.ts +22 -0
- package/models/namespace.js +34 -2
- package/models/pod.js +12 -10
- package/models/provisioning.cattle.io.cluster.js +20 -13
- package/models/rke-machine.cattle.io.vmwarevspheremachinetemplate.js +9 -0
- package/package.json +1 -1
- package/pages/auth/login.vue +1 -1
- package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +110 -0
- package/pages/c/_cluster/apps/charts/install.vue +29 -0
- package/pages/c/_cluster/auth/config/index.vue +11 -4
- package/pages/c/_cluster/explorer/workload-dashboard/__tests__/composable.test.ts +136 -1
- package/pages/c/_cluster/explorer/workload-dashboard/composable.ts +69 -1
- package/pages/c/_cluster/fleet/index.vue +5 -9
- package/pages/c/_cluster/monitoring/index.vue +6 -1
- package/pkg/vue.config.js +4 -3
- package/plugins/codemirror.js +2 -0
- package/plugins/dashboard-store/resource-class.js +13 -9
- package/plugins/steve/__tests__/steve-class-resource-api.test.ts +147 -0
- package/plugins/steve/steve-class.js +43 -0
- package/plugins/steve/subscribe.js +11 -0
- package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +115 -0
- package/rancher-components/Form/LabeledInput/LabeledInput.vue +83 -3
- package/store/__tests__/action-menu.test.ts +622 -0
- package/store/__tests__/auth.test.ts +18 -0
- package/store/__tests__/notifications.test.ts +466 -1
- package/store/__tests__/slideInPanel.test.ts +143 -43
- package/store/__tests__/ui-context.test.ts +255 -0
- package/store/__tests__/wm.test.ts +503 -0
- package/store/auth.js +11 -1
- package/store/aws.js +19 -4
- package/store/features.js +1 -0
- package/store/plugins.js +6 -0
- package/store/slideInPanel.ts +15 -3
- package/types/rancher/steve.api.ts +2 -2
- package/types/shell/index.d.ts +84 -2
- package/types/store/dashboard-store.types.ts +2 -2
- package/types/store/type-map.ts +262 -1
- package/utils/__tests__/fleet-appco.test.ts +23 -0
- package/utils/async.ts +2 -0
- package/utils/fleet-appco.ts +6 -1
- package/utils/grafana.js +2 -17
- package/utils/monitoring.js +38 -1
- package/utils/sort.js +1 -1
- package/core/__tests__/plugin-products.test.ts +0 -4694
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { mount } from '@vue/test-utils';
|
|
2
2
|
import Masthead from '@shell/components/Resource/Detail/Masthead/index.vue';
|
|
3
|
+
import TitleBar from '@shell/components/Resource/Detail/TitleBar/index.vue';
|
|
4
|
+
import Metadata from '@shell/components/Resource/Detail/Metadata/index.vue';
|
|
3
5
|
import Cards from '@shell/components/Resource/Detail/Cards.vue';
|
|
4
6
|
|
|
5
7
|
jest.mock('@shell/utils/clipboard', () => ({ copyTextToClipboard: jest.fn() }));
|
|
@@ -19,16 +21,18 @@ describe('component: Masthead/index', () => {
|
|
|
19
21
|
metadataProps: { items: [] }
|
|
20
22
|
};
|
|
21
23
|
|
|
24
|
+
const globalStubs = {
|
|
25
|
+
stubs: {
|
|
26
|
+
TitleBar: true,
|
|
27
|
+
Metadata: true,
|
|
28
|
+
Cards: true
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
22
32
|
it('should render the Cards component', () => {
|
|
23
33
|
const wrapper = mount(Masthead, {
|
|
24
34
|
props: defaultProps,
|
|
25
|
-
global:
|
|
26
|
-
stubs: {
|
|
27
|
-
TitleBar: true,
|
|
28
|
-
Metadata: true,
|
|
29
|
-
Cards: true
|
|
30
|
-
}
|
|
31
|
-
}
|
|
35
|
+
global: globalStubs
|
|
32
36
|
});
|
|
33
37
|
|
|
34
38
|
expect(wrapper.findComponent(Cards).exists()).toBe(true);
|
|
@@ -37,17 +41,67 @@ describe('component: Masthead/index', () => {
|
|
|
37
41
|
it('should pass the resource from titleBarProps to Cards', () => {
|
|
38
42
|
const wrapper = mount(Masthead, {
|
|
39
43
|
props: defaultProps,
|
|
40
|
-
global:
|
|
41
|
-
stubs: {
|
|
42
|
-
TitleBar: true,
|
|
43
|
-
Metadata: true,
|
|
44
|
-
Cards: true
|
|
45
|
-
}
|
|
46
|
-
}
|
|
44
|
+
global: globalStubs
|
|
47
45
|
});
|
|
48
46
|
|
|
49
47
|
const cardsComponent = wrapper.findComponent(Cards);
|
|
50
48
|
|
|
51
49
|
expect(cardsComponent.props('resource')).toStrictEqual(mockResource);
|
|
52
50
|
});
|
|
51
|
+
|
|
52
|
+
it('should render TitleBar when titleBarProps is provided', () => {
|
|
53
|
+
const wrapper = mount(Masthead, {
|
|
54
|
+
props: defaultProps,
|
|
55
|
+
global: globalStubs
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(wrapper.findComponent(TitleBar).exists()).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should render Metadata when metadataProps is provided', () => {
|
|
62
|
+
const wrapper = mount(Masthead, {
|
|
63
|
+
props: defaultProps,
|
|
64
|
+
global: globalStubs
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(wrapper.findComponent(Metadata).exists()).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should not render TitleBar when titleBarProps is undefined', () => {
|
|
71
|
+
const wrapper = mount(Masthead, {
|
|
72
|
+
props: { titleBarProps: undefined, metadataProps: { items: [] } },
|
|
73
|
+
global: globalStubs
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(wrapper.findComponent(TitleBar).exists()).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should not render Metadata when metadataProps is undefined', () => {
|
|
80
|
+
const wrapper = mount(Masthead, {
|
|
81
|
+
props: { titleBarProps: defaultProps.titleBarProps, metadataProps: undefined },
|
|
82
|
+
global: globalStubs
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(wrapper.findComponent(Metadata).exists()).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should not render Cards when titleBarProps is undefined', () => {
|
|
89
|
+
const wrapper = mount(Masthead, {
|
|
90
|
+
props: { titleBarProps: undefined, metadataProps: undefined },
|
|
91
|
+
global: globalStubs
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
expect(wrapper.findComponent(Cards).exists()).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should not render any children when both props are undefined', () => {
|
|
98
|
+
const wrapper = mount(Masthead, {
|
|
99
|
+
props: { titleBarProps: undefined, metadataProps: undefined },
|
|
100
|
+
global: globalStubs
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(wrapper.findComponent(TitleBar).exists()).toBe(false);
|
|
104
|
+
expect(wrapper.findComponent(Metadata).exists()).toBe(false);
|
|
105
|
+
expect(wrapper.findComponent(Cards).exists()).toBe(false);
|
|
106
|
+
});
|
|
53
107
|
});
|
|
@@ -4,8 +4,8 @@ import Metadata, { MetadataProps } from '@shell/components/Resource/Detail/Metad
|
|
|
4
4
|
import Cards from '@shell/components/Resource/Detail/Cards.vue';
|
|
5
5
|
|
|
6
6
|
export interface MastheadProps {
|
|
7
|
-
titleBarProps
|
|
8
|
-
metadataProps
|
|
7
|
+
titleBarProps?: TitleBarProps;
|
|
8
|
+
metadataProps?: MetadataProps;
|
|
9
9
|
}
|
|
10
10
|
</script>
|
|
11
11
|
|
|
@@ -15,7 +15,10 @@ const props = defineProps<MastheadProps>();
|
|
|
15
15
|
</script>
|
|
16
16
|
<template>
|
|
17
17
|
<div class="masthead">
|
|
18
|
-
<TitleBar
|
|
18
|
+
<TitleBar
|
|
19
|
+
v-if="props.titleBarProps"
|
|
20
|
+
v-bind="props.titleBarProps"
|
|
21
|
+
>
|
|
19
22
|
<template
|
|
20
23
|
v-if="$slots['additional-actions']"
|
|
21
24
|
#additional-actions
|
|
@@ -24,10 +27,12 @@ const props = defineProps<MastheadProps>();
|
|
|
24
27
|
</template>
|
|
25
28
|
</TitleBar>
|
|
26
29
|
<Metadata
|
|
30
|
+
v-if="props.metadataProps"
|
|
27
31
|
class="metadata-section"
|
|
28
32
|
v-bind="props.metadataProps"
|
|
29
33
|
/>
|
|
30
34
|
<Cards
|
|
35
|
+
v-if="props.titleBarProps"
|
|
31
36
|
class="cards-section"
|
|
32
37
|
:resource="props.titleBarProps.resource"
|
|
33
38
|
/>
|
|
@@ -59,6 +59,15 @@ describe('composables: IdentifyingFields', () => {
|
|
|
59
59
|
expect(result?.value.label).toStrictEqual('component.resource.detail.metadata.identifyingInformation.namespace');
|
|
60
60
|
expect(result?.value.valueDataTestid).toStrictEqual('masthead-subheader-namespace');
|
|
61
61
|
});
|
|
62
|
+
|
|
63
|
+
it('should return a plain text namespace row when the resource signals namespaceLocation is unreachable', () => {
|
|
64
|
+
mockStore.getters['cluster/canList'] = () => true;
|
|
65
|
+
const resource = { namespace: 'NAMESPACE', namespaceLocation: null };
|
|
66
|
+
const result = useNamespace(resource);
|
|
67
|
+
|
|
68
|
+
expect(result?.value.valueOverride).toBeUndefined();
|
|
69
|
+
expect(result?.value.value).toStrictEqual(resource.namespace);
|
|
70
|
+
});
|
|
62
71
|
});
|
|
63
72
|
|
|
64
73
|
describe('useWorkspace', () => {
|
|
@@ -27,10 +27,14 @@ export const useNamespace = (resource: any): ComputedRef<Row> | undefined => {
|
|
|
27
27
|
const currentStore = store.getters['currentStore'](NAMESPACE);
|
|
28
28
|
const canList = store.getters[`${ currentStore }/canList`](NAMESPACE);
|
|
29
29
|
|
|
30
|
+
// A resource that explicitly returns null from namespaceLocation is signaling that
|
|
31
|
+
// the namespace lives in a cluster the user can't reach (mirrors legacy.vue's hideNamespaceLocation)
|
|
32
|
+
const hasReachableLocation = resourceValue.namespaceLocation !== null;
|
|
33
|
+
|
|
30
34
|
const label = i18n.t('component.resource.detail.metadata.identifyingInformation.namespace');
|
|
31
35
|
const value = resourceValue.namespace;
|
|
32
36
|
const valueDataTestid = 'masthead-subheader-namespace';
|
|
33
|
-
const valueOverride = canList ? {
|
|
37
|
+
const valueOverride = canList && hasReachableLocation ? {
|
|
34
38
|
component: markRaw(defineAsyncComponent(() => import('@shell/components/Resource/Detail/ResourcePopover/index.vue'))),
|
|
35
39
|
props: {
|
|
36
40
|
type: NAMESPACE,
|
|
@@ -10,6 +10,13 @@ import { useRouter } from 'vue-router';
|
|
|
10
10
|
|
|
11
11
|
const HEADER_HEIGHT = 55;
|
|
12
12
|
|
|
13
|
+
const WIDTH_MAP = {
|
|
14
|
+
default: '33%',
|
|
15
|
+
wide: '73%'
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const HEIGHT_FULL = 'full';
|
|
19
|
+
|
|
13
20
|
const slideInPanelManager = useTemplateRef('SlideInPanelManager');
|
|
14
21
|
const slideInPanelManagerClose = useTemplateRef('SlideInPanelManagerClose');
|
|
15
22
|
|
|
@@ -19,12 +26,22 @@ const isClosing = computed(() => store.getters['slideInPanel/isClosing']);
|
|
|
19
26
|
const currentComponent = computed(() => store.getters['slideInPanel/component']);
|
|
20
27
|
const currentProps = computed(() => store.getters['slideInPanel/componentProps']);
|
|
21
28
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
const resolvedHeightMode = computed(() => {
|
|
30
|
+
if (currentProps.value?.height) {
|
|
31
|
+
return currentProps.value.height;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Deprecated: infer from raw top value
|
|
35
|
+
if (currentProps.value?.top === '0') {
|
|
36
|
+
return 'full';
|
|
26
37
|
}
|
|
27
38
|
|
|
39
|
+
return 'default';
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const isFullHeight = computed(() => resolvedHeightMode.value === HEIGHT_FULL);
|
|
43
|
+
|
|
44
|
+
const defaultTop = computed(() => {
|
|
28
45
|
const banner = document.getElementById('banner-header');
|
|
29
46
|
let height = HEADER_HEIGHT;
|
|
30
47
|
|
|
@@ -35,15 +52,51 @@ const panelTop = computed(() => {
|
|
|
35
52
|
return `${ height }px`;
|
|
36
53
|
});
|
|
37
54
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
55
|
+
const panelTop = computed(() => {
|
|
56
|
+
if (isFullHeight.value) {
|
|
57
|
+
return '0';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Deprecated: explicit top value
|
|
61
|
+
if (currentProps.value?.top) {
|
|
62
|
+
return currentProps.value.top;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return defaultTop.value;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const panelHeight = computed(() => {
|
|
69
|
+
if (isFullHeight.value) {
|
|
70
|
+
return '100vh';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return `calc(100vh - ${ panelTop.value })`;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const panelWidth = computed(() => {
|
|
77
|
+
const width = currentProps.value?.width as keyof typeof WIDTH_MAP | undefined;
|
|
78
|
+
|
|
79
|
+
return (width && WIDTH_MAP[width]) || WIDTH_MAP.default;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const panelRight = computed(() => (isOpen.value ? '0' : `-${ panelWidth.value }`));
|
|
83
|
+
|
|
84
|
+
const glassZIndex = computed(() => (isFullHeight.value ? 101 : undefined));
|
|
85
|
+
const panelZIndex = computed(() => (isFullHeight.value ? 102 : undefined));
|
|
86
|
+
|
|
87
|
+
const showHeader = computed(() => {
|
|
88
|
+
// Deprecated: explicit showHeader takes precedence for backwards compat
|
|
89
|
+
if (currentProps.value?.showHeader !== undefined) {
|
|
90
|
+
return currentProps.value.showHeader;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return !!currentProps.value?.title;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const panelTitle = computed(() => currentProps.value?.title || (showHeader.value ? 'Details' : ''));
|
|
42
97
|
|
|
43
|
-
const showHeader = computed(() => currentProps?.value?.showHeader ?? true);
|
|
44
|
-
const panelTitle = showHeader.value ? computed(() => currentProps?.value?.title || 'Details') : null;
|
|
45
98
|
const closeOnRouteChange = computed(() => {
|
|
46
|
-
const propsCloseOnRouteChange = currentProps?.
|
|
99
|
+
const propsCloseOnRouteChange = currentProps.value?.closeOnRouteChange;
|
|
47
100
|
|
|
48
101
|
if (!propsCloseOnRouteChange) {
|
|
49
102
|
return ['name', 'params', 'hash', 'query'];
|
|
@@ -51,27 +104,32 @@ const closeOnRouteChange = computed(() => {
|
|
|
51
104
|
|
|
52
105
|
return propsCloseOnRouteChange;
|
|
53
106
|
});
|
|
107
|
+
|
|
54
108
|
const router = useRouter();
|
|
55
109
|
|
|
56
110
|
watch(
|
|
57
111
|
/**
|
|
58
|
-
*
|
|
112
|
+
* Focus trap logic
|
|
59
113
|
*/
|
|
60
|
-
() => isOpen
|
|
114
|
+
() => isOpen.value,
|
|
61
115
|
(neu, old) => {
|
|
62
116
|
if (neu && neu !== old) {
|
|
63
|
-
|
|
117
|
+
if (currentProps.value?.disableFocusTrap) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const panelEl = slideInPanelManager.value as HTMLElement;
|
|
122
|
+
const closeEl = slideInPanelManagerClose.value;
|
|
123
|
+
|
|
124
|
+
const opts: any = {
|
|
64
125
|
...DEFAULT_FOCUS_TRAP_OPTS,
|
|
65
|
-
|
|
66
|
-
|
|
126
|
+
initialFocus: closeEl || panelEl,
|
|
127
|
+
fallbackFocus: panelEl
|
|
67
128
|
};
|
|
68
129
|
|
|
69
|
-
const returnFocusSelector = currentProps
|
|
130
|
+
const returnFocusSelector = currentProps.value?.returnFocusSelector;
|
|
70
131
|
|
|
71
132
|
if (returnFocusSelector) {
|
|
72
|
-
/**
|
|
73
|
-
* will return focus to the first iterable node of this container select
|
|
74
|
-
*/
|
|
75
133
|
opts.setReturnFocus = () => {
|
|
76
134
|
if (returnFocusSelector && !document.querySelector(returnFocusSelector)) {
|
|
77
135
|
console.warn('SlideInPanelManager: cannot find elem with "returnFocusSelector", returning focus to main view'); // eslint-disable-line no-console
|
|
@@ -85,13 +143,13 @@ watch(
|
|
|
85
143
|
|
|
86
144
|
useWatcherBasedSetupFocusTrapWithDestroyIncluded(
|
|
87
145
|
() => {
|
|
88
|
-
if (currentProps
|
|
146
|
+
if (currentProps.value?.focusTrapWatcherBasedVariable) {
|
|
89
147
|
return currentProps.value.focusTrapWatcherBasedVariable;
|
|
90
148
|
}
|
|
91
149
|
|
|
92
|
-
return isOpen
|
|
150
|
+
return isOpen.value && !isClosing.value;
|
|
93
151
|
},
|
|
94
|
-
|
|
152
|
+
panelEl,
|
|
95
153
|
opts,
|
|
96
154
|
false
|
|
97
155
|
);
|
|
@@ -102,7 +160,7 @@ watch(
|
|
|
102
160
|
watch(
|
|
103
161
|
() => router?.currentRoute?.value,
|
|
104
162
|
(newValue, oldValue) => {
|
|
105
|
-
if (!isOpen
|
|
163
|
+
if (!isOpen.value) {
|
|
106
164
|
return;
|
|
107
165
|
}
|
|
108
166
|
|
|
@@ -137,6 +195,7 @@ function closePanel() {
|
|
|
137
195
|
<div
|
|
138
196
|
id="slide-in-panel-manager"
|
|
139
197
|
ref="SlideInPanelManager"
|
|
198
|
+
tabindex="-1"
|
|
140
199
|
@keydown.escape="closePanel"
|
|
141
200
|
>
|
|
142
201
|
<div
|
|
@@ -144,6 +203,7 @@ function closePanel() {
|
|
|
144
203
|
data-testid="slide-in-glass"
|
|
145
204
|
class="slide-in-glass"
|
|
146
205
|
:class="{ 'slide-in-glass-open': isOpen }"
|
|
206
|
+
:style="{ zIndex: glassZIndex }"
|
|
147
207
|
@click="closePanel"
|
|
148
208
|
/>
|
|
149
209
|
<aside
|
|
@@ -154,6 +214,7 @@ function closePanel() {
|
|
|
154
214
|
right: panelRight,
|
|
155
215
|
top: panelTop,
|
|
156
216
|
height: panelHeight,
|
|
217
|
+
zIndex: panelZIndex,
|
|
157
218
|
}"
|
|
158
219
|
>
|
|
159
220
|
<div
|
|
@@ -167,6 +228,8 @@ function closePanel() {
|
|
|
167
228
|
ref="SlideInPanelManagerClose"
|
|
168
229
|
class="icon icon-close"
|
|
169
230
|
data-testid="slide-in-close"
|
|
231
|
+
role="button"
|
|
232
|
+
:aria-label="t('generic.close')"
|
|
170
233
|
:tabindex="isOpen ? 0 : -1"
|
|
171
234
|
@click="closePanel"
|
|
172
235
|
@keypress.enter="closePanel"
|
|
@@ -220,7 +283,7 @@ function closePanel() {
|
|
|
220
283
|
.header {
|
|
221
284
|
display: flex;
|
|
222
285
|
align-items: center;
|
|
223
|
-
padding: 4px;
|
|
286
|
+
padding: 4px 10px;
|
|
224
287
|
border-bottom: 1px solid var(--border);
|
|
225
288
|
|
|
226
289
|
.title {
|
|
@@ -229,11 +292,26 @@ function closePanel() {
|
|
|
229
292
|
}
|
|
230
293
|
|
|
231
294
|
.icon-close {
|
|
295
|
+
padding: 8px;
|
|
296
|
+
border-radius: 4px;
|
|
297
|
+
opacity: 0.7;
|
|
232
298
|
cursor: pointer;
|
|
299
|
+
|
|
300
|
+
&:hover {
|
|
301
|
+
background-color: var(--primary);
|
|
302
|
+
color: var(--primary-text);
|
|
303
|
+
opacity: 1;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
&:focus-visible {
|
|
307
|
+
@include focus-outline;
|
|
308
|
+
outline-offset: 2px;
|
|
309
|
+
}
|
|
233
310
|
}
|
|
234
311
|
}
|
|
235
312
|
|
|
236
313
|
.main-panel {
|
|
314
|
+
flex: 1;
|
|
237
315
|
padding: 10px;
|
|
238
316
|
overflow: auto;
|
|
239
317
|
}
|
|
@@ -5,7 +5,7 @@ import SlideInPanelManager from '@shell/components/SlideInPanelManager.vue';
|
|
|
5
5
|
|
|
6
6
|
const MockComponent = {
|
|
7
7
|
template: '<div data-testid="slide-in-panel-component">Mock Panel Content</div>',
|
|
8
|
-
props: ['
|
|
8
|
+
props: ['title', 'extraProp']
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
describe('slideInPanelManager.vue with Teleport', () => {
|
|
@@ -23,7 +23,7 @@ describe('slideInPanelManager.vue with Teleport', () => {
|
|
|
23
23
|
'slideInPanel/isOpen': () => true,
|
|
24
24
|
'slideInPanel/component': () => MockComponent,
|
|
25
25
|
'slideInPanel/componentProps': () => ({
|
|
26
|
-
width: '
|
|
26
|
+
width: 'default', title: 'Test Title', extraProp: 'extra'
|
|
27
27
|
})
|
|
28
28
|
};
|
|
29
29
|
|
|
@@ -61,15 +61,14 @@ describe('slideInPanelManager.vue with Teleport', () => {
|
|
|
61
61
|
|
|
62
62
|
const styleAttr = slidePanel.getAttribute('style') || '';
|
|
63
63
|
|
|
64
|
-
expect(styleAttr).toContain('width:
|
|
64
|
+
expect(styleAttr).toContain('width: 33%');
|
|
65
65
|
expect(styleAttr).toContain('top: 55px');
|
|
66
66
|
expect(styleAttr).toContain('height: calc(100vh - 55px)');
|
|
67
67
|
expect(styleAttr).toContain('right: 0');
|
|
68
68
|
});
|
|
69
69
|
|
|
70
|
-
it('
|
|
71
|
-
|
|
72
|
-
getters['slideInPanel/componentProps'] = () => ({ width: '40%' });
|
|
70
|
+
it('hides header when no title is provided', async() => {
|
|
71
|
+
getters['slideInPanel/componentProps'] = () => ({});
|
|
73
72
|
store = createStore({
|
|
74
73
|
getters,
|
|
75
74
|
mutations: { 'slideInPanel/close': jest.fn() }
|
|
@@ -77,9 +76,9 @@ describe('slideInPanelManager.vue with Teleport', () => {
|
|
|
77
76
|
factory();
|
|
78
77
|
await nextTick();
|
|
79
78
|
|
|
80
|
-
const
|
|
79
|
+
const header = document.querySelector('#slides #slide-in-panel-manager .header') as HTMLElement;
|
|
81
80
|
|
|
82
|
-
expect(
|
|
81
|
+
expect(header).toBeNull();
|
|
83
82
|
});
|
|
84
83
|
|
|
85
84
|
it('computes panelTop correctly when a banner exists', async() => {
|
|
@@ -122,8 +121,8 @@ describe('slideInPanelManager.vue with Teleport', () => {
|
|
|
122
121
|
const slidePanel = document.querySelector('#slides .slide-in') as HTMLElement;
|
|
123
122
|
const styleAttr = slidePanel.getAttribute('style') || '';
|
|
124
123
|
|
|
125
|
-
// With
|
|
126
|
-
expect(styleAttr).toContain('right: -
|
|
124
|
+
// With default width preset (33%), panelRight should be "-33%" when closed.
|
|
125
|
+
expect(styleAttr).toContain('right: -33%');
|
|
127
126
|
});
|
|
128
127
|
|
|
129
128
|
it('calls store commit when clicking on the slide-in glass overlay', async() => {
|
|
@@ -163,4 +162,148 @@ describe('slideInPanelManager.vue with Teleport', () => {
|
|
|
163
162
|
|
|
164
163
|
expect(closeMutation).toHaveBeenCalledWith({}, undefined);
|
|
165
164
|
});
|
|
165
|
+
|
|
166
|
+
describe('width preset', () => {
|
|
167
|
+
it('resolves width "wide" to 73%', async() => {
|
|
168
|
+
getters['slideInPanel/componentProps'] = () => ({ width: 'wide', title: 'Test' });
|
|
169
|
+
store = createStore({
|
|
170
|
+
getters,
|
|
171
|
+
mutations: { 'slideInPanel/close': jest.fn() }
|
|
172
|
+
});
|
|
173
|
+
factory();
|
|
174
|
+
await nextTick();
|
|
175
|
+
|
|
176
|
+
const slidePanel = document.querySelector('#slides .slide-in') as HTMLElement;
|
|
177
|
+
const styleAttr = slidePanel.getAttribute('style') || '';
|
|
178
|
+
|
|
179
|
+
expect(styleAttr).toContain('width: 73%');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('defaults to 33% when no width config is provided', async() => {
|
|
183
|
+
getters['slideInPanel/componentProps'] = () => ({ title: 'Test' });
|
|
184
|
+
store = createStore({
|
|
185
|
+
getters,
|
|
186
|
+
mutations: { 'slideInPanel/close': jest.fn() }
|
|
187
|
+
});
|
|
188
|
+
factory();
|
|
189
|
+
await nextTick();
|
|
190
|
+
|
|
191
|
+
const slidePanel = document.querySelector('#slides .slide-in') as HTMLElement;
|
|
192
|
+
const styleAttr = slidePanel.getAttribute('style') || '';
|
|
193
|
+
|
|
194
|
+
expect(styleAttr).toContain('width: 33%');
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('height preset', () => {
|
|
199
|
+
it('resolves height "full" to 100vh with top 0', async() => {
|
|
200
|
+
getters['slideInPanel/componentProps'] = () => ({ height: 'full', title: 'Test' });
|
|
201
|
+
store = createStore({
|
|
202
|
+
getters,
|
|
203
|
+
mutations: { 'slideInPanel/close': jest.fn() }
|
|
204
|
+
});
|
|
205
|
+
factory();
|
|
206
|
+
await nextTick();
|
|
207
|
+
|
|
208
|
+
const slidePanel = document.querySelector('#slides .slide-in') as HTMLElement;
|
|
209
|
+
const styleAttr = slidePanel.getAttribute('style') || '';
|
|
210
|
+
|
|
211
|
+
expect(styleAttr).toContain('height: 100vh');
|
|
212
|
+
expect(styleAttr).toContain('top: 0');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('applies elevated z-index when panelHeight is "full"', async() => {
|
|
216
|
+
getters['slideInPanel/componentProps'] = () => ({ height: 'full', title: 'Test' });
|
|
217
|
+
store = createStore({
|
|
218
|
+
getters,
|
|
219
|
+
mutations: { 'slideInPanel/close': jest.fn() }
|
|
220
|
+
});
|
|
221
|
+
factory();
|
|
222
|
+
await nextTick();
|
|
223
|
+
|
|
224
|
+
const slidePanel = document.querySelector('#slides .slide-in') as HTMLElement;
|
|
225
|
+
const styleAttr = slidePanel.getAttribute('style') || '';
|
|
226
|
+
|
|
227
|
+
expect(styleAttr).toContain('z-index: 102');
|
|
228
|
+
|
|
229
|
+
const slideGlass = document.querySelector('[data-testid="slide-in-glass"]') as HTMLElement;
|
|
230
|
+
const glassStyle = slideGlass.getAttribute('style') || '';
|
|
231
|
+
|
|
232
|
+
expect(glassStyle).toContain('z-index: 101');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('does not apply elevated z-index for default panelHeight', async() => {
|
|
236
|
+
getters['slideInPanel/componentProps'] = () => ({ title: 'Test' });
|
|
237
|
+
store = createStore({
|
|
238
|
+
getters,
|
|
239
|
+
mutations: { 'slideInPanel/close': jest.fn() }
|
|
240
|
+
});
|
|
241
|
+
factory();
|
|
242
|
+
await nextTick();
|
|
243
|
+
|
|
244
|
+
const slidePanel = document.querySelector('#slides .slide-in') as HTMLElement;
|
|
245
|
+
const styleAttr = slidePanel.getAttribute('style') || '';
|
|
246
|
+
|
|
247
|
+
expect(styleAttr).not.toContain('z-index: 102');
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe('showHeader inferred from title', () => {
|
|
252
|
+
it('shows header when title is provided', async() => {
|
|
253
|
+
getters['slideInPanel/componentProps'] = () => ({ title: 'My Panel' });
|
|
254
|
+
store = createStore({
|
|
255
|
+
getters,
|
|
256
|
+
mutations: { 'slideInPanel/close': jest.fn() }
|
|
257
|
+
});
|
|
258
|
+
factory();
|
|
259
|
+
await nextTick();
|
|
260
|
+
|
|
261
|
+
const header = document.querySelector('#slides .slide-in .header') as HTMLElement;
|
|
262
|
+
|
|
263
|
+
expect(header).toBeTruthy();
|
|
264
|
+
expect(header.textContent?.trim()).toContain('My Panel');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('hides header when no title is provided', async() => {
|
|
268
|
+
getters['slideInPanel/componentProps'] = () => ({});
|
|
269
|
+
store = createStore({
|
|
270
|
+
getters,
|
|
271
|
+
mutations: { 'slideInPanel/close': jest.fn() }
|
|
272
|
+
});
|
|
273
|
+
factory();
|
|
274
|
+
await nextTick();
|
|
275
|
+
|
|
276
|
+
const header = document.querySelector('#slides .slide-in .header') as HTMLElement;
|
|
277
|
+
|
|
278
|
+
expect(header).toBeNull();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('respects deprecated showHeader=true even without title', async() => {
|
|
282
|
+
getters['slideInPanel/componentProps'] = () => ({ showHeader: true });
|
|
283
|
+
store = createStore({
|
|
284
|
+
getters,
|
|
285
|
+
mutations: { 'slideInPanel/close': jest.fn() }
|
|
286
|
+
});
|
|
287
|
+
factory();
|
|
288
|
+
await nextTick();
|
|
289
|
+
|
|
290
|
+
const header = document.querySelector('#slides .slide-in .header') as HTMLElement;
|
|
291
|
+
|
|
292
|
+
expect(header).toBeTruthy();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('respects deprecated showHeader=false even with title', async() => {
|
|
296
|
+
getters['slideInPanel/componentProps'] = () => ({ showHeader: false, title: 'Should Not Show' });
|
|
297
|
+
store = createStore({
|
|
298
|
+
getters,
|
|
299
|
+
mutations: { 'slideInPanel/close': jest.fn() }
|
|
300
|
+
});
|
|
301
|
+
factory();
|
|
302
|
+
await nextTick();
|
|
303
|
+
|
|
304
|
+
const header = document.querySelector('#slides .slide-in .header') as HTMLElement;
|
|
305
|
+
|
|
306
|
+
expect(header).toBeNull();
|
|
307
|
+
});
|
|
308
|
+
});
|
|
166
309
|
});
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script>
|
|
2
|
+
import { get } from '@shell/utils/object';
|
|
3
|
+
|
|
2
4
|
export default {
|
|
3
5
|
props: {
|
|
4
6
|
row: {
|
|
@@ -6,6 +8,11 @@ export default {
|
|
|
6
8
|
required: true,
|
|
7
9
|
},
|
|
8
10
|
|
|
11
|
+
fallbackPath: {
|
|
12
|
+
type: String,
|
|
13
|
+
default: '',
|
|
14
|
+
},
|
|
15
|
+
|
|
9
16
|
prefix: {
|
|
10
17
|
type: String,
|
|
11
18
|
default: '',
|
|
@@ -18,8 +25,22 @@ export default {
|
|
|
18
25
|
},
|
|
19
26
|
|
|
20
27
|
computed: {
|
|
28
|
+
l10nId() {
|
|
29
|
+
return `${ this.prefix ? `${ this.prefix }.` : '' }${ this.value || this.row.id }`;
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
l10nFallback() {
|
|
33
|
+
const fallback = this.value;
|
|
34
|
+
|
|
35
|
+
if (this.fallbackPath) {
|
|
36
|
+
return get(this.row, this.fallbackPath) || fallback;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return fallback;
|
|
40
|
+
},
|
|
41
|
+
|
|
21
42
|
text() {
|
|
22
|
-
return this.$store.getters['i18n/withFallback'](
|
|
43
|
+
return this.$store.getters['i18n/withFallback'](this.l10nId, null, this.l10nFallback);
|
|
23
44
|
},
|
|
24
45
|
},
|
|
25
46
|
};
|