Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import useExportSubscription from '../hooks/use-export-subscription.js';
import useExportSubscription from '../hooks/use-export-subscription';


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,
Copy link
Contributor

@thangqp thangqp Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

must pass nodeUuid in the params of subscribeExport because it is activeNode.id not currentNode.id

BUG when a node is not selected but we do export on this node via context menu

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 @@ -38,6 +38,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 @@ -46,6 +47,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 @@ -142,6 +144,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 @@ -263,11 +267,15 @@ export function StudyContainer({ view, onChangeTab }) {
sendAlert(eventData);
return; // here, we do not want to update the redux state
}
if (isExportNetworkNotification(eventData)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sylvain has a ticket in this sprint to remove studyUpdated to redux.
In the future, each callback handler must subscribe separately by useNotificationListener and inside each handler we check the updateType, for example in yours case, using predicate isExportNetworkNotification

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 });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useExportNotification();

Expand Down
51 changes: 51 additions & 0 deletions src/hooks/use-export-download.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Three files use-export-xxx can be grouped in a file unique which presents a feature or move all into a directory, for example ./use-export-network

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() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export default function useExportNotificationHandler() {
export default function useExportNotification() {

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

const handleExportNotification = useCallback(
(eventData: ExportNetworkEventData) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
(eventData: ExportNetworkEventData) => {
(event: MessageEvent<string>) => {

const {
studyUuid,
node,
rootNetworkUuid,
format,
userId: useId,
exportUuid,
fileName,
error,
} = eventData.headers;
Comment on lines +62 to +71
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const {
studyUuid,
node,
rootNetworkUuid,
format,
userId: useId,
exportUuid,
fileName,
error,
} = eventData.headers;
const eventData = JSON.parse(event.data);
if (isExportNetworkNotification(eventData)) {
const {
studyUuid,
node,
rootNetworkUuid,
format,
userId: useId,
exportUuid,
fileName,
error,
} = eventData.headers;
// old code
}


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 };
Copy link
Contributor

@thangqp thangqp Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return { handleExportNotification };
useNotificationsListener(NotificationsUrlKeys.STUDY, { listenerCallbackMessage: handleExportNotification });

}
Loading
Loading