// Copyright (c) 2025-2026, RTE (https://www.rte-france.com) // This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. // If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, // you can obtain one at http://mozilla.org/MPL/2.0/. // SPDX-License-Identifier: MPL-2.0 // This file is part of Co-Study4Grid a Power Grid Study tool Assistant Interface to help solve contigencies for a grid state under study. import React, { useMemo } from 'react'; import Select, { type MultiValue } from 'react-select'; import type { ActionOverviewFilters } from '../types'; import SidebarSummary from './SidebarSummary'; import { colors, radius, space } from '../styles/tokens'; interface ContingencyOption { value: string; label: string; } interface AppSidebarProps { /** Currently APPLIED contingency (list of element IDs disconnected). */ selectedContingency: string[]; /** Pending list the user is composing — committed via Apply button. */ pendingContingency: string[]; branches: string[]; nameMap: Record; n1LinesOverloaded: string[] | undefined; n1LinesOverloadedRho: number[] | undefined; /** N-state pre-existing overloads, surfaced in the SidebarSummary info bubble. */ nLinesOverloaded?: string[]; nLinesOverloadedRho?: number[]; selectedOverloads: Set | null | undefined; /** Replace the pending list with the user's current selection. */ onPendingContingencyChange: (next: string[]) => void; /** Commit ``pendingContingency`` as the applied contingency. */ onContingencyApply: () => void; displayName: (id: string) => string; onContingencyZoom: (assetName: string) => void; onOverloadClick: (actionId: string, assetName: string, tab: 'n' | 'contingency') => void; /** Toggle an N-1 overload's inclusion in the analysis monitoring set. * Forwarded to SidebarSummary's info bubble. */ onToggleOverload?: (overload: string) => void; monitorDeselected?: boolean; onToggleMonitorDeselected?: () => void; monitoringHint?: string | null; /** Drop the current contingency to investigate a new one. Triggers * the confirmation dialog at the call site. */ onClearContingency?: () => void; /** Hide the "Select Contingency" card. Used after a contingency has * been committed AND its overloads detected, since the sticky banner * then carries the same info plus a Clear shortcut. */ hideContingencyPicker?: boolean; /** Collapsed mode: the sidebar shrinks to a thin strip so the * visualization panel can take the full width. */ collapsed?: boolean; onToggleCollapsed?: () => void; /** Shared severity + action-type filters; forwarded to * SidebarSummary so the persistent strip can host the filter * rings alongside the contingency / overload lines. */ overviewFilters?: ActionOverviewFilters; onOverviewFiltersChange?: (next: ActionOverviewFilters) => void; /** Whether the action feed has any card to filter right now. */ hasActions?: boolean; children: React.ReactNode; } /** * Left-sidebar layout shell: * * - A COMPACT sticky strip at the top () keeps only * the clickable fields of interest visible while scrolling * (selected contingency → zoom active tab; contingency overloads → * jump to the contingency tab + zoom). The strip also hosts the * Clear-contingency shortcut and the overload-info bubble. * - The Select Contingency card with the multi-select picker + Trigger * button shows below the strip — but only as long as no contingency * has been committed (after that, the strip carries the same info * and the picker would be redundant). * - The ActionFeed (rendered as ``children``) sits below. * * The shell can be collapsed to a thin strip when the operator wants * to expand the visualization panel. */ export default function AppSidebar({ selectedContingency, pendingContingency, branches, nameMap, n1LinesOverloaded, n1LinesOverloadedRho, nLinesOverloaded, nLinesOverloadedRho, selectedOverloads, onPendingContingencyChange, onContingencyApply, displayName, onContingencyZoom, onOverloadClick, onToggleOverload, monitorDeselected, onToggleMonitorDeselected, monitoringHint, onClearContingency, hideContingencyPicker, collapsed, onToggleCollapsed, overviewFilters, onOverviewFiltersChange, hasActions, children, }: AppSidebarProps) { // Pending differs from applied → user has unconfirmed edits to the // contingency that won't take effect until they hit Trigger. const samePendingApplied = pendingContingency.length === selectedContingency.length && pendingContingency.every((e, i) => e === selectedContingency[i]); const dirty = !samePendingApplied; const branchOptions: ContingencyOption[] = useMemo( () => branches.map(b => ({ value: b, label: nameMap[b] ? `${nameMap[b]} — ${b}` : b, })), [branches, nameMap], ); const optionByValue = useMemo(() => { const m = new Map(); for (const o of branchOptions) m.set(o.value, o); return m; }, [branchOptions]); const selectedOptions: ContingencyOption[] = useMemo( () => pendingContingency.map(id => optionByValue.get(id) ?? { value: id, label: nameMap[id] ? `${nameMap[id]} — ${id}` : id } ), [pendingContingency, optionByValue, nameMap], ); if (collapsed) { return (
); } return (
{onToggleCollapsed && ( )}
{branches.length > 0 && !hideContingencyPicker && (
isMulti isClearable={false} options={branchOptions} value={selectedOptions} onChange={(next: MultiValue) => onPendingContingencyChange(next.map(o => o.value)) } placeholder="Search line/bus…" noOptionsMessage={() => 'No matching elements'} classNamePrefix="cs4g-contingency" styles={{ control: (base) => ({ ...base, minHeight: 36, borderRadius: radius.sm, borderColor: colors.border, background: colors.surface, fontSize: '0.85rem', }), input: (base) => ({ ...base, color: colors.textPrimary }), placeholder: (base) => ({ ...base, color: colors.textTertiary }), multiValue: (base) => ({ ...base, background: colors.borderSubtle, border: `1px solid ${colors.border}`, }), multiValueLabel: (base) => ({ ...base, color: colors.textPrimary, fontSize: '0.78rem', }), option: (base, state) => ({ ...base, fontSize: '0.85rem', background: state.isFocused ? colors.borderSubtle : 'transparent', color: colors.textPrimary, cursor: 'pointer', }), menu: (base) => ({ ...base, zIndex: 30, background: colors.surface }), }} />
)} {children}
); }