feat: initial notifications support

Signed-off-by: Heikki Hellgren <heikki.hellgren@op.fi>
This commit is contained in:
Heikki Hellgren
2023-12-15 14:05:35 +02:00
parent 778ea9e152
commit f24a0c1f6a
59 changed files with 1964 additions and 7 deletions
@@ -0,0 +1,34 @@
/*
* Copyright 2023 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.
*/
import { createApiRef } from '@backstage/core-plugin-api';
import {
Notification,
NotificationStatus,
} from '@backstage/plugin-notifications-common';
/** @public */
export const notificationsApiRef = createApiRef<NotificationsApi>({
id: 'plugin.notifications.service',
});
/** @public */
export interface NotificationsApi {
getNotifications(): Promise<Notification[]>;
getStatus(): Promise<NotificationStatus>;
// TODO: Mark as read/unread by notification id(s)
}
@@ -0,0 +1,57 @@
/*
* Copyright 2023 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.
*/
import { NotificationsApi } from './NotificationsApi';
import { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api';
import { ResponseError } from '@backstage/errors';
import {
Notification,
NotificationStatus,
} from '@backstage/plugin-notifications-common';
/** @public */
export class NotificationsClient implements NotificationsApi {
private readonly discoveryApi: DiscoveryApi;
private readonly fetchApi: FetchApi;
public constructor(options: {
discoveryApi: DiscoveryApi;
fetchApi: FetchApi;
}) {
this.discoveryApi = options.discoveryApi;
this.fetchApi = options.fetchApi;
}
async getNotifications(): Promise<Notification[]> {
return await this.get<Notification[]>('notifications');
}
async getStatus(): Promise<NotificationStatus> {
return await this.get<NotificationStatus>('status');
}
private async get<T>(path: string): Promise<T> {
const baseUrl = `${await this.discoveryApi.getBaseUrl('notifications')}/`;
const url = new URL(path, baseUrl);
const response = await this.fetchApi.fetch(url.toString());
if (!response.ok) {
throw await ResponseError.fromResponse(response);
}
return response.json() as Promise<T>;
}
}
+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright 2023 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.
*/
export * from './NotificationsApi';
export * from './NotificationsClient';
@@ -0,0 +1,82 @@
/*
* Copyright 2023 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.
*/
import React from 'react';
import {
Content,
ErrorPanel,
PageWithHeader,
} from '@backstage/core-components';
import { NotificationsTable } from '../NotificationsTable';
import { useNotificationsApi } from '../../hooks';
import {
Button,
Grid,
makeStyles,
Paper,
TableContainer,
} from '@material-ui/core';
import Bookmark from '@material-ui/icons/Bookmark';
import Check from '@material-ui/icons/Check';
import Inbox from '@material-ui/icons/Inbox';
const useStyles = makeStyles(_theme => ({
filterButton: {
width: '100%',
justifyContent: 'start',
},
}));
export const NotificationsPage = () => {
const {
loading: _loading,
error,
value,
retry: _retry,
} = useNotificationsApi(api => api.getNotifications());
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 />}>
Inbox
</Button>
<Button className={styles.filterButton} startIcon={<Check />}>
Done
</Button>
<Button className={styles.filterButton} startIcon={<Bookmark />}>
Saved
</Button>
</Grid>
<Grid item xs={10}>
<TableContainer component={Paper}>
<NotificationsTable notifications={value} />
</TableContainer>
</Grid>
</Grid>
</Content>
</PageWithHeader>
);
};
@@ -0,0 +1,16 @@
/*
* Copyright 2023 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.
*/
export * from './NotificationsPage';
@@ -0,0 +1,49 @@
/*
* Copyright 2023 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.
*/
import React, { useEffect } from 'react';
import { useNotificationsApi } from '../../hooks';
import { SidebarItem } from '@backstage/core-components';
import NotificationsIcon from '@material-ui/icons/Notifications';
import { useRouteRef } from '@backstage/core-plugin-api';
import { rootRouteRef } from '../../routes';
/** @public */
export const NotificationsSidebarItem = () => {
const {
loading,
error,
value,
retry: _retry,
} = useNotificationsApi(api => api.getStatus());
const [unreadCount, setUnreadCount] = React.useState(0);
const notificationsRoute = useRouteRef(rootRouteRef);
useEffect(() => {
if (!loading && !error && value) {
setUnreadCount(value.unread);
}
}, [loading, error, value]);
// TODO: Figure out if there count can be added to hasNotifications
return (
<SidebarItem
icon={NotificationsIcon}
to={notificationsRoute()}
text="Notifications"
hasNotifications={!error && !!unreadCount}
/>
);
};
@@ -0,0 +1,16 @@
/*
* Copyright 2023 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.
*/
export * from './NotificationsSideBarItem';
@@ -0,0 +1,100 @@
/*
* Copyright 2023 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.
*/
import React from 'react';
import {
IconButton,
makeStyles,
Table,
TableCell,
TableHead,
TableRow,
Tooltip,
Typography,
} from '@material-ui/core';
import { Notification } from '@backstage/plugin-notifications-common';
import { useNavigate } from 'react-router-dom';
import NotificationsIcon from '@material-ui/icons/Notifications';
import Checkbox from '@material-ui/core/Checkbox';
import Check from '@material-ui/icons/Check';
import Bookmark from '@material-ui/icons/Bookmark';
const useStyles = makeStyles(theme => ({
notificationRow: {
cursor: 'pointer',
'&:hover': {
backgroundColor: theme.palette.linkHover,
},
},
checkBox: {
padding: '0 10px 10px 0',
},
}));
/** @public */
export const NotificationsTable = (props: {
notifications?: Notification[];
}) => {
const { notifications } = props;
const navigate = useNavigate();
const styles = useStyles();
// TODO: Add select all
// TODO: Make mark as read work
// TODO: Check status of the notification and change to "Mark as unread" if it's already read
// TODO: Add support to save notifications (storageApi)
// TODO: Show timestamp relative time (react-relative-time npm package)
// TODO: Add signals listener and refresh data on message
// TODO: Handle no notifications properly
// TODO: Handle loading notifications
return (
<Table size="small">
<TableHead>
<TableRow>
<TableCell colSpan={3}>
{notifications?.length ?? 0} notifications
</TableCell>
</TableRow>
</TableHead>
{props.notifications?.map(notification => {
return (
<TableRow key={notification.id} className={styles.notificationRow}>
<TableCell width={100} style={{ verticalAlign: 'center' }}>
<Checkbox className={styles.checkBox} size="small" />
{notification.icon ?? <NotificationsIcon fontSize="small" />}
</TableCell>
<TableCell onClick={() => navigate(notification.link)}>
<Typography variant="subtitle2">{notification.title}</Typography>
<Typography variant="body2">
{notification.description}
</Typography>
</TableCell>
<TableCell style={{ textAlign: 'right' }}>
<Tooltip title="Mark as read">
<IconButton>
<Check fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Save">
<IconButton>
<Bookmark fontSize="small" />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
);
})}
</Table>
);
};
@@ -0,0 +1,16 @@
/*
* Copyright 2023 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.
*/
export * from './NotificationsTable';
@@ -0,0 +1,17 @@
/*
* Copyright 2023 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.
*/
export * from './NotificationsSideBarItem';
export * from './NotificationsTable';
+16
View File
@@ -0,0 +1,16 @@
/*
* Copyright 2023 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.
*/
export * from './useNotificationsApi';
@@ -0,0 +1,31 @@
/*
* Copyright 2023 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.
*/
import { NotificationsApi, notificationsApiRef } from '../api';
import { useApi } from '@backstage/core-plugin-api';
import useAsyncRetry from 'react-use/lib/useAsyncRetry';
/** @public */
export function useNotificationsApi<T>(
f: (api: NotificationsApi) => Promise<T>,
deps: any[] = [],
) {
const notificationsApi = useApi(notificationsApiRef);
return useAsyncRetry(async () => {
return await f(notificationsApi);
}, deps);
}
+19
View File
@@ -0,0 +1,19 @@
/*
* Copyright 2023 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.
*/
export { notificationsPlugin, NotificationsPage } from './plugin';
export * from './api';
export * from './hooks';
export * from './components';
+22
View File
@@ -0,0 +1,22 @@
/*
* Copyright 2023 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.
*/
import { notificationsPlugin } from './plugin';
describe('notifications', () => {
it('should export plugin', () => {
expect(notificationsPlugin).toBeDefined();
});
});
+52
View File
@@ -0,0 +1,52 @@
/*
* Copyright 2023 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.
*/
import {
createApiFactory,
createPlugin,
createRoutableExtension,
discoveryApiRef,
fetchApiRef,
} from '@backstage/core-plugin-api';
import { rootRouteRef } from './routes';
import { notificationsApiRef } from './api/NotificationsApi';
import { NotificationsClient } from './api';
/** @public */
export const notificationsPlugin = createPlugin({
id: 'notifications',
routes: {
root: rootRouteRef,
},
apis: [
createApiFactory({
api: notificationsApiRef,
deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef },
factory: ({ discoveryApi, fetchApi }) =>
new NotificationsClient({ discoveryApi, fetchApi }),
}),
],
});
/** @public */
export const NotificationsPage = notificationsPlugin.provide(
createRoutableExtension({
name: 'NotificationsPage',
component: () =>
import('./components/NotificationsPage').then(m => m.NotificationsPage),
mountPoint: rootRouteRef,
}),
);
+20
View File
@@ -0,0 +1,20 @@
/*
* Copyright 2023 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.
*/
import { createRouteRef } from '@backstage/core-plugin-api';
export const rootRouteRef = createRouteRef({
id: 'notifications',
});
+16
View File
@@ -0,0 +1,16 @@
/*
* Copyright 2023 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.
*/
import '@testing-library/jest-dom';