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
4 changes: 2 additions & 2 deletions src/components/app-wrapper.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,8 @@ const lightTheme = createTheme({
reactflow: {
backgroundColor: 'white',
labeledGroup: {
backgroundColor: 'white',
borderColor: '#11161A',
backgroundColor: '#FAFAFA',
borderColor: '#BDBDBD',
},
edge: {
stroke: '#6F767B',
Expand Down
4 changes: 2 additions & 2 deletions src/components/graph/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import { NODE_HEIGHT, NODE_WIDTH } from './nodes/constants';
import { groupIdSuffix, LABELED_GROUP_TYPE } from './nodes/labeled-group-node.type';
import { CurrentTreeNode, isSecurityModificationNode, NetworkModificationNodeType } from './tree-node.type';

const widthSpacing = 70;
const heightSpacing = 90;
const widthSpacing = 110;
const heightSpacing = 140;
export const nodeWidth = NODE_WIDTH + widthSpacing;
export const nodeHeight = NODE_HEIGHT + heightSpacing;
export const snapGrid = [10, nodeHeight]; // Used for drag and drop
Expand Down
4 changes: 3 additions & 1 deletion src/components/graph/nodes/build-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@ export const BuildButton = ({
return !buildStatus || buildStatus === BUILD_STATUS.NOT_BUILT ? (
<PlayCircleFilled sx={styles.playColor} />
) : (
<StopCircleOutlined color="primary" />
<StopCircleOutlined
sx={{ color: (theme) => (theme.palette.mode === 'light' ? theme.palette.primary.light : undefined) }}
/>
);
};

Expand Down
24 changes: 24 additions & 0 deletions src/components/graph/nodes/build-status-chip.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* 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 { type SxStyle } from '@gridsuite/commons-ui';
import { Theme } from '@mui/material';

export const buildStatusChipStyles = {
base: (theme: Theme) =>
({
padding: theme.spacing(1, 0.5),
fontSize: '12px',
fontWeight: 400,
lineHeight: '100%',
}) as const,

notBuilt: {
background: (theme: Theme) => theme.node.buildStatus.notBuilt,
color: (theme: Theme) => theme.palette.getContrastText(theme.node.buildStatus.notBuilt),
} as SxStyle,
} as const;
81 changes: 30 additions & 51 deletions src/components/graph/nodes/build-status-chip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,53 +6,26 @@
*/

import React, { ReactElement } from 'react';
import { Chip } from '@mui/material';
import { Chip, useTheme } from '@mui/material';
import { useIntl } from 'react-intl';
import { BUILD_STATUS } from 'components/network/constants';
import { mergeSx, type SxStyle } from '@gridsuite/commons-ui';

function getBuildStatusSx(buildStatus: BUILD_STATUS | undefined): SxStyle {
return (theme) => {
const bs = theme.node.buildStatus;
// pick background based on status
let bg: string;

switch (buildStatus) {
case BUILD_STATUS.BUILT:
bg = bs.success;
break;
case BUILD_STATUS.BUILT_WITH_WARNING:
bg = bs.warning;
break;
case BUILD_STATUS.BUILT_WITH_ERROR:
bg = bs.error;
break;
default:
bg = bs.notBuilt;
break;
}

// only set explicit contrast color when it's the "notBuilt" background
const shouldSetContrast = bg === bs.notBuilt;

return {
background: bg,
...(shouldSetContrast ? { color: theme.palette.getContrastText(bg) } : {}),
'&:hover': {
backgroundColor: bg,
},
};
};
import { zoomStyles } from '../zoom.styles';
import { buildStatusChipStyles } from './build-status-chip.styles';

function getBuildStatusColor(buildStatus: BUILD_STATUS | undefined) {
switch (buildStatus) {
case BUILD_STATUS.BUILT:
return 'success';
case BUILD_STATUS.BUILT_WITH_WARNING:
return 'warning';
case BUILD_STATUS.BUILT_WITH_ERROR:
return 'error';
default:
return undefined;
}
}

const baseStyle: SxStyle = (theme) =>
({
padding: theme.spacing(1, 0.5),
fontSize: '12px',
fontWeight: 400,
lineHeight: '100%',
}) as const;

type BuildStatusChipProps = {
buildStatus?: BUILD_STATUS;
sx?: SxStyle;
Expand All @@ -62,18 +35,24 @@ type BuildStatusChipProps = {

const BuildStatusChip = ({ buildStatus = BUILD_STATUS.NOT_BUILT, sx, icon, onClick }: BuildStatusChipProps) => {
const intl = useIntl();
const theme = useTheme();

const showLabel = zoomStyles.visibility.showBuildStatusLabel(theme);
const label = showLabel ? intl.formatMessage({ id: buildStatus }) : undefined;
const color = getBuildStatusColor(buildStatus);

const label = intl.formatMessage({ id: buildStatus });
// Custom styling for NOT_BUILT status (no standard MUI color)
const notBuiltSx: SxStyle | undefined = color === undefined ? buildStatusChipStyles.notBuilt : undefined;

return (
<Chip
label={label}
size="small"
icon={icon}
onClick={onClick}
sx={mergeSx(getBuildStatusSx(buildStatus), sx, baseStyle)}
/>
// Combine styles: base + compact circular (if no label) + notBuilt color + parent overrides
const finalSx = mergeSx(
buildStatusChipStyles.base,
showLabel ? undefined : (theme) => zoomStyles.layout.getCompactChipSize(theme),
notBuiltSx,
sx
);

return <Chip label={label} size="small" icon={icon} onClick={onClick} color={color} sx={finalSx} />;
};

export default BuildStatusChip;
55 changes: 55 additions & 0 deletions src/components/graph/nodes/labeled-group-node.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* 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 { type MuiStyles } from '@gridsuite/commons-ui';
import { colors, Theme } from '@mui/material';
import { zoomStyles } from '../zoom.styles';
import { DETAIL_LEVELS } from '../zoom.constants';

export const LABEL_GROUP_OFFSET = 30;

export const getContainerStyle = (theme: Theme, isLight: boolean) => {
let backgroundColor = 'transparent';
if (theme.tree?.detailLevel === DETAIL_LEVELS.MINIMAL) {
backgroundColor = isLight ? theme.palette.grey[200] : colors.grey[700];
}

return {
...zoomStyles.labeledGroupBorder(theme),
background: backgroundColor,
borderColor: isLight ? colors.grey[400] : colors.grey[500],
};
};

export const labeledGroupNodeStyles = {
label: (theme) => ({
position: 'absolute',
top: -15,
right: 20,
backgroundColor: theme.reactflow.labeledGroup.backgroundColor,
padding: '3px 6px',
border: '1px solid',
borderColor: theme.reactflow.labeledGroup.borderColor,
boxShadow: theme.shadows[1],
fontSize: '13px',
display: theme.tree?.detailLevel === DETAIL_LEVELS.MINIMAL ? 'none' : 'flex',
gap: '4px',
alignItems: 'center',
justifyContent: 'center',
lineHeight: 1,
}),

icon: (theme) => ({
fontSize: zoomStyles.iconSize(theme),
verticalAlign: 'middle',
}),

text: (theme) => ({
display: theme.tree?.detailLevel === DETAIL_LEVELS.STANDARD ? 'inline' : 'none',
verticalAlign: 'middle',
}),
} as const satisfies MuiStyles;
48 changes: 16 additions & 32 deletions src/components/graph/nodes/labeled-group-node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,50 +5,34 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { Box } from '@mui/material';
import { NodeProps, ReactFlowState, useStore } from '@xyflow/react';
import { nodeHeight as nodeLayoutHeight, nodeWidth as nodeLayoutWidth } from '../layout';
import { Box, useTheme } from '@mui/material';
import { NodeProps } from '@xyflow/react';
import SecurityIcon from '@mui/icons-material/Security';
import { nodeHeight as nodeLayoutHeight, nodeWidth as nodeLayoutWidth } from '../layout';
import { FormattedMessage } from 'react-intl';
import { type MuiStyles } from '@gridsuite/commons-ui';
import { LabeledGroupNodeType } from './labeled-group-node.type';
import { NODE_HEIGHT, NODE_WIDTH } from './constants';

const styles = {
border: {
border: 'dashed 3px #8B8F8F',
borderRadius: '8px',
},
label: (theme) => ({
position: 'absolute',
top: -13,
right: 8,
backgroundColor: theme.reactflow.labeledGroup.backgroundColor,
padding: '0px 6px',
border: '1px solid',
borderColor: theme.reactflow.labeledGroup.borderColor,
fontSize: 12,
display: 'flex',
gap: '10px',
alignItems: 'center',
}),
} as const satisfies MuiStyles;
import { labeledGroupNodeStyles, getContainerStyle, LABEL_GROUP_OFFSET } from './labeled-group-node.styles';

export function LabeledGroupNode({ data }: NodeProps<LabeledGroupNodeType>) {
const theme = useTheme();
// Vertically, the border is halfway between the node and the edge above,
// and since that edge is centered between two nodes, we divide the space by 4.
const verticalPadding = (nodeLayoutHeight - NODE_HEIGHT) / 4;
// horizontally, the border is placed exactly halfway between two nodes — that's why we divide the space between them by 2.
const horizontalPadding = (nodeLayoutWidth - NODE_WIDTH) / 2;

const labeledGroupTopPosition = data.position.topLeft.row * nodeLayoutHeight - verticalPadding;
// Adjust position and size to account for border width and label block
const labeledGroupTopPosition = data.position.topLeft.row * nodeLayoutHeight - verticalPadding - LABEL_GROUP_OFFSET;
const labeledGroupLeftPosition = data.position.topLeft.column * nodeLayoutWidth - horizontalPadding;

const labeledGroupHeight =
(data.position.bottomRight.row - data.position.topLeft.row + 1) * nodeLayoutHeight - 2 * verticalPadding;
(data.position.bottomRight.row - data.position.topLeft.row + 1) * nodeLayoutHeight -
2 * verticalPadding +
2 * LABEL_GROUP_OFFSET;
const labeledGroupWidth = (data.position.bottomRight.column - data.position.topLeft.column + 1) * nodeLayoutWidth;

const zoom = useStore((s: ReactFlowState) => s.transform?.[2] ?? 1);
const isLight = theme.palette.mode === 'light';

return (
<Box
Expand All @@ -58,14 +42,14 @@ export function LabeledGroupNode({ data }: NodeProps<LabeledGroupNodeType>) {
left={labeledGroupLeftPosition}
height={labeledGroupHeight}
width={labeledGroupWidth}
sx={styles.border}
sx={getContainerStyle(theme, isLight)}
>
{zoom >= 0.5 && (
<Box sx={styles.label}>
<SecurityIcon sx={{ fontSize: '12px' }} />
<Box sx={labeledGroupNodeStyles.label}>
<SecurityIcon sx={labeledGroupNodeStyles.icon} />
<Box component="span" sx={labeledGroupNodeStyles.text}>
<FormattedMessage id="labeledGroupSecurity" />
</Box>
)}
</Box>
</Box>
);
}
92 changes: 92 additions & 0 deletions src/components/graph/nodes/network-modification-node.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* 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 { type MuiStyles } from '@gridsuite/commons-ui';
import { Theme } from '@mui/material';
import { baseNodeStyles, interactiveNodeStyles } from './styles';
import { zoomStyles } from '../zoom.styles';

export const getBorderWidthStyle = (theme: Theme, isSelected: boolean) => ({
borderWidth: zoomStyles.borderWidth(theme, isSelected),
});

export const getNodeBaseStyle = (theme: Theme, isSelected: boolean) =>
isSelected ? modificationNodeStyles.selected(theme) : modificationNodeStyles.default(theme);

export const modificationNodeStyles = {
selected: (theme) => ({
...baseNodeStyles(theme, 'column'),
background: theme.node.modification.selectedBackground,
border: theme.node.modification.selectedBorder,
boxShadow: theme.shadows[6],
...interactiveNodeStyles(theme, 'modification'),
}),

default: (theme) => ({
...baseNodeStyles(theme, 'column'),
border: theme.node.modification.border,
...interactiveNodeStyles(theme, 'modification'),
}),

contentBox: (theme) => ({
flexGrow: 1,
display: zoomStyles.visibility.showNodeContent(theme) ? 'flex' : 'none',
alignItems: 'flex-end',
marginLeft: theme.spacing(1),
marginRight: theme.spacing(1),
marginBottom: theme.spacing(1),
}),

typographyText: (theme) => ({
color: theme.palette.text.primary,
fontSize: '20px',
fontWeight: 400,
lineHeight: 'normal',
textAlign: 'left',
display: '-webkit-box',
WebkitBoxOrient: 'vertical',
WebkitLineClamp: 2,
overflow: 'hidden',
width: 'auto',
textOverflow: 'ellipsis',
wordBreak: 'break-word',
}),

footer: (theme) => ({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginLeft: theme.spacing(1),
marginRight: theme.spacing(1),
height: zoomStyles.layout.useFullHeightFooter(theme) ? '100%' : '35%',
}),

chipFloating: (theme) => ({
position: 'absolute',
top: theme.spacing(-4.3),
left: theme.spacing(1),
zIndex: 2,
}),

chipLarge: (theme) => zoomStyles.layout.getLargeChipSize(theme) ?? {},

buildButtonContainer: (theme) => ({
display: zoomStyles.visibility.showBuildButton(theme) ? 'block' : 'none',
}),

globalBuildStatusIcon: (theme) => ({
fontSize: zoomStyles.iconSize(theme),
'& path': {
stroke: `currentColor`,
strokeWidth: zoomStyles.iconStrokeWidth(theme),
},
}),

tooltip: {
maxWidth: '720px',
},
} as const satisfies MuiStyles;
Loading
Loading