rm NODE_OPTIONS;

Adds topicNames prop
Adds collapsible row for topics
Refactor getNotificationSettings for readability

Signed-off-by: Billy Stalnaker <bstalnaker@roadie.com>
This commit is contained in:
Billy Stalnaker
2025-03-24 15:02:54 -04:00
parent 5676296812
commit 8637567d53
5 changed files with 182 additions and 102 deletions
+1 -1
View File
@@ -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",
@@ -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: {
+27 -12
View File
@@ -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[];
};
@@ -25,6 +25,7 @@ import { UserNotificationSettingsPanel } from './UserNotificationSettingsPanel';
/** @public */
export const UserNotificationSettingsCard = (props: {
originNames?: Record<string, string>;
topicNames?: Record<string, string>;
}) => {
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}
/>
)}
</InfoCard>
@@ -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<string, string>;
topicNames?: Record<string, string>;
}) => {
const { settings, onChange } = props;
const [expandedRows, setExpandedRows] = useState<Set<string>>(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<string, string> | 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: {
<Table>
<TableHead>
<TableRow>
<CenterContentsTableCell>
<Typography variant="subtitle1">Origin</Typography> <ChevronRight />{' '}
<TableCell />
<TableCell>
<Typography variant="subtitle1">Origin</Typography>
</TableCell>
<TableCell>
<Typography variant="subtitle1">Topic</Typography>
</CenterContentsTableCell>
</TableCell>
{settings.channels.map(channel => (
<TableCell>
<TableCell key={channel.id}>
<Typography variant="subtitle1" align="center">
{channel.id}
</Typography>
@@ -141,7 +163,29 @@ export const UserNotificationSettingsPanel = (props: {
{settings.channels.map(channel =>
channel.origins.flatMap(origin => [
<TableRow key={`${channel.id}-${origin.id}`}>
<TableCell>
{origin.topics && origin.topics.length > 0 && (
<Tooltip
title={`Show Topics for the ${formatOriginName(
origin.id,
)} origin`}
>
<IconButton
aria-label="expand row"
size="small"
onClick={() => handleRowToggle(origin.id)}
>
{expandedRows.has(origin.id) ? (
<KeyboardArrowUpIcon />
) : (
<KeyboardArrowDownIcon />
)}
</IconButton>
</Tooltip>
)}
</TableCell>
<TableCell>{formatOriginName(origin.id)}</TableCell>
<TableCell>*</TableCell>
{settings.channels.map(ch => (
<TableCell key={ch.id} align="center">
<Tooltip
@@ -171,44 +215,45 @@ export const UserNotificationSettingsPanel = (props: {
</TableCell>
))}
</TableRow>,
...(origin.topics?.map(topic => (
<TopicTableRow key={`${origin.id}-${topic.id}`}>
<CenterContentsTableCell>
<ChevronRight />
{topic.id}
</CenterContentsTableCell>
{settings.channels.map(ch => (
<TableCell key={`${ch.id}-${topic.id}`} align="center">
<Tooltip
title={`Enable or disable ${channel.id.toLocaleLowerCase(
'en-US',
)} notifications for the ${
topic.id
} topic from ${formatOriginName(origin.id)}`}
>
<Switch
checked={isNotificationsEnabledFor(
settings,
ch.id,
origin.id,
topic.id,
)}
onChange={(
event: React.ChangeEvent<HTMLInputElement>,
) => {
handleChange(
ch.id,
origin.id,
...(expandedRows.has(origin.id)
? origin.topics?.map(topic => (
<TopicTableRow key={`${origin.id}-${topic.id}`}>
<TableCell />
<TableCell />
<TableCell>{formatTopicName(topic.id)}</TableCell>
{settings.channels.map(ch => (
<TableCell key={`${ch.id}-${topic.id}`} align="center">
<Tooltip
title={`Enable or disable ${channel.id.toLocaleLowerCase(
'en-US',
)} notifications for the ${formatTopicName(
topic.id,
event.target.checked,
);
}}
/>
</Tooltip>
</TableCell>
))}
</TopicTableRow>
)) || []),
)} topic from ${formatOriginName(origin.id)}`}
>
<Switch
checked={isNotificationsEnabledFor(
settings,
ch.id,
origin.id,
topic.id,
)}
onChange={(
event: React.ChangeEvent<HTMLInputElement>,
) => {
handleChange(
ch.id,
origin.id,
topic.id,
event.target.checked,
);
}}
/>
</Tooltip>
</TableCell>
))}
</TopicTableRow>
)) || []
: []),
]),
)}
</TableBody>