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
38 changes: 29 additions & 9 deletions app/components/chart/daily.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import CgmSampleIntervalRangeToggle from './cgmSampleIntervalRangeToggle';
import EventsInfoLabel from './eventsInfoLabel';
import { DEFAULT_CGM_SAMPLE_INTERVAL_RANGE } from '../../core/constants';

const DailyChart = withTranslation(null, { withRef: true })(class DailyChart extends Component {
export class DailyChart extends Component {
static propTypes = {
bgClasses: PropTypes.object.isRequired,
bgUnits: PropTypes.string.isRequired,
Expand All @@ -59,6 +59,7 @@ const DailyChart = withTranslation(null, { withRef: true })(class DailyChart ext
editedCarbs: PropTypes.bool,
initialDatetimeLocation: PropTypes.string,
patient: PropTypes.object,
siteChangeSource: PropTypes.string,
timePrefs: PropTypes.object.isRequired,
// message handlers
onCreateMessage: PropTypes.func.isRequired,
Expand Down Expand Up @@ -96,6 +97,7 @@ const DailyChart = withTranslation(null, { withRef: true })(class DailyChart ext
'dynamicCarbs',
'editedCarbs',
'insulinBolus',
'siteChangeSource',
'timePrefs',
'onBolusHover',
'onBolusOut',
Expand Down Expand Up @@ -230,7 +232,9 @@ const DailyChart = withTranslation(null, { withRef: true })(class DailyChart ext
editMessage = message => {
return this.chart?.editMessage(message);
};
});
}

const TranslatedDailyChart = withTranslation(null, { withRef: true })(DailyChart);

class Daily extends Component {
static propTypes = {
Expand All @@ -239,6 +243,7 @@ class Daily extends Component {
data: PropTypes.object.isRequired,
initialDatetimeLocation: PropTypes.string,
loading: PropTypes.bool.isRequired,
siteChangeSource: PropTypes.string,
mostRecentDatetimeLocation: PropTypes.string,
queryDataCount: PropTypes.number.isRequired,
stats: PropTypes.array.isRequired,
Expand Down Expand Up @@ -282,6 +287,7 @@ class Daily extends Component {
atMostRecent: false,
endpoints: [],
hasAlarmEventsInView: null,
hasSiteChangeEventsInView: null,
initialDatetimeLocation: this.props.initialDatetimeLocation,
inTransition: false,
title: '',
Expand All @@ -307,15 +313,25 @@ class Daily extends Component {
if (!_.isEmpty(updates)) this.chartRef.current?.rerenderChart(updates);
}

if (nextProps.data?.data?.combined && (this.state.hasAlarmEventsInView === null || newEndpointsReceived)) {
const eventFlagsUnset = this.state.hasAlarmEventsInView === null || this.state.hasSiteChangeEventsInView === null;

if (nextProps.data?.data?.combined && (eventFlagsUnset || newEndpointsReceived)) {
const isInView = d => d.normalTime >= nextProps.data.data.current.endpoints.range[0] && d.normalTime <= nextProps.data.data.current.endpoints.range[1];

const hasAlarmEventsInView = _.some(
_.filter(nextProps.data.data.combined, d => !!d.tags?.alarm),
d => d.normalTime >= nextProps.data.data.current.endpoints.range[0] && d.normalTime <= nextProps.data.data.current.endpoints.range[1]
isInView
);

Comment on lines 321 to 325
if (hasAlarmEventsInView !== this.state.hasAlarmEventsInView) {
this.setState({ hasAlarmEventsInView });
}
const hasSiteChangeEventsInView = _.some(
_.filter(nextProps.data.data.combined, d => !!d.tags?.siteChange),
isInView
);

const eventStateUpdates = {};
if (hasAlarmEventsInView !== this.state.hasAlarmEventsInView) eventStateUpdates.hasAlarmEventsInView = hasAlarmEventsInView;
if (hasSiteChangeEventsInView !== this.state.hasSiteChangeEventsInView) eventStateUpdates.hasSiteChangeEventsInView = hasSiteChangeEventsInView;
if (!_.isEmpty(eventStateUpdates)) this.setState(eventStateUpdates);
}
};

Expand Down Expand Up @@ -522,7 +538,10 @@ class Daily extends Component {
zIndex: 1,
}}
>
<EventsInfoLabel hasAlarmEventsInView={this.state.hasAlarmEventsInView} />
<EventsInfoLabel
hasAlarmEventsInView={this.state.hasAlarmEventsInView}
hasSiteChangeEventsInView={this.state.hasSiteChangeEventsInView}
/>

{/* TODO: re-enable CgmSampleIntervalRangeToggle once twiist data issue is resolved */}
{/* {showingCgmData && hasOneMinCgmSampleIntervalDevice && (
Expand All @@ -535,7 +554,7 @@ class Daily extends Component {
</Flex>

<Box sx={{ position: 'relative', top: '-24px' }}>
<DailyChart
<TranslatedDailyChart
automatedBasal={isAutomatedBasalDevice}
automatedBolus={isAutomatedBolusDevice}
insulinBolus={hasInsulinData}
Expand All @@ -547,6 +566,7 @@ class Daily extends Component {
dynamicCarbs={this.props.chartPrefs.dynamicCarbs}
editedCarbs={hasEditedCarbs}
initialDatetimeLocation={this.props.initialDatetimeLocation}
siteChangeSource={this.props.siteChangeSource}
timePrefs={timePrefs}
// message handlers
onCreateMessage={this.props.onCreateMessage}
Expand Down
4 changes: 2 additions & 2 deletions app/components/chart/eventsInfoLabel.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Body1 } from '../elements/FontStyles';
const { EventsInfoTooltip } = vizComponents;

const EventsInfoLabel = props => {
const { hasAlarmEventsInView } = props;
const { hasAlarmEventsInView, hasSiteChangeEventsInView } = props;
const { t } = useTranslation();
const [showTooltip, setShowTooltip] = React.useState(false);

Expand All @@ -30,7 +30,7 @@ const EventsInfoLabel = props => {
<Body1 sx={{ color: 'stat.text', fontWeight: 'bold' }}>{t('Events')}</Body1>
</Box>

{hasAlarmEventsInView && (
{(hasAlarmEventsInView || hasSiteChangeEventsInView) && (
<Flex className='events-label-tooltip' sx={{ position: 'relative', alignItems: 'center' }}>
<Icon
icon={InfoOutlinedIcon}
Expand Down
1 change: 1 addition & 0 deletions app/pages/patientdata/patientdata.js
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,7 @@ export const PatientDataClass = createReactClass({
onUpdateChartDateRange={this.handleChartDateRangeUpdate}
onClickChartDates={this.handleClickChartDates}
patient={this.props.patient}
siteChangeSource={vizUtils.aggregation.getSiteChangeSource(this.props.patient, this.getMetaData('latestPumpUpload.manufacturer', ''))}
stats={stats}
trackMetric={this.props.trackMetric}
updateChartPrefs={this.updateChartPrefs}
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"node": "24.15.0"
},
"packageManager": "yarn@3.6.4",
"version": "1.97.0-web-3359.3",
"version": "1.98.0-web-55-daily-view-site-change-events.1",
"private": true,
"scripts": {
"test": "TZ=UTC NODE_ENV=test yarn jest --verbose",
Expand Down Expand Up @@ -72,7 +72,7 @@
"@testing-library/react": "12.1.5",
"@testing-library/react-hooks": "8.0.1",
"@testing-library/user-event": "14.6.1",
"@tidepool/viz": "1.57.0-web-3359.4",
"@tidepool/viz": "1.58.0-web-55-daily-view-site-change-events.2",
"@types/jest": "29.5.14",
"async": "2.6.4",
"autoprefixer": "10.4.16",
Expand Down Expand Up @@ -182,7 +182,7 @@
"terser": "5.22.0",
"terser-webpack-plugin": "5.3.9",
"theme-ui": "0.16.1",
"tideline": "1.40.0-web-3359.4",
"tideline": "1.41.0-web-55-daily-view-site-change-events.1",
"tidepool-platform-client": "0.66.0",
"tidepool-standard-action": "0.1.1",
"ua-parser-js": "1.0.36",
Expand Down
149 changes: 148 additions & 1 deletion test/unit/components/chart/daily.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ var expect = chai.expect;
import { cleanup, fireEvent, render, waitFor } from '@testing-library/react';

import i18next from '../../../../app/core/language';
import Daily from '../../../../app/components/chart/daily';
import Daily, { DailyChart } from '../../../../app/components/chart/daily';
import EventsInfoLabel from '../../../../app/components/chart/eventsInfoLabel';
import { DEFAULT_CGM_SAMPLE_INTERVAL_RANGE, MGDL_UNITS, ONE_MINUTE_CGM_SAMPLE_INTERVAL_RANGE } from '../../../../app/core/constants';
import { components as vizComponents } from '@tidepool/viz';

Expand Down Expand Up @@ -309,6 +310,66 @@ describe('Daily', () => {
await waitFor(() => expect(tooltip()).to.equal(0));
});

it('should render the Events pool label info tooltip if there are site change events in view, even without alarms', async () => {
const { container, rerender } = render(<DailyClass {...baseProps} />);
const tooltip = () => container.querySelectorAll('.events-label-tooltip').length;

var dayDataReadyProps = _.assign({}, baseProps, {
loading: false,
data: {
query: { chartType: 'daily'},
bgPrefs,
timePrefs: {
timezoneAware: false,
timezoneName: 'US/Pacific',
},
data: {
combined: [{ type: 'cbg', normalTime: new Date('2018-01-15T12:00:00.000Z').valueOf() }],
current: { endpoints: { range: [new Date('2018-01-15T00:00:00.000Z').valueOf(), new Date('2018-01-16T00:00:00.000Z').valueOf()] } },
},
},
});

rerender(<DailyClass {...dayDataReadyProps} />);
expect(tooltip()).to.equal(0);

// Set data with a site change event in view (no alarm present)
rerender(<DailyClass {...{
...dayDataReadyProps,
data: {
query: { chartType: 'daily'},
bgPrefs,
timePrefs: {
timezoneAware: false,
timezoneName: 'US/Pacific',
},
data: {
combined: [{ tags: { siteChange: true }, normalTime: new Date('2018-01-15T12:00:00.000Z').valueOf() }],
current: { endpoints: { range: [new Date('2018-01-15T00:00:00.000Z').valueOf(), new Date('2018-01-16T00:00:00.000Z').valueOf()] } },
},
},
}} />);
await waitFor(() => expect(tooltip()).to.equal(1));

// Move endpoints so that the site change event is out of view
rerender(<DailyClass {...{
...dayDataReadyProps,
data: {
query: { chartType: 'daily'},
bgPrefs,
timePrefs: {
timezoneAware: false,
timezoneName: 'US/Pacific',
},
data: {
combined: [{ tags: { siteChange: true }, normalTime: new Date('2018-01-15T12:00:00.000Z').valueOf() }],
current: { endpoints: { range: [new Date('2018-01-16T00:00:00.000Z').valueOf(), new Date('2018-01-17T00:00:00.000Z').valueOf()] } },
},
},
}} />);
await waitFor(() => expect(tooltip()).to.equal(0));
});

it('should not render the cgm interval toggle, even if there is a current supporting device', () => {
const { container, rerender } = render(<DailyClass {...baseProps} />);
const toggle = () => container.querySelectorAll('.cgm-sample-interval-range-toggle').length;
Expand Down Expand Up @@ -717,4 +778,90 @@ describe('Daily', () => {
expect(instance.state.hoveredEvent).to.be.false;
});
});

describe('siteChangeSource threading', () => {
const dayDataReadyProps = _.assign({}, baseProps, {
loading: false,
data: {
query: { chartType: 'daily' },
bgPrefs,
timePrefs: { timezoneAware: false, timezoneName: 'US/Pacific' },
data: {
combined: [{ type: 'cbg', normalTime: new Date('2018-01-15T12:00:00.000Z').valueOf() }],
current: { endpoints: { range: [new Date('2018-01-15T00:00:00.000Z').valueOf(), new Date('2018-01-16T00:00:00.000Z').valueOf()] } },
},
},
});

const findDailyChart = component => {
const queue = [component.render()];

while (queue.length) {
const node = queue.shift();
if (!node || !node.props) continue;
if (node.props.onEventHover) return node;
queue.push(...React.Children.toArray(node.props.children));
}

return null;
};

it('includes siteChangeSource in the chart options picked for the tideline factory', () => {
const chartInstance = new DailyChart({ initialDatetimeLocation: baseProps.initialDateTimeLocation });

expect(chartInstance.chartOpts).to.include('siteChangeSource');
});

it('threads the siteChangeSource prop down to the DailyChart when a siteChangeSource prop exists', () => {
const props = _.assign({}, dayDataReadyProps, { siteChangeSource: 'cannulaPrime' });
const dailyChart = findDailyChart(createInstance(props));

expect(dailyChart.props.siteChangeSource).to.equal('cannulaPrime');
});

it('threads an undefined siteChangeSource when no siteChangeSource prop has been provided', () => {
const dailyChart = findDailyChart(createInstance(dayDataReadyProps));

expect(dailyChart.props.siteChangeSource).to.be.undefined;
});
});

describe('hasSiteChangeEventsInView', () => {
const withSiteChangeInRange = range => _.assign({}, baseProps, {
loading: false,
data: {
query: { chartType: 'daily' },
bgPrefs,
timePrefs: { timezoneAware: false, timezoneName: 'US/Pacific' },
data: {
combined: [{ tags: { siteChange: true }, normalTime: new Date('2018-01-15T12:00:00.000Z').valueOf() }],
current: { endpoints: { range } },
},
},
});

const inViewRange = [new Date('2018-01-15T00:00:00.000Z').valueOf(), new Date('2018-01-16T00:00:00.000Z').valueOf()];
const outOfViewRange = [new Date('2018-01-16T00:00:00.000Z').valueOf(), new Date('2018-01-17T00:00:00.000Z').valueOf()];

it('computes true when a site-change event falls within the current endpoints range', () => {
const dailyInstance = createInstance(baseProps);
dailyInstance.UNSAFE_componentWillReceiveProps(withSiteChangeInRange(inViewRange));
expect(dailyInstance.state.hasSiteChangeEventsInView).to.be.true;
});

it('computes false when the site-change event is outside the current endpoints range', () => {
const dailyInstance = createInstance(baseProps);
dailyInstance.UNSAFE_componentWillReceiveProps(withSiteChangeInRange(outOfViewRange));
expect(dailyInstance.state.hasSiteChangeEventsInView).to.be.false;
});

it('passes hasSiteChangeEventsInView through to EventsInfoLabel', () => {
const dailyInstance = createInstance(withSiteChangeInRange(inViewRange));
dailyInstance.UNSAFE_componentWillReceiveProps(dailyInstance.props);

const label = getNodeByType(dailyInstance, EventsInfoLabel);
expect(label).to.exist;
expect(label.props.hasSiteChangeEventsInView).to.equal(dailyInstance.state.hasSiteChangeEventsInView);
});
});
});
40 changes: 40 additions & 0 deletions test/unit/components/chart/eventsInfoLabel.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/* global chai */
/* global describe */
/* global it */
/* global afterEach */

import React from 'react';
import { cleanup, render } from '@testing-library/react';

import '../../../../app/core/language';
import EventsInfoLabel from '../../../../app/components/chart/eventsInfoLabel';

const expect = chai.expect;

describe('EventsInfoLabel', () => {
afterEach(() => {
cleanup();
});

const infoTooltip = container => container.querySelectorAll('.events-label-tooltip').length;

it('always renders the Events label text', () => {
const { container } = render(<EventsInfoLabel />);
expect(container.querySelector('.events-label-container').textContent).to.equal('Events');
});

it('renders the info tooltip when only site-change events are in view', () => {
const { container } = render(<EventsInfoLabel hasAlarmEventsInView={false} hasSiteChangeEventsInView={true} />);
expect(infoTooltip(container)).to.equal(1);
});

it('renders the info tooltip when only alarm events are in view', () => {
const { container } = render(<EventsInfoLabel hasAlarmEventsInView={true} hasSiteChangeEventsInView={false} />);
expect(infoTooltip(container)).to.equal(1);
});

it('renders no info tooltip when neither alarms nor site changes are in view', () => {
const { container } = render(<EventsInfoLabel hasAlarmEventsInView={false} hasSiteChangeEventsInView={false} />);
expect(infoTooltip(container)).to.equal(0);
});
});
Loading