Use unique keys to identify healthchecks

Summary: Added unique keys for each healthcheck and used them to save already seen failures. Later I will also use them for gathering doctor analytics.

Reviewed By: jknoxville

Differential Revision: D19301583

fbshipit-source-id: 0c0aa977ea73ce555e0d9fb8e8ead844624fb9cd
This commit is contained in:
Anton Nikolaev
2020-01-08 08:10:36 -08:00
committed by Facebook Github Bot
parent cdb0990ac8
commit 751c778069
10 changed files with 560 additions and 249 deletions

View File

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

View File

@@ -22,7 +22,8 @@ import {getEnvInfo} from './environmentInfo';
: {
label: category.label,
results: await Promise.all(
category.healthchecks.map(async ({label, run}) => ({
category.healthchecks.map(async ({key, label, run}) => ({
key,
label,
result: await run(environmentInfo),
})),

View File

@@ -33,6 +33,7 @@ export type Healthchecks = {
};
export type Healthcheck = {
key: string;
label: string;
isRequired?: boolean;
run: (
@@ -48,6 +49,7 @@ export type CategoryResult = [
{
label: string;
results: Array<{
key: string;
label: string;
isRequired: boolean;
result: {hasProblem: boolean};
@@ -63,6 +65,7 @@ export function getHealthchecks(): Healthchecks {
isSkipped: false,
healthchecks: [
{
key: 'common.openssl',
label: 'OpenSSL Installed',
run: async (_: EnvironmentInfo) => {
const isAvailable = await commandSucceeds('openssl version');
@@ -72,6 +75,7 @@ export function getHealthchecks(): Healthchecks {
},
},
{
key: 'common.watchman',
label: 'Watchman Installed',
run: async (_: EnvironmentInfo) => {
const isAvailable = await isWatchmanAvailable();
@@ -88,6 +92,7 @@ export function getHealthchecks(): Healthchecks {
isSkipped: false,
healthchecks: [
{
key: 'android.sdk',
label: 'SDK Installed',
isRequired: true,
run: async (e: EnvironmentInfo) => ({
@@ -104,6 +109,7 @@ export function getHealthchecks(): Healthchecks {
isSkipped: false,
healthchecks: [
{
key: 'ios.sdk',
label: 'SDK Installed',
isRequired: true,
run: async (e: EnvironmentInfo) => ({
@@ -111,6 +117,7 @@ export function getHealthchecks(): Healthchecks {
}),
},
{
key: 'ios.xcode',
label: 'XCode Installed',
isRequired: true,
run: async (e: EnvironmentInfo) => ({
@@ -118,6 +125,7 @@ export function getHealthchecks(): Healthchecks {
}),
},
{
key: 'ios.xcode-select',
label: 'xcode-select set',
isRequired: true,
run: async (_: EnvironmentInfo) => ({
@@ -125,6 +133,7 @@ export function getHealthchecks(): Healthchecks {
}),
},
{
key: 'ios.instruments',
label: 'Instruments exists',
isRequired: true,
run: async (_: EnvironmentInfo) => {
@@ -164,17 +173,20 @@ export async function runHealthchecks(): Promise<
{
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,
};
category.healthchecks.map(
async ({key, label, run, isRequired}) => ({
key,
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,
};
}),
}),
})),
),
),
},
];

View File

@@ -23,14 +23,14 @@ import runHealthchecks, {
HealthcheckEventsHandler,
} from '../utils/runHealthchecks';
import {
HealthcheckResult,
updateHealthcheckResult,
startHealthchecks,
finishHealthchecks,
HealthcheckStatus,
} from '../reducers/healthchecks';
type StateFromProps = {
healthcheckStatus: HealthcheckStatus;
healthcheckResult: HealthcheckResult;
} & HealthcheckSettings;
type DispatchFromProps = {
@@ -52,7 +52,11 @@ class DoctorBar extends Component<Props, State> {
}
async showMessageIfChecksFailed() {
await runHealthchecks(this.props);
if (this.props.healthcheckStatus === 'FAILED') {
if (
(this.props.healthcheckResult.status === 'FAILED' ||
this.props.healthcheckResult.status === 'WARNING') &&
!this.props.healthcheckResult.isAcknowledged
) {
this.setVisible(true);
}
}
@@ -99,11 +103,11 @@ export default connect<StateFromProps, DispatchFromProps, {}, Store>(
({
settingsState: {enableAndroid},
healthchecks: {
healthcheckReport: {status},
healthcheckReport: {result},
},
}) => ({
enableAndroid,
healthcheckStatus: status,
healthcheckResult: result,
}),
{
setActiveSheet,

View File

@@ -26,14 +26,12 @@ import {State as Store} from '../reducers';
import {
HealthcheckResult,
HealthcheckReportCategory,
HealthcheckReportItem,
HealthcheckReport,
startHealthchecks,
finishHealthchecks,
updateHealthcheckResult,
acknowledgeProblems,
resetAcknowledgedProblems,
HealthcheckStatus,
} from '../reducers/healthchecks';
import runHealthchecks, {
HealthcheckSettings,
@@ -123,17 +121,18 @@ function CenteredCheckbox(props: {
);
}
function HealthcheckIcon(props: {check: HealthcheckResult}) {
switch (props.check.status) {
function HealthcheckIcon(props: {checkResult: HealthcheckResult}) {
const {checkResult: check} = props;
switch (props.checkResult.status) {
case 'IN_PROGRESS':
return <LoadingIndicator size={16} title={props.check.message} />;
return <LoadingIndicator size={16} title={props.checkResult.message} />;
case 'SKIPPED':
return (
<Glyph
size={16}
name={'question'}
color={colors.grey}
title={props.check.message}
title={props.checkResult.message}
/>
);
case 'SUCCESS':
@@ -142,7 +141,7 @@ function HealthcheckIcon(props: {check: HealthcheckResult}) {
size={16}
name={'checkmark'}
color={colors.green}
title={props.check.message}
title={props.checkResult.message}
/>
);
case 'FAILED':
@@ -151,17 +150,8 @@ function HealthcheckIcon(props: {check: HealthcheckResult}) {
size={16}
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"
title={props.checkResult.message}
variant={check.isAcknowledged ? 'outline' : 'filled'}
/>
);
default:
@@ -170,26 +160,26 @@ function HealthcheckIcon(props: {check: HealthcheckResult}) {
size={16}
name={'caution'}
color={colors.yellow}
title={props.check.message}
title={props.checkResult.message}
/>
);
}
}
function HealthcheckDisplay(props: {
category: HealthcheckReportCategory;
check: HealthcheckReportItem;
label: string;
result: HealthcheckResult;
onClick?: () => void;
}) {
return (
<FlexColumn shrink>
<HealthcheckDisplayContainer shrink title={props.check.message}>
<HealthcheckIcon check={props.check} />
<HealthcheckDisplayContainer shrink title={props.result.message}>
<HealthcheckIcon checkResult={props.result} />
<HealthcheckLabel
underline={!!props.onClick}
cursor={props.onClick && 'pointer'}
onClick={props.onClick}>
{props.check.label}
{props.label}
</HealthcheckLabel>
</HealthcheckDisplayContainer>
</FlexColumn>
@@ -221,20 +211,13 @@ function SideMessageDisplay(props: {
}
}
function hasProblems(status: HealthcheckStatus) {
return (
status === 'FAILED' ||
status === 'FAILED_ACKNOWLEDGED' ||
status === 'WARNING'
);
function hasProblems(result: HealthcheckResult) {
const {status} = result;
return status === 'FAILED' || status === 'WARNING';
}
function hasFailedChecks(status: HealthcheckStatus) {
return status === 'FAILED' || status === 'FAILED_ACKNOWLEDGED';
}
function hasNewFailedChecks(status: HealthcheckStatus) {
return status === 'FAILED';
function hasNewProblems(result: HealthcheckResult) {
return hasProblems(result) && !result.isAcknowledged;
}
export type State = {
@@ -254,21 +237,21 @@ class DoctorSheet extends Component<Props, State> {
static getDerivedStateFromProps(props: Props, state: State): State | null {
if (
!state.acknowledgeCheckboxVisible &&
hasFailedChecks(props.healthcheckReport.status)
hasProblems(props.healthcheckReport.result)
) {
return {
...state,
acknowledgeCheckboxVisible: true,
acknowledgeOnClose:
state.acknowledgeOnClose === undefined
? !hasNewFailedChecks(props.healthcheckReport.status)
? !hasNewProblems(props.healthcheckReport.result)
: state.acknowledgeOnClose,
};
}
if (
state.acknowledgeCheckboxVisible &&
!hasFailedChecks(props.healthcheckReport.status)
!hasProblems(props.healthcheckReport.result)
) {
return {
...state,
@@ -296,8 +279,8 @@ class DoctorSheet extends Component<Props, State> {
});
}
openHelpUrl(check: HealthcheckReportItem): void {
check.helpUrl && shell.openExternal(check.helpUrl);
openHelpUrl(helpUrl?: string): void {
helpUrl && shell.openExternal(helpUrl);
}
async runHealthchecks() {
@@ -311,29 +294,34 @@ class DoctorSheet extends Component<Props, State> {
<FlexRow>
<HealthcheckListContainer>
{Object.values(this.props.healthcheckReport.categories).map(
(category: HealthcheckReportCategory, categoryIdx: number) => {
(category: HealthcheckReportCategory) => {
return (
<CategoryContainer key={categoryIdx}>
<HealthcheckDisplay check={category} category={category} />
{category.status !== 'SKIPPED' && (
<CategoryContainer key={category.key}>
<HealthcheckDisplay
label={category.label}
result={category.result}
/>
{category.result.status !== 'SKIPPED' && (
<CategoryContainer>
{category.checks.map((check, checkIdx) => (
{Object.values(category.checks).map(check => (
<HealthcheckDisplay
key={checkIdx}
category={category}
check={check}
key={check.key}
label={check.label}
result={check.result}
onClick={
check.helpUrl
? () => this.openHelpUrl(check)
check.result.helpUrl
? () => this.openHelpUrl(check.result.helpUrl)
: undefined
}
/>
))}
</CategoryContainer>
)}
{category.status === 'SKIPPED' && (
{category.result.status === 'SKIPPED' && (
<CategoryContainer>
<SkipReasonLabel>{category.message}</SkipReasonLabel>
<SkipReasonLabel>
{category.result.message}
</SkipReasonLabel>
</CategoryContainer>
)}
</CategoryContainer>
@@ -345,9 +333,9 @@ class DoctorSheet extends Component<Props, State> {
<SideContainer shrink>
<SideMessageDisplay
isHealthcheckInProgress={
this.props.healthcheckReport.status === 'IN_PROGRESS'
this.props.healthcheckReport.result.status === 'IN_PROGRESS'
}
hasProblems={hasProblems(this.props.healthcheckReport.status)}
hasProblems={hasProblems(this.props.healthcheckReport.result)}
/>
</SideContainer>
</FlexRow>
@@ -366,7 +354,9 @@ class DoctorSheet extends Component<Props, State> {
Close
</Button>
<Button
disabled={this.props.healthcheckReport.status === 'IN_PROGRESS'}
disabled={
this.props.healthcheckReport.result.status === 'IN_PROGRESS'
}
type="primary"
compact
padded

View File

@@ -0,0 +1,335 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`acknowledgeProblems 1`] = `
Object {
"acknowledgedProblems": Array [
"ios.sdk",
"common.openssl",
],
"healthcheckReport": Object {
"categories": Object {
"android": Object {
"checks": Object {
"android.sdk": Object {
"key": "android.sdk",
"label": "SDK Installed",
"result": Object {
"isAcknowledged": true,
"status": "SUCCESS",
},
},
},
"key": "android",
"label": "Android",
"result": Object {
"isAcknowledged": true,
"status": "SUCCESS",
},
},
"common": Object {
"checks": Object {
"common.openssl": Object {
"key": "common.openssl",
"label": "OpenSSL Istalled",
"result": Object {
"isAcknowledged": true,
"status": "FAILED",
},
},
},
"key": "common",
"label": "Common",
"result": Object {
"isAcknowledged": true,
"status": "FAILED",
},
},
"ios": Object {
"checks": Object {
"ios.sdk": Object {
"key": "ios.sdk",
"label": "SDK Installed",
"result": Object {
"isAcknowledged": true,
"status": "FAILED",
},
},
},
"key": "ios",
"label": "iOS",
"result": Object {
"isAcknowledged": true,
"status": "FAILED",
},
},
},
"result": Object {
"isAcknowledged": true,
"status": "FAILED",
},
},
}
`;
exports[`finish 1`] = `
Object {
"acknowledgedProblems": Array [],
"healthcheckReport": Object {
"categories": Object {
"android": Object {
"checks": Object {
"android.sdk": Object {
"key": "android.sdk",
"label": "SDK Installed",
"result": Object {
"isAcknowledged": false,
"message": "Updated Test Message",
"status": "SUCCESS",
},
},
},
"key": "android",
"label": "Android",
"result": Object {
"isAcknowledged": false,
"status": "SUCCESS",
},
},
"common": Object {
"checks": Object {
"common.openssl": Object {
"key": "common.openssl",
"label": "OpenSSL Istalled",
"result": Object {
"isAcknowledged": false,
"message": "Updated Test Message",
"status": "SUCCESS",
},
},
},
"key": "common",
"label": "Common",
"result": Object {
"isAcknowledged": false,
"status": "SUCCESS",
},
},
"ios": Object {
"checks": Object {
"ios.sdk": Object {
"key": "ios.sdk",
"label": "SDK Installed",
"result": Object {
"isAcknowledged": false,
"message": "Updated Test Message",
"status": "SUCCESS",
},
},
},
"key": "ios",
"label": "iOS",
"result": Object {
"isAcknowledged": false,
"status": "SUCCESS",
},
},
},
"result": Object {
"status": "SUCCESS",
},
},
}
`;
exports[`startHealthCheck 1`] = `
Object {
"acknowledgedProblems": Array [],
"healthcheckReport": Object {
"categories": Object {
"android": Object {
"checks": Object {
"android.sdk": Object {
"key": "android.sdk",
"label": "SDK Installed",
"result": Object {
"status": "IN_PROGRESS",
},
},
},
"key": "android",
"label": "Android",
"result": Object {
"status": "IN_PROGRESS",
},
},
"common": Object {
"checks": Object {
"common.openssl": Object {
"key": "common.openssl",
"label": "OpenSSL Istalled",
"result": Object {
"status": "IN_PROGRESS",
},
},
},
"key": "common",
"label": "Common",
"result": Object {
"status": "IN_PROGRESS",
},
},
"ios": Object {
"checks": Object {
"ios.sdk": Object {
"key": "ios.sdk",
"label": "SDK Installed",
"result": Object {
"status": "IN_PROGRESS",
},
},
},
"key": "ios",
"label": "iOS",
"result": Object {
"status": "IN_PROGRESS",
},
},
},
"result": Object {
"status": "IN_PROGRESS",
},
},
}
`;
exports[`statuses updated after healthchecks finished 1`] = `
Object {
"acknowledgedProblems": Array [],
"healthcheckReport": Object {
"categories": Object {
"android": Object {
"checks": Object {
"android.sdk": Object {
"key": "android.sdk",
"label": "SDK Installed",
"result": Object {
"isAcknowledged": false,
"message": "Updated Test Message",
"status": "FAILED",
},
},
},
"key": "android",
"label": "Android",
"result": Object {
"isAcknowledged": false,
"status": "FAILED",
},
},
"common": Object {
"checks": Object {
"common.openssl": Object {
"key": "common.openssl",
"label": "OpenSSL Istalled",
"result": Object {
"isAcknowledged": false,
"message": "Updated Test Message",
"status": "SUCCESS",
},
},
},
"key": "common",
"label": "Common",
"result": Object {
"status": "SUCCESS",
},
},
"ios": Object {
"checks": Object {
"ios.sdk": Object {
"key": "ios.sdk",
"label": "SDK Installed",
"result": Object {
"isAcknowledged": false,
"message": "Updated Test Message",
"status": "SUCCESS",
},
},
},
"key": "ios",
"label": "iOS",
"result": Object {
"status": "SUCCESS",
},
},
},
"result": Object {
"isAcknowledged": false,
"status": "FAILED",
},
},
}
`;
exports[`updateHealthcheckResult 1`] = `
Object {
"acknowledgedProblems": Array [],
"healthcheckReport": Object {
"categories": Object {
"android": Object {
"checks": Object {
"android.sdk": Object {
"key": "android.sdk",
"label": "SDK Installed",
"result": Object {
"isAcknowledged": false,
"message": "Updated Test Message",
"status": "SUCCESS",
},
},
},
"key": "android",
"label": "Android",
"result": Object {
"status": "IN_PROGRESS",
},
},
"common": Object {
"checks": Object {
"common.openssl": Object {
"key": "common.openssl",
"label": "OpenSSL Istalled",
"result": Object {
"status": "IN_PROGRESS",
},
},
},
"key": "common",
"label": "Common",
"result": Object {
"status": "IN_PROGRESS",
},
},
"ios": Object {
"checks": Object {
"ios.sdk": Object {
"key": "ios.sdk",
"label": "SDK Installed",
"result": Object {
"status": "IN_PROGRESS",
},
},
},
"key": "ios",
"label": "iOS",
"result": Object {
"status": "IN_PROGRESS",
},
},
},
"result": Object {
"status": "IN_PROGRESS",
},
},
}
`;

View File

@@ -64,143 +64,106 @@ const HEALTHCHECKS: Healthchecks = {
test('startHealthCheck', () => {
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',
);
expect(res).toMatchSnapshot();
});
test('updateHealthcheckResult', () => {
let res = reducer(undefined, startHealthchecks(HEALTHCHECKS));
res = reducer(
res,
updateHealthcheckResult(0, 0, {
updateHealthcheckResult('android', 'android.sdk', {
message: 'Updated Test Message',
isAcknowledged: false,
status: 'SUCCESS',
}),
);
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[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');
expect(res).toMatchSnapshot();
});
test('finish', () => {
let res = reducer(undefined, startHealthchecks(HEALTHCHECKS));
res = reducer(
res,
updateHealthcheckResult(0, 0, {
updateHealthcheckResult('ios', 'ios.sdk', {
message: 'Updated Test Message',
isAcknowledged: false,
status: 'SUCCESS',
}),
);
res = reducer(
res,
updateHealthcheckResult(1, 0, {
updateHealthcheckResult('android', 'android.sdk', {
message: 'Updated Test Message',
isAcknowledged: false,
status: 'SUCCESS',
}),
);
res = reducer(
res,
updateHealthcheckResult(2, 0, {
updateHealthcheckResult('common', 'common.openssl', {
message: 'Updated Test Message',
isAcknowledged: false,
status: 'SUCCESS',
}),
);
res = reducer(res, finishHealthchecks());
expect(res.healthcheckReport.status).toBe('SUCCESS');
expect(res.healthcheckReport.categories.map(c => c.status)).toEqual([
'SUCCESS',
'SUCCESS',
'SUCCESS',
]);
expect(res).toMatchSnapshot();
});
test('statuses updated after healthchecks finished', () => {
let res = reducer(undefined, startHealthchecks(HEALTHCHECKS));
res = reducer(
res,
updateHealthcheckResult(1, 0, {
updateHealthcheckResult('android', 'android.sdk', {
message: 'Updated Test Message',
isAcknowledged: false,
status: 'FAILED',
}),
);
res = reducer(
res,
updateHealthcheckResult(0, 0, {
updateHealthcheckResult('ios', 'ios.sdk', {
message: 'Updated Test Message',
isAcknowledged: false,
status: 'SUCCESS',
}),
);
res = reducer(
res,
updateHealthcheckResult(2, 0, {
updateHealthcheckResult('common', 'common.openssl', {
message: 'Updated Test Message',
isAcknowledged: false,
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(
'FAILED',
);
expect(res).toMatchSnapshot();
});
test('acknowledgeProblems', () => {
let res = reducer(undefined, startHealthchecks(HEALTHCHECKS));
res = reducer(
res,
updateHealthcheckResult(0, 0, {
updateHealthcheckResult('ios', 'ios.sdk', {
isAcknowledged: false,
status: 'FAILED',
}),
);
res = reducer(
res,
updateHealthcheckResult(1, 0, {
updateHealthcheckResult('android', 'android.sdk', {
isAcknowledged: false,
status: 'SUCCESS',
}),
);
res = reducer(
res,
updateHealthcheckResult(2, 0, {
updateHealthcheckResult('common', 'common.openssl', {
isAcknowledged: false,
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',
);
expect(res).toMatchSnapshot();
});

View File

@@ -27,8 +27,8 @@ export type Action =
| {
type: 'UPDATE_HEALTHCHECK_RESULT';
payload: {
categoryIdx: number;
itemIdx: number;
categoryKey: string;
itemKey: string;
result: HealthcheckResult;
};
}
@@ -41,157 +41,170 @@ export type Action =
const INITIAL_STATE: State = {
healthcheckReport: {
status: 'IN_PROGRESS',
categories: [],
result: {status: 'IN_PROGRESS'},
categories: {},
},
acknowledgedProblems: [],
};
type Dictionary<T> = {[key: string]: T};
export type HealthcheckStatus =
| 'IN_PROGRESS'
| 'SUCCESS'
| 'FAILED'
| 'FAILED_ACKNOWLEDGED'
| 'SKIPPED'
| 'WARNING';
export type HealthcheckResult = {
status: HealthcheckStatus;
isAcknowledged?: boolean;
message?: string;
helpUrl?: string;
};
export type HealthcheckReportItem = {
key: string;
label: string;
} & HealthcheckResult;
export type HealthcheckReportCategory = {
label: string;
checks: Array<HealthcheckReportItem>;
} & HealthcheckResult;
export type HealthcheckReport = {
status: HealthcheckStatus;
categories: Array<HealthcheckReportCategory>;
result: HealthcheckResult;
};
function getHealthcheckIdentifier(
category: HealthcheckReportCategory,
item: HealthcheckReportItem,
) {
return `${category.label} : ${item.label}`;
}
export type HealthcheckReportCategory = {
key: string;
label: string;
result: HealthcheckResult;
checks: Dictionary<HealthcheckReportItem>;
};
export type HealthcheckReport = {
result: HealthcheckResult;
categories: Dictionary<HealthcheckReportCategory>;
};
function recomputeHealthcheckStatus(draft: State): void {
draft.healthcheckReport.status = computeAggregatedStatus(
draft.healthcheckReport.categories.map(c => c.status),
draft.healthcheckReport.result = computeAggregatedResult(
Object.values(draft.healthcheckReport.categories).map(c => c.result),
);
}
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';
function computeAggregatedResult(
results: HealthcheckResult[],
): HealthcheckResult {
return results.some(r => r.status === 'IN_PROGRESS')
? {status: 'IN_PROGRESS'}
: results.every(r => r.status === 'SUCCESS')
? {status: 'SUCCESS'}
: results.some(r => r.status === 'FAILED' && !r.isAcknowledged)
? {status: 'FAILED', isAcknowledged: false}
: results.some(r => r.status === 'FAILED')
? {status: 'FAILED', isAcknowledged: true}
: results.some(r => r.status === 'WARNING' && !r.isAcknowledged)
? {status: 'WARNING', isAcknowledged: false}
: results.some(r => r.status === 'WARNING')
? {status: 'WARNING', isAcknowledged: true}
: {status: 'SKIPPED'};
}
const updateCheckResult = produce(
(
draft: State,
{
categoryIdx,
itemIdx,
categoryKey,
itemKey,
result,
}: {
categoryIdx: number;
itemIdx: number;
categoryKey: string;
itemKey: string;
result: HealthcheckResult;
},
) => {
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 category = draft.healthcheckReport.categories[categoryKey];
const item = category.checks[itemKey];
Object.assign(item.result, result);
item.result.isAcknowledged = draft.acknowledgedProblems.includes(item.key);
},
);
function createDict<T>(pairs: [string, T][]): Dictionary<T> {
const obj: Dictionary<T> = {};
for (const pair of pairs) {
obj[pair[0]] = pair[1];
}
return obj;
}
const start = produce((draft: State, healthchecks: Healthchecks) => {
draft.healthcheckReport = {
status: 'IN_PROGRESS',
categories: Object.values(healthchecks)
.map(category => {
result: {status: 'IN_PROGRESS'},
categories: createDict<HealthcheckReportCategory>(
Object.entries(healthchecks).map(([categoryKey, category]) => {
if (category.isSkipped) {
return {
status: 'SKIPPED',
label: category.label,
checks: [],
message: category.skipReason,
};
return [
categoryKey,
{
key: categoryKey,
result: {
status: 'SKIPPED',
message: category.skipReason,
},
label: category.label,
checks: createDict<HealthcheckReportItem>([]),
},
];
}
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),
return [
categoryKey,
{
key: categoryKey,
result: {status: 'IN_PROGRESS'},
label: category.label,
checks: createDict<HealthcheckReportItem>(
category.healthchecks.map(check => [
check.key,
{
key: check.key,
result: {status: 'IN_PROGRESS'},
label: check.label,
},
]),
),
},
];
}),
),
};
});
const finish = produce((draft: State) => {
draft.healthcheckReport.categories
.filter(cat => cat.status !== 'SKIPPED')
Object.values(draft.healthcheckReport.categories)
.filter(cat => cat.result.status !== 'SKIPPED')
.forEach(cat => {
cat.message = undefined;
cat.status = computeAggregatedStatus(cat.checks.map(c => c.status));
cat.result.message = undefined;
cat.result = computeAggregatedResult(
Object.values(cat.checks).map(c => c.result),
);
});
recomputeHealthcheckStatus(draft);
if (draft.healthcheckReport.status === 'SUCCESS') {
if (draft.healthcheckReport.result.status === 'SUCCESS') {
setAcknowledgedProblemsToEmpty(draft);
}
});
const acknowledge = produce((draft: State) => {
draft.acknowledgedProblems = ([] as string[]).concat(
...draft.healthcheckReport.categories.map(cat =>
cat.checks
...Object.values(draft.healthcheckReport.categories).map(cat =>
Object.values(cat.checks)
.filter(
chk =>
chk.status === 'FAILED' ||
chk.status === 'FAILED_ACKNOWLEDGED' ||
chk.status === 'WARNING',
chk.result.status === 'FAILED' || chk.result.status === 'WARNING',
)
.map(chk => getHealthcheckIdentifier(cat, chk)),
.map(chk => chk.key),
),
);
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';
}
Object.values(draft.healthcheckReport.categories).forEach(cat => {
cat.result.isAcknowledged = true;
Object.values(cat.checks).forEach(chk => {
chk.result.isAcknowledged = true;
});
});
recomputeHealthcheckStatus(draft);
@@ -199,14 +212,10 @@ const acknowledge = produce((draft: State) => {
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';
}
Object.values(draft.healthcheckReport.categories).forEach(cat => {
cat.result.isAcknowledged = false;
Object.values(cat.checks).forEach(chk => {
chk.result.isAcknowledged = false;
});
});
}
@@ -234,14 +243,14 @@ export default function reducer(
}
export const updateHealthcheckResult = (
categoryIdx: number,
itemIdx: number,
categoryKey: string,
itemKey: string,
result: HealthcheckResult,
): Action => ({
type: 'UPDATE_HEALTHCHECK_RESULT',
payload: {
categoryIdx,
itemIdx,
categoryKey,
itemKey,
result,
},
});

View File

@@ -15,8 +15,8 @@ let runningHealthcheck: Promise<void>;
export type HealthcheckEventsHandler = {
updateHealthcheckResult: (
categoryIdx: number,
itemIdx: number,
categoryKey: string,
itemKey: string,
result: HealthcheckResult,
) => void;
startHealthchecks: (healthchecks: Healthchecks) => void;
@@ -41,17 +41,11 @@ async function launchHealthchecks(options: HealthcheckOptions): Promise<void> {
}
options.startHealthchecks(healthchecks);
const environmentInfo = await getEnvInfo();
const categories = Object.values(healthchecks);
for (const [categoryIdx, category] of categories.entries()) {
for (const [categoryKey, category] of Object.entries(healthchecks)) {
if (category.isSkipped) {
continue;
}
for (
let healthcheckIdx = 0;
healthcheckIdx < category.healthchecks.length;
healthcheckIdx++
) {
const h = category.healthchecks[healthcheckIdx];
for (const h of category.healthchecks) {
const checkResult = await h.run(environmentInfo);
const result: HealthcheckResult =
checkResult.hasProblem && h.isRequired
@@ -65,7 +59,7 @@ async function launchHealthchecks(options: HealthcheckOptions): Promise<void> {
helpUrl: checkResult.helpUrl,
}
: {status: 'SUCCESS'};
options.updateHealthcheckResult(categoryIdx, healthcheckIdx, result);
options.updateHealthcheckResult(categoryKey, h.key, result);
}
}
options.finishHealthchecks();

View File

@@ -294,5 +294,8 @@
],
"play": [
16
],
"cross-outline": [
16
]
}