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

Client Count Calendar widget updates #13777

Merged
merged 23 commits into from
Feb 1, 2022
Merged
Show file tree
Hide file tree
Changes from 17 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
64 changes: 45 additions & 19 deletions ui/app/adapters/clients/activity.js
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) {
Copy link
Contributor

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.

Copy link
Contributor Author

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.

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();
Copy link
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also by using formatRFC3339(new Date(2021, 9)) we can use the 0-indexed month

}
// 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)
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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;
}
},
});
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;
});
},
});
}
}
55 changes: 18 additions & 37 deletions ui/app/components/calendar-widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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) {
Expand Down Expand Up @@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Expand All @@ -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);
94 changes: 67 additions & 27 deletions ui/app/components/clients/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand All @@ -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(
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Expand Down Expand Up @@ -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
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('string') formattedEndTime;
@attr('string') 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
11 changes: 9 additions & 2 deletions ui/app/routes/vault/cluster/clients/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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+/, '')},${
Copy link
Contributor

Choose a reason for hiding this comment

The 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!

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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,
});
},

Expand Down
Loading