diff --git a/src/components/computing-status/use-all-computing-status.ts b/src/components/computing-status/use-all-computing-status.ts index 008846af77..65fb022af0 100644 --- a/src/components/computing-status/use-all-computing-status.ts +++ b/src/components/computing-status/use-all-computing-status.ts @@ -6,6 +6,7 @@ */ import { useComputingStatus } from './use-computing-status'; +import { useSecurityAnalysisProgress } from './use-security-analysis-progress'; import { getDynamicMarginCalculationRunningStatus, getDynamicSecurityAnalysisRunningStatus, @@ -161,6 +162,8 @@ export const useAllComputingStatus = (studyUuid: UUID, currentNodeUuid: UUID, cu securityAnalysisAvailability ); + useSecurityAnalysisProgress(studyUuid, currentNodeUuid, currentRootNetworkUuid); + useComputingStatus( studyUuid, currentNodeUuid, diff --git a/src/components/computing-status/use-security-analysis-progress.ts b/src/components/computing-status/use-security-analysis-progress.ts new file mode 100644 index 0000000000..3ecc46244f --- /dev/null +++ b/src/components/computing-status/use-security-analysis-progress.ts @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2026, 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 { useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import { NotificationsUrlKeys, useNotificationsListener } from '@gridsuite/commons-ui'; +import type { UUID } from 'node:crypto'; +import { setSaProgress } from '../../redux/actions'; +import { AppDispatch } from '../../redux/store'; +import { + NotificationType, + parseEventData, + SecurityAnalysisProgressEventData, + StudyUpdateEventData, +} from '../../types/notification-types'; + +function isSecurityAnalysisProgressNotification(notif: unknown): notif is SecurityAnalysisProgressEventData { + return ( + (notif as SecurityAnalysisProgressEventData).headers?.updateType === NotificationType.SECURITY_ANALYSIS_PROGRESS + ); +} + +export function useSecurityAnalysisProgress(studyUuid: UUID, nodeUuid: UUID, currentRootNetworkUuid: UUID) { + const dispatch = useDispatch(); + + const handleProgressEvent = useCallback( + (event?: MessageEvent) => { + if (!studyUuid || !nodeUuid || !currentRootNetworkUuid) { + return; + } + const eventData = parseEventData(event ?? null); + if (!isSecurityAnalysisProgressNotification(eventData)) { + return; + } + const { node, rootNetworkUuid, progressCurrent, progressTotal } = eventData.headers; + if (node !== nodeUuid || rootNetworkUuid !== currentRootNetworkUuid) { + return; + } + dispatch(setSaProgress(progressCurrent, progressTotal)); + }, + [studyUuid, nodeUuid, currentRootNetworkUuid, dispatch] + ); + + useNotificationsListener(NotificationsUrlKeys.STUDY, { + listenerCallbackMessage: handleProgressEvent, + }); +} diff --git a/src/components/run-button.jsx b/src/components/run-button.jsx index 975a1e34cf..36450589d1 100644 --- a/src/components/run-button.jsx +++ b/src/components/run-button.jsx @@ -19,6 +19,7 @@ import { DialogContentText } from '@mui/material'; const RunButton = ({ runnables, activeRunnables, getStatus, computationStopped, disabled }) => { const intl = useIntl(); const isDirtyComputationParameters = useSelector((state) => state.isDirtyComputationParameters); + const securityAnalysisProgress = useSelector((state) => state.securityAnalysisProgress); const [isLaunchingPopupOpen, setIsLaunchingPopupOpen] = useState(false); // a transient state which is used only for a run with popup dialog @@ -127,6 +128,7 @@ const RunButton = ({ runnables, activeRunnables, getStatus, computationStopped, text={runnablesText[selectedRunnable] || ''} actionOnRunnable={runnables[selectedRunnable].actionOnRunnable} computationStopped={computationStopped} + progress={selectedRunnable === ComputingType.SECURITY_ANALYSIS ? securityAnalysisProgress : null} /> void; actionOnRunnable: () => void; onSelectionChange: (index: number) => void; + progress?: { current: number; total: number } | null; } const SplitButton = ({ @@ -154,6 +158,7 @@ const SplitButton = ({ onClick, actionOnRunnable, onSelectionChange, + progress, }: SplitButtonProps) => { const [open, setOpen] = useState(false); const computationStarting = useSelector((state: AppState) => state.computationStarting); @@ -190,6 +195,16 @@ const SplitButton = ({ const getRunningIcon = (status: RunningStatus) => { switch (status) { case RunningStatus.RUNNING: + if (progress && progress.total > 0) { + const pct = Math.round((progress.current / progress.total) * 100); + return ( + + + + + + ); + } return ; case RunningStatus.SUCCEED: return ; diff --git a/src/redux/actions.ts b/src/redux/actions.ts index e15f7dd656..804c63a089 100644 --- a/src/redux/actions.ts +++ b/src/redux/actions.ts @@ -976,6 +976,20 @@ export function removeFromGlobalFilterOptions(id: string): RemoveFromGlobalFilte }; } +export const SET_SA_PROGRESS = 'SET_SA_PROGRESS'; +export type SetSaProgressAction = Readonly> & { + current: number; + total: number; +}; + +export function setSaProgress(current: number, total: number): SetSaProgressAction { + return { + type: SET_SA_PROGRESS, + current, + total, + }; +} + export const SET_LAST_COMPLETED_COMPUTATION = 'SET_LAST_COMPLETED_COMPUTATION'; export type SetLastCompletedComputationAction = Readonly> & { lastCompletedComputation: ComputingType | null; diff --git a/src/redux/reducer.ts b/src/redux/reducer.ts index f34f006c1c..b47098d24c 100644 --- a/src/redux/reducer.ts +++ b/src/redux/reducer.ts @@ -166,6 +166,8 @@ import { SET_COMPUTING_STATUS_INFOS, SET_DIRTY_COMPUTATION_PARAMETERS, SET_LAST_COMPLETED_COMPUTATION, + SET_SA_PROGRESS, + type SetSaProgressAction, SET_MODIFICATIONS_IN_PROGRESS, SET_MONO_ROOT_STUDY, SET_ONE_BUS_SHORTCIRCUIT_ANALYSIS_DIAGRAM, @@ -523,6 +525,7 @@ const initialState: AppState = { computingStatusParameters: { [ComputingType.LOAD_FLOW]: null, }, + securityAnalysisProgress: null, computationStarting: false, optionalServices: (Object.keys(OptionalServicesNames) as OptionalServicesNames[]).map((key) => ({ name: key, @@ -1356,6 +1359,16 @@ export const reducer = createReducer(initialState, (builder) => { builder.addCase(SET_COMPUTING_STATUS, (state, action: SetComputingStatusAction) => { state.computingStatus[action.computingType] = action.runningStatus; + if ( + action.computingType === ComputingType.SECURITY_ANALYSIS && + action.runningStatus !== RunningStatus.RUNNING + ) { + state.securityAnalysisProgress = null; + } + }); + + builder.addCase(SET_SA_PROGRESS, (state, action: SetSaProgressAction) => { + state.securityAnalysisProgress = { current: action.current, total: action.total }; }); builder.addCase( diff --git a/src/redux/reducer.type.ts b/src/redux/reducer.type.ts index 77c1400584..1d4411d00b 100644 --- a/src/redux/reducer.type.ts +++ b/src/redux/reducer.type.ts @@ -224,6 +224,7 @@ export interface AppState extends CommonStoreState, AppConfigState { computingStatus: ComputingStatus; computingStatusParameters: ComputingStatusParameters; lastCompletedComputation: ComputingType | null; + securityAnalysisProgress: { current: number; total: number } | null; computationStarting: boolean; optionalServices: IOptionalService[]; oneBusShortCircuitAnalysisDiagram: OneBusShortCircuitAnalysisDiagram | null; diff --git a/src/types/notification-types.ts b/src/types/notification-types.ts index c1cf37068e..62d6b5567a 100644 --- a/src/types/notification-types.ts +++ b/src/types/notification-types.ts @@ -62,6 +62,7 @@ export enum NotificationType { SECURITY_ANALYSIS_RESULT = 'securityAnalysisResult', SECURITY_ANALYSIS_FAILED = 'securityAnalysis_failed', SECURITY_ANALYSIS_STATUS = 'securityAnalysis_status', + SECURITY_ANALYSIS_PROGRESS = 'securityAnalysis_progress', SENSITIVITY_ANALYSIS_RESULT = 'sensitivityAnalysisResult', SENSITIVITY_ANALYSIS_FAILED = 'sensitivityAnalysis_failed', SENSITIVITY_ANALYSIS_STATUS = 'sensitivityAnalysis_status', @@ -446,6 +447,14 @@ interface LoadflowStatusEventDataHeaders extends ComputationStatusEventDataHeade updateType: NotificationType.LOADFLOW_STATUS; } +interface SecurityAnalysisProgressEventDataHeaders extends CommonStudyEventDataHeaders { + updateType: NotificationType.SECURITY_ANALYSIS_PROGRESS; + node: UUID; + rootNetworkUuid: UUID; + progressCurrent: number; + progressTotal: number; +} + interface SecurityAnalysisResultEventDataHeaders extends ComputationResultEventDataHeaders { updateType: NotificationType.SECURITY_ANALYSIS_RESULT; } @@ -769,6 +778,11 @@ export interface LoadflowStatusEventData { payload: undefined; } +export interface SecurityAnalysisProgressEventData { + headers: SecurityAnalysisProgressEventDataHeaders; + payload: undefined; +} + export interface SecurityAnalysisResultEventData { headers: SecurityAnalysisResultEventDataHeaders; payload: undefined; @@ -1252,6 +1266,7 @@ export type StudyUpdateEventData = | LoadflowResultEventData | LoadflowFailedEventData | LoadflowStatusEventData + | SecurityAnalysisProgressEventData | SecurityAnalysisResultEventData | SecurityAnalysisFailedEventData | SecurityAnalysisStatusEventData