File size: 11,480 Bytes
a21c316
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
import React, { useState } from 'react';
import { useNetworkMonitorStore, NetworkRequest } from '../../stores/networkMonitorStore';
import { X, Play, Pause, Trash2, Activity, ChevronDown } from 'lucide-react';
import { useTranslation } from 'react-i18next';

const NetworkMonitor: React.FC = () => {
    const { requests, isOpen, setIsOpen, isRecording, toggleRecording, clearRequests } = useNetworkMonitorStore();
    const [selectedRequest, setSelectedRequest] = useState<NetworkRequest | null>(null);
    const { t } = useTranslation();

    // If not open, show a small floating button
    if (!isOpen) {
        return (
            <div className="fixed bottom-4 right-4 z-50">
                <button
                    onClick={() => setIsOpen(true)}
                    className="btn btn-circle btn-primary shadow-lg"
                    title={t('monitor.network.open', 'γƒγƒƒγƒˆγƒ―γƒΌγ‚―γƒ’γƒ‹γ‚ΏγƒΌγ‚’ι–‹γ')}
                >
                    <Activity size={24} />
                    {requests.filter(r => r.status === 'pending').length > 0 && (
                        <span className="absolute -top-1 -right-1 flex h-3 w-3">
                            <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-secondary opacity-75"></span>
                            <span className="relative inline-flex rounded-full h-3 w-3 bg-secondary"></span>
                        </span>
                    )}
                </button>
            </div>
        );
    }

    return (
        <div className="fixed inset-0 z-50 flex flex-col bg-base-100/95 backdrop-blur shadow-2xl transition-transform duration-300 pointer-events-auto border-t border-base-300 md:w-2/3 md:inset-y-0 md:right-0 md:left-auto md:border-t-0 md:border-l">
            {/* Header */}
            <div className="flex items-center justify-between p-4 border-b border-base-300 bg-base-200/50">
                <div className="flex items-center gap-2">
                    <Activity className="text-primary" size={20} />
                    <h2 className="font-bold text-lg">{t('monitor.network.title', 'γƒγƒƒγƒˆγƒ―γƒΌγ‚―γƒ’γƒ‹γ‚ΏγƒΌ')}</h2>
                    <span className="badge badge-sm">
                        {t('monitor.network.requests_count', '{{count}} δ»Ά', { count: requests.length })}
                    </span>
                </div>
                <div className="flex items-center gap-2">
                    <button
                        onClick={toggleRecording}
                        className={`btn btn-sm btn-circle ${isRecording ? 'btn-error' : 'btn-success'}`}
                        title={isRecording
                            ? t('monitor.network.stop_recording', 'θ¨˜ιŒ²γ‚’εœζ­’')
                            : t('monitor.network.start_recording', 'θ¨˜ιŒ²γ‚’ι–‹ε§‹')}
                    >
                        {isRecording ? <Pause size={14} /> : <Play size={14} />}
                    </button>
                    <button
                        onClick={clearRequests}
                        className="btn btn-sm btn-circle btn-ghost"
                        title={t('monitor.network.clear_requests', 'γƒͺγ‚―γ‚¨γ‚Ήγƒˆγ‚’γ‚―γƒͺγ‚’')}
                    >
                        <Trash2 size={16} />
                    </button>
                    <button
                        onClick={() => setIsOpen(false)}
                        className="btn btn-sm btn-circle btn-ghost"
                    >
                        <X size={20} />
                    </button>
                </div>
            </div>

            {/* Main Content */}
            <div className="flex-1 flex overflow-hidden">
                {/* Request List */}
                <div className={`flex-1 overflow-y-auto border-r border-base-300 ${selectedRequest ? 'hidden md:block md:w-1/2' : 'w-full'}`}>
                    <table className="table table-xs table-pin-rows w-full">
                        <thead>
                            <tr className="bg-base-200">
                                <th className="w-16">{t('monitor.network.table.status', 'ηŠΆζ…‹')}</th>
                                <th>{t('monitor.network.table.command', 'γ‚³γƒžγƒ³γƒ‰')}</th>
                                <th className="w-20 text-right">{t('monitor.network.table.time', 'ζ™‚εˆ»')}</th>
                                <th className="w-20 text-right">{t('monitor.network.table.duration', '所要時間')}</th>
                            </tr>
                        </thead>
                        <tbody>
                            {requests.map((req) => (
                                <tr
                                    key={req.id}
                                    className={`cursor-pointer hover:bg-base-200 ${selectedRequest?.id === req.id ? 'bg-primary/10' : ''}`}
                                    onClick={() => setSelectedRequest(req)}
                                >
                                    <td>
                                        <BadgeStatus status={req.status} />
                                    </td>
                                    <td className="font-mono text-xs truncate max-w-[200px]" title={req.cmd}>
                                        {req.cmd}
                                    </td>
                                    <td className="text-right text-xs opacity-70">
                                        {new Date(req.startTime).toLocaleTimeString()}
                                    </td>
                                    <td className="text-right text-xs opacity-70">
                                        {req.duration ? `${req.duration}ms` : '-'}
                                    </td>
                                </tr>
                            ))}
                            {requests.length === 0 && (
                                <tr>
                                    <td colSpan={4} className="text-center py-8 opacity-50">
                                        {t('monitor.network.empty', 'θ¨˜ιŒ²γ•γ‚ŒγŸγƒͺγ‚―γ‚¨γ‚Ήγƒˆγ―γ‚γ‚ŠγΎγ›γ‚“')}
                                    </td>
                                </tr>
                            )}
                        </tbody>
                    </table>
                </div>

                {/* Details Panel */}
                {selectedRequest && (
                    <div className="flex-1 md:w-1/2 overflow-y-auto bg-base-100 flex flex-col absolute inset-0 md:static z-10 w-full">
                        <div className="flex items-center justify-between p-2 border-b border-base-300 bg-base-200/30 md:hidden">
                            <button onClick={() => setSelectedRequest(null)} className="btn btn-sm btn-ghost">
                                <ChevronDown size={16} className="rotate-90" /> {t('common.back', 'ζˆ»γ‚‹')}
                            </button>
                            <span className="font-mono text-xs">{selectedRequest.cmd}</span>
                        </div>

                        <div className="p-4 space-y-4">
                            <div>
                                <h3 className="text-xs font-bold uppercase opacity-50 mb-1">{t('monitor.network.sections.general', '概要')}</h3>
                                <div className="bg-base-200 rounded p-2 text-xs space-y-1">
                                    <div className="flex justify-between">
                                        <span className="opacity-70">ID:</span>
                                        <span className="font-mono select-all">{selectedRequest.id}</span>
                                    </div>
                                    <div className="flex justify-between">
                                        <span className="opacity-70">{t('monitor.network.fields.status', 'ηŠΆζ…‹')}:</span>
                                        <BadgeStatus status={selectedRequest.status} />
                                    </div>
                                    <div className="flex justify-between">
                                        <span className="opacity-70">{t('monitor.network.fields.start_time', 'ι–‹ε§‹ζ™‚εˆ»')}:</span>
                                        <span>{new Date(selectedRequest.startTime).toLocaleString()}</span>
                                    </div>
                                    {selectedRequest.duration && (
                                        <div className="flex justify-between">
                                            <span className="opacity-70">{t('monitor.network.fields.duration', '所要時間')}:</span>
                                            <span>{selectedRequest.duration}ms</span>
                                        </div>
                                    )}
                                </div>
                            </div>

                            <div>
                                <h3 className="text-xs font-bold uppercase opacity-50 mb-1">{t('monitor.network.sections.request_args', 'γƒͺγ‚―γ‚¨γ‚ΉγƒˆεΌ•ζ•°')}</h3>
                                <JsonView data={selectedRequest.args} />
                            </div>

                            <div>
                                <h3 className="text-xs font-bold uppercase opacity-50 mb-1">
                                    {selectedRequest.status === 'error'
                                        ? t('monitor.network.sections.error_details', 'エラー詳細')
                                        : t('monitor.network.sections.response', 'レスポンス')}
                                </h3>
                                {(selectedRequest.response || selectedRequest.error) ? (
                                    <JsonView
                                        data={selectedRequest.status === 'error' ? selectedRequest.error : selectedRequest.response}
                                        isError={selectedRequest.status === 'error'}
                                    />
                                ) : (
                                    <div className="text-xs opacity-50 italic">{t('monitor.network.waiting', 'εΏœη­”εΎ…γ‘...')}</div>
                                )}
                            </div>
                        </div>
                    </div>
                )}
            </div>
        </div>
    );
};

const BadgeStatus = ({ status }: { status: NetworkRequest['status'] }) => {
    const { t } = useTranslation();
    switch (status) {
        case 'success':
            return <span className="badge badge-xs badge-success">200</span>;
        case 'error':
            return <span className="badge badge-xs badge-error">{t('monitor.network.badge_error', 'エラー')}</span>;
        case 'pending':
            return <span className="loading loading-spinner loading-xs text-warning"></span>;
    }
};

const JsonView = ({ data, isError = false }: { data: any, isError?: boolean }) => {
    const { t } = useTranslation();
    if (data === undefined || data === null) {
        return <div className="text-xs opacity-50 italic">{t('common.empty', 'η©Ί')}</div>;
    }

    return (
        <div className={`mockup-code bg-base-300 text-xs min-h-0 ${isError ? 'border border-error/50' : ''}`}>
            <pre className="px-4 py-2 overflow-x-auto">
                <code>{JSON.stringify(data, null, 2)}</code>
            </pre>
        </div>
    );
};

export default NetworkMonitor;