Skip to content

Commit

Permalink
Merge pull request #588 from HSF/sponce_cycleEnhancement
Browse files Browse the repository at this point in the history
Auto reload feature for cycle-event component
  • Loading branch information
EdwardMoyse authored Jul 21, 2023
2 parents 0d2dc7b + d7666ff commit 24b681c
Show file tree
Hide file tree
Showing 8 changed files with 280 additions and 116 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
<app-menu-toggle
<button
class="cycle-events"
[ngClass]=""
[matTooltip]="tooltip"
matTooltipPosition="above"
matTooltipTouchGestures="off"
tooltip="Cycle through events"
[active]="active"
icon="cycle-events"
(click)="toggleCycle()"
>
</app-menu-toggle>
<svg
class="cycle-events-icon"
[ngClass]="{ 'active-icon': active, 'reload-icon': reloading }"
>
<use href="assets/icons/cycle-events.svg#cycle-events"></use>
</svg>
</button>
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
:host {
display: flex;
margin: 0 0.6rem;

.cycle-events {
display: flex;
background: unset;
border: none;
height: 2.5rem;
width: 2.5rem;
min-height: 2.5rem;
min-width: 2.5rem;
padding: 0.65rem;
cursor: pointer;
align-self: center;
transition: all 0.4s;

&-icon {
width: 100%;
height: 100%;

&.active-icon {
--phoenix-options-icon-path: #00bcd4;
}
&.reload-icon {
--phoenix-options-icon-path: #77dd77;
}
}

&:hover {
background-color: var(--phoenix-options-icon-bg);
border-radius: 40%;
transition: all 0.4s;
}

&.disabled {
cursor: not-allowed;
opacity: 0.4;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,17 @@ describe('CycleEventsComponent', () => {
jest.clearAllTimers();
jest.useFakeTimers();
component.interval = 1000;

component.toggleCycle();

expect(component.active).toBeTruthy();

expect(component.reloading).toBeFalsy();
jest.advanceTimersByTime(1200);

expect(mockEventDisplay.loadEvent).toHaveBeenCalledWith('eventKey2');

component.toggleCycle();
expect(component.active).toBeTruthy();
expect(component.reloading).toBeTruthy();
component.toggleCycle();
expect(component.active).toBeFalsy();
expect(component.reloading).toBeFalsy();
jest.clearAllTimers();
});
});
Original file line number Diff line number Diff line change
@@ -1,43 +1,73 @@
import { Component, Input, OnInit } from '@angular/core';
import { EventDisplayService } from '../../../services/event-display.service';
import { FileLoaderService } from '../../../services/file-loader.service';

@Component({
selector: 'app-cycle-events',
templateUrl: './cycle-events.component.html',
styleUrls: ['./cycle-events.component.scss'],
})
export class CycleEventsComponent implements OnInit {
@Input()
interval: number;
@Input() interval: number;
@Input() tooltip: string;
@Input() icon: string;

// There are actually 3 states we go through when clicking this component :
// - not active : so we are not cycling throught events
// - active and not reloading : cycling over the events stored in events
// - active and reloading : cycling and reloading the events when reaching the end
// Last state is useful e.g. for live feed of events
active: boolean = false;
reloading: boolean = false;

private intervalId: NodeJS.Timer;

private events: string[];

constructor(private eventDisplay: EventDisplayService) {}
constructor(
private eventDisplay: EventDisplayService,
private fileLoader: FileLoaderService
) {}

ngOnInit() {
this.eventDisplay.listenToLoadedEventsChange(
(events) => (this.events = events)
);
this.eventDisplay.listenToLoadedEventsChange((events) => {
this.events = events;
if (this.active) {
// restart cycling from first event
clearInterval(this.intervalId);
this.startCycleInterval();
}
});
}

toggleCycle() {
this.active = !this.active;
this.reloading = this.active && !this.reloading;
this.active = !this.active || this.reloading;
console.log(this.active, this.reloading);
clearInterval(this.intervalId);

if (this.active) {
this.startCycleInterval();
}
}

private startCycleInterval(startIndex: number = 0) {
// load immediately first event
let index = startIndex;

this.eventDisplay.loadEvent(this.events[index]);
index = index + 1 >= this.events.length ? -1 : index + 1;
// launch automatic cycling
this.intervalId = setInterval(() => {
index = index >= this.events.length ? 0 : index + 1;
// special value -1 is used to denote wrapping of the current set of events
if (index == -1) {
if (this.reloading) {
// reload the current events
this.fileLoader.reloadLastEvents(this.eventDisplay);
}
// put back index to 0 to start with first event anyway
index = 0;
}
this.eventDisplay.loadEvent(this.events[index]);
index = index + 1 >= this.events.length ? -1 : index + 1;
}, this.interval);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { EventDisplayService } from '../../../../services/event-display.service';
import { FileNode } from '../../../file-explorer/file-explorer.component';
import { FileLoaderService } from '../../../../services/file-loader.service';
import { PhoenixUIModule } from '../../../phoenix-ui.module';
import { EventDataExplorerDialogData } from '../event-data-explorer.component';
import {
Expand All @@ -18,6 +19,10 @@ describe.skip('EventDataExplorerDialogComponent', () => {
loadStateFromJSON: jest.fn(),
};

const mockFileLoaderService = {
loadEvent: jest.fn(),
};

const mockEventDisplay = {};

const mockDialogRef = {
Expand Down Expand Up @@ -97,31 +102,30 @@ describe.skip('EventDataExplorerDialogComponent', () => {

it('should load event based on file type', () => {
jest
.spyOn(EventDataExplorerDialogComponent.prototype, 'makeRequest')
.spyOn(FileLoaderService.prototype, 'loadEvent')
.mockImplementation(
(_arg1: string, _arg2: string, onData: (data: string) => void) =>
onData('test')
(_arg1: string, eventDisplay: EventDisplayService) => true
);

jest.spyOn(component as any, 'loadJSONEvent');
jest.spyOn(FileLoaderService.prototype, 'loadEvent');
component.loadEvent('https://example.com/event_data/test.json');
expect((component as any).loadJSONEvent).toHaveBeenCalled();

jest.spyOn(component as any, 'loadJiveXMLEvent');
component.loadEvent('https://example.com/event_data/test.xml');
expect((component as any).loadJiveXMLEvent).toHaveBeenCalled();
expect(mockFileLoaderService.loadEvent).toHaveBeenCalled();
});

it('should load config', () => {
jest
.spyOn(EventDataExplorerDialogComponent.prototype, 'makeRequest')
.spyOn(FileLoaderService.prototype, 'makeRequest')
.mockImplementation(
(_arg1: string, _arg2: string, onData: (data: string) => void) =>
onData('{}')
(
_arg1: string,
_arg2: 'json' | 'blob' | 'text',
onData: (data: any) => void
) => {
onData('{}');
return true;
}
);

component.loadConfig('https://example.com/config_data/test.json');

expect(mockStateManager.loadStateFromJSON).toHaveBeenCalledWith({});
});
});
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { Component, Inject } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { JiveXMLLoader } from 'phoenix-event-display';
import { EventDisplayService } from '../../../../services/event-display.service';
import { FileNode } from '../../../file-explorer/file-explorer.component';
import { FileLoaderService } from '../../../../services/file-loader.service';
import { EventDataExplorerDialogData } from '../event-data-explorer.component';
import JSZip from 'jszip';

const supportFileTypes = ['json', 'xml'];

Expand All @@ -26,20 +25,25 @@ export class EventDataExplorerDialogComponent {

constructor(
private eventDisplay: EventDisplayService,
private fileLoader: FileLoaderService,
private dialogRef: MatDialogRef<EventDataExplorerDialogComponent>,
@Inject(MAT_DIALOG_DATA) private dialogData: EventDataExplorerDialogData
) {
// Event data
this.makeRequest(this.dialogData.apiURL, 'json', (res: FileResponse[]) => {
const filePaths = res.filter((file) =>
supportFileTypes.includes(file.name.split('.').pop())
);
fileLoader.makeRequest(
this.dialogData.apiURL,
'json',
(res: FileResponse[]) => {
const filePaths = res.filter((file) =>
supportFileTypes.includes(file.name.split('.').pop())
);

this.eventDataFileNode = this.buildFileNode(filePaths);
});
this.eventDataFileNode = this.buildFileNode(filePaths);
}
);

// Config
this.makeRequest(
fileLoader.makeRequest(
`${this.dialogData.apiURL}?type=config`,
'json',
(res: FileResponse[]) => {
Expand All @@ -53,98 +57,31 @@ export class EventDataExplorerDialogComponent {
}

loadEvent(file: string) {
const isZip = file.split('.').pop() === 'zip';
const rawfile = isZip ? file.substring(0, file.length - 4) : file;
this.makeRequest(file, isZip ? 'blob' : 'text', (eventData) => {
switch (rawfile.split('.').pop()) {
case 'xml':
this.loadJiveXMLEvent(eventData);
break;
case 'json':
this.loadJSONEvent(eventData);
break;
}
});
}

private loadJSONEvent(eventData: string) {
this.eventDisplay.parsePhoenixEvents(JSON.parse(eventData));
this.onClose();
}

private loadJiveXMLEvent(eventData: string) {
const jiveXMLLoader = new JiveXMLLoader();
jiveXMLLoader.process(eventData);
const processedEventData = jiveXMLLoader.getEventData();
this.eventDisplay.buildEventDataFromJSON(processedEventData);
this.onClose();
this.loading = true;
this.error = this.fileLoader.loadEvent(file, this.eventDisplay);
this.loading = false;
if (!this.error) this.onClose();
}

loadConfig(file: string) {
this.makeRequest(
this.loading = true;
this.error = this.fileLoader.makeRequest(
`${this.dialogData.apiURL}?type=config&f=${file}`,
'text',
(config) => {
const stateManager = this.eventDisplay.getStateManager();
stateManager.loadStateFromJSON(JSON.parse(config));

this.onClose();
}
);
this.loading = false;
}

// Helpers

onClose() {
this.dialogRef.close();
}

private async unzip(data: ArrayBuffer) {
const archive = new JSZip();
await archive.loadAsync(data);
let fileData = '';
let multiFile = false;
for (const filePath in archive.files) {
if (multiFile) {
console.error(
'Zip archive contains more than one file. Ignoring all but first'
);
break;
}
fileData = await archive.file(filePath).async('string');
multiFile = true;
}
return fileData;
}

makeRequest(
urlPath: string,
responseType: 'json' | 'text' | 'blob',
onData: (data: any) => void
) {
this.loading = true;
fetch(urlPath)
.then((res) => res[responseType]())
.then((data) => {
if (responseType === 'blob') {
data
.arrayBuffer()
.then((buf) => this.unzip(buf))
.then((d) => onData(d));
} else {
onData(data);
}
this.error = false;
})
.catch((error) => {
console.error(error);
this.error = true;
})
.finally(() => {
this.loading = false;
});
}

private buildFileNode(filePaths: FileResponse[]): FileNode {
const rootNode = new FileNode();
let fileNode = rootNode;
Expand Down
Loading

0 comments on commit 24b681c

Please sign in to comment.