-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Prevent activation of previous workspace when launching Connect via deep link to a different cluster #50063
base: master
Are you sure you want to change the base?
Conversation
…spaceDefaultState`
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So far I only looked through the code, I'll try to test this on Monday.
web/packages/teleterm/src/ui/services/workspacesService/workspacesService.ts
Show resolved
Hide resolved
web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.ts
Outdated
Show resolved
Hide resolved
web/packages/teleterm/src/ui/services/workspacesService/workspacesService.ts
Outdated
Show resolved
Hide resolved
web/packages/teleterm/src/ui/services/workspacesService/workspacesService.ts
Outdated
Show resolved
Hide resolved
web/packages/teleterm/src/ui/services/workspacesService/workspacesService.test.ts
Outdated
Show resolved
Hide resolved
web/packages/teleterm/src/ui/services/workspacesService/workspacesService.ts
Show resolved
Hide resolved
const persistedState = this.statePersistenceService.getWorkspacesState(); | ||
restorePersistedState(): void { | ||
const restoredState = this.statePersistenceService.getWorkspacesState(); | ||
this.restoredState = restoredState; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any way we can guarantee that this is not modified? Keep it as imerrized object instead of a standard JS one or something? Have a private setter and a public getter that always calls structuredClone
before returning the value?
The way it is right now, it's way to easy to overwrite it by mistake or hand out references to objects inside this.restoredState
. I tried using microsoft/TypeScript#13923 (comment) which would be pretty handy – the compiler would tell you to deep clone parts that you'd like to use for mutable purposes. Unfortunately, I couldn't come up with the right types for a function that would take a deep readonly value, call structuredClone
on it and return a "deep mutable" version of it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good idea to make it immutable! I decided to use produce
and Immutable
from immer.js, I think it's the simplest option.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, that seems much better than everything that I suggested.
web/packages/teleterm/src/ui/services/workspacesService/workspacesService.ts
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did a scan of the code but tested locally and it works. Leaving premp approval because I'm out next week
@@ -119,6 +115,12 @@ export class WorkspacesService extends ImmutableStore<WorkspacesState> { | |||
workspaces: {}, | |||
isInitialized: false, | |||
}; | |||
/** | |||
* Keeps the state restored when the app was launched. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* Keeps the state restored when the app was launched. | |
* Keeps the state that was restored from disk when the app was launched. |
resourceKinds: d.queryParams?.resourceKinds | ||
? [...d.queryParams.resourceKinds] // makes the array mutable | ||
: defaultParams.resourceKinds, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder why commenting out sort
above doesn't trigger a similar type error. I suppose types for readonly objects behave differently than types for readonly arrays?
const persistedState = this.statePersistenceService.getWorkspacesState(); | ||
restorePersistedState(): void { | ||
const restoredState = this.statePersistenceService.getWorkspacesState(); | ||
this.restoredState = restoredState; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, that seems much better than everything that I suggested.
}); | ||
break; | ||
case 'discarded': | ||
this.discardPreviousDocuments(clusterUri); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add tests for this behavior? Specifically that closing the modal either through the X button or through the "Start new session" button discards previous documents, but opening a new regular modal does not discard them? This behavior requires a lot of things to be wired up correctly and I worry that it might be broken easily when refactoring either WorkspacesService
, ModalsService
, ModalsHost
or how our modals work in general.
// The first one comes from restoring the previous session, but it is | ||
// immediately canceled and replaced with a dialog to the cluster from | ||
// the deep link. | ||
await waitFor( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couldn't we wait here for the connect-cluster dialog to be opened with the correct content rather than doing waitFor
for a mocked function? As in expect(screen.findByText(<whatever is shown in the modal>))
. If possible, we shouldn't use toHaveBeenCalled
as something that moves tests forward, but rather the actual results that the action has on the interface that the user sees.
* be changed to happen right before the modal is shown. Ultimately, the thing that interests us | ||
* the most is whether the state from disk was loaded into memory. Maybe in the future we will | ||
* need to separate values or an enum. | ||
* | ||
*/ | ||
isInitialized: boolean; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's add a comment above this line in vnetContext.tsx:
isWorkspaceStateInitialized && |
which explains that accessing resources through VNet might trigger the MFA modal, so we have to wait for the tshd events service to be initialized.
await this.clustersService.syncRootClustersAndCatchErrors(); | ||
this.workspacesService.restorePersistedState(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I close the app while I'm in workspace A with a valid cert and open it again through a deep link to workspace B, I can close the login modal and continue to use workspace A without making a decision wrt restored state. The user should be forced to make an action before interacting with a workspace.
valid-cert-on-launch.mov
How do we fix this? I almost feel like DeepLinksService
should always bail out to the "Connect" screen. As in, when loginAndSetActiveWorkspace
is called, we check if the current workspace is equal to the workspace from the URL. If not, then we always zero rootClusterUri
in WorkspacesService
to force the user back to cluster choice. Otherwise, I feel it kind of makes little sense to launch the app through a link to workspace B, interact with a modal about workspace B (the login modal), then close it and be back at workspace A, just because it was the last used workspace before the app was closed.
I'm just not sure at this point how this is going to interact with the rest of the app.
* be changed to happen right before the modal is shown. Ultimately, the thing that interests us | ||
* the most is whether the state from disk was loaded into memory. Maybe in the future we will | ||
* need to separate values or an enum. | ||
* | ||
*/ | ||
isInitialized: boolean; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As discussed on the call, move setting this field out of restorePersistedState
into a separate method that's called from pullInitialState
.
Closes #40962
Problem
Connect always tries to restore the previous workspace, even when it is run via a deep link. This is incorrect behavior- if the user wants to authorize a session for cluster A, we shouldn't force him to log in to a cluster B first.
Solution
Currently, restoring the workspaces from disk also actives the previously active workspace (this includes showing a login dialog if certs expired). Opening a deep link waits for that state to be restored, so it also waits for that dialog to be completed.
We don't want this, so we need to decouple workspace restoration from activating the previously active workspace. This will allow us to signal the frontend interface readiness before the login dialog finishes. When the frontend is ready, a deep link can be processed to activate its corresponding workspace.
Our modal service allows only one regular dialog is active at any time, so opening a new dialog will automatically cancel an existing one.
In other words:
While working on this fix, I cleaned up some code in
workspacesService
anddocumentsService
:setActiveWorkspace
had a mix of async/await and.then
, it has been rewritten to the newer syntax.previous
field in the workspace state. Instead, I store the entire restored state in a separate variable. I needed this, because if we don't restore the previousrootClusterUri
immediately, it is overwritten withundefined
insetState
. I think this approach is actually simpler than what we had before.