feat: make notification page filters work

Signed-off-by: Heikki Hellgren <heikki.hellgren@op.fi>
This commit is contained in:
Heikki Hellgren
2023-12-15 14:41:04 +02:00
parent f24a0c1f6a
commit 92c327211a
11 changed files with 100 additions and 35 deletions
@@ -22,14 +22,14 @@ export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
// TODO: Remove this test code
// setInterval(() => {
// env.notificationService.send(
// 'user:default/guest',
// 'Test',
// 'This is test notification',
// '/catalog',
// );
// }, 60000);
setInterval(() => {
env.notificationService.send(
'user:default/guest',
'Test',
'This is test notification',
'/catalog',
);
}, 60000);
return await createRouter({
logger: env.logger,
@@ -17,7 +17,10 @@ import { errorHandler } from '@backstage/backend-common';
import express, { Request } from 'express';
import Router from 'express-promise-router';
import { Logger } from 'winston';
import { NotificationService } from '@backstage/plugin-notifications-node';
import {
NotificationGetOptions,
NotificationService,
} from '@backstage/plugin-notifications-node';
import { IdentityApi } from '@backstage/plugin-auth-node';
/** @public */
@@ -50,7 +53,14 @@ export async function createRouter(
router.get('/notifications', async (req, res) => {
const user = await getUser(req);
const notifications = await store.getNotifications({ user_ref: user });
const opts: NotificationGetOptions = {
user_ref: user,
};
if (req.query.type) {
opts.type = req.query.type as any;
}
const notifications = await store.getNotifications(opts);
res.send(notifications);
});
@@ -21,4 +21,7 @@ export type NotificationStatus = {
unread: number;
read: number;
};
// @public (undocumented)
export type NotificationType = 'read' | 'unread' | 'saved';
```
@@ -14,6 +14,9 @@
* limitations under the License.
*/
/** @public */
export type NotificationType = 'read' | 'unread' | 'saved';
/** @public */
export type Notification = {
id: string;
+2
View File
@@ -5,6 +5,7 @@
```ts
import { Notification as Notification_2 } from '@backstage/plugin-notifications-common';
import { NotificationStatus } from '@backstage/plugin-notifications-common';
import { NotificationType } from '@backstage/plugin-notifications-common';
import { PluginDatabaseManager } from '@backstage/backend-common';
import { PluginEndpointDiscovery } from '@backstage/backend-common';
@@ -32,6 +33,7 @@ export class DatabaseNotificationsStore implements NotificationsStore {
// @public (undocumented)
export type NotificationGetOptions = {
user_ref: string;
type?: NotificationType;
};
// @public (undocumented)
@@ -55,15 +55,22 @@ export class DatabaseNotificationsStore implements NotificationsStore {
return typeof val === 'string' ? Number.parseInt(val, 10) : val ?? 0;
};
private getNotificationsBaseQuery = (options: NotificationGetOptions) => {
const { user_ref, type } = options;
const query = this.db('notifications').where('userRef', user_ref);
if (type === 'unread') {
query.whereNull('read');
} else if (type === 'read') {
query.whereNotNull('read');
}
// TODO: Saved
return query;
};
async getNotifications(options: NotificationGetOptions) {
const { user_ref } = options;
const notificationQuery = this.db('notifications').where(
'userRef',
user_ref,
);
const notificationQuery = this.getNotificationsBaseQuery(options);
const notifications = await notificationQuery.select('*');
return notifications;
}
@@ -72,12 +79,7 @@ export class DatabaseNotificationsStore implements NotificationsStore {
}
async getStatus(options: NotificationGetOptions) {
const { user_ref } = options;
const notificationQuery = this.db('notifications').where(
'userRef',
user_ref,
);
const notificationQuery = this.getNotificationsBaseQuery(options);
const unreadQuery = await notificationQuery
.clone()
.whereNull('read')
@@ -17,11 +17,13 @@
import {
Notification,
NotificationStatus,
NotificationType,
} from '@backstage/plugin-notifications-common';
/** @public */
export type NotificationGetOptions = {
user_ref: string;
type?: NotificationType;
};
/** @public */
+12 -2
View File
@@ -12,13 +12,21 @@ import { FetchApi } from '@backstage/core-plugin-api';
import { JSX as JSX_2 } from 'react';
import { Notification as Notification_2 } from '@backstage/plugin-notifications-common';
import { NotificationStatus } from '@backstage/plugin-notifications-common';
import { NotificationType } from '@backstage/plugin-notifications-common';
import { default as React_2 } from 'react';
import { RouteRef } from '@backstage/core-plugin-api';
// @public (undocumented)
export type GetNotificationsOptions = {
type?: NotificationType;
};
// @public (undocumented)
export interface NotificationsApi {
// (undocumented)
getNotifications(): Promise<Notification_2[]>;
getNotifications(
options?: GetNotificationsOptions,
): Promise<Notification_2[]>;
// (undocumented)
getStatus(): Promise<NotificationStatus>;
}
@@ -30,7 +38,9 @@ export const notificationsApiRef: ApiRef<NotificationsApi>;
export class NotificationsClient implements NotificationsApi {
constructor(options: { discoveryApi: DiscoveryApi; fetchApi: FetchApi });
// (undocumented)
getNotifications(): Promise<Notification_2[]>;
getNotifications(
options?: GetNotificationsOptions,
): Promise<Notification_2[]>;
// (undocumented)
getStatus(): Promise<NotificationStatus>;
}
@@ -17,6 +17,7 @@ import { createApiRef } from '@backstage/core-plugin-api';
import {
Notification,
NotificationStatus,
NotificationType,
} from '@backstage/plugin-notifications-common';
/** @public */
@@ -24,11 +25,17 @@ export const notificationsApiRef = createApiRef<NotificationsApi>({
id: 'plugin.notifications.service',
});
/** @public */
export type GetNotificationsOptions = {
type?: NotificationType;
};
/** @public */
export interface NotificationsApi {
getNotifications(): Promise<Notification[]>;
getNotifications(options?: GetNotificationsOptions): Promise<Notification[]>;
getStatus(): Promise<NotificationStatus>;
// TODO: Mark as read/unread by notification id(s)
// TODO: Mark as saved/unsaved by notification id(s)
}
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { NotificationsApi } from './NotificationsApi';
import { GetNotificationsOptions, NotificationsApi } from './NotificationsApi';
import { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api';
import { ResponseError } from '@backstage/errors';
import {
@@ -34,8 +34,17 @@ export class NotificationsClient implements NotificationsApi {
this.fetchApi = options.fetchApi;
}
async getNotifications(): Promise<Notification[]> {
return await this.get<Notification[]>('notifications');
async getNotifications(
options?: GetNotificationsOptions,
): Promise<Notification[]> {
const queryString = new URLSearchParams();
if (options?.type) {
queryString.append('type', options.type);
}
const urlSegment = `notifications?${queryString}`;
return await this.get<Notification[]>(urlSegment);
}
async getStatus(): Promise<NotificationStatus> {
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import React from 'react';
import React, { useState } from 'react';
import {
Content,
ErrorPanel,
@@ -32,6 +32,7 @@ import {
import Bookmark from '@material-ui/icons/Bookmark';
import Check from '@material-ui/icons/Check';
import Inbox from '@material-ui/icons/Inbox';
import { NotificationType } from '@backstage/plugin-notifications-common';
const useStyles = makeStyles(_theme => ({
filterButton: {
@@ -41,32 +42,48 @@ const useStyles = makeStyles(_theme => ({
}));
export const NotificationsPage = () => {
const [type, setType] = useState<NotificationType>('unread');
const {
loading: _loading,
error,
value,
retry: _retry,
} = useNotificationsApi(api => api.getNotifications());
} = useNotificationsApi(api => api.getNotifications({ type }), [type]);
const styles = useStyles();
if (error) {
return <ErrorPanel error={new Error('Failed to load notifications')} />;
}
// TODO: Make the filter buttons work
// TODO: Add signals listener and refresh data on message
return (
<PageWithHeader title="Notifications" themeId="tool">
<Content>
<Grid container>
<Grid item xs={2}>
<Button className={styles.filterButton} startIcon={<Inbox />}>
<Button
className={styles.filterButton}
startIcon={<Inbox />}
variant={type === 'unread' ? 'contained' : 'text'}
onClick={() => setType('unread')}
>
Inbox
</Button>
<Button className={styles.filterButton} startIcon={<Check />}>
<Button
className={styles.filterButton}
startIcon={<Check />}
variant={type === 'read' ? 'contained' : 'text'}
onClick={() => setType('read')}
>
Done
</Button>
<Button className={styles.filterButton} startIcon={<Bookmark />}>
<Button
className={styles.filterButton}
startIcon={<Bookmark />}
variant={type === 'saved' ? 'contained' : 'text'}
onClick={() => setType('saved')}
>
Saved
</Button>
</Grid>