Summary: Currently all of the modals are displayed using default positioning, which works fine for small modals and large app windows, but it cause small UI/UX issues (double scroll bars) when app window is quite small. Adding [`centered` prop](https://ant.design/components/modal/#API) for variable height modals improves this situation a bit, but it also changes a bit the modal position when using maximised window, but I think this is bearable looking at improvements gained when window size is reduced. ## Changelog * use `centered` prop for variable height modals to improve UI in small app window Pull Request resolved: https://github.com/facebook/flipper/pull/3334 Test Plan: The change has been testes by running the desktop Flipper app locally from source. ## Preview (before & after) #### Small App Window <img width="390" alt="Screenshot 2022-01-24 at 11 58 40" align="left" src="https://user-images.githubusercontent.com/719641/150771900-cb25d110-82c7-4ba9-8ee5-07a093c4f702.png"> <img width="390" alt="Screenshot 2022-01-24 at 11 58 37" src="https://user-images.githubusercontent.com/719641/150771911-c81592ac-1eba-4112-86ad-9549e6248239.png"> <img width="390" alt="Screenshot 2022-01-24 at 12 00 13" align="left" src="https://user-images.githubusercontent.com/719641/150772045-de37c432-4381-4207-8476-5f142dfb6fa5.png"> <img width="390" alt="Screenshot 2022-01-24 at 12 00 10" src="https://user-images.githubusercontent.com/719641/150772057-574cf6cd-6f1a-4ca2-a343-b8fd6342eddb.png"> #### Maximised App Window <img width="390" alt="Screenshot 2022-01-24 at 12 12 12" align="left" src="https://user-images.githubusercontent.com/719641/150772777-8ac3cb59-3b41-4dcb-9554-137e95857af8.png"> <img width="390" alt="Screenshot 2022-01-24 at 12 12 29" src="https://user-images.githubusercontent.com/719641/150772758-ca9f8c20-d6d0-412d-9067-7e756da0b56c.png"> Reviewed By: mweststrate Differential Revision: D33741484 Pulled By: lblasa fbshipit-source-id: 0c3ca883d051cf4fcce9f9c1b6688974b66fd0d8
280 lines
8.1 KiB
TypeScript
280 lines
8.1 KiB
TypeScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and 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,
|
|
} from '@ant-design/icons';
|
|
import {Layout, styled} from '../ui';
|
|
import {theme} from 'flipper-plugin';
|
|
import {
|
|
startHealthchecks,
|
|
updateHealthcheckResult,
|
|
finishHealthchecks,
|
|
acknowledgeProblems,
|
|
resetAcknowledgedProblems,
|
|
} from '../reducers/healthchecks';
|
|
import runHealthchecks from '../utils/runHealthchecks';
|
|
import type {FlipperDoctor} from 'flipper-common';
|
|
type Healthchecks = FlipperDoctor.Healthchecks;
|
|
import {reportUsage} from 'flipper-common';
|
|
|
|
const {Title, Paragraph, Text} = Typography;
|
|
|
|
const statusTypeAndMessage: {
|
|
[key in FlipperDoctor.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 discovered 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: FlipperDoctor.HealthcheckResult) {
|
|
return result.status === 'FAILED' || result.status === 'WARNING';
|
|
}
|
|
|
|
export function checkHasNewProblem(result: FlipperDoctor.HealthcheckResult) {
|
|
return checkHasProblem(result) && !result.isAcknowledged;
|
|
}
|
|
|
|
function ResultTopDialog(props: {status: FlipperDoctor.HealthcheckStatus}) {
|
|
const messages = statusTypeAndMessage[props.status];
|
|
return (
|
|
<Alert
|
|
type={messages.type}
|
|
showIcon
|
|
message={messages.message}
|
|
style={{
|
|
fontSize: theme.fontSize.small,
|
|
lineHeight: '16px',
|
|
fontWeight: 'bold',
|
|
paddingTop: '10px',
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function CheckIcon(props: {status: FlipperDoctor.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<FlipperDoctor.HealthcheckReportItem>;
|
|
}) {
|
|
return (
|
|
<Collapse ghost>
|
|
{props.checks.map((check) => (
|
|
<Collapse.Panel
|
|
key={check.key}
|
|
header={check.label}
|
|
extra={<CheckIcon status={check.result.status} />}>
|
|
<Paragraph>{check.result.message}</Paragraph>
|
|
</Collapse.Panel>
|
|
))}
|
|
</Collapse>
|
|
);
|
|
}
|
|
|
|
function HealthCheckList(props: {report: FlipperDoctor.HealthcheckReport}) {
|
|
useEffect(() => reportUsage('doctor:report:opened'), []);
|
|
return (
|
|
<Layout.Container gap>
|
|
<ResultTopDialog status={props.report.result.status} />
|
|
{Object.values(props.report.categories).map((category) => (
|
|
<Layout.Container key={category.key}>
|
|
<Title level={3}>{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.Container>
|
|
);
|
|
}
|
|
|
|
const FooterContainer = styled(Layout.Horizontal)({
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
});
|
|
|
|
function SetupDoctorFooter(props: {
|
|
onClose: () => void;
|
|
onRerunDoctor: () => Promise<void>;
|
|
showAcknowledgeCheckbox: boolean;
|
|
acknowledgeCheck: boolean;
|
|
onAcknowledgeCheck: (checked: boolean) => void;
|
|
disableRerun: boolean;
|
|
}) {
|
|
return (
|
|
<Layout.Right>
|
|
{props.showAcknowledgeCheckbox ? (
|
|
<FooterContainer>
|
|
<Checkbox
|
|
checked={props.acknowledgeCheck}
|
|
onChange={(e) => props.onAcknowledgeCheck(e.target.checked)}>
|
|
<Text style={{fontSize: theme.fontSize.small}}>
|
|
Do not show warning about these problems at startup
|
|
</Text>
|
|
</Checkbox>
|
|
</FooterContainer>
|
|
) : (
|
|
<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: FlipperDoctor.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
|
|
centered
|
|
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>
|
|
);
|
|
}
|