feat(notifications): added the topic filter for notifications

Signed-off-by: Marek Libra <marek.libra@gmail.com>
This commit is contained in:
Marek Libra
2024-12-21 21:53:05 +01:00
parent 0f2a750dac
commit 438c36c554
11 changed files with 310 additions and 48 deletions
@@ -27,18 +27,26 @@ export const notificationsApiRef = createApiRef<NotificationsApi>({
});
/** @public */
export type GetNotificationsOptions = {
offset?: number;
limit?: number;
export type GetNotificationsCommonOptions = {
search?: string;
read?: boolean;
saved?: boolean;
createdAfter?: Date;
sort?: 'created' | 'topic' | 'origin';
sortOrder?: 'asc' | 'desc';
minimumSeverity?: NotificationSeverity;
};
/** @public */
export type GetNotificationsOptions = GetNotificationsCommonOptions & {
offset?: number;
limit?: number;
sort?: 'created' | 'topic' | 'origin';
sortOrder?: 'asc' | 'desc';
topic?: string;
};
/** @public */
export type GetTopicsOptions = GetNotificationsCommonOptions;
/** @public */
export type UpdateNotificationsOptions = {
ids: string[];
@@ -52,6 +60,11 @@ export type GetNotificationsResponse = {
totalCount: number;
};
/** @public */
export type GetTopicsResponse = {
topics: string[];
};
/** @public */
export interface NotificationsApi {
getNotifications(
@@ -71,4 +84,6 @@ export interface NotificationsApi {
updateNotificationSettings(
settings: NotificationSettings,
): Promise<NotificationSettings>;
getTopics(options?: GetTopicsOptions): Promise<GetTopicsResponse>;
}
@@ -22,6 +22,8 @@ import { Notification } from '@backstage/plugin-notifications-common';
const server = setupServer();
const testTopic = 'test-topic';
const testNotification: Partial<Notification> = {
user: 'user:default/john.doe',
origin: 'plugin-test',
@@ -29,6 +31,7 @@ const testNotification: Partial<Notification> = {
title: 'Notification 1',
link: '/catalog',
severity: 'normal',
topic: testTopic,
},
};
@@ -75,6 +78,22 @@ describe('NotificationsClient', () => {
expect(response).toEqual(expectedResp);
});
it('should fetch notifications of the topic', async () => {
server.use(
rest.get(`${mockBaseUrl}/notifications`, (req, res, ctx) => {
expect(req.url.search).toBe(`?limit=10&offset=0&topic=${testTopic}`);
return res(ctx.json(expectedResp));
}),
);
const response = await client.getNotifications({
limit: 10,
offset: 0,
topic: testTopic,
});
expect(response).toEqual(expectedResp);
});
it('should omit unselected fetch options', async () => {
server.use(
rest.get(`${mockBaseUrl}/notifications`, (req, res, ctx) => {
@@ -131,4 +150,36 @@ describe('NotificationsClient', () => {
expect(response).toEqual(expectedResp);
});
});
describe('getTopics', () => {
const expectedResp = [testTopic];
it('should fetch topics from correct endpoint', async () => {
server.use(
rest.get(`${mockBaseUrl}/topics`, (_, res, ctx) =>
res(ctx.json(expectedResp)),
),
);
const response = await client.getTopics();
expect(response).toEqual(expectedResp);
});
it('should fetch topics with options', async () => {
server.use(
rest.get(`${mockBaseUrl}/topics`, (req, res, ctx) => {
expect(req.url.search).toBe(
'?search=find+me&read=true&createdAfter=1970-01-01T00%3A00%3A00.005Z',
);
return res(ctx.json(expectedResp));
}),
);
const response = await client.getTopics({
search: 'find me',
read: true,
createdAfter: new Date(5),
});
expect(response).toEqual(expectedResp);
});
});
});
@@ -14,8 +14,11 @@
* limitations under the License.
*/
import {
GetNotificationsCommonOptions,
GetNotificationsOptions,
GetNotificationsResponse,
GetTopicsOptions,
GetTopicsResponse,
NotificationsApi,
UpdateNotificationsOptions,
} from './NotificationsApi';
@@ -56,20 +59,11 @@ export class NotificationsClient implements NotificationsApi {
`${options.sort},${options?.sortOrder ?? 'desc'}`,
);
}
if (options?.search) {
queryString.append('search', options.search);
}
if (options?.read !== undefined) {
queryString.append('read', options.read ? 'true' : 'false');
}
if (options?.saved !== undefined) {
queryString.append('saved', options.saved ? 'true' : 'false');
}
if (options?.createdAfter !== undefined) {
queryString.append('createdAfter', options.createdAfter.toISOString());
}
if (options?.minimumSeverity !== undefined) {
queryString.append('minimumSeverity', options.minimumSeverity);
this.appendCommonQueryStrings(queryString, options);
if (options?.topic !== undefined) {
queryString.append('topic', options.topic);
}
return await this.request<GetNotificationsResponse>(
@@ -111,6 +105,34 @@ export class NotificationsClient implements NotificationsApi {
});
}
async getTopics(options?: GetTopicsOptions): Promise<GetTopicsResponse> {
const queryString = new URLSearchParams();
this.appendCommonQueryStrings(queryString, options);
return await this.request<GetTopicsResponse>(`/topics?${queryString}`);
}
private appendCommonQueryStrings(
queryString: URLSearchParams,
options?: GetNotificationsCommonOptions,
) {
if (options?.search) {
queryString.append('search', options.search);
}
if (options?.read !== undefined) {
queryString.append('read', options.read ? 'true' : 'false');
}
if (options?.saved !== undefined) {
queryString.append('saved', options.saved ? 'true' : 'false');
}
if (options?.createdAfter !== undefined) {
queryString.append('createdAfter', options.createdAfter.toISOString());
}
if (options?.minimumSeverity !== undefined) {
queryString.append('minimumSeverity', options.minimumSeverity);
}
}
private async request<T>(path: string, init?: RequestInit): Promise<T> {
const baseUrl = await this.discoveryApi.getBaseUrl('notifications');
const res = await this.fetchApi.fetch(`${baseUrl}${path}`, init);
@@ -40,8 +40,13 @@ export type NotificationsFiltersProps = {
onSavedChanged: (checked: boolean | undefined) => void;
severity: NotificationSeverity;
onSeverityChanged: (severity: NotificationSeverity) => void;
topic?: string;
onTopicChanged: (value: string | undefined) => void;
allTopics?: string[];
};
const ALL = '___all___';
export const CreatedAfterOptions: {
[key: string]: { label: string; getDate: () => Date };
} = {
@@ -127,6 +132,9 @@ export const NotificationsFilters = ({
onSavedChanged,
severity,
onSeverityChanged,
topic,
onTopicChanged,
allTopics,
}: NotificationsFiltersProps) => {
const sortByText = getSortByText(sorting);
@@ -180,6 +188,15 @@ export const NotificationsFilters = ({
onSeverityChanged(value);
};
const handleOnTopicChanged = (
event: React.ChangeEvent<{ name?: string; value: unknown }>,
) => {
const value = event.target.value as string;
onTopicChanged(value === ALL ? undefined : value);
};
const sortedAllTopics = (allTopics || []).sort((a, b) => a.localeCompare(b));
return (
<>
<Grid container>
@@ -265,6 +282,29 @@ export const NotificationsFilters = ({
</Select>
</FormControl>
</Grid>
<Grid item xs={12}>
<FormControl fullWidth variant="outlined" size="small">
<InputLabel id="notifications-filter-topic">Topic</InputLabel>
<Select
label="Topic"
labelId="notifications-filter-topic"
value={topic ?? ALL}
onChange={handleOnTopicChanged}
>
<MenuItem value={ALL} key={ALL}>
Any topic
</MenuItem>
{sortedAllTopics.map((item: string) => (
<MenuItem value={item} key={item}>
{item}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
</Grid>
</>
);
@@ -33,7 +33,11 @@ import {
SortBy,
SortByOptions,
} from '../NotificationsFilters';
import { GetNotificationsOptions, GetNotificationsResponse } from '../../api';
import {
GetNotificationsOptions,
GetNotificationsResponse,
GetTopicsResponse,
} from '../../api';
import {
NotificationSeverity,
NotificationStatus,
@@ -76,9 +80,10 @@ export const NotificationsPage = (props?: NotificationsPageProps) => {
SortByOptions.newest.sortBy,
);
const [severity, setSeverity] = React.useState<NotificationSeverity>('low');
const [topic, setTopic] = React.useState<string>();
const { error, value, retry, loading } = useNotificationsApi<
[GetNotificationsResponse, NotificationStatus]
[GetNotificationsResponse, NotificationStatus, GetTopicsResponse]
>(
api => {
const options: GetNotificationsOptions = {
@@ -94,13 +99,20 @@ export const NotificationsPage = (props?: NotificationsPageProps) => {
if (saved !== undefined) {
options.saved = saved;
}
if (topic !== undefined) {
options.topic = topic;
}
const createdAfterDate = CreatedAfterOptions[createdAfter].getDate();
if (createdAfterDate.valueOf() > 0) {
options.createdAfter = createdAfterDate;
}
return Promise.all([api.getNotifications(options), api.getStatus()]);
return Promise.all([
api.getNotifications(options),
api.getStatus(),
api.getTopics(options),
]);
},
[
containsText,
@@ -111,6 +123,7 @@ export const NotificationsPage = (props?: NotificationsPageProps) => {
sorting,
saved,
severity,
topic,
],
);
@@ -143,6 +156,7 @@ export const NotificationsPage = (props?: NotificationsPageProps) => {
const notifications = value?.[0]?.notifications;
const totalCount = value?.[0]?.totalCount;
const isUnread = !!value?.[1]?.unread;
const allTopics = value?.[2]?.topics;
let tableTitle = `All notifications (${totalCount})`;
if (saved) {
@@ -177,6 +191,9 @@ export const NotificationsPage = (props?: NotificationsPageProps) => {
onSavedChanged={setSaved}
severity={severity}
onSeverityChanged={setSeverity}
topic={topic}
onTopicChanged={setTopic}
allTopics={allTopics}
/>
</Grid>
<Grid item xs={10}>