diff --git a/doctor/src/cli.ts b/doctor/src/cli.ts index 457c353db..7257a111b 100644 --- a/doctor/src/cli.ts +++ b/doctor/src/cli.ts @@ -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)); })(); diff --git a/doctor/src/index.ts b/doctor/src/index.ts index ba6903bf2..a6e7cd627 100644 --- a/doctor/src/index.ts +++ b/doctor/src/index.ts @@ -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,33 +133,35 @@ export function getHealthchecks(): Healthchecks { export async function runHealthchecks(): Promise> { const environmentInfo = await getEnvInfo(); const healthchecks: Healthchecks = getHealthchecks(); - const results: Array = (await Promise.all( - Object.entries(healthchecks).map(async ([key, category]) => { - if (!category) { - return null; - } - const categoryResult: CategoryResult = [ - key, - { - label: category.label, - results: await Promise.all( - category.healthchecks.map(async ({label, run, isRequired}) => ({ - label, - isRequired: isRequired ?? true, - result: await run(environmentInfo).catch(e => { - console.error(e); - // TODO Improve result type to be: OK | Problem(message, fix...) - return { - hasProblem: true, - }; - }), - })), - ), - }, - ]; - return categoryResult; - }), - )).filter(notNull); + const results: Array = ( + await Promise.all( + Object.entries(healthchecks).map(async ([key, category]) => { + if (!category) { + return null; + } + const categoryResult: CategoryResult = [ + key, + { + label: category.label, + results: await Promise.all( + category.healthchecks.map(async ({label, run, isRequired}) => ({ + label, + isRequired: isRequired ?? true, + result: await run(environmentInfo).catch(e => { + console.error(e); + // TODO Improve result type to be: OK | Problem(message, fix...) + return { + hasProblem: true, + }; + }), + })), + ), + }, + ]; + return categoryResult; + }), + ) + ).filter(notNull); return results; } diff --git a/package.json b/package.json index d7db838fc..ca01e85e8 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App.tsx b/src/App.tsx index f4c6fbf87..008c99c99 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 { return ; case ACTIVE_SHEET_SETTINGS: return ; + case ACTIVE_SHEET_DOCTOR: + return ; case ACTIVE_SHEET_SELECT_PLUGINS_TO_EXPORT: return ; case ACTIVE_SHEET_SHARE_DATA: @@ -126,6 +131,7 @@ export class App extends React.Component { return ( + {this.getSheet} diff --git a/src/chrome/DoctorBar.tsx b/src/chrome/DoctorBar.tsx new file mode 100644 index 000000000..272bcf007 --- /dev/null +++ b/src/chrome/DoctorBar.tsx @@ -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 { + 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 && ( + + + + + + + + + + + Doctor has discovered problems with your installation + + + + + ) + ); + } + setVisible(visible: boolean) { + this.setState(prevState => { + return { + ...prevState, + visible, + }; + }); + } +} + +export default connect(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, +}); diff --git a/src/chrome/DoctorSheet.tsx b/src/chrome/DoctorSheet.tsx new file mode 100644 index 000000000..5fd1084c4 --- /dev/null +++ b/src/chrome/DoctorSheet.tsx @@ -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 ; + case 'SUCCESS': + return ( + + ); + case 'WARNING': + return ( + + ); + default: + return ( + + ); + } +} + +function HealthcheckDisplay(props: { + category: HealthcheckReportCategory; + check: HealthcheckReportItem; + onClick?: () => void; +}) { + return ( + + + + + {props.check.label} + + + + ); +} + +function SideMessageDisplay(props: { + isHealthcheckInProgress: boolean; + hasProblems: boolean; +}) { + if (props.isHealthcheckInProgress) { + return ( + + Doctor is running healthchecks... + + ); + } else if (props.hasProblems) { + return ( + + Doctor has discovered problems with your installation. + + ); + } else { + return ( + + All good! Doctor has not discovered any issues with your installation. + + ); + } +} + +export type State = {}; + +type Props = OwnProps & StateFromProps & DispatchFromProps; +class DoctorSheet extends Component { + 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 ( + + Doctor + + + {Object.values(this.props.report.categories).map( + (category, categoryIdx) => { + return ( + + + + {category.checks.map((check, checkIdx) => ( + this.openHelpUrl(check) + : undefined + } + /> + ))} + + + ); + }, + )} + + + + + + + + + + + + + ); + } +} + +export default connect( + ({healthchecks: {healthcheckReport}}) => ({ + report: healthcheckReport, + }), + { + initHealthcheckReport, + updateHealthcheckReportItem, + startHealthchecks, + finishHealthchecks, + }, +)(DoctorSheet); diff --git a/src/chrome/TitleBar.tsx b/src/chrome/TitleBar.tsx index a61a1d3aa..3665560cf 100644 --- a/src/chrome/TitleBar.tsx +++ b/src/chrome/TitleBar.tsx @@ -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 { version={this.props.version} /> )} + +