Do not show Doctor warning on startup for already seen problems

Summary: Doctor sometimes can show false-positives and in such case there is no way to suppress showing warning message on each startup. To reduce annoyance I've added an option to save the problems already seen by user and show the Doctor warning only for new problems.

Reviewed By: mweststrate

Differential Revision: D19187095

fbshipit-source-id: 14c1fcc9674f47fbe0b5b0f2d5d1bceb47f7b45d
This commit is contained in:
Anton Nikolaev
2020-01-02 08:53:45 -08:00
committed by Facebook Github Bot
parent 74daa3fb1a
commit 2bee021966
11 changed files with 635 additions and 405 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "flipper-doctor",
"version": "0.2.4",
"version": "0.4.1",
"description": "Utility for checking for issues with a flipper installation",
"main": "lib/index.js",
"types": "lib/index.d.ts",

View File

@@ -17,8 +17,9 @@ import {getEnvInfo} from './environmentInfo';
const results = await Promise.all(
Object.entries(healthchecks).map(async ([key, category]) => [
key,
category
? {
category.isSkipped
? category
: {
label: category.label,
results: await Promise.all(
category.healthchecks.map(async ({label, run}) => ({
@@ -26,8 +27,7 @@ import {getEnvInfo} from './environmentInfo';
result: await run(environmentInfo),
})),
),
}
: {},
},
]),
);

View File

@@ -12,19 +12,26 @@ import {promisify} from 'util';
import {EnvironmentInfo, getEnvInfo} from './environmentInfo';
export {getEnvInfo} from './environmentInfo';
type HealthcheckCategory = {
export type HealthcheckCategory = {
label: string;
isSkipped: false;
isRequired: boolean;
healthchecks: Healthcheck[];
};
type Healthchecks = {
common: HealthcheckCategory;
android: HealthcheckCategory;
ios?: HealthcheckCategory;
export type SkippedHealthcheckCategory = {
label: string;
isSkipped: true;
skipReason: string;
};
type Healthcheck = {
export type Healthchecks = {
common: HealthcheckCategory | SkippedHealthcheckCategory;
android: HealthcheckCategory | SkippedHealthcheckCategory;
ios: HealthcheckCategory | SkippedHealthcheckCategory;
};
export type Healthcheck = {
label: string;
isRequired?: boolean;
run: (
@@ -35,7 +42,7 @@ type Healthcheck = {
}>;
};
type CategoryResult = [
export type CategoryResult = [
string,
{
label: string;
@@ -52,6 +59,7 @@ export function getHealthchecks(): Healthchecks {
common: {
label: 'Common',
isRequired: true,
isSkipped: false,
healthchecks: [
{
label: 'OpenSSL Installed',
@@ -67,6 +75,7 @@ export function getHealthchecks(): Healthchecks {
android: {
label: 'Android',
isRequired: false,
isSkipped: false,
healthchecks: [
{
label: 'SDK Installed',
@@ -77,11 +86,12 @@ export function getHealthchecks(): Healthchecks {
},
],
},
...(process.platform === 'darwin'
? {
ios: {
label: 'iOS',
ios: {
label: 'iOS',
...(process.platform === 'darwin'
? {
isRequired: false,
isSkipped: false,
healthchecks: [
{
label: 'SDK Installed',
@@ -118,44 +128,49 @@ export function getHealthchecks(): Healthchecks {
},
},
],
},
}
: {}),
}
: {
isSkipped: true,
skipReason: `Healthcheck is skipped, because iOS development is not supported on the current platform "${process.platform}"`,
}),
},
};
}
export async function runHealthchecks(): Promise<Array<CategoryResult>> {
export async function runHealthchecks(): Promise<
Array<CategoryResult | SkippedHealthcheckCategory>
> {
const environmentInfo = await getEnvInfo();
const healthchecks: Healthchecks = getHealthchecks();
const results: Array<CategoryResult> = (
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<
CategoryResult | SkippedHealthcheckCategory
> = await Promise.all(
Object.entries(healthchecks).map(async ([key, category]) => {
if (category.isSkipped) {
return category;
}
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;
}),
);
return results;
}
@@ -164,7 +179,3 @@ async function commandSucceeds(command: string): Promise<boolean> {
.then(() => true)
.catch(() => false);
}
export function notNull<T>(x: T | null | undefined): x is T {
return x !== null && x !== undefined;
}

View File

@@ -138,7 +138,7 @@
"emotion": "^10.0.23",
"expand-tilde": "^2.0.2",
"express": "^4.15.2",
"flipper-doctor": "^0.2.4",
"flipper-doctor": "^0.4.1",
"fs-extra": "^8.0.1",
"immer": "^5.0.1",
"immutable": "^4.0.0-rc.12",

View File

@@ -23,22 +23,21 @@ import runHealthchecks, {
HealthcheckEventsHandler,
} from '../utils/runHealthchecks';
import {
initHealthcheckReport,
updateHealthcheckReportItemStatus,
updateHealthcheckReportCategoryStatus,
updateHealthcheckResult,
startHealthchecks,
finishHealthchecks,
HealthcheckStatus,
} from '../reducers/healthchecks';
type StateFromProps = HealthcheckSettings;
type StateFromProps = {
healthcheckStatus: HealthcheckStatus;
} & HealthcheckSettings;
type DispatchFromProps = {
setActiveSheet: (payload: ActiveSheet) => void;
} & HealthcheckEventsHandler;
type State = {
visible: boolean;
};
type State = {visible: boolean};
type Props = DispatchFromProps & StateFromProps;
class DoctorBar extends Component<Props, State> {
@@ -52,8 +51,8 @@ class DoctorBar extends Component<Props, State> {
this.showMessageIfChecksFailed();
}
async showMessageIfChecksFailed() {
const result = await runHealthchecks(this.props);
if (!result) {
await runHealthchecks(this.props);
if (this.props.healthcheckStatus === 'FAILED') {
this.setVisible(true);
}
}
@@ -66,9 +65,10 @@ class DoctorBar extends Component<Props, State> {
<ButtonSection>
<ButtonGroup>
<Button
onClick={() =>
this.props.setActiveSheet(ACTIVE_SHEET_DOCTOR)
}>
onClick={() => {
this.props.setActiveSheet(ACTIVE_SHEET_DOCTOR);
this.setVisible(false);
}}>
Show Problems
</Button>
<Button onClick={() => this.setVisible(false)}>
@@ -96,14 +96,18 @@ class DoctorBar extends Component<Props, State> {
}
export default connect<StateFromProps, DispatchFromProps, {}, Store>(
({settingsState}) => ({
enableAndroid: settingsState.enableAndroid,
({
settingsState: {enableAndroid},
healthchecks: {
healthcheckReport: {status},
},
}) => ({
enableAndroid,
healthcheckStatus: status,
}),
{
setActiveSheet,
initHealthcheckReport,
updateHealthcheckReportItemStatus,
updateHealthcheckReportCategoryStatus,
updateHealthcheckResult,
startHealthchecks,
finishHealthchecks,
},

View File

@@ -18,6 +18,7 @@ import {
Spacer,
Button,
FlexBox,
Checkbox,
} from 'flipper';
import React, {Component} from 'react';
import {connect} from 'react-redux';
@@ -27,11 +28,12 @@ import {
HealthcheckReportCategory,
HealthcheckReportItem,
HealthcheckReport,
initHealthcheckReport,
updateHealthcheckReportItemStatus,
updateHealthcheckReportCategoryStatus,
startHealthchecks,
finishHealthchecks,
updateHealthcheckResult,
acknowledgeProblems,
resetAcknowledgedProblems,
HealthcheckStatus,
} from '../reducers/healthchecks';
import runHealthchecks, {
HealthcheckSettings,
@@ -40,10 +42,13 @@ import runHealthchecks, {
import {shell} from 'electron';
type StateFromProps = {
report: HealthcheckReport;
healthcheckReport: HealthcheckReport;
} & HealthcheckSettings;
type DispatchFromProps = HealthcheckEventsHandler;
type DispatchFromProps = {
acknowledgeProblems: () => void;
resetAcknowledgedProblems: () => void;
} & HealthcheckEventsHandler;
const Container = styled(FlexColumn)({
padding: 20,
@@ -57,6 +62,7 @@ const HealthcheckDisplayContainer = styled(FlexRow)({
const HealthcheckListContainer = styled(FlexColumn)({
marginBottom: 20,
width: 300,
});
const Title = styled(Text)({
@@ -77,7 +83,7 @@ const SideContainer = styled(FlexBox)({
padding: 20,
backgroundColor: colors.highlightBackground,
border: '1px solid #b3b3b3',
width: 320,
width: 250,
});
const SideContainerText = styled(Text)({
@@ -89,10 +95,34 @@ const HealthcheckLabel = styled(Text)({
paddingLeft: 5,
});
const SkipReasonLabel = styled(Text)({
paddingLeft: 21,
fontStyle: 'italic',
});
const CenteredContainer = styled.label({
display: 'flex',
alignItems: 'center',
});
type OwnProps = {
onHide: () => void;
};
function CenteredCheckbox(props: {
checked: boolean;
text: string;
onChange: (checked: boolean) => void;
}) {
const {checked, onChange, text} = props;
return (
<CenteredContainer>
<Checkbox checked={checked} onChange={onChange} />
{text}
</CenteredContainer>
);
}
function HealthcheckIcon(props: {check: HealthcheckResult}) {
switch (props.check.status) {
case 'IN_PROGRESS':
@@ -115,21 +145,31 @@ function HealthcheckIcon(props: {check: HealthcheckResult}) {
title={props.check.message}
/>
);
case 'WARNING':
case 'FAILED':
return (
<Glyph
size={16}
name={'caution'}
color={colors.yellow}
name={'cross'}
color={colors.red}
title={props.check.message}
/>
);
case 'FAILED_ACKNOWLEDGED':
return (
<Glyph
size={16}
name={'cross'}
color={colors.red}
title={props.check.message}
variant="outline"
/>
);
default:
return (
<Glyph
size={16}
name={'cross'}
color={colors.red}
name={'caution'}
color={colors.yellow}
title={props.check.message}
/>
);
@@ -181,13 +221,79 @@ function SideMessageDisplay(props: {
}
}
export type State = {};
function hasProblems(status: HealthcheckStatus) {
return (
status === 'FAILED' ||
status === 'FAILED_ACKNOWLEDGED' ||
status === 'WARNING'
);
}
function hasFailedChecks(status: HealthcheckStatus) {
return status === 'FAILED' || status === 'FAILED_ACKNOWLEDGED';
}
function hasNewFailedChecks(status: HealthcheckStatus) {
return status === 'FAILED';
}
export type State = {
acknowledgeCheckboxVisible: boolean;
acknowledgeOnClose?: boolean;
};
type Props = OwnProps & StateFromProps & DispatchFromProps;
class DoctorSheet extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {};
this.state = {
acknowledgeCheckboxVisible: false,
};
}
static getDerivedStateFromProps(props: Props, state: State): State | null {
if (
!state.acknowledgeCheckboxVisible &&
hasFailedChecks(props.healthcheckReport.status)
) {
return {
...state,
acknowledgeCheckboxVisible: true,
acknowledgeOnClose:
state.acknowledgeOnClose === undefined
? !hasNewFailedChecks(props.healthcheckReport.status)
: state.acknowledgeOnClose,
};
}
if (
state.acknowledgeCheckboxVisible &&
!hasFailedChecks(props.healthcheckReport.status)
) {
return {
...state,
acknowledgeCheckboxVisible: false,
};
}
return null;
}
componentWillUnmount(): void {
if (this.state.acknowledgeOnClose) {
this.props.acknowledgeProblems();
} else {
this.props.resetAcknowledgedProblems();
}
}
onAcknowledgeOnCloseChanged(acknowledge: boolean): void {
this.setState(prevState => {
return {
...prevState,
acknowledgeOnClose: acknowledge,
};
});
}
openHelpUrl(check: HealthcheckReportItem): void {
@@ -195,47 +301,41 @@ class DoctorSheet extends Component<Props, State> {
}
async runHealthchecks() {
this.setState(prevState => {
return {
...prevState,
};
});
await runHealthchecks(this.props);
}
hasProblems() {
return this.props.report.categories.some(cat =>
cat.checks.some(
chk => chk.status === 'FAILED' || chk.status === 'WARNING',
),
);
}
render() {
return (
<Container>
<Title>Doctor</Title>
<FlexRow>
<HealthcheckListContainer>
{Object.values(this.props.report.categories).map(
(category, categoryIdx) => {
{Object.values(this.props.healthcheckReport.categories).map(
(category: HealthcheckReportCategory, categoryIdx: number) => {
return (
<CategoryContainer key={categoryIdx}>
<HealthcheckDisplay check={category} category={category} />
<CategoryContainer>
{category.checks.map((check, checkIdx) => (
<HealthcheckDisplay
key={checkIdx}
category={category}
check={check}
onClick={
check.helpUrl
? () => this.openHelpUrl(check)
: undefined
}
/>
))}
</CategoryContainer>
{category.status !== 'SKIPPED' && (
<CategoryContainer>
{category.checks.map((check, checkIdx) => (
<HealthcheckDisplay
key={checkIdx}
category={category}
check={check}
onClick={
check.helpUrl
? () => this.openHelpUrl(check)
: undefined
}
/>
))}
</CategoryContainer>
)}
{category.status === 'SKIPPED' && (
<CategoryContainer>
<SkipReasonLabel>{category.message}</SkipReasonLabel>
</CategoryContainer>
)}
</CategoryContainer>
);
},
@@ -245,19 +345,28 @@ class DoctorSheet extends Component<Props, State> {
<SideContainer shrink>
<SideMessageDisplay
isHealthcheckInProgress={
this.props.report.isHealthcheckInProgress
this.props.healthcheckReport.status === 'IN_PROGRESS'
}
hasProblems={this.hasProblems()}
hasProblems={hasProblems(this.props.healthcheckReport.status)}
/>
</SideContainer>
</FlexRow>
<FlexRow>
<Spacer />
{this.state.acknowledgeCheckboxVisible && (
<CenteredCheckbox
checked={!!this.state.acknowledgeOnClose}
onChange={this.onAcknowledgeOnCloseChanged.bind(this)}
text={
'Do not show warning about these problems on Flipper startup'
}
/>
)}
<Button compact padded onClick={this.props.onHide}>
Close
</Button>
<Button
disabled={this.props.report.isHealthcheckInProgress}
disabled={this.props.healthcheckReport.status === 'IN_PROGRESS'}
type="primary"
compact
padded
@@ -272,14 +381,14 @@ class DoctorSheet extends Component<Props, State> {
export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
({healthchecks: {healthcheckReport}, settingsState}) => ({
report: healthcheckReport,
healthcheckReport,
enableAndroid: settingsState.enableAndroid,
}),
{
initHealthcheckReport,
updateHealthcheckReportItemStatus,
updateHealthcheckReportCategoryStatus,
startHealthchecks,
finishHealthchecks,
updateHealthcheckResult,
acknowledgeProblems,
resetAcknowledgedProblems,
},
)(DoctorSheet);

View File

@@ -9,105 +9,195 @@
import {
default as reducer,
initHealthcheckReport,
startHealthchecks,
HealthcheckReportCategory,
HealthcheckReportItem,
finishHealthchecks,
updateHealthcheckReportItemStatus,
updateHealthcheckReportCategoryStatus,
updateHealthcheckResult,
acknowledgeProblems,
} from '../healthchecks';
import {Healthchecks} from 'flipper-doctor';
import {EnvironmentInfo} from 'flipper-doctor/lib/environmentInfo';
const HEALTHCHECK_ITEM: HealthcheckReportItem = {
label: 'Test Check',
status: 'WARNING',
message: "Something didn't quite work.",
const HEALTHCHECKS: Healthchecks = {
ios: {
label: 'iOS',
isSkipped: false,
isRequired: true,
healthchecks: [
{
label: 'SDK Installed',
run: async (_env: EnvironmentInfo) => {
return {hasProblem: false};
},
},
],
},
android: {
label: 'Android',
isSkipped: false,
isRequired: true,
healthchecks: [
{
label: 'SDK Installed',
run: async (_env: EnvironmentInfo) => {
return {hasProblem: true};
},
},
],
},
common: {
label: 'Common',
isSkipped: false,
isRequired: false,
healthchecks: [
{
label: 'OpenSSL Istalled',
run: async (_env: EnvironmentInfo) => {
return {hasProblem: false};
},
},
],
},
};
const HEALTHCHECK_CATEGORY: HealthcheckReportCategory = {
label: 'Test Category',
status: 'WARNING',
checks: [HEALTHCHECK_ITEM],
};
test('initHealthcheckReport', () => {
const report = {
isHealthcheckInProgress: false,
categories: [],
};
const res = reducer(undefined, initHealthcheckReport(report));
expect(res.healthcheckReport).toEqual(report);
});
test('startHealthCheck', () => {
const report = {
isHealthcheckInProgress: false,
categories: [HEALTHCHECK_CATEGORY],
};
let res = reducer(undefined, initHealthcheckReport(report));
res = reducer(res, startHealthchecks());
expect(res.healthcheckReport.isHealthcheckInProgress).toBeTruthy();
// This seems trivial, but by getting the spread wrong, it's easy
// to break this.
expect(res.healthcheckReport.categories).toEqual([HEALTHCHECK_CATEGORY]);
const res = reducer(undefined, startHealthchecks(HEALTHCHECKS));
expect(res.healthcheckReport.status).toBe('IN_PROGRESS');
expect(res.healthcheckReport.categories.length).toBe(3);
expect(res.healthcheckReport.categories[0].status).toEqual('IN_PROGRESS');
expect(res.healthcheckReport.categories[0].label).toEqual('iOS');
expect(res.healthcheckReport.categories[0].checks.length).toEqual(1);
expect(res.healthcheckReport.categories[0].checks[0].label).toEqual(
'SDK Installed',
);
expect(res.healthcheckReport.categories[0].checks[0].status).toEqual(
'IN_PROGRESS',
);
});
test('finish', () => {
const report = {
isHealthcheckInProgress: true,
categories: [HEALTHCHECK_CATEGORY],
};
let res = reducer(undefined, initHealthcheckReport(report));
res = reducer(res, finishHealthchecks());
expect(res.healthcheckReport.isHealthcheckInProgress).toBeFalsy();
expect(res.healthcheckReport.categories).toEqual([HEALTHCHECK_CATEGORY]);
});
test('updateHealthcheck', () => {
const report = {
isHealthcheckInProgress: true,
categories: [HEALTHCHECK_CATEGORY, HEALTHCHECK_CATEGORY],
};
let res = reducer(undefined, initHealthcheckReport(report));
test('updateHealthcheckResult', () => {
let res = reducer(undefined, startHealthchecks(HEALTHCHECKS));
res = reducer(
res,
updateHealthcheckReportItemStatus(0, 0, {
updateHealthcheckResult(0, 0, {
message: 'Updated Test Message',
status: 'SUCCESS',
}),
);
expect(res.healthcheckReport.isHealthcheckInProgress).toBeTruthy();
expect(res.healthcheckReport.status).toBe('IN_PROGRESS');
expect(res.healthcheckReport.categories[0].checks[0].message).toEqual(
'Updated Test Message',
);
expect(res.healthcheckReport.categories[0].checks[0].status).toEqual(
'SUCCESS',
);
expect(res.healthcheckReport.categories[1].checks[0].label).toEqual(
'Test Check',
expect(res.healthcheckReport.categories[0].status).toEqual('IN_PROGRESS');
expect(res.healthcheckReport.categories[1].checks[0].message).toBeUndefined();
expect(res.healthcheckReport.categories[1].checks[0].status).toEqual(
'IN_PROGRESS',
);
expect(res.healthcheckReport.categories[1].status).toEqual('IN_PROGRESS');
});
test('finish', () => {
let res = reducer(undefined, startHealthchecks(HEALTHCHECKS));
res = reducer(
res,
updateHealthcheckResult(0, 0, {
message: 'Updated Test Message',
status: 'SUCCESS',
}),
);
res = reducer(
res,
updateHealthcheckResult(1, 0, {
message: 'Updated Test Message',
status: 'SUCCESS',
}),
);
res = reducer(
res,
updateHealthcheckResult(2, 0, {
message: 'Updated Test Message',
status: 'SUCCESS',
}),
);
res = reducer(res, finishHealthchecks());
expect(res.healthcheckReport.status).toBe('SUCCESS');
expect(res.healthcheckReport.categories.map(c => c.status)).toEqual([
'SUCCESS',
'SUCCESS',
'SUCCESS',
]);
});
test('statuses updated after healthchecks finished', () => {
let res = reducer(undefined, startHealthchecks(HEALTHCHECKS));
res = reducer(
res,
updateHealthcheckResult(1, 0, {
message: 'Updated Test Message',
status: 'FAILED',
}),
);
res = reducer(
res,
updateHealthcheckResult(0, 0, {
message: 'Updated Test Message',
status: 'SUCCESS',
}),
);
res = reducer(
res,
updateHealthcheckResult(2, 0, {
message: 'Updated Test Message',
status: 'SUCCESS',
}),
);
res = reducer(res, finishHealthchecks());
expect(res.healthcheckReport.status).toBe('FAILED');
expect(res.healthcheckReport.categories.map(c => c.status)).toEqual([
'SUCCESS',
'FAILED',
'SUCCESS',
]);
expect(res.healthcheckReport.categories[1].checks[0].message).toEqual(
'Updated Test Message',
);
expect(res.healthcheckReport.categories[1].checks[0].status).toEqual(
'WARNING',
'FAILED',
);
});
test('updateHealthcheckCategoryStatus', () => {
const report = {
isHealthcheckInProgress: true,
categories: [HEALTHCHECK_CATEGORY, HEALTHCHECK_CATEGORY],
};
let res = reducer(undefined, initHealthcheckReport(report));
test('acknowledgeProblems', () => {
let res = reducer(undefined, startHealthchecks(HEALTHCHECKS));
res = reducer(
res,
updateHealthcheckReportCategoryStatus(1, {
updateHealthcheckResult(0, 0, {
status: 'FAILED',
message: 'Error message',
}),
);
expect(res.healthcheckReport.isHealthcheckInProgress).toBeTruthy();
expect(res.healthcheckReport.categories[0].label).toEqual('Test Category');
expect(res.healthcheckReport.categories[0].status).toEqual('WARNING');
expect(res.healthcheckReport.categories[1].label).toEqual('Test Category');
expect(res.healthcheckReport.categories[1].status).toEqual('FAILED');
expect(res.healthcheckReport.categories[1].message).toEqual('Error message');
res = reducer(
res,
updateHealthcheckResult(1, 0, {
status: 'SUCCESS',
}),
);
res = reducer(
res,
updateHealthcheckResult(2, 0, {
status: 'FAILED',
}),
);
res = reducer(res, finishHealthchecks());
res = reducer(res, acknowledgeProblems());
expect(res.healthcheckReport.categories[0].status).toEqual(
'FAILED_ACKNOWLEDGED',
);
expect(res.healthcheckReport.categories[0].checks[0].status).toEqual(
'FAILED_ACKNOWLEDGED',
);
expect(res.healthcheckReport.categories[1].status).toEqual('SUCCESS');
expect(res.healthcheckReport.categories[2].status).toEqual(
'FAILED_ACKNOWLEDGED',
);
});

View File

@@ -7,51 +7,51 @@
* @format
*/
import {produce} from 'immer';
import {Actions} from './';
import {produce} from 'flipper';
import {Healthchecks} from 'flipper-doctor';
export type State = {
healthcheckReport: HealthcheckReport;
acknowledgedProblems: string[];
};
export type Action =
| {
type: 'INIT_HEALTHCHECK_REPORT';
payload: HealthcheckReport;
}
| {
type: 'UPDATE_HEALTHCHECK_REPORT_ITEM_STATUS';
payload: {
categoryIdx: number;
itemIdx: number;
status: HealthcheckResult;
};
}
| {
type: 'UPDATE_HEALTHCHECK_REPORT_CATEGORY_STATUS';
payload: {
categoryIdx: number;
status: HealthcheckResult;
};
}
| {
type: 'START_HEALTHCHECKS';
payload: Healthchecks;
}
| {
type: 'FINISH_HEALTHCHECKS';
}
| {
type: 'UPDATE_HEALTHCHECK_RESULT';
payload: {
categoryIdx: number;
itemIdx: number;
result: HealthcheckResult;
};
}
| {
type: 'ACKNOWLEDGE_PROBLEMS';
}
| {
type: 'RESET_ACKNOWLEDGED_PROBLEMS';
};
const INITIAL_STATE: State = {
healthcheckReport: {
isHealthcheckInProgress: false,
status: 'IN_PROGRESS',
categories: [],
},
acknowledgedProblems: [],
};
export type HealthcheckStatus =
| 'IN_PROGRESS'
| 'SUCCESS'
| 'FAILED'
| 'FAILED_ACKNOWLEDGED'
| 'SKIPPED'
| 'WARNING';
@@ -71,95 +71,194 @@ export type HealthcheckReportCategory = {
} & HealthcheckResult;
export type HealthcheckReport = {
isHealthcheckInProgress: boolean;
status: HealthcheckStatus;
categories: Array<HealthcheckReportCategory>;
};
const updateReportItem = produce(
function getHealthcheckIdentifier(
category: HealthcheckReportCategory,
item: HealthcheckReportItem,
) {
return `${category.label} : ${item.label}`;
}
function recomputeHealthcheckStatus(draft: State): void {
draft.healthcheckReport.status = computeAggregatedStatus(
draft.healthcheckReport.categories.map(c => c.status),
);
}
function computeAggregatedStatus(
statuses: HealthcheckStatus[],
): HealthcheckStatus {
return statuses.some(s => s === 'IN_PROGRESS')
? 'IN_PROGRESS'
: statuses.every(s => s === 'SUCCESS')
? 'SUCCESS'
: statuses.some(s => s === 'FAILED')
? 'FAILED'
: statuses.some(s => s === 'FAILED_ACKNOWLEDGED')
? 'FAILED_ACKNOWLEDGED'
: statuses.some(s => s === 'WARNING')
? 'WARNING'
: 'SKIPPED';
}
const updateCheckResult = produce(
(
draft: State,
payload: {
{
categoryIdx,
itemIdx,
result,
}: {
categoryIdx: number;
itemIdx: number;
status: HealthcheckResult;
result: HealthcheckResult;
},
) => {
Object.assign(
draft.healthcheckReport.categories[payload.categoryIdx].checks[
payload.itemIdx
],
payload.status,
);
const category = draft.healthcheckReport.categories[categoryIdx];
const item = category.checks[itemIdx];
Object.assign(item, result);
if (
result.status === 'FAILED' &&
draft.acknowledgedProblems.includes(
getHealthcheckIdentifier(category, item),
)
) {
item.status = 'FAILED_ACKNOWLEDGED';
}
},
);
const updateCategoryStatus = produce(
(draft: State, payload: {categoryIdx: number; status: HealthcheckResult}) => {
Object.assign(
draft.healthcheckReport.categories[payload.categoryIdx],
payload.status,
);
},
);
const initReport = produce((draft: State, report: HealthcheckReport) => {
draft.healthcheckReport = report;
const start = produce((draft: State, healthchecks: Healthchecks) => {
draft.healthcheckReport = {
status: 'IN_PROGRESS',
categories: Object.values(healthchecks)
.map(category => {
if (category.isSkipped) {
return {
status: 'SKIPPED',
label: category.label,
checks: [],
message: category.skipReason,
};
}
return {
status: 'IN_PROGRESS',
label: category.label,
checks: category.healthchecks.map(x => ({
status: 'IN_PROGRESS',
label: x.label,
})),
};
})
.filter(x => !!x)
.map(x => x as HealthcheckReportCategory),
};
});
const setIsInProgress = produce((draft: State, isInProgress: boolean) => {
draft.healthcheckReport.isHealthcheckInProgress = isInProgress;
const finish = produce((draft: State) => {
draft.healthcheckReport.categories
.filter(cat => cat.status !== 'SKIPPED')
.forEach(cat => {
cat.message = undefined;
cat.status = computeAggregatedStatus(cat.checks.map(c => c.status));
});
recomputeHealthcheckStatus(draft);
if (draft.healthcheckReport.status === 'SUCCESS') {
setAcknowledgedProblemsToEmpty(draft);
}
});
const acknowledge = produce((draft: State) => {
draft.acknowledgedProblems = ([] as string[]).concat(
...draft.healthcheckReport.categories.map(cat =>
cat.checks
.filter(
chk =>
chk.status === 'FAILED' ||
chk.status === 'FAILED_ACKNOWLEDGED' ||
chk.status === 'WARNING',
)
.map(chk => getHealthcheckIdentifier(cat, chk)),
),
);
draft.healthcheckReport.categories.forEach(cat => {
if (cat.status === 'FAILED') {
cat.status = 'FAILED_ACKNOWLEDGED';
}
cat.checks.forEach(chk => {
if (chk.status == 'FAILED') {
chk.status = 'FAILED_ACKNOWLEDGED';
}
});
});
recomputeHealthcheckStatus(draft);
});
function setAcknowledgedProblemsToEmpty(draft: State) {
draft.acknowledgedProblems = [];
draft.healthcheckReport.categories.forEach(cat => {
if (cat.status === 'FAILED_ACKNOWLEDGED') {
cat.status = 'FAILED';
}
cat.checks.forEach(chk => {
if (chk.status == 'FAILED_ACKNOWLEDGED') {
chk.status = 'FAILED';
}
});
});
}
const resetAcknowledged = produce((draft: State) => {
setAcknowledgedProblemsToEmpty(draft);
recomputeHealthcheckStatus(draft);
});
export default function reducer(
draft: State | undefined = INITIAL_STATE,
action: Actions,
): State {
return action.type === 'INIT_HEALTHCHECK_REPORT'
? initReport(draft, action.payload)
: action.type === 'START_HEALTHCHECKS'
? setIsInProgress(draft, true)
return action.type === 'START_HEALTHCHECKS'
? start(draft, action.payload)
: action.type === 'FINISH_HEALTHCHECKS'
? setIsInProgress(draft, false)
: action.type === 'UPDATE_HEALTHCHECK_REPORT_ITEM_STATUS'
? updateReportItem(draft, action.payload)
: action.type === 'UPDATE_HEALTHCHECK_REPORT_CATEGORY_STATUS'
? updateCategoryStatus(draft, action.payload)
? finish(draft)
: action.type === 'UPDATE_HEALTHCHECK_RESULT'
? updateCheckResult(draft, action.payload)
: action.type === 'ACKNOWLEDGE_PROBLEMS'
? acknowledge(draft)
: action.type === 'RESET_ACKNOWLEDGED_PROBLEMS'
? resetAcknowledged(draft)
: draft;
}
export const initHealthcheckReport = (report: HealthcheckReport): Action => ({
type: 'INIT_HEALTHCHECK_REPORT',
payload: report,
});
export const updateHealthcheckReportItemStatus = (
export const updateHealthcheckResult = (
categoryIdx: number,
itemIdx: number,
status: HealthcheckResult,
result: HealthcheckResult,
): Action => ({
type: 'UPDATE_HEALTHCHECK_REPORT_ITEM_STATUS',
type: 'UPDATE_HEALTHCHECK_RESULT',
payload: {
categoryIdx,
itemIdx,
status,
result,
},
});
export const updateHealthcheckReportCategoryStatus = (
categoryIdx: number,
status: HealthcheckResult,
): Action => ({
type: 'UPDATE_HEALTHCHECK_REPORT_CATEGORY_STATUS',
payload: {
categoryIdx,
status,
},
});
export const startHealthchecks = (): Action => ({
export const startHealthchecks = (healthchecks: Healthchecks): Action => ({
type: 'START_HEALTHCHECKS',
payload: healthchecks,
});
export const finishHealthchecks = (): Action => ({
type: 'FINISH_HEALTHCHECKS',
});
export const acknowledgeProblems = (): Action => ({
type: 'ACKNOWLEDGE_PROBLEMS',
});
export const resetAcknowledgedProblems = (): Action => ({
type: 'RESET_ACKNOWLEDGED_PROBLEMS',
});

View File

@@ -92,7 +92,7 @@ export type State = {
launcherSettingsState: LauncherSettingsState & PersistPartial;
supportForm: SupportFormState;
pluginManager: PluginManagerState;
healthchecks: HealthcheckState;
healthchecks: HealthcheckState & PersistPartial;
};
export type Store = ReduxStore<State, Actions>;
@@ -159,5 +159,12 @@ export default combineReducers<State, Actions>({
},
launcherSettings,
),
healthchecks,
healthchecks: persistReducer<HealthcheckState, Actions>(
{
key: 'healthchecks',
storage,
whitelist: ['acknowledgedProblems'],
},
healthchecks,
),
});

View File

@@ -7,28 +7,19 @@
* @format
*/
import {
HealthcheckResult,
HealthcheckReport,
HealthcheckReportCategory,
} from '../reducers/healthchecks';
import {getHealthchecks, getEnvInfo} from 'flipper-doctor';
import {HealthcheckResult} from '../reducers/healthchecks';
import {getHealthchecks, getEnvInfo, Healthchecks} from 'flipper-doctor';
let healthcheckIsRunning: boolean;
let runningHealthcheck: Promise<boolean>;
let runningHealthcheck: Promise<void>;
export type HealthcheckEventsHandler = {
initHealthcheckReport: (report: HealthcheckReport) => void;
updateHealthcheckReportItemStatus: (
updateHealthcheckResult: (
categoryIdx: number,
itemIdx: number,
status: HealthcheckResult,
result: HealthcheckResult,
) => void;
updateHealthcheckReportCategoryStatus: (
categoryIdx: number,
status: HealthcheckResult,
) => void;
startHealthchecks: () => void;
startHealthchecks: (healthchecks: Healthchecks) => void;
finishHealthchecks: () => void;
};
@@ -38,132 +29,51 @@ export type HealthcheckSettings = {
export type HealthcheckOptions = HealthcheckEventsHandler & HealthcheckSettings;
async function launchHealthchecks(
options: HealthcheckOptions,
): Promise<boolean> {
let healthchecksResult: boolean = true;
options.startHealthchecks();
try {
const inProgressResult: HealthcheckResult = {
status: 'IN_PROGRESS',
message: 'The healthcheck is in progress',
async function launchHealthchecks(options: HealthcheckOptions): Promise<void> {
const healthchecks = getHealthchecks();
if (!options.enableAndroid) {
healthchecks.android = {
label: healthchecks.android.label,
isSkipped: true,
skipReason:
'Healthcheck is skipped, because "Android Development" option is disabled in the Flipper settings',
};
const androidSkippedResult: HealthcheckResult = {
status: 'SKIPPED',
message:
'The healthcheck was skipped because Android development is disabled in the Flipper settings',
};
const failedResult: HealthcheckResult = {
status: 'FAILED',
message: 'The healthcheck failed',
};
const warningResult: HealthcheckResult = {
status: 'WARNING',
message: 'The optional healthcheck failed',
};
const succeededResult: HealthcheckResult = {
status: 'SUCCESS',
message: undefined,
};
const healthchecks = getHealthchecks();
const hcState: HealthcheckReport = {
isHealthcheckInProgress: true,
categories: Object.entries(healthchecks)
.map(([categoryKey, category]) => {
if (!category) {
return null;
}
const state: HealthcheckResult =
categoryKey === 'android' && !options.enableAndroid
? androidSkippedResult
: inProgressResult;
return {
...state,
label: category.label,
checks: category.healthchecks.map(x => ({
...state,
label: x.label,
})),
};
})
.filter(x => !!x)
.map(x => x as HealthcheckReportCategory),
};
options.initHealthcheckReport(hcState);
const environmentInfo = await getEnvInfo();
const categories = Object.entries(healthchecks);
for (const [categoryIdx, [categoryKey, category]] of categories.entries()) {
if (!category) {
continue;
}
const isSkippedAndroidCategory =
categoryKey === 'android' && !options.enableAndroid;
const allResults: HealthcheckResult[] = [];
for (
let healthcheckIdx = 0;
healthcheckIdx < category.healthchecks.length;
healthcheckIdx++
) {
const h = category.healthchecks[healthcheckIdx];
if (isSkippedAndroidCategory) {
options.updateHealthcheckReportItemStatus(
categoryIdx,
healthcheckIdx,
androidSkippedResult,
);
allResults.push(androidSkippedResult);
} else {
const result = await h.run(environmentInfo);
if (result.hasProblem && h.isRequired) {
healthchecksResult = false;
}
const status: HealthcheckResult =
result.hasProblem && h.isRequired
? {
...failedResult,
helpUrl: result.helpUrl,
}
: result.hasProblem && !h.isRequired
? {
...warningResult,
helpUrl: result.helpUrl,
}
: succeededResult;
options.updateHealthcheckReportItemStatus(
categoryIdx,
healthcheckIdx,
status,
);
allResults.push(status);
}
}
const categoryStatus = {
label: category.label,
...(allResults.some(c => c.status === 'IN_PROGRESS')
? inProgressResult
: allResults.every(c => c.status === 'SUCCESS')
? succeededResult
: allResults.every(c => c.status === 'SKIPPED')
? androidSkippedResult
: allResults.some(c => c.status === 'FAILED')
? failedResult
: warningResult),
};
options.updateHealthcheckReportCategoryStatus(
categoryIdx,
categoryStatus,
);
}
} catch {
} finally {
options.finishHealthchecks();
}
return healthchecksResult;
options.startHealthchecks(healthchecks);
const environmentInfo = await getEnvInfo();
const categories = Object.values(healthchecks);
for (const [categoryIdx, category] of categories.entries()) {
if (category.isSkipped) {
continue;
}
for (
let healthcheckIdx = 0;
healthcheckIdx < category.healthchecks.length;
healthcheckIdx++
) {
const h = category.healthchecks[healthcheckIdx];
const checkResult = await h.run(environmentInfo);
const result: HealthcheckResult =
checkResult.hasProblem && h.isRequired
? {
status: 'FAILED',
helpUrl: checkResult.helpUrl,
}
: checkResult.hasProblem && !h.isRequired
? {
status: 'WARNING',
helpUrl: checkResult.helpUrl,
}
: {status: 'SUCCESS'};
options.updateHealthcheckResult(categoryIdx, healthcheckIdx, result);
}
}
options.finishHealthchecks();
}
export default async function runHealthchecks(
options: HealthcheckOptions,
): Promise<boolean> {
): Promise<void> {
if (healthcheckIsRunning) {
return runningHealthcheck;
}

View File

@@ -4220,10 +4220,10 @@ 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.4:
version "0.2.4"
resolved "https://registry.yarnpkg.com/flipper-doctor/-/flipper-doctor-0.2.4.tgz#907c14282a9f65737f96505dfb0c5022109a10fe"
integrity sha512-MjX66Yd1dPSLcnOXIfqPaB5KXmd+A7f+99ashGhbRV+2Y8bXadN4kvYrUZ4gD177LmtMdNjISjm87kBt5t9pnw==
flipper-doctor@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/flipper-doctor/-/flipper-doctor-0.4.1.tgz#bc4ce11a920e983da04e492ba04d5fe108659bb5"
integrity sha512-1N7wgM03i1aHgTgMiv4V1mWkfkfoAgbBQmt4ai7AW2rhb2ocmy6p+8I51L2rJEOVmsRP/oDafpdgVNm1ggHp4w==
dependencies:
"@types/node" "^12.12.12"
envinfo "^7.4.0"