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:
committed by
Facebook Github Bot
parent
cdb0990ac8
commit
751c778069
@@ -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",
|
||||
|
||||
@@ -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),
|
||||
})),
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}),
|
||||
}),
|
||||
})),
|
||||
),
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
335
src/reducers/__tests__/__snapshots__/healthchecks.node.tsx.snap
Normal file
335
src/reducers/__tests__/__snapshots__/healthchecks.node.tsx.snap
Normal 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",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -294,5 +294,8 @@
|
||||
],
|
||||
"play": [
|
||||
16
|
||||
],
|
||||
"cross-outline": [
|
||||
16
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user