diff --git a/web/packages/teleterm/src/ui/AccessRequestCheckout/useAccessRequestCheckout.ts b/web/packages/teleterm/src/ui/AccessRequestCheckout/useAccessRequestCheckout.ts index 4d70ada109b16..bbf092b32d565 100644 --- a/web/packages/teleterm/src/ui/AccessRequestCheckout/useAccessRequestCheckout.ts +++ b/web/packages/teleterm/src/ui/AccessRequestCheckout/useAccessRequestCheckout.ts @@ -33,6 +33,7 @@ import { useSpecifiableFields } from 'shared/components/AccessRequests/NewReques import { CreateRequest } from 'shared/components/AccessRequests/Shared/types'; import { useAppContext } from 'teleterm/ui/appContextProvider'; +import { useWorkspaceServiceState } from 'teleterm/ui/services/workspacesService'; import { PendingAccessRequest, extractResourceRequestProperties, @@ -54,7 +55,7 @@ import { makeUiAccessRequest } from '../DocumentAccessRequests/useAccessRequests export default function useAccessRequestCheckout() { const ctx = useAppContext(); - ctx.workspacesService.useState(); + useWorkspaceServiceState(); ctx.clustersService.useState(); const clusterUri = ctx.workspacesService?.getActiveWorkspace()?.localClusterUri; diff --git a/web/packages/teleterm/src/ui/ConnectMyComputer/connectMyComputerContext.test.tsx b/web/packages/teleterm/src/ui/ConnectMyComputer/connectMyComputerContext.test.tsx index 9bfe8e87e19c1..47f5e9efb4c69 100644 --- a/web/packages/teleterm/src/ui/ConnectMyComputer/connectMyComputerContext.test.tsx +++ b/web/packages/teleterm/src/ui/ConnectMyComputer/connectMyComputerContext.test.tsx @@ -18,13 +18,11 @@ import { EventEmitter } from 'node:events'; -import React from 'react'; import { act, renderHook, waitFor } from '@testing-library/react'; import { makeErrorAttempt } from 'shared/hooks/useAsync'; import { MockAppContextProvider } from 'teleterm/ui/fixtures/MockAppContextProvider'; import { MockAppContext } from 'teleterm/ui/fixtures/mocks'; -import { WorkspaceContextProvider } from 'teleterm/ui/Documents'; import { AgentProcessState } from 'teleterm/mainProcess/types'; import * as resourcesContext from 'teleterm/ui/DocumentCluster/resourcesContext'; import { @@ -90,13 +88,11 @@ function renderUseConnectMyComputerContextHook( return renderHook(() => useConnectMyComputerContext(), { wrapper: ({ children }) => ( - - - - {children} - - - + + + {children} + + ), }); @@ -322,15 +318,13 @@ describe('canUse', () => { const { result } = renderHook(() => useConnectMyComputerContext(), { wrapper: ({ children }) => ( - - - - {children} - - - + + + {children} + + ), }); diff --git a/web/packages/teleterm/src/ui/Documents/workspaceContext.tsx b/web/packages/teleterm/src/ui/Documents/workspaceContext.tsx index f3e89ee30c022..949ef3e96cefc 100644 --- a/web/packages/teleterm/src/ui/Documents/workspaceContext.tsx +++ b/web/packages/teleterm/src/ui/Documents/workspaceContext.tsx @@ -16,21 +16,27 @@ * along with this program. If not, see . */ -import React, { PropsWithChildren } from 'react'; +import { + FC, + PropsWithChildren, + useCallback, + useContext, + createContext, +} from 'react'; import { DocumentsService } from 'teleterm/ui/services/workspacesService'; import { AccessRequestsService } from 'teleterm/ui/services/workspacesService/accessRequestsService'; -import { useAppContext } from 'teleterm/ui/appContextProvider'; import { ClusterUri, RootClusterUri } from 'teleterm/ui/uri'; +import { useStoreSelector } from 'teleterm/ui/hooks/useStoreSelector'; -const WorkspaceContext = React.createContext<{ +const WorkspaceContext = createContext<{ rootClusterUri: RootClusterUri; localClusterUri: ClusterUri; documentsService: DocumentsService; accessRequestsService: AccessRequestsService; }>(null); -export const WorkspaceContextProvider: React.FC< +export const WorkspaceContextProvider: FC< PropsWithChildren<{ value: { rootClusterUri: RootClusterUri; @@ -40,12 +46,29 @@ export const WorkspaceContextProvider: React.FC< }; }> > = props => { + // Re-render the context provider whenever the state of the relevant workspace changes. The + // context provider cannot re-render only when its props change. + // For example, if a new document gets added, none of the props are going to change, but the + // callsite that uses useWorkspaceContext might want to get re-rendered in this case, as + // technically documentsService returned from useWorkspaceContext might return new state. + useStoreSelector( + 'workspacesService', + useCallback( + state => state.workspaces[props.value.rootClusterUri], + [props.value.rootClusterUri] + ) + ); return ; }; export const useWorkspaceContext = () => { - const ctx = useAppContext(); - ctx.workspacesService.useState(); + const context = useContext(WorkspaceContext); - return React.useContext(WorkspaceContext); + if (!context) { + throw new Error( + 'useWorkspaceContext must be used within a WorkspaceContextProvider' + ); + } + + return context; }; diff --git a/web/packages/teleterm/src/ui/Search/SearchBar.tsx b/web/packages/teleterm/src/ui/Search/SearchBar.tsx index 87f5c36264651..a25ef17f75aa6 100644 --- a/web/packages/teleterm/src/ui/Search/SearchBar.tsx +++ b/web/packages/teleterm/src/ui/Search/SearchBar.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import React, { useRef, useEffect } from 'react'; +import React, { useRef, useEffect, useCallback } from 'react'; import styled from 'styled-components'; import { Box, Flex } from 'design'; @@ -31,14 +31,17 @@ import { } from 'teleterm/ui/services/keyboardShortcuts'; import { useAppContext } from '../appContextProvider'; +import { useStoreSelector } from '../hooks/useStoreSelector'; const OPEN_SEARCH_BAR_SHORTCUT_ACTION: KeyboardShortcutAction = 'openSearchBar'; export function SearchBarConnected() { - const { workspacesService } = useAppContext(); - workspacesService.useState(); + const rootClusterUri = useStoreSelector( + 'workspacesService', + useCallback(state => state.rootClusterUri, []) + ); - if (!workspacesService.getRootClusterUri()) { + if (!rootClusterUri) { return null; } diff --git a/web/packages/teleterm/src/ui/Search/SearchContext.tsx b/web/packages/teleterm/src/ui/Search/SearchContext.tsx index 86682321d3ef8..49bdea9fe1df2 100644 --- a/web/packages/teleterm/src/ui/Search/SearchContext.tsx +++ b/web/packages/teleterm/src/ui/Search/SearchContext.tsx @@ -33,6 +33,7 @@ import { useAppContext } from 'teleterm/ui/appContextProvider'; import { Document, DocumentClusterQueryParams, + useWorkspaceServiceState, } from 'teleterm/ui/services/workspacesService'; import { actionPicker, SearchPicker } from './pickers/pickers'; @@ -130,7 +131,7 @@ export const SearchContextProvider: FC = props => { ); } - appContext.workspacesService.useState(); + useWorkspaceServiceState(); const activeDocument = appContext.workspacesService .getActiveWorkspaceDocumentService() ?.getActive(); diff --git a/web/packages/teleterm/src/ui/Search/pickers/useDisplayResults.ts b/web/packages/teleterm/src/ui/Search/pickers/useDisplayResults.ts index 84a53651f75e1..8e98ad977f6ec 100644 --- a/web/packages/teleterm/src/ui/Search/pickers/useDisplayResults.ts +++ b/web/packages/teleterm/src/ui/Search/pickers/useDisplayResults.ts @@ -25,13 +25,14 @@ import { DisplayResults, } from 'teleterm/ui/Search/searchResult'; import { useAppContext } from 'teleterm/ui/appContextProvider'; +import { useWorkspaceServiceState } from 'teleterm/ui/services/workspacesService'; export function useDisplayResults(args: { filters: SearchFilter[]; inputValue: string; }): DisplayResults { const { workspacesService } = useAppContext(); - workspacesService.useState(); + useWorkspaceServiceState(); const localClusterUri = workspacesService.getActiveWorkspace()?.localClusterUri; diff --git a/web/packages/teleterm/src/ui/StatusBar/ShareFeedback/ShareFeedback.test.tsx b/web/packages/teleterm/src/ui/StatusBar/ShareFeedback/ShareFeedback.test.tsx index 460126b2d0869..6e88d8c1ebba0 100644 --- a/web/packages/teleterm/src/ui/StatusBar/ShareFeedback/ShareFeedback.test.tsx +++ b/web/packages/teleterm/src/ui/StatusBar/ShareFeedback/ShareFeedback.test.tsx @@ -16,14 +16,17 @@ * along with this program. If not, see . */ -import React from 'react'; import { screen } from '@testing-library/react'; import { fireEvent, render } from 'design/utils/testing'; import { MockAppContextProvider } from 'teleterm/ui/fixtures/MockAppContextProvider'; import { MockAppContext } from 'teleterm/ui/fixtures/mocks'; import { IAppContext } from 'teleterm/ui/types'; -import { Cluster } from 'teleterm/services/tshd/types'; + +import { + makeLoggedInUser, + makeRootCluster, +} from 'teleterm/services/tshd/testHelpers'; import { ShareFeedback } from './ShareFeedback'; @@ -41,48 +44,42 @@ function renderOpenedShareFeedback(appContext: IAppContext) { test('email field is not prefilled with the username if is not an email', () => { const appContext = new MockAppContext(); const clusterUri = '/clusters/localhost'; - jest - .spyOn(appContext.clustersService, 'findCluster') - .mockImplementation(() => { - return { - loggedInUser: { name: 'alice' }, - } as Cluster; - }); - - jest - .spyOn(appContext.workspacesService, 'getRootClusterUri') - .mockReturnValue(clusterUri); + appContext.workspacesService.setState(draft => { + draft.rootClusterUri = clusterUri; + }); + appContext.clustersService.setState(draft => { + draft.clusters.set( + clusterUri, + makeRootCluster({ + uri: clusterUri, + loggedInUser: makeLoggedInUser({ name: 'alice' }), + }) + ); + }); renderOpenedShareFeedback(appContext); - expect(appContext.clustersService.findCluster).toHaveBeenCalledWith( - clusterUri - ); expect(screen.getByLabelText('Email Address')).toHaveValue(''); }); test('email field is prefilled with the username if it looks like an email', () => { const appContext = new MockAppContext(); const clusterUri = '/clusters/production'; - jest - .spyOn(appContext.clustersService, 'findCluster') - .mockImplementation(() => { - return { - loggedInUser: { - name: 'bob@prod.com', - }, - } as Cluster; - }); - - jest - .spyOn(appContext.workspacesService, 'getRootClusterUri') - .mockReturnValue(clusterUri); + appContext.workspacesService.setState(draft => { + draft.rootClusterUri = clusterUri; + }); + appContext.clustersService.setState(draft => { + draft.clusters.set( + clusterUri, + makeRootCluster({ + uri: clusterUri, + loggedInUser: makeLoggedInUser({ name: 'bob@prod.com' }), + }) + ); + }); renderOpenedShareFeedback(appContext); - expect(appContext.clustersService.findCluster).toHaveBeenCalledWith( - clusterUri - ); expect(screen.getByLabelText('Email Address')).toHaveValue('bob@prod.com'); }); diff --git a/web/packages/teleterm/src/ui/StatusBar/ShareFeedback/useShareFeedback.ts b/web/packages/teleterm/src/ui/StatusBar/ShareFeedback/useShareFeedback.ts index 2c3d695fc1b22..a180c47b2fa41 100644 --- a/web/packages/teleterm/src/ui/StatusBar/ShareFeedback/useShareFeedback.ts +++ b/web/packages/teleterm/src/ui/StatusBar/ShareFeedback/useShareFeedback.ts @@ -16,13 +16,14 @@ * along with this program. If not, see . */ -import { useState } from 'react'; +import { useCallback, useState } from 'react'; import { makeEmptyAttempt, useAsync } from 'shared/hooks/useAsync'; import { staticConfig } from 'teleterm/staticConfig'; import { useAppContext } from 'teleterm/ui/appContextProvider'; +import { useStoreSelector } from 'teleterm/ui/hooks/useStoreSelector'; import { ShareFeedbackFormValues } from './types'; @@ -30,7 +31,10 @@ export const FEEDBACK_TOO_LONG_ERROR = 'FEEDBACK_TOO_LONG_ERROR'; export function useShareFeedback() { const ctx = useAppContext(); - ctx.workspacesService.useState(); + const rootClusterUri = useStoreSelector( + 'workspacesService', + useCallback(state => state.rootClusterUri, []) + ); ctx.clustersService.useState(); const [isShareFeedbackOpened, setIsShareFeedbackOpened] = useState(false); @@ -74,9 +78,7 @@ export function useShareFeedback() { } function getEmailFromUserName(): string { - const cluster = ctx.clustersService.findCluster( - ctx.workspacesService.getRootClusterUri() - ); + const cluster = ctx.clustersService.findCluster(rootClusterUri); const userName = cluster?.loggedInUser?.name; if (/^\S+@\S+$/.test(userName)) { return userName; diff --git a/web/packages/teleterm/src/ui/StatusBar/useAccessRequestCheckoutButton.ts b/web/packages/teleterm/src/ui/StatusBar/useAccessRequestCheckoutButton.ts index 093ea470db3f7..486ee85c57856 100644 --- a/web/packages/teleterm/src/ui/StatusBar/useAccessRequestCheckoutButton.ts +++ b/web/packages/teleterm/src/ui/StatusBar/useAccessRequestCheckoutButton.ts @@ -16,11 +16,13 @@ * along with this program. If not, see . */ +import { useWorkspaceServiceState } from 'teleterm/ui/services/workspacesService'; + import { useAppContext } from '../appContextProvider'; export function useAccessRequestsButton() { const ctx = useAppContext(); - ctx.workspacesService.useState(); + useWorkspaceServiceState(); const workspaceAccessRequest = ctx.workspacesService.getActiveWorkspaceAccessRequestsService(); diff --git a/web/packages/teleterm/src/ui/StatusBar/useActiveDocumentClusterBreadcrumbs.ts b/web/packages/teleterm/src/ui/StatusBar/useActiveDocumentClusterBreadcrumbs.ts index c9650be3d94c5..bd785cf51d72a 100644 --- a/web/packages/teleterm/src/ui/StatusBar/useActiveDocumentClusterBreadcrumbs.ts +++ b/web/packages/teleterm/src/ui/StatusBar/useActiveDocumentClusterBreadcrumbs.ts @@ -17,12 +17,15 @@ */ import { useAppContext } from 'teleterm/ui/appContextProvider'; -import { getResourceUri } from 'teleterm/ui/services/workspacesService'; +import { + getResourceUri, + useWorkspaceServiceState, +} from 'teleterm/ui/services/workspacesService'; import { routing } from 'teleterm/ui/uri'; export function useActiveDocumentClusterBreadcrumbs(): string { const ctx = useAppContext(); - ctx.workspacesService.useState(); + useWorkspaceServiceState(); ctx.clustersService.useState(); const activeDocument = ctx.workspacesService diff --git a/web/packages/teleterm/src/ui/TabHost/TabHost.test.tsx b/web/packages/teleterm/src/ui/TabHost/TabHost.test.tsx index 3e3a6a392a69d..7410532aff073 100644 --- a/web/packages/teleterm/src/ui/TabHost/TabHost.test.tsx +++ b/web/packages/teleterm/src/ui/TabHost/TabHost.test.tsx @@ -18,7 +18,7 @@ import 'jest-canvas-mock'; import { createRef } from 'react'; -import { fireEvent, render, screen } from 'design/utils/testing'; +import { fireEvent, render, screen, act } from 'design/utils/testing'; import { TabHost } from 'teleterm/ui/TabHost/TabHost'; import { MockAppContextProvider } from 'teleterm/ui/fixtures/MockAppContextProvider'; @@ -26,7 +26,11 @@ import { Document } from 'teleterm/ui/services/workspacesService'; import { TabContextMenuOptions } from 'teleterm/mainProcess/types'; import { makeDocumentCluster } from 'teleterm/ui/services/workspacesService/documentsService/testHelpers'; import { MockAppContext } from 'teleterm/ui/fixtures/mocks'; -import { makeRootCluster } from 'teleterm/services/tshd/testHelpers'; +import { + makeRootCluster, + rootClusterUri, +} from 'teleterm/services/tshd/testHelpers'; +import { routing } from 'teleterm/ui/uri'; function getMockDocuments(): Document[] { return [ @@ -43,8 +47,6 @@ function getMockDocuments(): Document[] { ]; } -const rootClusterUri = '/clusters/test_uri'; - async function getTestSetup({ documents }: { documents: Document[] }) { const appContext = new MockAppContext(); jest.spyOn(appContext.mainProcessClient, 'openTabContextMenu'); @@ -64,7 +66,10 @@ async function getTestSetup({ documents }: { documents: Document[] }) { documents, location: documents[0]?.uri, localClusterUri: rootClusterUri, - accessRequests: undefined, + accessRequests: { + isBarCollapsed: true, + pending: { kind: 'resource', resources: new Map() }, + }, }; }); @@ -137,16 +142,24 @@ test('open context menu', async () => { const options: TabContextMenuOptions = openTabContextMenu.mock.calls[0][0]; expect(options.document).toEqual(document); - options.onClose(); + act(() => { + options.onClose(); + }); expect(close).toHaveBeenCalledWith(document.uri); - options.onCloseOthers(); + act(() => { + options.onCloseOthers(); + }); expect(closeOthers).toHaveBeenCalledWith(document.uri); - options.onCloseToRight(); + act(() => { + options.onCloseToRight(); + }); expect(closeToRight).toHaveBeenCalledWith(document.uri); - options.onDuplicatePty(); + act(() => { + options.onDuplicatePty(); + }); expect(duplicatePtyAndActivate).toHaveBeenCalledWith(document.uri); }); @@ -155,7 +168,15 @@ test('open new tab', async () => { documents: [getMockDocuments()[0]], }); const { add, open } = docsService; - const mockedClusterDocument = makeDocumentCluster(); + // Use a URI of a cluster that's not in ClustersService so that DocumentCluster doesn't render + // UnifiedResources for it. UnifiedResources requires a lot of mocks to be set up. + const nonExistentClusterUri = routing.getClusterUri({ + ...routing.parseClusterUri(rootClusterUri).params, + leafClusterId: 'nonexistent-leaf', + }); + const mockedClusterDocument = makeDocumentCluster({ + clusterUri: nonExistentClusterUri, + }); docsService.createClusterDocument = () => mockedClusterDocument; const $newTabButton = screen.getByTitle('New Tab', { exact: false }); diff --git a/web/packages/teleterm/src/ui/TabHost/TabHost.tsx b/web/packages/teleterm/src/ui/TabHost/TabHost.tsx index 8e59d1ef01668..504c53583e143 100644 --- a/web/packages/teleterm/src/ui/TabHost/TabHost.tsx +++ b/web/packages/teleterm/src/ui/TabHost/TabHost.tsx @@ -16,11 +16,12 @@ * along with this program. If not, see . */ -import React from 'react'; +import React, { useCallback } from 'react'; import styled from 'styled-components'; import { Flex } from 'design'; import { useAppContext } from 'teleterm/ui/appContextProvider'; +import { useWorkspaceServiceState } from 'teleterm/ui/services/workspacesService'; import * as types from 'teleterm/ui/services/workspacesService/documentsService/types'; import { canDocChangeShell } from 'teleterm/ui/services/workspacesService/documentsService/types'; import { Tabs } from 'teleterm/ui/Tabs'; @@ -29,6 +30,8 @@ import { IAppContext } from 'teleterm/ui/types'; import { useKeyboardShortcutFormatters } from 'teleterm/ui/services/keyboardShortcuts'; import { Shell } from 'teleterm/mainProcess/shell'; +import { useStoreSelector } from '../hooks/useStoreSelector'; + import { useTabShortcuts } from './useTabShortcuts'; import { useNewTabOpener } from './useNewTabOpener'; import { ClusterConnectPanel } from './ClusterConnectPanel/ClusterConnectPanel'; @@ -37,8 +40,10 @@ export function TabHostContainer(props: { topBarContainerRef: React.MutableRefObject; }) { const ctx = useAppContext(); - ctx.workspacesService.useState(); - const isRootClusterSelected = !!ctx.workspacesService.getRootClusterUri(); + const isRootClusterSelected = useStoreSelector( + 'workspacesService', + useCallback(state => !!state.rootClusterUri, []) + ); if (isRootClusterSelected) { return ; @@ -53,6 +58,7 @@ export function TabHost({ ctx: IAppContext; topBarContainerRef: React.MutableRefObject; }) { + useWorkspaceServiceState(); const documentsService = ctx.workspacesService.getActiveWorkspaceDocumentService(); const activeDocument = documentsService?.getActive(); diff --git a/web/packages/teleterm/src/ui/TopBar/AdditionalActions.tsx b/web/packages/teleterm/src/ui/TopBar/AdditionalActions.tsx index 493c7f2828cfe..17f59846bc01a 100644 --- a/web/packages/teleterm/src/ui/TopBar/AdditionalActions.tsx +++ b/web/packages/teleterm/src/ui/TopBar/AdditionalActions.tsx @@ -32,6 +32,7 @@ import { KeyboardShortcutAction } from 'teleterm/services/config'; import { useKeyboardShortcutFormatters } from 'teleterm/ui/services/keyboardShortcuts'; import { ListItem } from 'teleterm/ui/components/ListItem'; import { useNewTabOpener } from 'teleterm/ui/TabHost'; +import { useWorkspaceServiceState } from 'teleterm/ui/services/workspacesService'; type MenuItem = { title: string; @@ -48,7 +49,7 @@ type MenuItemConditionallyDisabled = { isDisabled: true; disabledText: string }; function useMenuItems(): MenuItem[] { const ctx = useAppContext(); const { workspacesService, mainProcessClient, notificationsService } = ctx; - workspacesService.useState(); + useWorkspaceServiceState(); ctx.clustersService.useState(); const documentsService = workspacesService.getActiveWorkspaceDocumentService(); diff --git a/web/packages/teleterm/src/ui/TopBar/Clusters/useClusters.ts b/web/packages/teleterm/src/ui/TopBar/Clusters/useClusters.ts index a51ddcb05bf1f..fb40c0aff790e 100644 --- a/web/packages/teleterm/src/ui/TopBar/Clusters/useClusters.ts +++ b/web/packages/teleterm/src/ui/TopBar/Clusters/useClusters.ts @@ -17,13 +17,14 @@ */ import { useAppContext } from 'teleterm/ui/appContextProvider'; +import { useWorkspaceServiceState } from 'teleterm/ui/services/workspacesService'; import { ClusterUri } from 'teleterm/ui/uri'; export function useClusters() { const { workspacesService, clustersService, commandLauncher } = useAppContext(); - workspacesService.useState(); + useWorkspaceServiceState(); clustersService.useState(); function findLeaves(clusterUri: string) { diff --git a/web/packages/teleterm/src/ui/TopBar/Identity/useIdentity.ts b/web/packages/teleterm/src/ui/TopBar/Identity/useIdentity.ts index 2df389a4205a9..581061db169aa 100644 --- a/web/packages/teleterm/src/ui/TopBar/Identity/useIdentity.ts +++ b/web/packages/teleterm/src/ui/TopBar/Identity/useIdentity.ts @@ -19,12 +19,13 @@ import { useAppContext } from 'teleterm/ui/appContextProvider'; import { Cluster, LoggedInUser } from 'teleterm/services/tshd/types'; import { RootClusterUri } from 'teleterm/ui/uri'; +import { useWorkspaceServiceState } from 'teleterm/ui/services/workspacesService'; export function useIdentity() { const ctx = useAppContext(); ctx.clustersService.useState(); - ctx.workspacesService.useState(); + useWorkspaceServiceState(); async function changeRootCluster(clusterUri: RootClusterUri): Promise { await ctx.workspacesService.setActiveWorkspace(clusterUri); diff --git a/web/packages/teleterm/src/ui/hooks/useLoggedInUser.ts b/web/packages/teleterm/src/ui/hooks/useLoggedInUser.ts index 42bc74a989bb6..42ee066e6b702 100644 --- a/web/packages/teleterm/src/ui/hooks/useLoggedInUser.ts +++ b/web/packages/teleterm/src/ui/hooks/useLoggedInUser.ts @@ -16,10 +16,14 @@ * along with this program. If not, see . */ +import { useCallback } from 'react'; + import { useAppContext } from 'teleterm/ui/appContextProvider'; import { useWorkspaceContext } from 'teleterm/ui/Documents'; import { LoggedInUser } from 'teleterm/services/tshd/types'; +import { useStoreSelector } from './useStoreSelector'; + /** * useLoggedInUser returns the user logged into the root cluster of the active workspace. The return * value changes depending on the active workspace. @@ -30,11 +34,14 @@ import { LoggedInUser } from 'teleterm/services/tshd/types'; * It might return undefined if there's no active workspace. */ export function useLoggedInUser(): LoggedInUser | undefined { - const { clustersService, workspacesService } = useAppContext(); + const { clustersService } = useAppContext(); clustersService.useState(); - workspacesService.useState(); - const clusterUri = workspacesService.getRootClusterUri(); + const clusterUri = useStoreSelector( + 'workspacesService', + useCallback(store => store.rootClusterUri, []) + ); + if (!clusterUri) { return; } diff --git a/web/packages/teleterm/src/ui/hooks/useStoreSelector.ts b/web/packages/teleterm/src/ui/hooks/useStoreSelector.ts index f0f2f07cbece2..16c1f133c5520 100644 --- a/web/packages/teleterm/src/ui/hooks/useStoreSelector.ts +++ b/web/packages/teleterm/src/ui/hooks/useStoreSelector.ts @@ -70,3 +70,15 @@ export const useStoreSelector = < type ImmutableStoreKeys = { [K in keyof T]: T[K] extends ImmutableStore ? K : never; }[keyof T]; + +/** + * identitySelector returns the whole state of the given store. + * + * Useful during refactorings of legacy code which depends on the useStore which triggers a + * re-render on any change to the store. + * + * Should be used sparingly. It's often a better idea to make the selector as narrow as possible. + */ +export function identitySelector(state: Value): Value { + return state; +} diff --git a/web/packages/teleterm/src/ui/services/workspacesService/workspacesService.ts b/web/packages/teleterm/src/ui/services/workspacesService/workspacesService.ts index d4288ac72dff5..d2899b02df939 100644 --- a/web/packages/teleterm/src/ui/services/workspacesService/workspacesService.ts +++ b/web/packages/teleterm/src/ui/services/workspacesService/workspacesService.ts @@ -17,7 +17,6 @@ */ import { z } from 'zod'; -import { useStore } from 'shared/libs/stores'; import { arrayObjectIsEqual } from 'shared/utils/highbar'; import { @@ -44,6 +43,11 @@ import { routing, } from 'teleterm/ui/uri'; +import { + identitySelector, + useStoreSelector, +} from 'teleterm/ui/hooks/useStoreSelector'; + import { AccessRequestsService, getEmptyPendingAccessRequest, @@ -220,10 +224,6 @@ export class WorkspacesService extends ImmutableStore { ); } - useState() { - return useStore(this); - } - setState(nextState: (draftState: WorkspacesState) => WorkspacesState | void) { super.setState(nextState); this.persistState(); @@ -616,3 +616,15 @@ const unifiedResourcePreferencesSchema = z type UnifiedResourcePreferencesSchemaAsRequired = Required< z.infer >; + +/** + * useWorkspaceServiceState is a replacement for the legacy useStore hook. Many components within + * teleterm depend on the behavior of useStore which re-renders the component on any change within + * the store. Most of the time, those components don't even use the state returned by useStore. + * + * @deprecated Prefer useStoreSelector with a selector that picks only what the callsite is going + * to use. useWorkspaceServiceState re-renders the component on any change within any workspace. + */ +export const useWorkspaceServiceState = () => { + return useStoreSelector('workspacesService', identitySelector); +};