Fix DataCenter emission calculation: VM-level processing, authentication fixes, and type safety improvements

This commit is contained in:
2025-07-22 07:35:23 +03:00
commit 32f457ef33
337 changed files with 854380 additions and 0 deletions

View File

@@ -0,0 +1,665 @@
import React, { useState, useEffect, memo, useCallback } from "react";
import { useSelector, useDispatch } from "react-redux";
import ReactPaginate from "react-paginate";
import { ChevronDown, MoreVertical, Plus, Trash } from "react-feather";
import DataTable from "react-data-table-component";
import {
Card,
CardHeader,
CardTitle,
Input,
Label,
Row,
Col,
Button,
Modal,
ModalHeader,
ModalBody,
ModalFooter,
UncontrolledDropdown,
DropdownToggle,
DropdownMenu,
DropdownItem,
FormGroup,
Form,
} from "reactstrap";
import Select from "react-select";
import { Edit } from "@mui/icons-material";
import { useSnackbar } from "notistack";
import { default as SweetAlert } from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { getDataCenters, createDataCenter, updateDataCenter, deleteDataCenter } from "../redux/actions/dataCenter";
import { useTranslation } from "react-i18next";
import { getAreas } from "../redux/actions/areas";
import { permissionCheck } from "../components/permission-check";
import { customFilterForSelect } from "../utility/Utils";
import { MapContainer, TileLayer, Marker, useMapEvents } from 'react-leaflet';
import 'leaflet/dist/leaflet.css';
import L from 'leaflet';
import axios from 'axios';
import { debounce } from 'lodash';
// Add Nominatim service configuration
const NOMINATIM_BASE_URL = 'https://nominatim.openstreetmap.org';
const nominatimAxios = axios.create({
baseURL: NOMINATIM_BASE_URL,
headers: {
'User-Agent': 'SGE-DataCenter-Management' // Required by Nominatim's usage policy
}
});
const Swal = withReactContent(SweetAlert);
// Fix Leaflet marker icon issue
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'),
iconUrl: require('leaflet/dist/images/marker-icon.png'),
shadowUrl: require('leaflet/dist/images/marker-shadow.png')
});
// Map marker component that handles clicks
const MapMarker = ({ position, setPosition, setSelectedDataCenter }) => {
useMapEvents({
click(e) {
setPosition([e.latlng.lat, e.latlng.lng]);
// Use Nominatim reverse geocoding directly
nominatimAxios.get(`/reverse?format=json&lat=${e.latlng.lat}&lon=${e.latlng.lng}`)
.then(response => {
const address = response.data.display_name;
setSelectedDataCenter(prev => ({
...prev,
address,
latitude: e.latlng.lat,
longitude: e.latlng.lng
}));
})
.catch(error => {
console.error('Error getting address:', error);
// Still update coordinates even if address lookup fails
setSelectedDataCenter(prev => ({
...prev,
latitude: e.latlng.lat,
longitude: e.latlng.lng
}));
});
}
});
// Only render marker if position exists and has valid coordinates
return position && position[0] && position[1] ? <Marker position={position} /> : null;
};
const DataCenterManagement = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const { enqueueSnackbar } = useSnackbar();
const [currentPage, setCurrentPage] = useState(1);
const [rowsPerPage, setRowsPerPage] = useState(10);
const [searchValue, setSearchValue] = useState("");
const [showAddModal, setShowAddModal] = useState(false);
const [selectedDataCenter, setSelectedDataCenter] = useState({
name: "",
externalId: "",
number: "",
areaId: null,
address: "",
latitude: null,
longitude: null,
ayposURL: ""
});
const [mapPosition, setMapPosition] = useState(null);
const dataCenterStore = useSelector((state) => state.dataCenter);
const areasStore = useSelector((state) => state.areas);
const [areasOptions, setAreasOptions] = useState([]);
const [editingDataCenter, setEditingDataCenter] = useState(null);
const initialColumns = [
{
name: t("DataCenter.id"),
selector: (row) => row.externalId,
sortable: true,
minWidth: "100px",
cell: (row) => <span>{row.externalId}</span>,
},
{
name: t("DataCenter.name"),
selector: (row) => row.dataCenter,
sortable: true,
minWidth: "200px",
},
{
name: t("DataCenter.city"),
selector: (row) => row.area?.cityNames,
sortable: true,
minWidth: "150px",
cell: (row) => <span>{row.area?.cityNames || "-"}</span>,
},
{
name: "AYPOS",
selector: (row) => row.ayposURL,
sortable: false,
minWidth: "150px",
cell: (row) => (
<div className="d-flex justify-content-center w-100">
{row.ayposURL ? (
<Button
color="primary"
size="sm"
onClick={() => window.open(row.ayposURL, '_blank')}
>
AYPOS
</Button>
) : (
<span>-</span>
)}
</div>
),
},
{
name: t("Actions"),
allowOverflow: false,
maxWidth: "150px",
cell: (row) => {
return (
<div className="d-flex">
<UncontrolledDropdown>
<DropdownToggle className="pl-1" tag="span">
<MoreVertical size={15} />
</DropdownToggle>
<DropdownMenu container={"body"} end>
{permissionCheck("datacenter_update") && (
<DropdownItem
tag="a"
className="w-100"
onClick={() => handleEditDataCenter(row)}
>
<Edit size={15} />
<span className="align-middle ml-50">{t("Cruds.edit")}</span>
</DropdownItem>
)}
{permissionCheck("datacenter_delete") && (
<DropdownItem
tag="a"
className="w-100"
onClick={() => handleDeleteDataCenter(row)}
>
<Trash size={15} />
<span className="align-middle ml-50">{t("Cruds.delete")}</span>
</DropdownItem>
)}
</DropdownMenu>
</UncontrolledDropdown>
</div>
);
},
},
];
const [serverSideColumns, setServerSideColumns] = useState(initialColumns);
useEffect(() => {
dispatch(getDataCenters());
dispatch(getAreas());
}, [dispatch]);
useEffect(() => {
setAreasOptions(
areasStore?.areas?.map((area) => ({
value: area?.id,
label: area?.tag,
}))
);
}, [areasStore]);
const handleEditDataCenter = (row) => {
setEditingDataCenter(row);
setSelectedDataCenter({
name: row.dataCenter,
externalId: row.externalId,
number: row.number,
areaId: row.area?.id,
address: row.address,
latitude: row.latitude,
longitude: row.longitude,
ayposURL: row.ayposURL
});
// Only set map position if we have both address and valid coordinates
setMapPosition(row.address && row.latitude && row.longitude ? [row.latitude, row.longitude] : null);
setShowAddModal(true);
};
const handleDeleteDataCenter = async (row) => {
const result = await Swal.fire({
title: t("Common.areYouSure"),
text: t("Common.cantRevert"),
icon: "warning",
showCancelButton: true,
confirmButtonText: t("Common.yes"),
cancelButtonText: t("Common.no"),
customClass: {
confirmButton: "btn btn-primary",
cancelButton: "btn btn-outline-danger ms-1",
},
buttonsStyling: false,
});
if (result.value) {
try {
await dispatch(deleteDataCenter(row.id));
enqueueSnackbar(t("DataCenter.deleteSuccess"), { variant: "success" });
} catch (error) {
console.error("Delete error:", error);
enqueueSnackbar(
error?.message || t("DataCenter.deleteError"),
{ variant: "error" }
);
}
}
};
const handleSubmit = async () => {
if (!selectedDataCenter.name || !selectedDataCenter.externalId) {
enqueueSnackbar(t("Common.fillRequiredFields"), { variant: "error" });
return;
}
try {
// Ensure number is set for new data centers
const dataToSubmit = {
...selectedDataCenter,
number: selectedDataCenter.number || 1 // Default to 1 if not set
};
if (editingDataCenter) {
// Update existing data center
await dispatch(updateDataCenter(editingDataCenter.id, dataToSubmit));
enqueueSnackbar(t("DataCenter.updateSuccess"), { variant: "success" });
} else {
// Create new data center
await dispatch(createDataCenter(dataToSubmit));
enqueueSnackbar(t("DataCenter.createSuccess"), { variant: "success" });
}
handleCloseModal();
} catch (error) {
console.error("Operation error:", error);
enqueueSnackbar(
error?.message || (editingDataCenter ? t("DataCenter.updateError") : t("DataCenter.createError")),
{ variant: "error" }
);
}
};
const handleCloseModal = () => {
setShowAddModal(false);
setSelectedDataCenter({
name: "",
externalId: "",
number: "",
areaId: null,
address: "",
latitude: null,
longitude: null,
ayposURL: ""
});
setMapPosition(null);
setEditingDataCenter(null);
};
const handleFilter = (e) => {
setSearchValue(e.target.value);
};
const CustomPagination = () => (
<ReactPaginate
previousLabel=""
nextLabel=""
forcePage={currentPage - 1}
onPageChange={(page) => setCurrentPage(page.selected + 1)}
pageCount={Math.ceil(dataCenterStore.total / rowsPerPage)}
breakLabel="..."
pageRangeDisplayed={2}
marginPagesDisplayed={2}
activeClassName="active"
pageClassName="page-item"
breakClassName="page-item"
nextLinkClassName="page-link"
pageLinkClassName="page-link"
breakLinkClassName="page-link"
previousLinkClassName="page-link"
nextClassName="page-item next-item"
previousClassName="page-item prev-item"
containerClassName="pagination react-paginate separated-pagination pagination-sm justify-content-end pe-1 mt-1"
/>
);
// Add geocoding function
const geocodeAddress = useCallback(async (address) => {
if (!address || !address.trim()) {
setMapPosition(null);
setSelectedDataCenter(prev => ({
...prev,
latitude: null,
longitude: null
}));
return false;
}
try {
const response = await nominatimAxios.get(`/search?format=json&q=${encodeURIComponent(address)}`);
if (response.data && response.data[0]) {
const { lat, lon } = response.data[0];
const newPosition = [parseFloat(lat), parseFloat(lon)];
setMapPosition(newPosition);
setSelectedDataCenter(prev => ({
...prev,
latitude: parseFloat(lat),
longitude: parseFloat(lon)
}));
return true;
}
// If no results found, clear the coordinates
setMapPosition(null);
setSelectedDataCenter(prev => ({
...prev,
latitude: null,
longitude: null
}));
return false;
} catch (error) {
console.error('Error geocoding address:', error);
// On error, clear the coordinates
setMapPosition(null);
setSelectedDataCenter(prev => ({
...prev,
latitude: null,
longitude: null
}));
return false;
}
}, []);
// Update the address input handler
const handleAddressChange = (e) => {
const newAddress = e.target.value;
setSelectedDataCenter(prev => ({
...prev,
address: newAddress
}));
// If address is empty, clear the coordinates
if (!newAddress.trim()) {
setMapPosition(null);
setSelectedDataCenter(prev => ({
...prev,
latitude: null,
longitude: null
}));
} else {
// If address is not empty, try to geocode it after a delay
debouncedGeocode(newAddress);
}
};
// Debounced version of geocoding
const debouncedGeocode = useCallback(
debounce((address) => geocodeAddress(address), 1000),
[geocodeAddress]
);
const renderModal = () => {
return (
<Modal
isOpen={showAddModal}
toggle={handleCloseModal}
className="modal-dialog-centered"
size="lg"
>
<ModalHeader toggle={handleCloseModal}>
{editingDataCenter
? t("DataCenter.edit")
: t("DataCenter.add")}
</ModalHeader>
<ModalBody>
<Form>
<Row>
<Col sm="12">
<FormGroup>
<Label for="name">
{t("DataCenter.name")} <span className="text-danger">*</span>
</Label>
<Input
type="text"
name="name"
id="name"
placeholder={t("DataCenter.name")}
value={selectedDataCenter.name}
onChange={(e) =>
setSelectedDataCenter({
...selectedDataCenter,
name: e.target.value,
})
}
/>
</FormGroup>
</Col>
<Col sm="6">
<FormGroup>
<Label for="externalId">
{t("DataCenter.externalId")} <span className="text-danger">*</span>
</Label>
<Input
type="text"
name="externalId"
id="externalId"
placeholder={t("DataCenter.externalId")}
value={selectedDataCenter.externalId}
onChange={(e) =>
setSelectedDataCenter({
...selectedDataCenter,
externalId: e.target.value,
})
}
/>
</FormGroup>
</Col>
<Col sm="6">
<FormGroup>
<Label for="ayposURL">{t("DataCenter.ayposURL")}</Label>
<Input
type="text"
name="ayposURL"
id="ayposURL"
placeholder={t("DataCenter.ayposURL")}
value={selectedDataCenter.ayposURL}
onChange={(e) =>
setSelectedDataCenter({
...selectedDataCenter,
ayposURL: e.target.value,
})
}
/>
</FormGroup>
</Col>
<Col sm="12">
<FormGroup>
<Label for="area">{t("DataCenter.area")}</Label>
<Select
id="area"
name="area"
placeholder={t("DataCenter.selectArea")}
options={areasOptions}
value={areasOptions?.find(
(option) => option.value === selectedDataCenter.areaId
)}
onChange={(option) =>
setSelectedDataCenter({
...selectedDataCenter,
areaId: option?.value,
})
}
isClearable
filterOption={customFilterForSelect}
/>
</FormGroup>
</Col>
<Col sm="12">
<FormGroup>
<Label for="address">{t("DataCenter.address")}</Label>
<div className="d-flex">
<Input
type="text"
name="address"
id="address"
placeholder={t("DataCenter.address")}
value={selectedDataCenter.address}
onChange={handleAddressChange}
/>
<Button
color="primary"
className="ml-1"
onClick={() => {
if (selectedDataCenter.address) {
geocodeAddress(selectedDataCenter.address);
}
}}
>
{t("DataCenter.findOnMap")}
</Button>
</div>
</FormGroup>
</Col>
<Col sm="12">
<FormGroup>
<Label>{t("DataCenter.location")}</Label>
<div style={{ height: "400px", width: "100%" }}>
<MapContainer
center={mapPosition || [39.9334, 32.8597]} // Default to Ankara
zoom={13}
style={{ height: "100%", width: "100%" }}
>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
/>
<MapMarker
position={mapPosition}
setPosition={(pos) => {
setMapPosition(pos);
setSelectedDataCenter({
...selectedDataCenter,
latitude: pos[0],
longitude: pos[1]
});
}}
setSelectedDataCenter={setSelectedDataCenter}
/>
</MapContainer>
</div>
</FormGroup>
</Col>
</Row>
</Form>
</ModalBody>
<ModalFooter>
<Button
color="primary"
onClick={handleSubmit}
disabled={!selectedDataCenter.name || !selectedDataCenter.externalId}
>
{t("Common.save")}
</Button>
<Button
color="secondary"
onClick={handleCloseModal}
>
{t("Common.cancel")}
</Button>
</ModalFooter>
</Modal>
);
};
return (
<Card>
<CardHeader className="border-bottom">
<CardTitle tag="h4">{t("DataCenter.title")}</CardTitle>
{permissionCheck("datacenter_create") && (
<Button
className="ml-2"
color="primary"
onClick={() => setShowAddModal(true)}
>
<Plus size={15} />
<span className="align-middle ml-50">{t("DataCenter.create")}</span>
</Button>
)}
</CardHeader>
<Row className="mx-0 mt-1 mb-50">
<Col sm="6">
<div className="d-flex align-items-center">
<Label for="sort-select">{t("Show")}</Label>
<Input
className="dataTable-select"
type="select"
id="sort-select"
value={rowsPerPage}
onChange={(e) => setRowsPerPage(Number(e.target.value))}
>
<option value={10}>10</option>
<option value={25}>25</option>
<option value={50}>50</option>
<option value={75}>75</option>
<option value={100}>100</option>
</Input>
</div>
</Col>
<Col
className="d-flex align-items-center justify-content-sm-end mt-sm-0 mt-1"
sm="6"
>
<Label className="me-1" for="search-input">
{t("Filter")}
</Label>
<Input
className="dataTable-filter"
type="text"
bsSize="sm"
id="search-input"
value={searchValue}
onChange={handleFilter}
placeholder={t("DataCenter.searchPlaceholder")}
/>
</Col>
</Row>
<div className="react-dataTable">
<DataTable
noHeader
pagination
columns={serverSideColumns}
paginationPerPage={rowsPerPage}
className="react-dataTable"
sortIcon={<ChevronDown size={10} />}
paginationDefaultPage={currentPage}
paginationComponent={CustomPagination}
data={dataCenterStore.dataCenters}
noDataComponent={
<div className="p-2 text-center">
{t("Common.noDataAvailable")}
</div>
}
/>
</div>
{renderModal()}
</Card>
);
};
export default memo(DataCenterManagement);