Add home page announcements

This commit is contained in:
Neil MacDougall 2025-10-19 20:07:25 +01:00
parent 07fc4842e5
commit 56bcea43ab
7 changed files with 149 additions and 3 deletions

View File

@ -0,0 +1,124 @@
<script>
import { NotificationLevel } from '@shell/types/notifications';
export default {
name: 'HomePageDynamicContent',
props: {
location: {
type: String,
default: 'banner'
}
},
computed: {
// Return the un-read hidden notifications for display on the home page
dynamicContent() {
let hiddenNotifications = this.$store.getters['notifications/all'].filter((n) => n.level === NotificationLevel.Hidden && !n.read);
hiddenNotifications = hiddenNotifications.filter((n) => n.data?.location === this.location);
return hiddenNotifications.length > 0 ? hiddenNotifications[0] : undefined;
}
},
methods: {
// Invoke action on either the primary or secondary buttons
// This can open a URL in a new tab OR navigate to an application route
action(action) {
if (action.target) {
window.open(action.target, '_blank');
} else if (action.route) {
try {
this.$router.push(action.route);
} catch (e) {
console.error('Error navigating to route for the notification action', e); // eslint-disable-line no-console
}
} else {
console.error('Notification action must either specify a "target" or a "route"'); // eslint-disable-line no-console
}
},
markRead(notification) {
this.$store.dispatch('notifications/markRead', notification.id);
}
}
};
</script>
<template>
<div
v-if="dynamicContent"
class="home-page-dynamic-content"
>
<div class="dc-content">
<div class="dc-title">
{{ dynamicContent.title }}
</div>
<div class="dc-message">
{{ dynamicContent.message }}
</div>
</div>
<div class="dc-actions">
<button
v-if="dynamicContent.primaryAction"
role="button"
class="btn btn-sm role-primary"
@click.stop.prevent="action(dynamicContent.primaryAction)"
>
{{ dynamicContent.primaryAction.label }}
</button>
<i
class="icon icon-close"
@click="markRead(dynamicContent)"
/>
</div>
</div>
</template>
<style lang="scss" scoped>
.home-page-dynamic-content {
background-color: #e8e8e8;
border-top: 1px solid var(--border);
border-bottom: 1px solid var(--border);
display: flex;
flex-direction: row;
align-items: center;
padding: 8px;
.dc-content {
display: flex;
flex-direction: column;
flex: 1;
}
.dc-title {
font-weight: bold;
font-size: 1.1em;
margin-bottom: 4px;
}
.dc-message {
font-size: 1em;
}
.dc-actions {
display: flex;
align-items: center;
margin: 0 8px;
i {
margin-left: 16px;
opacity: 0.5;
cursor: pointer;
border: 1px solid transparent;
padding: 4px;
border-radius: 4px;
&:hover {
opacity: 1;
color: var(--primary);
border-color: var(--primary);
}
}
}
}
</style>

View File

@ -11,9 +11,11 @@ import {
RcDropdownSeparator, RcDropdownSeparator,
RcDropdownTrigger RcDropdownTrigger
} from '@components/RcDropdown'; } from '@components/RcDropdown';
import { NotificationLevel, Notification as NotificationType } from '@shell/types/notifications';
const store = useStore(); const store = useStore();
const allNotifications = computed(() => store.getters['notifications/all']); // We don't want any hidden notifications showing in the notification center (these are shown elsewhere, e.g. home page dynamic content announcements)
const allNotifications = computed(() => store.getters['notifications/all'].filter((n: NotificationType) => n.level !== NotificationLevel.Hidden));
const unreadLevelClass = computed(() => { const unreadLevelClass = computed(() => {
return store.getters['notifications/unreadCount'] === 0 ? '' : 'unread'; return store.getters['notifications/unreadCount'] === 0 ? '' : 'unread';
}); });

View File

@ -7,6 +7,7 @@ import PaginatedResourceTable from '@shell/components/PaginatedResourceTable.vue
import { BadgeState } from '@components/BadgeState'; import { BadgeState } from '@components/BadgeState';
import CommunityLinks from '@shell/components/CommunityLinks.vue'; import CommunityLinks from '@shell/components/CommunityLinks.vue';
import SingleClusterInfo from '@shell/components/SingleClusterInfo.vue'; import SingleClusterInfo from '@shell/components/SingleClusterInfo.vue';
import HomePageDynamicContent from '@shell/components/HomePageDynamicContent.vue';
import { mapGetters, mapState } from 'vuex'; import { mapGetters, mapState } from 'vuex';
import { MANAGEMENT, CAPI, COUNT } from '@shell/config/types'; import { MANAGEMENT, CAPI, COUNT } from '@shell/config/types';
import { NAME as MANAGER } from '@shell/config/product/manager'; import { NAME as MANAGER } from '@shell/config/product/manager';
@ -47,6 +48,7 @@ export default defineComponent({
SingleClusterInfo, SingleClusterInfo,
TabTitle, TabTitle,
ResourceTable, ResourceTable,
HomePageDynamicContent,
}, },
mixins: [PageHeaderActions, Preset], mixins: [PageHeaderActions, Preset],
@ -610,6 +612,7 @@ export default defineComponent({
pref-key="welcomeBanner" pref-key="welcomeBanner"
data-testid="home-banner-graphic" data-testid="home-banner-graphic"
/> />
<HomePageDynamicContent location="banner" />
<IndentedPanel class="mt-20 mb-20"> <IndentedPanel class="mt-20 mb-20">
<div class="row home-panels"> <div class="row home-panels">
<div class="col main-panel"> <div class="col main-panel">

View File

@ -66,7 +66,8 @@ async function saveEncryptedNotification(getters: any, notification: Notificatio
primaryAction: notification.primaryAction, primaryAction: notification.primaryAction,
secondaryAction: notification.secondaryAction, secondaryAction: notification.secondaryAction,
preference: notification.preference, preference: notification.preference,
handlerName: notification.handlerName handlerName: notification.handlerName,
data: notification.data,
}; };
const localStorageKey = getters['localStorageKey']; const localStorageKey = getters['localStorageKey'];

View File

@ -14,6 +14,7 @@ export enum NotificationLevel {
Success, // eslint-disable-line no-unused-vars Success, // eslint-disable-line no-unused-vars
Warning, // eslint-disable-line no-unused-vars Warning, // eslint-disable-line no-unused-vars
Error, // eslint-disable-line no-unused-vars Error, // eslint-disable-line no-unused-vars
Hidden, // eslint-disable-line no-unused-vars
} }
/** /**
@ -52,6 +53,8 @@ export type EncryptedNotification = {
// Handler to be associated with this notification that can invoke additional behaviour when the notification changes // Handler to be associated with this notification that can invoke additional behaviour when the notification changes
// This is the name of the handler (the handlers are added as extensions). Notifications are persisted in the store, so can't use functions. // This is the name of the handler (the handlers are added as extensions). Notifications are persisted in the store, so can't use functions.
handlerName?: string; handlerName?: string;
// Additional data to be stored with the notification (optional)
data?: any;
}; };
/** /**

View File

@ -15,10 +15,15 @@ import { DynamicContentAnnouncementHandlerName } from './notification-handler';
// Prefixes used in the notifications IDs created here // Prefixes used in the notifications IDs created here
export const ANNOUNCEMENT_PREFIX = 'announcement-'; export const ANNOUNCEMENT_PREFIX = 'announcement-';
const TARGET_NOTIFICATION_CENTER = 'notification';
const TARGET_HOME_PAGE = 'homepage';
const ALLOWED_TARGETS = [TARGET_NOTIFICATION_CENTER, TARGET_HOME_PAGE];
const ALLOWED_NOTIFICATIONS: Record<string, NotificationLevel> = { const ALLOWED_NOTIFICATIONS: Record<string, NotificationLevel> = {
announcement: NotificationLevel.Announcement, announcement: NotificationLevel.Announcement,
info: NotificationLevel.Info, info: NotificationLevel.Info,
warning: NotificationLevel.Warning, warning: NotificationLevel.Warning,
homepage: NotificationLevel.Hidden,
}; };
/** /**
@ -49,6 +54,7 @@ export async function processAnnouncements(context: Context, announcements: Anno
// Check type // Check type
const targetSplit = announcement.target.split('/'); const targetSplit = announcement.target.split('/');
const target = targetSplit[0];
if (targetSplit[0] === 'notification') { if (targetSplit[0] === 'notification') {
// Show a notification // Show a notification
@ -85,6 +91,7 @@ export async function processAnnouncements(context: Context, announcements: Anno
title: announcement.title, title: announcement.title,
message: announcement.message, message: announcement.message,
handlerName: DynamicContentAnnouncementHandlerName, handlerName: DynamicContentAnnouncementHandlerName,
data,
}; };
if (announcement.cta?.primary) { if (announcement.cta?.primary) {
@ -101,7 +108,7 @@ export async function processAnnouncements(context: Context, announcements: Anno
}; };
} }
logger.info(`Adding announcement with ID ${ id } (title: ${ announcement.title })`); logger.info(`Adding announcement with ID ${ id } (title: ${ announcement.title }, target: ${ announcement.target })`);
await dispatch('notifications/add', notification); await dispatch('notifications/add', notification);
} }

View File

@ -108,12 +108,18 @@ export type Announcement = {
target: string; // Where the announcement should be shown target: string; // Where the announcement should be shown
version?: string; // Version or semver expression for when to show this announcement version?: string; // Version or semver expression for when to show this announcement
audience?: 'admin' | 'all'; // Audience - show for just Admins or for all users audience?: 'admin' | 'all'; // Audience - show for just Admins or for all users
icon?: string;
cta?: { cta?: {
primary: CallToAction, // Must have a primary call to action, if we have a cta field primary: CallToAction, // Must have a primary call to action, if we have a cta field
secondary?: CallToAction, secondary?: CallToAction,
} }
}; };
export type AnnouncementNotificationData = {
icon?: string;
location: string;
};
/** /**
* Main type for the metadata that is retrieved from the dynamic content endpoint * Main type for the metadata that is retrieved from the dynamic content endpoint
*/ */