Add Flipper Doctor Component
Summary: As designed by Vince [here](https://www.figma.com/file/4e6BMdm2SuZ1L7FSuOPQVC/Flipper?node-id=585%3A127550), this diff adds Flipper Doctor into Sandy Note: - The dot on Doctor icon will act similarly to `DoctorBar` - add type to `count` for `LeftRailButton` to act like dot badge - Get rid of padding in `antd` modal Reviewed By: nikoant, mweststrate Differential Revision: D24137349 fbshipit-source-id: 8ce441e0ed96083eba09d98dfd3a45ff9b5be027
This commit is contained in:
committed by
Facebook GitHub Bot
parent
41928e0e3a
commit
2b6bac4227
@@ -7,7 +7,7 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {cloneElement, useState, useCallback} from 'react';
|
import React, {cloneElement, useState, useCallback, useMemo} from 'react';
|
||||||
import {styled, Layout} from 'flipper';
|
import {styled, Layout} from 'flipper';
|
||||||
import {Button, Divider, Badge, Tooltip, Avatar, Popover} from 'antd';
|
import {Button, Divider, Badge, Tooltip, Avatar, Popover} from 'antd';
|
||||||
import {
|
import {
|
||||||
@@ -25,6 +25,7 @@ import {SidebarLeft, SidebarRight} from './SandyIcons';
|
|||||||
import {useDispatch, useStore} from '../utils/useStore';
|
import {useDispatch, useStore} from '../utils/useStore';
|
||||||
import {toggleLeftSidebarVisible} from '../reducers/application';
|
import {toggleLeftSidebarVisible} from '../reducers/application';
|
||||||
import {theme} from './theme';
|
import {theme} from './theme';
|
||||||
|
import SetupDoctorScreen, {checkHasNewProblem} from './SetupDoctorScreen';
|
||||||
import SettingsSheet from '../chrome/SettingsSheet';
|
import SettingsSheet from '../chrome/SettingsSheet';
|
||||||
import WelcomeScreen from './WelcomeScreen';
|
import WelcomeScreen from './WelcomeScreen';
|
||||||
import SignInSheet from '../chrome/SignInSheet';
|
import SignInSheet from '../chrome/SignInSheet';
|
||||||
@@ -56,14 +57,19 @@ function LeftRailButton({
|
|||||||
small?: boolean;
|
small?: boolean;
|
||||||
toggled?: boolean;
|
toggled?: boolean;
|
||||||
selected?: boolean; // TODO: make sure only one element can be selected
|
selected?: boolean; // TODO: make sure only one element can be selected
|
||||||
count?: number;
|
count?: number | true;
|
||||||
title: string;
|
title: string;
|
||||||
onClick?: React.MouseEventHandler<HTMLElement>;
|
onClick?: React.MouseEventHandler<HTMLElement>;
|
||||||
}) {
|
}) {
|
||||||
let iconElement =
|
let iconElement =
|
||||||
icon && cloneElement(icon, {style: {fontSize: small ? 16 : 24}});
|
icon && cloneElement(icon, {style: {fontSize: small ? 16 : 24}});
|
||||||
if (count !== undefined) {
|
if (count !== undefined) {
|
||||||
iconElement = <Badge count={count}>{iconElement}</Badge>;
|
iconElement =
|
||||||
|
count === true ? (
|
||||||
|
<Badge dot>{iconElement}</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge count={count}>{iconElement}</Badge>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Tooltip title={title} placement="right">
|
<Tooltip title={title} placement="right">
|
||||||
@@ -113,11 +119,7 @@ export function LeftRail({
|
|||||||
/>
|
/>
|
||||||
</Layout.Vertical>
|
</Layout.Vertical>
|
||||||
<Layout.Vertical center gap={10}>
|
<Layout.Vertical center gap={10}>
|
||||||
<LeftRailButton
|
<SetupDoctorButton />
|
||||||
icon={<MedicineBoxOutlined />}
|
|
||||||
small
|
|
||||||
title="Setup Doctor"
|
|
||||||
/>
|
|
||||||
<WelcomeScreenButton />
|
<WelcomeScreenButton />
|
||||||
<ShowSettingsButton />
|
<ShowSettingsButton />
|
||||||
<LeftRailButton
|
<LeftRailButton
|
||||||
@@ -176,6 +178,27 @@ function DebugLogsButton({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SetupDoctorButton() {
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const result = useStore(
|
||||||
|
(state) => state.healthchecks.healthcheckReport.result,
|
||||||
|
);
|
||||||
|
const hasNewProblem = useMemo(() => checkHasNewProblem(result), [result]);
|
||||||
|
const onClose = useCallback(() => setVisible(false), []);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<LeftRailButton
|
||||||
|
icon={<MedicineBoxOutlined />}
|
||||||
|
small
|
||||||
|
title="Setup Doctor"
|
||||||
|
count={hasNewProblem ? true : undefined}
|
||||||
|
onClick={() => setVisible(true)}
|
||||||
|
/>
|
||||||
|
<SetupDoctorScreen visible={visible} onClose={onClose} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function ShowSettingsButton() {
|
function ShowSettingsButton() {
|
||||||
const [showSettings, setShowSettings] = useState(false);
|
const [showSettings, setShowSettings] = useState(false);
|
||||||
const onClose = useCallback(() => setShowSettings(false), []);
|
const onClose = useCallback(() => setShowSettings(false), []);
|
||||||
|
|||||||
301
desktop/app/src/sandy-chrome/SetupDoctorScreen.tsx
Normal file
301
desktop/app/src/sandy-chrome/SetupDoctorScreen.tsx
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, {useEffect, useCallback, useMemo, useState} from 'react';
|
||||||
|
import {useDispatch, useStore} from '../utils/useStore';
|
||||||
|
import {Typography, Collapse, Button, Modal, Checkbox, Alert} from 'antd';
|
||||||
|
import {
|
||||||
|
CheckCircleFilled,
|
||||||
|
CloseCircleFilled,
|
||||||
|
WarningFilled,
|
||||||
|
QuestionCircleFilled,
|
||||||
|
LoadingOutlined,
|
||||||
|
UpOutlined,
|
||||||
|
DownOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import {Layout} from '../ui';
|
||||||
|
import {
|
||||||
|
HealthcheckReport,
|
||||||
|
HealthcheckReportItem,
|
||||||
|
HealthcheckStatus,
|
||||||
|
HealthcheckResult,
|
||||||
|
} from '../reducers/healthchecks';
|
||||||
|
import {theme} from './theme';
|
||||||
|
import {
|
||||||
|
startHealthchecks,
|
||||||
|
updateHealthcheckResult,
|
||||||
|
finishHealthchecks,
|
||||||
|
acknowledgeProblems,
|
||||||
|
resetAcknowledgedProblems,
|
||||||
|
} from '../reducers/healthchecks';
|
||||||
|
import runHealthchecks from '../utils/runHealthchecks';
|
||||||
|
import {Healthchecks} from 'flipper-doctor';
|
||||||
|
import {reportUsage} from '../utils/metrics';
|
||||||
|
|
||||||
|
const {Title, Paragraph, Text} = Typography;
|
||||||
|
|
||||||
|
const borderStyle = {
|
||||||
|
border: `0px solid ${theme.backgroundTransparentHover}`,
|
||||||
|
borderBottomWidth: '1px',
|
||||||
|
borderRadius: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusTypeAndMessage: {
|
||||||
|
[key in HealthcheckStatus]: {
|
||||||
|
type: 'success' | 'info' | 'warning' | 'error';
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
} = {
|
||||||
|
IN_PROGRESS: {type: 'info', message: 'Doctor is running healthchecks...'},
|
||||||
|
FAILED: {
|
||||||
|
type: 'error',
|
||||||
|
message:
|
||||||
|
'Problems have been discovered with your installation. Please expand items for details.',
|
||||||
|
},
|
||||||
|
WARNING: {
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Doctor has discoverd warnings. Please expand items for details.',
|
||||||
|
},
|
||||||
|
SUCCESS: {
|
||||||
|
type: 'success',
|
||||||
|
message:
|
||||||
|
'All good! Doctor has not discovered any issues with your installation.',
|
||||||
|
},
|
||||||
|
// This is deduced from default case (for completeness)
|
||||||
|
SKIPPED: {
|
||||||
|
type: 'success',
|
||||||
|
message:
|
||||||
|
'All good! Doctor has not discovered any issues with your installation.',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function checkHasProblem(result: HealthcheckResult) {
|
||||||
|
return result.status === 'FAILED' || result.status === 'WARNING';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkHasNewProblem(result: HealthcheckResult) {
|
||||||
|
return checkHasProblem(result) && !result.isAcknowledged;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ResultTopDialog(props: {status: HealthcheckStatus}) {
|
||||||
|
const messages = statusTypeAndMessage[props.status];
|
||||||
|
return (
|
||||||
|
<Alert
|
||||||
|
type={messages.type}
|
||||||
|
showIcon
|
||||||
|
message={messages.message}
|
||||||
|
style={{
|
||||||
|
fontSize: theme.fontSize.smallBody,
|
||||||
|
lineHeight: '16px',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
paddingTop: '10px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CheckIcon(props: {status: HealthcheckStatus}) {
|
||||||
|
switch (props.status) {
|
||||||
|
case 'SUCCESS':
|
||||||
|
return (
|
||||||
|
<CheckCircleFilled style={{fontSize: 24, color: theme.successColor}} />
|
||||||
|
);
|
||||||
|
case 'FAILED':
|
||||||
|
return (
|
||||||
|
<CloseCircleFilled style={{fontSize: 24, color: theme.errorColor}} />
|
||||||
|
);
|
||||||
|
case 'WARNING':
|
||||||
|
return (
|
||||||
|
<WarningFilled style={{fontSize: 24, color: theme.warningColor}} />
|
||||||
|
);
|
||||||
|
case 'SKIPPED':
|
||||||
|
return (
|
||||||
|
<QuestionCircleFilled
|
||||||
|
style={{fontSize: 24, color: theme.disabledColor}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'IN_PROGRESS':
|
||||||
|
return (
|
||||||
|
<LoadingOutlined style={{fontSize: 24, color: theme.primaryColor}} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function CollapsableCategory(props: {checks: Array<HealthcheckReportItem>}) {
|
||||||
|
return (
|
||||||
|
<Collapse
|
||||||
|
expandIconPosition="right"
|
||||||
|
expandIcon={({isActive}) =>
|
||||||
|
isActive ? <UpOutlined /> : <DownOutlined />
|
||||||
|
}
|
||||||
|
bordered={false}
|
||||||
|
style={{backgroundColor: theme.backgroundDefault, border: 0}}>
|
||||||
|
{props.checks.map((check) => (
|
||||||
|
<Collapse.Panel
|
||||||
|
key={check.key}
|
||||||
|
header={
|
||||||
|
<Layout.Container padv="small">
|
||||||
|
<Layout.Horizontal gap="medium" center>
|
||||||
|
<CheckIcon status={check.result.status} />
|
||||||
|
<Title level={4}>{check.label}</Title>
|
||||||
|
</Layout.Horizontal>
|
||||||
|
</Layout.Container>
|
||||||
|
}
|
||||||
|
style={borderStyle}>
|
||||||
|
<Paragraph style={{paddingLeft: '40px'}}>
|
||||||
|
{check.result.message}
|
||||||
|
</Paragraph>
|
||||||
|
</Collapse.Panel>
|
||||||
|
))}
|
||||||
|
</Collapse>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function HealthCheckList(props: {report: HealthcheckReport}) {
|
||||||
|
useEffect(() => reportUsage('doctor:report:opened'), []);
|
||||||
|
return (
|
||||||
|
<Layout.Vertical>
|
||||||
|
<ResultTopDialog status={props.report.result.status} />
|
||||||
|
{Object.values(props.report.categories).map((category) => (
|
||||||
|
<Layout.Container key={category.key}>
|
||||||
|
<Title
|
||||||
|
level={3}
|
||||||
|
style={{...borderStyle, padding: `${theme.space.small}px 0`}}>
|
||||||
|
{category.label}
|
||||||
|
</Title>
|
||||||
|
<CollapsableCategory
|
||||||
|
checks={
|
||||||
|
category.result.status !== 'SKIPPED'
|
||||||
|
? Object.values(category.checks)
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
key: 'Skipped',
|
||||||
|
label: 'Skipped',
|
||||||
|
result: {
|
||||||
|
status: 'SKIPPED',
|
||||||
|
message: category.result.message,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Layout.Container>
|
||||||
|
))}
|
||||||
|
</Layout.Vertical>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SetupDoctorFooter(props: {
|
||||||
|
onClose: () => void;
|
||||||
|
onRerunDoctor: () => Promise<void>;
|
||||||
|
showAcknowledgeCheckbox: boolean;
|
||||||
|
acknowledgeCheck: boolean;
|
||||||
|
onAcknowledgeCheck: (checked: boolean) => void;
|
||||||
|
disableRerun: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Layout.Right>
|
||||||
|
{props.showAcknowledgeCheckbox ? (
|
||||||
|
<Checkbox
|
||||||
|
checked={props.acknowledgeCheck}
|
||||||
|
onChange={(e) => props.onAcknowledgeCheck(e.target.checked)}
|
||||||
|
style={{display: 'flex', alignItems: 'center'}}>
|
||||||
|
<Text style={{fontSize: theme.fontSize.smallBody}}>
|
||||||
|
Do not show warning about these problems at startup
|
||||||
|
</Text>
|
||||||
|
</Checkbox>
|
||||||
|
) : (
|
||||||
|
<Layout.Container />
|
||||||
|
)}
|
||||||
|
<Layout.Horizontal>
|
||||||
|
<Button onClick={props.onClose}>Close</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => props.onRerunDoctor()}
|
||||||
|
disabled={props.disableRerun}>
|
||||||
|
Re-run
|
||||||
|
</Button>
|
||||||
|
</Layout.Horizontal>
|
||||||
|
</Layout.Right>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SetupDoctorScreen(props: {
|
||||||
|
visible: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}) {
|
||||||
|
const healthcheckReport = useStore(
|
||||||
|
(state) => state.healthchecks.healthcheckReport,
|
||||||
|
);
|
||||||
|
const settings = useStore((state) => state.settingsState);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const [acknowlodgeProblem, setAcknowlodgeProblem] = useState(
|
||||||
|
checkHasNewProblem(healthcheckReport.result),
|
||||||
|
);
|
||||||
|
const hasProblem = useMemo(() => checkHasProblem(healthcheckReport.result), [
|
||||||
|
healthcheckReport,
|
||||||
|
]);
|
||||||
|
const onCloseModal = useCallback(() => {
|
||||||
|
const hasNewProblem = checkHasNewProblem(healthcheckReport.result);
|
||||||
|
if (acknowlodgeProblem) {
|
||||||
|
if (hasNewProblem) {
|
||||||
|
reportUsage('doctor:report:closed:newProblems:acknowledged');
|
||||||
|
}
|
||||||
|
reportUsage('doctor:report:closed:acknowleged');
|
||||||
|
dispatch(acknowledgeProblems());
|
||||||
|
} else {
|
||||||
|
if (hasNewProblem) {
|
||||||
|
reportUsage('doctor:report:closed:newProblems:notAcknowledged');
|
||||||
|
}
|
||||||
|
reportUsage('doctor:report:closed:notAcknowledged');
|
||||||
|
dispatch(resetAcknowledgedProblems());
|
||||||
|
}
|
||||||
|
props.onClose();
|
||||||
|
}, [healthcheckReport.result, acknowlodgeProblem, props, dispatch]);
|
||||||
|
const runDoctor = useCallback(async () => {
|
||||||
|
await runHealthchecks({
|
||||||
|
settings,
|
||||||
|
startHealthchecks: (healthchecks: Healthchecks) =>
|
||||||
|
dispatch(startHealthchecks(healthchecks)),
|
||||||
|
updateHealthcheckResult: (
|
||||||
|
categoryKey: string,
|
||||||
|
itemKey: string,
|
||||||
|
result: HealthcheckResult,
|
||||||
|
) => dispatch(updateHealthcheckResult(categoryKey, itemKey, result)),
|
||||||
|
finishHealthchecks: () => dispatch(finishHealthchecks()),
|
||||||
|
});
|
||||||
|
}, [settings, dispatch]);
|
||||||
|
|
||||||
|
// This will act like componentDidMount
|
||||||
|
useEffect(() => {
|
||||||
|
runDoctor();
|
||||||
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
width={570}
|
||||||
|
title="Setup Doctor"
|
||||||
|
visible={props.visible}
|
||||||
|
destroyOnClose
|
||||||
|
footer={
|
||||||
|
<SetupDoctorFooter
|
||||||
|
onClose={onCloseModal}
|
||||||
|
onRerunDoctor={runDoctor}
|
||||||
|
showAcknowledgeCheckbox={hasProblem}
|
||||||
|
acknowledgeCheck={acknowlodgeProblem}
|
||||||
|
onAcknowledgeCheck={(checked) => setAcknowlodgeProblem(checked)}
|
||||||
|
disableRerun={healthcheckReport.result.status === 'IN_PROGRESS'}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onCancel={onCloseModal}>
|
||||||
|
<HealthCheckList report={healthcheckReport} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -18,6 +18,12 @@
|
|||||||
@code-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier,
|
@code-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier,
|
||||||
monospace;
|
monospace;
|
||||||
|
|
||||||
|
/**
|
||||||
|
This section adds specific paddings to ANT component to match design
|
||||||
|
*/
|
||||||
|
@collapse-header-padding: 0;
|
||||||
|
@collapse-content-padding: 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
This section maps theme base colors as defined in light/dark.less to ANT variables
|
This section maps theme base colors as defined in light/dark.less to ANT variables
|
||||||
(as far as they aren't already)
|
(as far as they aren't already)
|
||||||
|
|||||||
@@ -73,6 +73,10 @@
|
|||||||
@link-hover: @primary-color;
|
@link-hover: @primary-color;
|
||||||
@link-hover-decoration: underline;
|
@link-hover-decoration: underline;
|
||||||
|
|
||||||
|
// Modal
|
||||||
|
@modal-header-title-font-size: @heading-2-size;
|
||||||
|
@modal-header-title-line-height: @heading-2-line-height;
|
||||||
|
|
||||||
.flipperlegacy_design {
|
.flipperlegacy_design {
|
||||||
// Prevents ANT breaking global styles implicitly used by old Flipper design
|
// Prevents ANT breaking global styles implicitly used by old Flipper design
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user