Line data Source code
1 : import * as React from 'react';
2 : import { useLocation, useNavigate } from "react-router-dom";
3 : import { VariantKind } from "../../utils/variant.js";
4 : import { AppLoadingStatus } from '../loading/app_loading_status.js';
5 : import { SessionSetupStatus } from '../loading/session_setup_status.js';
6 : import { LoggableException } from '../../platform/logger/logger.js';
7 :
8 : export interface RouteContext {
9 : /// The app loading status
10 : appLoadingStatus: AppLoadingStatus;
11 : /// Confirmed the finished setup?
12 : confirmedFinishedSetup: boolean;
13 : /// The focused session id
14 : sessionId: string | null;
15 : /// The session setup status
16 : sessionSetupStatus: SessionSetupStatus;
17 : }
18 :
19 1 : export const SESSION_PATH = Symbol("NAVIGATE_SESSION");
20 1 : export const FINISH_SETUP = Symbol("FINISH_SETUP");
21 1 : export const CONFIRM_FINISHED_SETUP = Symbol("CONFIRM_FINISHED_SETUP");
22 1 : export const SKIP_SETUP = Symbol("SKIP_SETUP");
23 1 : export const SELECT_SESSION = Symbol("SELECT_SESSION");
24 1 : export const CHANGE_SESSION = Symbol("CHANGE_SESSION");
25 1 : export const BEGIN_SESSION_SETUP = Symbol("BEGIN_SESSION_SETUP");
26 1 : export const CANCEL_SESSION_SETUP = Symbol("CANCEL_SESSION_SETUP");
27 1 : export const SKIP_SESSION_SETUP = Symbol("SKIP_SESSION_SETUP");
28 1 : export const OPEN_LINK_SESSION = Symbol("OPEN_LINK_SESSION");
29 1 : export const OPEN_SESSION = Symbol("OPEN_SESSION");
30 1 : export const OPEN_EPHEMERAL_SESSION = Symbol("OPEN_EPHEMERAL_SESSION");
31 1 : export const PROMOTE_SESSION = Symbol("PROMOTE_SESSION");
32 :
33 1 : export function sessionPath(sessionId: string): string {
34 1 : return `/sessions/${encodeURIComponent(sessionId)}`;
35 1 : }
36 :
37 1 : export function sessionIdFromPathname(pathname: string): string | null {
38 1 : const match = /^\/sessions\/([^/]+)\/?$/.exec(pathname);
39 1 : if (match == null) return null;
40 1 : try {
41 1 : return decodeURIComponent(match[1]);
42 1 : } catch {
43 1 : return null;
44 1 : }
45 1 : }
46 :
47 : export type RouteTarget =
48 : VariantKind<typeof SESSION_PATH, string | null>
49 : | VariantKind<typeof FINISH_SETUP, null>
50 : | VariantKind<typeof CONFIRM_FINISHED_SETUP, boolean>
51 : | VariantKind<typeof SKIP_SETUP, null>
52 : | VariantKind<typeof SELECT_SESSION, string>
53 : | VariantKind<typeof CHANGE_SESSION, null>
54 : | VariantKind<typeof BEGIN_SESSION_SETUP, string>
55 : | VariantKind<typeof CANCEL_SESSION_SETUP, null>
56 : | VariantKind<typeof SKIP_SESSION_SETUP, null>
57 : | VariantKind<typeof OPEN_LINK_SESSION, string>
58 : | VariantKind<typeof OPEN_SESSION, string>
59 : | VariantKind<typeof OPEN_EPHEMERAL_SESSION, string>
60 : | VariantKind<typeof PROMOTE_SESSION, string>
61 : ;
62 :
63 1 : export function useRouteContext() {
64 1 : const location = useLocation();
65 1 : const route = location.state as RouteContext;
66 1 : const pathname = location.pathname;
67 : // Memoized on the location, callers depend on the context in callbacks and effects
68 1 : return React.useMemo(() => {
69 1 : if (!route) {
70 1 : return {
71 1 : appLoadingStatus: AppLoadingStatus.NOT_STARTED,
72 1 : confirmedFinishedSetup: false,
73 1 : sessionId: sessionIdFromPathname(pathname),
74 1 : sessionSetupStatus: SessionSetupStatus.NONE,
75 1 : };
76 : } else {
77 1 : return {
78 1 : ...route,
79 1 : sessionSetupStatus: route.sessionSetupStatus ?? SessionSetupStatus.NONE,
80 1 : };
81 : }
82 1 : }, [route, pathname]);
83 1 : }
84 :
85 1 : export function useRouterNavigate() {
86 1 : const navigate = useNavigate();
87 1 : const location = useLocation();
88 1 : const context = useRouteContext();
89 1 : return React.useCallback((route: RouteTarget) => {
90 1 : switch (route.type) {
91 1 : case SESSION_PATH:
92 1 : navigate(route.value == null ? "/" : sessionPath(route.value), {
93 1 : state: {
94 1 : ...context,
95 1 : sessionId: route.value ?? null,
96 1 : }
97 1 : });
98 1 : break;
99 1 : case SKIP_SETUP:
100 1 : navigate(location.pathname, {
101 1 : state: {
102 1 : ...context,
103 1 : appLoadingStatus: AppLoadingStatus.SETUP_DONE,
104 1 : }
105 1 : });
106 1 : break;
107 1 : case FINISH_SETUP:
108 1 : navigate(location.pathname, {
109 1 : state: {
110 1 : appLoadingStatus: AppLoadingStatus.SETUP_DONE,
111 1 : confirmedFinishedSetup: false,
112 1 : sessionId: null,
113 1 : sessionSetupStatus: SessionSetupStatus.NONE,
114 1 : }
115 1 : });
116 1 : break;
117 1 : case SELECT_SESSION:
118 1 : navigate(sessionPath(route.value), {
119 1 : state: {
120 1 : ...context,
121 1 : sessionId: route.value,
122 1 : sessionSetupStatus: SessionSetupStatus.NONE,
123 1 : }
124 1 : });
125 1 : break;
126 1 : case OPEN_SESSION:
127 1 : navigate(sessionPath(route.value), {
128 1 : state: {
129 1 : ...context,
130 1 : sessionId: route.value,
131 1 : sessionSetupStatus: SessionSetupStatus.OPENING,
132 1 : }
133 1 : });
134 1 : break;
135 1 : case CHANGE_SESSION:
136 1 : navigate("/", {
137 1 : state: {
138 1 : ...context,
139 1 : sessionId: null,
140 1 : sessionSetupStatus: SessionSetupStatus.NONE,
141 1 : }
142 1 : });
143 1 : break;
144 1 : case BEGIN_SESSION_SETUP:
145 1 : navigate(sessionPath(route.value), {
146 1 : state: {
147 1 : ...context,
148 1 : sessionId: route.value,
149 1 : sessionSetupStatus: SessionSetupStatus.CONFIGURING,
150 1 : }
151 1 : });
152 1 : break;
153 1 : case CANCEL_SESSION_SETUP:
154 1 : navigate("/", {
155 1 : state: {
156 1 : ...context,
157 1 : sessionId: null,
158 1 : sessionSetupStatus: SessionSetupStatus.NONE,
159 1 : }
160 1 : });
161 1 : break;
162 1 : case SKIP_SESSION_SETUP:
163 1 : navigate(location.pathname, {
164 1 : state: {
165 1 : ...context,
166 1 : sessionSetupStatus: SessionSetupStatus.NONE,
167 1 : }
168 1 : });
169 1 : break;
170 1 : case OPEN_LINK_SESSION:
171 : // A session arrived via a shared link and has been restored into the registries.
172 : // Start the common opening flow atomically so embedded connections can initialize
173 : // without briefly showing the connection setup screen.
174 1 : navigate(sessionPath(route.value), {
175 1 : state: {
176 1 : appLoadingStatus: AppLoadingStatus.SETUP_DONE,
177 1 : confirmedFinishedSetup: false,
178 1 : sessionId: route.value,
179 1 : sessionSetupStatus: SessionSetupStatus.OPENING,
180 1 : }
181 1 : });
182 1 : break;
183 1 : case OPEN_EPHEMERAL_SESSION:
184 : // The greeter stays at the root until a script promotes the session. The startup replaces its entry:
185 : // going back to it would find a root without a route context and show the loading page.
186 : // The status is not OPENING, the loader would open the path of the session.
187 1 : navigate("/", {
188 1 : replace: location.pathname === "/",
189 1 : state: {
190 1 : appLoadingStatus: AppLoadingStatus.SETUP_DONE,
191 1 : confirmedFinishedSetup: context.confirmedFinishedSetup,
192 1 : sessionId: route.value,
193 1 : sessionSetupStatus: SessionSetupStatus.NONE,
194 1 : }
195 1 : });
196 1 : break;
197 1 : case PROMOTE_SESSION:
198 : // Replaces the root, going back would promote the session again
199 1 : navigate(sessionPath(route.value), {
200 1 : replace: true,
201 1 : state: {
202 1 : ...context,
203 1 : sessionId: route.value,
204 1 : sessionSetupStatus: SessionSetupStatus.NONE,
205 1 : }
206 1 : });
207 1 : break;
208 1 : case CONFIRM_FINISHED_SETUP:
209 1 : if (context.appLoadingStatus != AppLoadingStatus.SETUP_DONE) {
210 1 : throw new LoggableException("tried to confirm a non-finished setup");
211 : }
212 1 : navigate(location.pathname, {
213 1 : state: {
214 1 : ...context,
215 1 : appLoadingStatus: AppLoadingStatus.SETUP_DONE,
216 1 : confirmedFinishedSetup: route.value,
217 1 : }
218 1 : });
219 : break;
220 1 : }
221 1 : }, [navigate, context, location]);
222 1 : }
|