From 2b6bac42279ff9e31e21549d86404f6cbd27d66d Mon Sep 17 00:00:00 2001 From: Chaiwat Ekkaewnumchai Date: Wed, 7 Oct 2020 08:46:21 -0700 Subject: [PATCH] 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 --- desktop/app/src/sandy-chrome/LeftRail.tsx | 39 ++- .../src/sandy-chrome/SetupDoctorScreen.tsx | 301 ++++++++++++++++++ desktop/themes/base.less | 6 + desktop/themes/typography.less | 4 + 4 files changed, 342 insertions(+), 8 deletions(-) create mode 100644 desktop/app/src/sandy-chrome/SetupDoctorScreen.tsx diff --git a/desktop/app/src/sandy-chrome/LeftRail.tsx b/desktop/app/src/sandy-chrome/LeftRail.tsx index 0ad429147..20f84a589 100644 --- a/desktop/app/src/sandy-chrome/LeftRail.tsx +++ b/desktop/app/src/sandy-chrome/LeftRail.tsx @@ -7,7 +7,7 @@ * @format */ -import React, {cloneElement, useState, useCallback} from 'react'; +import React, {cloneElement, useState, useCallback, useMemo} from 'react'; import {styled, Layout} from 'flipper'; import {Button, Divider, Badge, Tooltip, Avatar, Popover} from 'antd'; import { @@ -25,6 +25,7 @@ import {SidebarLeft, SidebarRight} from './SandyIcons'; import {useDispatch, useStore} from '../utils/useStore'; import {toggleLeftSidebarVisible} from '../reducers/application'; import {theme} from './theme'; +import SetupDoctorScreen, {checkHasNewProblem} from './SetupDoctorScreen'; import SettingsSheet from '../chrome/SettingsSheet'; import WelcomeScreen from './WelcomeScreen'; import SignInSheet from '../chrome/SignInSheet'; @@ -56,14 +57,19 @@ function LeftRailButton({ small?: boolean; toggled?: boolean; selected?: boolean; // TODO: make sure only one element can be selected - count?: number; + count?: number | true; title: string; onClick?: React.MouseEventHandler; }) { let iconElement = icon && cloneElement(icon, {style: {fontSize: small ? 16 : 24}}); if (count !== undefined) { - iconElement = {iconElement}; + iconElement = + count === true ? ( + {iconElement} + ) : ( + {iconElement} + ); } return ( @@ -113,11 +119,7 @@ export function LeftRail({ /> - } - small - title="Setup Doctor" - /> + state.healthchecks.healthcheckReport.result, + ); + const hasNewProblem = useMemo(() => checkHasNewProblem(result), [result]); + const onClose = useCallback(() => setVisible(false), []); + return ( + <> + } + small + title="Setup Doctor" + count={hasNewProblem ? true : undefined} + onClick={() => setVisible(true)} + /> + + + ); +} + function ShowSettingsButton() { const [showSettings, setShowSettings] = useState(false); const onClose = useCallback(() => setShowSettings(false), []); diff --git a/desktop/app/src/sandy-chrome/SetupDoctorScreen.tsx b/desktop/app/src/sandy-chrome/SetupDoctorScreen.tsx new file mode 100644 index 000000000..1b1604680 --- /dev/null +++ b/desktop/app/src/sandy-chrome/SetupDoctorScreen.tsx @@ -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 ( + + ); +} + +function CheckIcon(props: {status: HealthcheckStatus}) { + switch (props.status) { + case 'SUCCESS': + return ( + + ); + case 'FAILED': + return ( + + ); + case 'WARNING': + return ( + + ); + case 'SKIPPED': + return ( + + ); + case 'IN_PROGRESS': + return ( + + ); + } +} + +function CollapsableCategory(props: {checks: Array}) { + return ( + + isActive ? : + } + bordered={false} + style={{backgroundColor: theme.backgroundDefault, border: 0}}> + {props.checks.map((check) => ( + + + + {check.label} + + + } + style={borderStyle}> + + {check.result.message} + + + ))} + + ); +} + +function HealthCheckList(props: {report: HealthcheckReport}) { + useEffect(() => reportUsage('doctor:report:opened'), []); + return ( + + + {Object.values(props.report.categories).map((category) => ( + + + {category.label} + + + + ))} + + ); +} + +function SetupDoctorFooter(props: { + onClose: () => void; + onRerunDoctor: () => Promise; + showAcknowledgeCheckbox: boolean; + acknowledgeCheck: boolean; + onAcknowledgeCheck: (checked: boolean) => void; + disableRerun: boolean; +}) { + return ( + + {props.showAcknowledgeCheckbox ? ( + props.onAcknowledgeCheck(e.target.checked)} + style={{display: 'flex', alignItems: 'center'}}> + + Do not show warning about these problems at startup + + + ) : ( + + )} + + + + + + ); +} + +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 ( + setAcknowlodgeProblem(checked)} + disableRerun={healthcheckReport.result.status === 'IN_PROGRESS'} + /> + } + onCancel={onCloseModal}> + + + ); +} diff --git a/desktop/themes/base.less b/desktop/themes/base.less index bb0b8bef5..b4f6f503e 100644 --- a/desktop/themes/base.less +++ b/desktop/themes/base.less @@ -18,6 +18,12 @@ @code-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, 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 (as far as they aren't already) diff --git a/desktop/themes/typography.less b/desktop/themes/typography.less index 90d671ace..4cb53b494 100644 --- a/desktop/themes/typography.less +++ b/desktop/themes/typography.less @@ -73,6 +73,10 @@ @link-hover: @primary-color; @link-hover-decoration: underline; +// Modal +@modal-header-title-font-size: @heading-2-size; +@modal-header-title-line-height: @heading-2-line-height; + .flipperlegacy_design { // Prevents ANT breaking global styles implicitly used by old Flipper design line-height: 1;