diff --git a/.changeset/kind-owls-prove.md b/.changeset/kind-owls-prove.md new file mode 100644 index 0000000000..b07a189c7f --- /dev/null +++ b/.changeset/kind-owls-prove.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-notifications-backend': minor +'@backstage/plugin-notifications': minor +--- + +notifications can be newly sorted by list of predefined options diff --git a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts index 0ee5958713..db5dac8c4b 100644 --- a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts +++ b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts @@ -81,6 +81,7 @@ describe.each(databases.eachSupportedId())( user: notification.user, origin: notification.origin, created: notification.created, + topic: notification.payload?.topic, link: notification.payload?.link, title: notification.payload?.title, severity: notification.payload?.severity, @@ -288,6 +289,109 @@ describe.each(databases.eachSupportedId())( expect(allUserNotificationsPageTwo.at(1)?.id).toEqual(id4); expect(allUserNotificationsPageTwo.at(2)?.id).toEqual(id3); }); + + it('should sort result', async () => { + const now = Date.now(); + const timeDelay = 5 * 1000; /* 5 secs */ + + await insertNotification({ + id: id1, + user, + origin: 'Y', + payload: { + title: 'Notification 1', + link: '/catalog', + severity: 'normal', + topic: 'AAA', + }, + created: new Date(now - 10 * timeDelay), + }); + await insertNotification({ + id: id2, + user, + origin: 'Z', + payload: { + title: 'Notification 2', + link: '/catalog', + severity: 'normal', + topic: 'CCC', + }, + created: new Date(now), + }); + await insertNotification({ + id: id3, + user, + origin: 'X', + payload: { + title: 'Notification 3', + link: '/catalog', + severity: 'normal', + topic: 'BBB', + }, + created: new Date(now - 5 * timeDelay), + }); + + const notificationsCreatedAsc = await storage.getNotifications({ + user, + sort: 'created', + sortOrder: 'asc', + }); + expect(notificationsCreatedAsc.length).toBe(3); + expect(notificationsCreatedAsc.at(0)?.id).toEqual(id1); + expect(notificationsCreatedAsc.at(1)?.id).toEqual(id3); + expect(notificationsCreatedAsc.at(2)?.id).toEqual(id2); + + const notificationsCreatedDesc = await storage.getNotifications({ + user, + sort: 'created', + sortOrder: 'desc', + }); + expect(notificationsCreatedDesc.length).toBe(3); + expect(notificationsCreatedDesc.at(0)?.id).toEqual(id2); + expect(notificationsCreatedDesc.at(1)?.id).toEqual(id3); + expect(notificationsCreatedDesc.at(2)?.id).toEqual(id1); + + const notificationsTopicAsc = await storage.getNotifications({ + user, + sort: 'topic', + sortOrder: 'asc', + }); + expect(notificationsTopicAsc.length).toBe(3); + expect(notificationsTopicAsc.at(0)?.id).toEqual(id1); + expect(notificationsTopicAsc.at(1)?.id).toEqual(id3); + expect(notificationsTopicAsc.at(2)?.id).toEqual(id2); + + const notificationsTopicDesc = await storage.getNotifications({ + user, + sort: 'topic', + sortOrder: 'desc', + }); + expect(notificationsTopicDesc.length).toBe(3); + expect(notificationsTopicDesc.at(0)?.id).toEqual(id2); + expect(notificationsTopicDesc.at(1)?.id).toEqual(id3); + expect(notificationsTopicDesc.at(2)?.id).toEqual(id1); + + const notificationsOrigin = await storage.getNotifications({ + user, + sort: 'origin', + sortOrder: 'asc', + limit: 2, + offset: 0, + }); + expect(notificationsOrigin.length).toBe(2); + expect(notificationsOrigin.at(0)?.id).toEqual(id3); + expect(notificationsOrigin.at(1)?.id).toEqual(id1); + + const notificationsOriginNext = await storage.getNotifications({ + user, + sort: 'origin', + sortOrder: 'asc', + limit: 2, + offset: 2, + }); + expect(notificationsOriginNext.length).toBe(1); + expect(notificationsOriginNext.at(0)?.id).toEqual(id2); + }); }); describe('getStatus', () => { diff --git a/plugins/notifications-backend/src/database/NotificationsStore.ts b/plugins/notifications-backend/src/database/NotificationsStore.ts index 03f47263f1..92d9883255 100644 --- a/plugins/notifications-backend/src/database/NotificationsStore.ts +++ b/plugins/notifications-backend/src/database/NotificationsStore.ts @@ -27,7 +27,7 @@ export type NotificationGetOptions = { offset?: number; limit?: number; search?: string; - sort?: 'created' | 'read' | 'updated' | null; + sort?: 'created' | 'topic' | 'origin' | null; sortOrder?: 'asc' | 'desc'; read?: boolean; saved?: boolean; diff --git a/plugins/notifications-backend/src/service/router.ts b/plugins/notifications-backend/src/service/router.ts index 519919c6d4..b1425951d8 100644 --- a/plugins/notifications-backend/src/service/router.ts +++ b/plugins/notifications-backend/src/service/router.ts @@ -58,6 +58,28 @@ export interface RouterOptions { processors?: NotificationProcessor[]; } +const getSort = (input: string): NotificationGetOptions['sort'] | undefined => { + const valid: NotificationGetOptions['sort'][] = [ + 'created', + 'topic', + 'origin', + ]; + + if ((valid as string[]).includes(input)) { + return input as NotificationGetOptions['sort']; + } + return undefined; +}; + +const getSortOrder = (input: string): NotificationGetOptions['sortOrder'] => { + const valid: NotificationGetOptions['sortOrder'][] = ['asc', 'desc']; + + if ((valid as string[]).includes(input)) { + return input as NotificationGetOptions['sortOrder']; + } + return 'desc'; +}; + /** @internal */ export async function createRouter( options: RouterOptions, @@ -187,6 +209,12 @@ export async function createRouter( if (req.query.limit) { opts.limit = Number.parseInt(req.query.limit.toString(), 10); } + if (req.query.sort) { + opts.sort = getSort(req.query.sort.toString()); + } + if (req.query.sort_order) { + opts.sortOrder = getSortOrder(req.query.sort_order.toString()); + } if (req.query.search) { opts.search = req.query.search.toString(); } diff --git a/plugins/notifications/api-report.md b/plugins/notifications/api-report.md index caf9b4a2e5..70ebbd8bfd 100644 --- a/plugins/notifications/api-report.md +++ b/plugins/notifications/api-report.md @@ -23,6 +23,8 @@ export type GetNotificationsOptions = { search?: string; read?: boolean; createdAfter?: Date; + sort?: 'created' | 'topic' | 'origin'; + sortOrder?: 'asc' | 'desc'; }; // @public (undocumented) diff --git a/plugins/notifications/src/api/NotificationsApi.ts b/plugins/notifications/src/api/NotificationsApi.ts index da7d88dee1..9820373691 100644 --- a/plugins/notifications/src/api/NotificationsApi.ts +++ b/plugins/notifications/src/api/NotificationsApi.ts @@ -31,6 +31,8 @@ export type GetNotificationsOptions = { search?: string; read?: boolean; createdAfter?: Date; + sort?: 'created' | 'topic' | 'origin'; + sortOrder?: 'asc' | 'desc'; }; /** @public */ diff --git a/plugins/notifications/src/api/NotificationsClient.ts b/plugins/notifications/src/api/NotificationsClient.ts index ba0782fe6f..528325a957 100644 --- a/plugins/notifications/src/api/NotificationsClient.ts +++ b/plugins/notifications/src/api/NotificationsClient.ts @@ -49,6 +49,12 @@ export class NotificationsClient implements NotificationsApi { if (options?.offset !== undefined) { queryString.append('offset', options.offset.toString(10)); } + if (options?.sort !== undefined) { + queryString.append('sort', options.sort); + } + if (options?.sortOrder !== undefined) { + queryString.append('sort_order', options.sortOrder); + } if (options?.search) { queryString.append('search', options.search); } diff --git a/plugins/notifications/src/components/NotificationsFilters/NotificationsFilters.tsx b/plugins/notifications/src/components/NotificationsFilters/NotificationsFilters.tsx index 4645f46249..a44a58fe32 100644 --- a/plugins/notifications/src/components/NotificationsFilters/NotificationsFilters.tsx +++ b/plugins/notifications/src/components/NotificationsFilters/NotificationsFilters.tsx @@ -24,24 +24,19 @@ import { Select, Typography, } from '@material-ui/core'; +import { GetNotificationsOptions } from '../../api'; + +export type SortBy = Required< + Pick +>; export type NotificationsFiltersProps = { unreadOnly?: boolean; onUnreadOnlyChanged: (checked: boolean | undefined) => void; createdAfter?: string; onCreatedAfterChanged: (value: string) => void; - - // sorting?: { - // orderBy: GetNotificationsOrderByEnum; - // orderByDirec: GetNotificationsOrderByDirecEnum; - // }; - // setSorting: ({ - // orderBy, - // orderByDirec, - // }: { - // orderBy: GetNotificationsOrderByEnum; - // orderByDirec: GetNotificationsOrderByDirecEnum; - // }) => void; + sorting: SortBy; + onSortingChanged: (sortBy: SortBy) => void; }; export const CreatedAfterOptions: { @@ -61,62 +56,65 @@ export const CreatedAfterOptions: { }, }; -// export const SortByOptions: { -// [key: string]: { -// label: string; -// orderBy: GetNotificationsOrderByEnum; -// orderByDirec: GetNotificationsOrderByDirecEnum; -// }; -// } = { -// newest: { -// label: 'Newest on top', -// orderBy: GetNotificationsOrderByEnum.Created, -// orderByDirec: GetNotificationsOrderByDirecEnum.Asc, -// }, -// oldest: { -// label: 'Oldest on top', -// orderBy: GetNotificationsOrderByEnum.Created, -// orderByDirec: GetNotificationsOrderByDirecEnum.Desc, -// }, -// topic: { -// label: 'Topic', -// orderBy: GetNotificationsOrderByEnum.Topic, -// orderByDirec: GetNotificationsOrderByDirecEnum.Asc, -// }, -// origin: { -// label: 'Origin', -// orderBy: GetNotificationsOrderByEnum.Origin, -// orderByDirec: GetNotificationsOrderByDirecEnum.Asc, -// }, -// }; +export const SortByOptions: { + [key: string]: { + label: string; + sortBy: SortBy; + }; +} = { + newest: { + label: 'Newest on top', + sortBy: { + sort: 'created', + sortOrder: 'desc', + }, + }, + oldest: { + label: 'Oldest on top', + sortBy: { + sort: 'created', + sortOrder: 'asc', + }, + }, + topic: { + label: 'Topic', + sortBy: { + sort: 'topic', + sortOrder: 'asc', + }, + }, + origin: { + label: 'Origin', + sortBy: { + sort: 'origin', + sortOrder: 'asc', + }, + }, +}; -// TODO: Implement sorting on server (to work with pagination) -// const getSortBy = (sorting: NotificationsFiltersProps['sorting']): string => { -// if ( -// sorting?.orderBy === GetNotificationsOrderByEnum.Created && -// sorting.orderByDirec === GetNotificationsOrderByDirecEnum.Desc -// ) { -// return 'oldest'; -// } -// if (sorting?.orderBy === GetNotificationsOrderByEnum.Topic) { -// return 'topic'; -// } -// if (sorting?.orderBy === GetNotificationsOrderByEnum.Origin) { -// return 'origin'; -// } +const getSortByText = (sortBy?: SortBy): string => { + if (sortBy?.sort === 'created' && sortBy?.sortOrder === 'asc') { + return 'oldest'; + } + if (sortBy?.sort === 'topic') { + return 'topic'; + } + if (sortBy?.sort === 'origin') { + return 'origin'; + } -// return 'newest'; -// }; + return 'newest'; +}; export const NotificationsFilters = ({ - // sorting, - // setSorting, + sorting, + onSortingChanged, unreadOnly, onUnreadOnlyChanged, createdAfter, onCreatedAfterChanged, }: NotificationsFiltersProps) => { - // const sortBy = getSortBy(sorting); + const sortByText = getSortByText(sorting); const handleOnCreatedAfterChanged = ( event: React.ChangeEvent<{ name?: string; value: unknown }>, @@ -133,16 +131,13 @@ export const NotificationsFilters = ({ onUnreadOnlyChanged(value); }; - // const handleOnSortByChanged = ( - // event: React.ChangeEvent<{ name?: string; value: unknown }>, - // ) => { - // const idx = (event.target.value as string) || 'newest'; - // const option = SortByOptions[idx]; - // setSorting({ - // orderBy: option.orderBy, - // orderByDirec: option.orderByDirec, - // }); - // }; + const handleOnSortByChanged = ( + event: React.ChangeEvent<{ name?: string; value: unknown }>, + ) => { + const idx = (event.target.value as string) || 'newest'; + const option = SortByOptions[idx]; + onSortingChanged({ ...option.sortBy }); + }; let unreadOnlyValue = 'all'; if (unreadOnly) unreadOnlyValue = 'unread'; @@ -191,7 +186,6 @@ export const NotificationsFilters = ({ - {/* Sort by @@ -199,7 +193,7 @@ export const NotificationsFilters = ({ - */} + ); diff --git a/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx b/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx index 81c991798d..6b3292b892 100644 --- a/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx +++ b/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx @@ -28,6 +28,8 @@ import { useNotificationsApi } from '../../hooks'; import { CreatedAfterOptions, NotificationsFilters, + SortBy, + SortByOptions, } from '../NotificationsFilters'; import { GetNotificationsOptions } from '../../api'; @@ -39,6 +41,9 @@ export const NotificationsPage = () => { const [pageSize, setPageSize] = React.useState(5); const [containsText, setContainsText] = React.useState(); const [createdAfter, setCreatedAfter] = React.useState('lastWeek'); + const [sorting, setSorting] = React.useState( + SortByOptions.newest.sortBy, + ); const { error, value, retry, loading } = useNotificationsApi( api => { @@ -46,6 +51,7 @@ export const NotificationsPage = () => { search: containsText, limit: pageSize, offset: pageNumber * pageSize, + ...(sorting || {}), }; if (unreadOnly !== undefined) { options.read = !unreadOnly; @@ -58,7 +64,7 @@ export const NotificationsPage = () => { return api.getNotifications(options); }, - [containsText, unreadOnly, createdAfter, pageNumber, pageSize], + [containsText, unreadOnly, createdAfter, pageNumber, pageSize, sorting], ); useEffect(() => { @@ -92,8 +98,8 @@ export const NotificationsPage = () => { onUnreadOnlyChanged={setUnreadOnly} createdAfter={createdAfter} onCreatedAfterChanged={setCreatedAfter} - // setSorting={setSorting} - // sorting={sorting} + onSortingChanged={setSorting} + sorting={sorting} />