-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Client Count Calendar widget updates #13777
Changes from 17 commits
97174c6
61dbf73
1bcc475
8e1d1c2
7c3a556
ee59c95
ced1b71
9d839c2
9665132
4e232cc
f0a1dda
26c52b3
d4ef54f
b733242
9a9f8c5
8d16719
c88579d
d9b7b99
ab382b1
54d398e
43de131
ad592fb
de16cac
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,30 +1,56 @@ | ||
import Application from '../application'; | ||
import { zonedTimeToUtc } from 'date-fns-tz'; // https://github.com/marnusw/date-fns-tz#zonedtimetoutc | ||
|
||
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) { | ||
checkTimeType(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 start_time is a RFC3339 timestamp and if not convert to one. | ||
if (start_time.split(',').length > 1) { | ||
let utcDate = this.utcDate( | ||
new Date(Number(start_time.split(',')[1]), Number(start_time.split(',')[0] - 1)) | ||
); | ||
start_time = utcDate.toISOString(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is formatting it as ISO 8601 but above we call out RFC3339 -- We should format as RFC3339 for stability There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also by using |
||
} | ||
// look for end_time. If there is one check if it is a RFC3339 timestamp otherwise convert it. | ||
if (end_time) { | ||
let utcDateEnd = this.utcDate( | ||
new Date(Number(end_time.split(',')[1]), Number(end_time.split(',')[0])) | ||
); | ||
// ARG TODO !!!! SUPER IMPORTANT, with endDate you need to make it the last day of the month, right now it's the first!!! | ||
hashishaw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
end_time = utcDateEnd.toISOString(); | ||
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; | ||
} | ||
}, | ||
|
||
utcDate(dateObject) { | ||
// To remove the timezone of the local user (API returns and expects Zulu time/UTC) we need to use a method provided by date-fns-tz to return the UTC date | ||
let timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; // browser API method | ||
return zonedTimeToUtc(dateObject, timeZone); | ||
}, | ||
|
||
// ARG TODO current Month tab is hitting this endpoint. Need to amend so only hit on Monthly history (large payload) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. made a ticket for this comment. |
||
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.checkTimeType(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; | ||
} | ||
}, | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,27 +12,15 @@ import { tracked } from '@glimmer/tracking'; | |
* ```js | ||
* <CalendarWidget | ||
* @param {string} endTimeDisplay - The display value of the endTime. Ex: January 2022. | ||
* @param {string} endTimeFromResponse - Because endTimeDisplay is a tracked property of the parent it changes. But we need to keep a value unchanged that records the endTime returned by the counters/activity endpoint. This is that value. | ||
* @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 {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. | ||
* /> | ||
* | ||
* ``` | ||
*/ | ||
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 | ||
|
@@ -42,7 +30,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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. removed from calendar widget. This was a dropdown option we are no longer going to show in 1.10 |
||
|
||
// HELPER FUNCTIONS (alphabetically) // | ||
addClass(element, classString) { | ||
|
@@ -92,35 +79,36 @@ 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.split(' ')[1]) { | ||
let endMonth = this.args.endTimeFromResponse.split(' ')[0]; | ||
let endMonthIndex = this.args.arrayOfMonths.indexOf(endMonth); | ||
// 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 (endMonthIndex < 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All things singleMonth were removed because this is not something we're now doing in 1.10 |
||
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 | ||
|
@@ -134,13 +122,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); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,7 +2,8 @@ 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'; | ||
import { zonedTimeToUtc } from 'date-fns-tz'; // https://github.com/marnusw/date-fns-tz#zonedtimetoutc | ||
|
||
export default class Dashboard extends Component { | ||
arrayOfMonths = [ | ||
|
@@ -24,40 +25,61 @@ 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'); | ||
adapter = this.store.adapterFor('clients/activity'); | ||
|
||
// needed for startTime modal picker | ||
Monkeychip marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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: "3,2021" | ||
@tracked endTimeFromResponse = this.args.model.endTimeFromLicense; | ||
@tracked startMonth = null; | ||
@tracked startYear = null; | ||
@tracked selectedNamespace = null; | ||
// @tracked selectedNamespace = 'namespacelonglonglong4/'; // for testing namespace selection view | ||
|
||
// HELPER | ||
utcDate(dateObject) { | ||
// To remove the timezone of the local user (API returns and expects Zulu time/UTC) we need to use a method provided by date-fns-tz to return the UTC date | ||
let timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; // browser API method | ||
return zonedTimeToUtc(dateObject, timeZone); | ||
} | ||
|
||
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 = Number(this.startTimeFromResponse.split(',')[0]) - 1; | ||
let year = this.startTimeFromResponse.split(',')[1]; | ||
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 = Number(this.endTimeFromResponse.split(',')[0]) - 1; | ||
let year = this.endTimeFromResponse.split(',')[1]; | ||
return `${this.arrayOfMonths[month]} ${year}`; | ||
} | ||
|
||
get isDateRange() { | ||
return !isSameMonth(new Date(this.startTime), new Date(this.endTime)); | ||
return !isSameMonth( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just a note - this getter will change when my history/csv PR is merged |
||
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 | ||
|
@@ -92,38 +114,56 @@ export default class Dashboard extends Component { | |
return this.args.model.activity.responseTimestamp; | ||
} | ||
|
||
// 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 = `${monthIndex + 1},${year}`; // "1, 2021" | ||
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 = `${month},${year}`; // "1, 2021" | ||
} | ||
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; | ||
} | ||
|
||
this.startTimeFromResponse = response.formattedStartTime; | ||
this.endTimeFromResponse = response.formattedEndTime; | ||
if (this.startTimeRequested !== this.startTimeFromResponse) { | ||
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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -53,13 +53,20 @@ export default Route.extend(ClusterRoute, { | |
let activity = await this.getActivity(license.startTime); // returns client counts using license start_time. | ||
let monthly = await this.getMonthly(); // returns the partial month endpoint | ||
|
||
// all times to the component must be mm,yyyy ex: "1,2021" | ||
let endTimeFromLicense = `${activity.endTime.split('-')[1].replace(/^0+/, '')},${ | ||
activity.endTime.split('-')[0] | ||
}`; | ||
let startTimeFromLicense = `${license.startTime.split('-')[1].replace(/^0+/, '')},${ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This line and the one above are very similar, it might be worth splitting into another method. Then It might be easier to define what you expect from the license and what you expect to get back, and that makes it easier to test too! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We refactored this, should look a little more DRY now. |
||
license.startTime.split('-')[0] | ||
}`; | ||
return hash({ | ||
// ARG TODO will remove "hash" once remove "activity," which currently relies on it. | ||
activity, | ||
monthly, | ||
config, | ||
endTime: activity.endTime, | ||
startTime: license.startTime, | ||
endTimeFromLicense, | ||
startTimeFromLicense, | ||
}); | ||
}, | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: this method is so long that I think a name change here would be helpful to figure out what's going on. Something like
formatTimeParams
is indicative of the type of response one should expect, and what's happening within the code.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea and good name. Amended.