wip; DRAFT for visibility into changes
Signed-off-by: Billy Stalnaker <bstalnaker@roadie.com>
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 250 KiB |
+1
-1
@@ -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',
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -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;
|
||||
}[];
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
+118
-41
@@ -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 }}
|
||||
|
||||
Reference in New Issue
Block a user