From 440523cc00d6dc3824ffb3c5aaabb28dd271926b Mon Sep 17 00:00:00 2001 From: Lisa Kim Date: Wed, 4 Oct 2023 06:53:20 -0700 Subject: [PATCH 1/8] Add notification bell to top bar Also fixes topbar.test act warnings (even though tests passed) --- .../ClusterSelector/ClusterSelector.tsx | 6 +- .../TopBar/Notifications/Notifications.tsx | 129 ++++++++++++++++++ .../src/TopBar/Notifications/index.ts | 17 +++ web/packages/teleport/src/TopBar/Shared.tsx | 33 +++++ .../teleport/src/TopBar/TopBar.story.tsx | 50 ++++++- .../teleport/src/TopBar/TopBar.test.tsx | 86 +++++++----- web/packages/teleport/src/TopBar/TopBar.tsx | 27 +--- 7 files changed, 295 insertions(+), 53 deletions(-) create mode 100644 web/packages/teleport/src/TopBar/Notifications/Notifications.tsx create mode 100644 web/packages/teleport/src/TopBar/Notifications/index.ts create mode 100644 web/packages/teleport/src/TopBar/Shared.tsx diff --git a/web/packages/teleport/src/TopBar/ClusterSelector/ClusterSelector.tsx b/web/packages/teleport/src/TopBar/ClusterSelector/ClusterSelector.tsx index e813671b77034..edddfe6f7e7e7 100644 --- a/web/packages/teleport/src/TopBar/ClusterSelector/ClusterSelector.tsx +++ b/web/packages/teleport/src/TopBar/ClusterSelector/ClusterSelector.tsx @@ -82,7 +82,11 @@ export default function ClusterSelector({ } return ( - + ({ open, setOpen }); + + let transitionDelay = STARTING_TRANSITION_DELAY; + let items = notices.map(notice => { + let currentTransitionDelay = transitionDelay; + transitionDelay += INCREMENT_TRANSITION_DELAY; + + let item; + if (notice.kind === 'access-lists') { + item = ( + null}> + + + + + Access list {notice.resourceName} needs your review within{' '} + {formatDistanceToNow(notice.date)}. + + + ); + } + + return ( + + setOpen(false)}> + {item} + + + ); + }); + + return ( + + setOpen(!open)} + data-testid="tb-note-button" + > + {items.length > 0 && } + + + + + {items.length ? ( + items + ) : ( + + No notifications + + )} + + + ); +} + +const NotificationButtonContainer = styled.div` + position: relative; +`; + +const AttentionDot = styled.div` + position: absolute; + width: 7px; + height: 7px; + border-radius: 100px; + background-color: ${p => p.theme.colors.buttons.warning.default}; + top: 10px; + right: 15px; +`; + +export const NotificationItemButton = styled(DropdownItemButton)` + align-items: flex-start; + line-height: 20px; +`; + +export const NotificationLink = styled(DropdownItemLink)` + padding: 0; +`; diff --git a/web/packages/teleport/src/TopBar/Notifications/index.ts b/web/packages/teleport/src/TopBar/Notifications/index.ts new file mode 100644 index 0000000000000..0d5bf6f3eb944 --- /dev/null +++ b/web/packages/teleport/src/TopBar/Notifications/index.ts @@ -0,0 +1,17 @@ +/** + * Copyright 2023 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. + */ + +export { Notifications } from './Notifications'; diff --git a/web/packages/teleport/src/TopBar/Shared.tsx b/web/packages/teleport/src/TopBar/Shared.tsx new file mode 100644 index 0000000000000..59fe7d617738a --- /dev/null +++ b/web/packages/teleport/src/TopBar/Shared.tsx @@ -0,0 +1,33 @@ +/** + * Copyright 2023 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 styled from 'styled-components'; + +export const ButtonIconContainer = styled.div` + padding: 0 10px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 5px; + cursor: pointer; + user-select: none; + margin-right: 5px; + + &:hover { + background: ${props => props.theme.colors.spotBackground[0]}; + } +`; diff --git a/web/packages/teleport/src/TopBar/TopBar.story.tsx b/web/packages/teleport/src/TopBar/TopBar.story.tsx index 85b3673cc1ead..9aacdc9d056ce 100644 --- a/web/packages/teleport/src/TopBar/TopBar.story.tsx +++ b/web/packages/teleport/src/TopBar/TopBar.story.tsx @@ -55,5 +55,53 @@ export function Story() { ); } - Story.storyName = 'TopBar'; + +export function TopBarWithNotifications() { + const ctx = new TeleportContext(); + + ctx.storeUser.state = makeUserContext({ + userName: 'admin', + cluster: { + name: 'test-cluster', + lastConnected: Date.now(), + }, + }); + ctx.storeNotifications.state = { + notices: [ + { + kind: 'access-lists', + id: '111', + resourceName: 'banana', + date: new Date(), + route: '', + }, + { + kind: 'access-lists', + id: '222', + resourceName: 'apple', + date: new Date(), + route: '', + }, + { + kind: 'access-lists', + id: '333', + resourceName: 'carrot', + date: new Date(), + route: '', + }, + ], + }; + + return ( + + + + + + + + + + ); +} diff --git a/web/packages/teleport/src/TopBar/TopBar.test.tsx b/web/packages/teleport/src/TopBar/TopBar.test.tsx index 41a145f506ff6..2ffd3e70b63fd 100644 --- a/web/packages/teleport/src/TopBar/TopBar.test.tsx +++ b/web/packages/teleport/src/TopBar/TopBar.test.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { render, screen } from 'design/utils/testing'; +import { render, screen, userEvent } from 'design/utils/testing'; import { Router } from 'react-router'; import { createMemoryHistory } from 'history'; @@ -57,56 +57,80 @@ function setup(): void { mockUserContextProviderWith(makeTestUserContext()); } -test('does not show assist popup if hidePopup is true', () => { +test('does not show assist popup if hidePopup is true', async () => { setup(); - render( - - - - - - - - - - ); + render(getTopBar({ hidePopup: true })); + await screen.findByTestId('cluster-selector'); expect(screen.queryByTestId('assistPopup')).not.toBeInTheDocument(); }); -test('shows assist popup if hidePopup is absent', () => { +test('shows assist popup if hidePopup is absent', async () => { setup(); - render( - - - - - - - - - - ); + render(getTopBar({})); + await screen.findByTestId('cluster-selector'); + + expect(screen.getByTestId('assistPopup')).toBeInTheDocument(); +}); + +test('shows assist popup if hidePopup is false', async () => { + setup(); + + render(getTopBar({ hidePopup: false })); + await screen.findByTestId('cluster-selector'); expect(screen.getByTestId('assistPopup')).toBeInTheDocument(); }); -test('shows assist popup if hidePopup is false', () => { +test('notification bell without notification', async () => { + setup(); + + render(getTopBar({})); + await screen.findByTestId('cluster-selector'); + + expect(screen.getByTestId('tb-note')).toBeInTheDocument(); + expect(screen.queryByTestId('tb-note-attention')).not.toBeInTheDocument(); +}); + +test('notification bell with notification', async () => { setup(); + ctx.storeNotifications.state = { + notices: [ + { + kind: 'access-lists', + id: 'abc', + resourceName: 'banana', + date: new Date(), + route: '', + }, + ], + }; + + render(getTopBar({})); + await screen.findByTestId('cluster-selector'); + + expect(screen.getByTestId('tb-note')).toBeInTheDocument(); + expect(screen.getByTestId('tb-note-attention')).toBeInTheDocument(); + + // Test clicking and rendering of dropdown. + expect(screen.getByTestId('tb-note-dropdown')).not.toBeVisible(); + + await userEvent.click(screen.getByTestId('tb-note-button')); + expect(screen.getByTestId('tb-note-dropdown')).toBeVisible(); +}); - render( +const getTopBar = ({ hidePopup = null }: { hidePopup?: boolean }) => { + return ( - + ); - - expect(screen.getByTestId('assistPopup')).toBeInTheDocument(); -}); +}; diff --git a/web/packages/teleport/src/TopBar/TopBar.tsx b/web/packages/teleport/src/TopBar/TopBar.tsx index edd0d3a9288be..743602e806baf 100644 --- a/web/packages/teleport/src/TopBar/TopBar.tsx +++ b/web/packages/teleport/src/TopBar/TopBar.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { Suspense, useState } from 'react'; +import React, { Suspense, useState, lazy } from 'react'; import styled, { useTheme } from 'styled-components'; import { Flex, Text, TopNav } from 'design'; @@ -46,24 +46,10 @@ import { } from 'teleport/Assist/Popup/Popup'; import ClusterSelector from './ClusterSelector'; +import { Notifications } from './Notifications'; +import { ButtonIconContainer } from './Shared'; -const Assist = React.lazy(() => import('teleport/Assist')); - -const AssistButton = styled.div` - padding: 0 10px; - height: 48px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 5px; - cursor: pointer; - user-select: none; - margin-right: 5px; - - &:hover { - background: ${props => props.theme.colors.spotBackground[0]}; - } -`; +const Assist = lazy(() => import('teleport/Assist')); const AssistButtonContainer = styled.div` position: relative; @@ -189,9 +175,9 @@ export function TopBar({ hidePopup = false }: TopBarProps) { {!hasDockedElement && assistEnabled && ( - setShowAssist(true)}> + setShowAssist(true)}> - + {showAssistPopup && !hidePopup && ( <> @@ -217,6 +203,7 @@ export function TopBar({ hidePopup = false }: TopBarProps) { )} )} + From 74dbf8d4ccb3997cc68901d837238a6693482134 Mon Sep 17 00:00:00 2001 From: Lisa Kim Date: Wed, 4 Oct 2023 09:02:46 -0700 Subject: [PATCH 2/8] Add attention dot to navigation switcher --- .../Navigation/NavigationSwitcher.story.tsx | 79 ++++++++++++++++ .../Navigation/NavigationSwitcher.test.tsx | 91 +++++++++++++++++++ .../src/Navigation/NavigationSwitcher.tsx | 50 ++++++++-- 3 files changed, 213 insertions(+), 7 deletions(-) create mode 100644 web/packages/teleport/src/Navigation/NavigationSwitcher.story.tsx create mode 100644 web/packages/teleport/src/Navigation/NavigationSwitcher.test.tsx diff --git a/web/packages/teleport/src/Navigation/NavigationSwitcher.story.tsx b/web/packages/teleport/src/Navigation/NavigationSwitcher.story.tsx new file mode 100644 index 0000000000000..0d66caf2a1f5a --- /dev/null +++ b/web/packages/teleport/src/Navigation/NavigationSwitcher.story.tsx @@ -0,0 +1,79 @@ +/** + * Copyright 2023 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 from 'react'; +import Flex from 'design/Flex'; + +import { NavigationSwitcher } from './NavigationSwitcher'; +import { NavigationCategory } from './categories'; + +export default { + title: 'Teleport/Navigation', +}; + +const navItems = [ + { category: NavigationCategory.Management }, + { category: NavigationCategory.Resources }, +]; + +export function SwitcherResource() { + return ( + null} + items={navItems} + value={NavigationCategory.Resources} + /> + ); +} + +export function SwitcherManagement() { + return ( + null} + items={navItems} + value={NavigationCategory.Management} + /> + ); +} + +export function SwitcherRequiresManagementAttention() { + return ( + + null} + items={[ + { category: NavigationCategory.Resources }, + { category: NavigationCategory.Management, requiresAttention: true }, + ]} + value={NavigationCategory.Resources} + /> + + ); +} + +export function SwitcherRequiresResourcesAttention() { + return ( + + null} + items={[ + { category: NavigationCategory.Resources, requiresAttention: true }, + { category: NavigationCategory.Management }, + ]} + value={NavigationCategory.Management} + /> + + ); +} diff --git a/web/packages/teleport/src/Navigation/NavigationSwitcher.test.tsx b/web/packages/teleport/src/Navigation/NavigationSwitcher.test.tsx new file mode 100644 index 0000000000000..9cb3b3e1043bd --- /dev/null +++ b/web/packages/teleport/src/Navigation/NavigationSwitcher.test.tsx @@ -0,0 +1,91 @@ +/** + * Copyright 2023 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 from 'react'; +import { render, screen, userEvent } from 'design/utils/testing'; + +import { NavigationSwitcher } from './NavigationSwitcher'; +import { NavigationCategory } from './categories'; + +test('not requiring attention', async () => { + render( + null} + items={[ + { category: NavigationCategory.Management }, + { category: NavigationCategory.Resources }, + ]} + value={NavigationCategory.Resources} + /> + ); + + expect( + screen.queryByTestId('nav-switch-attention-dot') + ).not.toBeInTheDocument(); + expect(screen.queryByTestId('dd-item-attention-dot')).not.toBeInTheDocument(); + + // Test clicking + await userEvent.click(screen.getByTestId('nav-switch-button')); + expect( + screen.queryByTestId('nav-switch-attention-dot') + ).not.toBeInTheDocument(); + expect(screen.queryByTestId('dd-item-attention-dot')).not.toBeInTheDocument(); +}); + +test('requires attention: not at nav category target (management)', async () => { + render( + null} + items={[ + { category: NavigationCategory.Management, requiresAttention: true }, + { category: NavigationCategory.Resources }, + ]} + value={NavigationCategory.Resources} + /> + ); + + expect(screen.getByTestId('nav-switch-attention-dot')).toBeInTheDocument(); + expect(screen.queryByTestId('dd-item-attention-dot')).not.toBeVisible(); + + // Test clicking + await userEvent.click(screen.getByTestId('nav-switch-button')); + expect(screen.getByTestId('nav-switch-attention-dot')).toBeInTheDocument(); + expect(screen.getByTestId('dd-item-attention-dot')).toBeVisible(); +}); + +test('requires attention: being at the nav category target (management) should NOT render attention dot', async () => { + render( + null} + items={[ + { category: NavigationCategory.Management, requiresAttention: true }, + { category: NavigationCategory.Resources }, + ]} + value={NavigationCategory.Management} + /> + ); + + expect( + screen.queryByTestId('nav-switch-attention-dot') + ).not.toBeInTheDocument(); + expect(screen.queryByTestId('dd-item-attention-dot')).not.toBeInTheDocument(); + + // Test clicking + await userEvent.click(screen.getByTestId('nav-switch-button')); + expect( + screen.queryByTestId('nav-switch-attention-dot') + ).not.toBeInTheDocument(); + expect(screen.queryByTestId('dd-item-attention-dot')).not.toBeInTheDocument(); +}); diff --git a/web/packages/teleport/src/Navigation/NavigationSwitcher.tsx b/web/packages/teleport/src/Navigation/NavigationSwitcher.tsx index 86783ca0cf96b..24bb2d7b29e4d 100644 --- a/web/packages/teleport/src/Navigation/NavigationSwitcher.tsx +++ b/web/packages/teleport/src/Navigation/NavigationSwitcher.tsx @@ -21,10 +21,15 @@ import { ChevronDownIcon } from 'design/SVGIcon/ChevronDown'; import { NavigationCategory } from 'teleport/Navigation/categories'; +type NavigationItems = { + category: NavigationCategory; + requiresAttention?: boolean; +}; + interface NavigationSwitcherProps { onChange: (value: NavigationCategory) => void; value: NavigationCategory; - items: NavigationCategory[]; + items: NavigationItems[]; } interface OpenProps { @@ -117,7 +122,10 @@ export function NavigationSwitcher(props: NavigationSwitcherProps) { const activeValueRef = useRef(); const firstValueRef = useRef(); - const activeItem = props.items.find(item => item === props.value); + const activeItem = props.items.find(item => item.category === props.value); + const requiresAttentionButNotActive = props.items.some( + item => item.requiresAttention && item.category !== activeItem.category + ); const handleClickOutside = useCallback( (event: MouseEvent) => { @@ -219,28 +227,35 @@ export function NavigationSwitcher(props: NavigationSwitcherProps) { items.push( handleKeyDownLink(event, item)} + onKeyDown={event => handleKeyDownLink(event, item.category)} tabIndex={open ? 0 : -1} - onClick={() => handleChange(item)} + onClick={() => handleChange(item.category)} key={index} open={open} - active={item === props.value} + active={item.category === props.value} > - {item} + {item.category} + {item.requiresAttention && item.category !== activeItem.category && ( + + )} ); } return ( + {requiresAttentionButNotActive && ( + + )} setOpen(!open)} open={open} tabIndex={0} onKeyDown={handleKeyDown} + data-testid="nav-switch-button" > - {activeItem} + {activeItem.category} @@ -251,3 +266,24 @@ export function NavigationSwitcher(props: NavigationSwitcherProps) { ); } + +const NavSwitcherAttentionDot = styled.div` + position: absolute; + background-color: ${props => props.theme.colors.error.main}; + width: 10px; + height: 10px; + border-radius: 50%; + right: -3px; + top: -4px; + z-index: 100; +`; + +const DropDownItemAttentionDot = styled.div` + display: inline-block; + margin-left: 10px; + margin-top: 2px; + width: 7px; + height: 7px; + border-radius: 50%; + background-color: ${props => props.theme.colors.error.main}; +`; From 4cf4fd4ed36674b120591306b6e65987861f25bb Mon Sep 17 00:00:00 2001 From: Lisa Kim Date: Wed, 4 Oct 2023 09:03:12 -0700 Subject: [PATCH 3/8] Add attention dot to navigation item --- .../teleport/src/Navigation/Navigation.tsx | 10 +- .../src/Navigation/NavigationItem.test.tsx | 127 ++++++++++++------ .../src/Navigation/NavigationItem.tsx | 18 ++- 3 files changed, 111 insertions(+), 44 deletions(-) diff --git a/web/packages/teleport/src/Navigation/Navigation.tsx b/web/packages/teleport/src/Navigation/Navigation.tsx index a979dff351262..719d6ae18e9f4 100644 --- a/web/packages/teleport/src/Navigation/Navigation.tsx +++ b/web/packages/teleport/src/Navigation/Navigation.tsx @@ -176,7 +176,15 @@ export function Navigation({ n.kind === 'access-lists'), + }, + { category: NavigationCategory.Resources }, + ]} /> )} diff --git a/web/packages/teleport/src/Navigation/NavigationItem.test.tsx b/web/packages/teleport/src/Navigation/NavigationItem.test.tsx index 1c97c35efc140..82b9bcb3a925b 100644 --- a/web/packages/teleport/src/Navigation/NavigationItem.test.tsx +++ b/web/packages/teleport/src/Navigation/NavigationItem.test.tsx @@ -20,7 +20,7 @@ import { render, screen } from 'design/utils/testing'; import { generatePath, Router } from 'react-router'; -import { createMemoryHistory } from 'history'; +import { createMemoryHistory, MemoryHistory } from 'history'; import TeleportContextProvider from 'teleport/TeleportContextProvider'; import TeleportContext from 'teleport/teleportContext'; @@ -31,7 +31,7 @@ import { NavigationItem } from 'teleport/Navigation/NavigationItem'; import { NavigationItemSize } from 'teleport/Navigation/common'; import { makeUserContext } from 'teleport/services/user'; -class MockFeature implements TeleportFeature { +class MockUserFeature implements TeleportFeature { category = NavigationCategory.Resources; route = { @@ -55,32 +55,50 @@ class MockFeature implements TeleportFeature { }; } +class MockAccessListFeature implements TeleportFeature { + category = NavigationCategory.Resources; + + route = { + title: 'Users', + path: '/web/cluster/:clusterId/feature', + exact: true, + component: () =>
Test!
, + }; + + hasAccess() { + return true; + } + + navigationItem = { + title: NavTitle.AccessLists, + icon:
, + exact: true, + getLink(clusterId: string) { + return generatePath('/web/cluster/:clusterId/feature', { clusterId }); + }, + }; +} + describe('navigation items', () => { - it('should render the feature link correctly', () => { - const history = createMemoryHistory({ + let ctx: TeleportContext; + let history: MemoryHistory; + + beforeEach(() => { + history = createMemoryHistory({ initialEntries: ['/web/cluster/root/feature'], }); - const ctx = new TeleportContext(); + ctx = new TeleportContext(); ctx.storeUser.state = makeUserContext({ cluster: { name: 'test-cluster', lastConnected: Date.now(), }, }); + }); - render( - - - - - - ); + it('should render the feature link correctly', () => { + render(getNavigationItem({ ctx, history })); expect(screen.getByText('Users').closest('a')).toHaveAttribute( 'href', @@ -89,30 +107,7 @@ describe('navigation items', () => { }); it('should change the feature link to the leaf cluster when navigating to a leaf cluster', () => { - const history = createMemoryHistory({ - initialEntries: ['/web/cluster/root/feature'], - }); - - const ctx = new TeleportContext(); - ctx.storeUser.state = makeUserContext({ - cluster: { - name: 'test-cluster', - lastConnected: Date.now(), - }, - }); - - render( - - - - - - ); + render(getNavigationItem({ ctx, history })); expect(screen.getByText('Users').closest('a')).toHaveAttribute( 'href', @@ -126,4 +121,54 @@ describe('navigation items', () => { '/web/cluster/leaf/feature' ); }); + + it('rendeirng of attention dot for access list', () => { + const { rerender } = render( + getNavigationItem({ ctx, history, feature: new MockAccessListFeature() }) + ); + + expect( + screen.queryByTestId('nav-item-attention-dot') + ).not.toBeInTheDocument(); + + // Add in some notifications + ctx.storeNotifications.setNotifications([ + { + kind: 'access-lists', + id: 'abc', + resourceName: 'banana', + date: new Date(), + route: '', + }, + ]); + + rerender( + getNavigationItem({ ctx, history, feature: new MockAccessListFeature() }) + ); + + expect(screen.getByTestId('nav-item-attention-dot')).toBeInTheDocument(); + }); }); + +function getNavigationItem({ + ctx, + history, + feature = new MockUserFeature(), +}: { + ctx: TeleportContext; + history: MemoryHistory; + feature?: TeleportFeature; +}) { + return ( + + + + + + ); +} diff --git a/web/packages/teleport/src/Navigation/NavigationItem.tsx b/web/packages/teleport/src/Navigation/NavigationItem.tsx index 7b96af039d85a..4bfaf25a0964a 100644 --- a/web/packages/teleport/src/Navigation/NavigationItem.tsx +++ b/web/packages/teleport/src/Navigation/NavigationItem.tsx @@ -170,6 +170,18 @@ export function NavigationItem(props: NavigationItemProps) { // renderHighlightFeature returns red dot component if the feature recommendation state is 'NOTIFY' function renderHighlightFeature(featureName: NavTitle): JSX.Element { + if (featureName === NavTitle.AccessLists) { + const hasNotifications = ctx.storeNotifications + .getNotifications() + .some(n => n.kind === 'access-lists'); + + if (hasNotifications) { + return ; + } + + return null; + } + // Get onboarding status. We'll only recommend features once user completes // initial onboarding (i.e. connect resources to Teleport cluster). const onboard = localStorage.getOnboardDiscover(); @@ -183,7 +195,7 @@ export function NavigationItem(props: NavigationItemProps) { featureName === NavTitle.TrustedDevices && recommendFeatureStatus?.TrustedDevices === RecommendationStatus.Notify ) { - return ; + return ; } return null; } @@ -264,7 +276,9 @@ export function NavigationItem(props: NavigationItemProps) { ); } -const RedDot = styled.div` +const AttentionDot = styled.div.attrs(() => ({ + 'data-testid': 'nav-item-attention-dot', +}))` margin-left: 15px; margin-top: 2px; width: 7px; From 70a2afed679b08674a4b53d99b6c6eed94b3e4c3 Mon Sep 17 00:00:00 2001 From: Lisa Kim Date: Wed, 4 Oct 2023 09:21:51 -0700 Subject: [PATCH 4/8] Create store notification and enable it --- web/packages/teleport/src/stores/index.ts | 4 +- .../src/stores/storeNotifications.test.ts | 71 +++++++++++++++++++ .../teleport/src/stores/storeNotifications.ts | 61 ++++++++++++++++ web/packages/teleport/src/teleportContext.tsx | 3 +- 4 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 web/packages/teleport/src/stores/storeNotifications.test.ts create mode 100644 web/packages/teleport/src/stores/storeNotifications.ts diff --git a/web/packages/teleport/src/stores/index.ts b/web/packages/teleport/src/stores/index.ts index 0be09b9c0c712..f3c4288599038 100644 --- a/web/packages/teleport/src/stores/index.ts +++ b/web/packages/teleport/src/stores/index.ts @@ -16,4 +16,6 @@ import StoreNav, { defaultNavState } from './storeNav'; import StoreUserContext from './storeUserContext'; -export { StoreNav, StoreUserContext, defaultNavState }; +import { StoreNotifications } from './storeNotifications'; + +export { StoreNav, StoreUserContext, StoreNotifications, defaultNavState }; diff --git a/web/packages/teleport/src/stores/storeNotifications.test.ts b/web/packages/teleport/src/stores/storeNotifications.test.ts new file mode 100644 index 0000000000000..7e98c138fea94 --- /dev/null +++ b/web/packages/teleport/src/stores/storeNotifications.test.ts @@ -0,0 +1,71 @@ +/** + * Copyright 2023 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 { Notice, StoreNotifications } from './storeNotifications'; + +test('get/set/update notifications', async () => { + const store = new StoreNotifications(); + + expect(store.getNotifications()).toStrictEqual([]); + + // set some notifications, sorted by earliest date. + const newerNote: Notice = { + kind: 'access-lists', + id: '111', + resourceName: 'apple', + date: new Date('2023-10-04T09:09:22-07:00'), + route: '', + }; + const olderNote: Notice = { + kind: 'access-lists', + id: '222', + resourceName: 'banana', + date: new Date('2023-10-01T09:09:22-07:00'), + route: '', + }; + + store.setNotifications([newerNote, olderNote]); + expect(store.getNotifications()).toStrictEqual([olderNote, newerNote]); + + // Update notes, sorted by earliest date. + const newestNote: Notice = { + kind: 'access-lists', + id: '333', + resourceName: 'carrot', + date: new Date('2023-11-23T09:09:22-07:00'), + route: '', + }; + const newestOlderNote: Notice = { + kind: 'access-lists', + id: '444', + resourceName: 'carrot', + date: new Date('2023-10-03T09:09:22-07:00'), + route: '', + }; + const newestOldestNote: Notice = { + kind: 'access-lists', + id: '444', + resourceName: 'carrot', + date: new Date('2023-10-01T09:09:22-07:00'), + route: '', + }; + store.setNotifications([newestNote, newestOldestNote, newestOlderNote]); + expect(store.getNotifications()).toStrictEqual([ + newestOldestNote, + newestOlderNote, + newestNote, + ]); +}); diff --git a/web/packages/teleport/src/stores/storeNotifications.ts b/web/packages/teleport/src/stores/storeNotifications.ts new file mode 100644 index 0000000000000..245c24ad004ec --- /dev/null +++ b/web/packages/teleport/src/stores/storeNotifications.ts @@ -0,0 +1,61 @@ +/** + * Copyright 2023 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 { Store } from 'shared/libs/stores'; + +type NoticeKinds = 'access-lists'; + +export type Notice = { + kind: NoticeKinds; + id: string; + resourceName: string; + date: Date; + route: string; +}; + +export type NotificationState = { + notices: Notice[]; +}; + +const defaultNotificationState: NotificationState = { + notices: [], +}; + +export class StoreNotifications extends Store { + state: NotificationState = defaultNotificationState; + + getNotifications() { + return this.state.notices; + } + + setNotifications(notices: Notice[]) { + // Sort by earliest dates. + const sortedNotices = notices.sort((a, b) => { + return a.date.getTime() - b.date.getTime(); + }); + this.setState({ notices: [...sortedNotices] }); + } + + updateNotificationsByKind(notices: Notice[], kind: NoticeKinds) { + switch (kind) { + case 'access-lists': + const filtered = this.state.notices.filter( + n => n.kind !== 'access-lists' + ); + this.setNotifications([...filtered, ...notices]); + } + } +} diff --git a/web/packages/teleport/src/teleportContext.tsx b/web/packages/teleport/src/teleportContext.tsx index e7bcf83c836fe..babefbe3329de 100644 --- a/web/packages/teleport/src/teleportContext.tsx +++ b/web/packages/teleport/src/teleportContext.tsx @@ -16,7 +16,7 @@ limitations under the License. import cfg from 'teleport/config'; -import { StoreNav, StoreUserContext } from './stores'; +import { StoreNav, StoreUserContext, StoreNotifications } from './stores'; import * as types from './types'; import AuditService from './services/audit'; import RecordingsService from './services/recordings'; @@ -39,6 +39,7 @@ class TeleportContext implements types.Context { // stores storeNav = new StoreNav(); storeUser = new StoreUserContext(); + storeNotifications = new StoreNotifications(); // services auditService = new AuditService(); From 7b9e19af2bb6e04681990281ac04e38fc8fec8e2 Mon Sep 17 00:00:00 2001 From: Lisa Kim Date: Fri, 6 Oct 2023 14:38:03 -0700 Subject: [PATCH 5/8] Address review - Made store notification type more generic - Drop opactiy styling for top bar drop downs - Use assertUnreachable in switch cases --- .../shared/utils/{errorType.ts => error.ts} | 4 ++ .../CreateDatabase/useCreateDatabase.ts | 2 +- .../EnrollRdsDatabase/EnrollRdsDatabase.tsx | 2 +- .../Server/CreateEc2Ice/CreateEc2Ice.tsx | 2 +- .../CreateEc2Ice/CreateEc2IceDialog.tsx | 2 +- .../EnrollEc2Instance/EnrollEc2Instance.tsx | 2 +- web/packages/teleport/src/Login/useLogin.ts | 2 +- .../teleport/src/Navigation/Navigation.tsx | 11 ++-- .../src/Navigation/NavigationItem.test.tsx | 9 ++- .../src/Navigation/NavigationItem.tsx | 11 ++-- .../TopBar/Notifications/Notifications.tsx | 63 ++++++++++++------- .../teleport/src/TopBar/TopBar.story.tsx | 25 +++++--- .../teleport/src/TopBar/TopBar.test.tsx | 14 ++--- .../src/components/Dropdown/Dropdown.tsx | 1 - .../src/stores/storeNotifications.test.ts | 62 +++++++++++------- .../teleport/src/stores/storeNotifications.ts | 41 +++++++++--- .../src/ui/utils/assertUnreachable.ts | 3 + 17 files changed, 161 insertions(+), 95 deletions(-) rename web/packages/shared/utils/{errorType.ts => error.ts} (92%) diff --git a/web/packages/shared/utils/errorType.ts b/web/packages/shared/utils/error.ts similarity index 92% rename from web/packages/shared/utils/errorType.ts rename to web/packages/shared/utils/error.ts index 79e7aab24bc74..9b7a2238bb5e2 100644 --- a/web/packages/shared/utils/errorType.ts +++ b/web/packages/shared/utils/error.ts @@ -30,3 +30,7 @@ export function getErrMessage(err: unknown) { return message; } + +export function assertUnreachable(x: never): never { + throw new Error(`Unhandled case: ${x}`); +} diff --git a/web/packages/teleport/src/Discover/Database/CreateDatabase/useCreateDatabase.ts b/web/packages/teleport/src/Discover/Database/CreateDatabase/useCreateDatabase.ts index 7c589f65ab5a3..510048d43df1a 100644 --- a/web/packages/teleport/src/Discover/Database/CreateDatabase/useCreateDatabase.ts +++ b/web/packages/teleport/src/Discover/Database/CreateDatabase/useCreateDatabase.ts @@ -16,7 +16,7 @@ import { useEffect, useState } from 'react'; import useAttempt from 'shared/hooks/useAttemptNext'; -import { getErrMessage } from 'shared/utils/errorType'; +import { getErrMessage } from 'shared/utils/error'; import useTeleport from 'teleport/useTeleport'; import { useDiscover } from 'teleport/Discover/useDiscover'; diff --git a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.tsx b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.tsx index 24b864ae9fde7..d1f9e367e9bcd 100644 --- a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.tsx +++ b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.tsx @@ -20,7 +20,7 @@ import { FetchStatus } from 'design/DataTable/types'; import { Danger } from 'design/Alert'; import useAttempt from 'shared/hooks/useAttemptNext'; -import { getErrMessage } from 'shared/utils/errorType'; +import { getErrMessage } from 'shared/utils/error'; import { DbMeta, useDiscover } from 'teleport/Discover/useDiscover'; import { diff --git a/web/packages/teleport/src/Discover/Server/CreateEc2Ice/CreateEc2Ice.tsx b/web/packages/teleport/src/Discover/Server/CreateEc2Ice/CreateEc2Ice.tsx index fd05b3f145b87..811c6e6b2acea 100644 --- a/web/packages/teleport/src/Discover/Server/CreateEc2Ice/CreateEc2Ice.tsx +++ b/web/packages/teleport/src/Discover/Server/CreateEc2Ice/CreateEc2Ice.tsx @@ -21,7 +21,7 @@ import { Danger } from 'design/Alert'; import { FetchStatus } from 'design/DataTable/types'; import useAttempt from 'shared/hooks/useAttemptNext'; -import { getErrMessage } from 'shared/utils/errorType'; +import { getErrMessage } from 'shared/utils/error'; import { SecurityGroup, diff --git a/web/packages/teleport/src/Discover/Server/CreateEc2Ice/CreateEc2IceDialog.tsx b/web/packages/teleport/src/Discover/Server/CreateEc2Ice/CreateEc2IceDialog.tsx index bc6aaca30069b..b68c4ec6e436b 100644 --- a/web/packages/teleport/src/Discover/Server/CreateEc2Ice/CreateEc2IceDialog.tsx +++ b/web/packages/teleport/src/Discover/Server/CreateEc2Ice/CreateEc2IceDialog.tsx @@ -26,7 +26,7 @@ import { import * as Icons from 'design/Icon'; import Dialog, { DialogContent } from 'design/DialogConfirmation'; -import { getErrMessage } from 'shared/utils/errorType'; +import { getErrMessage } from 'shared/utils/error'; import useAttempt, { Attempt } from 'shared/hooks/useAttemptNext'; diff --git a/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.tsx b/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.tsx index 07687a4625612..d1ab1ba1d45c2 100644 --- a/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.tsx +++ b/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.tsx @@ -19,7 +19,7 @@ import { Box, Text } from 'design'; import { FetchStatus } from 'design/DataTable/types'; import useAttempt from 'shared/hooks/useAttemptNext'; -import { getErrMessage } from 'shared/utils/errorType'; +import { getErrMessage } from 'shared/utils/error'; import cfg from 'teleport/config'; import { NodeMeta, useDiscover } from 'teleport/Discover/useDiscover'; diff --git a/web/packages/teleport/src/Login/useLogin.ts b/web/packages/teleport/src/Login/useLogin.ts index 8cc60b3a751bd..045d628ba7c74 100644 --- a/web/packages/teleport/src/Login/useLogin.ts +++ b/web/packages/teleport/src/Login/useLogin.ts @@ -17,7 +17,7 @@ import { useState } from 'react'; import { useAttempt } from 'shared/hooks'; import { AuthProvider } from 'shared/services'; -import { isPrivateKeyRequiredError } from 'shared/utils/errorType'; +import { isPrivateKeyRequiredError } from 'shared/utils/error'; import history from 'teleport/services/history'; import cfg from 'teleport/config'; diff --git a/web/packages/teleport/src/Navigation/Navigation.tsx b/web/packages/teleport/src/Navigation/Navigation.tsx index 719d6ae18e9f4..2c77ed0f60906 100644 --- a/web/packages/teleport/src/Navigation/Navigation.tsx +++ b/web/packages/teleport/src/Navigation/Navigation.tsx @@ -17,20 +17,17 @@ limitations under the License. import React, { useCallback, useEffect, useState } from 'react'; import styled, { useTheme } from 'styled-components'; import { matchPath, useHistory, useLocation } from 'react-router'; - import { Image } from 'design'; import { NavigationSwitcher } from 'teleport/Navigation/NavigationSwitcher'; import cfg from 'teleport/config'; - import { NAVIGATION_CATEGORIES, NavigationCategory, } from 'teleport/Navigation/categories'; - import { useFeatures } from 'teleport/FeaturesContext'; - import { NavigationCategoryContainer } from 'teleport/Navigation/NavigationCategoryContainer'; +import { NotificationKind } from 'teleport/stores/storeNotifications'; import { useTeleport } from '..'; @@ -179,9 +176,9 @@ export function Navigation({ items={[ { category: NavigationCategory.Management, - requiresAttention: ctx.storeNotifications - .getNotifications() - .some(n => n.kind === 'access-lists'), + requiresAttention: ctx.storeNotifications.hasNotificationsByKind( + NotificationKind.AccessList + ), }, { category: NavigationCategory.Resources }, ]} diff --git a/web/packages/teleport/src/Navigation/NavigationItem.test.tsx b/web/packages/teleport/src/Navigation/NavigationItem.test.tsx index 82b9bcb3a925b..31be645607358 100644 --- a/web/packages/teleport/src/Navigation/NavigationItem.test.tsx +++ b/web/packages/teleport/src/Navigation/NavigationItem.test.tsx @@ -30,6 +30,7 @@ import { NavigationCategory } from 'teleport/Navigation/categories'; import { NavigationItem } from 'teleport/Navigation/NavigationItem'; import { NavigationItemSize } from 'teleport/Navigation/common'; import { makeUserContext } from 'teleport/services/user'; +import { NotificationKind } from 'teleport/stores/storeNotifications'; class MockUserFeature implements TeleportFeature { category = NavigationCategory.Resources; @@ -134,11 +135,13 @@ describe('navigation items', () => { // Add in some notifications ctx.storeNotifications.setNotifications([ { - kind: 'access-lists', + item: { + kind: NotificationKind.AccessList, + resourceName: 'banana', + route: '', + }, id: 'abc', - resourceName: 'banana', date: new Date(), - route: '', }, ]); diff --git a/web/packages/teleport/src/Navigation/NavigationItem.tsx b/web/packages/teleport/src/Navigation/NavigationItem.tsx index 4bfaf25a0964a..7022d58216a6b 100644 --- a/web/packages/teleport/src/Navigation/NavigationItem.tsx +++ b/web/packages/teleport/src/Navigation/NavigationItem.tsx @@ -28,14 +28,11 @@ import { LinkContent, NavigationItemSize, } from 'teleport/Navigation/common'; - import useStickyClusterId from 'teleport/useStickyClusterId'; - import localStorage from 'teleport/services/localStorage'; - import { useTeleport } from 'teleport'; - import { NavTitle, RecommendationStatus } from 'teleport/types'; +import { NotificationKind } from 'teleport/stores/storeNotifications'; import type { TeleportFeature, @@ -171,9 +168,9 @@ export function NavigationItem(props: NavigationItemProps) { // renderHighlightFeature returns red dot component if the feature recommendation state is 'NOTIFY' function renderHighlightFeature(featureName: NavTitle): JSX.Element { if (featureName === NavTitle.AccessLists) { - const hasNotifications = ctx.storeNotifications - .getNotifications() - .some(n => n.kind === 'access-lists'); + const hasNotifications = ctx.storeNotifications.hasNotificationsByKind( + NotificationKind.AccessList + ); if (hasNotifications) { return ; diff --git a/web/packages/teleport/src/TopBar/Notifications/Notifications.tsx b/web/packages/teleport/src/TopBar/Notifications/Notifications.tsx index 809fb2f8a990a..4d32e9dc88898 100644 --- a/web/packages/teleport/src/TopBar/Notifications/Notifications.tsx +++ b/web/packages/teleport/src/TopBar/Notifications/Notifications.tsx @@ -18,9 +18,10 @@ import { formatDistanceToNow } from 'date-fns'; import styled from 'styled-components'; import { Text } from 'design'; -import { Notification, UserList } from 'design/Icon'; +import { Notification as NotificationIcon, UserList } from 'design/Icon'; import { useRefClickOutside } from 'shared/hooks/useRefClickOutside'; import { useStore } from 'shared/libs/stores'; +import { assertUnreachable } from 'shared/utils/error'; import { Dropdown, @@ -32,6 +33,10 @@ import { DropdownItemLink, } from 'teleport/components/Dropdown'; import useTeleport from 'teleport/useTeleport'; +import { + Notification, + NotificationKind, +} from 'teleport/stores/storeNotifications'; import { ButtonIconContainer } from '../Shared'; @@ -46,34 +51,17 @@ export function Notifications() { const ref = useRefClickOutside({ open, setOpen }); let transitionDelay = STARTING_TRANSITION_DELAY; - let items = notices.map(notice => { - let currentTransitionDelay = transitionDelay; + const items = notices.map(notice => { + const currentTransitionDelay = transitionDelay; transitionDelay += INCREMENT_TRANSITION_DELAY; - let item; - if (notice.kind === 'access-lists') { - item = ( - null}> - - - - - Access list {notice.resourceName} needs your review within{' '} - {formatDistanceToNow(notice.date)}. - - - ); - } - return ( - setOpen(false)}> - {item} - + setOpen(false)} /> ); }); @@ -85,7 +73,7 @@ export function Notifications() { data-testid="tb-note-button" > {items.length > 0 && } - + + + + + + + Access list {notice.item.resourceName} needs your review + within {formatDistanceToNow(notice.date)}. + + + + ); + default: + assertUnreachable(notice.item.kind); + } +} + const NotificationButtonContainer = styled.div` position: relative; `; @@ -119,11 +134,11 @@ const AttentionDot = styled.div` right: 15px; `; -export const NotificationItemButton = styled(DropdownItemButton)` +const NotificationItemButton = styled(DropdownItemButton)` align-items: flex-start; line-height: 20px; `; -export const NotificationLink = styled(DropdownItemLink)` +const NotificationLink = styled(DropdownItemLink)` padding: 0; `; diff --git a/web/packages/teleport/src/TopBar/TopBar.story.tsx b/web/packages/teleport/src/TopBar/TopBar.story.tsx index 9aacdc9d056ce..f102f193cc686 100644 --- a/web/packages/teleport/src/TopBar/TopBar.story.tsx +++ b/web/packages/teleport/src/TopBar/TopBar.story.tsx @@ -24,6 +24,7 @@ import TeleportContext from 'teleport/teleportContext'; import { makeUserContext } from 'teleport/services/user'; import TeleportContextProvider from 'teleport/TeleportContextProvider'; import { LayoutContextProvider } from 'teleport/Main/LayoutContext'; +import { NotificationKind } from 'teleport/stores/storeNotifications'; import { TopBar } from './TopBar'; @@ -70,25 +71,31 @@ export function TopBarWithNotifications() { ctx.storeNotifications.state = { notices: [ { - kind: 'access-lists', + item: { + kind: NotificationKind.AccessList, + resourceName: 'banana', + route: '', + }, id: '111', - resourceName: 'banana', date: new Date(), - route: '', }, { - kind: 'access-lists', + item: { + kind: NotificationKind.AccessList, + resourceName: 'apple', + route: '', + }, id: '222', - resourceName: 'apple', date: new Date(), - route: '', }, { - kind: 'access-lists', + item: { + kind: NotificationKind.AccessList, + resourceName: 'carrot', + route: '', + }, id: '333', - resourceName: 'carrot', date: new Date(), - route: '', }, ], }; diff --git a/web/packages/teleport/src/TopBar/TopBar.test.tsx b/web/packages/teleport/src/TopBar/TopBar.test.tsx index 2ffd3e70b63fd..1a4e4a75cd1f8 100644 --- a/web/packages/teleport/src/TopBar/TopBar.test.tsx +++ b/web/packages/teleport/src/TopBar/TopBar.test.tsx @@ -16,7 +16,6 @@ import React from 'react'; import { render, screen, userEvent } from 'design/utils/testing'; - import { Router } from 'react-router'; import { createMemoryHistory } from 'history'; @@ -27,17 +26,16 @@ import { getOSSFeatures } from 'teleport/features'; import TeleportContext, { disabledFeatureFlags, } from 'teleport/teleportContext'; - import { makeUserContext } from 'teleport/services/user'; - import { mockUserContextProviderWith } from 'teleport/User/testHelpers/mockUserContextWith'; import { makeTestUserContext } from 'teleport/User/testHelpers/makeTestUserContext'; +import { NotificationKind } from 'teleport/stores/storeNotifications'; import { clusters } from 'teleport/Clusters/fixtures'; import { TopBar } from './TopBar'; -let ctx; +let ctx: TeleportContext; function setup(): void { ctx = new TeleportContext(); @@ -99,11 +97,13 @@ test('notification bell with notification', async () => { ctx.storeNotifications.state = { notices: [ { - kind: 'access-lists', + item: { + kind: NotificationKind.AccessList, + resourceName: 'banana', + route: '', + }, id: 'abc', - resourceName: 'banana', date: new Date(), - route: '', }, ], }; diff --git a/web/packages/teleport/src/components/Dropdown/Dropdown.tsx b/web/packages/teleport/src/components/Dropdown/Dropdown.tsx index 1350b42eb8472..1c86938b0715f 100644 --- a/web/packages/teleport/src/components/Dropdown/Dropdown.tsx +++ b/web/packages/teleport/src/components/Dropdown/Dropdown.tsx @@ -67,7 +67,6 @@ export const DropdownItem = styled.div` `; export const commonDropdownItemStyles = css` - opacity: 0.8; align-items: center; display: flex; padding: ${p => p.theme.space[1] * 3}px; diff --git a/web/packages/teleport/src/stores/storeNotifications.test.ts b/web/packages/teleport/src/stores/storeNotifications.test.ts index 7e98c138fea94..1a870efb2a9eb 100644 --- a/web/packages/teleport/src/stores/storeNotifications.test.ts +++ b/web/packages/teleport/src/stores/storeNotifications.test.ts @@ -14,53 +14,68 @@ * limitations under the License. */ -import { Notice, StoreNotifications } from './storeNotifications'; +import { + Notification, + NotificationKind, + StoreNotifications, +} from './storeNotifications'; test('get/set/update notifications', async () => { const store = new StoreNotifications(); expect(store.getNotifications()).toStrictEqual([]); + expect(store.hasNotificationsByKind(NotificationKind.AccessList)).toBeFalsy(); // set some notifications, sorted by earliest date. - const newerNote: Notice = { - kind: 'access-lists', + const newerNote: Notification = { + item: { + kind: NotificationKind.AccessList, + resourceName: 'apple', + route: '', + }, id: '111', - resourceName: 'apple', date: new Date('2023-10-04T09:09:22-07:00'), - route: '', }; - const olderNote: Notice = { - kind: 'access-lists', + const olderNote: Notification = { + item: { + kind: NotificationKind.AccessList, + resourceName: 'banana', + route: '', + }, id: '222', - resourceName: 'banana', date: new Date('2023-10-01T09:09:22-07:00'), - route: '', }; store.setNotifications([newerNote, olderNote]); expect(store.getNotifications()).toStrictEqual([olderNote, newerNote]); // Update notes, sorted by earliest date. - const newestNote: Notice = { - kind: 'access-lists', + const newestNote: Notification = { + item: { + kind: NotificationKind.AccessList, + resourceName: 'carrot', + route: '', + }, id: '333', - resourceName: 'carrot', date: new Date('2023-11-23T09:09:22-07:00'), - route: '', }; - const newestOlderNote: Notice = { - kind: 'access-lists', + const newestOlderNote: Notification = { + item: { + kind: NotificationKind.AccessList, + resourceName: 'carrot', + route: '', + }, id: '444', - resourceName: 'carrot', date: new Date('2023-10-03T09:09:22-07:00'), - route: '', }; - const newestOldestNote: Notice = { - kind: 'access-lists', + const newestOldestNote: Notification = { + item: { + kind: NotificationKind.AccessList, + resourceName: 'carrot', + route: '', + }, id: '444', - resourceName: 'carrot', date: new Date('2023-10-01T09:09:22-07:00'), - route: '', }; store.setNotifications([newestNote, newestOldestNote, newestOlderNote]); expect(store.getNotifications()).toStrictEqual([ @@ -68,4 +83,9 @@ test('get/set/update notifications', async () => { newestOlderNote, newestNote, ]); + + // Test has notifications + expect( + store.hasNotificationsByKind(NotificationKind.AccessList) + ).toBeTruthy(); }); diff --git a/web/packages/teleport/src/stores/storeNotifications.ts b/web/packages/teleport/src/stores/storeNotifications.ts index 245c24ad004ec..b6aa55347c369 100644 --- a/web/packages/teleport/src/stores/storeNotifications.ts +++ b/web/packages/teleport/src/stores/storeNotifications.ts @@ -15,19 +15,26 @@ */ import { Store } from 'shared/libs/stores'; +import { assertUnreachable } from 'shared/utils/error'; -type NoticeKinds = 'access-lists'; +export enum NotificationKind { + AccessList = 'access-list', +} -export type Notice = { - kind: NoticeKinds; - id: string; +type AccessListNotification = { + kind: NotificationKind.AccessList; resourceName: string; - date: Date; route: string; }; +export type Notification = { + item: AccessListNotification; + id: string; + date: Date; +}; + export type NotificationState = { - notices: Notice[]; + notices: Notification[]; }; const defaultNotificationState: NotificationState = { @@ -41,7 +48,7 @@ export class StoreNotifications extends Store { return this.state.notices; } - setNotifications(notices: Notice[]) { + setNotifications(notices: Notification[]) { // Sort by earliest dates. const sortedNotices = notices.sort((a, b) => { return a.date.getTime() - b.date.getTime(); @@ -49,13 +56,27 @@ export class StoreNotifications extends Store { this.setState({ notices: [...sortedNotices] }); } - updateNotificationsByKind(notices: Notice[], kind: NoticeKinds) { + updateNotificationsByKind(notices: Notification[], kind: NotificationKind) { switch (kind) { - case 'access-lists': + case NotificationKind.AccessList: const filtered = this.state.notices.filter( - n => n.kind !== 'access-lists' + n => n.item.kind !== NotificationKind.AccessList ); this.setNotifications([...filtered, ...notices]); + return; + default: + assertUnreachable(kind); + } + } + + hasNotificationsByKind(kind: NotificationKind) { + switch (kind) { + case NotificationKind.AccessList: + return this.getNotifications().some( + n => n.item.kind === NotificationKind.AccessList + ); + default: + assertUnreachable(kind); } } } diff --git a/web/packages/teleterm/src/ui/utils/assertUnreachable.ts b/web/packages/teleterm/src/ui/utils/assertUnreachable.ts index 0b9aed7fc3b88..3401ea692399c 100644 --- a/web/packages/teleterm/src/ui/utils/assertUnreachable.ts +++ b/web/packages/teleterm/src/ui/utils/assertUnreachable.ts @@ -14,6 +14,9 @@ * limitations under the License. */ +/** + * @deprecated Import assertUnreachable from `shared/utils/error` instead. + */ export function assertUnreachable(x: never): never { throw new Error(`Unhandled case: ${x}`); } From a7b97881a7f94f56c7c04fe1e45ec963d3bed4c7 Mon Sep 17 00:00:00 2001 From: Lisa Kim Date: Tue, 10 Oct 2023 18:28:45 -0700 Subject: [PATCH 6/8] Address CR --- web/packages/shared/utils/error.ts | 19 +---------- web/packages/shared/utils/errorType.ts | 32 +++++++++++++++++++ .../CreateDatabase/useCreateDatabase.ts | 2 +- .../EnrollRdsDatabase/EnrollRdsDatabase.tsx | 2 +- .../Server/CreateEc2Ice/CreateEc2Ice.tsx | 2 +- .../CreateEc2Ice/CreateEc2IceDialog.tsx | 2 +- .../EnrollEc2Instance/EnrollEc2Instance.tsx | 2 +- web/packages/teleport/src/Login/useLogin.ts | 2 +- .../teleport/src/TopBar/TopBar.story.tsx | 2 +- .../teleport/src/TopBar/TopBar.test.tsx | 2 +- .../src/components/Dropdown/Dropdown.tsx | 5 --- .../teleport/src/stores/storeNotifications.ts | 10 +++--- .../src/ui/utils/assertUnreachable.ts | 6 ++-- 13 files changed, 49 insertions(+), 39 deletions(-) create mode 100644 web/packages/shared/utils/errorType.ts diff --git a/web/packages/shared/utils/error.ts b/web/packages/shared/utils/error.ts index 9b7a2238bb5e2..4d91e495874f1 100644 --- a/web/packages/shared/utils/error.ts +++ b/web/packages/shared/utils/error.ts @@ -1,5 +1,5 @@ /** - * Copyright 2022 Gravitational, Inc. + * Copyright 2023 Gravitational, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,23 +14,6 @@ * limitations under the License. */ -import { privateKeyEnablingPolicies } from 'shared/services'; - -export function isPrivateKeyRequiredError(err: Error) { - return privateKeyEnablingPolicies.some(p => err.message.includes(p)); -} - -// getErrMessage first checks if the error is of type Error -// before attempting to access the error message field. -// Used with try catch blocks, where the error caught -// may not necessary be of type Error. -export function getErrMessage(err: unknown) { - let message = 'something went wrong'; - if (err instanceof Error) message = err.message; - - return message; -} - export function assertUnreachable(x: never): never { throw new Error(`Unhandled case: ${x}`); } diff --git a/web/packages/shared/utils/errorType.ts b/web/packages/shared/utils/errorType.ts new file mode 100644 index 0000000000000..79e7aab24bc74 --- /dev/null +++ b/web/packages/shared/utils/errorType.ts @@ -0,0 +1,32 @@ +/** + * 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 { privateKeyEnablingPolicies } from 'shared/services'; + +export function isPrivateKeyRequiredError(err: Error) { + return privateKeyEnablingPolicies.some(p => err.message.includes(p)); +} + +// getErrMessage first checks if the error is of type Error +// before attempting to access the error message field. +// Used with try catch blocks, where the error caught +// may not necessary be of type Error. +export function getErrMessage(err: unknown) { + let message = 'something went wrong'; + if (err instanceof Error) message = err.message; + + return message; +} diff --git a/web/packages/teleport/src/Discover/Database/CreateDatabase/useCreateDatabase.ts b/web/packages/teleport/src/Discover/Database/CreateDatabase/useCreateDatabase.ts index 510048d43df1a..a3882bef92b04 100644 --- a/web/packages/teleport/src/Discover/Database/CreateDatabase/useCreateDatabase.ts +++ b/web/packages/teleport/src/Discover/Database/CreateDatabase/useCreateDatabase.ts @@ -16,7 +16,7 @@ import { useEffect, useState } from 'react'; import useAttempt from 'shared/hooks/useAttemptNext'; -import { getErrMessage } from 'shared/utils/error'; +import { getErrMessage } from '@gravitational/shared/utils/errorType'; import useTeleport from 'teleport/useTeleport'; import { useDiscover } from 'teleport/Discover/useDiscover'; diff --git a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.tsx b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.tsx index d1f9e367e9bcd..6ad21188942f5 100644 --- a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.tsx +++ b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.tsx @@ -20,7 +20,7 @@ import { FetchStatus } from 'design/DataTable/types'; import { Danger } from 'design/Alert'; import useAttempt from 'shared/hooks/useAttemptNext'; -import { getErrMessage } from 'shared/utils/error'; +import { getErrMessage } from '@gravitational/shared/utils/errorType'; import { DbMeta, useDiscover } from 'teleport/Discover/useDiscover'; import { diff --git a/web/packages/teleport/src/Discover/Server/CreateEc2Ice/CreateEc2Ice.tsx b/web/packages/teleport/src/Discover/Server/CreateEc2Ice/CreateEc2Ice.tsx index 811c6e6b2acea..0cc709751cf36 100644 --- a/web/packages/teleport/src/Discover/Server/CreateEc2Ice/CreateEc2Ice.tsx +++ b/web/packages/teleport/src/Discover/Server/CreateEc2Ice/CreateEc2Ice.tsx @@ -21,7 +21,7 @@ import { Danger } from 'design/Alert'; import { FetchStatus } from 'design/DataTable/types'; import useAttempt from 'shared/hooks/useAttemptNext'; -import { getErrMessage } from 'shared/utils/error'; +import { getErrMessage } from '@gravitational/shared/utils/errorType'; import { SecurityGroup, diff --git a/web/packages/teleport/src/Discover/Server/CreateEc2Ice/CreateEc2IceDialog.tsx b/web/packages/teleport/src/Discover/Server/CreateEc2Ice/CreateEc2IceDialog.tsx index b68c4ec6e436b..4e81d153688a2 100644 --- a/web/packages/teleport/src/Discover/Server/CreateEc2Ice/CreateEc2IceDialog.tsx +++ b/web/packages/teleport/src/Discover/Server/CreateEc2Ice/CreateEc2IceDialog.tsx @@ -26,7 +26,7 @@ import { import * as Icons from 'design/Icon'; import Dialog, { DialogContent } from 'design/DialogConfirmation'; -import { getErrMessage } from 'shared/utils/error'; +import { getErrMessage } from '@gravitational/shared/utils/errorType'; import useAttempt, { Attempt } from 'shared/hooks/useAttemptNext'; diff --git a/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.tsx b/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.tsx index d1ab1ba1d45c2..9009ccb65ef3b 100644 --- a/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.tsx +++ b/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.tsx @@ -19,7 +19,7 @@ import { Box, Text } from 'design'; import { FetchStatus } from 'design/DataTable/types'; import useAttempt from 'shared/hooks/useAttemptNext'; -import { getErrMessage } from 'shared/utils/error'; +import { getErrMessage } from '@gravitational/shared/utils/errorType'; import cfg from 'teleport/config'; import { NodeMeta, useDiscover } from 'teleport/Discover/useDiscover'; diff --git a/web/packages/teleport/src/Login/useLogin.ts b/web/packages/teleport/src/Login/useLogin.ts index 045d628ba7c74..6a440b2473347 100644 --- a/web/packages/teleport/src/Login/useLogin.ts +++ b/web/packages/teleport/src/Login/useLogin.ts @@ -17,7 +17,7 @@ import { useState } from 'react'; import { useAttempt } from 'shared/hooks'; import { AuthProvider } from 'shared/services'; -import { isPrivateKeyRequiredError } from 'shared/utils/error'; +import { isPrivateKeyRequiredError } from '@gravitational/shared/utils/errorType'; import history from 'teleport/services/history'; import cfg from 'teleport/config'; diff --git a/web/packages/teleport/src/TopBar/TopBar.story.tsx b/web/packages/teleport/src/TopBar/TopBar.story.tsx index f102f193cc686..10244365e470c 100644 --- a/web/packages/teleport/src/TopBar/TopBar.story.tsx +++ b/web/packages/teleport/src/TopBar/TopBar.story.tsx @@ -69,7 +69,7 @@ export function TopBarWithNotifications() { }, }); ctx.storeNotifications.state = { - notices: [ + notifications: [ { item: { kind: NotificationKind.AccessList, diff --git a/web/packages/teleport/src/TopBar/TopBar.test.tsx b/web/packages/teleport/src/TopBar/TopBar.test.tsx index 1a4e4a75cd1f8..12ce0141dbf80 100644 --- a/web/packages/teleport/src/TopBar/TopBar.test.tsx +++ b/web/packages/teleport/src/TopBar/TopBar.test.tsx @@ -95,7 +95,7 @@ test('notification bell without notification', async () => { test('notification bell with notification', async () => { setup(); ctx.storeNotifications.state = { - notices: [ + notifications: [ { item: { kind: NotificationKind.AccessList, diff --git a/web/packages/teleport/src/components/Dropdown/Dropdown.tsx b/web/packages/teleport/src/components/Dropdown/Dropdown.tsx index 1c86938b0715f..e4d701a3b5bf6 100644 --- a/web/packages/teleport/src/components/Dropdown/Dropdown.tsx +++ b/web/packages/teleport/src/components/Dropdown/Dropdown.tsx @@ -72,11 +72,6 @@ export const commonDropdownItemStyles = css` padding: ${p => p.theme.space[1] * 3}px; color: ${props => props.theme.colors.text.main}; text-decoration: none; - transition: opacity 0.15s ease-in; - - &:hover { - opacity: 1; - } svg { height: 18px; diff --git a/web/packages/teleport/src/stores/storeNotifications.ts b/web/packages/teleport/src/stores/storeNotifications.ts index b6aa55347c369..41ee1754a8665 100644 --- a/web/packages/teleport/src/stores/storeNotifications.ts +++ b/web/packages/teleport/src/stores/storeNotifications.ts @@ -34,18 +34,18 @@ export type Notification = { }; export type NotificationState = { - notices: Notification[]; + notifications: Notification[]; }; const defaultNotificationState: NotificationState = { - notices: [], + notifications: [], }; export class StoreNotifications extends Store { state: NotificationState = defaultNotificationState; getNotifications() { - return this.state.notices; + return this.state.notifications; } setNotifications(notices: Notification[]) { @@ -53,13 +53,13 @@ export class StoreNotifications extends Store { const sortedNotices = notices.sort((a, b) => { return a.date.getTime() - b.date.getTime(); }); - this.setState({ notices: [...sortedNotices] }); + this.setState({ notifications: [...sortedNotices] }); } updateNotificationsByKind(notices: Notification[], kind: NotificationKind) { switch (kind) { case NotificationKind.AccessList: - const filtered = this.state.notices.filter( + const filtered = this.state.notifications.filter( n => n.item.kind !== NotificationKind.AccessList ); this.setNotifications([...filtered, ...notices]); diff --git a/web/packages/teleterm/src/ui/utils/assertUnreachable.ts b/web/packages/teleterm/src/ui/utils/assertUnreachable.ts index 3401ea692399c..94764249da741 100644 --- a/web/packages/teleterm/src/ui/utils/assertUnreachable.ts +++ b/web/packages/teleterm/src/ui/utils/assertUnreachable.ts @@ -14,9 +14,9 @@ * limitations under the License. */ +import { assertUnreachable } from 'shared/utils/error'; + /** * @deprecated Import assertUnreachable from `shared/utils/error` instead. */ -export function assertUnreachable(x: never): never { - throw new Error(`Unhandled case: ${x}`); -} +export { assertUnreachable }; From 8d1d97cde36054c04c27812df0636f01e91a2dad Mon Sep 17 00:00:00 2001 From: Lisa Kim Date: Wed, 11 Oct 2023 14:29:05 -0700 Subject: [PATCH 7/8] Fix import path and rename file --- web/packages/shared/utils/{error.ts => assertUnreachable.ts} | 0 .../src/Discover/Database/CreateDatabase/useCreateDatabase.ts | 2 +- .../Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.tsx | 2 +- .../src/Discover/Server/CreateEc2Ice/CreateEc2Ice.tsx | 2 +- .../src/Discover/Server/CreateEc2Ice/CreateEc2IceDialog.tsx | 2 +- .../Discover/Server/EnrollEc2Instance/EnrollEc2Instance.tsx | 2 +- web/packages/teleport/src/Login/useLogin.ts | 2 +- .../teleport/src/TopBar/Notifications/Notifications.tsx | 2 +- web/packages/teleport/src/stores/storeNotifications.ts | 2 +- web/packages/teleterm/src/ui/utils/assertUnreachable.ts | 4 ++-- 10 files changed, 10 insertions(+), 10 deletions(-) rename web/packages/shared/utils/{error.ts => assertUnreachable.ts} (100%) diff --git a/web/packages/shared/utils/error.ts b/web/packages/shared/utils/assertUnreachable.ts similarity index 100% rename from web/packages/shared/utils/error.ts rename to web/packages/shared/utils/assertUnreachable.ts diff --git a/web/packages/teleport/src/Discover/Database/CreateDatabase/useCreateDatabase.ts b/web/packages/teleport/src/Discover/Database/CreateDatabase/useCreateDatabase.ts index a3882bef92b04..7c589f65ab5a3 100644 --- a/web/packages/teleport/src/Discover/Database/CreateDatabase/useCreateDatabase.ts +++ b/web/packages/teleport/src/Discover/Database/CreateDatabase/useCreateDatabase.ts @@ -16,7 +16,7 @@ import { useEffect, useState } from 'react'; import useAttempt from 'shared/hooks/useAttemptNext'; -import { getErrMessage } from '@gravitational/shared/utils/errorType'; +import { getErrMessage } from 'shared/utils/errorType'; import useTeleport from 'teleport/useTeleport'; import { useDiscover } from 'teleport/Discover/useDiscover'; diff --git a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.tsx b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.tsx index 6ad21188942f5..24b864ae9fde7 100644 --- a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.tsx +++ b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.tsx @@ -20,7 +20,7 @@ import { FetchStatus } from 'design/DataTable/types'; import { Danger } from 'design/Alert'; import useAttempt from 'shared/hooks/useAttemptNext'; -import { getErrMessage } from '@gravitational/shared/utils/errorType'; +import { getErrMessage } from 'shared/utils/errorType'; import { DbMeta, useDiscover } from 'teleport/Discover/useDiscover'; import { diff --git a/web/packages/teleport/src/Discover/Server/CreateEc2Ice/CreateEc2Ice.tsx b/web/packages/teleport/src/Discover/Server/CreateEc2Ice/CreateEc2Ice.tsx index 0cc709751cf36..fd05b3f145b87 100644 --- a/web/packages/teleport/src/Discover/Server/CreateEc2Ice/CreateEc2Ice.tsx +++ b/web/packages/teleport/src/Discover/Server/CreateEc2Ice/CreateEc2Ice.tsx @@ -21,7 +21,7 @@ import { Danger } from 'design/Alert'; import { FetchStatus } from 'design/DataTable/types'; import useAttempt from 'shared/hooks/useAttemptNext'; -import { getErrMessage } from '@gravitational/shared/utils/errorType'; +import { getErrMessage } from 'shared/utils/errorType'; import { SecurityGroup, diff --git a/web/packages/teleport/src/Discover/Server/CreateEc2Ice/CreateEc2IceDialog.tsx b/web/packages/teleport/src/Discover/Server/CreateEc2Ice/CreateEc2IceDialog.tsx index 4e81d153688a2..bc6aaca30069b 100644 --- a/web/packages/teleport/src/Discover/Server/CreateEc2Ice/CreateEc2IceDialog.tsx +++ b/web/packages/teleport/src/Discover/Server/CreateEc2Ice/CreateEc2IceDialog.tsx @@ -26,7 +26,7 @@ import { import * as Icons from 'design/Icon'; import Dialog, { DialogContent } from 'design/DialogConfirmation'; -import { getErrMessage } from '@gravitational/shared/utils/errorType'; +import { getErrMessage } from 'shared/utils/errorType'; import useAttempt, { Attempt } from 'shared/hooks/useAttemptNext'; diff --git a/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.tsx b/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.tsx index 9009ccb65ef3b..07687a4625612 100644 --- a/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.tsx +++ b/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.tsx @@ -19,7 +19,7 @@ import { Box, Text } from 'design'; import { FetchStatus } from 'design/DataTable/types'; import useAttempt from 'shared/hooks/useAttemptNext'; -import { getErrMessage } from '@gravitational/shared/utils/errorType'; +import { getErrMessage } from 'shared/utils/errorType'; import cfg from 'teleport/config'; import { NodeMeta, useDiscover } from 'teleport/Discover/useDiscover'; diff --git a/web/packages/teleport/src/Login/useLogin.ts b/web/packages/teleport/src/Login/useLogin.ts index 6a440b2473347..8cc60b3a751bd 100644 --- a/web/packages/teleport/src/Login/useLogin.ts +++ b/web/packages/teleport/src/Login/useLogin.ts @@ -17,7 +17,7 @@ import { useState } from 'react'; import { useAttempt } from 'shared/hooks'; import { AuthProvider } from 'shared/services'; -import { isPrivateKeyRequiredError } from '@gravitational/shared/utils/errorType'; +import { isPrivateKeyRequiredError } from 'shared/utils/errorType'; import history from 'teleport/services/history'; import cfg from 'teleport/config'; diff --git a/web/packages/teleport/src/TopBar/Notifications/Notifications.tsx b/web/packages/teleport/src/TopBar/Notifications/Notifications.tsx index 4d32e9dc88898..650af19a086bc 100644 --- a/web/packages/teleport/src/TopBar/Notifications/Notifications.tsx +++ b/web/packages/teleport/src/TopBar/Notifications/Notifications.tsx @@ -21,7 +21,7 @@ import { Text } from 'design'; import { Notification as NotificationIcon, UserList } from 'design/Icon'; import { useRefClickOutside } from 'shared/hooks/useRefClickOutside'; import { useStore } from 'shared/libs/stores'; -import { assertUnreachable } from 'shared/utils/error'; +import { assertUnreachable } from 'shared/utils/assertUnreachable'; import { Dropdown, diff --git a/web/packages/teleport/src/stores/storeNotifications.ts b/web/packages/teleport/src/stores/storeNotifications.ts index 41ee1754a8665..cdab64cfbe4b8 100644 --- a/web/packages/teleport/src/stores/storeNotifications.ts +++ b/web/packages/teleport/src/stores/storeNotifications.ts @@ -15,7 +15,7 @@ */ import { Store } from 'shared/libs/stores'; -import { assertUnreachable } from 'shared/utils/error'; +import { assertUnreachable } from 'shared/utils/assertUnreachable'; export enum NotificationKind { AccessList = 'access-list', diff --git a/web/packages/teleterm/src/ui/utils/assertUnreachable.ts b/web/packages/teleterm/src/ui/utils/assertUnreachable.ts index 94764249da741..0eb634554301e 100644 --- a/web/packages/teleterm/src/ui/utils/assertUnreachable.ts +++ b/web/packages/teleterm/src/ui/utils/assertUnreachable.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { assertUnreachable } from 'shared/utils/error'; +import { assertUnreachable } from 'shared/utils/assertUnreachable'; /** - * @deprecated Import assertUnreachable from `shared/utils/error` instead. + * @deprecated Import assertUnreachable from `shared/utils/assertUnreachable` instead. */ export { assertUnreachable }; From dd69827cd7b580a4fb9cd035f747bf2972ad6556 Mon Sep 17 00:00:00 2001 From: Lisa Kim Date: Wed, 11 Oct 2023 15:18:16 -0700 Subject: [PATCH 8/8] Leave a TODO comment --- web/packages/teleport/src/stores/storeNotifications.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/web/packages/teleport/src/stores/storeNotifications.ts b/web/packages/teleport/src/stores/storeNotifications.ts index cdab64cfbe4b8..4d2f9169ba905 100644 --- a/web/packages/teleport/src/stores/storeNotifications.ts +++ b/web/packages/teleport/src/stores/storeNotifications.ts @@ -33,6 +33,10 @@ export type Notification = { date: Date; }; +// TODO?: based on a feedback, consider representing +// notifications as a collection of maps indexed by id +// which is then converted to a sorted list as needed +// (may be easier to work with) export type NotificationState = { notifications: Notification[]; };