mirror of
https://github.com/gravitational/teleport.git
synced 2026-11-03 16:38:35 +08:00
242 lines
5.8 KiB
TypeScript
242 lines
5.8 KiB
TypeScript
/*
|
|
Copyright 2022 Gravitational, Inc.
|
|
|
|
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, useRef, useState } from 'react';
|
|
import styled, { css, useTheme } from 'styled-components';
|
|
import { ButtonIcon, Flex, Link, Text } from 'design';
|
|
import { Close } from 'design/Icon';
|
|
|
|
import type { NotificationItem, NotificationItemContent } from './types';
|
|
|
|
interface NotificationProps {
|
|
item: NotificationItem;
|
|
|
|
onRemove(): void;
|
|
|
|
Icon: React.ElementType;
|
|
|
|
getColor(theme): string;
|
|
|
|
isAutoRemovable: boolean;
|
|
autoRemoveDurationMs?: number;
|
|
// Workaround until `styled` gets types.
|
|
// Once the types are available, we can switch the type of Notification props to:
|
|
//
|
|
// NotificationProps & React.ComponentProps<typeof Container>
|
|
//
|
|
// and remove the next line.
|
|
[key: string]: any;
|
|
}
|
|
|
|
const defaultAutoRemoveDurationMs = 10_000; // 10s
|
|
|
|
export function Notification(props: NotificationProps) {
|
|
const {
|
|
item,
|
|
onRemove,
|
|
Icon,
|
|
getColor,
|
|
isAutoRemovable,
|
|
autoRemoveDurationMs,
|
|
...styleProps
|
|
} = props;
|
|
const [isHovered, setIsHovered] = useState(false);
|
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
const timeoutHandler = useRef<number>();
|
|
const theme = useTheme();
|
|
|
|
useEffect(() => {
|
|
if (!isHovered && isAutoRemovable) {
|
|
timeoutHandler.current = setTimeout(
|
|
onRemove,
|
|
autoRemoveDurationMs || defaultAutoRemoveDurationMs
|
|
) as unknown as number;
|
|
}
|
|
|
|
return () => {
|
|
if (timeoutHandler.current) {
|
|
clearTimeout(timeoutHandler.current);
|
|
}
|
|
};
|
|
}, [isHovered]);
|
|
|
|
function toggleIsExpanded() {
|
|
setIsExpanded(wasExpanded => !wasExpanded);
|
|
}
|
|
|
|
const removeIcon = (
|
|
<ButtonIcon
|
|
size={0}
|
|
ml={1}
|
|
mr={-1}
|
|
alignSelf="baseline"
|
|
style={{ visibility: isHovered ? 'visible' : 'hidden' }}
|
|
onClick={e => {
|
|
e.stopPropagation();
|
|
onRemove();
|
|
}}
|
|
>
|
|
<Close />
|
|
</ButtonIcon>
|
|
);
|
|
|
|
return (
|
|
<Container
|
|
py={2}
|
|
pl={3}
|
|
pr={2}
|
|
onMouseOver={() => {
|
|
if (isHovered === false) {
|
|
setIsHovered(true);
|
|
}
|
|
}}
|
|
onMouseLeave={() => {
|
|
if (isHovered === true) {
|
|
setIsHovered(false);
|
|
}
|
|
}}
|
|
onClick={toggleIsExpanded}
|
|
{...styleProps}
|
|
>
|
|
<Flex alignItems="center" mr={1} minWidth="0" width="100%">
|
|
<Icon color={getColor(theme)} mr={3} fontSize={16} />
|
|
{getRenderedContent(item.content, isExpanded, removeIcon)}
|
|
</Flex>
|
|
</Container>
|
|
);
|
|
}
|
|
|
|
function getRenderedContent(
|
|
content: NotificationItemContent,
|
|
isExpanded: boolean,
|
|
removeIcon: React.ReactNode
|
|
) {
|
|
const longerTextCss = isExpanded ? textCss : shortTextCss;
|
|
|
|
if (typeof content === 'string') {
|
|
return (
|
|
<Flex alignItems="center" justifyContent="space-between" width="100%">
|
|
<Text
|
|
typography="body1"
|
|
fontSize={13}
|
|
lineHeight={20}
|
|
css={longerTextCss}
|
|
>
|
|
{content}
|
|
</Text>
|
|
{removeIcon}
|
|
</Flex>
|
|
);
|
|
}
|
|
if (typeof content === 'object') {
|
|
return (
|
|
<Flex flexDirection="column" minWidth="0" width="100%">
|
|
<div
|
|
css={`
|
|
position: relative;
|
|
`}
|
|
>
|
|
<Text
|
|
fontSize={13}
|
|
bold
|
|
mr="30px"
|
|
css={`
|
|
line-height: 20px;
|
|
`}
|
|
>
|
|
{content.title}
|
|
</Text>
|
|
<div
|
|
css={`
|
|
position: absolute;
|
|
top: 0;
|
|
right: 0;
|
|
`}
|
|
>
|
|
{removeIcon}
|
|
</div>
|
|
</div>
|
|
<Text
|
|
fontSize={13}
|
|
lineHeight={20}
|
|
color="text.secondary"
|
|
css={longerTextCss}
|
|
>
|
|
{content.list && <List items={content.list} />}
|
|
{content.description}
|
|
{content.link && (
|
|
<Link
|
|
css={`
|
|
display: block;
|
|
`}
|
|
href={content.link.href}
|
|
target="_blank"
|
|
>
|
|
{content.link.text}
|
|
</Link>
|
|
)}
|
|
</Text>
|
|
</Flex>
|
|
);
|
|
}
|
|
}
|
|
|
|
function List(props: { items: string[] }) {
|
|
return (
|
|
<ul
|
|
// Ideally we'd align the bullet point to the left without using list-style-position: inside
|
|
// (because it looks bad when the list item spans multiple lines).
|
|
//
|
|
// However, it seems impossible to use padding-inline-start for that because the result looks
|
|
// different on Retina vs non-Retina screens, the bullet point looks cut off on the latter if
|
|
// padding-inline-start is set to 1em. So instead we just set it to 2em.
|
|
css={`
|
|
margin: 0;
|
|
padding-inline-start: 2em;
|
|
`}
|
|
>
|
|
{props.items.map((item, index) => (
|
|
<li key={index}>{item}</li>
|
|
))}
|
|
</ul>
|
|
);
|
|
}
|
|
|
|
const textCss = css`
|
|
line-height: 20px;
|
|
overflow-wrap: break-word;
|
|
white-space: pre-line;
|
|
`;
|
|
|
|
const shortTextCss = css`
|
|
${textCss};
|
|
display: -webkit-box;
|
|
-webkit-box-orient: vertical;
|
|
-webkit-line-clamp: 3;
|
|
`;
|
|
|
|
const Container = styled(Flex)`
|
|
flex-direction: row;
|
|
justify-content: space-between;
|
|
background: ${props => props.theme.colors.primary.light};
|
|
min-height: 40px;
|
|
width: 320px;
|
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.24);
|
|
color: ${props => props.theme.colors.text.primary};
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
`;
|