wip; DRAFT for visibility into changes

Signed-off-by: Billy Stalnaker <bstalnaker@roadie.com>
This commit is contained in:
Billy Stalnaker
2025-03-19 09:28:14 -04:00
parent ad41ba7dc6
commit 5676296812
14 changed files with 304 additions and 63 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 250 KiB

+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": "yarn workspaces foreach -A --include example-backend --include example-app --parallel --jobs unlimited -v -i run start",
"dev": "NODE_OPTIONS=--no-node-snapshot 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",
@@ -0,0 +1,43 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
exports.up = async function up(knex) {
await knex.schema.alterTable('user_settings', table => {
table.string('topic', 255).nullable().after('origin');
});
await knex.raw(
'ALTER TABLE user_settings DROP CONSTRAINT user_settings_unique_idx',
);
await knex.schema.alterTable('user_settings', table => {
table.unique(['user', 'channel', 'origin', 'topic'], {
indexName: 'user_settings_unique_idx',
});
});
};
exports.down = async function down(knex) {
await knex.raw(
'ALTER TABLE user_settings DROP CONSTRAINT user_settings_unique_idx',
);
await knex.schema.alterTable('user_settings', table => {
table.dropColumn('topic');
table.unique(['user', 'channel', 'origin'], {
indexName: 'user_settings_unique_idx',
});
});
};
+2 -1
View File
@@ -68,9 +68,10 @@
| `channel` | `character varying` | false | 255 | - |
| `enabled` | `boolean` | false | - | `true` |
| `origin` | `character varying` | false | 255 | - |
| `topic` | `character varying` | true | 255 | - |
| `user` | `character varying` | false | 255 | - |
### Indices
- `user_settings_unique_idx` (`user`, `channel`, `origin`) unique
- `user_settings_unique_idx` (`user`, `channel`, `origin`, `topic`) unique
- `user_settings_user_idx` (`user`)
@@ -161,8 +161,14 @@ const notificationSettings: NotificationSettings = {
enabled: true,
},
{
id: 'plugin-test2',
enabled: false,
id: 'abcd-origin',
enabled: true,
topics: [
{
id: 'efgh-topic',
enabled: false,
},
],
},
],
},
@@ -169,10 +169,31 @@ export class DatabaseNotificationsStore implements NotificationsStore {
});
chan = acc.channels[acc.channels.length - 1];
}
chan.origins.push({
id: row.origin,
enabled: Boolean(row.enabled),
});
let origin = chan.origins.find(
(ori: { id: string }) => ori.id === row.origin,
);
if (!origin) {
origin = {
id: row.origin,
enabled: true,
topics: [],
};
chan.origins.push(origin);
}
if (row.topic === null) {
origin.enabled = Boolean(row.enabled);
} else {
let topic = origin.topics.find(
(top: { id: string }) => top.id === row.topic,
);
if (!topic) {
topic = {
id: row.topic,
enabled: Boolean(row.enabled),
};
origin.topics.push(topic);
}
}
return acc;
},
{ channels: [] },
@@ -518,10 +539,26 @@ export class DatabaseNotificationsStore implements NotificationsStore {
return { origins: rows.map(row => row.origin) };
}
async getUserNotificationTopics(options: {
user: string;
}): Promise<{ topics: { origin: string; topic: string }[] }> {
const rows: { topic: string; origin: string }[] =
await this.db<NotificationRowType>('notification')
.where('user', options.user)
.select('topic', 'origin')
.whereNotNull('topic')
.distinct();
return {
topics: rows.map(row => ({ origin: row.origin, topic: row.topic })),
};
}
async getNotificationSettings(options: {
user: string;
origin?: string;
channel?: string;
topic?: string;
}): Promise<NotificationSettings> {
const settingsQuery = this.db<UserSettingsRowType>('user_settings').where(
'user',
@@ -534,6 +571,10 @@ export class DatabaseNotificationsStore implements NotificationsStore {
if (options.channel) {
settingsQuery.where('channel', options.channel);
}
if (options.topic) {
settingsQuery.where('topic', options.topic);
}
const settings = await settingsQuery.select();
return this.mapToNotificationSettings(settings);
}
@@ -546,16 +587,29 @@ export class DatabaseNotificationsStore implements NotificationsStore {
user: string;
channel: string;
origin: string;
topic: string | null;
enabled: boolean;
}[] = [];
options.settings.channels.map(channel => {
channel.origins.map(origin => {
options.settings.channels.forEach(channel => {
channel.origins.forEach(origin => {
rows.push({
user: options.user,
channel: channel.id,
origin: origin.id,
topic: null,
enabled: origin.enabled,
});
origin.topics?.forEach(topic => {
rows.push({
user: options.user,
channel: channel.id,
origin: origin.id,
topic: topic.id,
enabled: topic.enabled,
});
});
});
});
@@ -95,21 +95,42 @@ export async function createRouter(
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(channel => {
const channelSettings = settings.channels.find(c => c.id === channel);
if (channelSettings) {
return channelSettings;
}
channels: channels.map(channelId => {
return {
id: channel,
origins: origins.map(origin => ({
id: origin,
enabled: true,
})),
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,
};
}),
};
}),
};
}),
};
@@ -120,9 +141,15 @@ export async function createRouter(
user: string;
channel: string;
origin: string;
topic: string | null;
}) => {
const settings = await getNotificationSettings(opts.user);
return isNotificationsEnabledFor(settings, opts.channel, opts.origin);
return isNotificationsEnabledFor(
settings,
opts.channel,
opts.origin,
opts.topic,
);
};
const filterProcessors = async (
@@ -139,6 +166,7 @@ export async function createRouter(
user,
origin,
channel: processor.getName(),
topic: payload.topic ?? null,
});
if (!enabled) {
continue;
@@ -496,6 +524,7 @@ export async function createRouter(
user,
channel: WEB_NOTIFICATION_CHANNEL,
origin: userNotification.origin,
topic: userNotification.payload.topic ?? null,
});
let ret = notification;
@@ -15,6 +15,7 @@ export const isNotificationsEnabledFor: (
settings: NotificationSettings,
channelId: string,
originId: string,
topicId: string | null,
) => boolean;
// @public (undocumented)
@@ -67,6 +68,10 @@ export type NotificationSettings = {
origins: {
id: string;
enabled: boolean;
topics?: {
id: string;
enabled: boolean;
}[];
}[];
}[];
};
@@ -135,6 +135,10 @@ export type NotificationSettings = {
origins: {
id: string;
enabled: boolean;
topics?: {
id: string;
enabled: boolean;
}[];
}[];
}[];
};
+9 -1
View File
@@ -20,6 +20,7 @@ export const isNotificationsEnabledFor = (
settings: NotificationSettings,
channelId: string,
originId: string,
topicId: string | null,
) => {
const channel = settings.channels.find(c => c.id === channelId);
if (!channel) {
@@ -30,5 +31,12 @@ export const isNotificationsEnabledFor = (
if (!origin) {
return true;
}
return origin.enabled;
if (topicId === null) {
return origin.enabled;
}
const topic = origin.topics?.find(t => t.id === topicId);
if (!topic) {
return origin.enabled;
}
return topic.enabled;
};
@@ -19,6 +19,7 @@ import {
isNotificationsEnabledFor,
NotificationSettings,
} from '@backstage/plugin-notifications-common';
import ChevronRight from '@material-ui/icons/ChevronRight';
import Table from '@material-ui/core/Table';
import MuiTableCell from '@material-ui/core/TableCell';
import { withStyles } from '@material-ui/core/styles';
@@ -36,23 +37,29 @@ const TableCell = withStyles({
},
})(MuiTableCell);
const CenterContentsTableCell = withStyles({
root: {
display: 'flex',
alignItems: 'center',
},
})(MuiTableCell);
const TopicTableRow = withStyles({
root: {
paddingLeft: '4px',
},
})(TableRow);
export const UserNotificationSettingsPanel = (props: {
settings: NotificationSettings;
onChange: (settings: NotificationSettings) => void;
originNames?: Record<string, string>;
}) => {
const { settings, onChange } = props;
const allOrigins = [
...new Set(
settings.channels.flatMap(channel =>
channel.origins.map(origin => origin.id),
),
),
];
const handleChange = (
channelId: string,
originId: string,
topicId: string | null,
enabled: boolean,
) => {
const updatedSettings = {
@@ -66,9 +73,30 @@ export const UserNotificationSettingsPanel = (props: {
if (origin.id !== originId) {
return origin;
}
if (topicId === null) {
return {
...origin,
enabled,
topics:
origin.topics?.map(topic => {
return { ...topic, enabled };
}) ?? [],
};
}
return {
...origin,
enabled,
topics:
origin.topics?.map(topic => {
if (topic.id === topicId) {
return {
...topic,
enabled: origin.enabled ? enabled : origin.enabled,
};
}
return topic;
}) ?? [],
};
}),
};
@@ -84,7 +112,7 @@ export const UserNotificationSettingsPanel = (props: {
return capitalize(originId.replaceAll(/[_:]/g, ' '));
};
if (settings.channels.length === 0 || allOrigins.length === 0) {
if (settings.channels.length === 0) {
return (
<Typography variant="body1">
No notification settings available, check back later
@@ -96,44 +124,93 @@ export const UserNotificationSettingsPanel = (props: {
<Table>
<TableHead>
<TableRow>
<TableCell>
<Typography variant="subtitle1">Origin</Typography>
</TableCell>
<CenterContentsTableCell>
<Typography variant="subtitle1">Origin</Typography> <ChevronRight />{' '}
<Typography variant="subtitle1">Topic</Typography>
</CenterContentsTableCell>
{settings.channels.map(channel => (
<TableCell>
<Typography variant="subtitle1">{channel.id}</Typography>
<Typography variant="subtitle1" align="center">
{channel.id}
</Typography>
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{allOrigins.map(origin => (
<TableRow>
<TableCell>{formatOriginName(origin)}</TableCell>
{settings.channels.map(channel => (
<TableCell>
<Tooltip
title={`Enable or disable ${channel.id.toLocaleLowerCase(
'en-US',
)} notifications from ${formatOriginName(
origin,
).toLocaleLowerCase('en-US')}`}
>
<Switch
checked={isNotificationsEnabledFor(
settings,
channel.id,
origin,
)}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
handleChange(channel.id, origin, event.target.checked);
}}
/>
</Tooltip>
</TableCell>
))}
</TableRow>
))}
{settings.channels.map(channel =>
channel.origins.flatMap(origin => [
<TableRow key={`${channel.id}-${origin.id}`}>
<TableCell>{formatOriginName(origin.id)}</TableCell>
{settings.channels.map(ch => (
<TableCell key={ch.id} align="center">
<Tooltip
title={`Enable or disable ${channel.id.toLocaleLowerCase(
'en-US',
)} notifications from ${formatOriginName(origin.id)}`}
>
<Switch
checked={isNotificationsEnabledFor(
settings,
ch.id,
origin.id,
null,
)}
onChange={(
event: React.ChangeEvent<HTMLInputElement>,
) => {
handleChange(
ch.id,
origin.id,
null,
event.target.checked,
);
}}
/>
</Tooltip>
</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,
topic.id,
event.target.checked,
);
}}
/>
</Tooltip>
</TableCell>
))}
</TopicTableRow>
)) || []),
]),
)}
</TableBody>
</Table>
);
@@ -17,6 +17,7 @@ export function createSendNotificationAction(options: {
recipients: string;
entityRefs?: string[];
title: string;
topic?: string;
info?: string;
link?: string;
severity?: NotificationSeverity;
@@ -35,6 +35,7 @@ export function createSendNotificationAction(options: {
recipients: string;
entityRefs?: string[];
title: string;
topic?: string;
info?: string;
link?: string;
severity?: NotificationSeverity;
@@ -86,6 +87,11 @@ export function createSendNotificationAction(options: {
description: `Notification severity`,
enum: ['low', 'normal', 'high', 'critical'],
},
topic: {
title: 'Topic',
description: 'Notification topic',
type: 'string',
},
scope: {
title: 'Scope',
description: 'Notification scope',
@@ -108,6 +114,7 @@ export function createSendNotificationAction(options: {
info,
link,
severity,
topic,
scope,
optional,
} = ctx.input;
@@ -129,6 +136,7 @@ export function createSendNotificationAction(options: {
description: info,
link,
severity,
topic,
scope,
};
@@ -50,6 +50,10 @@ spec:
- normal
- high
- critical
topic:
title: Topic
type: string
description: Notification topic
scope:
title: Scope
type: string
@@ -66,4 +70,5 @@ spec:
description: ${{ parameters.description }}
link: ${{ parameters.link }}
severity: ${{ parameters.severity }}
topic: ${{ parameters.topic }}
scope: ${{ parameters.scope }}