8.4 KB285 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 {AppMode, RepositoryError} from './types';
9
10import {Button} from 'isl-components/Button';
11import {ErrorBoundary, ErrorNotice} from 'isl-components/ErrorNotice';
12import {Icon} from 'isl-components/Icon';
13import {atom, useAtomValue, useSetAtom} from 'jotai';
14import {Suspense, useEffect, useMemo} from 'react';
15import {useThrottledEffect} from 'shared/hooks';
16import {AllProviders} from './AppWrapper';
17import {CommandHistoryAndProgress} from './CommandHistoryAndProgress';
18import {CommitInfoSidebar} from './CommitInfoView/CommitInfoView';
19import {CommitTreeList} from './CommitTreeList';
20import {ComparisonViewApp, ComparisonViewModal} from './ComparisonView/ComparisonViewModal';
21import {availableCwds, CwdSelections} from './CwdSelector';
22import {Drawers} from './Drawers';
23import {EmptyState} from './EmptyState';
24import {useCommand} from './ISLShortcuts';
25import {Internal} from './Internal';
26import {TopBar} from './TopBar';
27import {TopLevelAlerts} from './TopLevelAlert';
28import {CreateGroveRepoBanner} from './codeReview/grove/CreateGroveRepoBanner';
29import {TopLevelErrors} from './TopLevelErrors';
30import {tracker} from './analytics';
31import {islDrawerState} from './drawerState';
32import {t, T} from './i18n';
33import platform from './platform';
34import {useMainContentWidth} from './responsive';
35import {repositoryInfoOrError} from './serverAPIState';
36
37import clientToServerAPI from './ClientToServerAPI';
38import './index.css';
39import './stackEdit/ui/AISplitMessageHandlers';
40
41declare global {
42 interface Window {
43 /**
44 * AppMode that determines what feature the App is rendering.
45 * This is set at creation time (e.g. in HTML), and is not dynamic.
46 */
47 islAppMode?: AppMode;
48 }
49}
50let hasInitialized = false;
51export default function App() {
52 const mode = window.islAppMode ?? {mode: 'isl'};
53
54 useEffect(() => {
55 if (!hasInitialized) {
56 clientToServerAPI.postMessage({type: 'clientReady'});
57 hasInitialized = true;
58 }
59 }, []);
60
61 return (
62 <AllProviders>
63 {mode.mode === 'isl' ? (
64 <>
65 <NullStateOrDrawers />
66 <ComparisonViewModal />
67 </>
68 ) : (
69 <ComparisonApp />
70 )}
71 </AllProviders>
72 );
73}
74
75function ComparisonApp() {
76 return (
77 <Suspense fallback={<Icon icon="loading" />}>
78 <ComparisonViewApp />
79 </Suspense>
80 );
81}
82
83function NullStateOrDrawers() {
84 const repoInfo = useAtomValue(repositoryInfoOrError);
85 if (repoInfo != null && repoInfo.type !== 'success') {
86 return <ISLNullState repoError={repoInfo} />;
87 }
88 return <ISLDrawers />;
89}
90
91function ISLDrawers() {
92 const setDrawerState = useSetAtom(islDrawerState);
93 useCommand('ToggleSidebar', () => {
94 setDrawerState(state => ({
95 ...state,
96 right: {...state.right, collapsed: !state.right.collapsed},
97 }));
98 });
99
100 return (
101 <Drawers
102 rightLabel={
103 <>
104 <Icon icon="edit" />
105 <T>Commit Info</T>
106 </>
107 }
108 right={<CommitInfoSidebar />}
109 errorBoundary={ErrorBoundary}>
110 <MainContent />
111 <CommandHistoryAndProgress />
112 </Drawers>
113 );
114}
115
116function MainContent() {
117 const ref = useMainContentWidth();
118 return (
119 <div className="main-content-area" ref={ref}>
120 <TopBar />
121 <TopLevelErrors />
122 <TopLevelAlerts />
123 <CreateGroveRepoBanner />
124 <CommitTreeList />
125 </div>
126 );
127}
128
129function ISLNullState({repoError}: {repoError: RepositoryError}) {
130 const emptyCwds = useAtomValue(useMemo(() => atom(get => get(availableCwds).length === 0), []));
131 useThrottledEffect(
132 () => {
133 if (repoError != null) {
134 switch (repoError.type) {
135 case 'cwdNotARepository':
136 tracker.track('UIEmptyState', {extras: {cwd: repoError.cwd}, errorName: 'InvalidCwd'});
137 break;
138 case 'edenFsUnhealthy':
139 tracker.track('UIEmptyState', {
140 extras: {cwd: repoError.cwd},
141 errorName: 'EdenFsUnhealthy',
142 });
143 break;
144 case 'invalidCommand':
145 tracker.track('UIEmptyState', {
146 extras: {command: repoError.command},
147 errorName: 'InvalidCommand',
148 });
149 break;
150 case 'unknownError':
151 tracker.error('UIEmptyState', 'RepositoryError', repoError.error);
152 break;
153 }
154 }
155 },
156 1_000,
157 [repoError],
158 );
159 let content;
160 if (repoError != null) {
161 if (repoError.type === 'cwdNotARepository') {
162 if (platform.platformName === 'vscode' && emptyCwds) {
163 content = (
164 <>
165 <EmptyState>
166 <div>
167 <T>No folder opened</T>
168 </div>
169 <p>
170 <T>Open a folder to get started.</T>
171 </p>
172 </EmptyState>
173 </>
174 );
175 } else {
176 content = (
177 <>
178 <EmptyState>
179 <div>
180 <T>Not a valid repository</T>
181 </div>
182 <p>
183 <T replace={{$cwd: <code>{repoError.cwd}</code>}}>
184 $cwd is not a valid Sapling repository. Clone or init a repository to use ISL.
185 </T>
186 </p>
187 </EmptyState>
188 <CwdSelections dismiss={() => null} />
189 </>
190 );
191 }
192 } else if (repoError.type === 'cwdDoesNotExist') {
193 content = (
194 <>
195 {Internal.InternalInstallationDocs ? (
196 <Internal.InternalInstallationDocs repoError={repoError} />
197 ) : (
198 <ErrorNotice
199 title={
200 <T replace={{$cwd: repoError.cwd}}>
201 cwd $cwd does not exist. Make sure the folder exists.
202 </T>
203 }
204 error={
205 new Error(
206 t('$cwd not found', {
207 replace: {$cwd: repoError.cwd},
208 }),
209 )
210 }
211 buttons={[
212 <Button
213 key="help-button"
214 onClick={e => {
215 platform.openExternalLink(
216 'https://sapling-scm.com/docs/introduction/installation',
217 );
218 e.preventDefault();
219 e.stopPropagation();
220 }}>
221 <T>See installation docs</T>
222 </Button>,
223 ]}
224 />
225 )}
226 <CwdSelections dismiss={() => null} />
227 </>
228 );
229 } else if (repoError.type === 'edenFsUnhealthy') {
230 content = (
231 <>
232 <ErrorNotice
233 title={<T replace={{$cwd: repoError.cwd}}>EdenFS is not running properly in $cwd</T>}
234 description={
235 <T replace={{$edenDoctor: <code>eden doctor</code>}}>
236 Try running $edenDoctor and reloading the ISL window
237 </T>
238 }
239 error={
240 new Error(
241 t('README_EDEN.txt found in $cwd', {
242 replace: {$cwd: repoError.cwd},
243 }),
244 )
245 }
246 />
247 <CwdSelections dismiss={() => null} />
248 </>
249 );
250 } else if (repoError.type === 'invalidCommand') {
251 if (Internal.InvalidSlCommand) {
252 content = <Internal.InvalidSlCommand repoError={repoError} />;
253 } else {
254 content = (
255 <ErrorNotice
256 startExpanded
257 title={<T>Invalid Sapling command. Is Sapling installed correctly?</T>}
258 description={
259 <T replace={{$cmd: repoError.command}}>Command "$cmd" was not found in PATH</T>
260 }
261 details={<T replace={{$path: repoError.path ?? '(no path found)'}}>PATH: $path'</T>}
262 buttons={[
263 <Button
264 key="help-button"
265 onClick={e => {
266 platform.openExternalLink(
267 'https://sapling-scm.com/docs/introduction/installation',
268 );
269 e.preventDefault();
270 e.stopPropagation();
271 }}>
272 <T>See installation docs</T>
273 </Button>,
274 ]}
275 />
276 );
277 }
278 } else {
279 content = <ErrorNotice title={<T>Something went wrong</T>} error={repoError.error} />;
280 }
281 }
282
283 return <div className="empty-app-state">{content}</div>;
284}
285