Upload files to "z2"
This commit is contained in:
parent
e7da16d9c6
commit
997111a7ba
11
z2/Dockerfile
Normal file
11
z2/Dockerfile
Normal file
@ -0,0 +1,11 @@
|
||||
FROM python:3.9-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . /app/
|
||||
|
||||
RUN pip install flask mysql-connector-python
|
||||
|
||||
EXPOSE 5000
|
||||
|
||||
CMD ["python", "app.py"]
|
7
z2/Dockerfile.mysql
Normal file
7
z2/Dockerfile.mysql
Normal file
@ -0,0 +1,7 @@
|
||||
FROM mysql:8.0
|
||||
|
||||
ENV MYSQL_ALLOW_EMPTY_PASSWORD=true
|
||||
|
||||
COPY sensors_db.sql /docker-entrypoint-initdb.d/
|
||||
|
||||
EXPOSE 3306
|
104
z2/README.md
Normal file
104
z2/README.md
Normal file
@ -0,0 +1,104 @@
|
||||
# Interactive Floor Plan Application with Kubernetes
|
||||
|
||||
## Application Description
|
||||
|
||||
This application is an interactive floor plan visualization tool that displays sensor data on different floors of a building. Users can navigate between floors using the buttons at the bottom of the page and click on sensors to view their current data.
|
||||
|
||||
## Container List
|
||||
|
||||
1. **floorplan-webapp**
|
||||
- Web application container based on Python and Flask
|
||||
- Serves the frontend HTML, CSS, and JavaScript files
|
||||
- Provides an API endpoint for sensor data
|
||||
|
||||
2. **mysql-db**
|
||||
- MySQL database container
|
||||
- Stores sensor information including position, floor, location, and values
|
||||
|
||||
## Kubernetes Objects
|
||||
|
||||
1. **Namespace**
|
||||
- `sensor-app`: Isolated environment for all application resources
|
||||
|
||||
2. **Deployment**
|
||||
- `floorplan-webapp`: Manages the web application with 2 replicas for high availability
|
||||
|
||||
3. **StatefulSet**
|
||||
- `mysql`: Manages the MySQL database instance with persistent storage
|
||||
|
||||
4. **Services**
|
||||
- `floorplan-service`: NodePort service exposing the web application on port 30080
|
||||
- `mysql-service`: Headless service for MySQL database access
|
||||
|
||||
5. **PersistentVolume**
|
||||
- `mysql-pv`: 1GB volume for MySQL data storage
|
||||
|
||||
6. **PersistentVolumeClaim**
|
||||
- `mysql-pvc`: Claims storage from the persistent volume for MySQL
|
||||
|
||||
## Network and Storage Configuration
|
||||
|
||||
### Networks
|
||||
- The application uses Kubernetes' built-in networking
|
||||
- Web application connects to MySQL using the `mysql-service` DNS name
|
||||
- External access is provided through a NodePort service on port 30080
|
||||
|
||||
### Storage
|
||||
- The MySQL database uses a persistent volume mounted at `/var/lib/mysql`
|
||||
- The persistent volume is backed by a hostPath at `/mnt/data` on the host machine
|
||||
|
||||
## Container Configuration
|
||||
|
||||
### Web Application Container
|
||||
- Based on Python 3.9 slim image
|
||||
- Installed packages: Flask, mysql-connector-python
|
||||
- Configured to connect to MySQL database using environment variables
|
||||
- Exposes port 5000
|
||||
|
||||
### MySQL Container
|
||||
- Based on MySQL 8.0 image
|
||||
- Configured with empty root password for development purposes
|
||||
- Initializes with the sensors_db database and sample data
|
||||
- Exposes port 3306
|
||||
|
||||
## Application Management
|
||||
|
||||
### Preparation
|
||||
1. Make all scripts executable:
|
||||
```
|
||||
chmod +x prepare-app.sh start-app.sh stop-app.sh
|
||||
```
|
||||
2. Run the preparation script:
|
||||
```
|
||||
./prepare-app.sh
|
||||
```
|
||||
This will:
|
||||
- Create the required namespace
|
||||
- Prepare the persistent volume directory
|
||||
- Build the Docker images
|
||||
|
||||
### Starting the Application
|
||||
1. Run the start script:
|
||||
```
|
||||
./start-app.sh
|
||||
```
|
||||
This will:
|
||||
- Apply all Kubernetes configurations
|
||||
- Wait for all components to be ready
|
||||
- Display the URL to access the application
|
||||
|
||||
### Stopping the Application
|
||||
1. Run the stop script:
|
||||
```
|
||||
./stop-app.sh
|
||||
```
|
||||
This will:
|
||||
- Remove all Kubernetes objects created for the application
|
||||
- The persistent volume data will remain intact
|
||||
|
||||
### Viewing the Application
|
||||
1. Open your web browser
|
||||
2. Navigate to the URL displayed after running the start script (typically http://[NODE-IP]:30080)
|
||||
3. You should see the interactive floor plan with sensors
|
||||
4. Use the buttons at the bottom to switch between floors
|
||||
5. Click on sensors to view their data in the right panel
|
160
z2/app.js
Normal file
160
z2/app.js
Normal file
@ -0,0 +1,160 @@
|
||||
// Global variable to store the current floor
|
||||
let currentFloor = 1;
|
||||
|
||||
// Fallback data in case API fails
|
||||
const fallbackSensors = {
|
||||
1: [
|
||||
{ id: 1, x: 100, y: 100, floor: 1, location: "Room A", value: "22°C" },
|
||||
{ id: 2, x: 200, y: 200, floor: 1, location: "Room B", value: "24°C" },
|
||||
],
|
||||
2: [
|
||||
{ id: 3, x: 150, y: 150, floor: 2, location: "Living Room", value: "23°C" },
|
||||
{ id: 4, x: 250, y: 250, floor: 2, location: "Kitchen", value: "21°C" },
|
||||
],
|
||||
3: [
|
||||
{ id: 5, x: 120, y: 120, floor: 3, location: "Hallway", value: "22°C" },
|
||||
{ id: 6, x: 220, y: 220, floor: 3, location: "Bathroom", value: "23°C" },
|
||||
]
|
||||
};
|
||||
|
||||
// Function to fetch sensors from the API
|
||||
async function fetchSensors() {
|
||||
try {
|
||||
const response = await fetch('/api/sensors');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
console.log("Fetched sensors:", data);
|
||||
|
||||
if (data.length === 0) {
|
||||
console.warn("No sensors returned from API, using fallback data");
|
||||
return fallbackSensors[currentFloor];
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Could not fetch sensors:", error);
|
||||
console.warn("Using fallback sensor data due to API error");
|
||||
return fallbackSensors[currentFloor];
|
||||
}
|
||||
}
|
||||
|
||||
// Function to display the map for the corresponding floor
|
||||
function showFloor(floor) {
|
||||
console.log(`Showing floor ${floor}`);
|
||||
currentFloor = floor;
|
||||
const map = document.getElementById('map');
|
||||
map.src = `images/floor${floor}.png`;
|
||||
|
||||
// Fetch and display sensors for this floor
|
||||
fetchSensors().then(sensors => {
|
||||
// Filter sensors for the current floor
|
||||
const floorSensors = sensors.filter(sensor => parseInt(sensor.floor) === parseInt(floor));
|
||||
console.log(`Sensors for floor ${floor}:`, floorSensors);
|
||||
|
||||
if (floorSensors.length === 0) {
|
||||
console.warn(`No sensors found for floor ${floor}, adding test sensors`);
|
||||
// Add test sensors if none found
|
||||
addTestSensors();
|
||||
} else {
|
||||
addSensorsToMap(floorSensors);
|
||||
}
|
||||
|
||||
// Clear the sensor data table
|
||||
const table = document.getElementById('sensor-data');
|
||||
table.innerHTML = `
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Location</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
// Add sensor points to the map
|
||||
function addSensorsToMap(floorSensors) {
|
||||
// First clear all previous points
|
||||
const mapContainer = document.getElementById('map-container');
|
||||
const existingPoints = document.querySelectorAll('.sensor');
|
||||
existingPoints.forEach(point => point.remove());
|
||||
|
||||
console.log("Map container:", mapContainer);
|
||||
|
||||
// Check if we have sensors to display
|
||||
if (!floorSensors || floorSensors.length === 0) {
|
||||
console.warn("No sensors found for this floor");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new points
|
||||
floorSensors.forEach(sensor => {
|
||||
console.log(`Creating sensor point at x:${sensor.x}, y:${sensor.y}`);
|
||||
const sensorPoint = document.createElement('div');
|
||||
sensorPoint.classList.add('sensor');
|
||||
sensorPoint.style.position = 'absolute';
|
||||
sensorPoint.style.left = `${sensor.x}px`;
|
||||
sensorPoint.style.top = `${sensor.y}px`;
|
||||
sensorPoint.style.width = '15px';
|
||||
sensorPoint.style.height = '15px';
|
||||
sensorPoint.style.borderRadius = '50%';
|
||||
sensorPoint.style.backgroundColor = 'red';
|
||||
sensorPoint.style.border = '2px solid black';
|
||||
sensorPoint.style.zIndex = '1000';
|
||||
sensorPoint.setAttribute('data-id', sensor.id);
|
||||
sensorPoint.addEventListener('click', () => showSensorData(sensor));
|
||||
|
||||
mapContainer.appendChild(sensorPoint);
|
||||
});
|
||||
}
|
||||
|
||||
// Add test sensors if API sensors aren't working
|
||||
function addTestSensors() {
|
||||
const testSensors = fallbackSensors[currentFloor];
|
||||
addSensorsToMap(testSensors);
|
||||
}
|
||||
|
||||
// Display sensor data in the dashboard
|
||||
function showSensorData(sensor) {
|
||||
const table = document.getElementById('sensor-data');
|
||||
|
||||
// Clear the table
|
||||
table.innerHTML = `
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Location</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
// Add a new row with sensor data
|
||||
const row = table.insertRow(1);
|
||||
row.insertCell(0).textContent = sensor.id;
|
||||
row.insertCell(1).textContent = sensor.location;
|
||||
row.insertCell(2).textContent = sensor.value;
|
||||
}
|
||||
|
||||
// Initialize the app by showing floor 1 when the page loads
|
||||
window.onload = function() {
|
||||
console.log("Page loaded, initializing app...");
|
||||
showFloor(1);
|
||||
|
||||
// Add additional test sensor directly to verify positioning
|
||||
setTimeout(() => {
|
||||
const mapContainer = document.getElementById('map-container');
|
||||
const testPoint = document.createElement('div');
|
||||
testPoint.style.position = 'absolute';
|
||||
testPoint.style.left = '300px';
|
||||
testPoint.style.top = '300px';
|
||||
testPoint.style.width = '20px';
|
||||
testPoint.style.height = '20px';
|
||||
testPoint.style.borderRadius = '50%';
|
||||
testPoint.style.backgroundColor = 'blue';
|
||||
testPoint.style.border = '3px solid yellow';
|
||||
testPoint.style.zIndex = '9999';
|
||||
|
||||
mapContainer.appendChild(testPoint);
|
||||
console.log("Test point added to verify positioning");
|
||||
}, 3000);
|
||||
};
|
67
z2/app.py
Normal file
67
z2/app.py
Normal file
@ -0,0 +1,67 @@
|
||||
from flask import Flask, jsonify, send_from_directory
|
||||
import mysql.connector
|
||||
import os
|
||||
import time
|
||||
|
||||
app = Flask(__name__, static_folder='.', static_url_path='')
|
||||
|
||||
# Database connection with environment variables
|
||||
def get_db_connection():
|
||||
# Get environment variables or use defaults
|
||||
host = os.environ.get('MYSQL_HOST', 'mysql-service.sensor-app')
|
||||
user = os.environ.get('MYSQL_USER', 'root')
|
||||
password = os.environ.get('MYSQL_PASSWORD', '')
|
||||
database = os.environ.get('MYSQL_DB', 'sensors_db')
|
||||
|
||||
print(f"Connecting to MySQL: {host}, {user}, {database}")
|
||||
|
||||
# Try to connect with retries for Kubernetes startup sequence
|
||||
max_retries = 50
|
||||
retry_delay = 50
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
conn = mysql.connector.connect(
|
||||
host=host,
|
||||
user=user,
|
||||
password=password,
|
||||
database=database
|
||||
)
|
||||
print("Database connection successful!")
|
||||
return conn
|
||||
except mysql.connector.Error as err:
|
||||
print(f"Database connection error: {err}")
|
||||
if attempt < max_retries - 1:
|
||||
print(f"Database connection failed, retrying in {retry_delay} seconds...")
|
||||
time.sleep(retry_delay)
|
||||
else:
|
||||
print(f"Failed to connect to database after {max_retries} attempts")
|
||||
raise
|
||||
|
||||
@app.route('/api/sensors', methods=['GET'])
|
||||
def get_sensors():
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute('SELECT * FROM sensors')
|
||||
sensors = cursor.fetchall()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
print(f"Sensors fetched from database: {sensors}")
|
||||
return jsonify(sensors)
|
||||
except Exception as e:
|
||||
print(f"Error fetching sensors: {e}")
|
||||
# Return empty array instead of error for better frontend handling
|
||||
return jsonify([])
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return send_from_directory('.', 'index.html')
|
||||
|
||||
# Serve static files
|
||||
@app.route('/<path:path>')
|
||||
def static_files(path):
|
||||
return send_from_directory('.', path)
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True, host='0.0.0.0')
|
31
z2/deployment.yaml
Normal file
31
z2/deployment.yaml
Normal file
@ -0,0 +1,31 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: floorplan-webapp
|
||||
namespace: sensor-app
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app: floorplan-webapp
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: floorplan-webapp
|
||||
spec:
|
||||
containers:
|
||||
- name: webapp
|
||||
image: floorplan-webapp:latest
|
||||
imagePullPolicy: Never
|
||||
ports:
|
||||
- containerPort: 5000
|
||||
env:
|
||||
- name: MYSQL_HOST
|
||||
value: mysql-service.sensor-app
|
||||
resources:
|
||||
requests:
|
||||
memory: "128Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "256Mi"
|
||||
cpu: "200m"
|
BIN
z2/floor1.png
Normal file
BIN
z2/floor1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 114 KiB |
BIN
z2/floor2.png
Normal file
BIN
z2/floor2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 136 KiB |
BIN
z2/floor3.png
Normal file
BIN
z2/floor3.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 790 KiB |
42
z2/index.html
Normal file
42
z2/index.html
Normal file
@ -0,0 +1,42 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Building Map</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="header-content">
|
||||
<h1>Interactive Floor Plan with Sensors</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="main-content">
|
||||
<div id="map-container">
|
||||
<img id="map" src="images/floor1.png" alt="Floor 1">
|
||||
<!-- Sensor points will be added here dynamically -->
|
||||
</div>
|
||||
<div id="dashboard">
|
||||
<h2>Sensor Data</h2>
|
||||
<table id="sensor-data">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Location</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
<!-- Sensor data will be inserted here -->
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="buttons-container">
|
||||
<button onclick="showFloor(1)">Floor 1</button>
|
||||
<button onclick="showFloor(2)">Floor 2</button>
|
||||
<button onclick="showFloor(3)">Floor 3</button>
|
||||
</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
17
z2/prepare-app.sh
Normal file
17
z2/prepare-app.sh
Normal file
@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Create namespace
|
||||
kubectl create namespace sensor-app
|
||||
|
||||
# Create directory for persistent volume if not exists
|
||||
sudo mkdir -p /mnt/data
|
||||
sudo chmod 777 /mnt/data
|
||||
|
||||
# Build Docker images
|
||||
echo "Building web application image..."
|
||||
docker build -t floorplan-webapp:latest -f Dockerfile .
|
||||
|
||||
echo "Building MySQL database image..."
|
||||
docker build -t mysql-db:latest -f Dockerfile.mysql .
|
||||
|
||||
echo "Application prepared successfully!"
|
BIN
z2/sensors_db.sql
Normal file
BIN
z2/sensors_db.sql
Normal file
Binary file not shown.
26
z2/service.yaml
Normal file
26
z2/service.yaml
Normal file
@ -0,0 +1,26 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: floorplan-service
|
||||
namespace: sensor-app
|
||||
spec:
|
||||
type: NodePort
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 5000
|
||||
nodePort: 30080
|
||||
selector:
|
||||
app: floorplan-webapp
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: mysql-service
|
||||
namespace: sensor-app
|
||||
spec:
|
||||
ports:
|
||||
- port: 3306
|
||||
targetPort: 3306
|
||||
selector:
|
||||
app: mysql
|
||||
clusterIP: None # Headless service for StatefulSet
|
34
z2/start-app.sh
Normal file
34
z2/start-app.sh
Normal file
@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Apply Kubernetes configurations
|
||||
echo "Creating Kubernetes resources..."
|
||||
|
||||
# Create namespace if it doesn't exist
|
||||
kubectl get namespace sensor-app || kubectl create namespace sensor-app
|
||||
|
||||
# Apply configurations
|
||||
echo "Applying PersistentVolume, PersistentVolumeClaim, and StatefulSet..."
|
||||
kubectl apply -f statefulset.yaml
|
||||
|
||||
echo "Waiting for MySQL StatefulSet to be ready..."
|
||||
kubectl wait --for=condition=ready pod -l app=mysql --timeout=120s -n sensor-app
|
||||
|
||||
echo "Applying Deployment..."
|
||||
kubectl apply -f deployment.yaml
|
||||
|
||||
echo "Applying Service..."
|
||||
kubectl apply -f service.yaml
|
||||
|
||||
echo "Waiting for application to be ready..."
|
||||
kubectl wait --for=condition=ready pod -l app=floorplan-webapp --timeout=60s -n sensor-app
|
||||
|
||||
LOCAL_PORT=8080
|
||||
echo ""
|
||||
echo "Application started successfully!"
|
||||
echo "Setting up port forwarding from localhost:$LOCAL_PORT to the service..."
|
||||
echo "You can access the application at: http://localhost:$LOCAL_PORT"
|
||||
echo ""
|
||||
echo "Press Ctrl+C to stop port forwarding when done"
|
||||
|
||||
# Start port forwarding
|
||||
kubectl port-forward -n sensor-app service/floorplan-service $LOCAL_PORT:80
|
66
z2/statefulset.yaml
Normal file
66
z2/statefulset.yaml
Normal file
@ -0,0 +1,66 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: mysql-pv
|
||||
namespace: sensor-app
|
||||
labels:
|
||||
type: local
|
||||
spec:
|
||||
storageClassName: manual
|
||||
capacity:
|
||||
storage: 1Gi
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
hostPath:
|
||||
path: "/mnt/data"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: mysql-pvc
|
||||
namespace: sensor-app
|
||||
spec:
|
||||
storageClassName: manual
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: mysql
|
||||
namespace: sensor-app
|
||||
spec:
|
||||
serviceName: mysql-service
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: mysql
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: mysql
|
||||
spec:
|
||||
containers:
|
||||
- name: mysql
|
||||
image: mysql-db:latest
|
||||
imagePullPolicy: Never
|
||||
ports:
|
||||
- containerPort: 3306
|
||||
name: mysql
|
||||
env:
|
||||
- name: MYSQL_ALLOW_EMPTY_PASSWORD
|
||||
value: "true"
|
||||
volumeMounts:
|
||||
- name: mysql-persistent-storage
|
||||
mountPath: /var/lib/mysql
|
||||
resources:
|
||||
limits:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
volumes:
|
||||
- name: mysql-persistent-storage
|
||||
persistentVolumeClaim:
|
||||
claimName: mysql-pvc
|
10
z2/stop-app.sh
Normal file
10
z2/stop-app.sh
Normal file
@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Stopping and removing Kubernetes resources..."
|
||||
|
||||
# Delete all resources in the namespace
|
||||
kubectl delete -f service.yaml
|
||||
kubectl delete -f deployment.yaml
|
||||
kubectl delete -f statefulset.yaml
|
||||
|
||||
echo "Application stopped successfully!"
|
96
z2/styles.css
Normal file
96
z2/styles.css
Normal file
@ -0,0 +1,96 @@
|
||||
/* General Styles */
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
|
||||
header {
|
||||
background-color: #333;
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
#map-container {
|
||||
position: relative;
|
||||
width: 70%;
|
||||
height: 500px;
|
||||
border: 1px solid #ddd;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#map {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Sensor point styling */
|
||||
.sensor {
|
||||
position: absolute;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: 50%;
|
||||
background-color: red;
|
||||
border: 2px solid black;
|
||||
cursor: pointer;
|
||||
z-index: 100;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.sensor:hover {
|
||||
transform: scale(1.5);
|
||||
}
|
||||
|
||||
#dashboard {
|
||||
width: 25%;
|
||||
padding: 20px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #ddd;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
#sensor-data {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
#sensor-data th, #sensor-data td {
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
/* Button Styles */
|
||||
.buttons-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
margin: 5px;
|
||||
cursor: pointer;
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #45a049;
|
||||
}
|
Loading…
Reference in New Issue
Block a user