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:
committed by
Facebook Github Bot
parent
c1de6f4276
commit
ddb135ac39
@@ -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));
|
||||
})();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
142
src/chrome/DoctorBar.tsx
Normal 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
307
src/chrome/DoctorSheet.tsx
Normal 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);
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
154
src/reducers/healthchecks.tsx
Normal file
154
src/reducers/healthchecks.tsx
Normal 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',
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -45,6 +45,9 @@ const ICONS = {
|
||||
bug: [12],
|
||||
camcorder: [12],
|
||||
camera: [12],
|
||||
caution: [16],
|
||||
cross: [16],
|
||||
checkmark: [16],
|
||||
desktop: [12],
|
||||
directions: [12],
|
||||
internet: [12],
|
||||
|
||||
111
src/utils/runHealthchecks.tsx
Normal file
111
src/utils/runHealthchecks.tsx
Normal 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;
|
||||
}
|
||||
12
yarn.lock
12
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user