diff --git a/.changeset/five-hats-accept.md b/.changeset/five-hats-accept.md new file mode 100644 index 0000000000..11ee4ff276 --- /dev/null +++ b/.changeset/five-hats-accept.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-notifications-backend': patch +'@backstage/plugin-notifications': patch +--- + +The Notifications can be newly filtered based on the Created Date. diff --git a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts index c3e8ec95ed..e0af93a1d6 100644 --- a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts +++ b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts @@ -188,6 +188,33 @@ describe.each(databases.eachSupportedId())( expect(notifications.length).toBe(1); expect(notifications.at(0)?.id).toEqual(id1); }); + + it('should filter notifications based on created date', async () => { + const id1 = uuid(); + const id2 = uuid(); + await insertNotification({ + id: id1, + ...testNotification, + created: new Date(Date.now() - 1 * 60 * 60 * 1000 /* an hour ago */), + }); + await insertNotification({ + id: id2, + ...testNotification, + payload: { + severity: 'normal', + title: 'Please find me', + }, + created: new Date() /* now */, + }); + await insertNotification({ id: uuid(), ...otherUserNotification }); + + const notifications = await storage.getNotifications({ + user, + createdAfter: new Date(Date.now() - 5 * 60 * 1000 /* 5mins */), + }); + expect(notifications.length).toBe(1); + expect(notifications.at(0)?.id).toEqual(id2); + }); }); describe('getStatus', () => { diff --git a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts index 6870ece4be..9b654cfd04 100644 --- a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts +++ b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts @@ -98,6 +98,9 @@ export class DatabaseNotificationsStore implements NotificationsStore { options: NotificationGetOptions | NotificationModifyOptions, ) => { const { user } = options; + const isSQLite = this.db.client.config.client.includes('sqlite3'); + // const isPsql = this.db.client.config.client.includes('pg'); + const query = this.db('notification').where('user', user); if (options.sort !== undefined && options.sort !== null) { @@ -106,6 +109,22 @@ export class DatabaseNotificationsStore implements NotificationsStore { query.orderBy('created', options.sortOrder ?? 'desc'); } + if (options.createdAfter) { + if (isSQLite) { + query.where( + 'notification.created', + '>=', + options.createdAfter.valueOf(), + ); + } else { + query.where( + 'notification.created', + '>=', + options.createdAfter.toISOString(), + ); + } + } + if (options.limit) { query.limit(options.limit); } diff --git a/plugins/notifications-backend/src/database/NotificationsStore.ts b/plugins/notifications-backend/src/database/NotificationsStore.ts index 285f609446..0a7df92f03 100644 --- a/plugins/notifications-backend/src/database/NotificationsStore.ts +++ b/plugins/notifications-backend/src/database/NotificationsStore.ts @@ -31,6 +31,7 @@ export type NotificationGetOptions = { sortOrder?: 'asc' | 'desc'; read?: boolean; saved?: boolean; + createdAfter?: Date; }; /** @internal */ diff --git a/plugins/notifications-backend/src/service/router.ts b/plugins/notifications-backend/src/service/router.ts index d1bf281720..78d44dcd44 100644 --- a/plugins/notifications-backend/src/service/router.ts +++ b/plugins/notifications-backend/src/service/router.ts @@ -204,6 +204,13 @@ export async function createRouter( opts.read = false; // or keep undefined } + if (req.query.created_after) { + const sinceEpoch = Date.parse(req.query.created_after.toString()); + if (isNaN(sinceEpoch)) { + throw new InputError('Unexpected date format'); + } + opts.createdAfter = new Date(sinceEpoch); + } const notifications = await store.getNotifications(opts); res.send(notifications); diff --git a/plugins/notifications/api-report.md b/plugins/notifications/api-report.md index 41b82f62ab..666daebdfe 100644 --- a/plugins/notifications/api-report.md +++ b/plugins/notifications/api-report.md @@ -21,6 +21,7 @@ export type GetNotificationsOptions = { limit?: number; search?: string; read?: boolean; + createdAfter?: Date; }; // @public (undocumented) diff --git a/plugins/notifications/src/api/NotificationsApi.ts b/plugins/notifications/src/api/NotificationsApi.ts index 0125cc6a1e..4a1c792012 100644 --- a/plugins/notifications/src/api/NotificationsApi.ts +++ b/plugins/notifications/src/api/NotificationsApi.ts @@ -30,6 +30,7 @@ export type GetNotificationsOptions = { limit?: number; search?: string; read?: boolean; + createdAfter?: Date; }; /** @public */ diff --git a/plugins/notifications/src/api/NotificationsClient.test.ts b/plugins/notifications/src/api/NotificationsClient.test.ts index 09b5e3647c..daf9e71232 100644 --- a/plugins/notifications/src/api/NotificationsClient.test.ts +++ b/plugins/notifications/src/api/NotificationsClient.test.ts @@ -60,7 +60,7 @@ describe('NotificationsClient', () => { server.use( rest.get(`${mockBaseUrl}/`, (req, res, ctx) => { expect(req.url.search).toBe( - '?limit=10&offset=0&search=find+me&read=true', + '?limit=10&offset=0&search=find+me&read=true&created_after=1970-01-01T00%3A00%3A00.005Z', ); return res(ctx.json(expectedResp)); }), @@ -70,6 +70,21 @@ describe('NotificationsClient', () => { offset: 0, search: 'find me', read: true, + createdAfter: new Date(5), + }); + expect(response).toEqual(expectedResp); + }); + + it('should omit unselected fetch options', async () => { + server.use( + rest.get(`${mockBaseUrl}/`, (req, res, ctx) => { + expect(req.url.search).toBe('?limit=10'); + return res(ctx.json(expectedResp)); + }), + ); + const response = await client.getNotifications({ + limit: 10, + // do not put more options here }); expect(response).toEqual(expectedResp); }); diff --git a/plugins/notifications/src/api/NotificationsClient.ts b/plugins/notifications/src/api/NotificationsClient.ts index 03f9c406a6..1013497b3d 100644 --- a/plugins/notifications/src/api/NotificationsClient.ts +++ b/plugins/notifications/src/api/NotificationsClient.ts @@ -54,7 +54,9 @@ export class NotificationsClient implements NotificationsApi { if (options?.read !== undefined) { queryString.append('read', options.read ? 'true' : 'false'); } - + if (options?.createdAfter !== undefined) { + queryString.append('created_after', options.createdAfter.toISOString()); + } const urlSegment = `?${queryString}`; return await this.request(urlSegment); diff --git a/plugins/notifications/src/components/NotificationsFilters/NotificationsFilters.tsx b/plugins/notifications/src/components/NotificationsFilters/NotificationsFilters.tsx index ac4b02307a..4645f46249 100644 --- a/plugins/notifications/src/components/NotificationsFilters/NotificationsFilters.tsx +++ b/plugins/notifications/src/components/NotificationsFilters/NotificationsFilters.tsx @@ -28,12 +28,13 @@ import { export type NotificationsFiltersProps = { unreadOnly?: boolean; onUnreadOnlyChanged: (checked: boolean | undefined) => void; - // createdAfter?: string; + createdAfter?: string; + onCreatedAfterChanged: (value: string) => void; + // sorting?: { // orderBy: GetNotificationsOrderByEnum; // orderByDirec: GetNotificationsOrderByDirecEnum; // }; - // onCreatedAfterChanged: (value: string) => void; // setSorting: ({ // orderBy, // orderByDirec, @@ -43,22 +44,22 @@ export type NotificationsFiltersProps = { // }) => void; }; -// export const CreatedAfterOptions: { -// [key: string]: { label: string; getDate: () => Date }; -// } = { -// last24h: { -// label: 'Last 24h', -// getDate: () => new Date(Date.now() - 24 * 3600 * 1000), -// }, -// lastWeek: { -// label: 'Last week', -// getDate: () => new Date(Date.now() - 7 * 24 * 3600 * 1000), -// }, -// all: { -// label: 'Any time', -// getDate: () => new Date(0), -// }, -// }; +export const CreatedAfterOptions: { + [key: string]: { label: string; getDate: () => Date }; +} = { + last24h: { + label: 'Last 24h', + getDate: () => new Date(Date.now() - 24 * 3600 * 1000), + }, + lastWeek: { + label: 'Last week', + getDate: () => new Date(Date.now() - 7 * 24 * 3600 * 1000), + }, + all: { + label: 'Any time', + getDate: () => new Date(0), + }, +}; // export const SortByOptions: { // [key: string]: { @@ -108,20 +109,20 @@ export type NotificationsFiltersProps = { // }; export const NotificationsFilters = ({ - unreadOnly, - // createdAfter, // sorting, - // onCreatedAfterChanged, + // setSorting, + unreadOnly, onUnreadOnlyChanged, -}: // setSorting, -NotificationsFiltersProps) => { + createdAfter, + onCreatedAfterChanged, +}: NotificationsFiltersProps) => { // const sortBy = getSortBy(sorting); - // const handleOnCreatedAfterChanged = ( - // event: React.ChangeEvent<{ name?: string; value: unknown }>, - // ) => { - // onCreatedAfterChanged(event.target.value as string); - // }; + const handleOnCreatedAfterChanged = ( + event: React.ChangeEvent<{ name?: string; value: unknown }>, + ) => { + onCreatedAfterChanged(event.target.value as string); + }; const handleOnUnreadOnlyChanged = ( event: React.ChangeEvent<{ name?: string; value: unknown }>, @@ -169,7 +170,6 @@ NotificationsFiltersProps) => { - {/* TODO: extend BE to support following: @@ -190,6 +190,8 @@ NotificationsFiltersProps) => { + + {/* Sort by diff --git a/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx b/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx index 43a402ac73..1f8b141608 100644 --- a/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx +++ b/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx @@ -20,11 +20,15 @@ import { PageWithHeader, ResponseErrorPanel, } from '@backstage/core-components'; -import { NotificationsTable } from '../NotificationsTable'; -import { useNotificationsApi } from '../../hooks'; import { Grid } from '@material-ui/core'; import { useSignal } from '@backstage/plugin-signals-react'; -import { NotificationsFilters } from '../NotificationsFilters'; + +import { NotificationsTable } from '../NotificationsTable'; +import { useNotificationsApi } from '../../hooks'; +import { + CreatedAfterOptions, + NotificationsFilters, +} from '../NotificationsFilters'; import { GetNotificationsOptions } from '../../api'; export const NotificationsPage = () => { @@ -32,6 +36,7 @@ export const NotificationsPage = () => { const { lastSignal } = useSignal('notifications'); const [unreadOnly, setUnreadOnly] = React.useState(true); const [containsText, setContainsText] = React.useState(); + const [createdAfter, setCreatedAfter] = React.useState('lastWeek'); const { error, value, retry, loading } = useNotificationsApi( // TODO: add pagination and other filters @@ -40,9 +45,15 @@ export const NotificationsPage = () => { if (unreadOnly !== undefined) { options.read = !unreadOnly; } + + const createdAfterDate = CreatedAfterOptions[createdAfter].getDate(); + if (createdAfterDate.valueOf() > 0) { + options.createdAfter = createdAfterDate; + } + return api.getNotifications(options); }, - [containsText, unreadOnly], + [containsText, unreadOnly, createdAfter], ); useEffect(() => { @@ -72,10 +83,10 @@ export const NotificationsPage = () => {