Skip to content

Commit

Permalink
Client Count Calendar widget updates (#13777)
Browse files Browse the repository at this point in the history
* setup

* handle current billing period

* handle billing period selection

* clean up

* clean up

* turn serializer to class

* change to classes

* placeholding, handles timezone issues for this.startTime

* put in depen

* fixing timezone issues for endTime

* clean up

* move formating on Get to the adapter. Still need to return formating from Get on serializer

* fix current billing period

* move all inside queryRecord to hit serilaizer

* move to serializer

* clean up

* calendar clean up

* clean up

* fix styling

* small fixes

* small fixes

Co-authored-by: Claire Bontempo <[email protected]>
  • Loading branch information
Monkeychip and hellobontempo authored Feb 1, 2022
1 parent 5f5012c commit 057c67f
Show file tree
Hide file tree
Showing 15 changed files with 203 additions and 174 deletions.
61 changes: 42 additions & 19 deletions ui/app/adapters/clients/activity.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,53 @@
import Application from '../application';
import { formatRFC3339 } from 'date-fns';

export default Application.extend({
queryRecord(store, type, query) {
let url = `${this.buildURL()}/internal/counters/activity`;
// Query has startTime defined. The API will return the endTime if none is provided.
return this.ajax(url, 'GET', { data: query }).then((resp) => {
let response = resp || {};
// if the response is a 204 it has no request id (ARG TODO test that it returns a 204)
response.id = response.request_id || 'no-data';
return response;
});
},
// called from components
queryClientActivity(start_time, end_time) {
formatTimeParams(query) {
let { start_time, end_time } = query;
// do not query without start_time. Otherwise returns last year data, which is not reflective of billing data.
if (start_time) {
let url = `${this.buildURL()}/internal/counters/activity`;
let queryParams = {};
if (!end_time) {
queryParams = { data: { start_time } };
// check if it's an array, if it is, it's coming from an action like selecting a new startTime or new EndTime
if (Array.isArray(start_time)) {
let startYear = Number(start_time[0]);
let startMonth = Number(start_time[1]);
start_time = formatRFC3339(new Date(startYear, startMonth));
}
if (end_time) {
if (Array.isArray(end_time)) {
let endYear = Number(end_time[0]);
let endMonth = Number(end_time[1]);
end_time = formatRFC3339(new Date(endYear, endMonth));
}

return { start_time, end_time };
} else {
queryParams = { data: { start_time, end_time } };
return { start_time };
}
return this.ajax(url, 'GET', queryParams).then((resp) => {
return resp;
} else {
// did not have a start time, return null through to component.
return null;
}
},

// ARG TODO current Month tab is hitting this endpoint. Need to amend so only hit on Monthly history (large payload)
// query comes in as either: {start_time: '2021-03-17T00:00:00Z'} or
// {start_time: Array(2), end_time: Array(2)}
// end_time: (2) ['2022', 0]
// start_time: (2) ['2021', 2]
queryRecord(store, type, query) {
let url = `${this.buildURL()}/internal/counters/activity`;
// check if start and/or end times are in RFC3395 format, if not convert with timezone UTC/zulu.
let queryParams = this.formatTimeParams(query);
if (queryParams) {
return this.ajax(url, 'GET', { data: queryParams }).then((resp) => {
let response = resp || {};
// if the response is a 204 it has no request id (ARG TODO test that it returns a 204)
response.id = response.request_id || 'no-data';
return response;
});
} else {
// did not have a start time, return null through to component.
return null;
}
},
});
8 changes: 4 additions & 4 deletions ui/app/adapters/clients/monthly.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Application from '../application';
import ApplicationAdapter from '../application';

export default Application.extend({
export default class MonthlyAdapter extends ApplicationAdapter {
queryRecord() {
let url = `${this.buildURL()}/internal/counters/activity/monthly`;
// Query has startTime defined. The API will return the endTime if none is provided.
Expand All @@ -10,5 +10,5 @@ export default Application.extend({
response.id = response.request_id || 'no-data';
return response;
});
},
});
}
}
62 changes: 22 additions & 40 deletions ui/app/components/calendar-widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,17 @@ import { tracked } from '@glimmer/tracking';
* @example
* ```js
* <CalendarWidget
* @param {string} endTimeDisplay - The display value of the endTime. Ex: January 2022.
* @param {string} startTimeDisplay - The display value of startTime that the parent manages. This component is only responsible for modifying the endTime which is sends to the parent to make the network request.
* @param {array} arrayOfMonths - An array of all the months that the calendar widget iterates through.
* @param {string} endTimeDisplay - The formatted display value of the endTime. Ex: January 2022.
* @param {string} endTimeFromResponse - The value returned on the counters/activity endpoint, which shows the true endTime not the selected one, which can be different.
* @param {function} handleClientActivityQuery - a function passed from parent. This component sends the month and year to the parent via this method which then calculates the new data.
* @param {function} handleCurrentBillingPeriod - a function passed from parent. This component makes the parent aware that the user selected Current billing period and it handles resetting the data.
* @param {string} startTimeDisplay - The formatted display value of the endTime. Ex: January 2022. This component is only responsible for modifying the endTime which is sends to the parent to make the network request.
* />
*
* ```
*/
class CalendarWidget extends Component {
arrayOfMonths = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
];
currentDate = new Date();
currentYear = this.currentDate.getFullYear(); // integer
currentMonth = parseInt(this.currentDate.getMonth()); // integer and zero index
Expand All @@ -42,7 +31,6 @@ class CalendarWidget extends Component {
@tracked disablePastYear = this.isObsoleteYear(); // if obsolete year, disable left chevron
@tracked disableFutureYear = this.isCurrentYear(); // if current year, disable right chevron
@tracked showCalendar = false;
@tracked showSingleMonth = false;

// HELPER FUNCTIONS (alphabetically) //
addClass(element, classString) {
Expand All @@ -54,7 +42,7 @@ class CalendarWidget extends Component {
}

isObsoleteYear() {
// do not allow them to choose a end year before the this.args.startTimeDisplay
// do not allow them to choose a year before the this.args.startTimeDisplay
let startYear = this.args.startTimeDisplay.split(' ')[1];
return this.displayYear.toString() === startYear; // if on startYear then don't let them click back to the year prior
}
Expand All @@ -81,6 +69,7 @@ class CalendarWidget extends Component {

let elementMonthId = parseInt(e.id.split('-')[0]); // dependent on the shape of the element id
// for current year

if (this.currentMonth <= elementMonthId) {
// only disable months when current year is selected
if (this.isCurrentYear()) {
Expand All @@ -92,35 +81,35 @@ class CalendarWidget extends Component {
// if they are on the view where the start year equals the display year, check which months should not show.
let startMonth = this.args.startTimeDisplay.split(' ')[0]; // returns month name e.g. January
// return the index of the startMonth
let startMonthIndex = this.arrayOfMonths.indexOf(startMonth);
let startMonthIndex = this.args.arrayOfMonths.indexOf(startMonth);
// then add readOnly class to any month less than the startMonth index.
if (startMonthIndex > elementMonthId) {
e.classList.add('is-readOnly');
}
}
// Compare values so the user cannot select an endTime after the endTime returned from counters/activity response on page load.
// ARG TODO will need to test if no data is returned on page load.
if (this.displayYear.toString() === this.args.endTimeFromResponse[0]) {
let endMonth = this.args.endTimeFromResponse[1];
// add readOnly class to any month that is older (higher) than the endMonth index. (e.g. if nov is the endMonth of the endTimeDisplay, then 11 and 12 should not be displayed 10 < 11 and 10 < 12.)
if (endMonth < elementMonthId) {
e.classList.add('is-readOnly');
}
}
});
}

@action
selectCurrentBillingPeriod() {
// ARG TOOD send to dashboard the select current billing period. The parent may know this it's just a boolean.
// Turn the calendars off if they are showing.
selectCurrentBillingPeriod(D) {
this.args.handleCurrentBillingPeriod(); // resets the billing startTime and endTime to what it is on init via the parent.
this.showCalendar = false;
this.showSingleMonth = false;
D.actions.close(); // close the dropdown.
}
@action
selectEndMonth(month, year, element) {
this.addClass(element.target, 'is-selected');
selectEndMonth(month, year, D) {
this.toggleShowCalendar();
this.args.handleClientActivityQuery(month, year, 'endTime');
}

@action
selectSingleMonth(month, year, element) {
// select month
this.addClass(element.target, 'is-selected');
this.toggleSingleMonth();
// ARG TODO similar to selectEndMonth
D.actions.close(); // close the dropdown.
}

@action
Expand All @@ -134,13 +123,6 @@ class CalendarWidget extends Component {
@action
toggleShowCalendar() {
this.showCalendar = !this.showCalendar;
this.showSingleMonth = false;
}

@action
toggleSingleMonth() {
this.showSingleMonth = !this.showSingleMonth;
this.showCalendar = false;
}
}
export default setComponentTemplate(layout, CalendarWidget);
103 changes: 76 additions & 27 deletions ui/app/components/clients/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Component from '@glimmer/component';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { format, formatRFC3339, isSameMonth } from 'date-fns';
import { isSameMonth } from 'date-fns';

export default class Dashboard extends Component {
arrayOfMonths = [
Expand All @@ -24,40 +24,53 @@ export default class Dashboard extends Component {
{ key: 'entity_clients', label: 'entity clients' },
{ key: 'non_entity_clients', label: 'non-entity clients' },
];
// TODO remove this adapter variable? or set to /clients/activity ?
adapter = this.store.adapterFor('clients/new-init-activity');

// needed for startTime modal picker
months = Array.from({ length: 12 }, (item, i) => {
return new Date(0, i).toLocaleString('en-US', { month: 'long' });
});
years = Array.from({ length: 5 }, (item, i) => {
return new Date().getFullYear() - i;
});

@service store;

@tracked barChartSelection = false;
@tracked isEditStartMonthOpen = false;
@tracked startTime = this.args.model.startTime;
@tracked endTime = this.args.model.endTime;
@tracked responseRangeDiffMessage = null;
@tracked startTimeRequested = null;
@tracked startTimeFromResponse = this.args.model.startTimeFromLicense; // ex: ['2021', 3] is April 2021 (0 indexed)
@tracked endTimeFromResponse = this.args.model.endTimeFromLicense;
@tracked startMonth = null;
@tracked startYear = null;
@tracked selectedNamespace = null;
// @tracked selectedNamespace = 'namespacelonglonglong4/'; // for testing namespace selection view

get startTimeDisplay() {
if (!this.startTime) {
if (!this.startTimeFromResponse) {
// otherwise will return date of new Date(null)
return null;
}
let formattedAsDate = new Date(this.startTime); // on init it's formatted as a Date object, but when modified by modal it's formatted as RFC3339
return format(formattedAsDate, 'MMMM yyyy');
let month = this.startTimeFromResponse[1];
let year = this.startTimeFromResponse[0];
return `${this.arrayOfMonths[month]} ${year}`;
}

get endTimeDisplay() {
if (!this.endTime) {
if (!this.endTimeFromResponse) {
// otherwise will return date of new Date(null)
return null;
}
let formattedAsDate = new Date(this.endTime);
return format(formattedAsDate, 'MMMM yyyy');
let month = this.endTimeFromResponse[1];
let year = this.endTimeFromResponse[0];
return `${this.arrayOfMonths[month]} ${year}`;
}

get isDateRange() {
return !isSameMonth(new Date(this.startTime), new Date(this.endTime));
return !isSameMonth(
new Date(this.args.model.activity.startTime),
new Date(this.args.model.activity.endTime)
);
}

// Determine if we have client count data based on the current tab
Expand Down Expand Up @@ -91,39 +104,75 @@ export default class Dashboard extends Component {
}
return this.args.model.activity.responseTimestamp;
}
// HELPERS
areArraysTheSame(a1, a2) {
return (
a1 === a2 ||
(a1 !== null &&
a2 !== null &&
a1.length === a2.length &&
a1
.map(function (val, idx) {
return val === a2[idx];
})
.reduce(function (prev, cur) {
return prev && cur;
}, true))
);
}

// ACTIONS
@action
async handleClientActivityQuery(month, year, dateType) {
if (dateType === 'cancel') {
return;
}
// dateType is either startTime or endTime
let monthIndex = this.arrayOfMonths.indexOf(month);
// clicked "Current Billing period" in the calendar widget
if (dateType === 'reset') {
this.startTimeRequested = this.args.model.startTimeFromLicense;
this.endTimeRequested = null;
}
// clicked "Edit" Billing start month in Dashboard which opens a modal.
if (dateType === 'startTime') {
this.startTime = formatRFC3339(new Date(year, monthIndex));
this.endTime = null;
let monthIndex = this.arrayOfMonths.indexOf(month);
this.startTimeRequested = [year.toString(), monthIndex]; // ['2021', 0] (e.g. January 2021) // TODO CHANGE TO ARRAY
this.endTimeRequested = null;
}
// clicked "Custom End Month" from the calendar-widget
if (dateType === 'endTime') {
// this month comes in as an index
this.endTime = formatRFC3339(new Date(year, month));
// use the currently selected startTime for your startTimeRequested.
this.startTimeRequested = this.startTimeFromResponse;
this.endTimeRequested = [year.toString(), month]; // endTime comes in as a number/index whereas startTime comes in as a month name. Hence the difference between monthIndex and month.
}
try {
let response = await this.adapter.queryClientActivity(this.startTime, this.endTime);
// resets the endTime to what is returned on the response
this.endTime = response.data.end_time;

try {
let response = await this.store.queryRecord('clients/activity', {
start_time: this.startTimeRequested,
end_time: this.endTimeRequested,
});
if (!response) {
// this.endTime will be null and use this to show EmptyState message on the template.
return;
}
// note: this.startTimeDisplay (at getter) is updated by this.startTimeFromResponse
this.startTimeFromResponse = response.formattedStartTime;
this.endTimeFromResponse = response.formattedEndTime;
// compare if the response and what you requested are the same. If they are not throw a warning.
// this only gets triggered if the data was returned, which does not happen if the user selects a startTime after for which we have data. That's an adapter error and is captured differently.
if (!this.areArraysTheSame(this.startTimeFromResponse, this.startTimeRequested)) {
this.responseRangeDiffMessage = `You requested data from ${month} ${year}. We only have data from ${this.startTimeDisplay}, and that is what is being shown here.`;
} else {
this.responseRangeDiffMessage = null;
}
return response;
// ARG TODO this is the response you need to use to repopulate the chart data
} catch (e) {
// ARG TODO handle error
}
}

// ARG TODO this might be a carry over from history, will need to confirm
@action
resetData() {
this.barChartSelection = false;
this.selectedNamespace = null;
handleCurrentBillingPeriod() {
this.handleClientActivityQuery(0, 0, 'reset');
}

@action
Expand Down
2 changes: 2 additions & 0 deletions ui/app/models/clients/activity.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ export default class Activity extends Model {
@attr('string') responseTimestamp;
@attr('array') byNamespace;
@attr('string') endTime;
@attr('array') formattedEndTime;
@attr('array') formattedStartTime;
@attr('string') startTime;
@attr('object') total;
}
3 changes: 1 addition & 2 deletions ui/app/models/clients/monthly.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import Model, { attr } from '@ember-data/model';
// ARG TODO copied from before, modify for what you need
export default class Monthly extends Model {
export default class MonthlyModel extends Model {
@attr('string') responseTimestamp;
@attr('array') byNamespace;
@attr('object') total;
Expand Down
Loading

0 comments on commit 057c67f

Please sign in to comment.