Merge pull request 'multi-emission-feature' (#14) from multi-emission-feature into main

Reviewed-on: BLC/sgeUpdated#14
This commit is contained in:
2025-08-19 09:26:46 +03:00
20 changed files with 881 additions and 421 deletions

View File

@@ -121,7 +121,8 @@ public class SgsApplication implements CommandLineRunner {
@Autowired
public SgsApplication(RoleService roleService, PermissionService permissionService, RoleRepo roleRepo,
AreaService areaService, NeighborhoodRepo neighborhoodRepo, NeighborhoodService neighborhoodService, CityService cityService,
AreaService areaService, NeighborhoodRepo neighborhoodRepo, NeighborhoodService neighborhoodService,
CityService cityService,
CityRepo cityRepo, DistrictRepo districtRepo, DistrictService districtService, CountryRepo countryRepo,
CountryService countryService, OrganizationService organizationService, UserService userService,
PasswordEncoder passwordEncoder, SectorService sectorService, SubSectorService subSectorService,
@@ -789,7 +790,7 @@ public class SgsApplication implements CommandLineRunner {
paginate_datacenters_get.setDescription(PermissionDescription.PAGINATE_DATACENTERS_GET);
permissionService.save(paginate_datacenters_get);
}
// Ensure SUPER_ADMIN has all permissions
Optional<Role> superAdminRole = roleRepo.findByTag("SUPER_ADMIN");
if (superAdminRole.isPresent()) {
@@ -858,6 +859,7 @@ public class SgsApplication implements CommandLineRunner {
}
if (cityService.findAll().isEmpty()) {
createCitiesFromJson();
createDefaultArea();
}
if (districtService.findAll().isEmpty()) {
createDistrictFromJson();
@@ -865,33 +867,30 @@ public class SgsApplication implements CommandLineRunner {
if (neighborhoodService.findAll().isEmpty()) {
createNeighborhoodsFromJson();
}
if (!cityService.findAll().isEmpty()) {
createDefaultArea();
}
}
void createDefaultArea() {
// Check if default area already exists
List<Area> existingAreas = areaService.findAll();
boolean defaultAreaExists = existingAreas.stream()
.anyMatch(area -> "Turkiye".equals(area.getTag()) && area.isDefaultArea());
.anyMatch(area -> "Turkiye".equals(area.getTag()) && area.isDefaultArea());
if (!defaultAreaExists) {
Area defaultArea = new Area();
defaultArea.setTag("Turkiye");
defaultArea.setDefaultArea(true);
defaultArea.setDeleted(false);
// Get all cities to add to the default area
List<City> allCities = cityService.findAll();
defaultArea.setCities(allCities);
// Get the Turkey country to add to the default area
List<Country> countries = countryService.findAll();
if (!countries.isEmpty()) {
defaultArea.setCountries(countries);
}
areaService.save(defaultArea);
}
}

View File

@@ -3,6 +3,7 @@ package com.sgs.graphql.dataCenter.domain;
import com.fasterxml.jackson.annotation.JsonBackReference;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import com.sgs.graphql.area.domain.Area;
import com.sgs.graphql.city.domain.City;
import com.sgs.graphql.sector.domain.Sector;
import com.sgs.graphql.subSector.domain.SubSector;
import com.sgs.graphql.emissionScope.domain.EmissionScope;
@@ -29,6 +30,7 @@ public class DataCenter extends BaseDomain {
private Integer number;
private Area area;
private City city;
private List<PhysicalMachine> physicalMachines = new ArrayList<>();
private List<DataCenterEmissionSource> dataCenterEmissionSources = new ArrayList<>();
@@ -131,6 +133,16 @@ public class DataCenter extends BaseDomain {
this.area = area;
}
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "city_id")
public City getCity() {
return city;
}
public void setCity(City city) {
this.city = city;
}
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "sub_sector_id")
public SubSector getSubSector() {

View File

@@ -17,10 +17,10 @@ public class DataCenterDto {
private Integer externalId;
private Integer number;
private AreaDto area;
@JsonProperty("physical_machine")
@JsonProperty("physical_machines")
private Map<String, PhysicalMachineDto> physicalMachine;
// Emission calculation fields
private SectorDto sector;
private SubSectorDto subSector;
@@ -62,12 +62,12 @@ public class DataCenterDto {
this.number = number;
}
public AreaDto getArea() {
return area;
public AreaDto getArea() {
return area;
}
public void setArea(AreaDto area) {
this.area = area;
public void setArea(AreaDto area) {
this.area = area;
}
public Map<String, PhysicalMachineDto> getPhysicalMachine() {

View File

@@ -18,6 +18,8 @@ public class DataCenterCreateInput extends BaseCreateInput {
private UUID areaId;
private UUID cityId;
@NotNull(message = "Sektör ID gereklidir")
private UUID sectorId;
@@ -50,6 +52,9 @@ public class DataCenterCreateInput extends BaseCreateInput {
public UUID getAreaId() { return areaId; }
public void setAreaId(UUID areaId) { this.areaId = areaId; }
public UUID getCityId() { return cityId; }
public void setCityId(UUID cityId) { this.cityId = cityId; }
public UUID getSectorId() { return sectorId; }
public void setSectorId(UUID sectorId) { this.sectorId = sectorId; }

View File

@@ -13,6 +13,7 @@ public class DataCenterUpdateInput extends BaseUpdateInput {
private Double consuptionAmount;
private UUID areaId;
private UUID cityId;
@NotNull(message = "Sektör ID gereklidir")
private UUID sectorId;
private UUID subSectorId;
@@ -44,6 +45,9 @@ public class DataCenterUpdateInput extends BaseUpdateInput {
public UUID getAreaId() { return areaId; }
public void setAreaId(UUID areaId) { this.areaId = areaId; }
public UUID getCityId() { return cityId; }
public void setCityId(UUID cityId) { this.cityId = cityId; }
public UUID getSectorId() { return sectorId; }
public void setSectorId(UUID sectorId) { this.sectorId = sectorId; }

View File

@@ -12,6 +12,7 @@ import com.sgs.graphql.emissionSource.service.EmissionSourceService;
import com.sgs.graphql.emissionScope.service.EmissionScopeService;
import com.sgs.graphql.consuptionUnit.service.ConsuptionUnitService;
import com.sgs.graphql.activitySubUnit.service.ActivitySubUnitService;
import com.sgs.graphql.city.service.CityService;
import com.sgs.lib.dao.mutation.mapper.BaseCreateUpdateMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@@ -29,12 +30,13 @@ public class DataCenterMapper extends BaseCreateUpdateMapper<DataCenter, DataCen
private final EmissionScopeService emissionScopeService;
private final ConsuptionUnitService consuptionUnitService;
private final ActivitySubUnitService activitySubUnitService;
private final CityService cityService;
@Autowired
public DataCenterMapper(AreaService areaService, SectorService sectorService,
SubSectorService subSectorService, EmissionSourceService emissionSourceService,
EmissionScopeService emissionScopeService, ConsuptionUnitService consuptionUnitService,
ActivitySubUnitService activitySubUnitService) {
ActivitySubUnitService activitySubUnitService, CityService cityService) {
this.areaService = areaService;
this.sectorService = sectorService;
this.subSectorService = subSectorService;
@@ -42,73 +44,80 @@ public class DataCenterMapper extends BaseCreateUpdateMapper<DataCenter, DataCen
this.emissionScopeService = emissionScopeService;
this.consuptionUnitService = consuptionUnitService;
this.activitySubUnitService = activitySubUnitService;
this.cityService = cityService;
}
@Override
public DataCenter toEntity(DataCenterCreateInput input) {
DataCenter entity = new DataCenter();
// Basic fields
entity.setDataCenter(input.getDataCenter());
entity.setExternalId(input.getExternalId());
entity.setNumber(input.getNumber());
entity.setConsuptionAmount(input.getConsuptionAmount());
// Convert ID references to entities
if (input.getAreaId() != null) {
entity.setArea(areaService.findById(input.getAreaId()).orElse(null));
}
if (input.getCityId() != null) {
entity.setCity(cityService.findById(input.getCityId()).orElse(null));
}
if (input.getSectorId() != null) {
entity.setSector(sectorService.findById(input.getSectorId()).orElse(null));
}
if (input.getSubSectorId() != null) {
entity.setSubSector(subSectorService.findById(input.getSubSectorId()).orElse(null));
}
if (input.getEmissionScopeId() != null) {
entity.setEmissionScope(emissionScopeService.findById(input.getEmissionScopeId()).orElse(null));
}
if (input.getActivitySubUnitId() != null) {
entity.setActivitySubUnit(activitySubUnitService.findById(input.getActivitySubUnitId()).orElse(null));
}
// New attributes
entity.setAyposURL(input.getAyposURL());
entity.setAddress(input.getAddress());
entity.setLatitude(input.getLatitude());
entity.setLongitude(input.getLongitude());
// Handle multiple emission sources if provided
if (input.getDataCenterEmissionSources() != null && !input.getDataCenterEmissionSources().isEmpty()) {
List<DataCenterEmissionSource> dataCenterEmissionSources = new ArrayList<>();
for (DataCenterEmissionSourceInput emissionSourceInput : input.getDataCenterEmissionSources()) {
DataCenterEmissionSource dcEmissionSource = new DataCenterEmissionSource();
dcEmissionSource.setDataCenter(entity);
// Set EmissionSource
if (emissionSourceInput.getEmissionSourceId() != null) {
dcEmissionSource.setEmissionSource(emissionSourceService.findById(UUID.fromString(emissionSourceInput.getEmissionSourceId())).orElse(null));
dcEmissionSource.setEmissionSource(emissionSourceService
.findById(UUID.fromString(emissionSourceInput.getEmissionSourceId())).orElse(null));
}
// Set ConsuptionUnit
if (emissionSourceInput.getConsuptionUnitId() != null) {
dcEmissionSource.setConsuptionUnit(consuptionUnitService.findById(UUID.fromString(emissionSourceInput.getConsuptionUnitId())).orElse(null));
dcEmissionSource.setConsuptionUnit(consuptionUnitService
.findById(UUID.fromString(emissionSourceInput.getConsuptionUnitId())).orElse(null));
}
// Set optional fields
dcEmissionSource.setIsDefault(emissionSourceInput.getIsDefault() != null ? emissionSourceInput.getIsDefault() : false);
dcEmissionSource.setIsDefault(
emissionSourceInput.getIsDefault() != null ? emissionSourceInput.getIsDefault() : false);
dcEmissionSource.setPercentage(emissionSourceInput.getPercentage());
dataCenterEmissionSources.add(dcEmissionSource);
}
entity.setDataCenterEmissionSources(dataCenterEmissionSources);
}
return entity;
}
@@ -118,83 +127,89 @@ public class DataCenterMapper extends BaseCreateUpdateMapper<DataCenter, DataCen
if (input.getDataCenter() != null) {
entity.setDataCenter(input.getDataCenter());
}
if (input.getExternalId() != null) {
entity.setExternalId(input.getExternalId());
}
if (input.getNumber() != null) {
entity.setNumber(input.getNumber());
}
if (input.getConsuptionAmount() != null) {
entity.setConsuptionAmount(input.getConsuptionAmount());
}
// Update relationships only if provided
if (input.getAreaId() != null) {
entity.setArea(areaService.findById(input.getAreaId()).orElse(null));
}
if (input.getCityId() != null) {
entity.setCity(cityService.findById(input.getCityId()).orElse(null));
}
if (input.getSectorId() != null) {
entity.setSector(sectorService.findById(input.getSectorId()).orElse(null));
}
if (input.getSubSectorId() != null) {
entity.setSubSector(subSectorService.findById(input.getSubSectorId()).orElse(null));
}
if (input.getEmissionScopeId() != null) {
entity.setEmissionScope(emissionScopeService.findById(input.getEmissionScopeId()).orElse(null));
}
if (input.getActivitySubUnitId() != null) {
entity.setActivitySubUnit(activitySubUnitService.findById(input.getActivitySubUnitId()).orElse(null));
}
// Handle multiple emission sources update if provided
if (input.getDataCenterEmissionSources() != null) {
// Clear existing emission sources and add new ones
// Clear existing emission sources from the managed collection
entity.getDataCenterEmissionSources().clear();
List<DataCenterEmissionSource> dataCenterEmissionSources = new ArrayList<>();
// Add new emission sources to the same managed collection
for (DataCenterEmissionSourceInput emissionSourceInput : input.getDataCenterEmissionSources()) {
DataCenterEmissionSource dcEmissionSource = new DataCenterEmissionSource();
dcEmissionSource.setDataCenter(entity);
if (emissionSourceInput.getEmissionSourceId() != null) {
dcEmissionSource.setEmissionSource(emissionSourceService.findById(UUID.fromString(emissionSourceInput.getEmissionSourceId())).orElse(null));
dcEmissionSource.setEmissionSource(emissionSourceService
.findById(UUID.fromString(emissionSourceInput.getEmissionSourceId())).orElse(null));
}
if (emissionSourceInput.getConsuptionUnitId() != null) {
dcEmissionSource.setConsuptionUnit(consuptionUnitService.findById(UUID.fromString(emissionSourceInput.getConsuptionUnitId())).orElse(null));
dcEmissionSource.setConsuptionUnit(consuptionUnitService
.findById(UUID.fromString(emissionSourceInput.getConsuptionUnitId())).orElse(null));
}
dcEmissionSource.setIsDefault(emissionSourceInput.getIsDefault() != null ? emissionSourceInput.getIsDefault() : false);
dcEmissionSource.setIsDefault(
emissionSourceInput.getIsDefault() != null ? emissionSourceInput.getIsDefault() : false);
dcEmissionSource.setPercentage(emissionSourceInput.getPercentage());
dataCenterEmissionSources.add(dcEmissionSource);
// Add to the existing managed collection instead of creating a new one
entity.getDataCenterEmissionSources().add(dcEmissionSource);
}
entity.setDataCenterEmissionSources(dataCenterEmissionSources);
}
// New attributes (partial update - only if provided)
if (input.getAyposURL() != null) {
entity.setAyposURL(input.getAyposURL());
}
if (input.getAddress() != null) {
entity.setAddress(input.getAddress());
}
if (input.getLatitude() != null) {
entity.setLatitude(input.getLatitude());
}
if (input.getLongitude() != null) {
entity.setLongitude(input.getLongitude());
}
return entity;
}
}

View File

@@ -15,7 +15,7 @@ public class VMEmissionSummary {
private Double totalEmission; // Individual record's total emission
private LocalDateTime createdDate; // When this specific record was created
private String physicalMachine;
private String project;
private String cloudSystem; // From physical machine
private String dataCenter;
// Individual emission values for this specific record
@@ -31,7 +31,7 @@ public class VMEmissionSummary {
public VMEmissionSummary(UUID vmId, String vmName, Double vmPower, String vmStatus,
Double totalEmission, LocalDateTime createdDate,
String physicalMachine, String project, String dataCenter,
String physicalMachine, String cloudSystem, String dataCenter,
Double co2, Double ch4, Double n2o) {
this.vmId = vmId;
this.vmName = vmName;
@@ -40,7 +40,7 @@ public class VMEmissionSummary {
this.totalEmission = totalEmission;
this.createdDate = createdDate;
this.physicalMachine = physicalMachine;
this.project = project;
this.cloudSystem = cloudSystem;
this.dataCenter = dataCenter;
this.co2 = co2;
this.ch4 = ch4;
@@ -70,8 +70,8 @@ public class VMEmissionSummary {
public String getPhysicalMachine() { return physicalMachine; }
public void setPhysicalMachine(String physicalMachine) { this.physicalMachine = physicalMachine; }
public String getProject() { return project; }
public void setProject(String project) { this.project = project; }
public String getCloudSystem() { return cloudSystem; }
public void setCloudSystem(String cloudSystem) { this.cloudSystem = cloudSystem; }
public String getDataCenter() { return dataCenter; }
public void setDataCenter(String dataCenter) { this.dataCenter = dataCenter; }

View File

@@ -55,7 +55,7 @@ public class MainDataTableQueryResolver implements GraphQLQueryResolver {
* @param projectId Optional project ID to filter VMs by project
* @return List of VM emission summaries including datacenter, project, aggregate, and physical machine info
*/
public List<VMEmissionSummary> vmEmissionSummary(UUID datacenterId, UUID projectId) {
return mainDataTableService.getVMEmissionSummaries(datacenterId, projectId);
public List<VMEmissionSummary> vmEmissionSummary(UUID datacenterId) {
return mainDataTableService.getVMEmissionSummaries(datacenterId);
}
}

View File

@@ -34,59 +34,48 @@ public class MainDataTableService
}
public List<VMEmissionSummary> getVMEmissionSummaries() {
return getVMEmissionSummaries(null, null);
return getVMEmissionSummaries(null);
}
public List<VMEmissionSummary> getVMEmissionSummaries(UUID datacenterId) {
return getVMEmissionSummaries(datacenterId, null);
}
public List<VMEmissionSummary> getVMEmissionSummaries(UUID datacenterId, UUID projectId) {
List<String> whereConditions = new ArrayList<>();
if (datacenterId != null) {
whereConditions.add("dc.id = decode(replace(:datacenterId, '-', ''), 'hex')");
}
if (projectId != null) {
whereConditions.add("v.project = :projectId");
}
String whereClause = whereConditions.isEmpty() ? "" : "WHERE " + String.join(" AND ", whereConditions) + " ";
String whereClause = whereConditions.isEmpty() ? "" :
"WHERE " + String.join(" AND ", whereConditions) + " ";
String sql = """
SELECT
CAST(v.id AS VARCHAR) as vm_id,
v.vm_name as vm_name,
v.power as vm_power,
v.state as vm_status,
mdt.total_emission,
mdt.created_date,
pm.name as physical_machine_name,
v.project as project_name,
dc.data_center_name as datacenter_name,
mdt.co2,
mdt.ch4,
mdt.n2o
FROM main_data_table mdt
JOIN vm v ON mdt.vm_id = v.id
LEFT JOIN physical_machine pm ON v.physical_machine_id = pm.id
LEFT JOIN data_center dc ON pm.data_center_id = dc.id
""" + whereClause + """
ORDER BY mdt.created_date DESC, v.vm_name
""";
SELECT
CAST(v.id AS VARCHAR) as vm_id,
v.vm_name as vm_name,
v.power as vm_power,
v.state as vm_status,
mdt.total_emission,
mdt.created_date,
pm.name as physical_machine_name,
pm.cloud_system as cloud_system,
dc.data_center_name as datacenter_name,
mdt.co2,
mdt.ch4,
mdt.n2o
FROM main_data_table mdt
JOIN vm v ON mdt.vm_id = v.id
LEFT JOIN physical_machine pm ON v.physical_machine_id = pm.id
LEFT JOIN data_center dc ON pm.data_center_id = dc.id
""" + whereClause + """
ORDER BY mdt.created_date DESC, v.vm_name
""";
Query query = entityManager.createNativeQuery(sql);
// Add parameters if provided
if (datacenterId != null) {
query.setParameter("datacenterId", datacenterId.toString());
}
if (projectId != null) {
query.setParameter("projectId", projectId.toString());
}
@SuppressWarnings("unchecked")
List<Object[]> results = query.getResultList();
@@ -110,7 +99,7 @@ public class MainDataTableService
}
summary.setPhysicalMachine((String) row[6]);
summary.setProject((String) row[7]);
summary.setCloudSystem((String) row[7]);
summary.setDataCenter((String) row[8]);
// Individual emission values

View File

@@ -217,6 +217,7 @@ public class MessageListener {
pm.setName(pmDto.getName());
pm.setIp(pmIp); // Use the IP from the map key
pm.setTag(pmDto.getTag());
pm.setCloudSystem(pmDto.getCloudSystem());
pm.setPower(pmDto.getPower());
pm.setDataCenter(entity);
@@ -282,16 +283,17 @@ public class MessageListener {
pm.setName(newPm.getName());
pm.setIp(newPm.getIp());
pm.setTag(newPm.getTag());
pm.setCloudSystem(newPm.getCloudSystem());
pm.setPower(newPm.getPower());
System.out.println("✅ Updated existing PM: " + pm.getName() + " (IP: " + pm.getIp() + ")");
System.out.println("✅ Updated existing PM: " + pm.getName() + " (IP: " + pm.getIp() + ") - CloudSystem: " + pm.getCloudSystem());
} else {
// Create new PM
pm = newPm;
pm.setDataCenter(dc);
dc.getPhysicalMachines().add(pm);
System.out.println("✅ Created new PM: " + pm.getName() + " (IP: " + pm.getIp() + ")");
System.out.println("✅ Created new PM: " + pm.getName() + " (IP: " + pm.getIp() + ") - CloudSystem: " + pm.getCloudSystem());
}
// Process VMs that are already assigned to this PM
@@ -547,15 +549,46 @@ public class MessageListener {
return false;
}
// Find the emission source by name/tag
List<EmissionSource> emissionSources = emissionSourceRepo.findByTag(emissionSourceName);
if (emissionSources.isEmpty()) {
// Find the emission source by name/tag from datacenter's configured emission sources
EmissionSource emissionSource = null;
// First, try to find the emission source from datacenter's configured sources
if (dataCenter.getDataCenterEmissionSources() != null && !dataCenter.getDataCenterEmissionSources().isEmpty()) {
for (DataCenterEmissionSource dces : dataCenter.getDataCenterEmissionSources()) {
if (dces.getEmissionSource() != null &&
emissionSourceName.equalsIgnoreCase(dces.getEmissionSource().getTag())) {
emissionSource = dces.getEmissionSource();
System.out.println("✅ Found emission source '" + emissionSourceName +
"' in datacenter's configured sources (ID: " + emissionSource.getId() + ")");
break;
}
}
}
// If not found in datacenter's sources, fall back to subsector-specific search
if (emissionSource == null && dataCenter.getSubSector() != null) {
emissionSource = emissionSourceRepo.findByTagAndSubSectorIgnoreCase(emissionSourceName, dataCenter.getSubSector());
if (emissionSource != null) {
System.out.println("⚠️ Using subsector fallback for emission source '" + emissionSourceName +
"' (ID: " + emissionSource.getId() + ") - Consider configuring it for datacenter");
}
}
// Last resort: global search
if (emissionSource == null) {
List<EmissionSource> emissionSources = emissionSourceRepo.findByTag(emissionSourceName);
if (!emissionSources.isEmpty()) {
emissionSource = emissionSources.get(0);
System.out.println("⚠️ Using global fallback for emission source '" + emissionSourceName +
"' (ID: " + emissionSource.getId() + ") - This may cause incorrect calculations!");
}
}
if (emissionSource == null) {
System.err.println("❌ Could not find emission source: " + emissionSourceName);
return false;
}
EmissionSource emissionSource = emissionSources.get(0);
// Calculate power consumption for this emission source (percentage of total VM power)
double sourceSpecificPower = vm.getPower() * (percentage / 100.0);
@@ -805,10 +838,19 @@ public class MessageListener {
input.setVmId(vm.getId());
System.out.println("🔍 Setting VM ID: " + vm.getId());
// Use the source-specific power consumption (percentage of total VM power)
input.setConsuptionAmount(String.valueOf(sourceSpecificPower));
System.out.println("🔍 Setting Consumption Amount: " + sourceSpecificPower + "W");
// Use the source-specific power consumption (percentage of total VM power)
// Format to 6 decimal places to avoid very long strings
String formattedPower = String.format("%.6f", sourceSpecificPower);
input.setConsuptionAmount(formattedPower);
System.out.println("🔍 Setting Consumption Amount: " + formattedPower + "W");
// Validate field lengths to prevent database errors
System.out.println("🔍 Field length validation:");
System.out.println(" Year: " + (input.getYear() != null ? input.getYear().length() : "null"));
System.out.println(" Month: " + (input.getMonth() != null ? input.getMonth().length() : "null"));
System.out.println(" ConsuptionAmount: " + (input.getConsuptionAmount() != null ? input.getConsuptionAmount().length() : "null"));
System.out.println("🔍 VM Emission Input for Source:");
System.out.println(" VM ID: " + vm.getId());
System.out.println(" VM Name: " + vm.getVmName());

View File

@@ -11,6 +11,7 @@ input DataCenterCreateInput {
consuptionAmount: Float
areaId: ID
cityId: ID
number: Int
ayposURL: String
address: String
@@ -31,6 +32,7 @@ input DataCenterUpdateInput {
consuptionAmount: Float
areaId: ID
cityId: ID
number: Int
ayposURL: String
address: String

View File

@@ -15,6 +15,7 @@ type DataCenter {
physicalMachines: [PhysicalMachine]
area: Area
city: City
number: Int
ayposURL: String

View File

@@ -2,5 +2,5 @@ extend type Query{
mainDataTable(id: ID!): MainDataTable!
mainDataTables(criteria: MainDataTableCriteria, sortBy: [SortBy!]): [MainDataTable!]
paginateMainDataTables(pagination : Pagination!, criteria: MainDataTableCriteria, sortBy:[SortBy!] ) : MainDataTablePageable!
vmEmissionSummary(datacenterId: ID, projectId: ID): [VMEmissionSummary!]!
vmEmissionSummary(datacenterId: ID): [VMEmissionSummary!]!
}

View File

@@ -60,7 +60,7 @@ type Config {
totalEmission: Float!
createdDate: LocalDateTime!
physicalMachine: String
project: String
cloudSystem: String
dataCenter: String
# Individual emission values per record
co2: Float!

View File

@@ -45,6 +45,10 @@ export const getDataCenters = () => {
name
}
}
city {
id
name
}
emissionScope {
id
tag
@@ -201,6 +205,10 @@ export const createDataCenter = (dataCenterData) => {
name
}
}
city {
id
name
}
emissionScope {
id
tag
@@ -241,6 +249,7 @@ export const createDataCenter = (dataCenterData) => {
ayposURL: dataCenterData.ayposURL || "",
number: parseInt(dataCenterData.number) || 1,
areaId: dataCenterData.areaId || null,
cityId: dataCenterData.cityId || null,
address: dataCenterData.address || "",
latitude: dataCenterData.latitude
? parseFloat(dataCenterData.latitude)
@@ -331,6 +340,10 @@ export const updateDataCenter = (id, dataCenterData) => {
name
}
}
city {
id
name
}
emissionScope {
id
tag
@@ -372,6 +385,7 @@ export const updateDataCenter = (id, dataCenterData) => {
ayposURL: dataCenterData.ayposURL || "",
number: parseInt(dataCenterData.number) || 1,
areaId: dataCenterData.areaId || null,
cityId: dataCenterData.cityId || null,
address: dataCenterData.address || "",
latitude: dataCenterData.latitude
? parseFloat(dataCenterData.latitude)

View File

@@ -1,6 +1,6 @@
import ApplicationService from "../../../services/ApplicationService";
export const getVMEmissionSummary = () => {
export const getVMEmissionSummary = (datacenterId) => {
return async (dispatch) => {
try {
const response = await ApplicationService.http()
@@ -8,8 +8,8 @@ export const getVMEmissionSummary = () => {
"/graphql",
{
query: `
{
vmEmissionSummary {
query GetVMEmissions($datacenterId: ID) {
vmEmissionSummary(datacenterId: $datacenterId) {
vmId
vmName
vmPower
@@ -17,7 +17,7 @@ export const getVMEmissionSummary = () => {
totalEmission
createdDate
physicalMachine
project
cloudSystem
dataCenter
co2
ch4
@@ -25,7 +25,10 @@ export const getVMEmissionSummary = () => {
reportGeneratedTime
}
}
`
`,
variables: {
datacenterId: datacenterId
}
},
{
headers: {

View File

@@ -18,17 +18,16 @@ const DataCenter = () => {
const [refreshInterval, setRefreshInterval] = useState(null);
const getAllPhysicalMachines = (dataCenter) => {
if (!dataCenter.projects) return [];
return dataCenter.projects.flatMap(project =>
project.physicalMachines || []
);
// Physical machines are directly in the dataCenter object, not in projects
const pms = dataCenter.physicalMachines || [];
return pms;
};
// Table columns following your pattern
const initialColumns = [
{
name: "Number",
selector: (row) => row.number,
name: "External ID",
selector: (row) => row.externalId,
sortable: true,
minWidth: "100px",
},
@@ -38,28 +37,33 @@ const DataCenter = () => {
sortable: true,
minWidth: "200px",
},
// Projects
{
name: "Projects",
selector: (row) => (row.projects || []).length,
sortable: true,
minWidth: "200px",
cell: (row) => (
<div>
{(row.projects || []).length > 0 ? (
<div className="d-flex flex-column">
{row.projects.map((project, index) => (
<div key={project.id} className={`badge badge-light-primary ${index > 0 ? 'mt-1' : ''}`}>
{project.name}
</div>
))}
</div>
) : (
<span className="text-muted">-</span>
)}
</div>
),
},
// Projects - Based on API response, this field might not exist or be structured differently
// {
// name: "Projects",
// selector: (row) => row.projects?.length || 0,
// sortable: true,
// minWidth: "200px",
// cell: (row) => (
// <div>
// {row.projects && row.projects.length > 0 ? (
// <div className="d-flex flex-column">
// {row.projects.map((project, index) => (
// <div
// key={project.id}
// className={`badge badge-light-primary ${
// index > 0 ? "mt-1" : ""
// }`}
// >
// {project.name}
// </div>
// ))}
// </div>
// ) : (
// <span className="text-muted">-</span>
// )}
// </div>
// ),
// },
// Physical Machines
{
name: "Physical Machines",
@@ -80,27 +84,39 @@ const DataCenter = () => {
name: "Virtual Machines",
selector: (row) => {
const pms = getAllPhysicalMachines(row);
const vms = pms.reduce((acc, pm) => {
if (!pm.vms) return acc;
return {
active: acc.active + pm.vms.filter(vm => vm.state?.toLowerCase() === "active").length,
total: acc.total + pm.vms.length
};
}, { active: 0, total: 0 });
const vms = pms.reduce(
(acc, pm) => {
if (!pm.vms) return acc;
return {
active:
acc.active +
pm.vms.filter((vm) => vm.state?.toLowerCase() === "active")
.length,
total: acc.total + pm.vms.length,
};
},
{ active: 0, total: 0 }
);
return vms.total;
},
sortable: true,
minWidth: "200px",
cell: (row) => {
const pms = getAllPhysicalMachines(row);
const vms = pms.reduce((acc, pm) => {
if (!pm.vms) return acc;
return {
active: acc.active + pm.vms.filter(vm => vm.state?.toLowerCase() === "active").length,
total: acc.total + pm.vms.length
};
}, { active: 0, total: 0 });
const vms = pms.reduce(
(acc, pm) => {
if (!pm.vms) return acc;
return {
active:
acc.active +
pm.vms.filter((vm) => vm.state?.toLowerCase() === "active")
.length,
total: acc.total + pm.vms.length,
};
},
{ active: 0, total: 0 }
);
return (
<div className="d-flex align-items-center">
<Monitor size={16} className="mr-2" />
@@ -109,7 +125,9 @@ const DataCenter = () => {
<div className="small">
<span className="text-success">{vms.active} Active</span>
<span className="text-muted mx-1"></span>
<span className="text-warning">{vms.total - vms.active} Inactive</span>
<span className="text-warning">
{vms.total - vms.active} Inactive
</span>
</div>
</div>
</div>
@@ -183,14 +201,23 @@ const DataCenter = () => {
</thead>
<tbody>
{pm.vms.map((vm) => {
const isActive = vm.state && ["ACTIVE", "active"].includes(vm.state);
const isActive =
vm.state && ["ACTIVE", "active"].includes(vm.state);
return (
<tr key={vm.id}>
<td>
<span className="font-weight-bold">{vm.vmName || vm.vm_name}</span>
<span className="font-weight-bold">
{vm.vmName || vm.vm_name}
</span>
</td>
<td>
<div className={`d-inline-block px-2 py-1 rounded-pill ${isActive ? 'bg-light-success text-success' : 'bg-light-warning text-warning'}`}>
<div
className={`d-inline-block px-2 py-1 rounded-pill ${
isActive
? "bg-light-success text-success"
: "bg-light-warning text-warning"
}`}
>
{vm.state}
</div>
</td>
@@ -204,23 +231,48 @@ const DataCenter = () => {
<td>
<div className="d-flex align-items-center">
<div className="mr-3">
<small className="text-muted d-block">CPU</small>
<span>{(vm.config?.cpu || (vm.confg && vm.confg[1])) || '-'}</span>
<small className="text-muted d-block">
CPU
</small>
<span>
{vm.config?.cpu ||
(vm.confg && vm.confg[1]) ||
"-"}
</span>
</div>
<div className="mr-3">
<small className="text-muted d-block">RAM</small>
<span>{(vm.config?.ram || (vm.confg && vm.confg[2])) || '-'} GB</span>
<small className="text-muted d-block">
RAM
</small>
<span>
{vm.config?.ram ||
(vm.confg && vm.confg[2]) ||
"-"}{" "}
GB
</span>
</div>
<div>
<small className="text-muted d-block">Disk</small>
<span>{(vm.config?.disk || (vm.confg && vm.confg[3])) || '-'} GB</span>
<small className="text-muted d-block">
Disk
</small>
<span>
{vm.config?.disk ||
(vm.confg && vm.confg[3]) ||
"-"}{" "}
GB
</span>
</div>
</div>
</td>
<td>
<div className="d-flex align-items-center">
<Server size={14} className="mr-1" />
<span>{vm.host || vm.hostingPm || (vm.confg && vm.confg[4]) || '-'}</span>
<span>
{vm.host ||
vm.hostingPm ||
(vm.confg && vm.confg[4]) ||
"-"}
</span>
</div>
</td>
</tr>
@@ -241,8 +293,6 @@ const DataCenter = () => {
);
};
return (
<div style={{ marginTop: "2%" }}>
<Card>

View File

@@ -412,11 +412,11 @@ const DataCenterManagement = () => {
number: row.number?.toString(),
address: row.address || "",
areaId: row.area?.id || null,
cityId: null,
cityId: row.city?.id || null,
latitude: row.latitude,
longitude: row.longitude,
ayposURL: row.ayposURL || "",
city: row.city || "",
city: row.city?.name || "",
emissionScopeId: row.emissionScope?.id || null,
sectorId: row.sector?.id || null,
subSectorId: row.subSector?.id || null,
@@ -428,6 +428,23 @@ const DataCenterManagement = () => {
setSelectedSector(row.sector?.id);
setSelectedSubSector(row.subSector?.id);
// If there are existing emission sources, fetch consumption units for each
if (
row.dataCenterEmissionSources &&
row.dataCenterEmissionSources.length > 0
) {
row.dataCenterEmissionSources.forEach((dces) => {
if (dces.emissionSource && dces.emissionSource.id) {
dispatch(
getConsuptionUnits({
id: dces.emissionSource.id,
sector: row.sector?.id,
})
);
}
});
}
// Only set map position if we have both address and valid coordinates
setMapPosition(
row.address && row.latitude && row.longitude
@@ -594,6 +611,7 @@ const DataCenterManagement = () => {
number: parseInt(selectedDataCenter.number || "1"),
address: selectedDataCenter.address,
areaId: selectedDataCenter.areaId,
cityId: selectedDataCenter.cityId,
latitude: selectedDataCenter.latitude
? parseFloat(selectedDataCenter.latitude)
: null,
@@ -1029,189 +1047,232 @@ const DataCenterManagement = () => {
<Col sm="12">
<FormGroup>
<Label>Emission Sources & Consumption Units</Label>
<div className="border p-3 rounded bg-light">
<small className="text-muted mb-2 d-block">
Configure emission sources for this data center. At least
one emission source with consumption unit is required.
</small>
<div className="border rounded p-3 bg-light">
<div className="d-flex justify-content-between align-items-center mb-3">
<small className="text-muted">
Configure emission sources for this data center. At
least one emission source with consumption unit is
required.
</small>
<Button
color="primary"
size="sm"
onClick={() => {
setSelectedDataCenter({
...selectedDataCenter,
dataCenterEmissionSources: [
...selectedDataCenter.dataCenterEmissionSources,
{
emissionSourceId: null,
consuptionUnitId: null,
isDefault: false,
},
],
});
}}
disabled={!selectedDataCenter.subSectorId}
className="d-none d-sm-flex"
>
<Plus size={14} className="me-1" />
Add Source
</Button>
</div>
{selectedDataCenter.dataCenterEmissionSources.map(
(source, index) => (
<Row key={index} className="mb-2 align-items-end">
<Col sm="5">
<Label
for={`emissionSource-${index}`}
className="form-label"
>
Emission Source *
</Label>
<Select
id={`emissionSource-${index}`}
placeholder="Select emission source..."
options={emissionSourcesOptions}
value={emissionSourcesOptions?.find(
(option) =>
option.value === source.emissionSourceId
)}
onChange={(option) => {
const updatedSources = [
...selectedDataCenter.dataCenterEmissionSources,
];
updatedSources[index] = {
...updatedSources[index],
emissionSourceId: option?.value,
consuptionUnitId: null, // Reset consumption unit when emission source changes
};
setSelectedDataCenter({
...selectedDataCenter,
dataCenterEmissionSources: updatedSources,
});
// Fetch consumption units for the selected emission source
if (option?.value) {
dispatch(
getConsuptionUnits({
id: option.value,
sector: selectedDataCenter?.sectorId,
})
);
}
}}
isClearable
filterOption={customFilterForSelect}
isDisabled={!selectedDataCenter.subSectorId}
styles={{
placeholder: (provided) => ({
...provided,
color: "#6e6b7b",
}),
}}
menuPlacement="top"
/>
</Col>
<Col sm="4">
<Label
for={`consuptionUnit-${index}`}
className="form-label"
>
Consumption Unit *
</Label>
<Select
id={`consuptionUnit-${index}`}
placeholder="Select consumption unit..."
options={consuptionUnitsOptions}
value={consuptionUnitsOptions?.find(
(option) =>
option.value === source.consuptionUnitId
)}
onChange={(option) => {
const updatedSources = [
...selectedDataCenter.dataCenterEmissionSources,
];
updatedSources[index] = {
...updatedSources[index],
consuptionUnitId: option?.value,
};
setSelectedDataCenter({
...selectedDataCenter,
dataCenterEmissionSources: updatedSources,
});
}}
isClearable
filterOption={customFilterForSelect}
isDisabled={!source.emissionSourceId}
styles={{
placeholder: (provided) => ({
...provided,
color: "#6e6b7b",
}),
}}
menuPlacement="top"
/>
</Col>
<Col sm="3">
<Label className="form-label d-block">
Actions
</Label>
<div className="d-flex gap-2">
<Button
color={
source.isDefault
? "primary"
: "outline-secondary"
}
size="sm"
onClick={() => {
<div
key={index}
className="border rounded p-3 mb-3 bg-white"
>
<Row className="g-3">
<Col xs="12" md="6">
<Label
for={`emissionSource-${index}`}
className="form-label fw-bold"
>
Emission Source *
</Label>
<Select
id={`emissionSource-${index}`}
placeholder="Select emission source..."
options={emissionSourcesOptions}
value={emissionSourcesOptions?.find(
(option) =>
option.value === source.emissionSourceId
)}
onChange={(option) => {
const updatedSources = [
...selectedDataCenter.dataCenterEmissionSources,
];
// First, set all sources to not default
updatedSources.forEach(
(s) => (s.isDefault = false)
);
// Then set the selected one as default
updatedSources[index].isDefault = true;
updatedSources[index] = {
...updatedSources[index],
emissionSourceId: option?.value,
consuptionUnitId: null, // Reset consumption unit when emission source changes
};
setSelectedDataCenter({
...selectedDataCenter,
dataCenterEmissionSources: updatedSources,
});
}}
title="Set as default emission source"
>
{source.isDefault ? "★ Default" : "☆ Default"}
</Button>
<Button
color="outline-danger"
size="sm"
onClick={() => {
const updatedSources =
selectedDataCenter.dataCenterEmissionSources.filter(
(_, i) => i !== index
// Fetch consumption units for the selected emission source
if (option?.value) {
dispatch(
getConsuptionUnits({
id: option.value,
sector: selectedDataCenter?.sectorId,
})
);
// If we're removing the default source and there are other sources, make the first one default
if (
source.isDefault &&
updatedSources.length > 0
) {
updatedSources[0].isDefault = true;
}
}}
isClearable
filterOption={customFilterForSelect}
isDisabled={!selectedDataCenter.subSectorId}
styles={{
placeholder: (provided) => ({
...provided,
color: "#6e6b7b",
}),
}}
menuPlacement="auto"
/>
</Col>
<Col xs="12" md="6">
<Label
for={`consuptionUnit-${index}`}
className="form-label fw-bold"
>
Consumption Unit *
</Label>
<Select
id={`consuptionUnit-${index}`}
placeholder="Select consumption unit..."
options={consuptionUnitsOptions}
value={consuptionUnitsOptions?.find(
(option) =>
option.value === source.consuptionUnitId
)}
onChange={(option) => {
const updatedSources = [
...selectedDataCenter.dataCenterEmissionSources,
];
updatedSources[index] = {
...updatedSources[index],
consuptionUnitId: option?.value,
};
setSelectedDataCenter({
...selectedDataCenter,
dataCenterEmissionSources: updatedSources,
});
}}
disabled={
selectedDataCenter.dataCenterEmissionSources
.length === 1
}
title="Remove emission source"
>
<Trash size={14} />
</Button>
</div>
</Col>
</Row>
isClearable
filterOption={customFilterForSelect}
isDisabled={!source.emissionSourceId}
styles={{
placeholder: (provided) => ({
...provided,
color: "#6e6b7b",
}),
}}
menuPlacement="auto"
/>
</Col>
</Row>
<Row className="mt-3">
<Col xs="12">
<div className="d-flex flex-column flex-sm-row gap-2 justify-content-between align-items-start align-items-sm-center">
<div className="d-flex gap-2 flex-wrap">
<Button
color={
source.isDefault
? "primary"
: "outline-secondary"
}
size="sm"
onClick={() => {
const updatedSources = [
...selectedDataCenter.dataCenterEmissionSources,
];
// First, set all sources to not default
updatedSources.forEach(
(s) => (s.isDefault = false)
);
// Then set the selected one as default
updatedSources[index].isDefault = true;
setSelectedDataCenter({
...selectedDataCenter,
dataCenterEmissionSources:
updatedSources,
});
}}
title="Set as default emission source"
>
{source.isDefault
? "★ Default"
: "☆ Set Default"}
</Button>
{source.isDefault && (
<span className="badge bg-success align-self-center">
Default Source
</span>
)}
</div>
<Button
color="outline-danger"
size="sm"
onClick={() => {
const updatedSources =
selectedDataCenter.dataCenterEmissionSources.filter(
(_, i) => i !== index
);
// If we're removing the default source and there are other sources, make the first one default
if (
source.isDefault &&
updatedSources.length > 0
) {
updatedSources[0].isDefault = true;
}
setSelectedDataCenter({
...selectedDataCenter,
dataCenterEmissionSources: updatedSources,
});
}}
disabled={
selectedDataCenter.dataCenterEmissionSources
.length === 1
}
title="Remove emission source"
>
<Trash size={14} className="me-1" />
Remove
</Button>
</div>
</Col>
</Row>
</div>
)
)}
<Button
color="primary"
size="sm"
onClick={() => {
setSelectedDataCenter({
...selectedDataCenter,
dataCenterEmissionSources: [
...selectedDataCenter.dataCenterEmissionSources,
{
emissionSourceId: null,
consuptionUnitId: null,
isDefault: false,
},
],
});
}}
disabled={!selectedDataCenter.subSectorId}
>
<Plus size={14} className="me-1" />
Add Emission Source
</Button>
<div className="text-center mt-3">
<Button
color="primary"
size="sm"
onClick={() => {
setSelectedDataCenter({
...selectedDataCenter,
dataCenterEmissionSources: [
...selectedDataCenter.dataCenterEmissionSources,
{
emissionSourceId: null,
consuptionUnitId: null,
isDefault: false,
},
],
});
}}
disabled={!selectedDataCenter.subSectorId}
className="w-100 d-sm-none"
>
<Plus size={14} className="me-1" />
Add Another Emission Source
</Button>
</div>
</div>
</FormGroup>
</Col>

View File

@@ -1,56 +1,157 @@
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useMemo } from "react";
import { MaterialReactTable } from "material-react-table";
import { useDispatch, useSelector } from "react-redux";
import { useTranslation } from "react-i18next";
import { Card, CardHeader, CardTitle, Alert } from "reactstrap";
import {
Card,
CardHeader,
CardTitle,
Alert,
Row,
Col,
Label,
} from "reactstrap";
import { getVMEmissionSummary } from "../../redux/actions/mainDataTables/index";
import { getDataCenters } from "../../redux/actions/dataCenter";
import { editNumbers } from "../../components/edit-numbers";
import Select from "react-select";
function MainDataTables() {
const { t } = useTranslation();
const dispatch = useDispatch();
const mainDataTablesStore = useSelector((state) => state.mainDataTables);
const dataCenterStore = useSelector((state) => state.dataCenter);
const [error, setError] = useState(null);
const [selectedDataCenter, setSelectedDataCenter] = useState(null);
const [dataCenterOptions, setDataCenterOptions] = useState([]);
const [loading, setLoading] = useState(false);
// Fetch datacenters on component mount
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
await dispatch(getVMEmissionSummary());
} catch (err) {
console.error('Error in MainDataTables:', err);
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
dispatch(getDataCenters());
}, [dispatch]);
// Debug log for store data
// Update datacenter options when datacenters are loaded
useEffect(() => {
console.log('Current store data:', mainDataTablesStore);
}, [mainDataTablesStore]);
if (dataCenterStore?.dataCenters?.length > 0) {
const options = dataCenterStore.dataCenters.map((dataCenter) => ({
value: dataCenter.id,
label: dataCenter.dataCenter,
externalId: dataCenter.externalId,
}));
setDataCenterOptions(options);
}
}, [dataCenterStore?.dataCenters]);
const [loading, setLoading] = useState(true);
// Fetch VM emission data when datacenter is selected
useEffect(() => {
if (selectedDataCenter?.value) {
const fetchData = async () => {
try {
setLoading(true);
setError(null);
await dispatch(getVMEmissionSummary(selectedDataCenter.value));
} catch (err) {
console.error("Error in MainDataTables:", err);
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}
}, [selectedDataCenter, dispatch]);
const columns = [
{ header: t("VM ID"), accessorKey: "vmId", Cell: ({ cell }) => <span>{cell.getValue() || "-"}</span> },
{ header: t("VM Name"), accessorKey: "vmName", Cell: ({ cell }) => <span>{cell.getValue() || "-"}</span> },
{ header: t("VM Power"), accessorKey: "vmPower", Cell: ({ cell }) => <span>{editNumbers(cell.getValue()) || "-"}</span> },
{ header: t("VM Status"), accessorKey: "vmStatus", Cell: ({ cell }) => <span>{cell.getValue() || "-"}</span> },
{ header: t("Total Emission"), accessorKey: "totalEmission", Cell: ({ cell }) => <span>{editNumbers(cell.getValue()) || "-"}</span> },
{ header: t("Created Date"), accessorKey: "createdDate", Cell: ({ cell }) => (<span>{cell.getValue() ? new Date(cell.getValue()).toLocaleString() : "-"}</span>), sortable: true },
{ header: t("Physical Machine"), accessorKey: "physicalMachine", Cell: ({ cell }) => <span>{cell.getValue() || "-"}</span> },
{ header: t("Project"), accessorKey: "project", Cell: ({ cell }) => <span>{cell.getValue() || "-"}</span> },
{ header: t("Data Center"), accessorKey: "dataCenter", Cell: ({ cell }) => <span>{cell.getValue() || "-"}</span> },
{ header: "CO2", accessorKey: "co2", Cell: ({ cell }) => <span>{editNumbers(cell.getValue()) || "-"}</span> },
{ header: "CH4", accessorKey: "ch4", Cell: ({ cell }) => <span>{editNumbers(cell.getValue()) || "-"}</span> },
{ header: "N2O", accessorKey: "n2o", Cell: ({ cell }) => <span>{editNumbers(cell.getValue()) || "-"}</span> },
];
// Memoize columns to prevent re-renders
const columns = useMemo(
() => [
{
header: t("VM ID"),
accessorKey: "vmId",
size: 150,
Cell: ({ cell }) => <span>{cell.getValue() || "-"}</span>,
},
{
header: t("VM Name"),
accessorKey: "vmName",
size: 200,
Cell: ({ cell }) => <span>{cell.getValue() || "-"}</span>,
},
{
header: t("VM Power"),
accessorKey: "vmPower",
size: 120,
Cell: ({ cell }) => <span>{editNumbers(cell.getValue()) || "-"}</span>,
},
{
header: t("VM Status"),
accessorKey: "vmStatus",
size: 100,
Cell: ({ cell }) => <span>{cell.getValue() || "-"}</span>,
},
{
header: t("Total Emission"),
accessorKey: "totalEmission",
size: 150,
Cell: ({ cell }) => <span>{editNumbers(cell.getValue()) || "-"}</span>,
},
{
header: t("Physical Machine"),
accessorKey: "physicalMachine",
size: 150,
Cell: ({ cell }) => <span>{cell.getValue() || "-"}</span>,
},
{
header: t("Cloud System"),
accessorKey: "cloudSystem",
size: 150,
Cell: ({ cell }) => <span>{cell.getValue() || "-"}</span>,
},
{
header: t("Data Center"),
accessorKey: "dataCenter",
size: 150,
Cell: ({ cell }) => <span>{cell.getValue() || "-"}</span>,
},
{
header: "CO2",
accessorKey: "co2",
size: 100,
Cell: ({ cell }) => <span>{editNumbers(cell.getValue()) || "-"}</span>,
},
{
header: "CH4",
accessorKey: "ch4",
size: 100,
Cell: ({ cell }) => <span>{editNumbers(cell.getValue()) || "-"}</span>,
},
{
header: "N2O",
accessorKey: "n2o",
size: 100,
Cell: ({ cell }) => <span>{editNumbers(cell.getValue()) || "-"}</span>,
},
{
header: t("Created Date"),
accessorKey: "createdDate",
size: 180,
Cell: ({ cell }) => (
<span>
{cell.getValue() ? new Date(cell.getValue()).toLocaleString() : "-"}
</span>
),
sortable: true,
},
],
[t]
);
const tableData = mainDataTablesStore?.vmEmissionSummary || [];
console.log('VM Emission data:', tableData);
// Memoize table data to prevent unnecessary re-renders
const tableData = useMemo(() => {
const data = mainDataTablesStore?.vmEmissionSummary || [];
console.log("VM Emission data count:", data.length);
return data;
}, [mainDataTablesStore?.vmEmissionSummary]);
if (error) {
return (
@@ -65,38 +166,108 @@ function MainDataTables() {
<Card>
<CardHeader className="border-bottom">
<CardTitle tag="h4">{t("Raw Data")}</CardTitle>
{tableData.length > 0 && (
<small className="text-muted">
{tableData.length} records loaded
</small>
)}
</CardHeader>
<MaterialReactTable
columns={columns}
data={tableData}
enableColumnFilters={true}
enableFilters={true}
enableGlobalFilter={true}
enablePagination={true}
enableColumnResizing={true}
enableStickyHeader={true}
muiTableContainerProps={{ sx: { maxHeight: 'calc(100vh - 180px)' } }}
muiTableProps={{
sx: {
tableLayout: 'auto',
},
}}
initialState={{
pagination: {
pageSize: 100,
pageIndex: 0
},
sorting: [
{ id: 'createdDate', desc: true }
],
density: 'compact'
}}
state={{
isLoading: loading,
showProgressBars: true,
showSkeletons: true,
}}
/>
{/* Datacenter Selection */}
<div className="p-3 border-bottom">
<Row>
<Col md="6">
<Label for="datacenter-select">{t("Data Center")}</Label>
<Select
id="datacenter-select"
value={selectedDataCenter}
onChange={setSelectedDataCenter}
options={dataCenterOptions}
placeholder={t("Select a data center...")}
isClearable
isSearchable
isLoading={dataCenterStore?.loading}
noOptionsMessage={() => t("No data centers available")}
styles={{
menu: (provided) => ({
...provided,
zIndex: 9999, // Ensure dropdown appears above other elements
}),
}}
menuPortalTarget={document.body} // Render dropdown in body to avoid container overflow
/>
</Col>
</Row>
</div>
{selectedDataCenter ? (
<MaterialReactTable
columns={columns}
data={tableData}
// Performance optimizations for large datasets
enableColumnFilters={true}
enableFilters={true}
enableGlobalFilter={true}
enablePagination={true}
enableColumnResizing={true} // Disable resizing for better performance
enableStickyHeader={true}
enableRowVirtualization={true} // Enable virtualization for large datasets
enableColumnVirtualization={false} // Keep columns visible
// Pagination settings for large datasets
initialState={{
pagination: {
pageSize: 100, // Reduce page size for better performance
pageIndex: 0,
},
sorting: [{ id: "createdDate", desc: true }],
density: "compact",
}}
// Performance-optimized table props
muiTableContainerProps={{
sx: {
maxHeight: "calc(100vh - 250px)",
minHeight: "400px",
},
}}
muiTableProps={{
sx: {
tableLayout: "fixed", // Better performance with fixed layout
},
}}
// Pagination options
muiTablePaginationProps={{
rowsPerPageOptions: [10, 25, 50, 100],
showFirstButton: true,
showLastButton: true,
}}
// Loading and error states
state={{
isLoading: loading || mainDataTablesStore?.loading,
showProgressBars: loading || mainDataTablesStore?.loading,
showSkeletons: loading || mainDataTablesStore?.loading,
}}
// Disable features that can slow down large tables
enableRowSelection={false}
enableColumnOrdering={true}
enableColumnDragging={false}
enableDensityToggle={false}
enableFullScreenToggle={false}
// Custom loading overlay
renderProgressBarCell={({ cell }) => (
<div
style={{
width: "100%",
height: "20px",
backgroundColor: "#f0f0f0",
}}
/>
)}
/>
) : (
<div className="p-4 text-center text-muted">
{t("Please select a data center to view raw data")}
</div>
)}
</Card>
</div>
);

View File

@@ -41,6 +41,78 @@ import { ChromePicker } from "react-color";
import { customFilterForSelect } from "../utility/Utils";
import { permissionCheck } from "../components/permission-check";
import { getDataCenters } from "../redux/actions/dataCenter";
import L from "leaflet";
// Custom data center icon
const dataCenterIcon = new L.Icon({
iconUrl:
"data:image/svg+xml;base64," +
btoa(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<defs>
<linearGradient id="serverGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#4A90E2;stop-opacity:1" />
<stop offset="100%" style="stop-color:#2E5BBA;stop-opacity:1" />
</linearGradient>
<linearGradient id="rackGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#F5F5F5;stop-opacity:1" />
<stop offset="100%" style="stop-color:#E0E0E0;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Main server rack -->
<rect x="8" y="4" width="32" height="40" rx="2" ry="2" fill="url(#rackGradient)" stroke="#B0B0B0" stroke-width="1"/>
<!-- Server units -->
<rect x="10" y="6" width="28" height="4" rx="1" fill="url(#serverGradient)" stroke="#2E5BBA" stroke-width="0.5"/>
<rect x="10" y="12" width="28" height="4" rx="1" fill="url(#serverGradient)" stroke="#2E5BBA" stroke-width="0.5"/>
<rect x="10" y="18" width="28" height="4" rx="1" fill="url(#serverGradient)" stroke="#2E5BBA" stroke-width="0.5"/>
<rect x="10" y="24" width="28" height="4" rx="1" fill="url(#serverGradient)" stroke="#2E5BBA" stroke-width="0.5"/>
<rect x="10" y="30" width="28" height="4" rx="1" fill="url(#serverGradient)" stroke="#2E5BBA" stroke-width="0.5"/>
<rect x="10" y="36" width="28" height="4" rx="1" fill="url(#serverGradient)" stroke="#2E5BBA" stroke-width="0.5"/>
<!-- LED indicators -->
<circle cx="13" cy="8" r="0.8" fill="#00FF00"/>
<circle cx="13" cy="14" r="0.8" fill="#00FF00"/>
<circle cx="13" cy="20" r="0.8" fill="#FFFF00"/>
<circle cx="13" cy="26" r="0.8" fill="#00FF00"/>
<circle cx="13" cy="32" r="0.8" fill="#FF0000"/>
<circle cx="13" cy="38" r="0.8" fill="#00FF00"/>
<!-- Power indicators -->
<circle cx="35" cy="8" r="0.6" fill="#0080FF"/>
<circle cx="35" cy="14" r="0.6" fill="#0080FF"/>
<circle cx="35" cy="20" r="0.6" fill="#0080FF"/>
<circle cx="35" cy="26" r="0.6" fill="#0080FF"/>
<circle cx="35" cy="32" r="0.6" fill="#0080FF"/>
<circle cx="35" cy="38" r="0.6" fill="#0080FF"/>
<!-- Ventilation grilles -->
<rect x="16" y="7" width="16" height="0.5" fill="#1A4A8A"/>
<rect x="16" y="8.5" width="16" height="0.5" fill="#1A4A8A"/>
<rect x="16" y="13" width="16" height="0.5" fill="#1A4A8A"/>
<rect x="16" y="14.5" width="16" height="0.5" fill="#1A4A8A"/>
<rect x="16" y="19" width="16" height="0.5" fill="#1A4A8A"/>
<rect x="16" y="20.5" width="16" height="0.5" fill="#1A4A8A"/>
<rect x="16" y="25" width="16" height="0.5" fill="#1A4A8A"/>
<rect x="16" y="26.5" width="16" height="0.5" fill="#1A4A8A"/>
<rect x="16" y="31" width="16" height="0.5" fill="#1A4A8A"/>
<rect x="16" y="32.5" width="16" height="0.5" fill="#1A4A8A"/>
<rect x="16" y="37" width="16" height="0.5" fill="#1A4A8A"/>
<rect x="16" y="38.5" width="16" height="0.5" fill="#1A4A8A"/>
<!-- Base/feet -->
<rect x="6" y="42" width="4" height="2" rx="1" fill="#808080"/>
<rect x="38" y="42" width="4" height="2" rx="1" fill="#808080"/>
<!-- Shadow -->
<ellipse cx="24" cy="45" rx="18" ry="2" fill="#000000" opacity="0.2"/>
</svg>
`),
iconSize: [48, 48],
iconAnchor: [18, 36],
popupAnchor: [0, -36],
});
const ColorPicker = ({ selectedColors, setSelectedColors, index }) => {
const [showColorPicker, setShowColorPicker] = useState(false);
@@ -627,10 +699,25 @@ const Map = () => {
if (!dc.latitude || !dc.longitude) return null;
return (
<Marker key={dc.id} position={[dc.latitude, dc.longitude]}>
<Marker
key={dc.id}
position={[dc.latitude, dc.longitude]}
icon={dataCenterIcon}
>
<Popup>
<div className="data-center-popup">
<h5 className="mb-2">{dc.dataCenter}</h5>
<h5 className="mb-2 text-primary">{dc.dataCenter}</h5>
<div className="mb-2">
<p className="mb-1">
<strong>{t("DataCenter.city")}:</strong>{" "}
<span>{dc.city?.name || "-"}</span>
</p>
{dc.address && (
<p className="mb-1 small text-muted">
<strong>{t("Address")}:</strong> {dc.address}
</p>
)}
</div>
<p className="mb-1">
<strong>{t("DataCenter.number")}:</strong> {dc.number}
</p>
@@ -638,20 +725,25 @@ const Map = () => {
<strong>{t("DataCenter.externalId")}:</strong>{" "}
{dc.externalId}
</p>
<p className="mb-1">
<strong>{t("DataCenter.city")}:</strong>{" "}
{dc.area?.cities?.map((city) => city.name).join(", ") ||
"-"}
</p>
{dc.area && (
<p className="mb-1">
<strong>{t("Area")}:</strong> {dc.area.tag}
</p>
)}
{dc.physicalMachines?.length > 0 && (
<p className="mb-1">
<strong>{t("Physical Machines")}:</strong>{" "}
<span className="badge badge-secondary">
{dc.physicalMachines.length}
</span>
</p>
)}
{dc.dataCenterEmissionSources?.length > 0 && (
<p className="mb-1">
<strong>{t("EmissionSources")}:</strong>{" "}
{dc.dataCenterEmissionSources.length}
<strong>{t("EmissionSources.emissionSources")}:</strong>{" "}
<span className="badge badge-info">
{dc.dataCenterEmissionSources.length}
</span>
</p>
)}
{dc.ayposURL && (