Client Count Calendar widget updates (#13777)

* 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 <cbontempo@hashicorp.com>
This commit is contained in:
Angel Garbarino 2022-02-01 13:45:01 -07:00 committed by GitHub
parent 5032cfaf47
commit 53aae016f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 204 additions and 175 deletions

View File

@ -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 } };
} else {
queryParams = { data: { start_time, end_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));
}
return this.ajax(url, 'GET', queryParams).then((resp) => {
return resp;
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 {
return { start_time };
}
} 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;
}
},
});

View File

@ -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.
@ -10,5 +10,5 @@ export default Application.extend({
response.id = response.request_id || 'no-data';
return response;
});
},
});
}
}

View File

@ -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
@ -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) {
@ -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
}
@ -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()) {
@ -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
@ -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);

View File

@ -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 = [
@ -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
@ -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

View File

@ -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;
}

View File

@ -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;

View File

@ -42,6 +42,11 @@ export default Route.extend(ClusterRoute, {
}
},
rfc33395ToMonthYear(timestamp) {
// return [2021, 04 (e.g. 2021 March, make 0-indexed)
return [timestamp.split('-')[0], Number(timestamp.split('-')[1].replace(/^0+/, '')) - 1];
},
async model() {
let config = this.store.queryRecord('clients/config', {}).catch((e) => {
console.debug(e);
@ -52,14 +57,16 @@ export default Route.extend(ClusterRoute, {
let license = await this.getLicense(); // get default start_time
let activity = await this.getActivity(license.startTime); // returns client counts using license start_time.
let monthly = await this.getMonthly(); // returns the partial month endpoint
let endTimeFromLicense = this.rfc33395ToMonthYear(activity.endTime);
let startTimeFromLicense = this.rfc33395ToMonthYear(license.startTime);
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,
});
},

View File

@ -46,7 +46,7 @@ transformedPayload.by_namespace = [
]
*/
export default ApplicationSerializer.extend({
export default class ActivitySerializer extends ApplicationSerializer {
flattenDataset(payload) {
let topTen = payload.slice(0, 10);
@ -79,7 +79,7 @@ export default ApplicationSerializer.extend({
...flattenedNs,
};
});
},
}
// TODO CMB remove and use abstracted function above
// prior to 1.10, client count key names are "distinct_entities" and "non_entity_tokens" so mapping below wouldn't work
@ -107,7 +107,12 @@ export default ApplicationSerializer.extend({
mounts: namespaceMounts,
};
});
},
}
rfc33395ToMonthYear(timestamp) {
// return ['2021,' 04 (e.g. 2021 March, make 0-indexed)
return [timestamp.split('-')[0], Number(timestamp.split('-')[1].replace(/^0+/, '')) - 1];
}
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
let response_timestamp = formatISO(new Date());
@ -115,8 +120,10 @@ export default ApplicationSerializer.extend({
...payload,
response_timestamp,
by_namespace: this.flattenDataset(payload.data.by_namespace),
formatted_end_time: this.rfc33395ToMonthYear(payload.data.end_time),
formatted_start_time: this.rfc33395ToMonthYear(payload.data.start_time),
};
delete payload.data.by_namespace;
return this._super(store, primaryModelClass, transformedPayload, id, requestType);
},
});
return super.normalizeResponse(store, primaryModelClass, transformedPayload, id, requestType);
}
}

View File

@ -1,7 +1,7 @@
import ApplicationSerializer from '../application';
import { formatISO } from 'date-fns';
export default ApplicationSerializer.extend({
export default class MonthlySerializer extends ApplicationSerializer {
flattenDataset(payload) {
let topTen = payload ? payload.slice(0, 10) : [];
@ -34,7 +34,7 @@ export default ApplicationSerializer.extend({
...flattenedNs,
};
});
},
}
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
let { data } = payload;
@ -52,6 +52,6 @@ export default ApplicationSerializer.extend({
},
};
delete payload.data.by_namespace;
return this._super(store, primaryModelClass, transformedPayload, id, requestType);
},
});
return super.normalizeResponse(store, primaryModelClass, transformedPayload, id, requestType);
}
}

View File

@ -79,11 +79,6 @@ $dark-gray: #535f73;
color: lighten($dark-gray, 30%);
pointer-events: none;
}
&.is-selected {
background-color: $blue-500;
color: white;
text-align: center;
}
}
}

View File

@ -17,7 +17,7 @@
<button
class="link link-plain has-text-weight-semibold is-ghost"
type="button"
{{on "click" this.selectCurrentBillingPeriod}}
{{on "click" (fn this.selectCurrentBillingPeriod D)}}
>
Current billing period
</button>
@ -34,18 +34,6 @@
</div>
</button>
</li>
<li class="action">
<button
class={{concat "link link-plain has-text-weight-semibold is-ghost" (if this.showSingleMonth " is-active")}}
type="button"
{{on "click" this.toggleSingleMonth}}
>
<div class="level is-mobile">
<span class="level-left">Single Month</span>
<Chevron class="has-text-grey-light level-right" />
</div>
</button>
</li>
</ul>
</nav>
{{#if this.showCalendar}}
@ -65,10 +53,10 @@
</button>
</div>
<div {{did-insert this.disableMonths}} class="calendar-widget-grid calendar-widget">
{{#each this.arrayOfMonths as |month index|}}
{{#each @arrayOfMonths as |month index|}}
<button
type="button"
{{on "click" (fn this.selectEndMonth index this.displayYear)}}
{{on "click" (fn this.selectEndMonth index this.displayYear D)}}
class="is-month-list"
id={{concat index "-" this.displayYear}}
>
@ -78,36 +66,5 @@
</div>
</div>
{{/if}}
{{#if this.showSingleMonth}}
<div class="calendar-widget-container">
<div class="select-year">
<button type="button" class="button is-transparent" disabled={{this.disablePastYear}} {{on "click" this.subYear}}>
<Chevron @direction="left" @size="s" class="has-text-grey" />
</button>
<p>{{this.displayYear}}</p>
<button
type="button"
class="button is-transparent padding-right"
disabled={{this.disableFutureYear}}
{{on "click" this.addYear}}
>
<Chevron @direction="right" @size="s" class="has-text-grey" />
</button>
</div>
<div {{did-insert this.disableMonths}} class="calendar-widget-grid calendar-widget">
{{#each this.arrayOfMonths as |month index|}}
<button
type="button"
{{on "click" (fn this.selectSingleMonth index this.displayYear)}}
class="is-month-list"
id={{concat index "-" this.displayYear "-single"}}
>
{{month}}
</button>
{{/each}}
</div>
</div>
{{/if}}
</D.Content>
</BasicDropdown>

View File

@ -37,7 +37,8 @@
</button>
</div>
<p class="is-8 has-text-grey has-bottom-margin-xl">
This date is when client counting starts. Without this starting point, the data shown is not reliable.
This date comes from your license, and defines when client counting starts. Without this starting point, the data
shown is not reliable.
</p>
{{#if (eq @model.config.enabled "Off")}}
<AlertBanner data-test-tracking-disabled @type="warning" @title="Tracking is disabled">
@ -49,19 +50,25 @@
to enable tracking again.
</AlertBanner>
{{/if}}
{{! check for @startTime otherwise emptyState}}
{{#if @model.startTime}}
{{! check for startTimeFromLicense or startTimeFromResponse otherwise emptyState}}
{{#if (or @model.startTimeFromLicense this.startTimeFromResponse)}}
<div class="calendar-title">Filters</div>
<Toolbar>
<ToolbarFilters>
<CalendarWidget
@startTimeDisplay={{this.startTimeDisplay}}
@handleClientActivityQuery={{this.handleClientActivityQuery}}
@arrayOfMonths={{this.arrayOfMonths}}
@endTimeDisplay={{this.endTimeDisplay}}
@endTimeFromResponse={{this.endTimeFromResponse}}
@handleClientActivityQuery={{this.handleClientActivityQuery}}
@handleCurrentBillingPeriod={{this.handleCurrentBillingPeriod}}
@startTimeDisplay={{this.startTimeDisplay}}
/>
{{! ARG TODO more filters for namespace here }}
</ToolbarFilters>
</Toolbar>
{{#if this.responseRangeDiffMessage}}
<AlertBanner @type="warning" @class="has-top-margin-s" @message={{this.responseRangeDiffMessage}} />
{{/if}}
{{#if @isLoading}}
<LayoutLoading />
{{else if this.topTenNamespaces}}
@ -76,15 +83,15 @@
@isDateRange={{this.isDateRange}}
@timestamp={{this.responseTimestamp}}
/>
{{! If no endTime that means the counters/activity request did not return a payload. }}
{{else if this.endTime}}
<EmptyState
@title="No counter activity data"
@message="There is no data in the activity data yet. We collect it at the end of each month, so your data will be available on the first of next month."
/>
{{else}}
{{! ARG TODO remove once we have this dialed }}
<EmptyState @title="Coming soon" @message="Under construction for the 1.10 binary." />
{{/if}}
{{else}}
<EmptyState
@title="No billing start date found"
@message="In order to get the most from this data, please enter your billing period start month. This will ensure that the resulting data is accurate."
/>
{{/if}}
{{! Modal for startTime picker }}

View File

@ -11,8 +11,8 @@ export default function (server) {
return {
request_id: '26be5ab9-dcac-9237-ec12-269a8ca647d5',
data: {
start_time: '2021-02-01T00:00:00Z',
end_time: '2022-01-31T23:59:59Z',
start_time: '2021-03-17T00:00:00Z',
end_time: '2021-12-31T23:59:59Z',
total: {
_comment1: 'total client counts',
clients: 3637,

View File

@ -86,6 +86,7 @@
"d3-tip": "^0.9.1",
"d3-transition": "^1.2.0",
"date-fns": "^2.16.1",
"date-fns-tz": "^1.2.2",
"deepmerge": "^4.0.0",
"doctoc": "^1.4.0",
"ember-api-actions": "^0.2.8",

View File

@ -8994,6 +8994,11 @@ data-urls@^2.0.0:
whatwg-mimetype "^2.3.0"
whatwg-url "^8.0.0"
date-fns-tz@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/date-fns-tz/-/date-fns-tz-1.2.2.tgz#89432b54ce3fa7d050a2039e997e5b6a96df35dd"
integrity sha512-vWtn44eEqnLbkACb7T5G5gPgKR4nY8NkNMOCyoY49NsRGHrcDmY2aysCyzDeA+u+vcDBn/w6nQqEDyouRs4m8w==
date-fns@^2.16.1:
version "2.21.1"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.21.1.tgz#679a4ccaa584c0706ea70b3fa92262ac3009d2b0"
@ -11729,11 +11734,6 @@ fake-xml-http-request@^2.1.1:
resolved "https://registry.yarnpkg.com/fake-xml-http-request/-/fake-xml-http-request-2.1.2.tgz#f1786720cae50bbb46273035a0173414f3e85e74"
integrity sha512-HaFMBi7r+oEC9iJNpc3bvcW7Z7iLmM26hPDmlb0mFwyANSsOQAtJxbdWsXITKOzZUyMYK0zYCv3h5yDj9TsiXg==
faker@^5.5.3:
version "5.5.3"
resolved "https://registry.yarnpkg.com/faker/-/faker-5.5.3.tgz#c57974ee484431b25205c2c8dc09fda861e51e0e"
integrity sha512-wLTv2a28wjUyWkbnX7u/ABZBkUkIF2fCd73V6P2oFqEGEktDfzWx4UxrSqtPRw0xPRAcjeAOIiJWqZm3pP4u3g==
fast-deep-equal@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"