1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-23 11:56:00 +01:00

Export all events matching dates (#990)

* Export eagerly pulls down all events

Export does not add to rendered elements since that may cause slow down.
Export is tied to the currently rendered list of events though `dirtyDates` bool

* Use manual btn-submit class

* Remove unnecessary method

* Fix ExpressionChangedAfterItHasBeenCheckedError
This commit is contained in:
Matt Gibson 2021-06-02 07:21:57 -05:00 committed by GitHub
parent 744e86601f
commit 945e968e06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 80 additions and 39 deletions

View File

@ -4,21 +4,29 @@
<div class="form-inline"> <div class="form-inline">
<label class="sr-only" for="start">{{'startDate' | i18n}}</label> <label class="sr-only" for="start">{{'startDate' | i18n}}</label>
<input type="datetime-local" class="form-control form-control-sm" id="start" <input type="datetime-local" class="form-control form-control-sm" id="start"
placeholder="{{'startDate' | i18n}}" [(ngModel)]="start" placeholder="YYYY-MM-DDTHH:MM"> placeholder="{{'startDate' | i18n}}" [(ngModel)]="start" placeholder="YYYY-MM-DDTHH:MM"
(change)="dirtyDates = true">
<span class="mx-2">-</span> <span class="mx-2">-</span>
<label class="sr-only" for="end">{{'endDate' | i18n}}</label> <label class="sr-only" for="end">{{'endDate' | i18n}}</label>
<input type="datetime-local" class="form-control form-control-sm" id="end" <input type="datetime-local" class="form-control form-control-sm" id="end"
placeholder="{{'endDate' | i18n}}" [(ngModel)]="end" placeholder="YYYY-MM-DDTHH:MM"> placeholder="{{'endDate' | i18n}}" [(ngModel)]="end" placeholder="YYYY-MM-DDTHH:MM"
(change)="dirtyDates = true">
</div> </div>
<button #refreshBtn [appApiAction]="refreshPromise" type="button" class="btn btn-sm btn-outline-primary ml-3" <form #refreshForm [appApiAction]="refreshPromise" class="d-inline">
(click)="loadEvents(true)" [disabled]="loaded && refreshBtn.loading"> <button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="loadEvents(true)"
<i class="fa fa-refresh fa-fw" aria-hidden="true" [ngClass]="{'fa-spin': loaded && refreshBtn.loading}"></i> [disabled]="loaded && refreshForm.loading">
<i class="fa fa-refresh fa-fw" aria-hidden="true" [ngClass]="{'fa-spin': loaded && refreshForm.loading}"></i>
{{'refresh' | i18n}} {{'refresh' | i18n}}
</button> </button>
<button #exportBtn [appApiAction]="exportPromise" type="button" class="btn btn-sm btn-outline-primary ml-3" </form>
(click)="exportEvents()" [disabled]="loaded && refreshBtn.loading"> <form #exportForm [appApiAction]="exportForm" class="d-inline">
{{'export' | i18n}} <button type="button" class="btn btn-sm btn-outline-primary btn-submit manual ml-3"
[ngClass]="{loading:refreshForm.loading}" (click)="exportEvents()"
[disabled]="loaded && exportForm.loading || dirtyDates">
<i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
<span>{{'export' | i18n}}</span>
</button> </button>
</form>
</div> </div>
</div> </div>
<ng-container *ngIf="!loaded"> <ng-container *ngIf="!loaded">

View File

@ -29,6 +29,7 @@ export class EventsComponent implements OnInit {
events: EventView[]; events: EventView[];
start: string; start: string;
end: string; end: string;
dirtyDates: boolean = true;
continuationToken: string; continuationToken: string;
refreshPromise: Promise<any>; refreshPromise: Promise<any>;
exportPromise: Promise<any>; exportPromise: Promise<any>;
@ -69,16 +70,20 @@ export class EventsComponent implements OnInit {
} }
async exportEvents() { async exportEvents() {
if (this.appApiPromiseUnfulfilled()) { if (this.appApiPromiseUnfulfilled() || this.dirtyDates) {
return; return;
} }
this.loading = true; this.loading = true;
this.exportPromise = this.exportService.getEventExport(this.events).then(data => {
const fileName = this.exportService.getFileName('org-events', 'csv'); const dates = this.parseDates();
this.platformUtilsService.saveFile(window, data, { type: 'text/plain' }, fileName); if (dates == null) {
}); return;
}
try { try {
this.exportPromise = this.export(dates[0], dates[1]);
await this.exportPromise; await this.exportPromise;
} catch { } } catch { }
@ -91,29 +96,56 @@ export class EventsComponent implements OnInit {
return; return;
} }
let dates: string[] = null; const dates = this.parseDates();
try { if (dates == null) {
dates = this.eventService.formatDateFilters(this.start, this.end);
} catch (e) {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('invalidDateRange'));
return; return;
} }
this.loading = true; this.loading = true;
let response: ListResponse<EventResponse>; let events: EventView[] = [];
try { try {
const promise = this.apiService.getEventsOrganization(this.organizationId, dates[0], dates[1], const promise = this.loadAndParseEvents(dates[0], dates[1], clearExisting ? null : this.continuationToken);
clearExisting ? null : this.continuationToken);
if (clearExisting) { if (clearExisting) {
this.refreshPromise = promise; this.refreshPromise = promise;
} else { } else {
this.morePromise = promise; this.morePromise = promise;
} }
response = await promise; const result = await promise;
this.continuationToken = result.continuationToken;
events = result.events;
} catch { } } catch { }
this.continuationToken = response.continuationToken; if (!clearExisting && this.events != null && this.events.length > 0) {
this.events = this.events.concat(events);
} else {
this.events = events;
}
this.dirtyDates = false;
this.loading = false;
this.morePromise = null;
this.refreshPromise = null;
}
private async export(start: string, end: string) {
let continuationToken = this.continuationToken;
let events = [].concat(this.events);
while (continuationToken != null) {
const result = await this.loadAndParseEvents(start, end, continuationToken);
continuationToken = result.continuationToken;
events = events.concat(result.events);
}
const data = await this.exportService.getEventExport(events);
const fileName = this.exportService.getFileName('org-events', 'csv');
this.platformUtilsService.saveFile(window, data, { type: 'text/plain' }, fileName);
}
private async loadAndParseEvents(startDate: string, endDate: string, continuationToken: string) {
const response = await this.apiService.getEventsOrganization(this.organizationId, startDate, endDate,
continuationToken);
const events = await Promise.all(response.data.map(async r => { const events = await Promise.all(response.data.map(async r => {
const userId = r.actingUserId == null ? r.userId : r.actingUserId; const userId = r.actingUserId == null ? r.userId : r.actingUserId;
const eventInfo = await this.eventService.getEventInfo(r); const eventInfo = await this.eventService.getEventInfo(r);
@ -132,16 +164,19 @@ export class EventsComponent implements OnInit {
type: r.type, type: r.type,
}); });
})); }));
return { continuationToken: response.continuationToken, events: events };
if (!clearExisting && this.events != null && this.events.length > 0) {
this.events = this.events.concat(events);
} else {
this.events = events;
} }
this.loading = false; private parseDates() {
this.morePromise = null; let dates: string[] = null;
this.refreshPromise = null; try {
dates = this.eventService.formatDateFilters(this.start, this.end);
} catch (e) {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('invalidDateRange'));
return null;
}
return dates;
} }
private appApiPromiseUnfulfilled() { private appApiPromiseUnfulfilled() {

View File

@ -232,10 +232,8 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn btn-primary disabled" disabled=true *ngIf="disableSend"> <button type="submit" class="btn btn-primary btn-submit manual" [ngClass]="{loading:form.loading}"
<span>{{'save' | i18n}}</span> [disabled]="form.loading || disableSend">
</button>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading" *ngIf="!disableSend">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i> <i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'save' | i18n}}</span> <span>{{'save' | i18n}}</span>
</button> </button>