feat(notifications): implement severity of notifications

Signed-off-by: Marek Libra <marek.libra@gmail.com>
This commit is contained in:
Marek Libra
2024-03-03 09:15:37 +01:00
parent 739415b07c
commit dff7a7e9e2
13 changed files with 249 additions and 16 deletions
@@ -0,0 +1,28 @@
/*
* 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.
*/
exports.up = async function up(knex) {
await knex.schema.alterTable('notification', table => {
table.tinyint('severity').notNullable().alter();
// we do not need to migrate data since there are not real deployments so far
});
};
exports.down = async function down(knex) {
await knex.schema.alterTable('notification', table => {
table.string('severity').nullable().alter();
});
};
@@ -14,9 +14,15 @@
* limitations under the License.
*/
import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils';
import { DatabaseNotificationsStore } from './DatabaseNotificationsStore';
import {
DatabaseNotificationsStore,
getNumericSeverity,
} from './DatabaseNotificationsStore';
import { Knex } from 'knex';
import { Notification } from '@backstage/plugin-notifications-common';
import {
Notification,
NotificationSeverity,
} from '@backstage/plugin-notifications-common';
jest.setTimeout(60_000);
@@ -48,6 +54,7 @@ const id4 = '04e0871e-e60a-4f68-8110-5ae3513f992e';
const id5 = '05e0871e-e60a-4f68-8110-5ae3513f992e';
const id6 = '06e0871e-e60a-4f68-8110-5ae3513f992e';
const id7 = '07e0871e-e60a-4f68-8110-5ae3513f992e';
const ids = [id1, id2, id3, id4, id5, id6, id7];
const now = Date.now();
const timeDelay = 5 * 1000; /* 5 secs */
@@ -266,6 +273,64 @@ describe.each(databases.eachSupportedId())(
});
});
describe('getNotifications filters on severity', () => {
beforeEach(async () => {
const severities: (NotificationSeverity | undefined)[] = [
'normal',
undefined,
'critical',
'high',
'low',
];
await Promise.all(
severities.map((severity, idx) =>
storage.saveNotification({
id: ids[idx],
user,
origin: 'test-origin',
created: new Date(now - idx * timeDelay),
payload: {
title: severity || 'default',
severity,
},
}),
),
);
});
it('normal', async () => {
const normal = await storage.getNotifications({
user,
minimalSeverity: getNumericSeverity('normal'),
});
expect(normal.map(idOnly)).toEqual([id1, id2, id3, id4]);
});
it('critical', async () => {
const critical = await storage.getNotifications({
user,
minimalSeverity: getNumericSeverity('critical'),
});
expect(critical.length).toBe(1);
expect(critical.at(0)?.id).toEqual(id3);
});
it('high', async () => {
const high = await storage.getNotifications({
user,
minimalSeverity: getNumericSeverity('high'),
});
expect(high.map(idOnly)).toEqual([id3, id4]);
});
it('low', async () => {
const low = await storage.getNotifications({
user,
minimalSeverity: getNumericSeverity('low'),
});
expect(low.map(idOnly)).toEqual([id1, id2, id3, id4, id5]);
});
});
describe('getNotifications pagination', () => {
beforeEach(async () => {
await storage.saveNotification(testNotification1);
@@ -439,13 +504,14 @@ describe.each(databases.eachSupportedId())(
payload: {
title: 'New notification',
link: '/scaffolder/task/1234',
severity: 'normal',
severity: 'low',
},
} as any,
});
expect(existing).not.toBeNull();
expect(existing?.id).toEqual(id2);
expect(existing?.payload.title).toEqual('New notification');
expect(existing?.payload.severity).toEqual('low');
expect(existing?.read).toBeNull();
});
});
@@ -22,7 +22,10 @@ import {
NotificationModifyOptions,
NotificationsStore,
} from './NotificationsStore';
import { Notification } from '@backstage/plugin-notifications-common';
import {
Notification,
NotificationSeverity,
} from '@backstage/plugin-notifications-common';
import { Knex } from 'knex';
const migrationsDir = resolvePackagePath(
@@ -30,6 +33,17 @@ const migrationsDir = resolvePackagePath(
'migrations',
);
const severities: NotificationSeverity[] = [
'critical',
'high',
'normal',
'low',
];
export const getNumericSeverity = (severity: string): Number => {
const idx = severities.indexOf(severity as NotificationSeverity);
return idx >= 0 ? idx : 2 /* normal */;
};
/** @internal */
export class DatabaseNotificationsStore implements NotificationsStore {
private constructor(private readonly db: Knex) {}
@@ -70,7 +84,7 @@ export class DatabaseNotificationsStore implements NotificationsStore {
description: row.description,
link: row.link,
topic: row.topic,
severity: row.severity,
severity: severities[row.severity],
scope: row.scope,
icon: row.icon,
},
@@ -87,7 +101,7 @@ export class DatabaseNotificationsStore implements NotificationsStore {
link: notification.payload?.link,
title: notification.payload?.title,
description: notification.payload?.description,
severity: notification.payload?.severity,
severity: getNumericSeverity(notification.payload?.severity ?? 'normal'),
scope: notification.payload?.scope,
saved: notification.saved,
read: notification.read,
@@ -155,6 +169,10 @@ export class DatabaseNotificationsStore implements NotificationsStore {
query.whereNull('notification.saved');
} // or match both if undefined
if (options.minimalSeverity !== undefined) {
query.where('notification.severity', '<=', options.minimalSeverity);
}
return query;
};
@@ -225,25 +243,28 @@ export class DatabaseNotificationsStore implements NotificationsStore {
return rows[0] as Notification;
}
async restoreExistingNotification(options: {
async restoreExistingNotification({
id,
notification,
}: {
id: string;
notification: Notification;
}) {
const query = this.db('notification')
.where('id', options.id)
.where('user', options.notification.user);
.where('id', id)
.where('user', notification.user);
await query.update({
title: options.notification.payload.title,
description: options.notification.payload.description,
link: options.notification.payload.link,
topic: options.notification.payload.topic,
title: notification.payload.title,
description: notification.payload.description,
link: notification.payload.link,
topic: notification.payload.topic,
updated: new Date(),
severity: options.notification.payload.severity,
severity: getNumericSeverity(notification.payload?.severity ?? 'normal'),
read: null,
});
return await this.getNotification(options);
return await this.getNotification({ id });
}
async getNotification(options: { id: string }): Promise<Notification | null> {
@@ -32,6 +32,7 @@ export type NotificationGetOptions = {
read?: boolean;
saved?: boolean;
createdAfter?: Date;
minimalSeverity?: Number;
};
/** @internal */
@@ -18,6 +18,7 @@ import express, { Request } from 'express';
import Router from 'express-promise-router';
import {
DatabaseNotificationsStore,
getNumericSeverity,
NotificationGetOptions,
} from '../database';
import { v4 as uuid } from 'uuid';
@@ -237,6 +238,11 @@ export async function createRouter(
}
opts.createdAfter = new Date(sinceEpoch);
}
if (req.query.minimal_severity) {
opts.minimalSeverity = getNumericSeverity(
req.query.minimal_severity.toString(),
);
}
const [notifications, totalCount] = await Promise.all([
store.getNotifications(opts),