Compare commits
3 Commits
062fd5141f
...
d464c9cd71
| Author | SHA1 | Date |
|---|---|---|
|
|
d464c9cd71 | |
|
|
abedc6c203 | |
|
|
80639e9169 |
|
|
@ -15,7 +15,7 @@ public static class SessionMethods
|
|||
|
||||
return user is not null
|
||||
&& folder is not null
|
||||
&& user.UserType!=0
|
||||
&& user.UserType==2
|
||||
&& user.HasAccessTo(folder.Parent())
|
||||
&& Db.Create(folder) // TODO: these two in a transaction
|
||||
&& Db.Create(new FolderAccess { UserId = user.Id, FolderId = folder.Id });
|
||||
|
|
@ -44,7 +44,10 @@ public static class SessionMethods
|
|||
|
||||
return user is not null
|
||||
&& folder is not null
|
||||
&& user.UserType !=0
|
||||
&& parent is not null
|
||||
&& folderId != parentId // can't move into itself
|
||||
&& !IsFolderAncestorOf(folderId, parentId) // can't move into a descendant
|
||||
&& user.UserType==2
|
||||
&& user.HasAccessTo(folder)
|
||||
&& user.HasAccessTo(parent)
|
||||
&& folder
|
||||
|
|
@ -52,6 +55,19 @@ public static class SessionMethods
|
|||
.Apply(Db.Update);
|
||||
}
|
||||
|
||||
// Walks up the folder tree from candidateDescendantId to check if ancestorId is an ancestor.
|
||||
// Prevents circular references when moving a folder into one of its own descendants.
|
||||
private static Boolean IsFolderAncestorOf(Int64 ancestorId, Int64 candidateDescendantId)
|
||||
{
|
||||
var current = Db.GetFolderById(candidateDescendantId);
|
||||
while (current is not null && current.ParentId != 0)
|
||||
{
|
||||
if (current.ParentId == ancestorId) return true;
|
||||
current = Db.GetFolderById(current.ParentId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static Boolean MoveInstallation(this Session? session, Int64 installationId, Int64 parentId)
|
||||
{
|
||||
var user = session?.User;
|
||||
|
|
@ -61,7 +77,7 @@ public static class SessionMethods
|
|||
if(installation == null || installation.ParentId == parentId) return false;
|
||||
|
||||
return user is not null
|
||||
&& user.UserType !=0
|
||||
&& user.UserType==2
|
||||
&& user.HasAccessTo(installation)
|
||||
&& user.HasAccessTo(parent)
|
||||
&& installation
|
||||
|
|
@ -144,7 +160,7 @@ public static class SessionMethods
|
|||
var installation = Db.GetInstallationById(action.InstallationId);
|
||||
installation.TestingMode = action.TestingMode;
|
||||
installation.Apply(Db.Update);
|
||||
WebsocketManager.InformWebsocketsForInstallation(action.InstallationId);
|
||||
await WebsocketManager.InformWebsocketsForInstallation(action.InstallationId);
|
||||
|
||||
// Save the configuration change to the database
|
||||
Db.HandleAction(action);
|
||||
|
|
@ -163,7 +179,7 @@ public static class SessionMethods
|
|||
{
|
||||
installation.TestingMode = action.TestingMode;
|
||||
installation.Apply(Db.Update);
|
||||
WebsocketManager.InformWebsocketsForInstallation(action.InstallationId);
|
||||
await WebsocketManager.InformWebsocketsForInstallation(action.InstallationId);
|
||||
}
|
||||
|
||||
Db.UpdateAction(action);
|
||||
|
|
@ -183,7 +199,7 @@ public static class SessionMethods
|
|||
var installation = Db.GetInstallationById(action.InstallationId);
|
||||
installation.TestingMode = false;
|
||||
installation.Apply(Db.Update);
|
||||
WebsocketManager.InformWebsocketsForInstallation(action.InstallationId);
|
||||
await WebsocketManager.InformWebsocketsForInstallation(action.InstallationId);
|
||||
}
|
||||
|
||||
Db.Delete(action);
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ public static class RabbitMqManager
|
|||
//If the status has changed, update all the connected front-ends regarding this installation
|
||||
if(prevStatus != receivedStatusMessage.Status && WebsocketManager.InstallationConnections[installationId].Connections.Count > 0)
|
||||
{
|
||||
WebsocketManager.InformWebsocketsForInstallation(installationId);
|
||||
_ = WebsocketManager.InformWebsocketsForInstallation(installationId); // fire-and-forget: sync event handler, can't await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ public static class WebsocketManager
|
|||
{
|
||||
while (true)
|
||||
{
|
||||
var idsToInform = new List<Int64>();
|
||||
|
||||
lock (InstallationConnections)
|
||||
{
|
||||
Console.WriteLine("Monitoring installation table...");
|
||||
|
|
@ -31,10 +33,8 @@ public static class WebsocketManager
|
|||
(installationConnection.Value.Product == (int)ProductType.SodiStoreMax && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(2))
|
||||
)
|
||||
{
|
||||
|
||||
Console.WriteLine("Installation ID is " + installationConnection.Key);
|
||||
Console.WriteLine("installationConnection.Value.Timestamp is " + installationConnection.Value.Timestamp);
|
||||
// Console.WriteLine("diff is "+(DateTime.Now-installationConnection.Value.Timestamp));
|
||||
|
||||
installationConnection.Value.Status = (int)StatusType.Offline;
|
||||
Installation installation = Db.Installations.FirstOrDefault(f => f.Product == installationConnection.Value.Product && f.Id == installationConnection.Key);
|
||||
|
|
@ -42,20 +42,29 @@ public static class WebsocketManager
|
|||
installation.Apply(Db.Update);
|
||||
if (installationConnection.Value.Connections.Count > 0)
|
||||
{
|
||||
InformWebsocketsForInstallation(installationConnection.Key);
|
||||
idsToInform.Add(installationConnection.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send notifications outside the lock so we can await the async SendAsync calls
|
||||
foreach (var id in idsToInform)
|
||||
await InformWebsocketsForInstallation(id);
|
||||
|
||||
await Task.Delay(TimeSpan.FromMinutes(1));
|
||||
}
|
||||
}
|
||||
|
||||
//Inform all the connected websockets regarding installation "installationId"
|
||||
public static void InformWebsocketsForInstallation(Int64 installationId)
|
||||
public static async Task InformWebsocketsForInstallation(Int64 installationId)
|
||||
{
|
||||
var installation = Db.GetInstallationById(installationId);
|
||||
byte[] dataToSend;
|
||||
List<WebSocket> connections;
|
||||
|
||||
lock (InstallationConnections)
|
||||
{
|
||||
var installationConnection = InstallationConnections[installationId];
|
||||
Console.WriteLine("Update all the connected websockets for installation " + installation.Name);
|
||||
|
||||
|
|
@ -66,18 +75,26 @@ public static class WebsocketManager
|
|||
testingMode = installation.TestingMode
|
||||
};
|
||||
|
||||
string jsonString = JsonSerializer.Serialize(jsonObject);
|
||||
byte[] dataToSend = Encoding.UTF8.GetBytes(jsonString);
|
||||
|
||||
foreach (var connection in installationConnection.Connections)
|
||||
{
|
||||
connection.SendAsync(
|
||||
new ArraySegment<byte>(dataToSend, 0, dataToSend.Length),
|
||||
WebSocketMessageType.Text,
|
||||
true, // Indicates that this is the end of the message
|
||||
CancellationToken.None
|
||||
);
|
||||
dataToSend = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(jsonObject));
|
||||
connections = installationConnection.Connections.ToList(); // snapshot before releasing lock
|
||||
}
|
||||
|
||||
// Send to all connections concurrently (preserves original fire-and-forget intent),
|
||||
// but isolate failures so one closed socket doesn't affect others or crash the caller.
|
||||
await Task.WhenAll(connections
|
||||
.Where(c => c.State == WebSocketState.Open)
|
||||
.Select(async c =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await c.SendAsync(new ArraySegment<byte>(dataToSend, 0, dataToSend.Length),
|
||||
WebSocketMessageType.Text, true, CancellationToken.None);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"WebSocket send failed for installation {installationId}: {ex.Message}");
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -109,7 +126,7 @@ public static class WebsocketManager
|
|||
|
||||
var jsonString = JsonSerializer.Serialize(jsonObject);
|
||||
var dataToSend = Encoding.UTF8.GetBytes(jsonString);
|
||||
currentWebSocket.SendAsync(dataToSend,
|
||||
await currentWebSocket.SendAsync(dataToSend,
|
||||
WebSocketMessageType.Text,
|
||||
true,
|
||||
CancellationToken.None
|
||||
|
|
@ -120,6 +137,7 @@ public static class WebsocketManager
|
|||
|
||||
//Received a new message from this websocket.
|
||||
//We have a HandleWebSocketConnection per connected frontend
|
||||
byte[] encodedDataToSend;
|
||||
lock (InstallationConnections)
|
||||
{
|
||||
List<WebsocketMessage> dataToSend = new List<WebsocketMessage>();
|
||||
|
|
@ -157,15 +175,7 @@ public static class WebsocketManager
|
|||
|
||||
}
|
||||
var jsonString = JsonSerializer.Serialize(dataToSend);
|
||||
var encodedDataToSend = Encoding.UTF8.GetBytes(jsonString);
|
||||
|
||||
|
||||
currentWebSocket.SendAsync(encodedDataToSend,
|
||||
WebSocketMessageType.Text,
|
||||
true, // Indicates that this is the end of the message
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
encodedDataToSend = Encoding.UTF8.GetBytes(jsonString);
|
||||
|
||||
// Console.WriteLine("Printing installation connection list");
|
||||
// Console.WriteLine("----------------------------------------------");
|
||||
|
|
@ -175,6 +185,12 @@ public static class WebsocketManager
|
|||
// }
|
||||
// Console.WriteLine("----------------------------------------------");
|
||||
}
|
||||
|
||||
await currentWebSocket.SendAsync(encodedDataToSend,
|
||||
WebSocketMessageType.Text,
|
||||
true,
|
||||
CancellationToken.None
|
||||
);
|
||||
}
|
||||
|
||||
lock (InstallationConnections)
|
||||
|
|
|
|||
|
|
@ -1,15 +1,19 @@
|
|||
import React, { ReactNode, useContext, useState } from 'react';
|
||||
import { CircularProgress, ListItemIcon, useTheme } from '@mui/material';
|
||||
import { CircularProgress, IconButton, ListItemIcon, useTheme } from '@mui/material';
|
||||
import { TreeItem } from '@mui/lab';
|
||||
import { I_Folder, I_Installation } from 'src/interfaces/InstallationTypes';
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile';
|
||||
import DriveFileMoveOutlinedIcon from '@mui/icons-material/DriveFileMoveOutlined';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { makeStyles } from '@mui/styles';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import routes from 'src/Resources/routes.json';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { ProductIdContext } from '../../../contexts/ProductIdContextProvider';
|
||||
import { UserContext } from '../../../contexts/userContext';
|
||||
import { UserType } from '../../../interfaces/UserTypes';
|
||||
import MoveModal from './MoveModal';
|
||||
|
||||
interface CustomTreeItemProps {
|
||||
node: I_Installation | I_Folder;
|
||||
|
|
@ -41,8 +45,10 @@ function CustomTreeItem(props: CustomTreeItemProps) {
|
|||
const status = props.node.status;
|
||||
const navigate = useNavigate();
|
||||
const [selected, setSelected] = useState(false);
|
||||
const [openMoveModal, setOpenMoveModal] = useState(false);
|
||||
const currentLocation = useLocation();
|
||||
const { product } = useContext(ProductIdContext);
|
||||
const { currentUser } = useContext(UserContext);
|
||||
|
||||
const handleSelectOneInstallation = (): void => {
|
||||
let installation = props.node;
|
||||
|
|
@ -126,6 +132,24 @@ function CustomTreeItem(props: CustomTreeItemProps) {
|
|||
{props.node.name}
|
||||
</Typography>
|
||||
|
||||
{currentUser.userType === UserType.admin && (
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setOpenMoveModal(true)}
|
||||
sx={{ ml: 1 }}
|
||||
>
|
||||
<DriveFileMoveOutlinedIcon fontSize="small" />
|
||||
</IconButton>
|
||||
{openMoveModal && (
|
||||
<MoveModal
|
||||
node={props.node}
|
||||
onClose={() => setOpenMoveModal(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{props.node.type === 'Installation' && (
|
||||
<div>
|
||||
{status === -1 ? (
|
||||
|
|
|
|||
|
|
@ -0,0 +1,165 @@
|
|||
import React, { useContext, useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
CircularProgress,
|
||||
FormControl,
|
||||
IconButton,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Modal,
|
||||
Select,
|
||||
Typography,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import Button from '@mui/material/Button';
|
||||
import { Close as CloseIcon } from '@mui/icons-material';
|
||||
import { I_Folder, I_Installation } from 'src/interfaces/InstallationTypes';
|
||||
import { InstallationsContext } from 'src/contexts/InstallationsContextProvider';
|
||||
import { ProductIdContext } from 'src/contexts/ProductIdContextProvider';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
interface MoveModalProps {
|
||||
node: I_Installation | I_Folder;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
// Returns the IDs of a folder and all its descendants (to prevent circular moves)
|
||||
function getDescendantIds(folderId: number, allItems: any[]): Set<number> {
|
||||
const result = new Set<number>([folderId]);
|
||||
const queue = [folderId];
|
||||
while (queue.length > 0) {
|
||||
const currentId = queue.shift();
|
||||
allItems
|
||||
.filter((item) => item.parentId === currentId && item.type === 'Folder')
|
||||
.forEach((child) => {
|
||||
result.add(child.id);
|
||||
queue.push(child.id);
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function MoveModal(props: MoveModalProps) {
|
||||
const theme = useTheme();
|
||||
const { foldersAndInstallations, moveInstallation, moveFolder, loading, setLoading, error, setError } =
|
||||
useContext(InstallationsContext);
|
||||
const { product } = useContext(ProductIdContext);
|
||||
const [selectedFolderId, setSelectedFolderId] = useState<number | ''>('');
|
||||
|
||||
// For folders: exclude self and all descendants to prevent circular nesting
|
||||
// For installations: any folder is valid
|
||||
const excludedIds =
|
||||
props.node.type === 'Folder'
|
||||
? getDescendantIds(props.node.id, foldersAndInstallations)
|
||||
: new Set<number>();
|
||||
|
||||
const availableFolders = foldersAndInstallations.filter(
|
||||
(item) => item.type === 'Folder' && !excludedIds.has(item.id)
|
||||
);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (selectedFolderId === '') return;
|
||||
setLoading(true);
|
||||
setError(false);
|
||||
if (props.node.type === 'Installation') {
|
||||
await moveInstallation(props.node.id, selectedFolderId as number, product);
|
||||
} else {
|
||||
await moveFolder(props.node.id, selectedFolderId as number, product);
|
||||
}
|
||||
setLoading(false);
|
||||
props.onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={true}
|
||||
onClose={() => {}}
|
||||
aria-labelledby="move-modal"
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '40%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 500,
|
||||
bgcolor: 'background.paper',
|
||||
borderRadius: 4,
|
||||
boxShadow: 24,
|
||||
p: 4
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle1" fontWeight="bold" mb={2}>
|
||||
<FormattedMessage id="moveTo" defaultMessage="Move to" />:{' '}
|
||||
<span style={{ color: theme.palette.primary.main }}>
|
||||
{props.node.name}
|
||||
</span>
|
||||
</Typography>
|
||||
|
||||
<FormControl fullWidth sx={{ mb: 2 }}>
|
||||
<InputLabel sx={{ fontSize: 14, backgroundColor: 'white' }}>
|
||||
<FormattedMessage id="folder" defaultMessage="Folder" />
|
||||
</InputLabel>
|
||||
<Select
|
||||
value={selectedFolderId}
|
||||
onChange={(e) => setSelectedFolderId(e.target.value as number)}
|
||||
>
|
||||
{availableFolders.map((folder: I_Folder) => (
|
||||
<MenuItem key={folder.id} value={folder.id}>
|
||||
{folder.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', marginTop: 10 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleSubmit}
|
||||
disabled={selectedFolderId === ''}
|
||||
sx={{ marginLeft: '10px' }}
|
||||
>
|
||||
<FormattedMessage id="submit" defaultMessage="Submit" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={props.onClose}
|
||||
sx={{ marginLeft: '10px' }}
|
||||
>
|
||||
<FormattedMessage id="cancel" defaultMessage="Cancel" />
|
||||
</Button>
|
||||
|
||||
{loading && (
|
||||
<CircularProgress
|
||||
sx={{ color: theme.palette.primary.main, marginLeft: '20px' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Alert
|
||||
severity="error"
|
||||
sx={{ ml: 1, display: 'flex', alignItems: 'center' }}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="errorOccured"
|
||||
defaultMessage="An error has occurred"
|
||||
/>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
onClick={() => setError(false)}
|
||||
sx={{ marginLeft: '4px' }}
|
||||
>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default MoveModal;
|
||||
|
|
@ -349,6 +349,42 @@ const InstallationsContextProvider = ({
|
|||
[fetchAllFoldersAndInstallations, navigate, removeToken]
|
||||
);
|
||||
|
||||
const moveInstallation = useCallback(
|
||||
async (installationId: number, parentId: number, product: number) => {
|
||||
try {
|
||||
await axiosConfig.put(
|
||||
`/MoveInstallation?installationId=${installationId}&parentId=${parentId}`
|
||||
);
|
||||
await fetchAllFoldersAndInstallations(product);
|
||||
} catch (error) {
|
||||
setError(true);
|
||||
if (error.response?.status === 401) {
|
||||
removeToken();
|
||||
navigate(routes.login);
|
||||
}
|
||||
}
|
||||
},
|
||||
[fetchAllFoldersAndInstallations, navigate, removeToken]
|
||||
);
|
||||
|
||||
const moveFolder = useCallback(
|
||||
async (folderId: number, parentId: number, product: number) => {
|
||||
try {
|
||||
await axiosConfig.put(
|
||||
`/MoveFolder?folderId=${folderId}&parentId=${parentId}`
|
||||
);
|
||||
await fetchAllFoldersAndInstallations(product);
|
||||
} catch (error) {
|
||||
setError(true);
|
||||
if (error.response?.status === 401) {
|
||||
removeToken();
|
||||
navigate(routes.login);
|
||||
}
|
||||
}
|
||||
},
|
||||
[fetchAllFoldersAndInstallations, navigate, removeToken]
|
||||
);
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
salimax_or_sodistore_Installations,
|
||||
|
|
@ -369,6 +405,8 @@ const InstallationsContextProvider = ({
|
|||
createFolder,
|
||||
updateFolder,
|
||||
deleteFolder,
|
||||
moveInstallation,
|
||||
moveFolder,
|
||||
//currentProduct,
|
||||
socket,
|
||||
openSocket,
|
||||
|
|
|
|||
Loading…
Reference in New Issue