@rancher/shell 3.0.8-rc.6 → 3.0.8-rc.8

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 (75) hide show
  1. package/assets/brand/suse/favicon.png +0 -0
  2. package/assets/images/content/README.md +5 -0
  3. package/assets/images/content/cloud-native.svg +84 -0
  4. package/assets/images/content/dark/cloud-native.svg +21 -0
  5. package/assets/images/content/dark/shield.svg +59 -0
  6. package/assets/images/content/dark/suse.svg +10 -0
  7. package/assets/images/content/shield.svg +59 -0
  8. package/assets/images/content/suse.svg +10 -0
  9. package/assets/styles/themes/_dark.scss +1 -1
  10. package/assets/styles/themes/_light.scss +1 -1
  11. package/assets/styles/themes/_modern.scss +2 -2
  12. package/assets/styles/themes/_suse.scss +4 -3
  13. package/assets/translations/en-us.yaml +6 -0
  14. package/components/BannerGraphic.vue +4 -4
  15. package/components/Drawer/Chrome.vue +2 -6
  16. package/components/Drawer/ResourceDetailDrawer/ConfigTab.vue +3 -9
  17. package/components/Drawer/ResourceDetailDrawer/YamlTab.vue +3 -8
  18. package/components/Drawer/ResourceDetailDrawer/composables.ts +3 -4
  19. package/components/Drawer/ResourceDetailDrawer/index.vue +3 -9
  20. package/components/Drawer/ResourceDetailDrawer/types.ts +16 -0
  21. package/components/Drawer/types.ts +3 -0
  22. package/components/DynamicContent/DynamicContentBanner.vue +102 -0
  23. package/components/DynamicContent/DynamicContentCloseButton.vue +42 -0
  24. package/components/DynamicContent/DynamicContentIcon.vue +132 -0
  25. package/components/DynamicContent/DynamicContentPanel.vue +112 -0
  26. package/components/DynamicContent/content.ts +78 -0
  27. package/components/PaginatedResourceTable.vue +2 -6
  28. package/components/Resource/Detail/CopyToClipboard.vue +3 -0
  29. package/components/Resource/Detail/Metadata/KeyValueRow.vue +1 -1
  30. package/components/Resource/Detail/Metadata/composables.ts +9 -9
  31. package/components/Resource/Detail/TitleBar/composables.ts +2 -1
  32. package/components/Resource/Detail/composables.ts +12 -0
  33. package/components/nav/Header.vue +2 -3
  34. package/components/nav/NamespaceFilter.vue +13 -1
  35. package/components/nav/NotificationCenter/index.vue +2 -1
  36. package/components/nav/TopLevelMenu.helper.ts +16 -6
  37. package/components/templates/plain.vue +30 -4
  38. package/core/plugin-helpers.ts +2 -0
  39. package/edit/auth/__tests__/oidc.test.ts +26 -0
  40. package/edit/auth/oidc.vue +5 -1
  41. package/edit/provisioning.cattle.io.cluster/__tests__/Networking.test.ts +24 -1
  42. package/edit/provisioning.cattle.io.cluster/index.vue +7 -3
  43. package/edit/provisioning.cattle.io.cluster/rke2.vue +7 -5
  44. package/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue +3 -2
  45. package/edit/service.vue +8 -4
  46. package/machine-config/amazonec2.vue +7 -1
  47. package/machine-config/components/EC2Networking.vue +37 -21
  48. package/machine-config/components/__tests__/EC2Networking.test.ts +54 -0
  49. package/mixins/__tests__/chart.test.ts +21 -0
  50. package/mixins/chart.js +7 -1
  51. package/models/provisioning.cattle.io.cluster.js +5 -1
  52. package/package.json +1 -1
  53. package/pages/c/_cluster/manager/hostedprovider/index.vue +5 -0
  54. package/pages/home.vue +14 -3
  55. package/pkg/dynamic-importer.lib.js +4 -0
  56. package/plugins/dashboard-store/resource-class.js +1 -2
  57. package/plugins/steve/subscribe.js +17 -9
  58. package/plugins/subscribe-events.ts +4 -2
  59. package/rancher-components/RcDropdown/RcDropdownItem.vue +1 -0
  60. package/store/index.js +32 -13
  61. package/store/notifications.ts +21 -5
  62. package/store/type-map.js +3 -3
  63. package/types/notifications/index.ts +3 -0
  64. package/types/shell/index.d.ts +1 -0
  65. package/types/store/subscribe-events.types.ts +8 -1
  66. package/types/store/subscribe.types.ts +1 -0
  67. package/types/window-manager.ts +2 -0
  68. package/utils/__tests__/version.test.ts +19 -1
  69. package/utils/back-off.ts +3 -3
  70. package/utils/dynamic-content/__tests__/info.test.ts +15 -9
  71. package/utils/dynamic-content/announcement.ts +71 -41
  72. package/utils/dynamic-content/info.ts +1 -2
  73. package/utils/dynamic-content/types.d.ts +21 -1
  74. package/utils/pagination-wrapper.ts +12 -8
  75. package/utils/version.js +15 -0
@@ -0,0 +1,42 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Common close button for dynamic component banners and panels
4
+ */
5
+
6
+ import { useStore } from 'vuex';
7
+ import { RcButton } from '@components/RcButton';
8
+
9
+ const props = defineProps<{id: string}>();
10
+ const store = useStore();
11
+
12
+ /*
13
+ * Marks the notification as read when the close button is clicked
14
+ */
15
+ const markRead = () => {
16
+ store.dispatch('notifications/markRead', props.id);
17
+ };
18
+ </script>
19
+ <template>
20
+ <RcButton
21
+ ghost
22
+ small
23
+ :aria-label="t('dynamicContent.action.close')"
24
+ tabindex="0"
25
+ @click="markRead()"
26
+ >
27
+ <i class="dc-close-button icon icon-close" />
28
+ </RcButton>
29
+ </template>
30
+ <style lang="scss" scoped>
31
+ .dc-close-button {
32
+ opacity: 0.5;
33
+ cursor: pointer;
34
+ border: 1px solid transparent;
35
+ padding: 4px;
36
+ border-radius: 4px;
37
+
38
+ &:hover {
39
+ opacity: 1;
40
+ }
41
+ }
42
+ </style>
@@ -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
+ };
@@ -116,15 +116,11 @@ export default defineComponent({
116
116
  },
117
117
 
118
118
  async fetch() {
119
- const promises = [
120
- this.$fetchType(this.resource, [], this.overrideInStore || this.inStore),
121
- ];
122
-
123
119
  if (this.fetchSecondaryResources) {
124
- promises.push(this.fetchSecondaryResources({ canPaginate: this.canPaginate }));
120
+ await this.fetchSecondaryResources({ canPaginate: this.canPaginate });
125
121
  }
126
122
 
127
- await Promise.all(promises);
123
+ await this.$fetchType(this.resource, [], this.overrideInStore || this.inStore);
128
124
  },
129
125
 
130
126
  computed: {
@@ -52,6 +52,9 @@ const onClick = (ev: MouseEvent) => {
52
52
  border-radius: 50%;
53
53
  justify-content: center;
54
54
  align-items: center;
55
+ padding: 0;
56
+ line-height: initial;
57
+ min-height: initial;
55
58
 
56
59
  border: 1px solid var(--primary);
57
60
  color: var(--primary);
@@ -81,7 +81,7 @@ const previewId = randomStr();
81
81
  position: fixed;
82
82
 
83
83
  right: -20px;
84
- top: -9px;
84
+ top: -6px;
85
85
  z-index: 20px;
86
86
  }
87
87
 
@@ -6,18 +6,19 @@ import { computed, toValue, Ref } from 'vue';
6
6
  import {
7
7
  useLiveDate, useNamespace, useProject, useResourceDetails, useWorkspace
8
8
  } from '@shell/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields';
9
+ import { useOnShowConfiguration } from '@shell/components/Resource/Detail/composables';
9
10
 
10
11
  export const useBasicMetadata = (resource: any) => {
11
12
  const labels = useDefaultLabels(resource);
12
13
  const annotations = useDefaultAnnotations(resource);
13
- const resourceValue = toValue(resource);
14
+ const onShowConfiguration = useOnShowConfiguration(resource);
14
15
 
15
16
  return computed(() => {
16
17
  return {
17
- resource: toValue(resource),
18
- labels: labels.value,
19
- annotations: annotations.value,
20
- onShowConfiguration: () => resourceValue.showConfiguration()
18
+ resource: toValue(resource),
19
+ labels: labels.value,
20
+ annotations: annotations.value,
21
+ onShowConfiguration
21
22
  };
22
23
  });
23
24
  };
@@ -28,7 +29,7 @@ export const useDefaultMetadataProps = (resource: any, additionalIdentifyingInfo
28
29
 
29
30
  const identifyingInformation = computed(() => [...defaultIdentifyingInformation.value, ...(additionalIdentifyingInformationValue || [])]);
30
31
  const basicMetaData = useBasicMetadata(resource);
31
- const resourceValue = toValue(resource);
32
+ const onShowConfiguration = useOnShowConfiguration(resource);
32
33
 
33
34
  return computed(() => {
34
35
  return {
@@ -36,7 +37,7 @@ export const useDefaultMetadataProps = (resource: any, additionalIdentifyingInfo
36
37
  identifyingInformation: identifyingInformation.value,
37
38
  labels: basicMetaData.value.labels,
38
39
  annotations: basicMetaData.value.annotations,
39
- onShowConfiguration: (returnFocusSelector: string) => resourceValue.showConfiguration(returnFocusSelector)
40
+ onShowConfiguration
40
41
  };
41
42
  });
42
43
  };
@@ -47,7 +48,6 @@ export const useDefaultMetadataForLegacyPagesProps = (resource: any) => {
47
48
  const workspace = useWorkspace(resource);
48
49
  const namespace = useNamespace(resource);
49
50
  const liveDate = useLiveDate(resource);
50
- const resourceValue = toValue(resource);
51
51
 
52
52
  const identifyingInformation = computed((): IdentifyingInformationRow[] => {
53
53
  const defaultInfo = [
@@ -71,7 +71,7 @@ export const useDefaultMetadataForLegacyPagesProps = (resource: any) => {
71
71
  identifyingInformation: identifyingInformation.value,
72
72
  labels: basicMetaData.value.labels,
73
73
  annotations: basicMetaData.value.annotations,
74
- onShowConfiguration: (returnFocusSelector?: string) => resourceValue.showConfiguration(returnFocusSelector)
74
+ onShowConfiguration: basicMetaData.value.onShowConfiguration
75
75
  };
76
76
  });
77
77
  };
@@ -1,3 +1,4 @@
1
+ import { useOnShowConfiguration } from '@shell/components/Resource/Detail/composables';
1
2
  import { TitleBarProps } from '@shell/components/Resource/Detail/TitleBar/index.vue';
2
3
  import { computed, Ref, toValue } from 'vue';
3
4
  import { useRoute } from 'vue-router';
@@ -23,7 +24,7 @@ export const useDefaultTitleBarProps = (resource: any, resourceSubtype?: Ref<str
23
24
  resource: resourceValue.type
24
25
  }
25
26
  };
26
- const onShowConfiguration = resourceValue.disableResourceDetailDrawer ? undefined : (returnFocusSelector: string) => resourceValue.showConfiguration(returnFocusSelector);
27
+ const onShowConfiguration = resourceValue.disableResourceDetailDrawer ? undefined : useOnShowConfiguration(resource);
27
28
 
28
29
  return {
29
30
  resource: resourceValue,
@@ -2,6 +2,7 @@ import { computed, Ref, toValue } from 'vue';
2
2
  import { useStore } from 'vuex';
3
3
  import { Props as BannerProps } from '@components/Banner/Banner.vue';
4
4
  import { useI18n } from '@shell/composables/useI18n';
5
+ import ResourceClass from '@shell/plugins/dashboard-store/resource-class';
5
6
 
6
7
  export const useResourceDetailBannerProps = (resource: any): Ref<BannerProps | undefined> => {
7
8
  const store = useStore();
@@ -43,3 +44,14 @@ export const useResourceDetailBannerProps = (resource: any): Ref<BannerProps | u
43
44
  return undefined;
44
45
  });
45
46
  };
47
+
48
+ export const useOnShowConfiguration = (resource: any) => {
49
+ return (returnFocusSelector?: string) => {
50
+ const resourceValue = toValue(resource);
51
+ // Because extensions can make a copy of the resource-class it's possible that an extension will have a resource-class which predates the inclusion of showConfiguration
52
+ // to still the rest of shell to consume
53
+ const showConfiguration = resourceValue.showConfiguration ? resourceValue.showConfiguration.bind(resourceValue) : ResourceClass.prototype.showConfiguration.bind(resourceValue);
54
+
55
+ showConfiguration(returnFocusSelector);
56
+ };
57
+ };
@@ -97,7 +97,6 @@ export default {
97
97
  'isSingleProduct',
98
98
  'isRancherInHarvester',
99
99
  'showTopLevelMenu',
100
- 'isMultiCluster',
101
100
  'showWorkspaceSwitcher'
102
101
  ]),
103
102
 
@@ -424,7 +423,7 @@ export default {
424
423
  data-testid="header"
425
424
  >
426
425
  <div>
427
- <TopLevelMenu v-if="isRancherInHarvester || isMultiCluster || !isSingleProduct" />
426
+ <TopLevelMenu v-if="showTopLevelMenu" />
428
427
  </div>
429
428
 
430
429
  <div
@@ -495,7 +494,7 @@ export default {
495
494
  :alt="t('branding.logos.label')"
496
495
  />
497
496
  <div
498
- v-if="!currentCluster"
497
+ v-if="!currentCluster && !$route.path.startsWith('/c/')"
499
498
  class="simple-title"
500
499
  >
501
500
  <BrandImage
@@ -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
- const allNotifications = computed(() => store.getters['notifications/all']);
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
  });
@@ -28,6 +28,7 @@ interface UpdateArgs {
28
28
  searchTerm: string,
29
29
  pinnedIds: string[],
30
30
  unPinnedMax?: number,
31
+ forceWatch?: boolean
31
32
  }
32
33
 
33
34
  type MgmtCluster = {
@@ -192,9 +193,12 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
192
193
  this.clustersOthersWrapper = new PaginationWrapper({
193
194
  $store,
194
195
  id: 'tlm-unpinned-clusters',
195
- onChange: async() => {
196
+ onChange: async({ forceWatch }) => {
196
197
  if (this.args) {
197
- await this.update(this.args);
198
+ await this.update({
199
+ ...this.args,
200
+ forceWatch
201
+ });
198
202
  }
199
203
  },
200
204
  enabledFor: {
@@ -210,9 +214,12 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
210
214
  this.provClusterWrapper = new PaginationWrapper({
211
215
  $store,
212
216
  id: 'tlm-prov-clusters',
213
- onChange: async() => {
217
+ onChange: async({ forceWatch }) => {
214
218
  if (this.args) {
215
- await this.update(this.args);
219
+ await this.update({
220
+ ...this.args,
221
+ forceWatch
222
+ });
216
223
  }
217
224
  },
218
225
  enabledFor: {
@@ -244,7 +251,7 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
244
251
  pinned: MgmtCluster[],
245
252
  notPinned: MgmtCluster[]
246
253
  } = await allHash(promises) as any;
247
- const provClusters = await this.updateProvCluster(res.notPinned, res.pinned);
254
+ const provClusters = await this.updateProvCluster(res.notPinned, res.pinned, args.forceWatch || false);
248
255
  const provClustersByMgmtId = provClusters.reduce((res: { [mgmtId: string]: ProvCluster}, provCluster: ProvCluster) => {
249
256
  if (provCluster.mgmtClusterId) {
250
257
  res[provCluster.mgmtClusterId] = provCluster;
@@ -340,6 +347,7 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
340
347
  }
341
348
 
342
349
  return this.clustersPinnedWrapper.request({
350
+ forceWatch: args.forceWatch,
343
351
  pagination: {
344
352
  filters: this.constructParams({
345
353
  pinnedIds: args.pinnedIds,
@@ -357,6 +365,7 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
357
365
  */
358
366
  private async updateOthers(args: UpdateArgs): Promise<MgmtCluster[]> {
359
367
  return this.clustersOthersWrapper.request({
368
+ forceWatch: args.forceWatch,
360
369
  pagination: {
361
370
  filters: this.constructParams({
362
371
  searchTerm: args.searchTerm,
@@ -375,8 +384,9 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
375
384
  /**
376
385
  * Find all provisioning clusters associated with the displayed mgmt clusters
377
386
  */
378
- private async updateProvCluster(notPinned: MgmtCluster[], pinned: MgmtCluster[]): Promise<ProvCluster[]> {
387
+ private async updateProvCluster(notPinned: MgmtCluster[], pinned: MgmtCluster[], forceWatch: boolean): Promise<ProvCluster[]> {
379
388
  return this.provClusterWrapper.request({
389
+ forceWatch,
380
390
  pagination: {
381
391
  filters: [
382
392
  PaginationParamFilter.createMultipleFields(