mirror of
https://github.com/web-infra-dev/midscene.git
synced 2025-12-29 16:09:35 +00:00
feat(chrome-extension): enhance recording UI and session management
- Simplified the Record component structure by separating list and detail views for better user experience. - Improved styling for the recording sessions list and detail views, ensuring better responsiveness and usability. - Added functionality to switch between list and detail views for recording sessions. - Updated session management to allow for viewing, editing, and deleting sessions with improved user feedback. - Enhanced event handling and display for recorded events within the detail view.
This commit is contained in:
parent
cd293fcf53
commit
0ee39bd9a6
@ -52,9 +52,7 @@ export function PlaygroundPopup() {
|
||||
label: 'Record',
|
||||
icon: <VideoCameraOutlined />,
|
||||
children: (
|
||||
<div className="popup-record-container">
|
||||
<Record />
|
||||
</div>
|
||||
<Record />
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@ -2,36 +2,322 @@
|
||||
min-height: 500px;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden; // 禁用横向滚动条
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
|
||||
// 统一滚动条样式
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background: #a1a1a1;
|
||||
}
|
||||
}
|
||||
|
||||
// 防止所有子元素超出容器宽度
|
||||
* {
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
// View-specific styles
|
||||
.record-list-view {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
|
||||
.session-list {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
|
||||
.ant-list-item {
|
||||
padding: 8px 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
|
||||
.ant-card {
|
||||
transition: all 0.2s ease;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&.selected-session {
|
||||
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.15);
|
||||
|
||||
.ant-card-head {
|
||||
background-color: #f0f7ff;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-card-actions {
|
||||
background-color: #fafafa;
|
||||
|
||||
.ant-btn {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
|
||||
&:hover {
|
||||
background-color: #e6f7ff;
|
||||
}
|
||||
|
||||
&.ant-btn-dangerous:hover {
|
||||
background-color: #fff2f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.session-meta {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
|
||||
.session-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.session-details {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
line-height: 1.4;
|
||||
word-break: break-all;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.session-empty {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
|
||||
.ant-empty-description {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.record-detail-view {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
|
||||
// Header section
|
||||
.detail-header {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.back-button {
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
margin-top: 4px; // 对齐标题
|
||||
}
|
||||
|
||||
.session-title {
|
||||
flex: 1;
|
||||
min-width: 0; // 允许文本截断
|
||||
max-width: calc(100% - 120px); // 为返回按钮留出空间
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
margin-bottom: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ant-typography {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recording status section
|
||||
.recording-status {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin-bottom: 16px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
// Session info card
|
||||
.session-info-card {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.ant-card-body {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
// Controls section
|
||||
.controls-section {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.current-tab-info {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin-bottom: 12px;
|
||||
word-break: break-all;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.record-controls {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.ant-btn {
|
||||
flex-shrink: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Events section
|
||||
.events-section {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
|
||||
.events-header {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin-bottom: 12px;
|
||||
|
||||
h5 {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.events-container {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
|
||||
&.empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
min-height: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.session-info {
|
||||
.ant-space {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
|
||||
.ant-typography {
|
||||
margin: 0;
|
||||
word-break: break-all;
|
||||
white-space: normal;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.record-timeline {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
// 移除内部滚动条和固定高度,让内容自然展开
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 16px;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 6px;
|
||||
background-color: #fafafa;
|
||||
overflow: visible;
|
||||
box-sizing: border-box;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 3px;
|
||||
// 确保内部组件也不产生滚动条并充分利用宽度
|
||||
.ant-timeline {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow: visible;
|
||||
|
||||
&:hover {
|
||||
background: #a1a1a1;
|
||||
.ant-timeline-item {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
|
||||
.ant-timeline-item-content {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-card {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow: visible;
|
||||
box-sizing: border-box;
|
||||
|
||||
.ant-card-body {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 12px 16px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-space {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
|
||||
.ant-space-item {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.record-event {
|
||||
transition: all 0.2s ease;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
|
||||
&:hover {
|
||||
transform: translateX(2px);
|
||||
@ -44,9 +330,12 @@
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
|
||||
.ant-btn {
|
||||
flex-shrink: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,71 +347,8 @@
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid #1890ff;
|
||||
}
|
||||
|
||||
// Session management styles
|
||||
.session-list {
|
||||
.ant-list-item {
|
||||
padding: 8px 0;
|
||||
|
||||
.ant-card {
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&.selected-session {
|
||||
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.15);
|
||||
|
||||
.ant-card-head {
|
||||
background-color: #f0f7ff;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-card-actions {
|
||||
background-color: #fafafa;
|
||||
|
||||
.ant-btn {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
|
||||
&:hover {
|
||||
background-color: #e6f7ff;
|
||||
}
|
||||
|
||||
&.ant-btn-dangerous:hover {
|
||||
background-color: #fff2f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.session-meta {
|
||||
.session-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.session-details {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.session-empty {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
|
||||
.ant-empty-description {
|
||||
color: #999;
|
||||
}
|
||||
word-break: break-all;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
// Recording status indicator
|
||||
@ -192,36 +418,170 @@
|
||||
margin: 24px 0;
|
||||
border-color: #e8e8e8;
|
||||
}
|
||||
|
||||
// Events section
|
||||
.events-section {
|
||||
.events-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
// 全局禁用可能产生滚动条的 Ant Design 组件
|
||||
.ant-list {
|
||||
overflow: visible !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.ant-list-item {
|
||||
overflow: visible !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.ant-card {
|
||||
overflow: visible !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
overflow: visible !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.ant-timeline {
|
||||
overflow: visible !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.ant-timeline-item {
|
||||
overflow: visible !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.ant-space {
|
||||
overflow: visible !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.ant-empty {
|
||||
overflow: visible !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
// 防止文本和 URL 导致横向滚动
|
||||
.ant-typography {
|
||||
word-break: break-all !important;
|
||||
white-space: normal !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
// 确保输入框和按钮不超出容器
|
||||
.ant-input {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.ant-btn {
|
||||
max-width: 100% !important;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
// 特别针对 RecordTimeline 的样式优化
|
||||
.record-timeline {
|
||||
overflow: visible !important;
|
||||
max-width: 100% !important;
|
||||
|
||||
* {
|
||||
overflow: visible !important;
|
||||
box-sizing: border-box;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
// 如果内容很长,确保可以完整显示
|
||||
.ant-timeline-item-content {
|
||||
overflow: visible !important;
|
||||
word-break: break-word;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
// 确保时间轴项目充分利用宽度
|
||||
.ant-timeline-item {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
// 卡片样式优化
|
||||
.ant-card {
|
||||
margin-bottom: 8px;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
|
||||
.events-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.events-meta {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
.ant-card-body {
|
||||
padding: 12px 16px;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.events-container {
|
||||
min-height: 200px;
|
||||
// 特别处理长文本和 URL
|
||||
.ant-space-item {
|
||||
word-break: break-all;
|
||||
white-space: normal;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
// Timeline 组件内部的 Space 组件优化
|
||||
.ant-space {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
|
||||
&.empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
.ant-space-item {
|
||||
max-width: 100% !important;
|
||||
word-break: break-all !important;
|
||||
white-space: normal !important;
|
||||
overflow-wrap: break-word !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 RecordTimeline 内部的 Typography 组件
|
||||
.ant-typography {
|
||||
word-break: break-all !important;
|
||||
white-space: normal !important;
|
||||
max-width: 100% !important;
|
||||
overflow-wrap: break-word !important;
|
||||
}
|
||||
|
||||
// 特别处理可能的长 URL 显示
|
||||
.ant-typography[type="secondary"] {
|
||||
word-break: break-all !important;
|
||||
white-space: normal !important;
|
||||
max-width: 100% !important;
|
||||
overflow-wrap: anywhere !important;
|
||||
}
|
||||
|
||||
// 处理 Tag 组件
|
||||
.ant-tag {
|
||||
max-width: 100% !important;
|
||||
word-break: break-all !important;
|
||||
white-space: normal !important;
|
||||
}
|
||||
|
||||
// 处理 Button 组件
|
||||
.ant-btn {
|
||||
max-width: 100% !important;
|
||||
word-break: break-all !important;
|
||||
}
|
||||
|
||||
// 处理 Code 文本
|
||||
code {
|
||||
word-break: break-all !important;
|
||||
white-space: normal !important;
|
||||
max-width: 100% !important;
|
||||
overflow-wrap: break-word !important;
|
||||
}
|
||||
|
||||
// 处理可能的固定宽度元素
|
||||
img {
|
||||
max-width: 100% !important;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
// 确保所有文本内容都能正确换行
|
||||
* {
|
||||
word-wrap: break-word !important;
|
||||
overflow-wrap: break-word !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
/// <reference types="chrome" />
|
||||
import { DeleteOutlined, DownloadOutlined, EditOutlined, PlayCircleOutlined, PlusOutlined, StopOutlined } from '@ant-design/icons';
|
||||
import { ArrowLeftOutlined, DeleteOutlined, DownloadOutlined, EditOutlined, PlayCircleOutlined, PlusOutlined, StopOutlined } from '@ant-design/icons';
|
||||
import { RecordTimeline } from '@midscene/record';
|
||||
import { Alert, Button, Card, Divider, Empty, Form, Input, List, Modal, Popconfirm, Space, Tag, Typography, message } from 'antd';
|
||||
import type React from 'react';
|
||||
@ -15,6 +15,308 @@ interface RecordMessage {
|
||||
data?: RecordedEvent | RecordedEvent[];
|
||||
}
|
||||
|
||||
// View modes
|
||||
type ViewMode = 'list' | 'detail';
|
||||
|
||||
// Record List Component
|
||||
const RecordList: React.FC<{
|
||||
sessions: RecordingSession[];
|
||||
currentSessionId: string | null;
|
||||
onCreateSession: () => void;
|
||||
onEditSession: (session: RecordingSession) => void;
|
||||
onDeleteSession: (sessionId: string) => void;
|
||||
onSelectSession: (session: RecordingSession) => void;
|
||||
onExportSession: (session: RecordingSession) => void;
|
||||
onViewDetail: (session: RecordingSession) => void;
|
||||
}> = ({
|
||||
sessions,
|
||||
currentSessionId,
|
||||
onCreateSession,
|
||||
onEditSession,
|
||||
onDeleteSession,
|
||||
onSelectSession,
|
||||
onExportSession,
|
||||
onViewDetail
|
||||
}) => {
|
||||
return (
|
||||
<div className="record-list-view">
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
|
||||
<Title level={3} style={{ margin: 0 }}>Recording Sessions</Title>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={onCreateSession}
|
||||
>
|
||||
New Session
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{sessions.length === 0 ? (
|
||||
<div className="session-empty">
|
||||
<Empty
|
||||
description="No recording sessions yet"
|
||||
style={{ margin: '40px 0' }}
|
||||
>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={onCreateSession}
|
||||
>
|
||||
Create First Session
|
||||
</Button>
|
||||
</Empty>
|
||||
</div>
|
||||
) : (
|
||||
<List
|
||||
className="session-list"
|
||||
grid={{ gutter: 16, column: 1 }}
|
||||
dataSource={sessions}
|
||||
renderItem={(session) => (
|
||||
<List.Item>
|
||||
<Card
|
||||
size="small"
|
||||
className={session.id === currentSessionId ? 'selected-session' : ''}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
border: session.id === currentSessionId ? '2px solid #1890ff' : '1px solid #d9d9d9'
|
||||
}}
|
||||
onClick={() => onViewDetail(session)}
|
||||
actions={[
|
||||
<Button
|
||||
key="select"
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSelectSession(session);
|
||||
}}
|
||||
style={{ color: session.id === currentSessionId ? '#1890ff' : undefined }}
|
||||
>
|
||||
{session.id === currentSessionId ? 'Selected' : 'Select'}
|
||||
</Button>,
|
||||
<Button
|
||||
key="edit"
|
||||
type="text"
|
||||
icon={<EditOutlined />}
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEditSession(session);
|
||||
}}
|
||||
/>,
|
||||
<Button
|
||||
key="download"
|
||||
type="text"
|
||||
icon={<DownloadOutlined />}
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onExportSession(session);
|
||||
}}
|
||||
disabled={session.events.length === 0}
|
||||
/>,
|
||||
<Popconfirm
|
||||
key="delete"
|
||||
title="Delete session"
|
||||
description="Are you sure you want to delete this session?"
|
||||
onConfirm={(e) => {
|
||||
e?.stopPropagation();
|
||||
onDeleteSession(session.id);
|
||||
}}
|
||||
onCancel={(e) => e?.stopPropagation()}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
size="small"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</Popconfirm>
|
||||
]}
|
||||
>
|
||||
<Card.Meta
|
||||
title={
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<span>{session.name}</span>
|
||||
<Space>
|
||||
<Tag color={session.status === 'recording' ? 'red' : session.status === 'completed' ? 'green' : 'default'}>
|
||||
{session.status}
|
||||
</Tag>
|
||||
{session.id === currentSessionId && (
|
||||
<Tag color="blue">Current</Tag>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
}
|
||||
description={
|
||||
<div className="session-meta">
|
||||
{session.description && <div style={{ marginBottom: '4px' }}>{session.description}</div>}
|
||||
<div className="session-details">
|
||||
Events: {session.events.length} |
|
||||
Created: {new Date(session.createdAt).toLocaleString()} |
|
||||
{session.duration && ` Duration: ${(session.duration / 1000).toFixed(1)}s |`}
|
||||
{session.url && ` URL: ${session.url.slice(0, 50)}${session.url.length > 50 ? '...' : ''}`}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Record Detail Component
|
||||
const RecordDetail: React.FC<{
|
||||
session: RecordingSession;
|
||||
events: RecordedEvent[];
|
||||
isRecording: boolean;
|
||||
currentTab: chrome.tabs.Tab | null;
|
||||
onBack: () => void;
|
||||
onStartRecording: () => void;
|
||||
onStopRecording: () => void;
|
||||
onClearEvents: () => void;
|
||||
onExportEvents: () => void;
|
||||
}> = ({
|
||||
session,
|
||||
events,
|
||||
isRecording,
|
||||
currentTab,
|
||||
onBack,
|
||||
onStartRecording,
|
||||
onStopRecording,
|
||||
onClearEvents,
|
||||
onExportEvents
|
||||
}) => {
|
||||
return (
|
||||
<div className="record-detail-view">
|
||||
{/* Header with back button and session info */}
|
||||
<div className="detail-header">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={onBack}
|
||||
className="back-button"
|
||||
>
|
||||
Back to Sessions
|
||||
</Button>
|
||||
<div className="session-title">
|
||||
<Title level={4}>{session.name}</Title>
|
||||
<Text type="secondary">{session.description}</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recording Status Indicator */}
|
||||
<div className={`recording-status ${isRecording ? 'recording' : 'idle'}`}>
|
||||
{isRecording ? (
|
||||
<span>🔴 Recording in progress</span>
|
||||
) : (
|
||||
<span>✅ Ready to record</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Session Details */}
|
||||
<Card size="small" className="session-info-card">
|
||||
<div className="session-info">
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
<div>
|
||||
<Text strong>Status: </Text>
|
||||
<Tag color={session.status === 'recording' ? 'red' : session.status === 'completed' ? 'green' : 'default'}>
|
||||
{session.status}
|
||||
</Tag>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong>Events: </Text>
|
||||
<Text>{session.events.length}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong>Created: </Text>
|
||||
<Text>{new Date(session.createdAt).toLocaleString()}</Text>
|
||||
</div>
|
||||
{session.duration && (
|
||||
<div>
|
||||
<Text strong>Duration: </Text>
|
||||
<Text>{(session.duration / 1000).toFixed(1)}s</Text>
|
||||
</div>
|
||||
)}
|
||||
{session.url && (
|
||||
<div>
|
||||
<Text strong>URL: </Text>
|
||||
<Text>{session.url}</Text>
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Recording Controls */}
|
||||
<div className="controls-section">
|
||||
<div className="current-tab-info">
|
||||
<Text strong>Current Tab:</Text> {currentTab?.title || 'No tab selected'}
|
||||
</div>
|
||||
<Space className="record-controls">
|
||||
{!isRecording ? (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlayCircleOutlined />}
|
||||
onClick={onStartRecording}
|
||||
disabled={!currentTab}
|
||||
>
|
||||
Start Recording
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
danger
|
||||
icon={<StopOutlined />}
|
||||
onClick={onStopRecording}
|
||||
>
|
||||
Stop Recording
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={onClearEvents}
|
||||
disabled={events.length === 0 || isRecording}
|
||||
>
|
||||
Clear Events
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={onExportEvents}
|
||||
disabled={events.length === 0}
|
||||
>
|
||||
Export Events
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Events Display */}
|
||||
<div className="events-section">
|
||||
<div className="events-header">
|
||||
<Title level={5}>
|
||||
Recorded Events ({events.length})
|
||||
</Title>
|
||||
</div>
|
||||
<div className={`events-container ${events.length === 0 ? 'empty' : ''}`}>
|
||||
{events.length === 0 ? (
|
||||
<Empty description="No events recorded yet" />
|
||||
) : (
|
||||
<RecordTimeline events={events} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function Record() {
|
||||
const { isRecording, events, setIsRecording, addEvent, clearEvents, setEvents } = useRecordStore();
|
||||
const {
|
||||
@ -26,6 +328,11 @@ export default function Record() {
|
||||
setCurrentSession,
|
||||
getCurrentSession
|
||||
} = useRecordingSessionStore();
|
||||
|
||||
// View state management
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
||||
const [selectedSession, setSelectedSession] = useState<RecordingSession | null>(null);
|
||||
|
||||
const [currentTab, setCurrentTab] = useState<chrome.tabs.Tab | null>(null);
|
||||
const [isInjected, setIsInjected] = useState(false);
|
||||
const [isCreateModalVisible, setIsCreateModalVisible] = useState(false);
|
||||
@ -202,6 +509,10 @@ export default function Record() {
|
||||
setIsCreateModalVisible(false);
|
||||
form.resetFields();
|
||||
message.success(`Session "${values.name}" created successfully`);
|
||||
|
||||
// Switch to detail view for the new session
|
||||
setSelectedSession(newSession);
|
||||
setViewMode('detail');
|
||||
};
|
||||
|
||||
// Edit session
|
||||
@ -224,6 +535,16 @@ export default function Record() {
|
||||
updatedAt: Date.now()
|
||||
});
|
||||
|
||||
// Update selectedSession if it's the one being edited
|
||||
if (selectedSession?.id === editingSession.id) {
|
||||
setSelectedSession({
|
||||
...editingSession,
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
updatedAt: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
setIsEditModalVisible(false);
|
||||
setEditingSession(null);
|
||||
editForm.resetFields();
|
||||
@ -237,10 +558,15 @@ export default function Record() {
|
||||
setCurrentSession(null);
|
||||
clearEvents();
|
||||
}
|
||||
// If we're viewing the deleted session, go back to list
|
||||
if (selectedSession?.id === sessionId) {
|
||||
setViewMode('list');
|
||||
setSelectedSession(null);
|
||||
}
|
||||
message.success('Session deleted successfully');
|
||||
};
|
||||
|
||||
// Select session
|
||||
// Select session (set as current)
|
||||
const handleSelectSession = (session: RecordingSession) => {
|
||||
// Stop current recording if any
|
||||
if (isRecording) {
|
||||
@ -257,6 +583,23 @@ export default function Record() {
|
||||
message.success(`Switched to session "${session.name}"`);
|
||||
};
|
||||
|
||||
// View session detail
|
||||
const handleViewDetail = (session: RecordingSession) => {
|
||||
setSelectedSession(session);
|
||||
setViewMode('detail');
|
||||
|
||||
// If not already the current session, switch to it
|
||||
if (currentSessionId !== session.id) {
|
||||
handleSelectSession(session);
|
||||
}
|
||||
};
|
||||
|
||||
// Go back to list view
|
||||
const handleBackToList = () => {
|
||||
setViewMode('list');
|
||||
setSelectedSession(null);
|
||||
};
|
||||
|
||||
// Start recording
|
||||
const startRecording = async () => {
|
||||
// Check if there's a current session
|
||||
@ -338,6 +681,18 @@ export default function Record() {
|
||||
duration,
|
||||
updatedAt: Date.now()
|
||||
});
|
||||
|
||||
// Update selectedSession if it's the current one
|
||||
if (selectedSession?.id === currentSessionId) {
|
||||
setSelectedSession({
|
||||
...selectedSession,
|
||||
status: 'completed',
|
||||
events: [...events],
|
||||
duration,
|
||||
updatedAt: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
message.success(`Recording saved to session "${session.name}"`);
|
||||
}
|
||||
}
|
||||
@ -392,215 +747,34 @@ export default function Record() {
|
||||
message.success('Events exported successfully');
|
||||
};
|
||||
|
||||
const currentSession = getCurrentSession();
|
||||
|
||||
return (
|
||||
<div className="popup-record-container" style={{ padding: '16px' }}>
|
||||
<Alert
|
||||
message="Event Recording"
|
||||
description="Manage recording sessions and capture user interactions on the current tab."
|
||||
type="info"
|
||||
style={{ marginBottom: '16px' }}
|
||||
/>
|
||||
|
||||
{/* Recording Status Indicator */}
|
||||
{currentSession && (
|
||||
<div className={`recording-status ${isRecording ? 'recording' : 'idle'}`}>
|
||||
{isRecording ? (
|
||||
<span>🔴 Recording in progress in session "{currentSession.name}"</span>
|
||||
) : (
|
||||
<span>✅ Ready to record in session "{currentSession.name}"</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Session Management Section */}
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
|
||||
<Title level={4} style={{ margin: 0 }}>Recording Sessions</Title>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setIsCreateModalVisible(true)}
|
||||
>
|
||||
New Session
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{sessions.length === 0 ? (
|
||||
<div className="session-empty">
|
||||
<Empty
|
||||
description="No recording sessions yet"
|
||||
style={{ margin: '20px 0' }}
|
||||
>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setIsCreateModalVisible(true)}
|
||||
>
|
||||
Create First Session
|
||||
</Button>
|
||||
</Empty>
|
||||
</div>
|
||||
) : (
|
||||
<List
|
||||
className="session-list"
|
||||
grid={{ gutter: 16, column: 1 }}
|
||||
dataSource={sessions}
|
||||
renderItem={(session) => (
|
||||
<List.Item>
|
||||
<Card
|
||||
size="small"
|
||||
className={session.id === currentSessionId ? 'selected-session' : ''}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
border: session.id === currentSessionId ? '2px solid #1890ff' : '1px solid #d9d9d9'
|
||||
}}
|
||||
onClick={() => handleSelectSession(session)}
|
||||
actions={[
|
||||
<Button
|
||||
key="edit"
|
||||
type="text"
|
||||
icon={<EditOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleEditSession(session);
|
||||
}}
|
||||
/>,
|
||||
<Button
|
||||
key="download"
|
||||
type="text"
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
exportSessionEvents(session);
|
||||
}}
|
||||
disabled={session.events.length === 0}
|
||||
/>,
|
||||
<Popconfirm
|
||||
key="delete"
|
||||
title="Delete session"
|
||||
description="Are you sure you want to delete this session?"
|
||||
onConfirm={(e) => {
|
||||
e?.stopPropagation();
|
||||
handleDeleteSession(session.id);
|
||||
}}
|
||||
onCancel={(e) => e?.stopPropagation()}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</Popconfirm>
|
||||
]}
|
||||
>
|
||||
<Card.Meta
|
||||
title={
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<span>{session.name}</span>
|
||||
<Space>
|
||||
<Tag color={session.status === 'recording' ? 'red' : session.status === 'completed' ? 'green' : 'default'}>
|
||||
{session.status}
|
||||
</Tag>
|
||||
{session.id === currentSessionId && (
|
||||
<Tag color="blue">Current</Tag>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
}
|
||||
description={
|
||||
<div className="session-meta">
|
||||
{session.description && <div style={{ marginBottom: '4px' }}>{session.description}</div>}
|
||||
<div className="session-details">
|
||||
Events: {session.events.length} |
|
||||
Created: {new Date(session.createdAt).toLocaleString()} |
|
||||
{session.duration && ` Duration: ${(session.duration / 1000).toFixed(1)}s |`}
|
||||
{session.url && ` URL: ${session.url.slice(0, 50)}${session.url.length > 50 ? '...' : ''}`}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
</List.Item>
|
||||
)}
|
||||
<div className="popup-record-container">
|
||||
{viewMode === 'list' ? (
|
||||
<RecordList
|
||||
sessions={sessions}
|
||||
currentSessionId={currentSessionId}
|
||||
onCreateSession={() => setIsCreateModalVisible(true)}
|
||||
onEditSession={handleEditSession}
|
||||
onDeleteSession={handleDeleteSession}
|
||||
onSelectSession={handleSelectSession}
|
||||
onExportSession={exportSessionEvents}
|
||||
onViewDetail={handleViewDetail}
|
||||
/>
|
||||
) : (
|
||||
selectedSession && (
|
||||
<RecordDetail
|
||||
session={selectedSession}
|
||||
events={events}
|
||||
isRecording={isRecording}
|
||||
currentTab={currentTab}
|
||||
onBack={handleBackToList}
|
||||
onStartRecording={startRecording}
|
||||
onStopRecording={stopRecording}
|
||||
onClearEvents={clearEvents}
|
||||
onExportEvents={exportEvents}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Divider className="section-divider" />
|
||||
|
||||
{/* Recording Controls Section */}
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<div className="current-tab-info" style={{ marginBottom: '12px' }}>
|
||||
<Text strong>Current Tab:</Text> {currentTab?.title || 'No tab selected'}
|
||||
{currentSession && (
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Text strong>Active Session:</Text> {currentSession.name}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Space className="record-controls">
|
||||
{!isRecording ? (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlayCircleOutlined />}
|
||||
onClick={startRecording}
|
||||
disabled={!currentTab || !currentSessionId}
|
||||
>
|
||||
Start Recording
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
danger
|
||||
icon={<StopOutlined />}
|
||||
onClick={stopRecording}
|
||||
>
|
||||
Stop Recording
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={clearEvents}
|
||||
disabled={events.length === 0 || isRecording}
|
||||
>
|
||||
Clear Events
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={exportEvents}
|
||||
disabled={events.length === 0}
|
||||
>
|
||||
Export Current
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Divider className="section-divider" />
|
||||
|
||||
{/* Events Display Section */}
|
||||
<div className="events-section">
|
||||
<div className="events-header">
|
||||
<div className="events-title">
|
||||
Current Events ({events.length})
|
||||
</div>
|
||||
{currentSession && events.length > 0 && (
|
||||
<div className="events-meta">
|
||||
from session: {currentSession.name}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={`events-container ${events.length === 0 ? 'empty' : ''}`}>
|
||||
{events.length === 0 ? (
|
||||
<div>No events recorded yet</div>
|
||||
) : (
|
||||
<RecordTimeline events={events} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* Create Session Modal */}
|
||||
<Modal
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user