UPDATE
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
# Backend Requirements For Sensor Content Page
|
||||
|
||||
## Goal
|
||||
|
||||
This page is now focused on showing the content of a selected sensor, plus three analytics cards that each consume their own API:
|
||||
|
||||
- `SensorComparisonChart.tsx`
|
||||
- `SensorValuesList.tsx`
|
||||
- `SensorRadarChart.tsx`
|
||||
|
||||
The page currently uses mock data in the frontend. To switch it to real data, the backend needs to provide the APIs and payload contracts below.
|
||||
|
||||
---
|
||||
|
||||
## 1) Main Sensor Logs API
|
||||
|
||||
This API powers:
|
||||
|
||||
- sensor selector
|
||||
- top summary cards
|
||||
- health panel
|
||||
- logs table
|
||||
|
||||
### Suggested endpoint
|
||||
|
||||
`GET /api/sensor-external-api/logs/`
|
||||
|
||||
### Required query params
|
||||
|
||||
- `farm_uuid` - required
|
||||
- `page` - required for pagination
|
||||
- `page_size` - required for pagination
|
||||
|
||||
### Recommended extra query params
|
||||
|
||||
- `physical_device_uuid` - to fetch only one sensor directly from backend
|
||||
- `sensor_type` - optional future filter
|
||||
- `date_from` - optional
|
||||
- `date_to` - optional
|
||||
|
||||
### Required response shape
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"count": 120,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"data": [
|
||||
{
|
||||
"id": 108,
|
||||
"farm_uuid": "11111111-1111-1111-1111-111111111111",
|
||||
"sensor_catalog_uuid": "catalog-7in1-01",
|
||||
"physical_device_uuid": "device-7in1-0001",
|
||||
"farm_sensor": {
|
||||
"uuid": "farm-sensor-7in1-01",
|
||||
"sensor_catalog_uuid": "catalog-7in1-01",
|
||||
"physical_device_uuid": "device-7in1-0001",
|
||||
"name": "سنسور خاک 7 در 1 - بلوک شمالی",
|
||||
"sensor_type": "Soil 7 in 1",
|
||||
"is_active": true,
|
||||
"specifications": {},
|
||||
"power_source": {},
|
||||
"created_at": "2025-01-01T08:00:00Z",
|
||||
"updated_at": "2025-01-10T08:00:00Z"
|
||||
},
|
||||
"sensor_catalog": {
|
||||
"uuid": "catalog-7in1-01",
|
||||
"code": "SOIL-7IN1",
|
||||
"name": "کاتالوگ سنسور خاک 7 در 1",
|
||||
"description": "string",
|
||||
"customizable_fields": [],
|
||||
"supported_power_sources": [],
|
||||
"returned_data_fields": [
|
||||
"moisture",
|
||||
"temperature",
|
||||
"humidity",
|
||||
"ph",
|
||||
"ec",
|
||||
"nitrogen",
|
||||
"phosphorus",
|
||||
"potassium"
|
||||
],
|
||||
"sample_payload": {},
|
||||
"is_active": true,
|
||||
"created_at": "2025-01-01T08:00:00Z",
|
||||
"updated_at": "2025-01-10T08:00:00Z"
|
||||
},
|
||||
"payload": {
|
||||
"moisture": 61,
|
||||
"temperature": 27.4,
|
||||
"humidity": 58,
|
||||
"ph": 6.7,
|
||||
"ec": 1.45,
|
||||
"nitrogen": 44,
|
||||
"phosphorus": 32,
|
||||
"potassium": 39
|
||||
},
|
||||
"created_at": "2025-05-02T11:20:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Notes
|
||||
|
||||
- `payload` must keep numeric values as numbers, not strings
|
||||
- `physical_device_uuid` is required because the page groups and filters by sensor device
|
||||
- `farm_sensor.name` and `farm_sensor.sensor_type` are needed for sensor dropdown labels
|
||||
- `sensor_catalog.returned_data_fields` is needed to understand sensor structure
|
||||
|
||||
---
|
||||
|
||||
## 2) Comparison Chart API
|
||||
|
||||
This API powers `src/views/dashboards/farm/SensorComparisonChart.tsx`
|
||||
|
||||
### Suggested endpoint
|
||||
|
||||
`GET /api/sensors/comparison-chart/`
|
||||
|
||||
### Required query params
|
||||
|
||||
- `farm_uuid`
|
||||
- `physical_device_uuid`
|
||||
- `range` - example: `7d`, `30d`
|
||||
|
||||
### Required response shape
|
||||
|
||||
```json
|
||||
{
|
||||
"series": [
|
||||
{
|
||||
"name": "moisture",
|
||||
"data": [56, 58, 55, 60, 62, 61, 59]
|
||||
},
|
||||
{
|
||||
"name": "temperature",
|
||||
"data": [26.2, 26.7, 27.0, 27.2, 27.5, 27.4, 27.1]
|
||||
}
|
||||
],
|
||||
"categories": ["شنبه", "یکشنبه", "دوشنبه", "سه شنبه", "چهارشنبه", "پنج شنبه", "جمعه"],
|
||||
"currentValue": 61,
|
||||
"vsLastWeek": "+5.4%"
|
||||
}
|
||||
```
|
||||
|
||||
### Notes
|
||||
|
||||
- This endpoint is independent and should not be derived in frontend from logs
|
||||
- `currentValue` is shown in large text in the card
|
||||
- `vsLastWeek` is shown as summary text in the card
|
||||
|
||||
---
|
||||
|
||||
## 3) Sensor Values List API
|
||||
|
||||
This API powers `src/views/dashboards/farm/SensorValuesList.tsx`
|
||||
|
||||
### Suggested endpoint
|
||||
|
||||
`GET /api/sensors/values-list/`
|
||||
|
||||
### Required query params
|
||||
|
||||
- `farm_uuid`
|
||||
- `physical_device_uuid`
|
||||
- `range` - example: `1h`, `24h`, `7d`
|
||||
|
||||
### Required response shape
|
||||
|
||||
```json
|
||||
{
|
||||
"sensors": [
|
||||
{
|
||||
"title": "Moisture",
|
||||
"subtitle": "مقدار فعلی: 61%",
|
||||
"trendNumber": 4.8,
|
||||
"trend": "positive",
|
||||
"unit": "%"
|
||||
},
|
||||
{
|
||||
"title": "Temperature",
|
||||
"subtitle": "مقدار فعلی: 27.4°C",
|
||||
"trendNumber": -1.2,
|
||||
"trend": "negative",
|
||||
"unit": "°C"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Notes
|
||||
|
||||
- `trend` must be either `positive` or `negative`
|
||||
- `trendNumber` should be numeric
|
||||
- `subtitle` can already be formatted by backend if preferred
|
||||
|
||||
---
|
||||
|
||||
## 4) Radar Chart API
|
||||
|
||||
This API powers `src/views/dashboards/farm/SensorRadarChart.tsx`
|
||||
|
||||
### Suggested endpoint
|
||||
|
||||
`GET /api/sensors/radar-chart/`
|
||||
|
||||
### Required query params
|
||||
|
||||
- `farm_uuid`
|
||||
- `physical_device_uuid`
|
||||
- `range` - example: `today`, `7d`, `30d`
|
||||
|
||||
### Required response shape
|
||||
|
||||
```json
|
||||
{
|
||||
"labels": [
|
||||
"Moisture",
|
||||
"Temperature",
|
||||
"Humidity",
|
||||
"PH",
|
||||
"EC",
|
||||
"Nitrogen",
|
||||
"Potassium"
|
||||
],
|
||||
"series": [
|
||||
{
|
||||
"name": "وضعیت فعلی",
|
||||
"data": [61, 27.4, 58, 6.7, 1.45, 44, 39]
|
||||
},
|
||||
{
|
||||
"name": "بازه ایده آل",
|
||||
"data": [60, 26, 55, 6.5, 1.3, 42, 38]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Notes
|
||||
|
||||
- This endpoint is also independent
|
||||
- `labels.length` must match every `series[i].data.length`
|
||||
|
||||
---
|
||||
|
||||
## 5) Recommended Shared Backend Rules
|
||||
|
||||
These rules should be consistent across all four APIs:
|
||||
|
||||
- all timestamps in ISO 8601 format
|
||||
- all numeric sensor values returned as numbers
|
||||
- all UUID-like identifiers stable and non-null when available
|
||||
- responses filtered by `farm_uuid`
|
||||
- responses filtered by `physical_device_uuid` when a single sensor page is opened
|
||||
- clear 4xx/5xx error messages in `msg` or `detail`
|
||||
|
||||
---
|
||||
|
||||
## 6) Sensor Metadata Recommended For Future Use
|
||||
|
||||
These fields are not all required immediately, but they will help future UI work:
|
||||
|
||||
- sensor display name
|
||||
- sensor type
|
||||
- installation location
|
||||
- block / zone / greenhouse name
|
||||
- unit for each measurable field
|
||||
- min / max ideal thresholds per field
|
||||
- sensor online / offline state
|
||||
- last sync timestamp
|
||||
- battery status
|
||||
- signal strength
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"field_meta": {
|
||||
"moisture": { "label": "Moisture", "unit": "%", "ideal_min": 45, "ideal_max": 70 },
|
||||
"temperature": { "label": "Temperature", "unit": "°C", "ideal_min": 18, "ideal_max": 30 },
|
||||
"ph": { "label": "PH", "unit": "", "ideal_min": 5.8, "ideal_max": 7.2 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7) Current Frontend State
|
||||
|
||||
Right now the page:
|
||||
|
||||
- uses full mock data for logs and sensor details
|
||||
- uses mock-derived data for charts
|
||||
- has the right-side details panel removed
|
||||
- expects the three chart cards to be fed from three separate APIs later
|
||||
|
||||
---
|
||||
|
||||
## 8) Minimum Backend Delivery Checklist
|
||||
|
||||
To connect the page to real backend data, the minimum required items are:
|
||||
|
||||
- one paginated logs API with `farm_uuid` and ideally `physical_device_uuid`
|
||||
- one API for comparison chart card
|
||||
- one API for values list card
|
||||
- one API for radar chart card
|
||||
- numeric sensor payload values
|
||||
- stable sensor identity using `physical_device_uuid`
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,363 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
|
||||
import Alert from "@mui/material/Alert";
|
||||
import Box from "@mui/material/Box";
|
||||
import Card from "@mui/material/Card";
|
||||
import CardContent from "@mui/material/CardContent";
|
||||
import Chip from "@mui/material/Chip";
|
||||
import Grid from "@mui/material/Grid2";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { alpha, useTheme } from "@mui/material/styles";
|
||||
|
||||
type StatusColor = "success" | "warning" | "error" | "info";
|
||||
type AccentTone = "primary" | "success" | "warning" | "info";
|
||||
type HealthTone = "good" | "medium" | "critical" | "unknown";
|
||||
|
||||
export interface SensorHealthPanelProps {
|
||||
payload?: Record<string, unknown> | null;
|
||||
sensorName?: string | null;
|
||||
title?: string;
|
||||
description?: string;
|
||||
emptyMessage?: string;
|
||||
maxItems?: number;
|
||||
}
|
||||
|
||||
const formatPersianNumber = (value: number): string =>
|
||||
value.toLocaleString("fa-IR");
|
||||
|
||||
const formatScalar = (value: unknown): string => {
|
||||
if (value === null) return "null";
|
||||
if (value === undefined) return "undefined";
|
||||
if (Array.isArray(value)) return `[${value.length} item]`;
|
||||
if (typeof value === "object") return "{...}";
|
||||
return String(value);
|
||||
};
|
||||
|
||||
const normalizeFieldLabel = (key: string): string =>
|
||||
key
|
||||
.replace(/_/g, " ")
|
||||
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
||||
.trim();
|
||||
|
||||
const formatFieldValue = (value: unknown): string => {
|
||||
if (typeof value === "number") {
|
||||
return Number.isInteger(value)
|
||||
? formatPersianNumber(value)
|
||||
: value.toLocaleString("fa-IR", { maximumFractionDigits: 2 });
|
||||
}
|
||||
|
||||
if (typeof value === "boolean") {
|
||||
return value ? "فعال" : "غیرفعال";
|
||||
}
|
||||
|
||||
return formatScalar(value);
|
||||
};
|
||||
|
||||
const getHealthTone = (key: string, value: unknown): HealthTone => {
|
||||
const normalizedKey = key.toLowerCase();
|
||||
|
||||
if (typeof value === "boolean") {
|
||||
return value ? "good" : "critical";
|
||||
}
|
||||
|
||||
if (typeof value !== "number" || Number.isNaN(value)) {
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
if (
|
||||
normalizedKey.includes("temperature") ||
|
||||
normalizedKey.includes("temp") ||
|
||||
normalizedKey.includes("soil_temp")
|
||||
) {
|
||||
if (value >= 16 && value <= 30) return "good";
|
||||
if ((value >= 10 && value < 16) || (value > 30 && value <= 36)) return "medium";
|
||||
return "critical";
|
||||
}
|
||||
|
||||
if (
|
||||
normalizedKey.includes("humidity") ||
|
||||
normalizedKey.includes("moisture") ||
|
||||
normalizedKey.includes("water")
|
||||
) {
|
||||
if (value >= 35 && value <= 75) return "good";
|
||||
if ((value >= 20 && value < 35) || (value > 75 && value <= 90)) return "medium";
|
||||
return "critical";
|
||||
}
|
||||
|
||||
if (normalizedKey.includes("ph")) {
|
||||
if (value >= 5.8 && value <= 7.2) return "good";
|
||||
if ((value >= 5.2 && value < 5.8) || (value > 7.2 && value <= 7.8)) return "medium";
|
||||
return "critical";
|
||||
}
|
||||
|
||||
if (normalizedKey.includes("ec") || normalizedKey.includes("salinity")) {
|
||||
if (value >= 0.8 && value <= 2.2) return "good";
|
||||
if ((value >= 0.4 && value < 0.8) || (value > 2.2 && value <= 3.2)) return "medium";
|
||||
return "critical";
|
||||
}
|
||||
|
||||
if (
|
||||
normalizedKey.includes("nitrogen") ||
|
||||
normalizedKey === "n" ||
|
||||
normalizedKey.includes("phosphorus") ||
|
||||
normalizedKey === "p" ||
|
||||
normalizedKey.includes("potassium") ||
|
||||
normalizedKey === "k"
|
||||
) {
|
||||
if (value >= 20 && value <= 80) return "good";
|
||||
if ((value >= 10 && value < 20) || (value > 80 && value <= 100)) return "medium";
|
||||
return "critical";
|
||||
}
|
||||
|
||||
if (value >= 0 && value <= 100) {
|
||||
if (value >= 35 && value <= 75) return "good";
|
||||
if ((value >= 20 && value < 35) || (value > 75 && value <= 90)) return "medium";
|
||||
return "critical";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
};
|
||||
|
||||
const getHealthToneMeta = (
|
||||
tone: HealthTone,
|
||||
): { label: string; muiColor: StatusColor; paletteKey: AccentTone } => {
|
||||
switch (tone) {
|
||||
case "good":
|
||||
return { label: "سالم", muiColor: "success", paletteKey: "success" };
|
||||
case "medium":
|
||||
return { label: "قابل بررسی", muiColor: "warning", paletteKey: "warning" };
|
||||
case "critical":
|
||||
return { label: "نیازمند رسیدگی", muiColor: "error", paletteKey: "warning" };
|
||||
default:
|
||||
return { label: "نامشخص", muiColor: "info", paletteKey: "info" };
|
||||
}
|
||||
};
|
||||
|
||||
const SensorHealthPanel = ({
|
||||
payload,
|
||||
sensorName,
|
||||
title = "سلامت بخش های سنسور",
|
||||
description = "این بخش یک نمای تصویری از وضعیت قسمت های مختلف سنسور و داده های آخرین payload را نشان می دهد.",
|
||||
emptyMessage = "هنوز داده کافی برای ساخت نمای سلامت سنسور در آخرین payload وجود ندارد.",
|
||||
maxItems = 6,
|
||||
}: SensorHealthPanelProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const healthItems = useMemo(() => {
|
||||
if (!payload || typeof payload !== "object") return [];
|
||||
|
||||
return Object.entries(payload)
|
||||
.filter(([, value]) =>
|
||||
["number", "string", "boolean"].includes(typeof value) || value === null,
|
||||
)
|
||||
.slice(0, maxItems)
|
||||
.map(([key, value]) => {
|
||||
const tone = getHealthTone(key, value);
|
||||
|
||||
return {
|
||||
key,
|
||||
label: normalizeFieldLabel(key),
|
||||
value: formatFieldValue(value),
|
||||
tone,
|
||||
meta: getHealthToneMeta(tone),
|
||||
};
|
||||
});
|
||||
}, [maxItems, payload]);
|
||||
|
||||
const goodCount = healthItems.filter((item) => item.tone === "good").length;
|
||||
const warningCount = healthItems.filter((item) => item.tone === "medium").length;
|
||||
const criticalCount = healthItems.filter((item) => item.tone === "critical").length;
|
||||
|
||||
return (
|
||||
<Card
|
||||
elevation={0}
|
||||
sx={{
|
||||
borderRadius: 5,
|
||||
border: `1px solid ${alpha(theme.palette.primary.main, 0.14)}`,
|
||||
background: `linear-gradient(135deg, ${alpha(
|
||||
theme.palette.primary.main,
|
||||
0.08,
|
||||
)} 0%, ${alpha(theme.palette.info.main, 0.06)} 48%, ${alpha(
|
||||
theme.palette.background.paper,
|
||||
0.98,
|
||||
)} 100%)`,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<CardContent className="p-5 sm:p-6">
|
||||
<Grid container spacing={3} alignItems="center">
|
||||
<Grid size={{ xs: 12, lg: 5 }}>
|
||||
<Box
|
||||
sx={{
|
||||
position: "relative",
|
||||
minHeight: 260,
|
||||
borderRadius: 5,
|
||||
overflow: "hidden",
|
||||
border: `1px solid ${alpha(theme.palette.primary.main, 0.1)}`,
|
||||
background: `radial-gradient(circle at 50% 20%, ${alpha(
|
||||
theme.palette.info.light,
|
||||
0.36,
|
||||
)} 0%, ${alpha(theme.palette.primary.main, 0.14)} 30%, ${alpha(
|
||||
theme.palette.background.paper,
|
||||
0.92,
|
||||
)} 75%)`,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
backgroundImage: `linear-gradient(${alpha(
|
||||
theme.palette.primary.main,
|
||||
0.08,
|
||||
)} 1px, transparent 1px), linear-gradient(90deg, ${alpha(
|
||||
theme.palette.primary.main,
|
||||
0.08,
|
||||
)} 1px, transparent 1px)`,
|
||||
backgroundSize: "28px 28px",
|
||||
}}
|
||||
/>
|
||||
|
||||
{[0, 1, 2].map((layer) => (
|
||||
<Box
|
||||
key={layer}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
insetInline: "14%",
|
||||
bottom: 30 + layer * 36,
|
||||
height: 56,
|
||||
borderRadius: "50%",
|
||||
border: `1px solid ${alpha(theme.palette.primary.main, 0.18)}`,
|
||||
background: `linear-gradient(180deg, ${alpha(
|
||||
theme.palette.background.paper,
|
||||
0.94 - layer * 0.14,
|
||||
)} 0%, ${alpha(theme.palette.primary.main, 0.12 + layer * 0.04)} 100%)`,
|
||||
transform: `perspective(900px) rotateX(68deg) scale(${1 - layer * 0.08})`,
|
||||
boxShadow: `0 16px 30px ${alpha(theme.palette.primary.main, 0.08)}`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
{healthItems.slice(0, 4).map((item, index) => {
|
||||
const palette = theme.palette[item.meta.paletteKey];
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={item.key}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 38 + index * 34,
|
||||
insetInlineStart: index % 2 === 0 ? "18%" : "56%",
|
||||
px: 1.25,
|
||||
py: 0.75,
|
||||
borderRadius: 3,
|
||||
border: `1px solid ${alpha(palette.main, 0.28)}`,
|
||||
backgroundColor: alpha(palette.main, 0.12),
|
||||
backdropFilter: "blur(8px)",
|
||||
boxShadow: `0 10px 24px ${alpha(palette.main, 0.14)}`,
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption" display="block" fontWeight={700}>
|
||||
{item.label}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{item.value}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
insetInline: 20,
|
||||
bottom: 18,
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Chip size="small" color="success" label={`${formatPersianNumber(goodCount)} سالم`} />
|
||||
<Chip size="small" color="warning" label={`${formatPersianNumber(warningCount)} بررسی`} />
|
||||
<Chip size="small" color="error" label={`${formatPersianNumber(criticalCount)} بحرانی`} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, lg: 7 }}>
|
||||
<Stack spacing={2.5}>
|
||||
<Box>
|
||||
<Typography variant="h5" className="font-semibold">
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" className="mt-1">
|
||||
{description}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
p: 2,
|
||||
borderRadius: 4,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
backgroundColor: alpha(theme.palette.background.paper, 0.74),
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption" color="text.disabled">
|
||||
سنسور فعال
|
||||
</Typography>
|
||||
<Typography variant="h6" className="mt-1">
|
||||
{sensorName || "سنسور انتخاب نشده"}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{healthItems.length ? (
|
||||
<Grid container spacing={2}>
|
||||
{healthItems.map((item) => (
|
||||
<Grid key={item.key} size={{ xs: 12, sm: 6 }}>
|
||||
<Box
|
||||
sx={{
|
||||
p: 2,
|
||||
borderRadius: 4,
|
||||
border: `1px solid ${alpha(
|
||||
theme.palette[item.meta.paletteKey].main,
|
||||
0.2,
|
||||
)}`,
|
||||
backgroundColor: alpha(
|
||||
theme.palette[item.meta.paletteKey].main,
|
||||
0.08,
|
||||
),
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
spacing={1}
|
||||
className="mb-2"
|
||||
>
|
||||
<Typography variant="body2" fontWeight={700}>
|
||||
{item.label}
|
||||
</Typography>
|
||||
<Chip size="small" color={item.meta.muiColor} label={item.meta.label} />
|
||||
</Stack>
|
||||
<Typography variant="h6">{item.value}</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
) : (
|
||||
<Alert severity="info">{emptyMessage}</Alert>
|
||||
)}
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default SensorHealthPanel;
|
||||
Reference in New Issue
Block a user