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

Add endpoint error page #3991

Merged
merged 13 commits into from
Nov 11, 2019
Prev Previous commit
Next Next commit
Add ability to dismiss error events
  • Loading branch information
KlapTrap committed Nov 1, 2019
commit 6ceafd7f046fe97a053485dbf48c6d46cce2e171
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@ <h2>{{errorDetails.endpoint.name}}</h2>
</a>
</div>
</div>
<div *ngIf="errorDetails.errors && errorDetails.errors.length ; else noEvents">

<div class="error-page__errors" *ngIf="errorDetails.errors && errorDetails.errors.length ; else noEvents">
<span class="error-page__errors-header">
<button mat-button color="warn" (click)="dismissEndpointErrors(errorDetails.endpoint.guid)">Dismiss
errors</button>
</span>
<mat-list>
richard-cox marked this conversation as resolved.
Show resolved Hide resolved
<mat-card class="error-page__event-row-card" *ngFor="let error of errorDetails.errors">
<mat-card-header>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@
white-space: pre-wrap;
}
}
&__errors {
display: flex;
flex-direction: column;
}
&__errors-header {
display: flex;
flex-direction: row-reverse;
}
&__event-row {
margin-right: 15px;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { InternalEventState } from '../../../../../store/src/types/internal-even
import { endpointSchemaKey } from '../../../../../store/src/helpers/entity-factory';
import { StratosStatus } from '../../../shared/shared.types';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { SendClearEndpointEventsAction } from '../../../../../store/src/actions/internal-events.actions';

@Component({
selector: 'app-events-page',
Expand All @@ -37,6 +38,11 @@ export class ErrorPageComponent implements OnInit {
}
};
}

public dismissEndpointErrors(endpointGuid: string) {
this.store.dispatch(new SendClearEndpointEventsAction(endpointGuid));
}

ngOnInit() {
const endpointId = this.activatedRoute.snapshot.params.endpointId;
if (endpointId) {
Expand All @@ -47,15 +53,14 @@ export class ErrorPageComponent implements OnInit {
endpointEntitySchema
);
const cfEndpointEventMonitor = this.internalEventMonitorFactory.getMonitor(endpointSchemaKey, of([endpointId]));
this.errorDetails$ = cfEndpointEventMonitor.hasErroredOverTime().pipe(
this.errorDetails$ = cfEndpointEventMonitor.hasErroredOverTimeNoPoll().pipe(
withLatestFrom(endpointMonitor.entity$),
map(([errors, endpoint]) => {
return {
endpoint,
errors: errors && errors[endpointId] ? errors[endpointId].map(error => this.stringifyErrorResponse(error)) : errors[endpointId]
};
}),
first()
})
);
this.jsonDownloadHref$ = this.errorDetails$.pipe(
map((info) => {
Expand All @@ -66,10 +71,6 @@ export class ErrorPageComponent implements OnInit {
}
}

private isStringable(obj: any) {
return obj !== undefined && obj !== null && (obj.constructor === Object || Array.isArray(obj.constructor));
}

constructor(
private activatedRoute: ActivatedRoute,
private store: Store<AppState>,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
<div *ngIf="errorMessage$ | async as message" class="header-event--error">
<div [ngClass]="{'header-event--minimized' : eventMinimized$ | async}" class="header-event__outer">
<span class="header-event">
<mat-icon class="header-event__error-icon">error</mat-icon>
<div class="header-event__message-wrapper">{{ message }} <span *ngIf="endpointId"> - <a
class="header-event__details-link" [routerLink]="['/errors/', endpointId]">view details</a>
<div *ngIf="errorMessage$ | async as message">
<div *ngIf="message" class="header-event--error">
<div class="header-event__outer">
<span class="header-event">
<span class="header-event__error-message">
<mat-icon class="header-event__error-icon">error</mat-icon>
<div class="header-event__message-wrapper">{{ message }} -
<span *ngIf="!endpointId">
<a class="header-event__details-link" [routerLink]="['/endpoints']">view endpoints</a>
</span>
<span *ngIf="endpointId">
<a class="header-event__details-link" [routerLink]="['/errors/', endpointId]">view details</a>
</span>
</div>
</span>
</div>
</span>
<mat-icon *ngIf="endpointId" class="header-event__error-dismiss" (click)="dismissEndpointErrors(endpointId)">
clear
</mat-icon>
</span>
</div>
</div>
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@
overflow: visible;
z-index: 1;
}
&__error-message {
display: flex;
flex: 1;
}
&__error-dismiss {
cursor: pointer;
}
// &__details-link {
// &:hover {
// font-weight: bold;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import { combineLatest, Observable, of as observableOf } from 'rxjs';
import { distinctUntilChanged, filter, map } from 'rxjs/operators';

import { CFAppState } from '../../../../../../cloud-foundry/src/cf-app-state';
import { ToggleHeaderEvent } from '../../../../../../store/src/actions/dashboard-actions';
import { endpointSchemaKey } from '../../../../../../store/src/helpers/entity-factory';
import { endpointListKey, EndpointModel } from '../../../../../../store/src/types/endpoint.types';
import { endpointEntitySchema } from '../../../../base-entity-schemas';
import { InternalEventMonitorFactory } from '../../../monitors/internal-event-monitor.factory';
import { PaginationMonitor } from '../../../monitors/pagination-monitor';
import { SendClearEndpointEventsAction } from '../../../../../../store/src/actions/internal-events.actions';


@Component({
Expand Down Expand Up @@ -39,23 +39,17 @@ export class PageHeaderEventsComponent implements OnInit {
@Input()
public simpleErrorMessage = false;

public eventMinimized$: Observable<boolean>;
public errorMessage$: Observable<string>;
endpointId: any;

constructor(
private internalEventMonitorFactory: InternalEventMonitorFactory,
private activatedRoute: ActivatedRoute,
private store: Store<CFAppState>
) {
this.eventMinimized$ = this.store.select('dashboard').pipe(
map(dashboardState => dashboardState.headerEventMinimized),
distinctUntilChanged()
);
}
) { }

public toggleEvent() {
this.store.dispatch(new ToggleHeaderEvent());
public dismissEndpointErrors(endpointGuid: string) {
this.store.dispatch(new SendClearEndpointEventsAction(endpointGuid));
}

ngOnInit() {
Expand All @@ -75,9 +69,13 @@ export class PageHeaderEventsComponent implements OnInit {
cfEndpointEventMonitor.hasErroredOverTime(),
endpointMonitor.currentPage$
).pipe(
filter(([errors]) => !!Object.keys(errors).length),
map(([errors, endpoints]) => {
const endpointString = Object.keys(errors)
const keys = errors ? Object.keys(errors) : null;
if (!keys || !keys.length) {
return null;
}
console.log(keys);
const endpointString = keys
.map(id => endpoints.find(endpoint => {
return endpoint.guid === id;
}))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,26 +67,32 @@ export class InternalEventMonitor {
);
}


private getErrorsOverTimePeriod(state: InternalEventSubjectState, minutes: number) {
const time = moment().subtract(minutes, 'minutes').unix() * 1000;
return Object.keys(state).reduce<Record<string, InternalEventState[]>>((errorObject, key) => {
const events = state[key];
const hasErrorEvent = !!events.find(event => {
const isError500 = event.eventCode[0] === '5';
return event.severity === InternalEventSeverity.ERROR && isError500 && event.timestamp > time;
});
if (hasErrorEvent) {
errorObject[key] = events;
}
return errorObject;
}, {});
}
public hasErroredOverTime(minutes = 5) {
const interval$ = newNonAngularInterval(this.ngZone, 30000).pipe(
startWith(-1)
);
return combineLatest(this.events$, interval$).pipe(
map(([state]) => {
const time = moment().subtract(minutes, 'minutes').unix() * 1000;
return Object.keys(state).reduce<Record<string, InternalEventState[]>>((errorObject, key) => {
const events = state[key];
const hasErrorEvent = !!events.find(event => {
const isError500 = event.eventCode[0] === '5';
return event.severity === InternalEventSeverity.ERROR && isError500 && event.timestamp > time;
});
if (hasErrorEvent) {
errorObject[key] = events;
}
return errorObject;
}, {});
})
map(([state]) => this.getErrorsOverTimePeriod(state, minutes))
);
}

public hasErroredOverTimeNoPoll(minutes = 5) {
return this.events$.pipe(
map(state => this.getErrorsOverTimePeriod(state, minutes))
);
}

Expand Down
5 changes: 0 additions & 5 deletions src/frontend/packages/store/src/actions/dashboard-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { DashboardState } from '../reducers/dashboard-reducer';
export const OPEN_SIDE_NAV = '[Dashboard] Open side nav';
export const CLOSE_SIDE_NAV = '[Dashboard] Close side nav';
export const TOGGLE_SIDE_NAV = '[Dashboard] Toggle side nav';
export const TOGGLE_HEADER_EVENT = '[Dashboard] Toggle header event';
export const SET_HEADER_EVENT = '[Dashboard] Set header event';

export const ENABLE_SIDE_NAV_MOBILE_MODE = '[Dashboard] Enable mobile nav';
Expand Down Expand Up @@ -37,10 +36,6 @@ export class ToggleSideNav implements Action {
type = TOGGLE_SIDE_NAV;
}

export class ToggleHeaderEvent implements Action {
type = TOGGLE_HEADER_EVENT;
}

export class ShowSideHelp implements Action {
constructor(public document: string) { }
type = SHOW_SIDE_HELP;
Expand Down
14 changes: 11 additions & 3 deletions src/frontend/packages/store/src/actions/internal-events.actions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Action } from '@ngrx/store';
import * as moment from 'moment';

import { SEND_EVENT, InternalEventSeverity, CLEAR_EVENTS, InternalEventState, InternalEventStateMetadata } from '../types/internal-events.types';
import { SEND_EVENT, InternalEventSeverity, CLEAR_EVENTS, InternalEventState, InternalEventStateMetadata, CLEAR_ENDPOINT_ERROR_EVENTS } from '../types/internal-events.types';

export class SendEventAction<T = InternalEventStateMetadata> implements Action {
public type = SEND_EVENT;
Expand All @@ -26,12 +26,20 @@ export class SendClearEventAction implements Action {
public params: {
timestamp?: number,
eventCode?: string,
endpointGuid?: string
clean: boolean
}
) {
const { timestamp, eventCode, clean } = params;
if (!timestamp && !eventCode && !clean) {
const { timestamp, eventCode, endpointGuid, clean } = params;
if (!timestamp && !eventCode && !endpointGuid && !clean) {
throw new Error('Either a timestamp or event code is needed to clear events');
}
}
}

export class SendClearEndpointEventsAction implements Action {
public type = CLEAR_ENDPOINT_ERROR_EVENTS;
constructor(
public endpointGuid: string
) { }
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ export class EndpointApiError {
metadata: {
httpMethod: action.apiAction.options.method,
errorResponse: error,
// FIXME We can do a better job at displaying the full url once
// the angular 8 HttpClient migration is done.
// action.apiAction.options
url,
},
}),
Expand Down
10 changes: 0 additions & 10 deletions src/frontend/packages/store/src/reducers/dashboard-reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
SetHeaderEvent,
SetPollingEnabledAction,
SHOW_SIDE_HELP,
TOGGLE_HEADER_EVENT,
TOGGLE_SIDE_NAV,
} from '../actions/dashboard-actions';
import {
Expand All @@ -26,7 +25,6 @@ export interface DashboardState {
isMobile: boolean;
isMobileNavOpen: boolean;
sideNavPinned: boolean;
headerEventMinimized: boolean;
sideHelpOpen: boolean;
sideHelpDocument: string;
}
Expand All @@ -38,7 +36,6 @@ export const defaultDashboardState: DashboardState = {
isMobile: false,
isMobileNavOpen: false,
sideNavPinned: true,
headerEventMinimized: false,
sideHelpOpen: false,
sideHelpDocument: null,
};
Expand All @@ -64,17 +61,10 @@ export function dashboardReducer(state: DashboardState = defaultDashboardState,
return { ...state, isMobile: true, isMobileNavOpen: false };
case DISABLE_SIDE_NAV_MOBILE_MODE:
return { ...state, isMobile: false, isMobileNavOpen: false };
case TOGGLE_HEADER_EVENT:
return { ...state, headerEventMinimized: !state.headerEventMinimized };
case SHOW_SIDE_HELP:
return { ...state, sideHelpOpen: true, sideHelpDocument: action.document };
case CLOSE_SIDE_HELP:
return { ...state, sideHelpOpen: false, sideHelpDocument: '' };
case SET_HEADER_EVENT:
const setHeaderEvent = action as SetHeaderEvent;
return {
...state, headerEventMinimized: setHeaderEvent.minimised
};
case TIMEOUT_SESSION:
const timeoutSessionAction = action as SetSessionTimeoutAction;
return {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { Action } from '@ngrx/store';

import { SendClearEventAction, SendEventAction } from '../actions/internal-events.actions';
import { SendClearEventAction, SendEventAction, SendClearEndpointEventsAction } from '../actions/internal-events.actions';
import {
CLEAR_EVENTS,
GLOBAL_EVENT,
InternalEventsState,
InternalEventState,
SEND_EVENT,
CLEAR_ENDPOINT_ERROR_EVENTS,
} from '../types/internal-events.types';
import { endpointSchemaKey } from './../helpers/entity-factory';
import { DISCONNECT_ENDPOINTS, DISCONNECT_ENDPOINTS_SUCCESS, DisconnectEndpoint, UNREGISTER_ENDPOINTS_SUCCESS, CONNECT_ENDPOINTS_SUCCESS } from '../actions/endpoint.actions';

const defaultState: InternalEventsState = {
types: {
Expand All @@ -30,6 +32,16 @@ export function internalEventReducer(state: InternalEventsState = defaultState,
case CLEAR_EVENTS: {
return clearEvents(state, action as SendClearEventAction);
}
case CLEAR_ENDPOINT_ERROR_EVENTS: {
const clearEndpointAction = action as SendClearEndpointEventsAction;
return clearEndpointEvents(state, clearEndpointAction.endpointGuid);
}
case DISCONNECT_ENDPOINTS_SUCCESS:
case UNREGISTER_ENDPOINTS_SUCCESS:
case CONNECT_ENDPOINTS_SUCCESS: {
const clearEndpointAction = action as DisconnectEndpoint;
return clearEndpointEvents(state, clearEndpointAction.guid);
}
}
return state;
}
Expand All @@ -51,8 +63,29 @@ function setSubjectEvents(state: InternalEventsState, eventSubjectId: string, ev
return newState;
}

function clearEndpointEvents(state: InternalEventsState, endpointGuid: string): InternalEventsState {
if (state.types.endpoint[endpointGuid]) {
const {
[endpointGuid]: cleared,
...endpoint
} = state.types.endpoint;
return {
...state,
types: {
...state.types,
endpoint
}
};
} else {
return state;
}
}

function clearEvents(state: InternalEventsState, clearAction: SendClearEventAction) {
const { eventSubjectId, eventType, params } = clearAction;
if (params.endpointGuid) {

}
const events = getEvents(state, eventSubjectId, eventType);
const filteredEvents = clearAction.params.clean ? [] : events.filter((event: InternalEventState) => {
if (params.timestamp) {
Expand Down
Loading