@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.
Files changed (116) hide show
  1. package/apis/intf/resources-api/resource-base.ts +42 -2
  2. package/apis/intf/resources-api/resource-instance.ts +101 -0
  3. package/apis/intf/resources-api/resources-api.ts +177 -23
  4. package/apis/intf/resources.ts +8 -6
  5. package/apis/intf/shell-api/slide-in.ts +46 -17
  6. package/apis/resources/__tests__/resources-api-class.test.ts +309 -28
  7. package/apis/resources/resources-api-class.ts +232 -43
  8. package/apis/shell/__tests__/slide-in.test.ts +83 -2
  9. package/apis/shell/slide-in.ts +20 -0
  10. package/assets/images/providers/traefik.png +0 -0
  11. package/assets/translations/en-us.yaml +10 -4
  12. package/chart/__tests__/rancher-monitoring-dashboards-index.test.ts +269 -0
  13. package/chart/rancher-monitoring-dashboards/index.vue +155 -0
  14. package/cloud-credential/__tests__/generic.test.ts +132 -0
  15. package/cloud-credential/generic.vue +33 -3
  16. package/components/EtcdInfoBanner.vue +2 -12
  17. package/components/ExplorerProjectsNamespaces.vue +2 -2
  18. package/components/GrafanaDashboard.vue +2 -8
  19. package/components/Resource/Detail/Masthead/__tests__/index.test.ts +68 -14
  20. package/components/Resource/Detail/Masthead/index.vue +8 -3
  21. package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/identifying-fields.test.ts +9 -0
  22. package/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields.ts +5 -1
  23. package/components/SlideInPanelManager.vue +103 -25
  24. package/components/__tests__/SlideInPanelManager.spec.ts +153 -10
  25. package/components/auth/AuthBanner.vue +1 -1
  26. package/components/form/Security.vue +1 -1
  27. package/components/formatter/Translate.vue +22 -1
  28. package/components/nav/Group.vue +5 -10
  29. package/components/nav/TopLevelMenu.vue +1 -30
  30. package/components/nav/__tests__/Group.test.ts +61 -0
  31. package/config/labels-annotations.js +1 -0
  32. package/config/table-headers.js +5 -2
  33. package/core/__tests__/plugin-products-apply.test.ts +787 -0
  34. package/core/__tests__/plugin-products-extend.test.ts +189 -0
  35. package/core/__tests__/plugin-products-helpers.test.ts +81 -78
  36. package/core/__tests__/plugin-products-new.test.ts +389 -0
  37. package/core/__tests__/plugin-products-scenarios.test.ts +980 -0
  38. package/core/__tests__/plugin-products-side-menu.test.ts +1935 -0
  39. package/core/plugin-products-base.ts +201 -93
  40. package/core/plugin-products-extending.ts +1 -1
  41. package/core/plugin-products-external.ts +380 -0
  42. package/core/plugin-products-helpers.ts +28 -24
  43. package/core/plugin-products-internal.ts +207 -0
  44. package/core/plugin-products-top-level.ts +10 -5
  45. package/core/plugin-products-type-guards.ts +17 -6
  46. package/core/plugin-products.ts +5 -7
  47. package/core/plugin-types.ts +1 -389
  48. package/core/plugin.ts +10 -11
  49. package/core/plugins-loader.js +2 -0
  50. package/core/types.ts +6 -202
  51. package/detail/__tests__/provisioning.cattle.io.cluster.test.ts +52 -0
  52. package/detail/pod.vue +1 -1
  53. package/detail/provisioning.cattle.io.cluster.vue +0 -2
  54. package/edit/auth/AuthProviderWarningBanners.vue +12 -0
  55. package/edit/auth/__tests__/AuthProviderWarningBanners.test.ts +10 -1
  56. package/edit/auth/__tests__/azuread.test.ts +1 -0
  57. package/edit/auth/__tests__/github.test.ts +1 -0
  58. package/edit/auth/__tests__/oidc.test.ts +1 -0
  59. package/edit/auth/__tests__/saml.test.ts +1 -0
  60. package/edit/auth/ldap/__tests__/index.test.ts +1 -0
  61. package/edit/provisioning.cattle.io.cluster/index.vue +1 -1
  62. package/edit/provisioning.cattle.io.cluster/rke2.vue +1 -1
  63. package/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue +2 -2
  64. package/list/management.cattle.io.user.vue +24 -5
  65. package/list/utils/management.cattle.io.cluster.utils.ts +1 -1
  66. package/machine-config/__tests__/generic.test.ts +163 -0
  67. package/machine-config/components/EC2Networking.vue +31 -7
  68. package/machine-config/components/__tests__/EC2Networking.test.ts +38 -15
  69. package/machine-config/generic.vue +32 -5
  70. package/mixins/resource-fetch.js +1 -1
  71. package/models/__tests__/namespace.test.ts +133 -8
  72. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +57 -1
  73. package/models/__tests__/rke-machine.cattle.io.vmwarevspheremachinetemplate.test.ts +22 -0
  74. package/models/namespace.js +34 -2
  75. package/models/pod.js +12 -10
  76. package/models/provisioning.cattle.io.cluster.js +20 -13
  77. package/models/rke-machine.cattle.io.vmwarevspheremachinetemplate.js +9 -0
  78. package/package.json +1 -1
  79. package/pages/auth/login.vue +1 -1
  80. package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +110 -0
  81. package/pages/c/_cluster/apps/charts/install.vue +29 -0
  82. package/pages/c/_cluster/auth/config/index.vue +11 -4
  83. package/pages/c/_cluster/explorer/workload-dashboard/__tests__/composable.test.ts +136 -1
  84. package/pages/c/_cluster/explorer/workload-dashboard/composable.ts +69 -1
  85. package/pages/c/_cluster/fleet/index.vue +5 -9
  86. package/pages/c/_cluster/monitoring/index.vue +6 -1
  87. package/pkg/vue.config.js +4 -3
  88. package/plugins/codemirror.js +2 -0
  89. package/plugins/dashboard-store/resource-class.js +13 -9
  90. package/plugins/steve/__tests__/steve-class-resource-api.test.ts +147 -0
  91. package/plugins/steve/steve-class.js +43 -0
  92. package/plugins/steve/subscribe.js +11 -0
  93. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +115 -0
  94. package/rancher-components/Form/LabeledInput/LabeledInput.vue +83 -3
  95. package/store/__tests__/action-menu.test.ts +622 -0
  96. package/store/__tests__/auth.test.ts +18 -0
  97. package/store/__tests__/notifications.test.ts +466 -1
  98. package/store/__tests__/slideInPanel.test.ts +143 -43
  99. package/store/__tests__/ui-context.test.ts +255 -0
  100. package/store/__tests__/wm.test.ts +503 -0
  101. package/store/auth.js +11 -1
  102. package/store/aws.js +19 -4
  103. package/store/features.js +1 -0
  104. package/store/plugins.js +6 -0
  105. package/store/slideInPanel.ts +15 -3
  106. package/types/rancher/steve.api.ts +2 -2
  107. package/types/shell/index.d.ts +84 -2
  108. package/types/store/dashboard-store.types.ts +2 -2
  109. package/types/store/type-map.ts +262 -1
  110. package/utils/__tests__/fleet-appco.test.ts +23 -0
  111. package/utils/async.ts +2 -0
  112. package/utils/fleet-appco.ts +6 -1
  113. package/utils/grafana.js +2 -17
  114. package/utils/monitoring.js +38 -1
  115. package/utils/sort.js +1 -1
  116. 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: TitleBarProps;
8
- metadataProps: 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 v-bind="props.titleBarProps">
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 panelTop = computed(() => {
23
- // Some components like the ResourceDetailDrawer are designed to take up the full height of the viewport so we want to be able to specify the top.
24
- if (currentProps?.value?.top) {
25
- return currentProps?.value?.top;
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
- // Some components like the ResourceDetailDrawer are designed to take up the full height of the viewport so we want to be able to specify the height.
39
- const panelHeight = computed(() => (currentProps?.value?.height) ? (currentProps?.value?.height) : `calc(100vh - ${ panelTop?.value })`);
40
- const panelWidth = computed(() => currentProps?.value?.width || '33%');
41
- const panelRight = computed(() => (isOpen?.value ? '0' : `-${ panelWidth?.value }`));
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?.value.closeOnRouteChange;
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
- * trigger focus trap
112
+ * Focus trap logic
59
113
  */
60
- () => isOpen?.value,
114
+ () => isOpen.value,
61
115
  (neu, old) => {
62
116
  if (neu && neu !== old) {
63
- const opts:any = {
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
- // putting the initial focus on the first element that is not conditionally displayed
66
- initialFocus: slideInPanelManagerClose.value
126
+ initialFocus: closeEl || panelEl,
127
+ fallbackFocus: panelEl
67
128
  };
68
129
 
69
- const returnFocusSelector = currentProps?.value?.returnFocusSelector;
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?.value?.focusTrapWatcherBasedVariable) {
146
+ if (currentProps.value?.focusTrapWatcherBasedVariable) {
89
147
  return currentProps.value.focusTrapWatcherBasedVariable;
90
148
  }
91
149
 
92
- return isOpen?.value && !isClosing?.value;
150
+ return isOpen.value && !isClosing.value;
93
151
  },
94
- slideInPanelManager.value as HTMLElement,
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?.value) {
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: ['width', 'title', 'extraProp']
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: '40%', title: 'Test Title', extraProp: 'extra'
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: 40%');
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('renders default panel title when no title is provided', async() => {
71
- // Update getter so that no title is provided
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 headerTitle = document.querySelector('#slides #slide-in-panel-manager .header .title') as HTMLElement;
79
+ const header = document.querySelector('#slides #slide-in-panel-manager .header') as HTMLElement;
81
80
 
82
- expect(headerTitle.textContent?.trim()).toBe('Details');
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 currentProps width "40%", panelRight should be "-40%" when closed.
126
- expect(styleAttr).toContain('right: -40%');
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
  });
@@ -60,7 +60,7 @@ export default {
60
60
  <slot name="actions" />
61
61
  <button
62
62
  type="button"
63
- class="btn-sm role-primary"
63
+ class="btn btn-sm role-primary"
64
64
  @click="edit"
65
65
  >
66
66
  {{ t('action.edit') }}
@@ -285,7 +285,7 @@ export default {
285
285
  </legend>
286
286
  <LabeledInput
287
287
  v-model:value.number="securityContext.runAsUser"
288
- type="number"
288
+ type="integer"
289
289
  min="0"
290
290
  :label="t('workload.container.security.runAsUser.label')"
291
291
  :mode="mode"
@@ -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'](`${ this.prefix }.${ this.value || this.row.id }`, null, this.value);
43
+ return this.$store.getters['i18n/withFallback'](this.l10nId, null, this.l10nFallback);
23
44
  },
24
45
  },
25
46
  };