Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add visual cue on CSCDetail and CSCExpanded to identify CSCs on simulation mode #651

Merged
merged 10 commits into from
Jul 10, 2024
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
Version History
===============

v6.2.0
------

* Add visual cue on CSCDetail and CSCExpanded to identify CSCs on simulation mode `<https://github.com/lsst-ts/LOVE-frontend/pull/651>`_

v6.1.1
------

Expand Down
16 changes: 9 additions & 7 deletions love/src/components/CSCSummary/CSCDetail/CSCDetail.container.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ const CSCDetailContainer = ({
serverTime,
embedded,
withWarning,
simulationMode,
isRaw,
subscriptions,
}) => {
Expand All @@ -112,6 +113,7 @@ const CSCDetailContainer = ({
embedded={embedded}
withWarning={withWarning}
serverTime={serverTime}
simulationMode={simulationMode}
/>
);
};
Expand All @@ -121,6 +123,7 @@ const mapDispatchToProps = (dispatch, ownProps) => {
`event-${ownProps.name}-${ownProps.salindex}-summaryState`,
`event-${ownProps.name}-${ownProps.salindex}-logMessage`,
`event-${ownProps.name}-${ownProps.salindex}-errorCode`,
`event-${ownProps.name}-${ownProps.salindex}-simulationMode`,
`event-Heartbeat-0-stream`,
];
return {
Expand All @@ -135,16 +138,15 @@ const mapDispatchToProps = (dispatch, ownProps) => {
};

const mapStateToProps = (state, ownProps) => {
const withWarning = getCSCWithWarning(state, ownProps.name, ownProps.salindex);
let summaryStateData = getStreamData(state, `event-${ownProps.name}-${ownProps.salindex}-summaryState`);
let heartbeatData = getCSCHeartbeat(state, ownProps.name, ownProps.salindex);
const serverTime = getServerTime(state);
if (!summaryStateData) {
summaryStateData = {};
}
const withWarning = getCSCWithWarning(state, ownProps.name, ownProps.salindex);
const heartbeatData = getCSCHeartbeat(state, ownProps.name, ownProps.salindex);
const summaryStateData = getStreamData(state, `event-${ownProps.name}-${ownProps.salindex}-summaryState`)?.[0];
const simulationMode = getStreamData(state, `event-${ownProps.name}-${ownProps.salindex}-simulationMode`)?.[0];

return {
summaryStateData: summaryStateData[0],
summaryStateData,
simulationMode,
heartbeatData,
withWarning,
serverTime,
Expand Down
118 changes: 78 additions & 40 deletions love/src/components/CSCSummary/CSCDetail/CSCDetail.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,33 +26,48 @@ import styles from './CSCDetail.module.css';

export default class CSCDetail extends Component {
static propTypes = {
/** Name of the CSC */
name: PropTypes.string,
/** Belonging group of the CSC */
group: PropTypes.string,
/** Index of the CSC */
salindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
data: PropTypes.object,
/** Function to execute when the CSC is clicked
* It will be called with an object containing the following keys:
* - group: The group of the CSC
* - csc: The name of the CSC
* - salindex: The index of the CSC
*/
onCSCClick: PropTypes.func,
/** Heartbeat stream */
heartbeatData: PropTypes.object,
/** Summary State stream */
summaryStateData: PropTypes.object,
/** Function to subscribe to streams */
subscribeToStreams: PropTypes.func,
/** Function to unsubscribe to streams */
unsubscribeToStreams: PropTypes.func,
/** Whereas to apply a minWidth */
embedded: PropTypes.bool,
/* Whether the component should subscribe to streams*/
shouldSubscribe: PropTypes.bool,
isRaw: PropTypes.bool,
/** Make heartbeat display optional */
hasHeartbeat: PropTypes.bool,
/** Whereas to show a warning icon */
withWarning: PropTypes.bool,
/** Server time object */
serverTime: PropTypes.object,
/** Simulation Mode stream */
simulationMode: PropTypes.object,
};

static defaultProps = {
name: '',
group: '',
data: {},
onCSCClick: () => 0,
heartbeatData: null,
summaryStateData: undefined,
onCSCClick: () => {},
subscribeToStreams: () => {},
unsubscribeToStreams: () => {},
embedded: false,
shouldSubscribe: true,
isRaw: false,
hasHeartbeat: true,
withWarning: false,
};

static states = {
Expand Down Expand Up @@ -95,27 +110,42 @@ export default class CSCDetail extends Component {
};

componentDidMount = () => {
this.props.subscribeToStreams(this.props.name, this.props.salindex);
const { name, salindex, subscribeToStreams } = this.props;
subscribeToStreams(name, salindex);
};

componentWillUnmount = () => {
this.props.unsubscribeToStreams(this.props.name, this.props.salindex);
const { name, salindex, unsubscribeToStreams } = this.props;
unsubscribeToStreams(name, salindex);
};

render() {
const { props } = this;
const {
group,
name,
salindex,
summaryStateData,
heartbeatData,
onCSCClick,
embedded,
hasHeartbeat,
withWarning,
serverTime,
simulationMode,
} = this.props;
let heartbeatStatus = 'unknown';
let nLost = 0;
let timeDiff = null;
let timeDiffText = 'No heartbeat from producer.';

if (this.props.heartbeatData) {
nLost = this.props.heartbeatData.lost;
if (this.props.heartbeatData.last_heartbeat_timestamp === -1) timeDiff = -1;
else timeDiff = Math.ceil(props.serverTime.tai * 1000 - this.props.heartbeatData.last_heartbeat_timestamp);
heartbeatStatus = this.props.heartbeatData.lost > 0 || timeDiff < 0 ? 'alert' : 'ok';
if (heartbeatData) {
nLost = heartbeatData.lost;
if (heartbeatData.last_heartbeat_timestamp === -1) timeDiff = -1;
else timeDiff = Math.ceil(serverTime.tai * 1000 - heartbeatData.last_heartbeat_timestamp);
heartbeatStatus = heartbeatData.lost > 0 || timeDiff < 0 ? 'alert' : 'ok';
}
if (props.hasHeartbeat === false) {

if (!hasHeartbeat) {
heartbeatStatus = 'ok';
}

Expand All @@ -125,48 +155,56 @@ export default class CSCDetail extends Component {
timeDiffText = timeDiff < 0 ? 'Stale' : `${timeDiff} seconds ago`;
}

let title = `${cscText(this.props.name, this.props.salindex)} heartbeat\nLost: ${nLost}\n`;

let title = `${cscText(name, salindex)} heartbeat\nLost: ${nLost}\n`;
if (timeDiff === null) {
title += `${timeDiffText}`;
} else {
title += timeDiff < 0 ? `Last seen: ${timeDiffText}` : `${timeDiffText}`;
}
const summaryStateValue = this.props.summaryStateData ? this.props.summaryStateData.summaryState.value : 0;

const summaryStateValue = summaryStateData ? summaryStateData.summaryState.value : 0;
const summaryState = CSCDetail.states[summaryStateValue];
let stateClass = heartbeatStatus === 'alert' ? styles.alert : summaryState.class;
if (heartbeatStatus === 'unknown') stateClass = CSCDetail.states[0].class;
if (summaryState.name === 'UNKNOWN') stateClass = CSCDetail.states[0].class;

const summaryStateIsUnknown = summaryState === 'UNKNOWN';
let stateClass = summaryState.class;
if (heartbeatStatus === 'alert' && !summaryStateIsUnknown) {
stateClass = styles.alert;
}
if (heartbeatStatus === 'unknown' && !summaryStateIsUnknown) {
stateClass = styles.unknown;
}

const heartbeatIsOk = heartbeatStatus === 'ok';
const isSimulated = simulationMode?.mode.value > 0;
return (
<div
onClick={() => this.props.onCSCClick({ group: props.group, csc: props.name, salindex: props.salindex })}
className={[styles.CSCDetailContainer, this.props.embedded ? styles.minWidth : ''].join(' ')}
onClick={() => onCSCClick({ group: group, csc: name, salindex: salindex })}
className={[
styles.CSCDetailContainer,
embedded ? styles.minWidth : '',
isSimulated ? styles.simulated : '',
].join(' ')}
>
<div className={[styles.summaryStateSection, summaryState.class].join(' ')}>
<span className={styles.summaryState} title={summaryState.userReadable}>
{summaryState.char}
</span>
</div>
<div className={[styles.heartbeatSection, stateClass].join(' ')}>
<div
className={[
styles.heartbeatIconWrapper,
heartbeatStatus === 'ok' && props.hasHeartbeat !== false ? styles.hidden : '',
].join(' ')}
>
<HeartbeatIcon
status={heartbeatStatus === 'alert' || props.hasHeartbeat === false ? 'unknown' : heartbeatStatus}
title={title}
/>
<div className={[styles.heartbeatIconWrapper, !hasHeartbeat || heartbeatIsOk ? styles.hidden : ''].join(' ')}>
<HeartbeatIcon status={heartbeatStatus} title={title} />
</div>
</div>

<div className={[styles.nameSection, stateClass].join(' ')} title={this.props.name + '.' + this.props.salindex}>
{cscText(this.props.name, this.props.salindex)}
<div
className={[styles.nameSection, stateClass].join(' ')}
title={name + '.' + salindex + (isSimulated ? ' (SIMULATED)' : '')}
>
{cscText(name, salindex)}
</div>

<div className={[styles.warningIconSection, stateClass].join(' ')}>
<div className={[styles.warningIconWrapper, props.withWarning !== true ? styles.hidden : ''].join(' ')}>
<div className={[styles.warningIconWrapper, !withWarning ? styles.hidden : ''].join(' ')}>
<WarningIcon title="warning" />
</div>
</div>
Expand Down
4 changes: 4 additions & 0 deletions love/src/components/CSCSummary/CSCDetail/CSCDetail.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,7 @@ this program. If not, see <http://www.gnu.org/licenses/>.
.hidden {
display: none;
}

.simulated {
border: 0.2em dashed var(--info-background-color);
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ const CSCExpandedContainer = ({
subscribeToStreams,
unsubscribeToStreams,
heartbeatData,
simulationMode,
displaySummaryState = true,
hideTitle = false,
}) => {
Expand All @@ -107,6 +108,7 @@ const CSCExpandedContainer = ({
logMessageData={logMessageData}
heartbeatData={heartbeatData}
clearCSCLogMessages={clearCSCLogMessages}
simulationMode={simulationMode}
displaySummaryState={displaySummaryState}
hideTitle={hideTitle}
/>
Expand All @@ -124,6 +126,7 @@ const mapDispatchToProps = (dispatch) => {
dispatch(addGroup(`event-${cscName}-${index}-softwareVersions`));
dispatch(addGroup(`event-${cscName}-${index}-configurationsAvailable`));
dispatch(addGroup(`event-${cscName}-${index}-configurationApplied`));
dispatch(addGroup(`event-${cscName}-${index}-simulationMode`));
},
unsubscribeToStreams: (cscName, index) => {
dispatch(removeGroup('event-Heartbeat-0-stream'));
Expand All @@ -134,6 +137,7 @@ const mapDispatchToProps = (dispatch) => {
dispatch(removeGroup(`event-${cscName}-${index}-softwareVersions`));
dispatch(removeGroup(`event-${cscName}-${index}-configurationsAvailable`));
dispatch(removeGroup(`event-${cscName}-${index}-configurationApplied`));
dispatch(removeGroup(`event-${cscName}-${index}-simulationMode`));
},
clearCSCLogMessages: (csc, salindex) => {
dispatch(removeCSCLogMessages(csc, salindex));
Expand All @@ -152,29 +156,30 @@ const mapDispatchToProps = (dispatch) => {
};

const mapStateToProps = (state, ownProps) => {
let summaryStateData = getStreamData(state, `event-${ownProps.name}-${ownProps.salindex}-summaryState`);
let heartbeatData = getCSCHeartbeat(state, ownProps.name, ownProps.salindex);
let softwareVersions = getStreamData(state, `event-${ownProps.name}-${ownProps.salindex}-softwareVersions`);
let configurationsAvailable = getStreamData(
const heartbeatData = getCSCHeartbeat(state, ownProps.name, ownProps.salindex);
const logMessageData = getCSCLogMessages(state, ownProps.name, ownProps.salindex);
const errorCodeData = getCSCErrorCodeData(state, ownProps.name, ownProps.salindex);

const summaryStateData = getStreamData(state, `event-${ownProps.name}-${ownProps.salindex}-summaryState`);
const softwareVersions = getStreamData(state, `event-${ownProps.name}-${ownProps.salindex}-softwareVersions`);
const configurationsAvailable = getStreamData(
state,
`event-${ownProps.name}-${ownProps.salindex}-configurationsAvailable`,
);
let configurationApplied = getStreamData(state, `event-${ownProps.name}-${ownProps.salindex}-configurationApplied`);
let cscLogLevelData = getStreamData(state, `event-${ownProps.name}-${ownProps.salindex}-logLevel`);

const logMessageData = getCSCLogMessages(state, ownProps.name, ownProps.salindex);
const errorCodeData = getCSCErrorCodeData(state, ownProps.name, ownProps.salindex);
summaryStateData = summaryStateData ? summaryStateData : {};
const configurationApplied = getStreamData(state, `event-${ownProps.name}-${ownProps.salindex}-configurationApplied`);
const cscLogLevelData = getStreamData(state, `event-${ownProps.name}-${ownProps.salindex}-logLevel`);
const simulationMode = getStreamData(state, `event-${ownProps.name}-${ownProps.salindex}-simulationMode`);

return {
summaryStateData: summaryStateData ? summaryStateData?.[0] : undefined,
softwareVersions: softwareVersions ? softwareVersions?.[0] : undefined,
configurationsAvailable: configurationsAvailable ? configurationsAvailable?.[0] : undefined,
configurationApplied: configurationApplied ? configurationApplied?.[0] : undefined,
cscLogLevelData: cscLogLevelData ? cscLogLevelData?.[0] : undefined,
heartbeatData,
logMessageData,
errorCodeData,
summaryStateData: summaryStateData?.[0],
softwareVersions: softwareVersions?.[0],
configurationsAvailable: configurationsAvailable?.[0],
configurationApplied: configurationApplied?.[0],
cscLogLevelData: cscLogLevelData?.[0],
simulationMode: simulationMode?.[0],
};
};

Expand Down
Loading
Loading