feat: add user specific notification settings
The settings can be customized for each origin and each processor individually. The default Web indicates notifications shown in the Backstage UI. By default, if there are no settings saved in the database, all notifications are enabled for all processors. The origins will populate by time for each user as they receive the first notification from that origin. Processors are shown as their own columns. Later, if it makes sense, allow users to also disable/enable notifications based on notification topic. Signed-off-by: Heikki Hellgren <heikki.hellgren@op.fi>
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
import { createApiRef } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
Notification,
|
||||
NotificationSettings,
|
||||
NotificationSeverity,
|
||||
NotificationStatus,
|
||||
} from '@backstage/plugin-notifications-common';
|
||||
@@ -64,4 +65,10 @@ export interface NotificationsApi {
|
||||
updateNotifications(
|
||||
options: UpdateNotificationsOptions,
|
||||
): Promise<Notification[]>;
|
||||
|
||||
getNotificationSettings(): Promise<NotificationSettings>;
|
||||
|
||||
updateNotificationSettings(
|
||||
settings: NotificationSettings,
|
||||
): Promise<NotificationSettings>;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api';
|
||||
import { ResponseError } from '@backstage/errors';
|
||||
import {
|
||||
Notification,
|
||||
NotificationSettings,
|
||||
NotificationStatus,
|
||||
} from '@backstage/plugin-notifications-common';
|
||||
|
||||
@@ -93,6 +94,20 @@ export class NotificationsClient implements NotificationsApi {
|
||||
});
|
||||
}
|
||||
|
||||
async getNotificationSettings(): Promise<NotificationSettings> {
|
||||
return await this.request<NotificationSettings>('settings');
|
||||
}
|
||||
|
||||
async updateNotificationSettings(
|
||||
settings: NotificationSettings,
|
||||
): Promise<NotificationSettings> {
|
||||
return await this.request<NotificationSettings>('settings', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(settings),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
private async request<T>(path: string, init?: any): Promise<T> {
|
||||
const baseUrl = `${await this.discoveryApi.getBaseUrl('notifications')}/`;
|
||||
const url = new URL(path, baseUrl);
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { ErrorPanel, InfoCard, Progress } from '@backstage/core-components';
|
||||
import { useNotificationsApi } from '../../hooks';
|
||||
import { NotificationSettings } from '@backstage/plugin-notifications-common';
|
||||
import { notificationsApiRef } from '../../api';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { UserNotificationSettingsPanel } from './UserNotificationSettingsPanel';
|
||||
|
||||
/** @public */
|
||||
export const UserNotificationSettingsCard = (props: {
|
||||
originNames?: Record<string, string>;
|
||||
}) => {
|
||||
const [settings, setNotificationSettings] = React.useState<
|
||||
NotificationSettings | undefined
|
||||
>(undefined);
|
||||
|
||||
const client = useApi(notificationsApiRef);
|
||||
const { error, value, loading } = useNotificationsApi(api => {
|
||||
return api.getNotificationSettings();
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !error) {
|
||||
setNotificationSettings(value);
|
||||
}
|
||||
}, [loading, value, error]);
|
||||
|
||||
const onUpdate = (newSettings: NotificationSettings) => {
|
||||
client
|
||||
.updateNotificationSettings(newSettings)
|
||||
.then(updatedSettings => setNotificationSettings(updatedSettings));
|
||||
};
|
||||
|
||||
return (
|
||||
<InfoCard title="Notification settings" variant="gridItem">
|
||||
{loading && <Progress />}
|
||||
{error && <ErrorPanel title="Failed to load settings" error={error} />}
|
||||
{settings && (
|
||||
<UserNotificationSettingsPanel
|
||||
settings={settings}
|
||||
onChange={onUpdate}
|
||||
originNames={props.originNames}
|
||||
/>
|
||||
)}
|
||||
</InfoCard>
|
||||
);
|
||||
};
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
isNotificationsEnabledFor,
|
||||
NotificationSettings,
|
||||
} from '@backstage/plugin-notifications-common';
|
||||
import Table from '@material-ui/core/Table';
|
||||
import MuiTableCell from '@material-ui/core/TableCell';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import TableHead from '@material-ui/core/TableHead';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import TableBody from '@material-ui/core/TableBody';
|
||||
import TableRow from '@material-ui/core/TableRow';
|
||||
import Switch from '@material-ui/core/Switch';
|
||||
import { capitalize } from 'lodash';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
|
||||
const TableCell = withStyles({
|
||||
root: {
|
||||
borderBottom: 'none',
|
||||
},
|
||||
})(MuiTableCell);
|
||||
|
||||
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,
|
||||
enabled: boolean,
|
||||
) => {
|
||||
const updatedSettings = {
|
||||
channels: settings.channels.map(channel => {
|
||||
if (channel.id !== channelId) {
|
||||
return channel;
|
||||
}
|
||||
return {
|
||||
...channel,
|
||||
origins: channel.origins.map(origin => {
|
||||
if (origin.id !== originId) {
|
||||
return origin;
|
||||
}
|
||||
return {
|
||||
...origin,
|
||||
enabled,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}),
|
||||
};
|
||||
onChange(updatedSettings);
|
||||
};
|
||||
|
||||
const formatOriginName = (originId: string) => {
|
||||
if (props.originNames && originId in props.originNames) {
|
||||
return props.originNames[originId];
|
||||
}
|
||||
return capitalize(originId.replaceAll(/[_:]/g, ' '));
|
||||
};
|
||||
|
||||
if (settings.channels.length === 0 || allOrigins.length === 0) {
|
||||
return (
|
||||
<Typography variant="body1">
|
||||
No notification settings available, check back later
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography variant="subtitle1">Origin</Typography>
|
||||
</TableCell>
|
||||
{settings.channels.map(channel => (
|
||||
<TableCell>
|
||||
<Typography variant="subtitle1">{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>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
export { UserNotificationSettingsCard } from './UserNotificationSettingsCard';
|
||||
@@ -15,4 +15,5 @@
|
||||
*/
|
||||
export * from './NotificationsSideBarItem';
|
||||
export * from './NotificationsTable';
|
||||
export * from './UserNotificationSettingsCard';
|
||||
export type { NotificationsPageProps } from './NotificationsPage';
|
||||
|
||||
Reference in New Issue
Block a user