openreplay/frontend/app/components/UsabilityTesting/TestOverview.tsx
2023-12-04 10:05:59 +01:00

404 lines
14 KiB
TypeScript

import { durationFormatted } from 'App/date';
import usePageTitle from 'App/hooks/usePageTitle';
import { numberWithCommas } from 'App/utils';
import { getPdf2 } from 'Components/AssistStats/pdfGenerator';
import { useModal } from 'Components/Modal';
import LiveTestsModal from "Components/UsabilityTesting/LiveTestsModal";
import React from 'react';
import { Button, Typography, Select, Space, Popover, Dropdown } from 'antd';
import { withSiteId, usabilityTesting, usabilityTestingEdit } from 'App/routes';
import { useParams, useHistory } from 'react-router-dom';
import Breadcrumb from 'Shared/Breadcrumb';
import { observer } from 'mobx-react-lite';
import { useStore } from 'App/mstore';
import {
EditOutlined,
ShareAltOutlined,
ArrowRightOutlined,
MoreOutlined,
UserOutlined,
UserDeleteOutlined,
CheckCircleOutlined,
FastForwardOutlined,
PauseCircleOutlined,
StopOutlined,
HourglassOutlined,
FilePdfOutlined,
DeleteOutlined,
ClockCircleOutlined,
} from '@ant-design/icons';
import SessionItem from 'Shared/SessionItem';
import { Loader, NoContent, Pagination } from 'UI';
import copy from 'copy-to-clipboard';
import { Stage } from 'Components/Funnels/FunnelWidget/FunnelWidget';
import { confirm } from 'UI';
import ResponsesOverview from './ResponsesOverview';
const { Option } = Select;
const statusItems = [
{ value: 'in-progress', label: 'Ongoing', icon: <HourglassOutlined rev={undefined} /> },
{ value: 'paused', label: 'On Hold', icon: <PauseCircleOutlined rev={undefined} /> },
{ value: 'closed', label: 'Closed', icon: <StopOutlined rev={undefined} /> },
];
const menuItems = [
{
key: '1',
label: 'Download Results',
icon: <FilePdfOutlined rev={undefined} />,
},
{
key: '2',
label: 'Edit',
icon: <EditOutlined rev={undefined} />,
},
{
key: '3',
label: 'Delete',
icon: <DeleteOutlined rev={undefined} />,
},
];
function TestOverview() {
// @ts-ignore
const { siteId, testId } = useParams();
const { showModal, hideModal } = useModal();
const { uxtestingStore } = useStore();
usePageTitle(`Usability Tests | ${uxtestingStore.instance?.title || ''}`);
React.useEffect(() => {
uxtestingStore.getTest(testId);
}, [testId]);
if (!uxtestingStore.instance) {
return <Loader loading={uxtestingStore.isLoading}>No data.</Loader>;
}
const onPageChange = (page: number) => {
uxtestingStore.setSessionsPage(page);
};
return (
<div className="w-full mx-auto" style={{ maxWidth: '1360px' }}>
<Breadcrumb
items={[
{
label: 'Usability Testing',
to: withSiteId(usabilityTesting(), siteId),
},
{
label: uxtestingStore.instance.title,
},
]}
/>
<div className={'rounded border bg-white'} id={'pdf-anchor'}>
<Title testId={testId} siteId={siteId} />
{uxtestingStore.instance.liveCount ? (
<div className={'p-4 flex items-center gap-2'}>
<div className={'relative h-4 w-4'}>
<div className={'absolute w-4 h-4 animate-ping bg-red rounded-full opacity-75'} />
<div className={'absolute w-4 h-4 bg-red rounded-full'} />
</div>
<Typography.Text>
{uxtestingStore.instance.liveCount} participants are engaged in this usability test at
the moment.
</Typography.Text>
<Button onClick={() => {
showModal(<LiveTestsModal closeModal={hideModal} testId={testId} />, { right: true, width: 900 })
}}>
<Space align={'center'}>
Moderate Real-Time
<ArrowRightOutlined rev={undefined} />
</Space>
</Button>
</div>
) : null}
</div>
<ParticipantOverview />
<TaskSummary />
<div className={'mt-2 rounded border p-4 bg-white flex gap-2 items-center'}>
<Typography.Title style={{ marginBottom: 0 }} level={5}>
Open-ended task responses
</Typography.Title>
{uxtestingStore.instance.responsesCount ? (
<Button onClick={() => showModal(<ResponsesOverview />, { right: true, width: 900 })}>
<Space align={'center'}>
Review All {uxtestingStore.instance.responsesCount} Responses
<ArrowRightOutlined rev={undefined} />
</Space>
</Button>
) : (
<Typography.Text>0 at the moment.</Typography.Text>
)}
</div>
<div className={'mt-2 rounded border p-4 bg-white flex gap-1 items-start flex-col'}>
<Typography.Title style={{ marginBottom: 0 }} level={5}>
Sessions
</Typography.Title>
{/*<Typography.Text>in your selection</Typography.Text>*/}
{/*<div className={'flex gap-1 link'}>clear selection</div>*/}
<div className={'flex flex-col w-full'}>
<Loader loading={uxtestingStore.isLoading}>
<NoContent show={uxtestingStore.testSessions.list.length == 0} title="No data">
{uxtestingStore.testSessions.list.map((session) => (
// @ts-ignore
<SessionItem session={session} query={'?utx=true'} />
))}
<div className={'flex items-center justify-between'}>
<div>
Showing{' '}
<span className="font-medium">
{(uxtestingStore.testSessions.page - 1) * 10 + 1}
</span>{' '}
to{' '}
<span className="font-medium">
{(uxtestingStore.page - 1) * 10 + uxtestingStore.testSessions.list.length}
</span>{' '}
of{' '}
<span className="font-medium">
{numberWithCommas(uxtestingStore.testSessions.total)}
</span>{' '}
tests.
</div>
<Pagination
page={uxtestingStore.testSessions.page}
totalPages={Math.ceil(uxtestingStore.testSessions.total / 10)}
onPageChange={onPageChange}
limit={10}
debounceRequest={200}
/>
</div>
</NoContent>
</Loader>
</div>
</div>
</div>
);
}
const ParticipantOverview = observer(() => {
const { uxtestingStore } = useStore();
return (
<div className={'p-4 rounded border bg-white mt-2'}>
<Typography.Title level={5}>Participant Overview</Typography.Title>
{uxtestingStore.testStats ? (
<div className={'flex gap-4'}>
<div className={'rounded border p-2 flex-1'}>
<div className={'flex items-center gap-2 mb-2'}>
<UserOutlined style={{ fontSize: 18, color: '#394EFF' }} rev={undefined} />
<Typography.Text strong>Total Participants</Typography.Text>
</div>
<Typography.Title level={4}>{uxtestingStore.testStats.tests_attempts}</Typography.Title>
</div>
<div className={'rounded border p-2 flex-1'}>
<div className={'flex items-center gap-2 mb-2'}>
<CheckCircleOutlined style={{ fontSize: 18, color: '#389E0D' }} rev={undefined} />
<Typography.Text strong>Completed all tasks</Typography.Text>
</div>
<div className={'flex items-center gap-2'}>
{uxtestingStore.testStats.tests_attempts > 0 ? (
<Typography.Title level={4}>
{Math.round(
(uxtestingStore.testStats.completed_all_tasks /
uxtestingStore.testStats.tests_attempts) *
100
)}
%
</Typography.Title>
) : null}
<Typography.Text>{uxtestingStore.testStats.completed_all_tasks}</Typography.Text>
</div>
</div>
<div className={'rounded border p-2 flex-1'}>
<div className={'flex items-center gap-2 mb-2'}>
<FastForwardOutlined style={{ fontSize: 18, color: '#874D00' }} rev={undefined} />
<Typography.Text strong>Skipped tasks</Typography.Text>
</div>
<div className={'flex items-center gap-2'}>
{uxtestingStore.testStats.tests_attempts > 0 ? (
<Typography.Title level={4}>
{Math.round(
(uxtestingStore.testStats.tasks_skipped /
uxtestingStore.testStats.tests_attempts) *
100
)}
%
</Typography.Title>
) : null}
<Typography.Text>{uxtestingStore.testStats.tasks_skipped}</Typography.Text>
</div>
</div>
<div className={'rounded border p-2 flex-1'}>
<div className={'flex items-center gap-2 mb-2'}>
<UserDeleteOutlined style={{ fontSize: 18, color: '#CC0000' }} rev={undefined} />
<Typography.Text strong>Aborted the test</Typography.Text>
</div>
<div className={'flex items-center gap-2'}>
{uxtestingStore.testStats.tests_attempts > 0 ? (
<Typography.Title level={4}>
{Math.round(
(uxtestingStore.testStats.tests_skipped /
uxtestingStore.testStats.tests_attempts) *
100
)}
%
</Typography.Title>
) : null}
<Typography.Text>{uxtestingStore.testStats.tests_skipped}</Typography.Text>
</div>
</div>
</div>
) : null}
</div>
);
});
const TaskSummary = observer(() => {
const { uxtestingStore } = useStore();
return (
<div className={'mt-2 rounded border p-4 bg-white'}>
<div className={'flex justify-between items-center'}>
<Typography.Title level={5}>Task Summary</Typography.Title>
{uxtestingStore.taskStats.length ? (
<div className={'p-2 rounded bg-teal-light flex items-center gap-1'}>
<Typography.Text>Average completion time for all tasks:</Typography.Text>
<Typography.Text strong>
{uxtestingStore.taskStats
? durationFormatted(
uxtestingStore.taskStats.reduce(
(stats, task) => stats + task.avgCompletionTime,
0
) / uxtestingStore.taskStats.length
)
: null}
</Typography.Text>
<ClockCircleOutlined rev={undefined} />
</div>
) : null}
</div>
{!uxtestingStore.taskStats.length ? <NoContent show title={'No data'} /> : null}
{uxtestingStore.taskStats.map((tst, index) => (
<Stage stage={{ ...tst, isActive: true }} uxt index={index + 1} />
))}
</div>
);
});
const Title = observer(({ testId, siteId }: any) => {
const [truncate, setTruncate] = React.useState(true);
const { uxtestingStore } = useStore();
const history = useHistory();
const handleChange = (value: string) => {
uxtestingStore.updateTestStatus(value);
};
const onMenuClick = async ({ key }: any) => {
if (key === '1') {
void getPdf2();
}
if (key === '2') {
await redirectToEdit();
}
if (key === '3') {
if (
await confirm({
confirmation:
'Are you sure you want to delete this usability test? This action cannot be undone.',
})
) {
uxtestingStore.deleteTest(testId).then(() => {
history.push(withSiteId(usabilityTesting(), siteId));
});
}
}
};
if (!uxtestingStore.instance) {
return null;
}
const truncatedDescr = uxtestingStore.instance.description.length > 250 && truncate ? uxtestingStore.instance?.description.substring(0, 250) + '...' : uxtestingStore.instance?.description;
const redirectToEdit = async () => {
if (
await confirm({
confirmation:
'This test already has responses, making edits at this stage may result in confusing outcomes.',
confirmButton: 'Edit',
})
) {
history.push(withSiteId(usabilityTestingEdit(testId), siteId));
}
};
return (
<div className={'p-4 border-b'}>
<div className={'flex items-center gap-2'}>
<Typography.Title level={4}>{uxtestingStore.instance!.title}</Typography.Title>
<div className={'ml-auto'} />
<Select
value={uxtestingStore.instance!.status}
style={{ width: 150 }}
onChange={handleChange}
>
{statusItems.map((item) => (
<Option key={item.value} value={item.value} label={item.label}>
<Space align={'center'}>
{item.icon} {item.label}
</Space>
</Option>
))}
</Select>
<Button type={'primary'} onClick={redirectToEdit}>
<Space align={'center'}>
{uxtestingStore.instance!.tasks.length} Tasks <EditOutlined rev={undefined} />{' '}
</Space>
</Button>
<Popover
trigger={'click'}
title={'Participants Link'}
content={
<div style={{ width: '220px' }}>
<div className={'p-2 bg-white rounded border break-all mb-2'}>
{`${uxtestingStore.instance!.startingPath}?oruxt=${
uxtestingStore.instance!.testId
}`}
</div>
<Button
onClick={() => {
copy(
`${uxtestingStore.instance!.startingPath}?oruxt=${
uxtestingStore.instance!.testId
}`
);
}}
>
Copy
</Button>
</div>
}
>
<Button type={'primary'} ghost>
<Space align={'center'}>
Distribute
<ShareAltOutlined rev={undefined} />
</Space>
</Button>
</Popover>
<Dropdown menu={{ items: menuItems, onClick: onMenuClick }}>
<Button icon={<MoreOutlined rev={undefined} />}></Button>
</Dropdown>
</div>
<div className={'whitespace-pre-wrap'}>
{truncatedDescr}
</div>
{uxtestingStore.instance.description.length > 250 ? (<div className={'link'} onClick={() => setTruncate(!truncate)}>{truncate ? 'Show more' : 'Show less'}</div>) : null}
</div>
);
});
export default observer(TestOverview);