Basic Doctor UI

Summary:
- Basic Doctor UI showing issues with installation
- Run healthchecks in background on startup and show warning message if something is wrong

Reviewed By: jknoxville

Differential Revision: D18502599

fbshipit-source-id: 194939a080ba7412ed3293d95c533bfad7031d3b
This commit is contained in:
Anton Nikolaev
2019-11-21 02:51:35 -08:00
committed by Facebook Github Bot
parent c1de6f4276
commit ddb135ac39
16 changed files with 823 additions and 37 deletions

View File

@@ -7,10 +7,29 @@
* @format
*/
import {runHealthchecks} from './index';
import {getHealthchecks} from './index';
import {getEnvInfo} from './environmentInfo';
(async () => {
const results = await runHealthchecks();
const environmentInfo = await getEnvInfo();
console.log(JSON.stringify(environmentInfo));
const healthchecks = getHealthchecks();
const results = await Promise.all(
Object.entries(healthchecks).map(async ([key, category]) => [
key,
category
? {
label: category.label,
results: await Promise.all(
category.healthchecks.map(async ({label, run}) => ({
label,
result: await run(environmentInfo),
})),
),
}
: {},
]),
);
console.log(JSON.stringify(results, null, 2));
})();

View File

@@ -7,10 +7,10 @@
* @format
*/
import {getEnvInfo, EnvironmentInfo} from './environmentInfo';
export {getEnvInfo} from './environmentInfo';
import {exec} from 'child_process';
import {promisify} from 'util';
import {EnvironmentInfo, getEnvInfo} from './environmentInfo';
export {getEnvInfo} from './environmentInfo';
type HealthcheckCategory = {
label: string;
@@ -31,6 +31,7 @@ type Healthcheck = {
env: EnvironmentInfo,
) => Promise<{
hasProblem: boolean;
helpUrl?: string;
}>;
};
@@ -54,7 +55,6 @@ export function getHealthchecks(): Healthchecks {
healthchecks: [
{
label: 'OpenSSL Installed',
isRequired: true,
run: async (_: EnvironmentInfo) => {
const isAvailable = await commandSucceeds('openssl version');
return {
@@ -75,6 +75,12 @@ export function getHealthchecks(): Healthchecks {
hasProblem: e.SDKs['Android SDK'] === 'Not Found',
}),
},
{
label: 'ANDROID_HOME set',
run: async (e: EnvironmentInfo) => ({
hasProblem: !!process.env.ANDROID_HOME,
}),
},
],
},
...(process.platform === 'darwin'
@@ -127,7 +133,8 @@ export function getHealthchecks(): Healthchecks {
export async function runHealthchecks(): Promise<Array<CategoryResult>> {
const environmentInfo = await getEnvInfo();
const healthchecks: Healthchecks = getHealthchecks();
const results: Array<CategoryResult> = (await Promise.all(
const results: Array<CategoryResult> = (
await Promise.all(
Object.entries(healthchecks).map(async ([key, category]) => {
if (!category) {
return null;
@@ -153,7 +160,8 @@ export async function runHealthchecks(): Promise<Array<CategoryResult>> {
];
return categoryResult;
}),
)).filter(notNull);
)
).filter(notNull);
return results;
}

View File

@@ -133,6 +133,7 @@
"emotion": "^9.2.6",
"expand-tilde": "^2.0.2",
"express": "^4.15.2",
"flipper-doctor": "^0.2.0",
"fs-extra": "^8.0.1",
"immutable": "^4.0.0-rc.12",
"invariant": "^2.2.2",

View File

@@ -14,6 +14,7 @@ import TitleBar from './chrome/TitleBar';
import MainSidebar from './chrome/MainSidebar';
import BugReporterDialog from './chrome/BugReporterDialog';
import ErrorBar from './chrome/ErrorBar';
import DoctorBar from './chrome/DoctorBar';
import ShareSheetExportUrl from './chrome/ShareSheetExportUrl';
import SignInSheet from './chrome/SignInSheet';
import ExportDataPluginSheet from './chrome/ExportDataPluginSheet';
@@ -29,6 +30,7 @@ import {
ACTIVE_SHEET_SHARE_DATA,
ACTIVE_SHEET_SIGN_IN,
ACTIVE_SHEET_SETTINGS,
ACTIVE_SHEET_DOCTOR,
ACTIVE_SHEET_SHARE_DATA_IN_FILE,
ACTIVE_SHEET_SELECT_PLUGINS_TO_EXPORT,
ACTIVE_SHEET_PLUGIN_SHEET,
@@ -40,6 +42,7 @@ import {StaticView, FlipperError} from './reducers/connections';
import PluginManager from './chrome/PluginManager';
import StatusBar from './chrome/StatusBar';
import SettingsSheet from './chrome/SettingsSheet';
import DoctorSheet from './chrome/DoctorSheet';
const version = remote.app.getVersion();
type OwnProps = {
@@ -89,6 +92,8 @@ export class App extends React.Component<Props> {
return <SignInSheet onHide={onHide} />;
case ACTIVE_SHEET_SETTINGS:
return <SettingsSheet onHide={onHide} />;
case ACTIVE_SHEET_DOCTOR:
return <DoctorSheet onHide={onHide} />;
case ACTIVE_SHEET_SELECT_PLUGINS_TO_EXPORT:
return <ExportDataPluginSheet onHide={onHide} />;
case ACTIVE_SHEET_SHARE_DATA:
@@ -126,6 +131,7 @@ export class App extends React.Component<Props> {
return (
<FlexColumn grow={true}>
<TitleBar version={version} />
<DoctorBar />
<ErrorBar />
<Sheet>{this.getSheet}</Sheet>
<FlexRow grow={true}>

142
src/chrome/DoctorBar.tsx Normal file
View File

@@ -0,0 +1,142 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {styled, colors} from 'flipper';
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {
setActiveSheet,
ActiveSheet,
ACTIVE_SHEET_DOCTOR,
} from '../reducers/application';
import {State as Store} from '../reducers/index';
import {ButtonGroup, Button} from 'flipper';
import {FlexColumn, FlexRow} from 'flipper';
import runHealthchecks from '../utils/runHealthchecks';
import {
initHealthcheckReport,
updateHealthcheckReportItem,
startHealthchecks,
finishHealthchecks,
HealthcheckReport,
HealthcheckReportItem,
} from '../reducers/healthchecks';
type StateFromProps = {};
type DispatchFromProps = {
setActiveSheet: (payload: ActiveSheet) => void;
initHealthcheckReport: (report: HealthcheckReport) => void;
updateHealthcheckReportItem: (
categoryIdx: number,
itemIdx: number,
item: HealthcheckReportItem,
) => void;
startHealthchecks: () => void;
finishHealthchecks: () => void;
};
type State = {
visible: boolean;
};
type Props = DispatchFromProps & StateFromProps;
class DoctorBar extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
visible: false,
};
}
componentDidMount() {
this.showMessageIfChecksFailed();
}
async showMessageIfChecksFailed() {
const result = await runHealthchecks({
initHealthcheckReport: this.props.initHealthcheckReport,
updateHealthcheckReportItem: this.props.updateHealthcheckReportItem,
startHealthchecks: this.props.startHealthchecks,
finishHealthchecks: this.props.finishHealthchecks,
});
if (!result) {
this.setVisible(true);
}
}
render() {
return (
this.state.visible && (
<Container>
<WarningContainer>
<FlexRow style={{flexDirection: 'row-reverse'}}>
<ButtonSection>
<ButtonGroup>
<Button
onClick={() =>
this.props.setActiveSheet(ACTIVE_SHEET_DOCTOR)
}>
Show Problems
</Button>
<Button onClick={() => this.setVisible(false)}>
Dismiss
</Button>
</ButtonGroup>
</ButtonSection>
<FlexColumn style={{flexGrow: 1}}>
Doctor has discovered problems with your installation
</FlexColumn>
</FlexRow>
</WarningContainer>
</Container>
)
);
}
setVisible(visible: boolean) {
this.setState(prevState => {
return {
...prevState,
visible,
};
});
}
}
export default connect<StateFromProps, DispatchFromProps, {}, Store>(null, {
setActiveSheet,
initHealthcheckReport,
updateHealthcheckReportItem,
startHealthchecks,
finishHealthchecks,
})(DoctorBar);
const Container = styled('div')({
boxShadow: '2px 2px 2px #ccc',
userSelect: 'text',
});
const WarningContainer = styled('div')({
backgroundColor: colors.orange,
color: '#fff',
maxHeight: '600px',
overflowY: 'auto',
overflowX: 'hidden',
transition: 'max-height 0.3s ease',
'&.collapsed': {
maxHeight: '0px',
},
padding: '4px 12px',
borderBottom: '1px solid ' + colors.orangeDark3,
verticalAlign: 'middle',
lineHeight: '28px',
});
const ButtonSection = styled(FlexColumn)({
marginLeft: '8px',
flexShrink: 0,
flexGrow: 0,
});

307
src/chrome/DoctorSheet.tsx Normal file
View File

@@ -0,0 +1,307 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {
FlexColumn,
styled,
Text,
FlexRow,
Glyph,
LoadingIndicator,
colors,
Spacer,
Button,
FlexBox,
} from 'flipper';
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {State as Store} from '../reducers';
import {
HealthcheckResult,
HealthcheckReportCategory,
HealthcheckReportItem,
HealthcheckReport,
initHealthcheckReport,
updateHealthcheckReportItem,
startHealthchecks,
finishHealthchecks,
} from '../reducers/healthchecks';
import runHealthchecks from '../utils/runHealthchecks';
import {shell} from 'electron';
type StateFromProps = {
report: HealthcheckReport;
};
type DispatchFromProps = {
initHealthcheckReport: (report: HealthcheckReport) => void;
updateHealthcheckReportItem: (
categoryIdx: number,
itemIdx: number,
item: HealthcheckReportItem,
) => void;
startHealthchecks: () => void;
finishHealthchecks: () => void;
};
const Container = styled(FlexColumn)({
padding: 20,
width: 600,
});
const HealthcheckDisplayContainer = styled(FlexRow)({
alignItems: 'center',
marginBottom: 5,
});
const HealthcheckListContainer = styled(FlexColumn)({
marginBottom: 20,
});
const Title = styled(Text)({
marginBottom: 18,
marginRight: 10,
fontWeight: 100,
fontSize: '40px',
});
const CategoryContainer = styled(FlexColumn)({
marginBottom: 5,
marginLeft: 20,
marginRight: 20,
});
const SideContainer = styled(FlexBox)({
marginBottom: 20,
padding: 20,
backgroundColor: colors.highlightBackground,
border: '1px solid #b3b3b3',
width: 320,
});
const SideContainerText = styled(Text)({
display: 'block',
'word-wrap': 'break-word',
});
const HealthcheckLabel = styled(Text)({
paddingLeft: 5,
});
type OwnProps = {
onHide: () => void;
};
function HealthcheckIcon(props: {check: HealthcheckResult}) {
switch (props.check.status) {
case 'IN_PROGRESS':
return <LoadingIndicator size={16} title={props.check.message} />;
case 'SUCCESS':
return (
<Glyph
size={16}
name={'checkmark'}
color={colors.green}
title={props.check.message}
/>
);
case 'WARNING':
return (
<Glyph
size={16}
name={'caution'}
color={colors.yellow}
title={props.check.message}
/>
);
default:
return (
<Glyph
size={16}
name={'cross'}
color={colors.red}
title={props.check.message}
/>
);
}
}
function HealthcheckDisplay(props: {
category: HealthcheckReportCategory;
check: HealthcheckReportItem;
onClick?: () => void;
}) {
return (
<FlexColumn shrink>
<HealthcheckDisplayContainer shrink title={props.check.message}>
<HealthcheckIcon check={props.check} />
<HealthcheckLabel
underline={!!props.onClick}
cursor={props.onClick && 'pointer'}
onClick={props.onClick}>
{props.check.label}
</HealthcheckLabel>
</HealthcheckDisplayContainer>
</FlexColumn>
);
}
function SideMessageDisplay(props: {
isHealthcheckInProgress: boolean;
hasProblems: boolean;
}) {
if (props.isHealthcheckInProgress) {
return (
<SideContainerText selectable>
Doctor is running healthchecks...
</SideContainerText>
);
} else if (props.hasProblems) {
return (
<SideContainerText selectable>
Doctor has discovered problems with your installation.
</SideContainerText>
);
} else {
return (
<SideContainerText selectable>
All good! Doctor has not discovered any issues with your installation.
</SideContainerText>
);
}
}
export type State = {};
type Props = OwnProps & StateFromProps & DispatchFromProps;
class DoctorSheet extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {};
}
openHelpUrl(check: HealthcheckReportItem): void {
check.helpUrl && shell.openExternal(check.helpUrl);
}
async runHealthchecks() {
this.setState(prevState => {
return {
...prevState,
};
});
await runHealthchecks({
initHealthcheckReport: this.props.initHealthcheckReport,
updateHealthcheckReportItem: this.props.updateHealthcheckReportItem,
startHealthchecks: this.props.startHealthchecks,
finishHealthchecks: this.props.finishHealthchecks,
});
}
hasProblems() {
return this.props.report.categories.some(cat =>
cat.checks.some(chk => chk.status != 'SUCCESS'),
);
}
getHealthcheckCategoryReportItem(
state: HealthcheckReportCategory,
): HealthcheckReportItem {
return {
label: state.label,
...(state.checks.some(c => c.status === 'IN_PROGRESS')
? {status: 'IN_PROGRESS'}
: state.checks.every(c => c.status === 'SUCCESS')
? {status: 'SUCCESS'}
: state.checks.some(c => c.status === 'FAILED')
? {
status: 'FAILED',
message: 'Doctor discovered problems with the current installation',
}
: {
status: 'WARNING',
message:
'Doctor discovered non-blocking problems with the current installation',
}),
};
}
render() {
return (
<Container>
<Title>Doctor</Title>
<FlexRow>
<HealthcheckListContainer>
{Object.values(this.props.report.categories).map(
(category, categoryIdx) => {
return (
<CategoryContainer key={categoryIdx}>
<HealthcheckDisplay
check={this.getHealthcheckCategoryReportItem(category)}
category={category}
/>
<CategoryContainer>
{category.checks.map((check, checkIdx) => (
<HealthcheckDisplay
key={checkIdx}
category={category}
check={check}
onClick={
check.helpUrl
? () => this.openHelpUrl(check)
: undefined
}
/>
))}
</CategoryContainer>
</CategoryContainer>
);
},
)}
</HealthcheckListContainer>
<Spacer />
<SideContainer shrink>
<SideMessageDisplay
isHealthcheckInProgress={
this.props.report.isHealthcheckInProgress
}
hasProblems={this.hasProblems()}
/>
</SideContainer>
</FlexRow>
<FlexRow>
<Spacer />
<Button compact padded onClick={this.props.onHide}>
Close
</Button>
<Button
disabled={this.props.report.isHealthcheckInProgress}
type="primary"
compact
padded
onClick={() => this.runHealthchecks()}>
Re-run
</Button>
</FlexRow>
</Container>
);
}
}
export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
({healthchecks: {healthcheckReport}}) => ({
report: healthcheckReport,
}),
{
initHealthcheckReport,
updateHealthcheckReportItem,
startHealthchecks,
finishHealthchecks,
},
)(DoctorSheet);

View File

@@ -16,6 +16,7 @@ import {
toggleRightSidebarVisible,
ACTIVE_SHEET_BUG_REPORTER,
ACTIVE_SHEET_SETTINGS,
ACTIVE_SHEET_DOCTOR,
} from '../reducers/application';
import {
colors,
@@ -169,6 +170,13 @@ class TitleBar extends React.Component<Props, StateFromProps> {
version={this.props.version}
/>
)}
<Button
icon="settings"
title="Settings"
compact={true}
onClick={() => this.props.setActiveSheet(ACTIVE_SHEET_SETTINGS)}
/>
{config.bugReportButtonVisible && (
<Button
compact={true}
@@ -178,10 +186,10 @@ class TitleBar extends React.Component<Props, StateFromProps> {
/>
)}
<Button
icon="settings"
title="Settings"
icon="first-aid"
title="Doctor"
compact={true}
onClick={() => this.props.setActiveSheet(ACTIVE_SHEET_SETTINGS)}
onClick={() => this.props.setActiveSheet(ACTIVE_SHEET_DOCTOR)}
/>
<ButtonGroup>
<Button

View File

@@ -12,7 +12,7 @@ exports[`ShareSheetPendingDialog is rendered with status update 1`] = `
size={30}
/>
<span
className="css-18qh9b2"
className="css-91luyc"
color="#6f6f6f"
>
Update
@@ -57,7 +57,7 @@ exports[`ShareSheetPendingDialog is rendered without status update 1`] = `
size={30}
/>
<span
className="css-18qh9b2"
className="css-91luyc"
color="#6f6f6f"
/>
</div>

View File

@@ -20,6 +20,7 @@ export const ACTIVE_SHEET_SELECT_PLUGINS_TO_EXPORT: 'SELECT_PLUGINS_TO_EXPORT' =
export const ACTIVE_SHEET_SHARE_DATA: 'SHARE_DATA' = 'SHARE_DATA';
export const ACTIVE_SHEET_SIGN_IN: 'SIGN_IN' = 'SIGN_IN';
export const ACTIVE_SHEET_SETTINGS: 'SETTINGS' = 'SETTINGS';
export const ACTIVE_SHEET_DOCTOR: 'DOCTOR' = 'DOCTOR';
export const ACTIVE_SHEET_SHARE_DATA_IN_FILE: 'SHARE_DATA_IN_FILE' =
'SHARE_DATA_IN_FILE';
export const SET_EXPORT_STATUS_MESSAGE: 'SET_EXPORT_STATUS_MESSAGE' =
@@ -33,6 +34,7 @@ export type ActiveSheet =
| typeof ACTIVE_SHEET_SHARE_DATA
| typeof ACTIVE_SHEET_SIGN_IN
| typeof ACTIVE_SHEET_SETTINGS
| typeof ACTIVE_SHEET_DOCTOR
| typeof ACTIVE_SHEET_SHARE_DATA_IN_FILE
| typeof ACTIVE_SHEET_SELECT_PLUGINS_TO_EXPORT
| null;

View File

@@ -0,0 +1,154 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {Actions} from './';
export type State = {
healthcheckReport: HealthcheckReport;
};
export type Action =
| {
type: 'INIT_HEALTHCHECK_REPORT';
payload: HealthcheckReport;
}
| {
type: 'UPDATE_HEALTHCHECK_REPORT_ITEM';
payload: {
categoryIdx: number;
itemIdx: number;
item: HealthcheckReportItem;
};
}
| {
type: 'START_HEALTHCHECKS';
}
| {
type: 'FINISH_HEALTHCHECKS';
};
const INITIAL_STATE: State = {
healthcheckReport: {
isHealthcheckInProgress: false,
categories: [],
},
};
export type HealthcheckStatus =
| 'IN_PROGRESS'
| 'SUCCESS'
| 'FAILED'
| 'WARNING';
export type HealthcheckResult = {
status: HealthcheckStatus;
message?: string;
helpUrl?: string;
};
export type HealthcheckReportItem = {
label: string;
} & HealthcheckResult;
export type HealthcheckReportCategory = {
label: string;
status: HealthcheckStatus;
checks: Array<HealthcheckReportItem>;
};
export type HealthcheckReport = {
isHealthcheckInProgress: boolean;
categories: Array<HealthcheckReportCategory>;
};
export default function reducer(
state: State | undefined = INITIAL_STATE,
action: Actions,
): State {
if (action.type === 'INIT_HEALTHCHECK_REPORT') {
return {
...state,
healthcheckReport: action.payload,
};
} else if (action.type === 'START_HEALTHCHECKS') {
return {
...state,
healthcheckReport: {
...state.healthcheckReport,
isHealthcheckInProgress: true,
},
};
} else if (action.type === 'FINISH_HEALTHCHECKS') {
return {
...state,
healthcheckReport: {
...state.healthcheckReport,
isHealthcheckInProgress: false,
},
};
} else if (action.type === 'UPDATE_HEALTHCHECK_REPORT_ITEM') {
return {
...state,
healthcheckReport: {
...state.healthcheckReport,
categories: [
...state.healthcheckReport.categories.slice(
0,
action.payload.categoryIdx,
),
{
...state.healthcheckReport.categories[action.payload.categoryIdx],
checks: [
...state.healthcheckReport.categories[
action.payload.categoryIdx
].checks.slice(0, action.payload.itemIdx),
{
...action.payload.item,
},
...state.healthcheckReport.categories[
action.payload.categoryIdx
].checks.slice(action.payload.itemIdx + 1),
],
},
...state.healthcheckReport.categories.slice(
action.payload.categoryIdx + 1,
),
],
},
};
} else {
return state;
}
}
export const initHealthcheckReport = (report: HealthcheckReport): Action => ({
type: 'INIT_HEALTHCHECK_REPORT',
payload: report,
});
export const updateHealthcheckReportItem = (
categoryIdx: number,
itemIdx: number,
item: HealthcheckReportItem,
): Action => ({
type: 'UPDATE_HEALTHCHECK_REPORT_ITEM',
payload: {
categoryIdx,
itemIdx,
item,
},
});
export const startHealthchecks = (): Action => ({
type: 'START_HEALTHCHECKS',
});
export const finishHealthchecks = (): Action => ({
type: 'FINISH_HEALTHCHECKS',
});

View File

@@ -40,6 +40,10 @@ import pluginManager, {
State as PluginManagerState,
Action as PluginManagerAction,
} from './pluginManager';
import healthchecks, {
Action as HealthcheckAction,
State as HealthcheckState,
} from './healthchecks';
import user, {State as UserState, Action as UserAction} from './user';
import JsonFileStorage from '../utils/jsonFileReduxPersistStorage';
import os from 'os';
@@ -61,6 +65,7 @@ export type Actions =
| SettingsAction
| SupportFormAction
| PluginManagerAction
| HealthcheckAction
| {type: 'INIT'};
export type State = {
@@ -73,6 +78,7 @@ export type State = {
settingsState: SettingsState & PersistPartial;
supportForm: SupportFormState;
pluginManager: PluginManagerState;
healthchecks: HealthcheckState;
};
export type Store = ReduxStore<State, Actions>;
@@ -124,4 +130,5 @@ export default combineReducers<State, Actions>({
{key: 'settings', storage: settingsStorage},
settings,
),
healthchecks,
});

View File

@@ -93,6 +93,7 @@ export default class Glyph extends React.PureComponent<{
className?: string;
color?: string;
style?: React.CSSProperties;
title?: string;
}> {
render() {
const {name, size = 16, variant, color, className, style} = this.props;

View File

@@ -15,6 +15,7 @@ import {
FontFamilyProperty,
WhiteSpaceProperty,
WordWrapProperty,
CursorProperty,
} from 'csstype';
/**
@@ -25,6 +26,7 @@ const Text = styled('span')(
color?: ColorProperty;
bold?: boolean;
italic?: boolean;
underline?: boolean;
align?: TextAlignProperty;
size?: FontSizeProperty<number>;
code?: boolean;
@@ -32,13 +34,16 @@ const Text = styled('span')(
selectable?: boolean;
wordWrap?: WordWrapProperty;
whiteSpace?: WhiteSpaceProperty;
cursor?: CursorProperty;
}) => ({
color: props.color ? props.color : 'inherit',
cursor: props.cursor ? props.cursor : 'auto',
display: 'inline',
fontWeight: props.bold ? 'bold' : 'inherit',
fontStyle: props.italic ? 'italic' : 'normal',
textAlign: props.align || 'left',
fontSize: props.size == null && props.code ? 12 : props.size,
textDecoration: props.underline ? 'underline' : 'initial',
fontFamily: props.code
? 'SF Mono, Monaco, Andale Mono, monospace'
: props.family,

View File

@@ -45,6 +45,9 @@ const ICONS = {
bug: [12],
camcorder: [12],
camera: [12],
caution: [16],
cross: [16],
checkmark: [16],
desktop: [12],
directions: [12],
internet: [12],

View File

@@ -0,0 +1,111 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {
HealthcheckResult,
HealthcheckReport,
HealthcheckReportItem,
HealthcheckReportCategory,
} from '../reducers/healthchecks';
import {getHealthchecks, getEnvInfo} from 'flipper-doctor';
let healthcheckIsRunning: boolean;
let runningHealthcheck: Promise<boolean>;
export type HealthcheckEventsHandler = {
initHealthcheckReport: (report: HealthcheckReport) => void;
updateHealthcheckReportItem: (
categoryIdx: number,
itemIdx: number,
item: HealthcheckReportItem,
) => void;
startHealthchecks: () => void;
finishHealthchecks: () => void;
};
async function launchHealthchecks(
dispatch: HealthcheckEventsHandler,
): Promise<boolean> {
let hasProblems: boolean = true;
dispatch.startHealthchecks();
try {
const initialState: HealthcheckResult = {
status: 'IN_PROGRESS',
message: 'The healthcheck is in progress',
};
const hcState: HealthcheckReport = {
isHealthcheckInProgress: true,
categories: Object.values(getHealthchecks())
.map(category => {
if (!category) {
return null;
}
return {
...initialState,
label: category.label,
checks: category.healthchecks.map(x => ({
...initialState,
label: x.label,
})),
};
})
.filter(x => !!x)
.map(x => x as HealthcheckReportCategory),
};
dispatch.initHealthcheckReport(hcState);
const environmentInfo = await getEnvInfo();
const categories = Object.values(getHealthchecks());
for (let cIdx = 0; cIdx < categories.length; cIdx++) {
const c = categories[cIdx];
if (!c) {
continue;
}
for (let hIdx = 0; hIdx < c.healthchecks.length; hIdx++) {
const h = c.healthchecks[hIdx];
const result = await h.run(environmentInfo);
if (result.hasProblem) {
hasProblems = false;
}
dispatch.updateHealthcheckReportItem(cIdx, hIdx, {
...h,
...(result.hasProblem && h.isRequired
? {
status: 'FAILED',
message: 'The healthcheck failed',
helpUrl: result.helpUrl,
}
: result.hasProblem && !h.isRequired
? {
status: 'WARNING',
message: 'Doctor discovered a problem during the healthcech',
helpUrl: result.helpUrl,
}
: {
status: 'SUCCESS',
message: 'The healthcheck completed succesfully',
}),
});
}
}
} catch {
} finally {
dispatch.finishHealthchecks();
}
return hasProblems;
}
export default async function runHealthchecks(
dispatch: HealthcheckEventsHandler,
): Promise<boolean> {
if (healthcheckIsRunning) {
return runningHealthcheck;
}
runningHealthcheck = launchHealthchecks(dispatch);
return runningHealthcheck;
}

View File

@@ -3444,6 +3444,11 @@ envify@^4.0.0:
esprima "^4.0.0"
through "~2.3.4"
envinfo@^7.4.0:
version "7.4.0"
resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.4.0.tgz#bef4ece9e717423aaf0c3584651430b735ad6630"
integrity sha512-FdDfnWnCVjxTTpWE3d6Jgh5JDIA3Cw7LCgpM/pI7kK1ORkjaqI2r6NqQ+ln2j0dfpgxY00AWieSvtkiZQKIItA==
err-code@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/err-code/-/err-code-1.1.2.tgz#06e0116d3028f6aef4806849eb0ea6a748ae6960"
@@ -4115,6 +4120,13 @@ flatted@^2.0.0:
resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.1.tgz#69e57caa8f0eacbc281d2e2cb458d46fdb449e08"
integrity sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==
flipper-doctor@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/flipper-doctor/-/flipper-doctor-0.2.0.tgz#b0b1b51f3e6b99cf82eefe68098b74f75455e9d4"
integrity sha512-OQ+L4bm6OEcVGmmeQVwWJcPx6y6letE9BV+vkT2pS49jk2q1q++aJ+Z+aH7P0f/VfXpGYc3JaNUhMFie0A6LVQ==
dependencies:
envinfo "^7.4.0"
flow-bin@0.112.0:
version "0.112.0"
resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.112.0.tgz#6a21c31937c4a2f23a750056a364c598a95ea216"