Refactor video streaming feature and update dependencies
- Replaced npm ci with npm install in docker-compose for better package management. - Introduced remote component loading for the VideoStreamingPage with error handling. - Updated the title in index.html to "Experiments Dashboard" for clarity. - Added new video remote service configuration in docker-compose for improved integration. - Removed deprecated files and components related to the video streaming feature to streamline the codebase. - Updated package.json and package-lock.json to include @originjs/vite-plugin-federation for module federation support.
This commit is contained in:
36
video-remote/src/components/FiltersBar.tsx
Normal file
36
video-remote/src/components/FiltersBar.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React, { useState } from 'react'
|
||||
|
||||
type Props = {
|
||||
onChange: (filters: { camera_name?: string; start_date?: string; end_date?: string }) => void
|
||||
}
|
||||
|
||||
export const FiltersBar: React.FC<Props> = ({ onChange }) => {
|
||||
const [camera, setCamera] = useState('')
|
||||
const [start, setStart] = useState('')
|
||||
const [end, setEnd] = useState('')
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm mb-1">Camera</label>
|
||||
<input value={camera} onChange={e=>setCamera(e.target.value)} placeholder="camera1" className="w-full px-3 py-2 border rounded" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm mb-1">Start date</label>
|
||||
<input type="date" value={start} onChange={e=>setStart(e.target.value)} className="w-full px-3 py-2 border rounded" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm mb-1">End date</label>
|
||||
<input type="date" value={end} onChange={e=>setEnd(e.target.value)} className="w-full px-3 py-2 border rounded" />
|
||||
</div>
|
||||
<div className="flex items-end gap-2">
|
||||
<button className="px-3 py-2 border rounded" onClick={()=>onChange({ camera_name: camera || undefined, start_date: start || undefined, end_date: end || undefined })}>Apply</button>
|
||||
<button className="px-3 py-2 border rounded" onClick={()=>{ setCamera(''); setStart(''); setEnd(''); onChange({}) }}>Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FiltersBar
|
||||
|
||||
|
||||
22
video-remote/src/components/Pagination.tsx
Normal file
22
video-remote/src/components/Pagination.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from 'react'
|
||||
|
||||
type Props = {
|
||||
page: number
|
||||
totalPages: number
|
||||
onChange: (page: number) => void
|
||||
}
|
||||
|
||||
export const Pagination: React.FC<Props> = ({ page, totalPages, onChange }) => {
|
||||
if (totalPages <= 1) return null
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<button disabled={page<=1} onClick={()=>onChange(page-1)} className="px-3 py-1 border rounded disabled:opacity-50">Prev</button>
|
||||
<span className="text-sm">Page {page} / {totalPages}</span>
|
||||
<button disabled={page>=totalPages} onClick={()=>onChange(page+1)} className="px-3 py-1 border rounded disabled:opacity-50">Next</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Pagination
|
||||
|
||||
|
||||
42
video-remote/src/components/VideoCard.tsx
Normal file
42
video-remote/src/components/VideoCard.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from 'react'
|
||||
import { VideoThumbnail } from './VideoThumbnail'
|
||||
|
||||
export type VideoFile = {
|
||||
file_id: string
|
||||
filename: string
|
||||
camera_name: string
|
||||
file_size_bytes: number
|
||||
created_at: string
|
||||
status: string
|
||||
format: string
|
||||
is_streamable?: boolean
|
||||
needs_conversion?: boolean
|
||||
metadata?: { duration_seconds: number; width: number; height: number; fps: number; codec: string }
|
||||
}
|
||||
|
||||
export type VideoCardProps = {
|
||||
video: VideoFile
|
||||
onClick?: (video: VideoFile) => void
|
||||
showMetadata?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const VideoCard: React.FC<VideoCardProps> = ({ video, onClick }) => {
|
||||
return (
|
||||
<div
|
||||
className={`bg-white rounded-xl border border-gray-200 shadow-sm hover:shadow-md transition-shadow overflow-hidden ${onClick ? 'cursor-pointer' : ''}`}
|
||||
onClick={onClick ? () => onClick(video) : undefined}
|
||||
>
|
||||
<VideoThumbnail fileId={video.file_id} width={640} height={360} className="w-full h-auto" alt={video.filename} />
|
||||
<div className="p-4">
|
||||
<div className="text-sm text-gray-500 mb-1">{video.camera_name}</div>
|
||||
<div className="font-semibold text-gray-900 truncate" title={video.filename}>{video.filename}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default VideoCard
|
||||
|
||||
|
||||
|
||||
52
video-remote/src/components/VideoList.tsx
Normal file
52
video-remote/src/components/VideoList.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { fetchVideos } from '../services/video'
|
||||
import { Pagination } from './Pagination'
|
||||
import VideoCard from './VideoCard'
|
||||
import { FiltersBar } from './FiltersBar'
|
||||
|
||||
type Props = { onSelect?: (fileId: string) => void }
|
||||
|
||||
export const VideoList: React.FC<Props> = ({ onSelect }) => {
|
||||
const [videos, setVideos] = useState<any[]>([])
|
||||
const [page, setPage] = useState(1)
|
||||
const [totalPages, setTotalPages] = useState(1)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const [filters, setFilters] = useState<{ camera_name?: string; start_date?: string; end_date?: string }>({})
|
||||
|
||||
async function load(p: number, f = filters) {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const res = await fetchVideos({ page: p, limit: 12, ...f })
|
||||
setVideos(res.videos)
|
||||
setTotalPages(res.total_pages || 1)
|
||||
} catch (e: any) {
|
||||
setError(e?.message || 'Failed to load videos')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => { load(page) }, [page])
|
||||
|
||||
if (loading) return <div className="p-6">Loading videos…</div>
|
||||
if (error) return <div className="p-6 text-red-600">{error}</div>
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<FiltersBar onChange={(f) => { setFilters(f); setPage(1); load(1, f) }} />
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
||||
{videos.map(v => (
|
||||
<VideoCard key={v.file_id} video={v} onClick={onSelect ? () => onSelect(v.file_id) : undefined} />
|
||||
))}
|
||||
</div>
|
||||
<Pagination page={page} totalPages={totalPages} onChange={setPage} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default VideoList
|
||||
|
||||
|
||||
29
video-remote/src/components/VideoModal.tsx
Normal file
29
video-remote/src/components/VideoModal.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react'
|
||||
const BASE = (import.meta as any).env?.VITE_VISION_API_URL || '/api'
|
||||
|
||||
type Props = {
|
||||
fileId: string | null
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export const VideoModal: React.FC<Props> = ({ fileId, onClose }) => {
|
||||
if (!fileId) return null
|
||||
const src = `${BASE}/videos/${fileId}/stream`
|
||||
return (
|
||||
<div className="fixed inset-0 z-[1000] bg-black/50 flex items-center justify-center">
|
||||
<div className="bg-white rounded-lg w-full max-w-3xl overflow-hidden">
|
||||
<div className="flex items-center justify-between p-3 border-b">
|
||||
<div className="font-semibold">Video</div>
|
||||
<button onClick={onClose} className="px-2 py-1 border rounded">Close</button>
|
||||
</div>
|
||||
<div className="p-3">
|
||||
<video src={src} controls className="w-full h-auto" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default VideoModal
|
||||
|
||||
|
||||
26
video-remote/src/components/VideoThumbnail.tsx
Normal file
26
video-remote/src/components/VideoThumbnail.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react'
|
||||
const BASE = (import.meta as any).env?.VITE_VISION_API_URL || '/api'
|
||||
|
||||
type Props = {
|
||||
fileId: string
|
||||
width?: number
|
||||
height?: number
|
||||
alt?: string
|
||||
className?: string
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
export const VideoThumbnail: React.FC<Props> = ({ fileId, width = 320, height = 180, alt = '', className = '', onClick }) => {
|
||||
const p = new URLSearchParams()
|
||||
if (width) p.append('width', String(width))
|
||||
if (height) p.append('height', String(height))
|
||||
const qs = p.toString()
|
||||
const src = `${BASE}/videos/${fileId}/thumbnail${qs ? `?${qs}` : ''}`
|
||||
return (
|
||||
<img src={src} alt={alt} width={width} height={height} className={className} onClick={onClick} />
|
||||
)
|
||||
}
|
||||
|
||||
export default VideoThumbnail
|
||||
|
||||
|
||||
Reference in New Issue
Block a user