TableSchema with TAG and FIELD columnstablename.tagvalue1.tagvalue2...sensor_data.device_001.room_A.floor_3root.building1.floor2.sensor3All endpoints work transparently with both models:
devices parameter filters by device identifiers (constructed from TAG values in Table Model)measurements parameter filters by FIELD columns in both modelsmeasurements map for Table Model (for display and filtering)Always use pagination for large datasets
{ "fileId": "abc123", "limit": 100, // Recommended: 100-500 "offset": 0 }
hasMore field in response to determine if more pages existMost efficient filter - uses chunk-level optimization
{ "fileId": "abc123", "startTime": 1705478400000, "endTime": 1705564800000 // 24-hour window recommended }
Reduces data transfer and processing
{ "fileId": "abc123", "devices": ["sensor_table.device_001.room_A"], "measurements": ["temperature", "humidity"] }
Use aggregation and downsampling for charts
{ "fileId": "abc123", "measurements": ["temperature"], "aggregation": "AVG", "windowSize": 60000, // 1-minute windows "maxPoints": 1000 // LTTB downsampling }
The backend implements two-tier caching:
Reader Cache (30 min TTL, 100 files)
Metadata Cache (60 min TTL, 1000 entries)
Best Practices:
fileId for multiple queries to benefit from cachingtsfile.query.timeout-seconds in application.ymlUsed in: File Selection View
Browse server directory tree with lazy loading.
Parameters:
root (required): Whitelisted root directorypath (optional): Subdirectory for lazy loadingExample:
GET /api/files/tree?root=/data/tsfiles&path=sensors/2024
Response:
{ "name": "2024", "path": "/data/tsfiles/sensors/2024", "isDirectory": true, "children": [ { "name": "january.tsfile", "path": "/data/tsfiles/sensors/2024/january.tsfile", "isDirectory": false, "size": 10485760 } ] }
UI Usage:
FileSelectionView.vue: Directory tree navigationFileTree.vue: Lazy-loaded tree componentUsed in: File Selection View
Upload a TSFile to server.
Request: multipart/form-data with file Max size: 100MB (configurable via spring.servlet.multipart.max-file-size)
Response:
{ "fileId": "f7a3b2c1-4d5e-6f7g-8h9i-0j1k2l3m4n5o", "fileName": "sensor_data.tsfile", "size": 10485760 }
Validation:
.tsfile extensionUsed in: Metadata View, Filter Panel
Retrieve comprehensive TSFile metadata.
Example:
GET /api/meta/f7a3b2c1-4d5e-6f7g-8h9i-0j1k2l3m4n5o
Table Model Response:
{ "fileId": "f7a3b2c1-4d5e-6f7g-8h9i-0j1k2l3m4n5o", "fileName": "sensor_data.tsfile", "fileSize": 10485760, "version": "v4", "timeRange": { "startTime": 1705478400000, "endTime": 1705564800000 }, "deviceCount": 50, "measurementCount": 12, "tables": [ { "tableName": "sensor_table", "tagColumns": [ { "name": "device_id", "dataType": "STRING", "category": "TAG" }, { "name": "location", "dataType": "STRING", "category": "TAG" } ], "fieldColumns": [ { "name": "temperature", "dataType": "FLOAT", "encoding": "GORILLA", "compression": "SNAPPY", "category": "FIELD" }, { "name": "humidity", "dataType": "FLOAT", "encoding": "GORILLA", "compression": "SNAPPY", "category": "FIELD" } ] } ], "rowGroups": [ { "index": 0, "device": "sensor_table.device_001.room_A", "startTime": 1705478400000, "endTime": 1705492800000, "chunkCount": 8 } ] }
Tree Model Response:
{ "fileId": "xyz789", "fileName": "legacy_sensors.tsfile", "version": "v3", "deviceCount": 10, "measurementCount": 5, "measurements": [ { "name": "s1", "dataType": "FLOAT", "encoding": "GORILLA", "compression": "SNAPPY" }, { "name": "s2", "dataType": "INT64", "encoding": "RLE", "compression": "SNAPPY" } ], "rowGroups": [ { "index": 0, "device": "root.sensor1", "startTime": 1705478400000, "endTime": 1705564800000, "chunkCount": 5 } ] }
UI Usage:
MetadataView.vue: Display file metadata, tables, measurements, row groupsFilterPanel.vue: Populate device/measurement selection dropdownsMetaCards.vue: Show statistics cards (device count, measurement count, time range)Performance: Cached for 60 minutes after first load
Used in: Data Preview View
Query and preview TSFile data with filtering and pagination.
Table Model Example:
{ "fileId": "abc123", "devices": ["sensor_table.device_001.room_A"], "measurements": ["temperature", "humidity"], "startTime": 1705478400000, "endTime": 1705564800000, "limit": 100, "offset": 0 }
Table Model Response:
{ "data": [ { "timestamp": 1705478400000, "device": "sensor_table.device_001.room_A", "measurements": { "device_id": "device_001", "location": "room_A", "temperature": 25.5, "humidity": 60.0 } } ], "total": 8640, "offset": 0, "limit": 100, "hasMore": true, "columnNames": ["Time", "Device", "device_id", "location", "temperature", "humidity"] }
Tree Model Example:
{ "fileId": "xyz789", "devices": ["root.sensor1"], "measurements": ["s1", "s2"], "startTime": 1705478400000, "endTime": 1705564800000, "limit": 100, "offset": 0 }
Tree Model Response:
{ "data": [ { "timestamp": 1705478400000, "device": "root.sensor1", "measurements": { "s1": 10.5, "s2": 100 } } ], "total": 8640, "offset": 0, "limit": 100, "hasMore": true, "columnNames": ["Time", "Device", "s1", "s2"] }
Value Range Filtering:
{ "fileId": "abc123", "measurements": ["temperature"], "valueRange": { "min": 20.0, "max": 30.0 } }
Note: Value range filter requires ALL numeric measurements in a row to fall within the range (strictest filtering mode).
Multi-Table Query (Table Model):
{ "fileId": "abc123", "devices": [ "table1.device_001.room_A", "table2.device_002.room_B" ], "measurements": ["temperature"] }
Backend automatically handles queries spanning multiple tables by device identifier prefix.
UI Usage:
DataPreviewView.vue: Main data browsing interfaceFilterPanel.vue: Build query from user selectionsDataTable.vue: Display paginated results with column headersPerformance Tips:
Used in: Chart Visualization View
Query data optimized for visualization with aggregation and downsampling.
Basic Visualization Query:
{ "fileId": "abc123", "devices": ["sensor_table.device_001.room_A"], "measurements": ["temperature", "humidity"], "startTime": 1705478400000, "endTime": 1705564800000, "maxPoints": 1000 }
Response:
{ "series": [ { "name": "temperature", "device": "sensor_table.device_001.room_A", "data": [ {"timestamp": 1705478400000, "value": 25.5}, {"timestamp": 1705478460000, "value": 25.6} ], "unit": "°C" } ], "timeRange": { "start": 1705478400000, "end": 1705564800000 }, "downsampled": true, "originalPoints": 86400, "returnedPoints": 1000 }
Time Window Aggregation:
{ "fileId": "abc123", "measurements": ["temperature"], "aggregation": "AVG", "windowSize": 300000, // 5-minute windows "startTime": 1705478400000, "endTime": 1705564800000 }
Response with Aggregation:
{ "series": [ { "name": "temperature_AVG", "device": "sensor_table.device_001.room_A", "data": [ {"timestamp": 1705478400000, "value": 25.3, "count": 300}, {"timestamp": 1705478700000, "value": 25.5, "count": 300} ], "aggregation": "AVG", "windowSize": 300000 } ] }
Aggregation Types:
AVG: Average value per windowMIN: Minimum value per windowMAX: Maximum value per windowCOUNT: Count of points per windowDownsampling (LTTB):
maxPointsUI Usage:
ChartVisualizationView.vue: Interactive chart interfaceChartPanel.vue: ECharts visualization componentPerformance Tips:
FileSelectionView.vue)Primary APIs:
GET /api/files/tree - Browse server directoriesPOST /api/files/upload - Upload local TSFilesIntegration:
// Browse directory tree const response = await fetch(`/api/files/tree?root=${encodedRoot}&path=${encodedPath}`) const treeNode = await response.json() // Upload file const formData = new FormData() formData.append('file', file) const uploadResponse = await fetch('/api/files/upload', { method: 'POST', body: formData }) const { fileId } = await uploadResponse.json() router.push(`/meta/${fileId}`)
User Flow:
MetadataView.vue)Primary APIs:
GET /api/meta/{fileId} - Load metadataIntegration:
const metadata = await metadataApi.getMetadata(fileId) // Display metadata cards displayMetaCards({ deviceCount: metadata.deviceCount, measurementCount: metadata.measurementCount, timeRange: metadata.timeRange, fileSize: metadata.fileSize }) // Show tables (Table Model) or measurements (Tree Model) if (metadata.tables) { displayTables(metadata.tables) // Table Model } else { displayMeasurements(metadata.measurements) // Tree Model } // Display row groups and chunks displayRowGroups(metadata.rowGroups)
User Flow:
DataPreviewView.vue)Primary APIs:
GET /api/meta/{fileId} - Populate filters (via FilterPanel)POST /api/data/preview - Query data with filtersIntegration:
// FilterPanel populates dropdowns from metadata const metadata = await metadataApi.getMetadata(fileId) const devices = metadata.rowGroups.map(rg => rg.device) const measurements = metadata.tables ? metadata.tables[0].fieldColumns.map(fc => fc.name) : metadata.measurements.map(m => m.name) // Query data when filters change async function handleFilterChange(filters) { const request = { fileId, ...filters, limit: 100, offset: 0 } const response = await dataApi.previewData(request) displayData(response.data, response.columnNames) updatePagination(response.total, response.offset, response.hasMore) } // Handle pagination async function handlePageChange(direction) { const newOffset = direction === 'next' ? currentOffset + limit : currentOffset - limit const request = { fileId, ...currentFilters, limit, offset: newOffset } const response = await dataApi.previewData(request) // Update display... }
User Flow:
ChartVisualizationView.vue)Primary APIs:
POST /api/data/query - Query visualization dataIntegration:
async function loadChartData() { const measurements = measurementsInput.split(',').map(m => m.trim()) const request = { fileId, measurements, startTime: startTime ? new Date(startTime).getTime() : undefined, endTime: endTime ? new Date(endTime).getTime() : undefined, aggregation: aggregationType || undefined, windowSize: aggregationType ? windowSize : undefined, maxPoints: 1000 } const response = await dataApi.queryChartData(request) // Configure ECharts const chartConfig = { series: response.series.map(s => ({ name: s.name, type: 'line', data: s.data.map(d => [d.timestamp, d.value]), smooth: true })), xAxis: { type: 'time' }, yAxis: { type: 'value' } } chartInstance.setOption(chartConfig) }
User Flow:
{ "status": 400, "error": "Bad Request", "message": "Invalid file ID format", "timestamp": "2024-01-17T10:30:00Z", "path": "/api/data/preview" }
Common Causes:
{ "status": 403, "error": "Forbidden", "message": "Directory path outside whitelist", "path": "/api/files/tree" }
Cause: Attempted to access directory not in tsfile.allowed-directories
{ "status": 404, "error": "Not Found", "message": "TSFile not found for fileId: abc123", "path": "/api/meta/abc123" }
Common Causes:
{ "status": 413, "error": "Payload Too Large", "message": "File size exceeds maximum allowed size of 100MB" }
Cause: Upload file exceeds spring.servlet.multipart.max-file-size
{ "status": 504, "error": "Gateway Timeout", "message": "Query execution exceeded timeout of 30 seconds" }
Causes:
Mitigation:
Always handle loading states
const loading = ref(false) const error = ref(null) try { loading.value = true const response = await dataApi.previewData(request) // Handle success... } catch (e) { error.value = e.message showErrorToast(e.message) } finally { loading.value = false }
Use TypeScript types from API
import type { DataRow, DataPreviewRequest, TSFileMetadata } from '@/api/types'
Implement pagination properly
Cache fileId in stores
const fileStore = useFileStore() fileStore.setCurrentFile(fileId, fileName)
Provide user feedback
Whitelist directories
tsfile: allowed-directories: - /data/tsfiles - /mnt/storage/sensors
Tune cache settings based on usage
tsfile: cache: metadata: max-size: 1000 # Increase for many files ttl-minutes: 60 reader: max-size: 100 # Increase for concurrent users ttl-minutes: 30
Adjust upload limits
spring: servlet: multipart: max-file-size: 500MB # For large TSFiles max-request-size: 500MB
Set appropriate timeout
tsfile: query: timeout-seconds: 60 # For large files
Current API version: v1 (implicit in /api path)
Future versions will use explicit versioning:
/api/v2/data/previewFor issues, feature requests, or questions: