Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 11 additions & 26 deletions src/components/dialogs/export-network-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,17 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import {
Alert,
Collapse,
Dialog,
DialogTitle,
Stack,
Typography,
InputLabel,
Alert,
FormControl,
Select,
MenuItem,
CircularProgress,
IconButton,
InputLabel,
MenuItem,
Select,
Stack,
Typography,
} from '@mui/material';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
Expand All @@ -25,7 +24,7 @@ import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import Button from '@mui/material/Button';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { CancelButton, FlatParameters, fetchDirectoryElementPath, useSnackMessage } from '@gridsuite/commons-ui';
import { CancelButton, fetchDirectoryElementPath, FlatParameters, useSnackMessage } from '@gridsuite/commons-ui';
import { ExportFormatProperties, getAvailableExportFormats } from '../../services/study';
import { getExportUrl } from '../../services/study/network';
import { isBlankOrEmpty } from 'components/utils/validation-functions';
Expand All @@ -51,7 +50,7 @@ const STRING_LIST = 'STRING_LIST';
interface ExportNetworkDialogProps {
open: boolean;
onClose: () => void;
onClick: (url: string) => void;
onClick: (url: string, selectedFormat: string, fileName: string) => void;
studyUuid: UUID;
nodeUuid: UUID;
rootNetworkUuid: UUID;
Expand All @@ -68,10 +67,9 @@ export function ExportNetworkDialog({
const intl = useIntl();
const [formatsWithParameters, setFormatsWithParameters] = useState<Record<string, ExportFormatProperties>>({});
const [selectedFormat, setSelectedFormat] = useState('');
const [loading, setLoading] = useState(false);
const [exportStudyErr, setExportStudyErr] = useState('');
const { snackError } = useSnackMessage();
const [fileName, setFileName] = useState<string>();
const [fileName, setFileName] = useState<string>('');
const [enableDeveloperMode] = useParameterState(PARAM_DEVELOPER_MODE);
const [unfolded, setUnfolded] = useState(false);

Expand Down Expand Up @@ -150,8 +148,7 @@ export function ExportNetworkDialog({

// we have already as parameters, the access tokens, so use '&' instead of '?'
suffix = urlSearchParams.toString() ? '&' + urlSearchParams.toString() : '';
setLoading(true);
onClick(downloadUrl + suffix);
onClick(downloadUrl + suffix, selectedFormat, fileName);
} else {
setExportStudyErr(intl.formatMessage({ id: 'exportStudyErrorMsg' }));
}
Expand All @@ -161,7 +158,6 @@ export function ExportNetworkDialog({
setCurrentParameters({});
setExportStudyErr('');
setSelectedFormat('');
setLoading(false);
onClose();
};

Expand All @@ -183,7 +179,7 @@ export function ExportNetworkDialog({
margin="dense"
label={<FormattedMessage id="download.fileName" />}
id="fileName"
value={fileName}
value={fileName || ''}
sx={{ width: '100%', marginBottom: 1 }}
fullWidth
variant="filled"
Expand Down Expand Up @@ -234,17 +230,6 @@ export function ExportNetworkDialog({
/>
</Collapse>
{exportStudyErr !== '' && <Alert severity="error">{exportStudyErr}</Alert>}
{loading && (
<div
style={{
display: 'flex',
justifyContent: 'center',
marginTop: '5px',
}}
>
<CircularProgress />
</div>
)}
</DialogContent>
<DialogActions>
<CancelButton onClick={handleClose} />
Expand Down
24 changes: 16 additions & 8 deletions src/components/network-modification-tree-pane.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@

import { useCallback, useEffect, useRef, useState } from 'react';
import {
deletedOrRenamedNodes,
networkModificationHandleSubtree,
networkModificationTreeNodeAdded,
networkModificationTreeNodeMoved,
networkModificationTreeNodesRemoved,
networkModificationTreeNodesUpdated,
removeNotificationByNode,
networkModificationHandleSubtree,
setNodeSelectionForCopy,
resetLogsFilter,
reorderNetworkModificationTreeNodes,
deletedOrRenamedNodes,
resetLogsFilter,
resetLogsPagination,
setNodeSelectionForCopy,
} from '../redux/actions';
import { useDispatch, useSelector } from 'react-redux';
import PropTypes from 'prop-types';
Expand All @@ -29,20 +29,21 @@ import { BUILD_STATUS } from './network/constants';
import {
copySubtree,
copyTreeNode,
createNodeSequence,
createTreeNode,
cutSubtree,
cutTreeNode,
stashSubtree,
stashTreeNode,
fetchNetworkModificationSubtree,
fetchNetworkModificationTreeNode,
fetchStashedNodes,
createNodeSequence,
stashSubtree,
stashTreeNode,
} from '../services/study/tree-subtree';
import { buildNode, getUniqueNodeName, unbuildNode } from '../services/study/index';
import { RestoreNodesDialog } from './dialogs/restore-node-dialog';
import { CopyType } from './network-modification.type';
import { NodeSequenceType, NotificationType, PENDING_MODIFICATION_NOTIFICATION_TYPES } from 'types/notification-types';
import useExportSubscription from '../hooks/use-export-subscription.js';

const noNodeSelectionForCopy = {
sourceStudyUuid: null,
Expand Down Expand Up @@ -123,6 +124,12 @@ export const NetworkModificationTreePane = ({ studyUuid, currentRootNetworkUuid,

const studyUpdatedForce = useSelector((state) => state.studyUpdated);

const { subscribeExport } = useExportSubscription({
studyUuid: studyUuid,
nodeUuid: currentNode?.id,
rootNetworkUuid: currentRootNetworkUuid,
});

const updateNodes = useCallback(
(updatedNodesIds) => {
Promise.all(
Expand Down Expand Up @@ -461,7 +468,8 @@ export const NetworkModificationTreePane = ({ studyUuid, currentRootNetworkUuid,

const [openExportDialog, setOpenExportDialog] = useState(false);

const handleClickExportNodeNetwork = (url) => {
const handleClickExportNodeNetwork = (url, selectedFormat, fileName) => {
subscribeExport(selectedFormat, fileName);
window.open(url, DownloadIframe);
setOpenExportDialog(false);
};
Expand Down
10 changes: 9 additions & 1 deletion src/components/study-container.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { fetchStudy, recreateStudyNetwork, reindexAllRootNetwork } from 'service
import { HttpStatusCode } from 'utils/http-status-code';
import { NodeType } from './graph/tree-node.type';
import {
isExportNetworkNotification,
isIndexationStatusNotification,
isLoadflowResultNotification,
isStateEstimationResultNotification,
Expand All @@ -47,6 +48,7 @@ import {
RootNetworkIndexationStatus,
} from 'types/notification-types';
import { useDiagramGridLayout } from 'hooks/use-diagram-grid-layout';
import useExportNotificationHandler from '../hooks/use-export-notification-handler.js';

function useStudy(studyUuidRequest) {
const dispatch = useDispatch();
Expand Down Expand Up @@ -143,6 +145,8 @@ export function StudyContainer({ view, onChangeTab }) {

const { snackError, snackWarning, snackInfo } = useSnackMessage();

const { handleExportNotification } = useExportNotificationHandler();

const displayErrorNotifications = useCallback(
(eventData) => {
const updateTypeHeader = eventData.headers.updateType;
Expand Down Expand Up @@ -264,11 +268,15 @@ export function StudyContainer({ view, onChangeTab }) {
sendAlert(eventData);
return; // here, we do not want to update the redux state
}
if (isExportNetworkNotification(eventData)) {
handleExportNotification(eventData);
return;
}
displayErrorNotifications(eventData);
dispatch(studyUpdated(eventData));
},
// Note: dispatch doesn't change
[dispatch, displayErrorNotifications, sendAlert]
[dispatch, displayErrorNotifications, handleExportNotification, sendAlert]
);

useNotificationsListener(NotificationsUrlKeys.STUDY, { listenerCallbackMessage: handleStudyUpdate });
Expand Down
51 changes: 51 additions & 0 deletions src/hooks/use-export-download.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* Copyright (c) 2025, RTE (http://www.rte-france.com)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { UUID } from 'crypto';
import { useSnackMessage } from '@gridsuite/commons-ui';
import { useCallback } from 'react';
import { fetchExportNetworkFile } from '../services/study';
import { downloadZipFile } from '../services/utils';
import { HttpStatusCode } from '../utils/http-status-code';

export function useExportDownload() {
const { snackWarning, snackError } = useSnackMessage();

const downloadExportNetworkFile = useCallback(
(exportUuid: UUID) => {
fetchExportNetworkFile(exportUuid)
.then(async (response) => {
const contentDisposition = response.headers.get('Content-Disposition');
let filename = 'export.zip';
if (contentDisposition?.includes('filename=')) {
const regex = /filename="?([^"]+)"?/;
const match = regex.exec(contentDisposition);
if (match?.[1]) {
filename = match[1];
}
}

const blob = await response.blob();
downloadZipFile(blob, filename);
})
.catch((responseError: any) => {
const error = responseError as Error & { status: number };
if (error.status === HttpStatusCode.NOT_FOUND) {
snackWarning({
headerId: 'export.header.fileNotFound',
});
} else {
snackError({
messageTxt: error.message,
headerId: 'export.header.fileError',
});
}
});
},
[snackWarning, snackError]
);
return { downloadExportNetworkFile };
}
103 changes: 103 additions & 0 deletions src/hooks/use-export-notification-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* Copyright (c) 2025, RTE (http://www.rte-france.com)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { UUID } from 'crypto';
import { useIntl } from 'react-intl';
import { useSnackMessage } from '@gridsuite/commons-ui';
import { useCallback } from 'react';
import { useExportDownload } from './use-export-download';
import { useSelector } from 'react-redux';
import { AppState } from '../redux/reducer';
import { ExportNetworkEventData } from '../types/notification-types';

export function buildExportIdentifier({
studyUuid,
nodeUuid,
rootNetworkUuid,
format,
fileName,
}: {
studyUuid: UUID;
nodeUuid: UUID;
rootNetworkUuid: UUID;
format: string;
fileName: string;
}) {
return `${studyUuid}|${rootNetworkUuid}|${nodeUuid}|${fileName}|${format}`;
}

function getExportState(): Set<string> | null {
const state = sessionStorage.getItem('export-subscriptions');
return state ? new Set<string>(JSON.parse(state)) : null;
}

function saveExportState(state: Set<string>): void {
sessionStorage.setItem('export-subscriptions', JSON.stringify([...state]));
}

export function isExportSubscribed(identifier: string): boolean {
const exportState = getExportState();
return exportState?.has(identifier) ?? false;
}

export function unsetExportSubscription(identifier: string): void {
const exportState = getExportState();
if (exportState) {
exportState.delete(identifier);
saveExportState(exportState);
}
}

export default function useExportNotificationHandler() {
const intl = useIntl();
const { snackWarning, snackInfo } = useSnackMessage();
const { downloadExportNetworkFile } = useExportDownload();
const userId = useSelector((state: AppState) => state.user?.profile.sub);

const handleExportNotification = useCallback(
(eventData: ExportNetworkEventData) => {
const {
studyUuid,
node,
rootNetworkUuid,
format,
userId: useId,
exportUuid,
fileName,
error,
} = eventData.headers;

const exportIdentifierNotif = buildExportIdentifier({
studyUuid: studyUuid,
nodeUuid: node,
rootNetworkUuid: rootNetworkUuid,
format,
fileName,
});

const isSubscribed = isExportSubscribed(exportIdentifierNotif);

if (isSubscribed && useId === userId) {
unsetExportSubscription(exportIdentifierNotif);

if (error) {
snackWarning({
messageTxt: error,
});
} else {
downloadExportNetworkFile(exportUuid);
snackInfo({
headerTxt: intl.formatMessage({ id: 'exportNetwork' }),
messageTxt: intl.formatMessage({ id: 'export.message.downloadStarted' }, { fileName }),
});
}
}
},
[userId, snackWarning, downloadExportNetworkFile, snackInfo, intl]
);

return { handleExportNotification };
}
Loading
Loading