Building a Locomotive System (Simulation) with React.js, Node.js, Java Spring, Kafka, MongoDB, MySQL, Logging, and Telegram Notifications
Hello everyone,
I wanted to talk about how I made a locomotive simulation system with React , Node.js , Java Spring, Kafka , MongoDB , Oracle MySQL , logging, and Telegram Notifications. This system's strength lies in its use of microservices, breaking down tasks into smaller, cohesive units that function together seamlessly.
In this article, I’ll show you how to make a system that makes dummy locomotive data regularly. It saves this data, makes simple summary reports, and sends them through Telegram. Also, there’s a live dashboard that shows real-time locomotive info.
To make this happen, I used a Java Spring Scheduler to create a schedule that generates dummy locomotive data every 10 seconds. This data includes important info like the locomotive's code, name, dimension, status, and when it was recorded.
In handling the data, the Java Spring Scheduler takes charge of storing it. Simultaneously, the Node.js API helps out by fetching data from Kafka and smoothly moving it into MongoDB. This way of working ensures that the system manages locomotive data effectively, making it easy to handle a lot of data without slowing down.
Besides, I created a Scheduler Report using Java Spring Scheduler to produce detailed summaries every 11 seconds. This specialized system collects data from MongoDB and compiles essential locomotive information. These summaries are saved in MySQL databases, making it simple to retrieve and analyze them.
Furthermore, I added a notification feature to send these brief reports directly to Telegram for quick access. To keep track of everything and help with troubleshooting, I included a comprehensive logging system in the Scheduler. This whole setup ensures not just efficient report creation but also smooth delivery, while maintaining detailed logs for analysis and problem-solving.
For the user interface, I developed a Front-End Dashboard using React.js and Vite. This dashboard provides an intuitive and informative interface displaying real-time locomotive information for enhanced monitoring and analysis purposes.
Environment Set Up
Firstly, we need to prepare the system's environment. I'll be demonstrating the setup using IntelliJ because that's what I'm using, but feel free to use any other platform that suits you best.
To get started, let's use IntelliJ to create a new empty project. Once that's done, we can start building different modules within this project. But before we dive into creating these modules, let's set up where we'll store our data.
Kafka
To set up Kafka, you'll need to download it first. Get the Kafka file from this link, ensuring it's a Binary download. Extract the file and save its contents in a directory of your choice. Change the name of the extracted folder with 'kafka'.
Next, copy the path of the Kafka folder. Then, navigate to the 'config' directory within the Kafka folder and open the 'zookeeper.properties' file. Find the specified path next to the 'dataDir' field and append /zookeeper to this path.
In the same 'config' folder, locate and open the 'server.properties' file. Scroll down to find the 'log.dirs' field and paste the copied path. Then, to this path, append /kafka-logs.
With the zookeeper and Kafka server configuration done, let's proceed. Open the command prompt and navigate to the Kafka folder by changing the directory. To kick things off, start zookeeper by entering the following command:
.\bin\windows\zookeeper-server-start.bat .\config\zookeeper.properties
Next, open a new command prompt and navigate to the Kafka folder by changing the directory. Then, run the Kafka server using the following command:
.\bin\windows\kafka-server-start.bat .\config\server.properties
Kafka is now up and running, all set to stream data.
MongoDB
In this setup, I'm using MongoDB through MongoDB Compass. When you have MongoDB Compass ready, create a new connection named 'locomotive'. After setting up the new connection, the next step is to create a new database. I'll name the database 'locomotive-db' and the collection 'locomotive_infos'. Feel free to personalize all the necessary names according to your preferences.
Once the database is created within our connection, your MongoDB setup is ready to go.
MySQL
Initially, you need to establish a connection to MySQL. Once you have it set up, proceed to create a database containing two tables within it. Once again, feel free to personalize all the necessary names according to your preferences.
Once we create the database with the two tables inside, the next step is to initialize initial values within both tables.
We can initialize the data because we already know several data categories we want, such as the three existing locomotive statuses and the ten different types of locomotives. Once all of this is completed, your MySQL setup is ready to go.
Node.js (Express)
Once our data storage is set up, let's add the first module to our empty project, which will be Node.js (Express). You can name it as you like, just remember to save it within the project you've created earlier.
Once we create the Node.js module, our project structure will look like this.
However, we'll keep it simple to avoid needing multiple files. Let's structure it like this instead. As you can see, we only have one necessary file, which is index file.
Next, to get data from Kafka and save it into MongoDB using Node.js API, we'll need the 'kafka-node' and 'mongoose' packages. Let's install these packages first (ensure to install them within the express module/directory).
npm install kafka-node
npm install mongoose
Then, in our 'index' file, we'll import these packages, configure the port for our Node.js service (feel free to modify the port to your preferred choice), and establish connections to Kafka and MongoDB.
const express = require('express');
const kafka = require('kafka-node');
const mongoose = require('mongoose');
const app = express();
const port = 3001;
mongoose.connect('mongodb://127.0.0.1:27017/locomotive-db')
.then(() => {
console.log('Terhubung ke database MongoDB');
})
.catch((err) => {
console.error('Gagal terhubung ke MongoDB:', err);
});
const client = new kafka.KafkaClient({ kafkaHost: 'localhost:9092' });
const consumer = new kafka.Consumer(client, [{ topic: 'loco' }]);
consumer.on('ready', () => {
console.log('Terhubung ke Kafka');
});
consumer.on('error', (err) => {
console.error('Ada kesalahan:', err);
});
app.listen(port, () => {
console.log(`Server Express berjalan di http://localhost:${port}`);
});
Now, if you can run it without any error. Then congratulations, your Node.js (Express) service is up and running.
Java Spring
For Java Spring, we require two services: one for the scheduler info and another for the scheduler report. Therefore, for the setup, we'll create two Java Spring services. Let's begin by setting up the Java Spring Scheduler Info.
Let's create a new module similar to when we created the Node.js (Express) module, but this time, let's choose Spring Initializr. Feel free to modify the names and settings as you prefer, but I'll set it up like this.
Next, we'll add dependencies. You can add them initially while creating a new module or later in the pom.xml file. These dependencies are essential for the Java Spring Info Scheduler:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-parent</artifactId>
<version>2.0.5</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.8</version>
</dependency>
After adding the dependencies, navigate to 'src/main/resources/application.properties', then add the following lines to configure with Kafka.
spring.kafka.bootstrap-servers=localhost:9092
spring.kafka.consumer.group-id=locomotive-group
Once you've completed these steps, the Java Spring Info Scheduler service is ready to go. We'll build its functionalities in the next stage.
Moving forward, let's create the service for the Java Spring Report Scheduler.
These are the dependencies used within the Java Spring Report Scheduler:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-parent</artifactId>
<version>2.0.5</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.8</version>
</dependency>
<dependency>
<groupId>org.telegram</groupId>
<artifactId>telegrambots</artifactId>
<version>6.7.0</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
After adding the dependencies, similar to the Info Scheduler, we need to add a few lines in 'src/main/resources/application.properties'. These lines are meant to connect with MongoDB and MySQL, and we'll also configure the port to avoid conflicts with the Info Scheduler.
# MongoDB configuration
spring.data.mongodb.host=localhost
spring.data.mongodb.port=27017
spring.data.mongodb.database=locomotive-db
# MySQL configuration
spring.datasource.url=jdbc:mysql://localhost:3306/locomotive_summary
spring.datasource.username=root
spring.datasource.password=
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.hibernate.ddl-auto=update
# Port Configuration
server.port=9090
Once you've completed these steps, the Java Spring Report Scheduler service is also ready to go. We'll build its functionalities in the next stage.
React + Vite
To create the frontend dashboard, we'll use React.js with Vite. Let's begin by adding the modules for this.
Once we've created the React module using Vite, it will generate a default folder structure. We'll customize it according to our requirements by creating several new folders and files. For the new files, let's leave them empty for now.
Now, we'll install the necessary packages required to build the dashboard. For the Dashboard, I'll utilize Material UI and some animations to make it more visually appealing. Additionally, I'll use Axios for API handling. You can customize the front-end according to your preferences. Before installing, ensure that you're installing within your React folder.
npm install
npm install @mui/material @emotion/react @emotion/styled
npm install axios
npm install react-router-dom
npm install react-spring
npm install date-fns
npm install @mui/x-charts
npm install @mui/icons-material
Once you've installed the packages, now edit the index.html file to change the title and icon (you can name and use an image of your choice).
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dashboard</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
Congratulations, the setup for this project's environment is now complete! Next, we'll proceed to create functionalities for each of our services.
Recommended by LinkedIn
Developing Features and Functionalities
Java Spring Info Scheduler
The first service we'll work on is the info scheduler. Now, create two new packages (folders) to develop functionalities for the Info Scheduler.
Consumer and producer are two essential components in Kafka. It's beneficial to explore and understand them for a clearer understanding of Kafka's flow. Inside the 'consumer' folder, we'll create two new files.
The first file is 'KafkaConsumerConfig':
package com.locomotive.infoscheduler.consumer;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class KafkaConsumerConfig {
@Bean
public ConsumerFactory<String, String> consumerFactory() {
Map<String, Object> configProps = new HashMap<>();
configProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
configProps.put(ConsumerConfig.GROUP_ID_CONFIG, "locomotive-group");
configProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
configProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
return new DefaultKafkaConsumerFactory<>(configProps);
}
@Bean
public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
return factory;
}
}
The second file is 'MessageConsumer':
package com.locomotive.infoscheduler.consumer;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
@Component
public class MessageConsumer {
@KafkaListener(topics = "loco", groupId = "locomotive-group")
public void listen(String message) {
System.out.println("Received message: " + message);
}
}
Now Inside the 'producer' folder, we'll create two new files.
The first file is 'KafkaProducerConfig':
package com.locomotive.infoscheduler.producer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.core.ProducerFactory;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class KafkaProducerConfig {
@Bean
public ProducerFactory<String, String> producerFactory() {
Map<String, Object> configProps = new HashMap<>();
configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
return new DefaultKafkaProducerFactory<>(configProps);
}
@Bean
public KafkaTemplate<String, String> kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}
}
The second file is 'MessageProducer':
package com.locomotive.infoscheduler.producer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Random;
@Component
public class MessageProducer {
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
private static final String TOPIC = "loco";
private static final Logger log = LoggerFactory.getLogger(MessageProducer.class);
private final String[] locomotiveCodes = {"L001", "L002", "L003", "L004", "L005", "L006", "L007", "L008", "L009", "L010"};
private final String[] locomotiveNames = {"Loco 1", "Loco 2", "Loco 3", "Loco 4", "Loco 5", "Loco 6", "Loco 7", "Loco 8", "Loco 9", "Loco 10"};
private final String[] dimensions = {"10x5x3", "8x4x2", "12x6x4", "9x3x5", "11x7x6", "15x9x8", "13x6x3", "7x5x2", "14x8x7", "16x10x9"};
private final String[] statuses = {"Active", "Inactive", "Under Maintenance"};
@Scheduled(fixedRate = 10000)
public void sendMessage() {
Random random = new Random();
int index = random.nextInt(locomotiveCodes.length);
String code = locomotiveCodes[index];
String name = locomotiveNames[index];
String dimension = dimensions[index];
String status = statuses[random.nextInt(statuses.length)];
LocalDateTime now = LocalDateTime.now();
String dateTime = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
String message = "Locomotive Info - Code: " + code +
", Name: " + name +
", Dimension: " + dimension +
", Status: " + status +
", Date and Time: " + dateTime;
kafkaTemplate.send(TOPIC, message).whenComplete(
(stringStringSendResult, throwable) -> log.info("Message result: {}", stringStringSendResult)
);
}
}
Now that we've created the consumer and producer for Kafka, we need to edit the Application file to enable scheduling.
package com.locomotive.infoscheduler;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class InfoSchedulerApplication {
public static void main(String[] args) {
SpringApplication.run(InfoSchedulerApplication.class, args);
}
}
Run the service and our Spring Java Info Scheduler can generate locomotive info every ten seconds and store it in Kafka.
Node.js (Express)
Once the locomotive info is stored in Kafka, Node.js (Express) will retrieve that data and save it in MongoDB. To accomplish this, we need to add a few lines in the index file of the Node.js (Express) service as follows:
const express = require('express');
const kafka = require('kafka-node');
const mongoose = require('mongoose');
const app = express();
const port = 3001;
mongoose.connect('mongodb://127.0.0.1:27017/locomotive-db')
.then(() => {
console.log('Terhubung ke database MongoDB');
})
.catch((err) => {
console.error('Gagal terhubung ke MongoDB:', err);
});
const dataSchema = new mongoose.Schema({
Code: String,
Name: String,
Dimension: String,
Status: String,
DateTime: String,
});
const Data = mongoose.model('locomotive_infos', dataSchema);
const client = new kafka.KafkaClient({ kafkaHost: 'localhost:9092' });
const consumer = new kafka.Consumer(client, [{ topic: 'loco' }]);
consumer.on('ready', () => {
console.log('Terhubung ke Kafka');
});
consumer.on('error', (err) => {
console.error('Ada kesalahan:', err);
});
const dataToSave = [];
consumer.on('message', async (message) => {
const dataFromKafka = extractInfo(message.value);
if (dataFromKafka) {
dataToSave.push(dataFromKafka);
}
});
setInterval(async () => {
if (dataToSave.length > 0) {
try {
await Data.insertMany(dataToSave);
console.log(dataToSave);
console.log('Data disimpan ke MongoDB.');
dataToSave.length = 0;
} catch (error) {
console.error('Gagal menyimpan data ke MongoDB:', error);
}
}
}, 10000);
app.listen(port, () => {
console.log(`Server Express berjalan di http://localhost:${port}`);
});
const extractInfo = (message) => {
const regex = /Code: (.*?), Name: (.*?), Dimension: (.*?), Status: (.*?), Date and Time: (.*)/;
const match = message.match(regex);
if (match && match.length === 6) {
return {
Code: match[1],
Name: match[2],
Dimension: match[3],
Status: match[4],
DateTime: match[5]
};
}
return null;
};
Run the service and now the locomotive info we receive will be stored in MongoDB through the Node.js (Express) service.
Java Spring Report Scheduler
Next, after storing the locomotive info in MongoDB, that data will be summarized and saved in MySQL. Additionally, the summary will be sent via Telegram. Now, what we need to do is create the required file structure. We'll add several packages (folders) with a few files inside each.
Inside the 'component' folder, we'll create a new file named 'LokoSummaryBot':
package com.locomotive.reportscheduler.component;
import org.springframework.stereotype.Component;
import org.telegram.telegrambots.meta.api.methods.send.SendMessage;
import org.telegram.telegrambots.meta.exceptions.TelegramApiException;
import org.telegram.telegrambots.bots.TelegramLongPollingBot;
import org.telegram.telegrambots.meta.api.objects.Update;
@Component
public class LokoSummaryBot extends TelegramLongPollingBot {
private final String BOT_TOKEN = "6980637379:AAGMJSskh5cqelOpbRfS4XdksFiMLk4ZdWo";
private final String CHAT_ID = "1345918218";
@Override
public void onUpdateReceived(Update update) {
}
public void sendSummaryReport(String summary) {
SendMessage message = new SendMessage();
message.setChatId(CHAT_ID);
message.setText(summary);
try {
execute(message);
} catch (TelegramApiException e) {
e.printStackTrace();
}
}
@Override
public String getBotUsername() {
return "YourBotUsername";
}
@Override
public String getBotToken() {
return BOT_TOKEN;
}
}
This file is used to send reports via Telegram. There are a few prerequisites: creating a bot using BotFather in Telegram to obtain a bot token, and using userinfobot to obtain your chat ID.
Now Inside the 'configuration' folder, we'll create a new file named 'WebMvcConfig':
package com.locomotive.reportscheduler.configuration;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry cors) {
// @formatter:off
cors
.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("*")
.allowedHeaders("*")
.allowCredentials(true)
;
// @formatter:on
}
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.defaultContentType(new MediaType("application", "vnd.api+json"), MediaType.APPLICATION_JSON);
}
}
Inside the 'controller' folder, we'll create a new file named 'SummaryController':
package com.locomotive.reportscheduler.controller;
import com.locomotive.reportscheduler.model.SummaryLokoModel;
import com.locomotive.reportscheduler.model.SummaryStatusLokoModel;
import com.locomotive.reportscheduler.service.ReportLokomotifService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/summary")
public class SummaryController {
@Autowired
private ReportLokomotifService reportLokomotifService;
@GetMapping("/status")
public ResponseEntity<?> getStatusSummary() {
try {
List<SummaryStatusLokoModel> statusSummary = reportLokomotifService.getStatusSummary();
return ResponseEntity.ok(statusSummary);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Terjadi kesalahan saat mengambil status summary: " + e.getMessage());
}
}
@GetMapping("/locomotives")
public ResponseEntity<?> getLocomotiveSummary() {
try {
List<SummaryLokoModel> summaryLokoModels = reportLokomotifService.getLocomotiveSummary();
return ResponseEntity.ok(summaryLokoModels);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Terjadi kesalahan saat mengambil data: " + e.getMessage());
}
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResponseEntity<?> handleException(Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Terjadi kesalahan pada server: " + e.getMessage());
}
}
Inside the 'model' folder, we'll create three new files.
The first file is 'InfoLokomotifModel':
package com.locomotive.reportscheduler.model;
import org.bson.types.ObjectId;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import lombok.Data;
import org.springframework.data.mongodb.core.mapping.Field;
@Data
@Document(collection = "locomotive_infos")
public class InfoLokomotifModel {
@Id
private ObjectId id;
@Field("Code")
private String code;
@Field("Name")
private String name;
@Field("Dimension")
private String dimension;
@Field("Status")
private String status;
@Field("DateTime")
private String dateTime;
}
The second file is 'SummaryLokoModel':
package com.locomotive.reportscheduler.model;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Entity
@Data
@Table(name = "summary_loco")
@AllArgsConstructor
@NoArgsConstructor
public class SummaryLokoModel {
@Id
private String code;
private String name;
private Integer active;
private Integer inactive;
private Integer under_maintenance;
private LocalDateTime updated_at;
}
The third file is 'SummaryStatusLokoModel':
package com.locomotive.reportscheduler.model;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Entity
@Data
@Table(name = "summary_loco_status")
@AllArgsConstructor
@NoArgsConstructor
public class SummaryStatusLokoModel {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String status;
private Integer total;
private LocalDateTime updated_at;
}
Inside the 'repository' folder, we'll create three new files.
The first file is 'InfoLokomotifRepository':
package com.locomotive.reportscheduler.repository;
import com.locomotive.reportscheduler.model.InfoLokomotifModel;
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.repository.MongoRepository;
public interface InfoLokomotifRepository extends MongoRepository<InfoLokomotifModel, ObjectId> {
}
The second file is 'SummaryLokoRepository':
package com.locomotive.reportscheduler.repository;
import com.locomotive.reportscheduler.model.SummaryLokoModel;
import org.springframework.data.jpa.repository.JpaRepository;
public interface SummaryLokoRepository extends JpaRepository<SummaryLokoModel, String> {
SummaryLokoModel findByCode(String code);
}
The second file is 'SummaryStatusLokoRepository':
package com.locomotive.reportscheduler.repository;
import com.locomotive.reportscheduler.model.SummaryStatusLokoModel;
import org.springframework.data.jpa.repository.JpaRepository;
public interface SummaryStatusLokoRepository extends JpaRepository<SummaryStatusLokoModel, Integer> {
SummaryStatusLokoModel findByStatus(String status);
}
Finaly, inside the 'service' folder, we'll create a new file named 'ReportLokomotifService':
package com.locomotive.reportscheduler.service;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import com.locomotive.reportscheduler.component.LokoSummaryBot;
import com.locomotive.reportscheduler.model.InfoLokomotifModel;
import com.locomotive.reportscheduler.model.SummaryLokoModel;
import com.locomotive.reportscheduler.model.SummaryStatusLokoModel;
import com.locomotive.reportscheduler.repository.InfoLokomotifRepository;
import com.locomotive.reportscheduler.repository.SummaryLokoRepository;
import com.locomotive.reportscheduler.repository.SummaryStatusLokoRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
@Service
public class ReportLokomotifService {
private final InfoLokomotifRepository infoLokomotifRepository;
private final SummaryStatusLokoRepository summaryStatusLokoRepository;
private final SummaryLokoRepository summaryLokoRepository;
private static final Logger LOGGER = LoggerFactory.getLogger(ReportLokomotifService.class);
private final LokoSummaryBot telegramBot;
@Autowired
public ReportLokomotifService(InfoLokomotifRepository infoLokomotifRepository,
SummaryStatusLokoRepository summaryStatusLokoRepository,
SummaryLokoRepository summaryLokoRepository,
LokoSummaryBot telegramBot) {
this.infoLokomotifRepository = infoLokomotifRepository;
this.summaryStatusLokoRepository = summaryStatusLokoRepository;
this.summaryLokoRepository = summaryLokoRepository;
this.telegramBot = telegramBot;
}
@Scheduled(fixedRate = 10000)
public void LokoStatusSummary() {
try {
LOGGER.info("Menghitung summary status lokomotif...");
List<InfoLokomotifModel> allInfoLokomotif = infoLokomotifRepository.findAll();
StringBuilder summaryReport = new StringBuilder();
summaryReport.append("Summary Report - Locomotive Status:\n\n");
Map<String, Long> statusCounts = allInfoLokomotif.stream()
.filter(info -> info.getStatus() != null)
.collect(Collectors.groupingBy(InfoLokomotifModel::getStatus, Collectors.counting()));
statusCounts.forEach((status, count) -> {
SummaryStatusLokoModel existingSummary = summaryStatusLokoRepository.findByStatus(status);
summaryReport.append("Status - ")
.append(status)
.append(" : ")
.append(count.intValue())
.append("\n");
if (existingSummary != null) {
existingSummary.setTotal(count.intValue());
existingSummary.setUpdated_at(LocalDateTime.now());
summaryStatusLokoRepository.save(existingSummary);
} else {
SummaryStatusLokoModel newSummary = new SummaryStatusLokoModel();
newSummary.setStatus(status);
newSummary.setTotal(count.intValue());
newSummary.setUpdated_at(LocalDateTime.now());
summaryStatusLokoRepository.save(newSummary);
}
});
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formattedDateTime = LocalDateTime.now().format(formatter);
summaryReport.append("\n").append("Updated at: ").append(formattedDateTime).append("\n");
LOGGER.info("Summary status lokomotif telah diupdate.");
telegramBot.sendSummaryReport(summaryReport.toString());
} catch (Exception e) {
LOGGER.error("Terjadi kesalahan saat menghitung summary status lokomotif: " + e.getMessage(), e);
}
}
public List<SummaryStatusLokoModel> getStatusSummary() {
try {
List<SummaryStatusLokoModel> summaryList = summaryStatusLokoRepository.findAll();
return summaryList;
} catch (Exception e) {
LOGGER.error("Terjadi kesalahan saat mengambil status summary: " + e.getMessage(), e);
throw new RuntimeException("Gagal mengambil status summary: " + e.getMessage(), e);
}
}
@Scheduled(fixedRate = 11000)
public void LocomotiveSummary() {
try {
LOGGER.info("Membuat summary tiap lokomotif...");
List<InfoLokomotifModel> allInfoLokomotif = infoLokomotifRepository.findAll();
Map<String, Map<String, Long>> statusSummary = allInfoLokomotif.stream()
.collect(Collectors.groupingBy(
InfoLokomotifModel::getCode,
Collectors.groupingBy(
InfoLokomotifModel::getStatus,
Collectors.counting()
)
));
StringBuilder summaryReport = new StringBuilder();
summaryReport.append("Summary Report - Locomotive Status by Code:\n");
statusSummary.forEach((code, statusCountMap) -> {
summaryReport.append("\n").append("Code: ").append(code).append("\n");
statusCountMap.forEach((status, count) -> {
summaryReport.append("Status - ").append(status)
.append(" : ").append(count).append("\n");
});
SummaryLokoModel summary = summaryLokoRepository.findByCode(code);
summary.setActive(statusCountMap.getOrDefault("Active", 0L).intValue());
summary.setInactive(statusCountMap.getOrDefault("Inactive", 0L).intValue());
summary.setUnder_maintenance(statusCountMap.getOrDefault("Under Maintenance", 0L).intValue());
summary.setUpdated_at(LocalDateTime.now());
summaryLokoRepository.save(summary);
});
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formattedDateTime = LocalDateTime.now().format(formatter);
summaryReport.append("\n").append("Updated at: ").append(formattedDateTime).append("\n\n");
LOGGER.info("Proses pembuatan summary tiap lokomotif selesai.");
telegramBot.sendSummaryReport(summaryReport.toString());
} catch (Exception e) {
LOGGER.error("Terjadi kesalahan saat membuat summary tiap lokomotif: " + e.getMessage(), e);
}
}
public List<SummaryLokoModel> getLocomotiveSummary() {
try {
List<SummaryLokoModel> summaryList = summaryLokoRepository.findAll();
summaryList.sort(Comparator.comparing(SummaryLokoModel::getCode));
return summaryList;
} catch (Exception e) {
LOGGER.error("Terjadi kesalahan saat mengambil summary lokomotif: " + e.getMessage(), e);
throw new RuntimeException("Gagal mengambil summary lokomotif: " + e.getMessage(), e);
}
}
}
If everything has been added correctly, there's just one final step to ensure the Report Scheduler runs smoothly. We need to add one crucial line in the Application file to enable scheduling for the Report Scheduler, just like the Info Scheduler.
@EnableScheduling
Great! Now, the Java Spring Report Scheduler can retrieve data from MongoDB, summarize it, save it in MySQL, and send the summary via Telegram. It's also equipped with logging, facilitating the development process.
Dashboard
Once the report is ready, the next and final step is to display it to users through an engaging dashboard. Let's edit some of the files we created earlier in the React module for this purpose.
Let's edit the component files first.
AlertSnackBar.jsx:
import { Snackbar, Backdrop } from "@mui/material";
export default function AlertSnackBar({ open, handleCloseModal, column }) {
const vertical = "top";
const horizontal = "center";
let message = null;
if (column === 'Internal Server' || column === 0) {
message = 'Terjadi kesalahan server. Silakan coba kembali.';
}
return (
<div>
<Snackbar
anchorOrigin={{ vertical, horizontal }}
open={open}
onClose={handleCloseModal}
autoHideDuration={6000}
message={message}
key={vertical + horizontal}
ContentProps={{
sx: {
backgroundColor: '#CF1D1D',
fontFamily: "Inter, sans-serif",
fontWeight: 600,
display: 'block',
textAlign: "center",
paddingX: 4,
}
}}
/>
<Backdrop sx={{ color: "#fff", zIndex: (theme) => theme.zIndex.drawer + 1 }} open={open}/>
</div>
);
}
LocomotiveTable.jsx:
import React, { useEffect, useState } from 'react';
import { Table, TableHead, TableRow, TableCell, TableBody, Chip } from '@mui/material';
import { getLocomotiveSummary, getStatusSummary } from '../apis/index.js';
function LocomotiveTable({ summaryData }) {
const formatDate = (datetime) => {
console.log(datetime)
const year = datetime.slice(0, 4);
const month = datetime.slice(5, 7);
const day = datetime.slice(8, 10);
const hour = datetime.slice(11, 13);
const minute = datetime.slice(14, 16);
const second = datetime.slice(17, 19);
return `${month}/${day}/${year} ${hour}:${minute}:${second}`;
};
const tableHeaders = [
{ id: 'code', label: 'Code' },
{ id: 'name', label: 'Name' },
{ id: 'active', label: 'Active' },
{ id: 'inactive', label: 'Inactive' },
{ id: 'under_maintenance', label: 'Under Maintenance' },
{ id: 'updated_at', label: 'Updated At' },
];
return (
<Table sx={{ borderRadius: '12px', overflow: 'hidden' }}>
<TableHead>
<TableRow>
{tableHeaders.map((header) => (
<TableCell key={header.id} align="center">
{header.label}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{summaryData.map((row) => (
<TableRow key={row.code}>
{tableHeaders.map((header) => (
<TableCell
key={header.id}
align="center"
sx={{
borderBottom: '1px solid rgba(224, 224, 224, 1)',
borderRadius: '0',
paddingY: '15px',
}}
>
{header.id === 'active' || header.id === 'inactive' || header.id === 'under_maintenance' ? (
<Chip
label={row[header.id]}
color="primary"
sx={{
padding: '10px 8px',
height: 0,
backgroundColor:
header.id === 'active'
? '#36AE7C'
: header.id === 'inactive'
? '#EB5353'
: header.id === 'under_maintenance'
? '#F9D923'
: 'default',
color: '#FFFFFF',
}}
/>
) : header.id === 'updated_at' ? (
<span>
{formatDate(row[header.id].toString())}
</span>
) : (
row[header.id]
)}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
);
}
export default LocomotiveTable;
NavBar.jsx:
import React from 'react';
import { AppBar, Toolbar, Typography, IconButton } from '@mui/material';
import DashboardIcon from '@mui/icons-material/Dashboard';
import InfoIcon from '@mui/icons-material/Info';
import { Link } from 'react-router-dom';
const NavBar = () => {
const iconButtonStyle = {
color: '#000000',
};
return (
<AppBar position="static" sx={{ backgroundColor: '#FFFFFF', color: '#000000', boxShadow: 'none' }}>
<Toolbar>
<Link to="/dashboard" color="inherit" style={{ textDecoration: 'none' }}>
<IconButton color="inherit" disableRipple sx={iconButtonStyle}>
<DashboardIcon />
<Typography variant="h6" sx={{ fontSize: '1rem', marginLeft: '3px' }}>
Dashboard
</Typography>
</IconButton>
</Link>
<div style={{ flexGrow: 1 }} />
<Link to="/info" color="inherit" style={{ textDecoration: 'none' }}>
<IconButton color="inherit" disableRipple sx={iconButtonStyle}>
<InfoIcon />
<Typography variant="caption" sx={{ fontSize: '1rem', marginLeft: '3px' }}>Info</Typography>
</IconButton>
</Link>
</Toolbar>
</AppBar>
);
};
export default NavBar;
StatusInfo.jsx:
import React from 'react';
import { Divider, Stack, Typography } from '@mui/material';
import { PieChart } from '@mui/x-charts/PieChart';
import { useSpring, animated } from 'react-spring';
const AnimatedCount = ({ value }) => {
const props = useSpring({
value,
from: { value: 0 },
config: { duration: 500 },
});
return <animated.span>{props.value.interpolate((val) => Math.floor(val))}</animated.span>;
};
const StatusInfo = ({ activeCount, inactiveCount, maintenanceCount }) => {
const active = activeCount ?? 0;
const inactive = inactiveCount ?? 0;
const maintenance = maintenanceCount ?? 0;
return (
<Stack
direction={{ xs: 'column', sm: 'row' }}
spacing={{ xs: 1, sm: 2, md: 4 }}
divider={<Divider orientation="vertical" sx={{ height: '80px' }} />}
sx={{ display: 'flex', alignItems: 'center' }}
>
<Stack>
<Typography sx={{ fontWeight: 600, color: '#737373' }}>Active</Typography>
<Typography sx={{ fontSize: '2rem', fontWeight: 500, color: '#36AE7C' }}>
<AnimatedCount value={active} />
</Typography>
</Stack>
<Stack>
<Typography sx={{ fontWeight: 600, color: '#737373' }}>Inactive</Typography>
<Typography sx={{ fontSize: '2rem', fontWeight: 500, color: '#EB5353' }}>
<AnimatedCount value={inactive} />
</Typography>
</Stack>
<Stack>
<Typography sx={{ fontWeight: 600, color: '#737373' }}>Under Maintenance</Typography>
<Typography sx={{ fontSize: '2rem', fontWeight: 500, color: '#F9D923' }}>
<AnimatedCount value={maintenance} />
</Typography>
</Stack>
<PieChart
colors={['#36AE7C', '#EB5353', '#F9D923']}
series={[
{
data: [
{ id: 0, value: active, label: 'Active' },
{ id: 1, value: inactive, label: 'Inactive' },
{ id: 2, value: maintenance, label: 'Under Maintenance' },
],
innerRadius: 15,
outerRadius: 70,
paddingAngle: 5,
cornerRadius: 5,
cx: 65,
highlightScope: { faded: 'global', highlighted: 'item' },
faded: { innerRadius: 30, additionalRadius: -10, color: 'gray' },
},
]}
width={370}
height={200}
/>
</Stack>
);
};
export default StatusInfo;
Next, let's edit the pages files.
Dashboard.jsx:
import React, { useState, useEffect } from 'react';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import { Typography, Box } from '@mui/material';
import LocomotiveTable from "../components/LocomotiveTable.jsx";
import NavBar from "../components/NavBar.jsx";
import StatusInfo from "../components/StatusInfo.jsx";
import {getLocomotiveSummary, getStatusSummary} from "../apis/index.js";
import AlertSnackBar from "../components/AlertSnackBar.jsx";
const theme = createTheme({
typography: {
fontFamily: [
'Poppins',
'sans-serif',
].join(','),
},
palette: {
text: {
primary: '#737373',
},
},
});
function Dashboard() {
const [summaryStatusData, setSummaryStatusData] = useState([]);
const [summaryLocomotiveData, setSummaryLocomotiveData] = useState([]);
const [error, setError] = useState(false);
const fetchStatusData = async () => {
try {
const response = await getStatusSummary();
if (response.message == "Network Error") {
setError(true);
} else {
setSummaryStatusData(response);
setError(false);
}
} catch (error) {
setError(true);
console.error('Error fetching status data:', error);
}
};
const fetchLocomotiveData = async () => {
try {
const response = await getLocomotiveSummary();
console.log(response.message);
if (response.message == "Network Error") {
setError(true);
} else {
setSummaryLocomotiveData(response);
setError(false);
}
} catch (error) {
setError(true);
console.error('Error fetching locomotive data:', error);
}
};
useEffect(() => {
fetchStatusData();
fetchLocomotiveData();
const statusInterval = setInterval(fetchStatusData, 10001);
const locomotiveInterval = setInterval(fetchLocomotiveData, 11001);
return () => {
clearInterval(statusInterval);
clearInterval(locomotiveInterval);
};
}, []);
const activeCount = summaryStatusData.find(summary => summary.status === 'Active')?.total || 0;
const inactiveCount = summaryStatusData.find(summary => summary.status === 'Inactive')?.total || 0;
const maintenanceCount = summaryStatusData.find(summary => summary.status === 'Under Maintenance')?.total || 0;
return (
<ThemeProvider theme={theme}>
<div>
{error && (
<AlertSnackBar
open={true}
handleCloseModal={() => setError(false)}
column={'Internal Server'}
/>
)}
<NavBar/>
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', marginY: 2, gap: 2, flexDirection: 'column'}}>
<StatusInfo
activeCount={activeCount}
inactiveCount={inactiveCount}
maintenanceCount={maintenanceCount}
/>
<Box sx={{display: 'flex', width: '75%'}}>
<LocomotiveTable summaryData={summaryLocomotiveData} />
</Box>
<Typography fontSize="0.7rem" fontWeight={300} marginY={2}>© 2023 Locomotive Use Case</Typography>
</Box>
</div>
</ThemeProvider>
);
}
export default Dashboard;
Info.jsx:
import React from 'react';
import { Container, Typography, Grid, Paper, List, ListItem, ListItemText } from '@mui/material';
import {createTheme, ThemeProvider} from "@mui/material/styles";
import NavBar from "../components/NavBar.jsx";
const theme = createTheme({
typography: {
fontFamily: [
'Poppins',
'sans-serif',
].join(','),
},
palette: {
text: {
primary: '#2F2F2F',
},
},
});
const Info = () => {
const paperStyle = {
padding: '1rem',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
boxShadow: '0px 0px 2px rgba(0, 0, 0, 0.2)',
'&:hover': {
backgroundColor: '#FEFEFE'
},
};
return (
<ThemeProvider theme={theme}>
<NavBar />
<Container maxWidth="lg" sx={{ marginY: '2rem', color: '#2F2F2F' }}>
<Typography variant="h5" fontWeight={600} gutterBottom>
Locomotive Information Management Platform
</Typography>
<Typography variant="body2" fontWeight={300} gutterBottom>
This is a web platform designed for locomotive information management. It employs NodeJS (Express), Java Spring,
Kafka, MongoDB, and MySQL/Postgres, integrating various components for different tasks.
</Typography>
<Typography variant="h6" fontWeight={600} gutterBottom sx={{ marginTop: '2rem' }}>
Component Overview
</Typography>
<Grid container spacing={2}>
<Grid item sm={12} md={6}>
<Paper sx={{ ...paperStyle, minHeight: '3rem' }}>
<Typography variant="subtitle2">
NodeJS (Express)
</Typography>
<Typography variant="body2" fontWeight={300}>
Builds APIs to save/read data to/from Kafka and store it in MongoDB.
</Typography>
</Paper>
</Grid>
<Grid item sm={12} md={6}>
<Paper sx={{ ...paperStyle, minHeight: '3rem' }}>
<Typography variant="subtitle2">
Java Spring
</Typography>
<Typography variant="body2" fontWeight={300}>
Generates dummy locomotive details data every 10s and saves it to Kafka.
</Typography>
</Paper>
</Grid>
<Grid item sm={12} md={6}>
<Paper sx={{ ...paperStyle, minHeight: '3rem' }}>
<Typography variant="subtitle2">
Kafka (Message Broker)
</Typography>
<Typography variant="body2" fontWeight={300}>
Acts as a broker to store and retrieve data.
</Typography>
</Paper>
</Grid>
<Grid item sm={12} md={6}>
<Paper sx={{ ...paperStyle, minHeight: '3rem' }}>
<Typography variant="subtitle2">
MongoDB (NoSQL)
</Typography>
<Typography variant="body2" fontWeight={300}>
Stores data from Kafka.
</Typography>
</Paper>
</Grid>
<Grid item sm={12} md={6}>
<Paper sx={{ ...paperStyle, minHeight: '3rem' }}>
<Typography variant="subtitle2">
MySQL/Postgres
</Typography>
<Typography variant="body2" fontWeight={300}>
Stores summary data generated by the Scheduler Report.
</Typography>
</Paper>
</Grid>
</Grid>
<Typography variant="h6" fontWeight={600} gutterBottom sx={{ marginTop: '2rem' }}>
Key Features
</Typography>
<Grid container spacing={2}>
<Grid item sm={12} md={6}>
<Paper sx={{ ...paperStyle, minHeight: '4rem' }}>
<Typography variant="subtitle2">
Create Scheduler Info
</Typography>
<Typography variant="body2" fontWeight={300}>
The Java Spring Scheduler regularly generates dummy data about locomotive information and stores it in Kafka.
</Typography>
</Paper>
</Grid>
<Grid item sm={12} md={6}>
<Paper sx={{ ...paperStyle, minHeight: '4rem' }}>
<Typography variant="subtitle2">
Create API NodeJS
</Typography>
<Typography variant="body2" fontWeight={300}>
Reads data from Kafka, saving it to MongoDB.
</Typography>
</Paper>
</Grid>
<Grid item sm={12} md={6}>
<Paper sx={{ ...paperStyle, minHeight: '4rem' }}>
<Typography variant="subtitle2">
Create Scheduler Report
</Typography>
<Typography variant="body2" fontWeight={300}>
Fetches data from MongoDB, creates locomotive info summaries, saves them in MySQL/Postgres, and sends summary reports to Telegram.
</Typography>
</Paper>
</Grid>
<Grid item sm={12} md={6}>
<Paper sx={{ ...paperStyle, minHeight: '4rem' }}>
<Typography variant="subtitle2">
Create Front End Dashboard
</Typography>
<Typography variant="body2" fontWeight={300}>
Utilizes React Js + Vite to display locomotive information in an interactive dashboard format.
</Typography>
</Paper>
</Grid>
</Grid>
<Typography variant="h6" fontWeight={600} gutterBottom sx={{ marginTop: '2rem' }}>
Primary Functions
</Typography>
<Grid container spacing={2}>
<Grid item sm={12} md={6}>
<Paper sx={{ ...paperStyle, minHeight: '5rem' }}>
<Typography variant="subtitle2">
Data Monitoring and Management
</Typography>
<Typography variant="body2" fontWeight={300}>
The platform allows monitoring and managing locomotive-related data, from dummy data creation and storage to summary creation and visualization in the dashboard.
</Typography>
</Paper>
</Grid>
<Grid item sm={12} md={6}>
<Paper sx={{ ...paperStyle, minHeight: '5rem' }}>
<Typography variant="subtitle2">
Component Communication
</Typography>
<Typography variant="body2" fontWeight={300}>
Leveraging Kafka as a message broker, the platform communicates data across different components.
</Typography>
</Paper>
</Grid>
<Grid item sm={12} md={6}>
<Paper sx={{ ...paperStyle, minHeight: '5rem' }}>
<Typography variant="subtitle2">
User Interaction
</Typography>
<Typography variant="body2" fontWeight={300}>
The dashboard built with React allows users to see details about locomotives in a user-friendly and interactive way.
</Typography>
</Paper>
</Grid>
</Grid>
</Container>
</ThemeProvider>
);
};
export default Info;
After that, we need to set up fetching data from the API, which we'll do from the 'apis' folder in the index.js file:
import axios from "axios";
const API_BASE_URL = "http://localhost:9090/";
export const getStatusSummary = async () => {
try {
const url = `${API_BASE_URL}summary/status`;
const response = await axios.get(url);
const responseData = response.data;
return responseData;
} catch (error) {
const errorData = {
status: error.response ? error.response.status : null,
data: error.response ? error.response.data : null,
message: error.message,
};
return errorData;
}
};
export const getLocomotiveSummary = async () => {
try {
const url = `${API_BASE_URL}summary/locomotives`;
const response = await axios.get(url);
const responseData = response.data;
return responseData;
} catch (error) {
const errorData = {
status: error.response ? error.response.status : null,
data: error.response ? error.response.data : null,
message: error.message,
};
return errorData;
}
};
Now, we just need to set up the routing or navigation in main.jsx:
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import Dashboard from './pages/Dashboard.jsx';
import Info from "./pages/Info.jsx";
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<Router>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/info" element={<Info />} />
</Routes>
</Router>
</React.StrictMode>
);
Now that the dashboard module is up and running, our system can effectively showcase data to users in an engaging format.
Congratulations! Now, all that's left to do is to run all the services, and our system will be up and running smoothly. Our system is capable of generating dummy locomotive info every ten seconds, storing it in MongoDB, summarizing and saving it in MySQL, sending it via Telegram, and finally displaying the data to users through an appealing dashboard.
Overall, this is an interesting mini-project that can enhance various skills in utilizing technology, particularly in implementing microservices within a system.
That wraps it up! Thanks for reading along. Until next time, happy coding and keep exploring!