addons/isl/src/TopLevelErrors.tsxblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import type {TrackErrorName} from 'isl-server/src/analytics/eventNames';
b69ab319import type {ReactNode} from 'react';
b69ab3110import type {MessageBusStatus} from './MessageBus';
b69ab3111import type {RepoInfo} from './types';
b69ab3112
b69ab3113import {Button} from 'isl-components/Button';
b69ab3114import {ErrorNotice} from 'isl-components/ErrorNotice';
b69ab3115import {useAtomValue} from 'jotai';
b69ab3116import {useThrottledEffect} from 'shared/hooks';
b69ab3117import {Internal} from './Internal';
b69ab3118import {tracker} from './analytics';
b69ab3119import {allDiffSummaries} from './codeReview/CodeReviewInfo';
b69ab3120import {t, T} from './i18n';
b69ab3121import platform from './platform';
b69ab3122import {reconnectingStatus, repositoryInfoOrError} from './serverAPIState';
b69ab3123
b69ab3124type TopLevelErrorInfo = {
b69ab3125 title: ReactNode;
b69ab3126 error: Error;
b69ab3127 buttons?: Array<ReactNode>;
b69ab3128 trackErrorName?: TrackErrorName;
b69ab3129};
b69ab3130
b69ab3131function computeTopLevelError(
b69ab3132 repoInfo: RepoInfo | undefined,
b69ab3133 reconnectStatus: MessageBusStatus,
b69ab3134 diffFetchError: Error | undefined,
b69ab3135): TopLevelErrorInfo | undefined {
b69ab3136 if (reconnectStatus.type === 'reconnecting') {
b69ab3137 return {
b69ab3138 title: <T>Connection to server was lost</T>,
b69ab3139 error: new Error(t('Attempting to reconnect...')),
b69ab3140 };
b69ab3141 } else if (reconnectStatus.type === 'error') {
b69ab3142 if (reconnectStatus.error === 'Invalid token') {
b69ab3143 return {
b69ab3144 title: (
b69ab3145 <T>
b69ab3146 Unable to connect to server. Try closing this window and accessing ISL with a fresh
b69ab3147 link.
b69ab3148 </T>
b69ab3149 ),
b69ab3150 error: new Error(
b69ab3151 t(
b69ab3152 'Invalid connection token. ' +
b69ab3153 'For security, you need to open a new ISL window when the server is restarted.',
b69ab3154 ),
b69ab3155 ),
b69ab3156 };
b69ab3157 }
b69ab3158 return {
b69ab3159 title: <T>Error connecting to server</T>,
b69ab3160 error: new Error(reconnectStatus.error),
b69ab3161 // no use tracking, since we can't reach the server anyway.
b69ab3162 };
b69ab3163 } else if (diffFetchError) {
b69ab3164 const internalResult = Internal.findInternalError?.(diffFetchError) as
b69ab3165 | TopLevelErrorInfo
b69ab3166 | undefined;
b69ab3167 if (internalResult != null) {
b69ab3168 return internalResult;
b69ab3169 } else if (repoInfo?.type === 'success' && repoInfo.codeReviewSystem.type === 'github') {
b69ab3170 const learnAboutGhButton = (
b69ab3171 <Button
b69ab3172 onClick={e => {
b69ab3173 platform.openExternalLink('https://sapling-scm.com/docs/git/intro');
b69ab3174 e.preventDefault();
b69ab3175 e.stopPropagation();
b69ab3176 }}>
b69ab3177 <T>Learn more</T>
b69ab3178 </Button>
b69ab3179 );
b69ab3180 if (diffFetchError.message.startsWith('NotAuthenticatedError')) {
b69ab3181 const error = new Error(
b69ab3182 t('Log in to gh CLI with `gh auth login` to allow requests to GitHub'),
b69ab3183 );
b69ab3184 error.stack = diffFetchError.stack;
b69ab3185 return {
b69ab3186 title: <T>Not Authenticated to GitHub with `gh` CLI</T>,
b69ab3187 error,
b69ab3188 buttons: [learnAboutGhButton],
b69ab3189 trackErrorName: 'GhCliNotAuthenticated',
b69ab3190 };
b69ab3191 } else if (diffFetchError.message.startsWith('GhNotInstalledError')) {
b69ab3192 const error = new Error(t('Install the `gh` CLI to make requests to GitHub'));
b69ab3193 error.stack = diffFetchError.stack;
b69ab3194 return {
b69ab3195 title: <T>Unable to fetch data from Github</T>,
b69ab3196 error,
b69ab3197 buttons: [learnAboutGhButton],
b69ab3198 trackErrorName: 'GhCliNotInstalled',
b69ab3199 };
b69ab31100 }
b69ab31101 }
b69ab31102 return {
b69ab31103 title: <T>Failed to fetch Diffs</T>,
b69ab31104 error: diffFetchError,
b69ab31105 trackErrorName: 'DiffFetchFailed',
b69ab31106 };
b69ab31107 }
b69ab31108
b69ab31109 return undefined;
b69ab31110}
b69ab31111
b69ab31112export function TopLevelErrors() {
b69ab31113 const reconnectStatus = useAtomValue(reconnectingStatus);
b69ab31114 const repoInfo = useAtomValue(repositoryInfoOrError);
b69ab31115 const diffFetchError = useAtomValue(allDiffSummaries).error;
b69ab31116
b69ab31117 const info = computeTopLevelError(repoInfo, reconnectStatus, diffFetchError);
b69ab31118
b69ab31119 if (info == null) {
b69ab31120 return null;
b69ab31121 }
b69ab31122
b69ab31123 return <TrackedError info={info} />;
b69ab31124}
b69ab31125
b69ab31126function TrackedError({info}: {info: TopLevelErrorInfo}) {
b69ab31127 useThrottledEffect(
b69ab31128 () => {
b69ab31129 if (info.trackErrorName != null) {
b69ab31130 tracker.error('TopLevelErrorShown', info.trackErrorName, info.error);
b69ab31131 }
b69ab31132 },
b69ab31133 1_000,
b69ab31134 [info.trackErrorName, info.error],
b69ab31135 );
b69ab31136 return <ErrorNotice title={info.title} error={info.error} buttons={info.buttons} />;
b69ab31137}