4.3 KB138 lines
Blame
1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8import type {TrackErrorName} from 'isl-server/src/analytics/eventNames';
9import type {ReactNode} from 'react';
10import type {MessageBusStatus} from './MessageBus';
11import type {RepoInfo} from './types';
12
13import {Button} from 'isl-components/Button';
14import {ErrorNotice} from 'isl-components/ErrorNotice';
15import {useAtomValue} from 'jotai';
16import {useThrottledEffect} from 'shared/hooks';
17import {Internal} from './Internal';
18import {tracker} from './analytics';
19import {allDiffSummaries} from './codeReview/CodeReviewInfo';
20import {t, T} from './i18n';
21import platform from './platform';
22import {reconnectingStatus, repositoryInfoOrError} from './serverAPIState';
23
24type TopLevelErrorInfo = {
25 title: ReactNode;
26 error: Error;
27 buttons?: Array<ReactNode>;
28 trackErrorName?: TrackErrorName;
29};
30
31function computeTopLevelError(
32 repoInfo: RepoInfo | undefined,
33 reconnectStatus: MessageBusStatus,
34 diffFetchError: Error | undefined,
35): TopLevelErrorInfo | undefined {
36 if (reconnectStatus.type === 'reconnecting') {
37 return {
38 title: <T>Connection to server was lost</T>,
39 error: new Error(t('Attempting to reconnect...')),
40 };
41 } else if (reconnectStatus.type === 'error') {
42 if (reconnectStatus.error === 'Invalid token') {
43 return {
44 title: (
45 <T>
46 Unable to connect to server. Try closing this window and accessing ISL with a fresh
47 link.
48 </T>
49 ),
50 error: new Error(
51 t(
52 'Invalid connection token. ' +
53 'For security, you need to open a new ISL window when the server is restarted.',
54 ),
55 ),
56 };
57 }
58 return {
59 title: <T>Error connecting to server</T>,
60 error: new Error(reconnectStatus.error),
61 // no use tracking, since we can't reach the server anyway.
62 };
63 } else if (diffFetchError) {
64 const internalResult = Internal.findInternalError?.(diffFetchError) as
65 | TopLevelErrorInfo
66 | undefined;
67 if (internalResult != null) {
68 return internalResult;
69 } else if (repoInfo?.type === 'success' && repoInfo.codeReviewSystem.type === 'github') {
70 const learnAboutGhButton = (
71 <Button
72 onClick={e => {
73 platform.openExternalLink('https://sapling-scm.com/docs/git/intro');
74 e.preventDefault();
75 e.stopPropagation();
76 }}>
77 <T>Learn more</T>
78 </Button>
79 );
80 if (diffFetchError.message.startsWith('NotAuthenticatedError')) {
81 const error = new Error(
82 t('Log in to gh CLI with `gh auth login` to allow requests to GitHub'),
83 );
84 error.stack = diffFetchError.stack;
85 return {
86 title: <T>Not Authenticated to GitHub with `gh` CLI</T>,
87 error,
88 buttons: [learnAboutGhButton],
89 trackErrorName: 'GhCliNotAuthenticated',
90 };
91 } else if (diffFetchError.message.startsWith('GhNotInstalledError')) {
92 const error = new Error(t('Install the `gh` CLI to make requests to GitHub'));
93 error.stack = diffFetchError.stack;
94 return {
95 title: <T>Unable to fetch data from Github</T>,
96 error,
97 buttons: [learnAboutGhButton],
98 trackErrorName: 'GhCliNotInstalled',
99 };
100 }
101 }
102 return {
103 title: <T>Failed to fetch Diffs</T>,
104 error: diffFetchError,
105 trackErrorName: 'DiffFetchFailed',
106 };
107 }
108
109 return undefined;
110}
111
112export function TopLevelErrors() {
113 const reconnectStatus = useAtomValue(reconnectingStatus);
114 const repoInfo = useAtomValue(repositoryInfoOrError);
115 const diffFetchError = useAtomValue(allDiffSummaries).error;
116
117 const info = computeTopLevelError(repoInfo, reconnectStatus, diffFetchError);
118
119 if (info == null) {
120 return null;
121 }
122
123 return <TrackedError info={info} />;
124}
125
126function TrackedError({info}: {info: TopLevelErrorInfo}) {
127 useThrottledEffect(
128 () => {
129 if (info.trackErrorName != null) {
130 tracker.error('TopLevelErrorShown', info.trackErrorName, info.error);
131 }
132 },
133 1_000,
134 [info.trackErrorName, info.error],
135 );
136 return <ErrorNotice title={info.title} error={info.error} buttons={info.buttons} />;
137}
138