feat(notifications): migrate to backstage ui
migrated notifications plugin to use backstage ui instead material ui. Signed-off-by: Hellgren Heikki <heikki.hellgren@op.fi>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-notifications': patch
|
||||
---
|
||||
|
||||
Migrated notifications plugin to use backstage UI
|
||||
@@ -21,7 +21,18 @@ import {
|
||||
} from '../src';
|
||||
import { signalsPlugin } from '@backstage/plugin-signals';
|
||||
import { SidebarItem } from '@backstage/core-components';
|
||||
import AddAlert from '@material-ui/icons/AddAlert';
|
||||
import { IconComponent } from '@backstage/core-plugin-api';
|
||||
import { RiBellLine } from '@remixicon/react';
|
||||
|
||||
const AddAlertIcon: IconComponent = props => {
|
||||
let size = 24;
|
||||
if (props.fontSize === 'large') {
|
||||
size = 32;
|
||||
} else if (props.fontSize === 'small') {
|
||||
size = 16;
|
||||
}
|
||||
return <RiBellLine size={size} />;
|
||||
};
|
||||
|
||||
createDevApp()
|
||||
.registerPlugin(notificationsPlugin)
|
||||
@@ -38,7 +49,7 @@ createDevApp()
|
||||
.addSidebarItem(<NotificationsSidebarItem webNotificationsEnabled />)
|
||||
.addSidebarItem(
|
||||
<SidebarItem
|
||||
icon={AddAlert}
|
||||
icon={AddAlertIcon}
|
||||
text="Random notification"
|
||||
onClick={() => {
|
||||
fetch('http://localhost:7007/api/notifications-debug/', {
|
||||
|
||||
@@ -59,11 +59,8 @@
|
||||
"@backstage/plugin-signals-react": "workspace:^",
|
||||
"@backstage/theme": "workspace:^",
|
||||
"@backstage/ui": "workspace:^",
|
||||
"@material-ui/core": "^4.9.13",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@remixicon/react": "^4.6.0",
|
||||
"lodash": "^4.17.21",
|
||||
"material-ui-confirm": "^3.0.12",
|
||||
"notistack": "^3.0.1",
|
||||
"react-relative-time": "^0.0.9",
|
||||
"react-use": "^17.2.4"
|
||||
|
||||
@@ -137,8 +137,9 @@ export const notificationsTranslationRef: TranslationRef<
|
||||
readonly 'table.bulkActions.returnSelectedAmongUnread': 'Return selected among unread';
|
||||
readonly 'table.bulkActions.saveSelectedForLater': 'Save selected for later';
|
||||
readonly 'table.bulkActions.undoSaveForSelected': 'Undo save for selected';
|
||||
readonly 'table.confirmDialog.cancel': 'Cancel';
|
||||
readonly 'table.confirmDialog.title': 'Are you sure?';
|
||||
readonly 'table.confirmDialog.markAllReadDescription': 'Mark <b>all</b> notifications as <b>read</b>.';
|
||||
readonly 'table.confirmDialog.markAllReadDescription': 'Mark all notifications as read.';
|
||||
readonly 'table.confirmDialog.markAllReadConfirmation': 'Mark All';
|
||||
readonly 'filters.view.all': 'All';
|
||||
readonly 'filters.view.label': 'View';
|
||||
|
||||
@@ -205,8 +205,9 @@ export const notificationsTranslationRef: TranslationRef<
|
||||
readonly 'table.bulkActions.returnSelectedAmongUnread': 'Return selected among unread';
|
||||
readonly 'table.bulkActions.saveSelectedForLater': 'Save selected for later';
|
||||
readonly 'table.bulkActions.undoSaveForSelected': 'Undo save for selected';
|
||||
readonly 'table.confirmDialog.cancel': 'Cancel';
|
||||
readonly 'table.confirmDialog.title': 'Are you sure?';
|
||||
readonly 'table.confirmDialog.markAllReadDescription': 'Mark <b>all</b> notifications as <b>read</b>.';
|
||||
readonly 'table.confirmDialog.markAllReadDescription': 'Mark all notifications as read.';
|
||||
readonly 'table.confirmDialog.markAllReadConfirmation': 'Mark All';
|
||||
readonly 'filters.view.all': 'All';
|
||||
readonly 'filters.view.label': 'View';
|
||||
|
||||
+79
-148
@@ -13,14 +13,8 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { ChangeEvent } from 'react';
|
||||
import FormControl from '@material-ui/core/FormControl';
|
||||
import Divider from '@material-ui/core/Divider';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import InputLabel from '@material-ui/core/InputLabel';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import Select from '@material-ui/core/Select';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { Select, Text, Flex } from '@backstage/ui';
|
||||
import type { Key } from 'react-aria-components';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { notificationsTranslationRef } from '../../translation';
|
||||
import { GetNotificationsOptions } from '../../api';
|
||||
@@ -128,22 +122,20 @@ export const NotificationsFilters = ({
|
||||
const { t } = useTranslationRef(notificationsTranslationRef);
|
||||
const sortByText = getSortByText(sorting);
|
||||
|
||||
const handleOnCreatedAfterChanged = (
|
||||
event: ChangeEvent<{ name?: string; value: unknown }>,
|
||||
) => {
|
||||
onCreatedAfterChanged(event.target.value as string);
|
||||
const handleOnCreatedAfterChanged = (key: Key | Key[] | null) => {
|
||||
if (key !== null && !Array.isArray(key))
|
||||
onCreatedAfterChanged(key as string);
|
||||
};
|
||||
|
||||
const handleOnViewChanged = (
|
||||
event: ChangeEvent<{ name?: string; value: unknown }>,
|
||||
) => {
|
||||
if (event.target.value === 'unread') {
|
||||
const handleOnViewChanged = (key: Key | Key[] | null) => {
|
||||
const value = Array.isArray(key) ? key[0] : key;
|
||||
if (value === 'unread') {
|
||||
onUnreadOnlyChanged(true);
|
||||
onSavedChanged(undefined);
|
||||
} else if (event.target.value === 'read') {
|
||||
} else if (value === 'read') {
|
||||
onUnreadOnlyChanged(false);
|
||||
onSavedChanged(undefined);
|
||||
} else if (event.target.value === 'saved') {
|
||||
} else if (value === 'saved') {
|
||||
onUnreadOnlyChanged(undefined);
|
||||
onSavedChanged(true);
|
||||
} else {
|
||||
@@ -153,10 +145,8 @@ export const NotificationsFilters = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnSortByChanged = (
|
||||
event: ChangeEvent<{ name?: string; value: unknown }>,
|
||||
) => {
|
||||
const idx = ((event.target.value as string) ||
|
||||
const handleOnSortByChanged = (key: Key | Key[] | null) => {
|
||||
const idx = (((Array.isArray(key) ? key[0] : key) as string) ||
|
||||
'newest') as keyof typeof SortByOptions;
|
||||
const option = SortByOptions[idx];
|
||||
onSortingChanged({ ...option.sortBy });
|
||||
@@ -171,145 +161,86 @@ export const NotificationsFilters = ({
|
||||
viewValue = 'read';
|
||||
}
|
||||
|
||||
const handleOnSeverityChanged = (
|
||||
event: ChangeEvent<{ name?: string; value: unknown }>,
|
||||
) => {
|
||||
const handleOnSeverityChanged = (key: Key | Key[] | null) => {
|
||||
const value: NotificationSeverity =
|
||||
(event.target.value as NotificationSeverity) || 'normal';
|
||||
((Array.isArray(key) ? key[0] : key) as NotificationSeverity) || 'normal';
|
||||
onSeverityChanged(value);
|
||||
};
|
||||
|
||||
const handleOnTopicChanged = (
|
||||
event: ChangeEvent<{ name?: string; value: unknown }>,
|
||||
) => {
|
||||
const value = event.target.value as string;
|
||||
const handleOnTopicChanged = (key: Key | Key[] | null) => {
|
||||
const value = (Array.isArray(key) ? key[0] : key) as string;
|
||||
onTopicChanged(value === ALL ? undefined : value);
|
||||
};
|
||||
|
||||
const sortedAllTopics = (allTopics || []).sort((a, b) => a.localeCompare(b));
|
||||
const sortedAllTopics = [...(allTopics ?? [])].sort((a, b) =>
|
||||
a.localeCompare(b),
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid container>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h6">{t('filters.title')}</Typography>
|
||||
<Divider variant="fullWidth" />
|
||||
</Grid>
|
||||
<Flex direction="column" gap="4">
|
||||
<div>
|
||||
<Text variant="title-x-small">{t('filters.title')}</Text>
|
||||
</div>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<FormControl fullWidth variant="outlined" size="small">
|
||||
<InputLabel id="notifications-filter-view">
|
||||
{t('filters.view.label')}
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId="notifications-filter-view"
|
||||
label={t('filters.view.label')}
|
||||
value={viewValue}
|
||||
onChange={handleOnViewChanged}
|
||||
>
|
||||
<MenuItem value="unread">{t('filters.view.unread')}</MenuItem>
|
||||
<MenuItem value="read">{t('filters.view.read')}</MenuItem>
|
||||
<MenuItem value="saved">{t('filters.view.saved')}</MenuItem>
|
||||
<MenuItem value="all">{t('filters.view.all')}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Select
|
||||
label={t('filters.view.label')}
|
||||
value={viewValue}
|
||||
onChange={handleOnViewChanged}
|
||||
options={[
|
||||
{ value: 'unread', label: t('filters.view.unread') },
|
||||
{ value: 'read', label: t('filters.view.read') },
|
||||
{ value: 'saved', label: t('filters.view.saved') },
|
||||
{ value: 'all', label: t('filters.view.all') },
|
||||
]}
|
||||
/>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<FormControl fullWidth variant="outlined" size="small">
|
||||
<InputLabel id="notifications-filter-created">
|
||||
{t('filters.createdAfter.label')}
|
||||
</InputLabel>
|
||||
<Select
|
||||
label={t('filters.createdAfter.label')}
|
||||
value={createdAfter}
|
||||
onChange={handleOnCreatedAfterChanged}
|
||||
options={Object.keys(CreatedAfterOptions).map((key: string) => ({
|
||||
value: key,
|
||||
label: t(
|
||||
CreatedAfterOptions[key as keyof typeof CreatedAfterOptions]
|
||||
.labelKey,
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
|
||||
<Select
|
||||
label={t('filters.createdAfter.label')}
|
||||
labelId="notifications-filter-created"
|
||||
placeholder={t('filters.createdAfter.placeholder')}
|
||||
value={createdAfter}
|
||||
onChange={handleOnCreatedAfterChanged}
|
||||
>
|
||||
{Object.keys(CreatedAfterOptions).map((key: string) => (
|
||||
<MenuItem value={key} key={key}>
|
||||
{t(
|
||||
CreatedAfterOptions[key as keyof typeof CreatedAfterOptions]
|
||||
.labelKey,
|
||||
)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Select
|
||||
label={t('filters.sortBy.label')}
|
||||
value={sortByText}
|
||||
onChange={handleOnSortByChanged}
|
||||
options={Object.keys(SortByOptions).map((key: string) => ({
|
||||
value: key,
|
||||
label: t(SortByOptions[key as keyof typeof SortByOptions].labelKey),
|
||||
}))}
|
||||
/>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<FormControl fullWidth variant="outlined" size="small">
|
||||
<InputLabel id="notifications-filter-sort">
|
||||
{t('filters.sortBy.label')}
|
||||
</InputLabel>
|
||||
<Select
|
||||
label={t('filters.severity.label')}
|
||||
value={severity}
|
||||
onChange={handleOnSeverityChanged}
|
||||
options={(
|
||||
['critical', 'high', 'normal', 'low'] as NotificationSeverity[]
|
||||
).map(key => ({
|
||||
value: key,
|
||||
label: t(`filters.severity.${key}`),
|
||||
}))}
|
||||
/>
|
||||
|
||||
<Select
|
||||
label={t('filters.sortBy.label')}
|
||||
labelId="notifications-filter-sort"
|
||||
placeholder={t('filters.sortBy.placeholder')}
|
||||
value={sortByText}
|
||||
onChange={handleOnSortByChanged}
|
||||
>
|
||||
{Object.keys(SortByOptions).map((key: string) => (
|
||||
<MenuItem value={key} key={key}>
|
||||
{t(SortByOptions[key as keyof typeof SortByOptions].labelKey)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<FormControl fullWidth variant="outlined" size="small">
|
||||
<InputLabel id="notifications-filter-severity">
|
||||
{t('filters.severity.label')}
|
||||
</InputLabel>
|
||||
|
||||
<Select
|
||||
label={t('filters.severity.label')}
|
||||
labelId="notifications-filter-severity"
|
||||
value={severity}
|
||||
onChange={handleOnSeverityChanged}
|
||||
>
|
||||
{(
|
||||
['critical', 'high', 'normal', 'low'] as NotificationSeverity[]
|
||||
).map(key => (
|
||||
<MenuItem value={key} key={key}>
|
||||
{t(`filters.severity.${key}`)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<FormControl fullWidth variant="outlined" size="small">
|
||||
<InputLabel id="notifications-filter-topic">
|
||||
{t('filters.topic.label')}
|
||||
</InputLabel>
|
||||
|
||||
<Select
|
||||
label={t('filters.topic.label')}
|
||||
labelId="notifications-filter-topic"
|
||||
value={topic ?? ALL}
|
||||
onChange={handleOnTopicChanged}
|
||||
>
|
||||
<MenuItem value={ALL} key={ALL}>
|
||||
{t('filters.topic.anyTopic')}
|
||||
</MenuItem>
|
||||
|
||||
{sortedAllTopics.map((item: string) => (
|
||||
<MenuItem value={item} key={item}>
|
||||
{item}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
<Select
|
||||
label={t('filters.topic.label')}
|
||||
value={topic ?? ALL}
|
||||
onChange={handleOnTopicChanged}
|
||||
options={[
|
||||
{ value: ALL, label: t('filters.topic.anyTopic') },
|
||||
...sortedAllTopics.map((item: string) => ({
|
||||
value: item,
|
||||
label: item,
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -21,8 +21,7 @@ import {
|
||||
PageWithHeader,
|
||||
ResponseErrorPanel,
|
||||
} from '@backstage/core-components';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import { ConfirmProvider } from 'material-ui-confirm';
|
||||
import { Grid } from '@backstage/ui';
|
||||
import { useSignal } from '@backstage/plugin-signals-react';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { notificationsTranslationRef } from '../../translation';
|
||||
@@ -191,43 +190,41 @@ function NotificationsPageContent(
|
||||
|
||||
const pageContent = (
|
||||
<Content>
|
||||
<ConfirmProvider>
|
||||
<Grid container>
|
||||
<Grid item xs={2}>
|
||||
<NotificationsFilters
|
||||
unreadOnly={unreadOnly}
|
||||
onUnreadOnlyChanged={setUnreadOnly}
|
||||
createdAfter={createdAfter}
|
||||
onCreatedAfterChanged={setCreatedAfter}
|
||||
onSortingChanged={setSorting}
|
||||
sorting={sorting}
|
||||
saved={saved}
|
||||
onSavedChanged={setSaved}
|
||||
severity={severity}
|
||||
onSeverityChanged={setSeverity}
|
||||
topic={topic}
|
||||
onTopicChanged={setTopic}
|
||||
allTopics={allTopics}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={10}>
|
||||
<NotificationsTable
|
||||
title={tableTitle}
|
||||
isLoading={loading}
|
||||
isUnread={isUnread}
|
||||
markAsReadOnLinkOpen={markAsReadOnLinkOpen}
|
||||
notifications={notifications}
|
||||
onUpdate={onUpdate}
|
||||
setContainsText={setContainsText}
|
||||
onPageChange={setPageNumber}
|
||||
onRowsPerPageChange={setPageSize}
|
||||
page={pageNumber}
|
||||
pageSize={pageSize}
|
||||
totalCount={totalCount}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ConfirmProvider>
|
||||
<Grid.Root columns="12" gap="6">
|
||||
<Grid.Item colSpan="2">
|
||||
<NotificationsFilters
|
||||
unreadOnly={unreadOnly}
|
||||
onUnreadOnlyChanged={setUnreadOnly}
|
||||
createdAfter={createdAfter}
|
||||
onCreatedAfterChanged={setCreatedAfter}
|
||||
onSortingChanged={setSorting}
|
||||
sorting={sorting}
|
||||
saved={saved}
|
||||
onSavedChanged={setSaved}
|
||||
severity={severity}
|
||||
onSeverityChanged={setSeverity}
|
||||
topic={topic}
|
||||
onTopicChanged={setTopic}
|
||||
allTopics={allTopics}
|
||||
/>
|
||||
</Grid.Item>
|
||||
<Grid.Item colSpan="10">
|
||||
<NotificationsTable
|
||||
title={tableTitle}
|
||||
isLoading={loading}
|
||||
isUnread={isUnread}
|
||||
markAsReadOnLinkOpen={markAsReadOnLinkOpen}
|
||||
notifications={notifications}
|
||||
onUpdate={onUpdate}
|
||||
setContainsText={setContainsText}
|
||||
onPageChange={setPageNumber}
|
||||
onRowsPerPageChange={setPageSize}
|
||||
page={pageNumber}
|
||||
pageSize={pageSize}
|
||||
totalCount={totalCount}
|
||||
/>
|
||||
</Grid.Item>
|
||||
</Grid.Root>
|
||||
</Content>
|
||||
);
|
||||
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@layer components {
|
||||
.snackbarContent {
|
||||
background-color: var(--bui-bg-app);
|
||||
color: var(--bui-fg-primary);
|
||||
}
|
||||
|
||||
.snackbarIcon {
|
||||
margin-right: var(--bui-space-2);
|
||||
}
|
||||
}
|
||||
+73
-66
@@ -13,16 +13,11 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useNotificationsApi } from '../../hooks';
|
||||
import { Link, SidebarItem } from '@backstage/core-components';
|
||||
import NotificationsIcon from '@material-ui/icons/Notifications';
|
||||
import {
|
||||
alertApiRef,
|
||||
IconComponent,
|
||||
useApi,
|
||||
useRouteRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { SidebarItem } from '@backstage/core-components';
|
||||
import { IconComponent, useApi, useRouteRef } from '@backstage/core-plugin-api';
|
||||
import { toastApiRef } from '@backstage/frontend-plugin-api';
|
||||
import { rootRouteRef } from '../../routes';
|
||||
import { useSignal } from '@backstage/plugin-signals-react';
|
||||
import {
|
||||
@@ -43,31 +38,34 @@ import {
|
||||
VariantType,
|
||||
} from 'notistack';
|
||||
import { SeverityIcon } from '../NotificationsTable/SeverityIcon';
|
||||
import OpenInNew from '@material-ui/icons/OpenInNew';
|
||||
import MarkAsReadIcon from '@material-ui/icons/CheckCircle';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Chip from '@material-ui/core/Chip';
|
||||
import { styled } from '@material-ui/core/styles';
|
||||
import { ButtonIcon, Flex, Tag, TagGroup } from '@backstage/ui';
|
||||
import {
|
||||
RiExternalLinkLine,
|
||||
RiCheckboxCircleLine,
|
||||
RiNotification2Line,
|
||||
} from '@remixicon/react';
|
||||
import styles from './NotificationsSideBarItem.module.css';
|
||||
|
||||
const StyledMaterialDesignContent = styled(MaterialDesignContent)(
|
||||
({ theme }) => ({
|
||||
'&.notistack-MuiContent-low': {
|
||||
backgroundColor: theme.palette.background.default,
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
'&.notistack-MuiContent-normal': {
|
||||
backgroundColor: theme.palette.background.default,
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
'&.notistack-MuiContent-high': {
|
||||
backgroundColor: theme.palette.background.default,
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
'&.notistack-MuiContent-critical': {
|
||||
backgroundColor: theme.palette.background.default,
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}),
|
||||
const NotificationsIcon: IconComponent = props => {
|
||||
let size = 24;
|
||||
if (props.fontSize === 'large') {
|
||||
size = 32;
|
||||
} else if (props.fontSize === 'small') {
|
||||
size = 16;
|
||||
}
|
||||
return <RiNotification2Line size={size} />;
|
||||
};
|
||||
|
||||
const StyledMaterialDesignContent = forwardRef<HTMLDivElement, any>(
|
||||
(props: any, ref: React.Ref<HTMLDivElement>) => (
|
||||
<MaterialDesignContent
|
||||
{...props}
|
||||
ref={ref}
|
||||
className={[props.className, styles.snackbarContent]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
/>
|
||||
),
|
||||
);
|
||||
|
||||
declare module 'notistack' {
|
||||
@@ -146,7 +144,7 @@ export const NotificationsSidebarItem = (
|
||||
webNotificationsEnabled = false,
|
||||
titleCounterEnabled = true,
|
||||
snackbarEnabled = true,
|
||||
snackbarAutoHideDuration = 10000,
|
||||
snackbarAutoHideDuration = 15000,
|
||||
icon = NotificationsIcon,
|
||||
text = 'Notifications',
|
||||
...restProps
|
||||
@@ -155,7 +153,7 @@ export const NotificationsSidebarItem = (
|
||||
titleCounterEnabled: true,
|
||||
snackbarProps: {
|
||||
enabled: true,
|
||||
autoHideDuration: 10000,
|
||||
autoHideDuration: 15000,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -172,7 +170,7 @@ export const NotificationsSidebarItem = (
|
||||
api.getStatus(),
|
||||
);
|
||||
const notificationsApi = useApi(notificationsApiRef);
|
||||
const alertApi = useApi(alertApiRef);
|
||||
const toastApi = useApi(toastApiRef);
|
||||
const [unreadCount, setUnreadCount] = useState(0);
|
||||
const notificationsRoute = useRouteRef(rootRouteRef)();
|
||||
// TODO: Do we want to add long polling in case signals are not available
|
||||
@@ -186,11 +184,14 @@ export const NotificationsSidebarItem = (
|
||||
const getSnackbarProperties = useCallback(
|
||||
(notification: Notification) => {
|
||||
const action = (snackBarId: SnackbarKey) => (
|
||||
<>
|
||||
<IconButton
|
||||
component={Link}
|
||||
to={notification.payload.link ?? notificationsRoute}
|
||||
onClick={() => {
|
||||
<Flex gap="1">
|
||||
<ButtonIcon
|
||||
aria-label="open notification"
|
||||
icon={<RiExternalLinkLine size={16} />}
|
||||
variant="secondary"
|
||||
onPress={() => {
|
||||
const link = notification.payload.link ?? notificationsRoute;
|
||||
window.open(link, '_blank', 'noopener,noreferrer');
|
||||
if (notification.payload.link) {
|
||||
notificationsApi
|
||||
.updateNotifications({
|
||||
@@ -198,19 +199,20 @@ export const NotificationsSidebarItem = (
|
||||
read: true,
|
||||
})
|
||||
.catch(() => {
|
||||
alertApi.post({
|
||||
message: 'Failed to mark notification as read',
|
||||
severity: 'error',
|
||||
toastApi.post({
|
||||
title: 'Failed to mark notification as read',
|
||||
status: 'danger',
|
||||
});
|
||||
});
|
||||
}
|
||||
closeSnackbar(snackBarId);
|
||||
}}
|
||||
>
|
||||
<OpenInNew fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
/>
|
||||
<ButtonIcon
|
||||
aria-label="mark as read"
|
||||
icon={<RiCheckboxCircleLine size={16} />}
|
||||
variant="secondary"
|
||||
onPress={() => {
|
||||
notificationsApi
|
||||
.updateNotifications({
|
||||
ids: [notification.id],
|
||||
@@ -220,21 +222,19 @@ export const NotificationsSidebarItem = (
|
||||
closeSnackbar(snackBarId);
|
||||
})
|
||||
.catch(() => {
|
||||
alertApi.post({
|
||||
message: 'Failed to mark notification as read',
|
||||
severity: 'error',
|
||||
toastApi.post({
|
||||
title: 'Failed to mark notification as read',
|
||||
status: 'danger',
|
||||
});
|
||||
});
|
||||
}}
|
||||
>
|
||||
<MarkAsReadIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</>
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
return { action };
|
||||
},
|
||||
[notificationsRoute, notificationsApi, alertApi],
|
||||
[notificationsRoute, notificationsApi, toastApi],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -286,9 +286,9 @@ export const NotificationsSidebarItem = (
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
alertApi.post({
|
||||
message: 'Failed to fetch notification',
|
||||
severity: 'error',
|
||||
toastApi.post({
|
||||
title: 'Failed to fetch notification',
|
||||
status: 'danger',
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -302,7 +302,7 @@ export const NotificationsSidebarItem = (
|
||||
sendWebNotification,
|
||||
webNotificationsEnabled,
|
||||
notificationsApi,
|
||||
alertApi,
|
||||
toastApi,
|
||||
getSnackbarProperties,
|
||||
snackbarProps,
|
||||
]);
|
||||
@@ -341,16 +341,19 @@ export const NotificationsSidebarItem = (
|
||||
<SnackbarProvider
|
||||
iconVariant={{
|
||||
normal: snackbarProps?.iconVariant?.normal ?? (
|
||||
<SeverityIcon severity="normal" />
|
||||
<SeverityIcon severity="normal" className={styles.snackbarIcon} />
|
||||
),
|
||||
critical: snackbarProps?.iconVariant?.critical ?? (
|
||||
<SeverityIcon severity="critical" />
|
||||
<SeverityIcon
|
||||
severity="critical"
|
||||
className={styles.snackbarIcon}
|
||||
/>
|
||||
),
|
||||
high: snackbarProps?.iconVariant?.high ?? (
|
||||
<SeverityIcon severity="high" />
|
||||
<SeverityIcon severity="high" className={styles.snackbarIcon} />
|
||||
),
|
||||
low: snackbarProps?.iconVariant?.low ?? (
|
||||
<SeverityIcon severity="low" />
|
||||
<SeverityIcon severity="low" className={styles.snackbarIcon} />
|
||||
),
|
||||
}}
|
||||
dense={snackbarProps?.dense}
|
||||
@@ -377,7 +380,11 @@ export const NotificationsSidebarItem = (
|
||||
icon={icon}
|
||||
{...restProps}
|
||||
>
|
||||
{count && <Chip size="small" label={count > 99 ? '99+' : count} />}
|
||||
{count && (
|
||||
<TagGroup aria-label="Unread notifications">
|
||||
<Tag size="small">{count > 99 ? '99+' : count}</Tag>
|
||||
</TagGroup>
|
||||
)}
|
||||
</SidebarItem>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -14,14 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Notification } from '@backstage/plugin-notifications-common';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import MarkAsUnreadIcon from '@material-ui/icons/Markunread' /* TODO: use Drafts and MarkAsUnread once we have mui 5 icons */;
|
||||
import MarkAsReadIcon from '@material-ui/icons/CheckCircle';
|
||||
import MarkAsUnsavedIcon from '@material-ui/icons/LabelOff' /* TODO: use BookmarkRemove and BookmarkAdd once we have mui 5 icons */;
|
||||
import MarkAsSavedIcon from '@material-ui/icons/Label';
|
||||
import MarkAllReadIcon from '@material-ui/icons/DoneAll';
|
||||
import { ButtonIcon, Flex, Tooltip, TooltipTrigger } from '@backstage/ui';
|
||||
import {
|
||||
RiCheckDoubleLine,
|
||||
RiBookmarkLine,
|
||||
RiBookmark3Line,
|
||||
RiCheckboxCircleLine,
|
||||
RiMailLine,
|
||||
} from '@remixicon/react';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { notificationsTranslationRef } from '../../translation';
|
||||
|
||||
@@ -56,61 +56,56 @@ export const BulkActions = ({
|
||||
const markAsReadText = isOneRead
|
||||
? t('table.bulkActions.returnSelectedAmongUnread')
|
||||
: t('table.bulkActions.markSelectedAsRead');
|
||||
const IconComponent = isOneRead ? MarkAsUnreadIcon : MarkAsReadIcon;
|
||||
const ReadIcon = isOneRead ? RiMailLine : RiCheckboxCircleLine;
|
||||
|
||||
const markAsSavedText = isOneSaved
|
||||
? t('table.bulkActions.undoSaveForSelected')
|
||||
: t('table.bulkActions.saveSelectedForLater');
|
||||
const SavedIconComponent = isOneSaved ? MarkAsUnsavedIcon : MarkAsSavedIcon;
|
||||
const SavedIcon = isOneSaved ? RiBookmark3Line : RiBookmarkLine;
|
||||
const markAllReadText = t('table.bulkActions.markAllRead');
|
||||
|
||||
return (
|
||||
<Grid container wrap="nowrap">
|
||||
<Grid item xs={3}>
|
||||
{onMarkAllRead ? (
|
||||
<Tooltip title={markAllReadText}>
|
||||
<div>
|
||||
{/* The <div> here is a workaround for the Tooltip which does not work for a "disabled" child */}
|
||||
<IconButton disabled={!isUnread} onClick={onMarkAllRead}>
|
||||
<MarkAllReadIcon aria-label={markAllReadText} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</Grid>
|
||||
<Flex gap="1">
|
||||
{onMarkAllRead ? (
|
||||
<TooltipTrigger>
|
||||
<ButtonIcon
|
||||
aria-label={markAllReadText}
|
||||
isDisabled={!isUnread}
|
||||
onPress={onMarkAllRead}
|
||||
icon={<RiCheckDoubleLine size={16} />}
|
||||
variant="secondary"
|
||||
/>
|
||||
<Tooltip>{markAllReadText}</Tooltip>
|
||||
</TooltipTrigger>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
|
||||
<Grid item xs={3}>
|
||||
<Tooltip title={markAsSavedText}>
|
||||
<div>
|
||||
{/* The <div> here is a workaround for the Tooltip which does not work for a "disabled" child */}
|
||||
<IconButton
|
||||
disabled={isDisabled}
|
||||
onClick={() => {
|
||||
onSwitchSavedStatus([...selectedNotifications], !isOneSaved);
|
||||
}}
|
||||
>
|
||||
<SavedIconComponent aria-label={markAsSavedText} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
<TooltipTrigger>
|
||||
<ButtonIcon
|
||||
aria-label={markAsSavedText}
|
||||
isDisabled={isDisabled}
|
||||
onPress={() => {
|
||||
onSwitchSavedStatus([...selectedNotifications], !isOneSaved);
|
||||
}}
|
||||
icon={<SavedIcon size={16} />}
|
||||
variant="secondary"
|
||||
/>
|
||||
<Tooltip>{markAsSavedText}</Tooltip>
|
||||
</TooltipTrigger>
|
||||
|
||||
<Grid item xs={3}>
|
||||
<Tooltip title={markAsReadText}>
|
||||
<div>
|
||||
<IconButton
|
||||
disabled={isDisabled}
|
||||
onClick={() => {
|
||||
onSwitchReadStatus([...selectedNotifications], !isOneRead);
|
||||
}}
|
||||
>
|
||||
<IconComponent aria-label={markAsReadText} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<TooltipTrigger>
|
||||
<ButtonIcon
|
||||
aria-label={markAsReadText}
|
||||
isDisabled={isDisabled}
|
||||
onPress={() => {
|
||||
onSwitchReadStatus([...selectedNotifications], !isOneRead);
|
||||
}}
|
||||
icon={<ReadIcon size={16} />}
|
||||
variant="secondary"
|
||||
/>
|
||||
<Tooltip>{markAsReadText}</Tooltip>
|
||||
</TooltipTrigger>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
+10
-13
@@ -13,8 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import { Text, Button } from '@backstage/ui';
|
||||
import { useState } from 'react';
|
||||
|
||||
const MAX_LENGTH = 100;
|
||||
@@ -25,37 +24,35 @@ export const NotificationDescription = (props: { description: string }) => {
|
||||
const isLong = description.length > MAX_LENGTH;
|
||||
|
||||
if (!isLong) {
|
||||
return <Typography variant="body2">{description}</Typography>;
|
||||
return <Text variant="body-small">{description}</Text>;
|
||||
}
|
||||
|
||||
if (shown) {
|
||||
return (
|
||||
<Typography variant="body2">
|
||||
<Text variant="body-small">
|
||||
{description}{' '}
|
||||
<Button
|
||||
variant="text"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
variant="tertiary"
|
||||
onPress={() => {
|
||||
setShown(false);
|
||||
}}
|
||||
>
|
||||
Show less
|
||||
</Button>
|
||||
</Typography>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Typography variant="body2">
|
||||
<Text variant="body-small">
|
||||
{description.substring(0, MAX_LENGTH)}...{' '}
|
||||
<Button
|
||||
variant="text"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
variant="tertiary"
|
||||
onPress={() => {
|
||||
setShown(true);
|
||||
}}
|
||||
>
|
||||
Show more
|
||||
</Button>
|
||||
</Typography>
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Notification } from '@backstage/plugin-notifications-common';
|
||||
import SvgIcon from '@material-ui/core/SvgIcon';
|
||||
import { useApp } from '@backstage/core-plugin-api';
|
||||
import { SeverityIcon } from './SeverityIcon';
|
||||
|
||||
@@ -26,8 +25,10 @@ export const NotificationIcon = ({
|
||||
const app = useApp();
|
||||
|
||||
if (notification.payload.icon) {
|
||||
const Icon = app.getSystemIcon(notification.payload.icon) ?? SvgIcon;
|
||||
return <Icon />;
|
||||
const Icon = app.getSystemIcon(notification.payload.icon);
|
||||
if (Icon) {
|
||||
return <Icon />;
|
||||
}
|
||||
}
|
||||
return <SeverityIcon severity={notification.payload.severity} />;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@layer components {
|
||||
.severityItem {
|
||||
align-content: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.broadcastIcon {
|
||||
font-size: 1rem;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
.notificationInfoRow {
|
||||
margin-right: var(--bui-space-1);
|
||||
}
|
||||
|
||||
.notificationInfoRow:not(:first-child) {
|
||||
margin-left: var(--bui-space-1);
|
||||
}
|
||||
}
|
||||
@@ -17,15 +17,20 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import throttle from 'lodash/throttle';
|
||||
// @ts-ignore
|
||||
import RelativeTime from 'react-relative-time';
|
||||
import Box from '@material-ui/core/Box';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import CheckBox from '@material-ui/core/Checkbox';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import { Notification } from '@backstage/plugin-notifications-common';
|
||||
import { useConfirm } from 'material-ui-confirm';
|
||||
import BroadcastIcon from '@material-ui/icons/RssFeed';
|
||||
import { alertApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Dialog,
|
||||
DialogBody,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
Flex,
|
||||
Text,
|
||||
} from '@backstage/ui';
|
||||
import { RiRssFill } from '@remixicon/react';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { toastApiRef } from '@backstage/frontend-plugin-api';
|
||||
import {
|
||||
Link,
|
||||
Table,
|
||||
@@ -41,23 +46,9 @@ import { BulkActions } from './BulkActions';
|
||||
import { NotificationIcon } from './NotificationIcon';
|
||||
import { NotificationDescription } from './NotificationDescription';
|
||||
|
||||
const ThrottleDelayMs = 1000;
|
||||
import styles from './NotificationsTable.module.css';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
severityItem: {
|
||||
alignContent: 'center',
|
||||
},
|
||||
broadcastIcon: {
|
||||
fontSize: '1rem',
|
||||
verticalAlign: 'text-bottom',
|
||||
},
|
||||
notificationInfoRow: {
|
||||
marginRight: theme.spacing(0.5),
|
||||
'&:not(:first-child)': {
|
||||
marginLeft: theme.spacing(0.5),
|
||||
},
|
||||
},
|
||||
}));
|
||||
const ThrottleDelayMs = 1000;
|
||||
|
||||
/** @public */
|
||||
export type NotificationsTableProps = Pick<
|
||||
@@ -89,10 +80,9 @@ export const NotificationsTable = ({
|
||||
totalCount,
|
||||
}: NotificationsTableProps) => {
|
||||
const { t } = useTranslationRef(notificationsTranslationRef);
|
||||
const classes = useStyles();
|
||||
const notificationsApi = useApi(notificationsApiRef);
|
||||
const alertApi = useApi(alertApiRef);
|
||||
const confirm = useConfirm();
|
||||
const toastApi = useApi(toastApiRef);
|
||||
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
|
||||
|
||||
const [selectedNotifications, setSelectedNotifications] = useState(
|
||||
new Set<Notification['id']>(),
|
||||
@@ -136,38 +126,26 @@ export const NotificationsTable = ({
|
||||
[notificationsApi, onUpdate],
|
||||
);
|
||||
|
||||
const onMarkAllRead = useCallback(() => {
|
||||
confirm({
|
||||
title: 'Are you sure?',
|
||||
description: (
|
||||
<>
|
||||
Mark <b>all</b> notifications as <b>read</b>.
|
||||
</>
|
||||
),
|
||||
confirmationText: 'Mark All',
|
||||
})
|
||||
.then(async () => {
|
||||
const ids = (
|
||||
await notificationsApi.getNotifications({ read: false })
|
||||
).notifications?.map(notification => notification.id);
|
||||
|
||||
return notificationsApi
|
||||
.updateNotifications({
|
||||
ids,
|
||||
read: true,
|
||||
})
|
||||
.then(onUpdate);
|
||||
})
|
||||
.catch(e => {
|
||||
if (e) {
|
||||
// if e === undefined, the Cancel button has been hit
|
||||
alertApi.post({
|
||||
message: 'Failed to mark all notifications as read',
|
||||
severity: 'error',
|
||||
});
|
||||
}
|
||||
const doMarkAllRead = useCallback(async () => {
|
||||
setConfirmDialogOpen(false);
|
||||
try {
|
||||
const result = await notificationsApi.getNotifications({ read: false });
|
||||
const ids =
|
||||
result.notifications?.map(notification => notification.id) ?? [];
|
||||
if (ids.length === 0) return;
|
||||
await notificationsApi.updateNotifications({ ids, read: true });
|
||||
onUpdate();
|
||||
} catch {
|
||||
toastApi.post({
|
||||
title: t('table.errors.markAllReadFailed'),
|
||||
status: 'danger',
|
||||
});
|
||||
}, [alertApi, confirm, notificationsApi, onUpdate]);
|
||||
}
|
||||
}, [notificationsApi, onUpdate, toastApi, t]);
|
||||
|
||||
const onMarkAllRead = useCallback(() => {
|
||||
setConfirmDialogOpen(true);
|
||||
}, []);
|
||||
|
||||
const throttledContainsTextHandler = useMemo(
|
||||
() => throttle(setContainsText, ThrottleDelayMs),
|
||||
@@ -190,6 +168,7 @@ export const NotificationsTable = ({
|
||||
{
|
||||
/* selection column */
|
||||
width: '1rem',
|
||||
cellStyle: { paddingRight: '2.5rem' },
|
||||
title: showToolbar ? (
|
||||
<SelectAll
|
||||
count={selectedNotifications.size}
|
||||
@@ -203,10 +182,10 @@ export const NotificationsTable = ({
|
||||
/>
|
||||
) : undefined,
|
||||
render: (notification: Notification) => (
|
||||
<CheckBox
|
||||
color="primary"
|
||||
checked={selectedNotifications.has(notification.id)}
|
||||
onChange={(_, checked) =>
|
||||
<Checkbox
|
||||
aria-label="Select notification"
|
||||
isSelected={selectedNotifications.has(notification.id)}
|
||||
onChange={checked =>
|
||||
onNotificationsSelectChange([notification.id], checked)
|
||||
}
|
||||
/>
|
||||
@@ -216,75 +195,66 @@ export const NotificationsTable = ({
|
||||
/* compact-data column */
|
||||
customFilterAndSearch: () =>
|
||||
true /* Keep sorting&filtering on backend due to pagination. */,
|
||||
cellStyle: { paddingLeft: 0 },
|
||||
render: (notification: Notification) => {
|
||||
// Compact content
|
||||
return (
|
||||
<Grid container>
|
||||
<Grid item className={classes.severityItem}>
|
||||
<Flex gap="4" align="center">
|
||||
<div className={styles.severityItem}>
|
||||
<NotificationIcon notification={notification} />
|
||||
</Grid>
|
||||
<Grid item xs={11}>
|
||||
<Box>
|
||||
<Typography variant="subtitle1">
|
||||
{notification.payload.link ? (
|
||||
<Link
|
||||
to={notification.payload.link}
|
||||
onClick={() => {
|
||||
if (markAsReadOnLinkOpen && !notification.read) {
|
||||
onSwitchReadStatus([notification.id], true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{notification.payload.title}
|
||||
</Link>
|
||||
) : (
|
||||
notification.payload.title
|
||||
)}
|
||||
</Typography>
|
||||
{notification.payload.description ? (
|
||||
<NotificationDescription
|
||||
description={notification.payload.description}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<Flex direction="column" gap="1">
|
||||
<Text variant="body-medium">
|
||||
{notification.payload.link ? (
|
||||
<Link
|
||||
to={notification.payload.link}
|
||||
onClick={() => {
|
||||
if (markAsReadOnLinkOpen && !notification.read) {
|
||||
onSwitchReadStatus([notification.id], true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{notification.payload.title}
|
||||
</Link>
|
||||
) : (
|
||||
notification.payload.title
|
||||
)}
|
||||
</Text>
|
||||
{notification.payload.description ? (
|
||||
<NotificationDescription
|
||||
description={notification.payload.description}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Typography variant="caption">
|
||||
{!notification.user && (
|
||||
<>
|
||||
<BroadcastIcon className={classes.broadcastIcon} />
|
||||
</>
|
||||
)}
|
||||
{notification.origin && (
|
||||
<>
|
||||
<Typography
|
||||
variant="inherit"
|
||||
className={classes.notificationInfoRow}
|
||||
>
|
||||
{notification.origin}
|
||||
</Typography>
|
||||
•
|
||||
</>
|
||||
)}
|
||||
{notification.payload.topic && (
|
||||
<>
|
||||
<Typography
|
||||
variant="inherit"
|
||||
className={classes.notificationInfoRow}
|
||||
>
|
||||
{notification.payload.topic}
|
||||
</Typography>
|
||||
•
|
||||
</>
|
||||
)}
|
||||
{notification.created && (
|
||||
<RelativeTime
|
||||
value={notification.created}
|
||||
className={classes.notificationInfoRow}
|
||||
/>
|
||||
)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Text variant="body-small" color="secondary">
|
||||
{!notification.user && (
|
||||
<RiRssFill size={14} className={styles.broadcastIcon} />
|
||||
)}
|
||||
{notification.origin && (
|
||||
<>
|
||||
<span className={styles.notificationInfoRow}>
|
||||
{notification.origin}
|
||||
</span>
|
||||
•
|
||||
</>
|
||||
)}
|
||||
{notification.payload.topic && (
|
||||
<>
|
||||
<span className={styles.notificationInfoRow}>
|
||||
{notification.payload.topic}
|
||||
</span>
|
||||
•
|
||||
</>
|
||||
)}
|
||||
{notification.created && (
|
||||
<RelativeTime
|
||||
value={notification.created}
|
||||
className={styles.notificationInfoRow}
|
||||
/>
|
||||
)}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -319,44 +289,63 @@ export const NotificationsTable = ({
|
||||
onSwitchSavedStatus,
|
||||
onMarkAllRead,
|
||||
onNotificationsSelectChange,
|
||||
classes.severityItem,
|
||||
classes.broadcastIcon,
|
||||
classes.notificationInfoRow,
|
||||
markAsReadOnLinkOpen,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Table<Notification>
|
||||
isLoading={isLoading}
|
||||
options={{
|
||||
padding: 'dense',
|
||||
search: true,
|
||||
paging: true,
|
||||
pageSize,
|
||||
header: true,
|
||||
sorting: false,
|
||||
}}
|
||||
title={title}
|
||||
onPageChange={onPageChange}
|
||||
onRowsPerPageChange={onRowsPerPageChange}
|
||||
page={page}
|
||||
totalCount={totalCount}
|
||||
onSearchChange={throttledContainsTextHandler}
|
||||
data={notifications}
|
||||
columns={compactColumns}
|
||||
localization={{
|
||||
body: {
|
||||
emptyDataSourceMessage: t('table.emptyMessage'),
|
||||
},
|
||||
pagination: {
|
||||
firstTooltip: t('table.pagination.firstTooltip'),
|
||||
labelDisplayedRows: t('table.pagination.labelDisplayedRows'),
|
||||
labelRowsSelect: t('table.pagination.labelRowsSelect'),
|
||||
lastTooltip: t('table.pagination.lastTooltip'),
|
||||
nextTooltip: t('table.pagination.nextTooltip'),
|
||||
previousTooltip: t('table.pagination.previousTooltip'),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
<Table<Notification>
|
||||
isLoading={isLoading}
|
||||
options={{
|
||||
padding: 'dense',
|
||||
search: true,
|
||||
paging: true,
|
||||
pageSize,
|
||||
header: true,
|
||||
sorting: false,
|
||||
}}
|
||||
title={title}
|
||||
onPageChange={onPageChange}
|
||||
onRowsPerPageChange={onRowsPerPageChange}
|
||||
page={page}
|
||||
totalCount={totalCount}
|
||||
onSearchChange={throttledContainsTextHandler}
|
||||
data={notifications}
|
||||
columns={compactColumns}
|
||||
localization={{
|
||||
body: {
|
||||
emptyDataSourceMessage: t('table.emptyMessage'),
|
||||
},
|
||||
pagination: {
|
||||
firstTooltip: t('table.pagination.firstTooltip'),
|
||||
labelDisplayedRows: t('table.pagination.labelDisplayedRows'),
|
||||
labelRowsSelect: t('table.pagination.labelRowsSelect'),
|
||||
lastTooltip: t('table.pagination.lastTooltip'),
|
||||
nextTooltip: t('table.pagination.nextTooltip'),
|
||||
previousTooltip: t('table.pagination.previousTooltip'),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Dialog
|
||||
isOpen={confirmDialogOpen}
|
||||
onOpenChange={setConfirmDialogOpen}
|
||||
isDismissable
|
||||
>
|
||||
<DialogHeader>{t('table.confirmDialog.title')}</DialogHeader>
|
||||
<DialogBody>
|
||||
<Text variant="body-medium">
|
||||
{t('table.confirmDialog.markAllReadDescription')}
|
||||
</Text>
|
||||
</DialogBody>
|
||||
<DialogFooter>
|
||||
<Button variant="primary" onPress={doMarkAllRead}>
|
||||
{t('table.confirmDialog.markAllReadConfirmation')}
|
||||
</Button>
|
||||
<Button variant="secondary" slot="close">
|
||||
{t('table.confirmDialog.cancel')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@layer components {
|
||||
.label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 0;
|
||||
max-width: 4rem;
|
||||
gap: var(--bui-space-1);
|
||||
cursor: pointer;
|
||||
font-size: var(--bui-font-size-1);
|
||||
color: var(--bui-fg-primary);
|
||||
}
|
||||
}
|
||||
@@ -13,21 +13,8 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import Checkbox from '@material-ui/core/Checkbox';
|
||||
import FormControlLabel from '@material-ui/core/FormControlLabel';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
label: {
|
||||
marginLeft: '0px',
|
||||
maxWidth: '2rem',
|
||||
'& span': {
|
||||
paddingRight: '0px',
|
||||
marginRight: '2px',
|
||||
},
|
||||
},
|
||||
});
|
||||
import { Checkbox, Tooltip, TooltipTrigger } from '@backstage/ui';
|
||||
import styles from './SelectAll.module.css';
|
||||
|
||||
export const SelectAll = ({
|
||||
count,
|
||||
@@ -38,23 +25,19 @@ export const SelectAll = ({
|
||||
totalCount: number;
|
||||
onSelectAll: () => void;
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<FormControlLabel
|
||||
label={count > 0 ? `(${count})` : undefined}
|
||||
className={classes.label}
|
||||
control={
|
||||
<Tooltip title="Select all">
|
||||
<Checkbox
|
||||
color="primary"
|
||||
disabled={!totalCount}
|
||||
checked={count > 0}
|
||||
indeterminate={count > 0 && totalCount !== count}
|
||||
onChange={onSelectAll}
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
<TooltipTrigger>
|
||||
<Checkbox
|
||||
className={styles.label}
|
||||
aria-label="Select all"
|
||||
isDisabled={!totalCount}
|
||||
isSelected={count > 0}
|
||||
isIndeterminate={count > 0 && totalCount !== count}
|
||||
onChange={() => onSelectAll()}
|
||||
>
|
||||
{count > 0 ? `(${count})` : undefined}
|
||||
</Checkbox>
|
||||
<Tooltip>Select all</Tooltip>
|
||||
</TooltipTrigger>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@layer components {
|
||||
.critical {
|
||||
color: var(--bui-fg-danger);
|
||||
}
|
||||
|
||||
.high {
|
||||
color: var(--bui-fg-warning);
|
||||
}
|
||||
|
||||
.normal {
|
||||
color: var(--bui-fg-success);
|
||||
}
|
||||
|
||||
.low {
|
||||
color: var(--bui-fg-info);
|
||||
}
|
||||
}
|
||||
@@ -14,43 +14,56 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { NotificationSeverity } from '@backstage/plugin-notifications-common';
|
||||
import NormalIcon from '@material-ui/icons/CheckOutlined';
|
||||
import CriticalIcon from '@material-ui/icons/ErrorOutline';
|
||||
import HighIcon from '@material-ui/icons/WarningOutlined';
|
||||
import LowIcon from '@material-ui/icons/InfoOutlined';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
critical: {
|
||||
color: theme.palette.status.error,
|
||||
},
|
||||
high: {
|
||||
color: theme.palette.status.warning,
|
||||
},
|
||||
normal: {
|
||||
color: theme.palette.status.ok,
|
||||
},
|
||||
low: {
|
||||
color: theme.palette.status.running,
|
||||
},
|
||||
}));
|
||||
import {
|
||||
RiCheckLine,
|
||||
RiErrorWarningLine,
|
||||
RiAlertLine,
|
||||
RiInformationLine,
|
||||
} from '@remixicon/react';
|
||||
import styles from './SeverityIcon.module.css';
|
||||
|
||||
export const SeverityIcon = ({
|
||||
severity,
|
||||
className,
|
||||
style,
|
||||
}: {
|
||||
severity?: NotificationSeverity;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
|
||||
switch (severity) {
|
||||
case 'critical':
|
||||
return <CriticalIcon className={classes.critical} />;
|
||||
return (
|
||||
<RiErrorWarningLine
|
||||
size={20}
|
||||
className={[styles.critical, className].filter(Boolean).join(' ')}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
case 'high':
|
||||
return <HighIcon className={classes.high} />;
|
||||
return (
|
||||
<RiAlertLine
|
||||
size={20}
|
||||
className={[styles.high, className].filter(Boolean).join(' ')}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
case 'low':
|
||||
return <LowIcon className={classes.low} />;
|
||||
return (
|
||||
<RiInformationLine
|
||||
size={20}
|
||||
className={[styles.low, className].filter(Boolean).join(' ')}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
case 'normal':
|
||||
default:
|
||||
return <NormalIcon className={classes.normal} />;
|
||||
return (
|
||||
<RiCheckLine
|
||||
size={20}
|
||||
className={[styles.normal, className].filter(Boolean).join(' ')}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@layer components {
|
||||
.cell {
|
||||
border-bottom: none;
|
||||
padding: var(--bui-space-2) var(--bui-space-3);
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
+13
-7
@@ -14,11 +14,17 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import MuiTableCell from '@material-ui/core/TableCell';
|
||||
import { ReactNode } from 'react';
|
||||
import styles from './NoBorderTableCell.module.css';
|
||||
|
||||
export const NoBorderTableCell = withStyles({
|
||||
root: {
|
||||
borderBottom: 'none',
|
||||
},
|
||||
})(MuiTableCell);
|
||||
export const NoBorderTableCell = ({
|
||||
children,
|
||||
align,
|
||||
}: {
|
||||
children?: ReactNode;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
}) => (
|
||||
<td className={styles.cell} style={align ? { textAlign: align } : undefined}>
|
||||
{children}
|
||||
</td>
|
||||
);
|
||||
|
||||
@@ -19,12 +19,8 @@ import {
|
||||
NotificationSettings,
|
||||
OriginSetting,
|
||||
} from '@backstage/plugin-notifications-common';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Switch from '@material-ui/core/Switch';
|
||||
import TableRow from '@material-ui/core/TableRow';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
|
||||
import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp';
|
||||
import { ButtonIcon, Switch, Tooltip, TooltipTrigger } from '@backstage/ui';
|
||||
import { RiArrowDownSLine, RiArrowUpSLine } from '@remixicon/react';
|
||||
import { NoBorderTableCell } from './NoBorderTableCell';
|
||||
import { useNotificationFormat } from './UserNotificationSettingsCard';
|
||||
|
||||
@@ -43,45 +39,50 @@ export const OriginRow = (props: {
|
||||
const { origin, settings, handleChange, open, handleRowToggle } = props;
|
||||
const { formatOriginName } = useNotificationFormat();
|
||||
return (
|
||||
<TableRow>
|
||||
<tr>
|
||||
<NoBorderTableCell>
|
||||
{origin.topics && origin.topics.length > 0 && (
|
||||
<Tooltip
|
||||
title={`Show Topics for the ${formatOriginName(origin.id)} origin`}
|
||||
>
|
||||
<IconButton
|
||||
<TooltipTrigger>
|
||||
<ButtonIcon
|
||||
aria-label="expand row"
|
||||
size="small"
|
||||
onClick={() => handleRowToggle(origin.id)}
|
||||
>
|
||||
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
onPress={() => handleRowToggle(origin.id)}
|
||||
icon={
|
||||
open ? (
|
||||
<RiArrowUpSLine size={16} />
|
||||
) : (
|
||||
<RiArrowDownSLine size={16} />
|
||||
)
|
||||
}
|
||||
variant="secondary"
|
||||
/>
|
||||
<Tooltip>{`Show Topics for the ${formatOriginName(
|
||||
origin.id,
|
||||
)} origin`}</Tooltip>
|
||||
</TooltipTrigger>
|
||||
)}
|
||||
</NoBorderTableCell>
|
||||
<NoBorderTableCell>{formatOriginName(origin.id)}</NoBorderTableCell>
|
||||
<NoBorderTableCell>all</NoBorderTableCell>
|
||||
{settings.channels.map(ch => (
|
||||
<NoBorderTableCell key={ch.id} align="center">
|
||||
<Tooltip
|
||||
title={`Enable or disable ${ch.id.toLocaleLowerCase(
|
||||
'en-US',
|
||||
)} notifications from ${formatOriginName(origin.id)}`}
|
||||
>
|
||||
<TooltipTrigger>
|
||||
<Switch
|
||||
checked={isNotificationsEnabledFor(
|
||||
isSelected={isNotificationsEnabledFor(
|
||||
settings,
|
||||
ch.id,
|
||||
origin.id,
|
||||
null,
|
||||
)}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
handleChange(ch.id, origin.id, null, event.target.checked);
|
||||
onChange={(isSelected: boolean) => {
|
||||
handleChange(ch.id, origin.id, null, isSelected);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip>{`Enable or disable ${ch.id.toLocaleLowerCase(
|
||||
'en-US',
|
||||
)} notifications from ${formatOriginName(origin.id)}`}</Tooltip>
|
||||
</TooltipTrigger>
|
||||
</NoBorderTableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -20,19 +20,10 @@ import {
|
||||
OriginSetting,
|
||||
TopicSetting,
|
||||
} from '@backstage/plugin-notifications-common';
|
||||
import TableRow from '@material-ui/core/TableRow';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import Switch from '@material-ui/core/Switch';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import { Switch, Tooltip, TooltipTrigger } from '@backstage/ui';
|
||||
import { NoBorderTableCell } from './NoBorderTableCell';
|
||||
import { useNotificationFormat } from './UserNotificationSettingsCard';
|
||||
|
||||
const TopicTableRow = withStyles({
|
||||
root: {
|
||||
paddingLeft: '4px',
|
||||
},
|
||||
})(TableRow);
|
||||
|
||||
export const TopicRow = (props: {
|
||||
topic: TopicSetting;
|
||||
origin: OriginSetting;
|
||||
@@ -47,33 +38,32 @@ export const TopicRow = (props: {
|
||||
const { topic, origin, settings, handleChange } = props;
|
||||
const { formatOriginName, formatTopicName } = useNotificationFormat();
|
||||
return (
|
||||
<TopicTableRow>
|
||||
<tr>
|
||||
<NoBorderTableCell />
|
||||
<NoBorderTableCell />
|
||||
<NoBorderTableCell>{formatTopicName(topic.id)}</NoBorderTableCell>
|
||||
{settings.channels.map(ch => (
|
||||
<NoBorderTableCell key={`${ch.id}-${topic}`} align="center">
|
||||
<Tooltip
|
||||
title={`Enable or disable ${ch.id.toLocaleLowerCase(
|
||||
'en-US',
|
||||
)} notifications for the ${formatTopicName(
|
||||
topic.id,
|
||||
)} topic from ${formatOriginName(origin.id)}`}
|
||||
>
|
||||
<NoBorderTableCell key={`${ch.id}-${topic.id}`} align="center">
|
||||
<TooltipTrigger>
|
||||
<Switch
|
||||
checked={isNotificationsEnabledFor(
|
||||
isSelected={isNotificationsEnabledFor(
|
||||
settings,
|
||||
ch.id,
|
||||
origin.id,
|
||||
topic.id,
|
||||
)}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
handleChange(ch.id, origin.id, topic.id, event.target.checked);
|
||||
onChange={(isSelected: boolean) => {
|
||||
handleChange(ch.id, origin.id, topic.id, isSelected);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip>{`Enable or disable ${ch.id.toLocaleLowerCase(
|
||||
'en-US',
|
||||
)} notifications for the ${formatTopicName(
|
||||
topic.id,
|
||||
)} topic from ${formatOriginName(origin.id)}`}</Tooltip>
|
||||
</TooltipTrigger>
|
||||
</NoBorderTableCell>
|
||||
))}
|
||||
</TopicTableRow>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@layer components {
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.headerCell {
|
||||
border-bottom: 1px solid var(--bui-border-1);
|
||||
padding: var(--bui-space-2) var(--bui-space-3);
|
||||
text-align: left;
|
||||
font-weight: var(--bui-font-weight-bold);
|
||||
}
|
||||
}
|
||||
+28
-35
@@ -19,21 +19,12 @@ import {
|
||||
NotificationSettings,
|
||||
OriginSetting,
|
||||
} from '@backstage/plugin-notifications-common';
|
||||
import Table from '@material-ui/core/Table';
|
||||
import MuiTableCell from '@material-ui/core/TableCell';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import TableHead from '@material-ui/core/TableHead';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import TableBody from '@material-ui/core/TableBody';
|
||||
import TableRow from '@material-ui/core/TableRow';
|
||||
import { Text } from '@backstage/ui';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { notificationsTranslationRef } from '../../translation';
|
||||
import { TopicRow } from './TopicRow';
|
||||
import { OriginRow } from './OriginRow';
|
||||
|
||||
const TableCell = withStyles({
|
||||
root: {
|
||||
borderBottom: 'none',
|
||||
},
|
||||
})(MuiTableCell);
|
||||
import styles from './UserNotificationSettingsPanel.module.css';
|
||||
|
||||
export const UserNotificationSettingsPanel = (props: {
|
||||
settings: NotificationSettings;
|
||||
@@ -42,6 +33,7 @@ export const UserNotificationSettingsPanel = (props: {
|
||||
topicNames?: Record<string, string>;
|
||||
}) => {
|
||||
const { settings, onChange } = props;
|
||||
const { t } = useTranslationRef(notificationsTranslationRef);
|
||||
const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set());
|
||||
|
||||
const handleRowToggle = (originId: string) => {
|
||||
@@ -106,9 +98,7 @@ export const UserNotificationSettingsPanel = (props: {
|
||||
|
||||
if (settings.channels.length === 0) {
|
||||
return (
|
||||
<Typography variant="body1">
|
||||
No notification settings available, check back later
|
||||
</Typography>
|
||||
<Text variant="body-medium">{t('settings.noSettingsAvailable')}</Text>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -125,26 +115,29 @@ export const UserNotificationSettingsPanel = (props: {
|
||||
const uniqueOrigins = Array.from(uniqueOriginsMap);
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell />
|
||||
<TableCell>
|
||||
<Typography variant="subtitle1">Origin</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="subtitle1">Topic</Typography>
|
||||
</TableCell>
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className={styles.headerCell} />
|
||||
<th className={styles.headerCell}>
|
||||
<Text variant="title-x-small">{t('settings.table.origin')}</Text>
|
||||
</th>
|
||||
<th className={styles.headerCell}>
|
||||
<Text variant="title-x-small">{t('settings.table.topic')}</Text>
|
||||
</th>
|
||||
{settings.channels.map(channel => (
|
||||
<TableCell key={channel.id}>
|
||||
<Typography variant="subtitle1" align="center">
|
||||
<th key={channel.id} className={styles.headerCell}>
|
||||
<Text
|
||||
variant="title-x-small"
|
||||
style={{ textAlign: 'center', display: 'block' }}
|
||||
>
|
||||
{channel.id}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</Text>
|
||||
</th>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{uniqueOrigins.flatMap(origin => [
|
||||
<OriginRow
|
||||
key={origin.id}
|
||||
@@ -166,7 +159,7 @@ export const UserNotificationSettingsPanel = (props: {
|
||||
)) || []
|
||||
: []),
|
||||
])}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -88,8 +88,9 @@ export const notificationsTranslationRef = createTranslationRef({
|
||||
},
|
||||
confirmDialog: {
|
||||
title: 'Are you sure?',
|
||||
markAllReadDescription: 'Mark <b>all</b> notifications as <b>read</b>.',
|
||||
markAllReadDescription: 'Mark all notifications as read.',
|
||||
markAllReadConfirmation: 'Mark All',
|
||||
cancel: 'Cancel',
|
||||
},
|
||||
errors: {
|
||||
markAllReadFailed: 'Failed to mark all notifications as read',
|
||||
|
||||
@@ -6277,14 +6277,11 @@ __metadata:
|
||||
"@backstage/test-utils": "workspace:^"
|
||||
"@backstage/theme": "workspace:^"
|
||||
"@backstage/ui": "workspace:^"
|
||||
"@material-ui/core": "npm:^4.9.13"
|
||||
"@material-ui/icons": "npm:^4.9.1"
|
||||
"@remixicon/react": "npm:^4.6.0"
|
||||
"@testing-library/jest-dom": "npm:^6.0.0"
|
||||
"@testing-library/react": "npm:^16.0.0"
|
||||
"@types/react": "npm:^18.0.0"
|
||||
lodash: "npm:^4.17.21"
|
||||
material-ui-confirm: "npm:^3.0.12"
|
||||
msw: "npm:^1.0.0"
|
||||
notistack: "npm:^3.0.1"
|
||||
react: "npm:^18.0.2"
|
||||
@@ -37746,17 +37743,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"material-ui-confirm@npm:^3.0.12":
|
||||
version: 3.0.18
|
||||
resolution: "material-ui-confirm@npm:3.0.18"
|
||||
peerDependencies:
|
||||
"@mui/material": ">= 5.0.0"
|
||||
react: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
checksum: 10/c2013449b9af11bed9fbf63cb15022d8399379547fedc8ce9f9c4039606536b34cce8b737b82b9628df95291d7b631dba90dd59c6ccbc7c17f9b8dcab50fdccf
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"material-ui-popup-state@npm:^5.3.6":
|
||||
version: 5.3.6
|
||||
resolution: "material-ui-popup-state@npm:5.3.6"
|
||||
|
||||
Reference in New Issue
Block a user