diff --git a/package.json b/package.json index 74b98d9d65..083c70e483 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "build:plugins-report": "node ./scripts/build-plugins-report", "clean": "backstage-cli repo clean", "create-plugin": "echo \"use 'yarn new' instead\"", - "dev": "NODE_OPTIONS=--no-node-snapshot yarn workspaces foreach -A --include example-backend --include example-app --parallel --jobs unlimited -v -i run start", + "dev": "yarn workspaces foreach -A --include example-backend --include example-app --parallel --jobs unlimited -v -i run start", "dev:next": "yarn workspaces foreach -A --include example-backend --include example-app-next --parallel --jobs unlimited -v -i run start", "docker-build": "yarn tsc && yarn workspace example-backend build && yarn workspace example-backend build-image", "fix": "backstage-cli repo fix --publish", diff --git a/plugins/notifications-backend/src/service/router.ts b/plugins/notifications-backend/src/service/router.ts index 126963932b..9eb774a30f 100644 --- a/plugins/notifications-backend/src/service/router.ts +++ b/plugins/notifications-backend/src/service/router.ts @@ -38,6 +38,7 @@ import { } from '@backstage/backend-plugin-api'; import { SignalsService } from '@backstage/plugin-signals-node'; import { + ChannelSetting, isNotificationsEnabledFor, NewNotificationSignal, Notification, @@ -45,6 +46,7 @@ import { NotificationSettings, notificationSeverities, NotificationStatus, + OriginSetting, } from '@backstage/plugin-notifications-common'; import { parseEntityOrderFieldParams } from './parseEntityOrderFieldParams'; import { getUsersForEntityRef } from './getUsersForEntityRef'; @@ -89,52 +91,68 @@ export async function createRouter( return info.userEntityRef; }; + const getTopicSettings = ( + topic: any, + existingOrigin: OriginSetting | undefined, + defaultEnabled: boolean, + ) => { + const existingTopic = existingOrigin?.topics?.find( + t => t.id === topic.topic, + ); + return { + id: topic.topic, + enabled: existingTopic ? existingTopic.enabled : defaultEnabled, + }; + }; + + const getOriginSettings = ( + originId: string, + existingChannel: ChannelSetting | undefined, + topics: { origin: string; topic: string }[], + ) => { + const existingOrigin = existingChannel?.origins.find( + o => o.id === originId, + ); + const defaultEnabled = existingOrigin ? existingOrigin.enabled : true; + return { + id: originId, + enabled: defaultEnabled, + topics: topics + .filter(t => t.origin === originId) + .map(t => getTopicSettings(t, existingOrigin, defaultEnabled)), + }; + }; + const getNotificationChannels = () => { return [WEB_NOTIFICATION_CHANNEL, ...processors.map(p => p.getName())]; }; + const getChannelSettings = ( + channelId: string, + settings: NotificationSettings, + origins: string[], + topics: { origin: string; topic: string }[], + ) => { + const existingChannel = settings.channels.find(c => c.id === channelId); + return { + id: channelId, + origins: origins.map(originId => + getOriginSettings(originId, existingChannel, topics), + ), + }; + }; + const getNotificationSettings = async (user: string) => { const { origins } = await store.getUserNotificationOrigins({ user }); const { topics } = await store.getUserNotificationTopics({ user }); const settings = await store.getNotificationSettings({ user }); const channels = getNotificationChannels(); - const response: NotificationSettings = { - channels: channels.map(channelId => { - return { - id: channelId, - origins: origins.map(originId => { - const existingChannel = settings.channels.find( - c => c.id === channelId, - ); - const existingOrigin = existingChannel?.origins.find( - o => o.id === originId, - ); - const defaultEnabled = existingOrigin - ? existingOrigin.enabled - : true; - return { - id: originId, - enabled: defaultEnabled, - topics: topics - .filter(t => t.origin === originId) - .map(t => { - const existingTopic = - existingOrigin?.topics && - existingOrigin.topics.find(topic => topic.id === t.topic); - return { - id: t.topic, - enabled: existingTopic - ? existingTopic.enabled - : defaultEnabled, - }; - }), - }; - }), - }; - }), + return { + channels: channels.map(channelId => + getChannelSettings(channelId, settings, origins, topics), + ), }; - return response; }; const isNotificationsEnabled = async (opts: { diff --git a/plugins/notifications-common/src/types.ts b/plugins/notifications-common/src/types.ts index 4bf9bf1bac..48f89cbbb4 100644 --- a/plugins/notifications-common/src/types.ts +++ b/plugins/notifications-common/src/types.ts @@ -129,16 +129,31 @@ export type NotificationProcessorFilters = { /** * @public */ -export type NotificationSettings = { - channels: { - id: string; - origins: { - id: string; - enabled: boolean; - topics?: { - id: string; - enabled: boolean; - }[]; - }[]; - }[]; +export type TopicSetting = { + id: string; + enabled: boolean; +}; + +/** + * @public + */ +export type OriginSetting = { + id: string; + enabled: boolean; + topics?: TopicSetting[]; +}; + +/** + * @public + */ +export type ChannelSetting = { + id: string; + origins: OriginSetting[]; +}; + +/** + * @public + */ +export type NotificationSettings = { + channels: ChannelSetting[]; }; diff --git a/plugins/notifications/src/components/UserNotificationSettingsCard/UserNotificationSettingsCard.tsx b/plugins/notifications/src/components/UserNotificationSettingsCard/UserNotificationSettingsCard.tsx index 81d109e5cc..f74c7882e3 100644 --- a/plugins/notifications/src/components/UserNotificationSettingsCard/UserNotificationSettingsCard.tsx +++ b/plugins/notifications/src/components/UserNotificationSettingsCard/UserNotificationSettingsCard.tsx @@ -25,6 +25,7 @@ import { UserNotificationSettingsPanel } from './UserNotificationSettingsPanel'; /** @public */ export const UserNotificationSettingsCard = (props: { originNames?: Record; + topicNames?: Record; }) => { const [settings, setNotificationSettings] = React.useState< NotificationSettings | undefined @@ -56,6 +57,7 @@ export const UserNotificationSettingsCard = (props: { settings={settings} onChange={onUpdate} originNames={props.originNames} + topicNames={props.topicNames} /> )} diff --git a/plugins/notifications/src/components/UserNotificationSettingsCard/UserNotificationSettingsPanel.tsx b/plugins/notifications/src/components/UserNotificationSettingsCard/UserNotificationSettingsPanel.tsx index 691c95e430..e6ed892fe5 100644 --- a/plugins/notifications/src/components/UserNotificationSettingsCard/UserNotificationSettingsPanel.tsx +++ b/plugins/notifications/src/components/UserNotificationSettingsCard/UserNotificationSettingsPanel.tsx @@ -14,15 +14,17 @@ * limitations under the License. */ -import React from 'react'; +import React, { useState } from 'react'; import { isNotificationsEnabledFor, NotificationSettings, } from '@backstage/plugin-notifications-common'; -import ChevronRight from '@material-ui/icons/ChevronRight'; +import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown'; +import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp'; import Table from '@material-ui/core/Table'; import MuiTableCell from '@material-ui/core/TableCell'; import { withStyles } from '@material-ui/core/styles'; +import IconButton from '@material-ui/core/IconButton'; import TableHead from '@material-ui/core/TableHead'; import Typography from '@material-ui/core/Typography'; import TableBody from '@material-ui/core/TableBody'; @@ -37,13 +39,6 @@ const TableCell = withStyles({ }, })(MuiTableCell); -const CenterContentsTableCell = withStyles({ - root: { - display: 'flex', - alignItems: 'center', - }, -})(MuiTableCell); - const TopicTableRow = withStyles({ root: { paddingLeft: '4px', @@ -54,8 +49,22 @@ export const UserNotificationSettingsPanel = (props: { settings: NotificationSettings; onChange: (settings: NotificationSettings) => void; originNames?: Record; + topicNames?: Record; }) => { const { settings, onChange } = props; + const [expandedRows, setExpandedRows] = useState>(new Set()); + + const handleRowToggle = (originId: string) => { + setExpandedRows(prevState => { + const newExpandedRows = new Set(prevState); + if (newExpandedRows.has(originId)) { + newExpandedRows.delete(originId); + } else { + newExpandedRows.add(originId); + } + return newExpandedRows; + }); + }; const handleChange = ( channelId: string, originId: string, @@ -104,12 +113,22 @@ export const UserNotificationSettingsPanel = (props: { }; onChange(updatedSettings); }; + const formatName = ( + id: string, + nameMap: Record | undefined, + ) => { + if (nameMap && id in nameMap) { + return nameMap[id]; + } + return capitalize(id.replaceAll(/[-_:]/g, ' ')); + }; const formatOriginName = (originId: string) => { - if (props.originNames && originId in props.originNames) { - return props.originNames[originId]; - } - return capitalize(originId.replaceAll(/[_:]/g, ' ')); + return formatName(originId, props.originNames); + }; + + const formatTopicName = (topicId: string) => { + return formatName(topicId, props.topicNames); }; if (settings.channels.length === 0) { @@ -124,12 +143,15 @@ export const UserNotificationSettingsPanel = (props: { - - Origin {' '} + + + Origin + + Topic - + {settings.channels.map(channel => ( - + {channel.id} @@ -141,7 +163,29 @@ export const UserNotificationSettingsPanel = (props: { {settings.channels.map(channel => channel.origins.flatMap(origin => [ + + {origin.topics && origin.topics.length > 0 && ( + + handleRowToggle(origin.id)} + > + {expandedRows.has(origin.id) ? ( + + ) : ( + + )} + + + )} + {formatOriginName(origin.id)} + * {settings.channels.map(ch => ( ))} , - ...(origin.topics?.map(topic => ( - - - - {topic.id} - - {settings.channels.map(ch => ( - - - , - ) => { - handleChange( - ch.id, - origin.id, + ...(expandedRows.has(origin.id) + ? origin.topics?.map(topic => ( + + + + {formatTopicName(topic.id)} + {settings.channels.map(ch => ( + + - - - ))} - - )) || []), + )} topic from ${formatOriginName(origin.id)}`} + > + , + ) => { + handleChange( + ch.id, + origin.id, + topic.id, + event.target.checked, + ); + }} + /> + + + ))} + + )) || [] + : []), ]), )}