|
1 | 1 | import { css, type Interpolation, type Theme, useTheme } from "@emotion/react";
|
2 |
| -import KeyboardArrowDownOutlined from "@mui/icons-material/KeyboardArrowDownOutlined"; |
3 | 2 | import MenuIcon from "@mui/icons-material/Menu";
|
4 |
| -import Button from "@mui/material/Button"; |
5 |
| -import Divider from "@mui/material/Divider"; |
6 | 3 | import Drawer from "@mui/material/Drawer";
|
7 | 4 | import IconButton from "@mui/material/IconButton";
|
8 |
| -import Menu from "@mui/material/Menu"; |
9 |
| -import MenuItem from "@mui/material/MenuItem"; |
10 |
| -import Skeleton from "@mui/material/Skeleton"; |
11 |
| -import { visuallyHidden } from "@mui/utils"; |
12 |
| -import { type FC, useRef, useState } from "react"; |
13 |
| -import { NavLink, useLocation, useNavigate } from "react-router-dom"; |
| 5 | +import { type FC, useState } from "react"; |
| 6 | +import { NavLink, useLocation } from "react-router-dom"; |
14 | 7 | import type * as TypesGen from "api/typesGenerated";
|
15 |
| -import { Abbr } from "components/Abbr/Abbr"; |
16 | 8 | import { ExternalImage } from "components/ExternalImage/ExternalImage";
|
17 |
| -import { displayError } from "components/GlobalSnackbar/utils"; |
18 | 9 | import { CoderIcon } from "components/Icons/CoderIcon";
|
19 |
| -import { Latency } from "components/Latency/Latency"; |
20 |
| -import { useAuthenticated } from "contexts/auth/RequireAuth"; |
21 | 10 | import type { ProxyContextValue } from "contexts/ProxyContext";
|
22 |
| -import { BUTTON_SM_HEIGHT, navHeight } from "theme/constants"; |
| 11 | +import { navHeight } from "theme/constants"; |
23 | 12 | import { DeploymentDropdown } from "./DeploymentDropdown";
|
| 13 | +import { ProxyMenu } from "./ProxyMenu"; |
24 | 14 | import { UserDropdown } from "./UserDropdown/UserDropdown";
|
25 | 15 |
|
26 | 16 | export interface NavbarViewProps {
|
@@ -163,237 +153,6 @@ export const NavbarView: FC<NavbarViewProps> = ({
|
163 | 153 | );
|
164 | 154 | };
|
165 | 155 |
|
166 |
| -interface ProxyMenuProps { |
167 |
| - proxyContextValue: ProxyContextValue; |
168 |
| -} |
169 |
| - |
170 |
| -const ProxyMenu: FC<ProxyMenuProps> = ({ proxyContextValue }) => { |
171 |
| - const theme = useTheme(); |
172 |
| - const buttonRef = useRef<HTMLButtonElement>(null); |
173 |
| - const [isOpen, setIsOpen] = useState(false); |
174 |
| - const [refetchDate, setRefetchDate] = useState<Date>(); |
175 |
| - const selectedProxy = proxyContextValue.proxy.proxy; |
176 |
| - const refreshLatencies = proxyContextValue.refetchProxyLatencies; |
177 |
| - const closeMenu = () => setIsOpen(false); |
178 |
| - const navigate = useNavigate(); |
179 |
| - const latencies = proxyContextValue.proxyLatencies; |
180 |
| - const isLoadingLatencies = Object.keys(latencies).length === 0; |
181 |
| - const isLoading = proxyContextValue.isLoading || isLoadingLatencies; |
182 |
| - const { permissions } = useAuthenticated(); |
183 |
| - |
184 |
| - const proxyLatencyLoading = (proxy: TypesGen.Region): boolean => { |
185 |
| - if (!refetchDate) { |
186 |
| - // Only show loading if the user manually requested a refetch |
187 |
| - return false; |
188 |
| - } |
189 |
| - |
190 |
| - // Only show a loading spinner if: |
191 |
| - // - A latency exists. This means the latency was fetched at some point, so |
192 |
| - // the loader *should* be resolved. |
193 |
| - // - The proxy is healthy. If it is not, the loader might never resolve. |
194 |
| - // - The latency reported is older than the refetch date. This means the |
195 |
| - // latency is stale and we should show a loading spinner until the new |
196 |
| - // latency is fetched. |
197 |
| - const latency = latencies[proxy.id]; |
198 |
| - return proxy.healthy && latency !== undefined && latency.at < refetchDate; |
199 |
| - }; |
200 |
| - |
201 |
| - // This endpoint returns a 404 when not using enterprise. |
202 |
| - // If we don't return null, then it looks like this is |
203 |
| - // loading forever! |
204 |
| - if (proxyContextValue.error) { |
205 |
| - return null; |
206 |
| - } |
207 |
| - |
208 |
| - if (isLoading) { |
209 |
| - return ( |
210 |
| - <Skeleton |
211 |
| - width="110px" |
212 |
| - height={BUTTON_SM_HEIGHT} |
213 |
| - css={{ borderRadius: "9999px", transform: "none" }} |
214 |
| - /> |
215 |
| - ); |
216 |
| - } |
217 |
| - |
218 |
| - return ( |
219 |
| - <> |
220 |
| - <Button |
221 |
| - ref={buttonRef} |
222 |
| - onClick={() => setIsOpen(true)} |
223 |
| - size="small" |
224 |
| - endIcon={<KeyboardArrowDownOutlined />} |
225 |
| - css={{ |
226 |
| - "& .MuiSvgIcon-root": { fontSize: 14 }, |
227 |
| - }} |
228 |
| - > |
229 |
| - <span css={{ ...visuallyHidden }}> |
230 |
| - Latency for {selectedProxy?.display_name ?? "your region"} |
231 |
| - </span> |
232 |
| - |
233 |
| - {selectedProxy ? ( |
234 |
| - <div css={{ display: "flex", gap: 8, alignItems: "center" }}> |
235 |
| - <div css={{ width: 16, height: 16, lineHeight: 0 }}> |
236 |
| - <img |
237 |
| - // Empty alt text used because we don't want to double up on |
238 |
| - // screen reader announcements from visually-hidden span |
239 |
| - alt="" |
240 |
| - src={selectedProxy.icon_url} |
241 |
| - css={{ |
242 |
| - objectFit: "contain", |
243 |
| - width: "100%", |
244 |
| - height: "100%", |
245 |
| - }} |
246 |
| - /> |
247 |
| - </div> |
248 |
| - |
249 |
| - <Latency |
250 |
| - latency={latencies?.[selectedProxy.id]?.latencyMS} |
251 |
| - isLoading={proxyLatencyLoading(selectedProxy)} |
252 |
| - /> |
253 |
| - </div> |
254 |
| - ) : ( |
255 |
| - "Select Proxy" |
256 |
| - )} |
257 |
| - </Button> |
258 |
| - |
259 |
| - <Menu |
260 |
| - open={isOpen} |
261 |
| - anchorEl={buttonRef.current} |
262 |
| - onClick={closeMenu} |
263 |
| - onClose={closeMenu} |
264 |
| - css={{ "& .MuiMenu-paper": { paddingTop: 8, paddingBottom: 8 } }} |
265 |
| - // autoFocus here does not affect modal focus; it affects whether the |
266 |
| - // first item in the list will get auto-focus when the menu opens. Have |
267 |
| - // to turn this off because otherwise, screen readers will skip over all |
268 |
| - // the descriptive text and will only have access to the latency options |
269 |
| - autoFocus={false} |
270 |
| - > |
271 |
| - <div |
272 |
| - css={{ |
273 |
| - width: "100%", |
274 |
| - maxWidth: "320px", |
275 |
| - fontSize: 14, |
276 |
| - padding: 16, |
277 |
| - lineHeight: "140%", |
278 |
| - }} |
279 |
| - > |
280 |
| - <h4 |
281 |
| - autoFocus |
282 |
| - tabIndex={-1} |
283 |
| - css={{ |
284 |
| - fontSize: "inherit", |
285 |
| - fontWeight: 600, |
286 |
| - lineHeight: "inherit", |
287 |
| - margin: 0, |
288 |
| - marginBottom: 4, |
289 |
| - }} |
290 |
| - > |
291 |
| - Select a region nearest to you |
292 |
| - </h4> |
293 |
| - |
294 |
| - <p |
295 |
| - css={{ |
296 |
| - fontSize: 13, |
297 |
| - color: theme.palette.text.secondary, |
298 |
| - lineHeight: "inherit", |
299 |
| - marginTop: 0.5, |
300 |
| - }} |
301 |
| - > |
302 |
| - Workspace proxies improve terminal and web app connections to |
303 |
| - workspaces. This does not apply to{" "} |
304 |
| - <Abbr title="Command-Line Interface" pronunciation="initialism"> |
305 |
| - CLI |
306 |
| - </Abbr>{" "} |
307 |
| - connections. A region must be manually selected, otherwise the |
308 |
| - default primary region will be used. |
309 |
| - </p> |
310 |
| - </div> |
311 |
| - |
312 |
| - <Divider css={{ borderColor: theme.palette.divider }} /> |
313 |
| - |
314 |
| - {proxyContextValue.proxies && |
315 |
| - [...proxyContextValue.proxies] |
316 |
| - .sort((a, b) => { |
317 |
| - const latencyA = latencies?.[a.id]?.latencyMS ?? Infinity; |
318 |
| - const latencyB = latencies?.[b.id]?.latencyMS ?? Infinity; |
319 |
| - return latencyA - latencyB; |
320 |
| - }) |
321 |
| - .map((proxy) => ( |
322 |
| - <MenuItem |
323 |
| - key={proxy.id} |
324 |
| - selected={proxy.id === selectedProxy?.id} |
325 |
| - css={{ fontSize: 14 }} |
326 |
| - onClick={() => { |
327 |
| - if (!proxy.healthy) { |
328 |
| - displayError("Please select a healthy workspace proxy."); |
329 |
| - closeMenu(); |
330 |
| - return; |
331 |
| - } |
332 |
| - |
333 |
| - proxyContextValue.setProxy(proxy); |
334 |
| - closeMenu(); |
335 |
| - }} |
336 |
| - > |
337 |
| - <div |
338 |
| - css={{ |
339 |
| - display: "flex", |
340 |
| - gap: 24, |
341 |
| - alignItems: "center", |
342 |
| - width: "100%", |
343 |
| - }} |
344 |
| - > |
345 |
| - <div css={{ width: 14, height: 14, lineHeight: 0 }}> |
346 |
| - <img |
347 |
| - src={proxy.icon_url} |
348 |
| - alt="" |
349 |
| - css={{ |
350 |
| - objectFit: "contain", |
351 |
| - width: "100%", |
352 |
| - height: "100%", |
353 |
| - }} |
354 |
| - /> |
355 |
| - </div> |
356 |
| - |
357 |
| - {proxy.display_name} |
358 |
| - |
359 |
| - <Latency |
360 |
| - latency={latencies?.[proxy.id]?.latencyMS} |
361 |
| - isLoading={proxyLatencyLoading(proxy)} |
362 |
| - /> |
363 |
| - </div> |
364 |
| - </MenuItem> |
365 |
| - ))} |
366 |
| - |
367 |
| - <Divider css={{ borderColor: theme.palette.divider }} /> |
368 |
| - |
369 |
| - {Boolean(permissions.editWorkspaceProxies) && ( |
370 |
| - <MenuItem |
371 |
| - css={{ fontSize: 14 }} |
372 |
| - onClick={() => { |
373 |
| - navigate("/deployment/workspace-proxies"); |
374 |
| - }} |
375 |
| - > |
376 |
| - Proxy settings |
377 |
| - </MenuItem> |
378 |
| - )} |
379 |
| - |
380 |
| - <MenuItem |
381 |
| - css={{ fontSize: 14 }} |
382 |
| - onClick={(e) => { |
383 |
| - // Stop the menu from closing |
384 |
| - e.stopPropagation(); |
385 |
| - // Refresh the latencies. |
386 |
| - const refetchDate = refreshLatencies(); |
387 |
| - setRefetchDate(refetchDate); |
388 |
| - }} |
389 |
| - > |
390 |
| - Refresh Latencies |
391 |
| - </MenuItem> |
392 |
| - </Menu> |
393 |
| - </> |
394 |
| - ); |
395 |
| -}; |
396 |
| - |
397 | 156 | const styles = {
|
398 | 157 | desktopNavItems: (theme) => css`
|
399 | 158 | display: none;
|
|
0 commit comments