reduces component bloat; adds logic to ensure topics are always disabled if the origin is

Signed-off-by: Billy Stalnaker <bstalnaker@roadie.com>
This commit is contained in:
Billy Stalnaker
2025-03-26 09:43:40 -04:00
parent a7d0a0b2f5
commit d9e99214fb
6 changed files with 281 additions and 129 deletions
@@ -0,0 +1,24 @@
/*
* 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.
*/
import { withStyles } from '@material-ui/core/styles';
import MuiTableCell from '@material-ui/core/TableCell';
export const NoBorderTableCell = withStyles({
root: {
borderBottom: 'none',
},
})(MuiTableCell);
@@ -0,0 +1,91 @@
/*
* 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.
*/
import React from 'react';
import {
ChannelSetting,
isNotificationsEnabledFor,
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 { NoBorderTableCell } from './NoBorderTableCell';
import { useNotificationFormat } from './UserNotificationSettingsCard';
export const OriginRow = (props: {
channel: ChannelSetting;
origin: OriginSetting;
settings: NotificationSettings;
handleChange: (
channel: string,
origin: string,
topic: string | null,
enabled: boolean,
) => void;
open: boolean;
handleRowToggle: (originId: string) => void;
}) => {
const { channel, origin, settings, handleChange, open, handleRowToggle } =
props;
const { formatOriginName } = useNotificationFormat();
return (
<TableRow>
<NoBorderTableCell>
{origin.topics && origin.topics.length > 0 && (
<Tooltip
title={`Show Topics for the ${formatOriginName(origin.id)} origin`}
>
<IconButton
aria-label="expand row"
size="small"
onClick={() => handleRowToggle(origin.id)}
>
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
</IconButton>
</Tooltip>
)}
</NoBorderTableCell>
<NoBorderTableCell>{formatOriginName(origin.id)}</NoBorderTableCell>
<NoBorderTableCell>*</NoBorderTableCell>
{settings.channels.map(ch => (
<NoBorderTableCell key={ch.id} align="center">
<Tooltip
title={`Enable or disable ${channel.id.toLocaleLowerCase(
'en-US',
)} notifications from ${formatOriginName(origin.id)}`}
>
<Switch
checked={isNotificationsEnabledFor(
settings,
ch.id,
origin.id,
null,
)}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
handleChange(ch.id, origin.id, null, event.target.checked);
}}
/>
</Tooltip>
</NoBorderTableCell>
))}
</TableRow>
);
};
@@ -0,0 +1,80 @@
/*
* 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.
*/
import React from 'react';
import {
isNotificationsEnabledFor,
NotificationSettings,
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 { NoBorderTableCell } from './NoBorderTableCell';
import { useNotificationFormat } from './UserNotificationSettingsCard';
const TopicTableRow = withStyles({
root: {
paddingLeft: '4px',
},
})(TableRow);
export const TopicRow = (props: {
topic: TopicSetting;
origin: OriginSetting;
settings: NotificationSettings;
handleChange: (
channel: string,
origin: string,
topic: string | null,
enabled: boolean,
) => void;
}) => {
const { topic, origin, settings, handleChange } = props;
const { formatOriginName, formatTopicName } = useNotificationFormat();
return (
<TopicTableRow>
<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)}`}
>
<Switch
checked={isNotificationsEnabledFor(
settings,
ch.id,
origin.id,
topic.id,
)}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
handleChange(ch.id, origin.id, topic.id, event.target.checked);
}}
/>
</Tooltip>
</NoBorderTableCell>
))}
</TopicTableRow>
);
};
@@ -14,13 +14,69 @@
* limitations under the License.
*/
import React, { useEffect } from 'react';
import React, { createContext, useContext, useEffect } from 'react';
import { ErrorPanel, InfoCard, Progress } from '@backstage/core-components';
import { useNotificationsApi } from '../../hooks';
import { NotificationSettings } from '@backstage/plugin-notifications-common';
import { notificationsApiRef } from '../../api';
import { useApi } from '@backstage/core-plugin-api';
import { UserNotificationSettingsPanel } from './UserNotificationSettingsPanel';
import { capitalize } from 'lodash';
type FormatContextType = {
formatOriginName: (id: string) => string;
formatTopicName: (id: string) => string;
};
const NotificationFormatContext = createContext<FormatContextType | undefined>(
undefined,
);
export const useNotificationFormat = () => {
const context = useContext(NotificationFormatContext);
if (!context)
throw new Error(
'useNotificationFormat must be used within a NotificationFormatProvider',
);
return context;
};
type Props = {
children: React.ReactNode;
originMap: Record<string, string> | undefined;
topicMap: Record<string, string> | undefined;
};
export const NotificationFormatProvider = ({
children,
originMap,
topicMap,
}: Props) => {
const formatName = (
id: string,
nameMap: Record<string, string> | undefined,
) => {
if (nameMap && id in nameMap) {
return nameMap[id];
}
return capitalize(id.replaceAll(/[-_:]/g, ' '));
};
const formatOriginName = (originId: string) => {
return formatName(originId, originMap);
};
const formatTopicName = (topicId: string) => {
return formatName(topicId, topicMap);
};
return (
<NotificationFormatContext.Provider
value={{ formatOriginName, formatTopicName }}
>
{children}
</NotificationFormatContext.Provider>
);
};
/** @public */
export const UserNotificationSettingsCard = (props: {
@@ -53,12 +109,15 @@ export const UserNotificationSettingsCard = (props: {
{loading && <Progress />}
{error && <ErrorPanel title="Failed to load settings" error={error} />}
{settings && (
<UserNotificationSettingsPanel
settings={settings}
onChange={onUpdate}
originNames={props.originNames}
topicNames={props.topicNames}
/>
<NotificationFormatProvider
originMap={props.originNames}
topicMap={props.topicNames}
>
<UserNotificationSettingsPanel
settings={settings}
onChange={onUpdate}
/>
</NotificationFormatProvider>
)}
</InfoCard>
);
@@ -15,23 +15,16 @@
*/
import React, { useState } from 'react';
import {
isNotificationsEnabledFor,
NotificationSettings,
} from '@backstage/plugin-notifications-common';
import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp';
import { NotificationSettings } 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 IconButton from '@material-ui/core/IconButton';
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 Switch from '@material-ui/core/Switch';
import { capitalize } from 'lodash';
import Tooltip from '@material-ui/core/Tooltip';
import { TopicRow } from './TopicRow';
import { OriginRow } from './OriginRow';
const TableCell = withStyles({
root: {
@@ -39,12 +32,6 @@ const TableCell = withStyles({
},
})(MuiTableCell);
const TopicTableRow = withStyles({
root: {
paddingLeft: '4px',
},
})(TableRow);
export const UserNotificationSettingsPanel = (props: {
settings: NotificationSettings;
onChange: (settings: NotificationSettings) => void;
@@ -113,23 +100,6 @@ export const UserNotificationSettingsPanel = (props: {
};
onChange(updatedSettings);
};
const formatName = (
id: string,
nameMap: Record<string, string> | undefined,
) => {
if (nameMap && id in nameMap) {
return nameMap[id];
}
return capitalize(id.replaceAll(/[-_:]/g, ' '));
};
const formatOriginName = (originId: string) => {
return formatName(originId, props.originNames);
};
const formatTopicName = (topicId: string) => {
return formatName(topicId, props.topicNames);
};
if (settings.channels.length === 0) {
return (
@@ -162,96 +132,24 @@ export const UserNotificationSettingsPanel = (props: {
<TableBody>
{settings.channels.map(channel =>
channel.origins.flatMap(origin => [
<TableRow key={`${channel.id}-${origin.id}`}>
<TableCell>
{origin.topics && origin.topics.length > 0 && (
<Tooltip
title={`Show Topics for the ${formatOriginName(
origin.id,
)} origin`}
>
<IconButton
aria-label="expand row"
size="small"
onClick={() => handleRowToggle(origin.id)}
>
{expandedRows.has(origin.id) ? (
<KeyboardArrowUpIcon />
) : (
<KeyboardArrowDownIcon />
)}
</IconButton>
</Tooltip>
)}
</TableCell>
<TableCell>{formatOriginName(origin.id)}</TableCell>
<TableCell>*</TableCell>
{settings.channels.map(ch => (
<TableCell key={ch.id} align="center">
<Tooltip
title={`Enable or disable ${channel.id.toLocaleLowerCase(
'en-US',
)} notifications from ${formatOriginName(origin.id)}`}
>
<Switch
checked={isNotificationsEnabledFor(
settings,
ch.id,
origin.id,
null,
)}
onChange={(
event: React.ChangeEvent<HTMLInputElement>,
) => {
handleChange(
ch.id,
origin.id,
null,
event.target.checked,
);
}}
/>
</Tooltip>
</TableCell>
))}
</TableRow>,
<OriginRow
key={origin.id}
channel={channel}
origin={origin}
settings={settings}
open={expandedRows.has(origin.id)}
handleChange={handleChange}
handleRowToggle={handleRowToggle}
/>,
...(expandedRows.has(origin.id)
? origin.topics?.map(topic => (
<TopicTableRow key={`${origin.id}-${topic.id}`}>
<TableCell />
<TableCell />
<TableCell>{formatTopicName(topic.id)}</TableCell>
{settings.channels.map(ch => (
<TableCell key={`${ch.id}-${topic.id}`} align="center">
<Tooltip
title={`Enable or disable ${channel.id.toLocaleLowerCase(
'en-US',
)} notifications for the ${formatTopicName(
topic.id,
)} topic from ${formatOriginName(origin.id)}`}
>
<Switch
checked={isNotificationsEnabledFor(
settings,
ch.id,
origin.id,
topic.id,
)}
onChange={(
event: React.ChangeEvent<HTMLInputElement>,
) => {
handleChange(
ch.id,
origin.id,
topic.id,
event.target.checked,
);
}}
/>
</Tooltip>
</TableCell>
))}
</TopicTableRow>
<TopicRow
key={`${origin.id}-${topic.id}`}
topic={topic}
origin={origin}
settings={settings}
handleChange={handleChange}
/>
)) || []
: []),
]),