web/storagenode: notifications domain and app types separations
WHAT: notifications store, api, types refactoring; service creation; notifications module store tests creation; WHY: to separate domain and app types; to be able to reuse code more easier; Change-Id: I01c6584fc41bbf73e0b6f84501cc66bbebd50ace
This commit is contained in:
parent
074784e1b4
commit
3b388c21cf
@ -68,7 +68,6 @@ import { APPSTATE_ACTIONS } from '@/app/store/modules/appState';
|
|||||||
import { NODE_ACTIONS } from '@/app/store/modules/node';
|
import { NODE_ACTIONS } from '@/app/store/modules/node';
|
||||||
import { NOTIFICATIONS_ACTIONS } from '@/app/store/modules/notifications';
|
import { NOTIFICATIONS_ACTIONS } from '@/app/store/modules/notifications';
|
||||||
import { PAYOUT_ACTIONS } from '@/app/store/modules/payout';
|
import { PAYOUT_ACTIONS } from '@/app/store/modules/payout';
|
||||||
import { NotificationsCursor } from '@/app/types/notifications';
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
GET_NODE_INFO,
|
GET_NODE_INFO,
|
||||||
@ -90,18 +89,23 @@ const {
|
|||||||
export default class SNOHeader extends Vue {
|
export default class SNOHeader extends Vue {
|
||||||
public isNotificationPopupShown: boolean = false;
|
public isNotificationPopupShown: boolean = false;
|
||||||
public isOptionsShown: boolean = false;
|
public isOptionsShown: boolean = false;
|
||||||
|
private readonly FIRST_PAGE: number = 1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lifecycle hook before render.
|
* Lifecycle hook before render.
|
||||||
* Fetches first page of notifications.
|
* Fetches first page of notifications.
|
||||||
*/
|
*/
|
||||||
public beforeMount(): void {
|
public async beforeMount(): Promise<void> {
|
||||||
|
await this.$store.dispatch(APPSTATE_ACTIONS.SET_LOADING, true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.$store.dispatch(NODE_ACTIONS.GET_NODE_INFO);
|
await this.$store.dispatch(NODE_ACTIONS.GET_NODE_INFO);
|
||||||
this.$store.dispatch(NOTIFICATIONS_ACTIONS.GET_NOTIFICATIONS, new NotificationsCursor(1));
|
await this.$store.dispatch(NOTIFICATIONS_ACTIONS.GET_NOTIFICATIONS, this.FIRST_PAGE);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error.message);
|
console.error(error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.$store.dispatch(APPSTATE_ACTIONS.SET_LOADING, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get nodeId(): string {
|
public get nodeId(): string {
|
||||||
@ -192,7 +196,7 @@ export default class SNOHeader extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.$store.dispatch(NOTIFICATIONS_ACTIONS.GET_NOTIFICATIONS, new NotificationsCursor(1));
|
await this.$store.dispatch(NOTIFICATIONS_ACTIONS.GET_NOTIFICATIONS, this.FIRST_PAGE);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error.message);
|
console.error(error.message);
|
||||||
}
|
}
|
||||||
|
@ -12,10 +12,10 @@
|
|||||||
<div
|
<div
|
||||||
class="notification-popup-container__content"
|
class="notification-popup-container__content"
|
||||||
:class="{'collapsed': isCollapsed}"
|
:class="{'collapsed': isCollapsed}"
|
||||||
v-if="latestNotifications.length"
|
v-if="latest.length"
|
||||||
>
|
>
|
||||||
<SNONotification
|
<SNONotification
|
||||||
v-for="notification in latestNotifications"
|
v-for="notification in latest"
|
||||||
:key="notification.id"
|
:key="notification.id"
|
||||||
is-small="true"
|
is-small="true"
|
||||||
:notification="notification"
|
:notification="notification"
|
||||||
@ -34,6 +34,7 @@ import { Component, Vue } from 'vue-property-decorator';
|
|||||||
import SNONotification from '@/app/components/notifications/SNONotification.vue';
|
import SNONotification from '@/app/components/notifications/SNONotification.vue';
|
||||||
|
|
||||||
import { RouteConfig } from '@/app/router';
|
import { RouteConfig } from '@/app/router';
|
||||||
|
import { UINotification } from '@/app/types/notifications';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
@ -49,7 +50,7 @@ export default class NotificationsPopup extends Vue {
|
|||||||
/**
|
/**
|
||||||
* Represents first page of notifications.
|
* Represents first page of notifications.
|
||||||
*/
|
*/
|
||||||
public get latestNotifications(): Notification[] {
|
public get latest(): UINotification[] {
|
||||||
return this.$store.state.notificationsModule.latestNotifications;
|
return this.$store.state.notificationsModule.latestNotifications;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,7 +58,7 @@ export default class NotificationsPopup extends Vue {
|
|||||||
* Indicates if popup is smaller than with scroll.
|
* Indicates if popup is smaller than with scroll.
|
||||||
*/
|
*/
|
||||||
public get isCollapsed(): boolean {
|
public get isCollapsed(): boolean {
|
||||||
return this.latestNotifications.length < 4;
|
return this.latest.length < 4;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -30,12 +30,12 @@
|
|||||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||||
|
|
||||||
import { NOTIFICATIONS_ACTIONS } from '@/app/store/modules/notifications';
|
import { NOTIFICATIONS_ACTIONS } from '@/app/store/modules/notifications';
|
||||||
import { Notification } from '@/app/types/notifications';
|
import { UINotification } from '@/app/types/notifications';
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class SNONotification extends Vue {
|
export default class SNONotification extends Vue {
|
||||||
@Prop({default: () => new Notification()})
|
@Prop({default: () => new UINotification()})
|
||||||
public readonly notification: Notification;
|
public readonly notification: UINotification;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* isSmall props indicates if component used in popup.
|
* isSmall props indicates if component used in popup.
|
||||||
|
@ -4,17 +4,19 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Vuex from 'vuex';
|
import Vuex from 'vuex';
|
||||||
|
|
||||||
import { makeNotificationsModule } from '@/app/store/modules/notifications';
|
import { newNotificationsModule } from '@/app/store/modules/notifications';
|
||||||
import { makePayoutModule } from '@/app/store/modules/payout';
|
import { makePayoutModule } from '@/app/store/modules/payout';
|
||||||
import { NotificationsHttpApi } from '@/storagenode/api/notifications';
|
import { NotificationsHttpApi } from '@/storagenode/api/notifications';
|
||||||
import { PayoutHttpApi } from '@/storagenode/api/payout';
|
import { PayoutHttpApi } from '@/storagenode/api/payout';
|
||||||
import { SNOApi } from '@/storagenode/api/storagenode';
|
import { SNOApi } from '@/storagenode/api/storagenode';
|
||||||
|
import { NotificationsService } from '@/storagenode/notifications/service';
|
||||||
import { PayoutService } from '@/storagenode/payouts/service';
|
import { PayoutService } from '@/storagenode/payouts/service';
|
||||||
|
|
||||||
import { appStateModule } from './modules/appState';
|
import { appStateModule } from './modules/appState';
|
||||||
import { makeNodeModule } from './modules/node';
|
import { makeNodeModule } from './modules/node';
|
||||||
|
|
||||||
const notificationsApi = new NotificationsHttpApi();
|
const notificationsApi = new NotificationsHttpApi();
|
||||||
|
const notificationsService = new NotificationsService(notificationsApi);
|
||||||
const payoutApi = new PayoutHttpApi();
|
const payoutApi = new PayoutHttpApi();
|
||||||
const payoutService = new PayoutService(payoutApi);
|
const payoutService = new PayoutService(payoutApi);
|
||||||
const nodeApi = new SNOApi();
|
const nodeApi = new SNOApi();
|
||||||
@ -28,7 +30,7 @@ export const store = new Vuex.Store({
|
|||||||
modules: {
|
modules: {
|
||||||
node: makeNodeModule(nodeApi),
|
node: makeNodeModule(nodeApi),
|
||||||
appStateModule,
|
appStateModule,
|
||||||
notificationsModule: makeNotificationsModule(notificationsApi),
|
notificationsModule: newNotificationsModule(notificationsService),
|
||||||
payoutModule: makePayoutModule(payoutApi, payoutService),
|
payoutModule: makePayoutModule(payoutApi, payoutService),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,12 +1,8 @@
|
|||||||
// Copyright (C) 2019 Storj Labs, Inc.
|
// Copyright (C) 2019 Storj Labs, Inc.
|
||||||
// See LICENSE for copying information.
|
// See LICENSE for copying information.
|
||||||
|
|
||||||
import {
|
import { NotificationsState, UINotification } from '@/app/types/notifications';
|
||||||
Notification,
|
import { NotificationsService } from '@/storagenode/notifications/service';
|
||||||
NotificationsApi,
|
|
||||||
NotificationsCursor,
|
|
||||||
NotificationsState,
|
|
||||||
} from '@/app/types/notifications';
|
|
||||||
|
|
||||||
export const NOTIFICATIONS_MUTATIONS = {
|
export const NOTIFICATIONS_MUTATIONS = {
|
||||||
SET_NOTIFICATIONS: 'SET_NOTIFICATIONS',
|
SET_NOTIFICATIONS: 'SET_NOTIFICATIONS',
|
||||||
@ -24,22 +20,22 @@ export const NOTIFICATIONS_ACTIONS = {
|
|||||||
/**
|
/**
|
||||||
* creates notifications module with all dependencies
|
* creates notifications module with all dependencies
|
||||||
*
|
*
|
||||||
* @param api - payments api
|
* @param service - payments service
|
||||||
*/
|
*/
|
||||||
export function makeNotificationsModule(api: NotificationsApi) {
|
export function newNotificationsModule(service: NotificationsService) {
|
||||||
return {
|
return {
|
||||||
state: new NotificationsState(),
|
state: new NotificationsState(),
|
||||||
mutations: {
|
mutations: {
|
||||||
[NOTIFICATIONS_MUTATIONS.SET_NOTIFICATIONS](state: NotificationsState, notificationsResponse: NotificationsState): void {
|
[NOTIFICATIONS_MUTATIONS.SET_NOTIFICATIONS](state: NotificationsState, notificationsState: NotificationsState): void {
|
||||||
state.notifications = notificationsResponse.notifications;
|
state.notifications = notificationsState.notifications;
|
||||||
state.pageCount = notificationsResponse.pageCount;
|
state.pageCount = notificationsState.pageCount;
|
||||||
state.unreadCount = notificationsResponse.unreadCount;
|
state.unreadCount = notificationsState.unreadCount;
|
||||||
},
|
},
|
||||||
[NOTIFICATIONS_MUTATIONS.SET_LATEST](state: NotificationsState, notificationsResponse: NotificationsState): void {
|
[NOTIFICATIONS_MUTATIONS.SET_LATEST](state: NotificationsState, notificationsState: NotificationsState): void {
|
||||||
state.latestNotifications = notificationsResponse.notifications;
|
state.latestNotifications = notificationsState.notifications;
|
||||||
},
|
},
|
||||||
[NOTIFICATIONS_MUTATIONS.MARK_AS_READ](state: NotificationsState, id: string): void {
|
[NOTIFICATIONS_MUTATIONS.MARK_AS_READ](state: NotificationsState, id: string): void {
|
||||||
state.notifications = state.notifications.map((notification: Notification) => {
|
state.notifications = state.notifications.map((notification: UINotification) => {
|
||||||
if (notification.id === id) {
|
if (notification.id === id) {
|
||||||
notification.markAsRead();
|
notification.markAsRead();
|
||||||
}
|
}
|
||||||
@ -48,7 +44,7 @@ export function makeNotificationsModule(api: NotificationsApi) {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
[NOTIFICATIONS_MUTATIONS.READ_ALL](state: NotificationsState): void {
|
[NOTIFICATIONS_MUTATIONS.READ_ALL](state: NotificationsState): void {
|
||||||
state.notifications = state.notifications.map((notification: Notification) => {
|
state.notifications = state.notifications.map((notification: UINotification) => {
|
||||||
notification.markAsRead();
|
notification.markAsRead();
|
||||||
|
|
||||||
return notification;
|
return notification;
|
||||||
@ -58,24 +54,26 @@ export function makeNotificationsModule(api: NotificationsApi) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
[NOTIFICATIONS_ACTIONS.GET_NOTIFICATIONS]: async function ({commit}: any, cursor: NotificationsCursor): Promise<NotificationsState> {
|
[NOTIFICATIONS_ACTIONS.GET_NOTIFICATIONS]: async function ({commit}: any, pageIndex: number): Promise<void> {
|
||||||
const notificationsResponse = await api.get(cursor);
|
const notificationsResponse = await service.notifications(pageIndex);
|
||||||
|
|
||||||
commit(NOTIFICATIONS_MUTATIONS.SET_NOTIFICATIONS, notificationsResponse);
|
const notifications = notificationsResponse.page.notifications.map(notification => new UINotification(notification));
|
||||||
|
|
||||||
if (cursor.page === 1) {
|
const notificationState = new NotificationsState(notifications, notificationsResponse.page.pageCount, notificationsResponse.unreadCount);
|
||||||
commit(NOTIFICATIONS_MUTATIONS.SET_LATEST, notificationsResponse);
|
|
||||||
|
commit(NOTIFICATIONS_MUTATIONS.SET_NOTIFICATIONS, notificationState);
|
||||||
|
|
||||||
|
if (pageIndex === 1) {
|
||||||
|
commit(NOTIFICATIONS_MUTATIONS.SET_LATEST, notificationState);
|
||||||
}
|
}
|
||||||
|
|
||||||
return notificationsResponse;
|
|
||||||
},
|
},
|
||||||
[NOTIFICATIONS_ACTIONS.MARK_AS_READ]: async function ({commit}: any, id: string): Promise<any> {
|
[NOTIFICATIONS_ACTIONS.MARK_AS_READ]: async function ({commit}: any, id: string): Promise<void> {
|
||||||
await api.read(id);
|
await service.readSingeNotification(id);
|
||||||
|
|
||||||
commit(NOTIFICATIONS_MUTATIONS.MARK_AS_READ, id);
|
commit(NOTIFICATIONS_MUTATIONS.MARK_AS_READ, id);
|
||||||
},
|
},
|
||||||
[NOTIFICATIONS_ACTIONS.READ_ALL]: async function ({commit}: any): Promise<any> {
|
[NOTIFICATIONS_ACTIONS.READ_ALL]: async function ({commit}: any): Promise<void> {
|
||||||
await api.readAll();
|
await service.readAllNotifications();
|
||||||
|
|
||||||
commit(NOTIFICATIONS_MUTATIONS.READ_ALL);
|
commit(NOTIFICATIONS_MUTATIONS.READ_ALL);
|
||||||
},
|
},
|
||||||
|
@ -2,23 +2,39 @@
|
|||||||
// See LICENSE for copying information.
|
// See LICENSE for copying information.
|
||||||
|
|
||||||
import { NotificationIcon } from '@/app/utils/notificationIcons';
|
import { NotificationIcon } from '@/app/utils/notificationIcons';
|
||||||
|
import { Notification, NotificationTypes } from '@/storagenode/notifications/notifications';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds all notifications module state.
|
||||||
|
*/
|
||||||
|
export class NotificationsState {
|
||||||
|
public latestNotifications: UINotification[] = [];
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
public notifications: UINotification[] = [],
|
||||||
|
public pageCount: number = 0,
|
||||||
|
public unreadCount: number = 0,
|
||||||
|
) { }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Describes notification entity.
|
* Describes notification entity.
|
||||||
*/
|
*/
|
||||||
export class Notification {
|
export class UINotification {
|
||||||
public icon: NotificationIcon;
|
public icon: NotificationIcon;
|
||||||
|
public isRead: boolean;
|
||||||
|
public id: string;
|
||||||
|
public senderId: string;
|
||||||
|
public type: NotificationTypes;
|
||||||
|
public title: string;
|
||||||
|
public message: string;
|
||||||
|
public readAt: Date | null;
|
||||||
|
public createdAt: Date;
|
||||||
|
|
||||||
public constructor(
|
public constructor(notification: Partial<UINotification> = new Notification()) {
|
||||||
public id: string = '',
|
Object.assign(this, notification);
|
||||||
public senderId: string = '',
|
|
||||||
public type: NotificationTypes = NotificationTypes.Custom,
|
|
||||||
public title: string = '',
|
|
||||||
public message: string = '',
|
|
||||||
public isRead: boolean = false,
|
|
||||||
public createdAt: Date = new Date(),
|
|
||||||
) {
|
|
||||||
this.setIcon();
|
this.setIcon();
|
||||||
|
this.isRead = !!this.readAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -70,60 +86,3 @@ export class Notification {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Describes all current notifications types.
|
|
||||||
*/
|
|
||||||
export enum NotificationTypes {
|
|
||||||
Custom = 0,
|
|
||||||
AuditCheckFailure = 1,
|
|
||||||
UptimeCheckFailure = 2,
|
|
||||||
Disqualification = 3,
|
|
||||||
Suspension = 4,
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Describes page offset for pagination.
|
|
||||||
*/
|
|
||||||
export class NotificationsCursor {
|
|
||||||
public constructor(
|
|
||||||
public page: number = 0,
|
|
||||||
public limit: number = 7,
|
|
||||||
) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Holds all notifications module state.
|
|
||||||
*/
|
|
||||||
export class NotificationsState {
|
|
||||||
public latestNotifications: Notification[] = [];
|
|
||||||
|
|
||||||
public constructor(
|
|
||||||
public notifications: Notification[] = [],
|
|
||||||
public pageCount: number = 0,
|
|
||||||
public unreadCount: number = 0,
|
|
||||||
) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exposes all notifications-related functionality.
|
|
||||||
*/
|
|
||||||
export interface NotificationsApi {
|
|
||||||
/**
|
|
||||||
* Fetches notifications.
|
|
||||||
* @throws Error
|
|
||||||
*/
|
|
||||||
get(cursor: NotificationsCursor): Promise<NotificationsState>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Marks single notification as read.
|
|
||||||
* @throws Error
|
|
||||||
*/
|
|
||||||
read(id: string): Promise<void>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Marks all notification as read.
|
|
||||||
* @throws Error
|
|
||||||
*/
|
|
||||||
readAll(): Promise<void>;
|
|
||||||
}
|
|
||||||
|
@ -20,7 +20,6 @@ import { APPSTATE_ACTIONS } from '@/app/store/modules/appState';
|
|||||||
import { NODE_ACTIONS } from '@/app/store/modules/node';
|
import { NODE_ACTIONS } from '@/app/store/modules/node';
|
||||||
import { NOTIFICATIONS_ACTIONS } from '@/app/store/modules/notifications';
|
import { NOTIFICATIONS_ACTIONS } from '@/app/store/modules/notifications';
|
||||||
import { PAYOUT_ACTIONS } from '@/app/store/modules/payout';
|
import { PAYOUT_ACTIONS } from '@/app/store/modules/payout';
|
||||||
import { NotificationsCursor } from '@/app/types/notifications';
|
|
||||||
|
|
||||||
@Component ({
|
@Component ({
|
||||||
components: {
|
components: {
|
||||||
@ -43,7 +42,7 @@ export default class Dashboard extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.$store.dispatch(NOTIFICATIONS_ACTIONS.GET_NOTIFICATIONS, new NotificationsCursor(1));
|
await this.$store.dispatch(NOTIFICATIONS_ACTIONS.GET_NOTIFICATIONS, 1);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,7 @@ import VPagination from '@/app/components/VPagination.vue';
|
|||||||
import BackArrowIcon from '@/../static/images/notifications/backArrow.svg';
|
import BackArrowIcon from '@/../static/images/notifications/backArrow.svg';
|
||||||
|
|
||||||
import { NOTIFICATIONS_ACTIONS } from '@/app/store/modules/notifications';
|
import { NOTIFICATIONS_ACTIONS } from '@/app/store/modules/notifications';
|
||||||
import { Notification, NotificationsCursor } from '@/app/types/notifications';
|
import { UINotification } from '@/app/types/notifications';
|
||||||
|
|
||||||
@Component ({
|
@Component ({
|
||||||
components: {
|
components: {
|
||||||
@ -67,7 +67,7 @@ export default class NotificationsArea extends Vue {
|
|||||||
/**
|
/**
|
||||||
* Returns notification of current page.
|
* Returns notification of current page.
|
||||||
*/
|
*/
|
||||||
public get notifications(): Notification[] {
|
public get notifications(): UINotification[] {
|
||||||
return this.$store.state.notificationsModule.notifications;
|
return this.$store.state.notificationsModule.notifications;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,7 +92,7 @@ export default class NotificationsArea extends Vue {
|
|||||||
*/
|
*/
|
||||||
public async onPageClick(index: number): Promise<void> {
|
public async onPageClick(index: number): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await this.$store.dispatch(NOTIFICATIONS_ACTIONS.GET_NOTIFICATIONS, new NotificationsCursor(index));
|
await this.$store.dispatch(NOTIFICATIONS_ACTIONS.GET_NOTIFICATIONS, index);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error.message);
|
console.error(error.message);
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,6 @@ import { APPSTATE_ACTIONS } from '@/app/store/modules/appState';
|
|||||||
import { NODE_ACTIONS } from '@/app/store/modules/node';
|
import { NODE_ACTIONS } from '@/app/store/modules/node';
|
||||||
import { NOTIFICATIONS_ACTIONS } from '@/app/store/modules/notifications';
|
import { NOTIFICATIONS_ACTIONS } from '@/app/store/modules/notifications';
|
||||||
import { PAYOUT_ACTIONS } from '@/app/store/modules/payout';
|
import { PAYOUT_ACTIONS } from '@/app/store/modules/payout';
|
||||||
import { NotificationsCursor } from '@/app/types/notifications';
|
|
||||||
import { PayoutPeriod, TotalHeldAndPaid } from '@/storagenode/payouts/payouts';
|
import { PayoutPeriod, TotalHeldAndPaid } from '@/storagenode/payouts/payouts';
|
||||||
|
|
||||||
@Component ({
|
@Component ({
|
||||||
@ -88,7 +87,7 @@ export default class PayoutArea extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.$store.dispatch(NOTIFICATIONS_ACTIONS.GET_NOTIFICATIONS, new NotificationsCursor(1));
|
await this.$store.dispatch(NOTIFICATIONS_ACTIONS.GET_NOTIFICATIONS, 1);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
// Copyright (C) 2019 Storj Labs, Inc.
|
// Copyright (C) 2019 Storj Labs, Inc.
|
||||||
// See LICENSE for copying information.
|
// See LICENSE for copying information.
|
||||||
|
|
||||||
import { Notification, NotificationsApi, NotificationsCursor, NotificationsState } from '@/app/types/notifications';
|
import {
|
||||||
|
NotificationsApi,
|
||||||
|
NotificationsCursor,
|
||||||
|
NotificationsPage,
|
||||||
|
NotificationsResponse,
|
||||||
|
} from '@/storagenode/notifications/notifications';
|
||||||
import { HttpClient } from '@/storagenode/utils/httpClient';
|
import { HttpClient } from '@/storagenode/utils/httpClient';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -15,10 +20,10 @@ export class NotificationsHttpApi implements NotificationsApi {
|
|||||||
/**
|
/**
|
||||||
* Fetch notifications.
|
* Fetch notifications.
|
||||||
*
|
*
|
||||||
* @returns notifications state
|
* @returns notifications response.
|
||||||
* @throws Error
|
* @throws Error
|
||||||
*/
|
*/
|
||||||
public async get(cursor: NotificationsCursor): Promise<NotificationsState> {
|
public async get(cursor: NotificationsCursor): Promise<NotificationsResponse> {
|
||||||
const path = `${this.ROOT_PATH}/list?page=${cursor.page}&limit=${cursor.limit}`;
|
const path = `${this.ROOT_PATH}/list?page=${cursor.page}&limit=${cursor.limit}`;
|
||||||
const response = await this.client.get(path);
|
const response = await this.client.get(path);
|
||||||
|
|
||||||
@ -27,28 +32,12 @@ export class NotificationsHttpApi implements NotificationsApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const notificationResponse = await response.json();
|
const notificationResponse = await response.json();
|
||||||
let notifications: Notification[] = [];
|
|
||||||
let pageCount: number = 0;
|
|
||||||
let unreadCount: number = 0;
|
|
||||||
|
|
||||||
if (notificationResponse) {
|
return new NotificationsResponse(
|
||||||
notifications = notificationResponse.page.notifications.map(item =>
|
new NotificationsPage(notificationResponse.page.notifications, notificationResponse.page.pageCount),
|
||||||
new Notification(
|
notificationResponse.unreadCount,
|
||||||
item.id,
|
notificationResponse.totalCount,
|
||||||
item.senderId,
|
);
|
||||||
item.type,
|
|
||||||
item.title,
|
|
||||||
item.message,
|
|
||||||
!!item.readAt,
|
|
||||||
new Date(item.createdAt),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
pageCount = notificationResponse.page.pageCount;
|
|
||||||
unreadCount = notificationResponse.unreadCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new NotificationsState(notifications, pageCount, unreadCount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -0,0 +1,88 @@
|
|||||||
|
// Copyright (C) 2020 Storj Labs, Inc.
|
||||||
|
// See LICENSE for copying information.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exposes all notifications-related functionality.
|
||||||
|
*/
|
||||||
|
export interface NotificationsApi {
|
||||||
|
/**
|
||||||
|
* Fetches notifications.
|
||||||
|
* @throws Error
|
||||||
|
*/
|
||||||
|
get(cursor: NotificationsCursor): Promise<NotificationsResponse>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks single notification as read.
|
||||||
|
* @throws Error
|
||||||
|
*/
|
||||||
|
read(id: string): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks all notification as read.
|
||||||
|
* @throws Error
|
||||||
|
*/
|
||||||
|
readAll(): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes notification entity.
|
||||||
|
*/
|
||||||
|
export class Notification {
|
||||||
|
public constructor(
|
||||||
|
public id: string = '',
|
||||||
|
public senderId: string = '',
|
||||||
|
public type: NotificationTypes = NotificationTypes.Custom,
|
||||||
|
public title: string = '',
|
||||||
|
public message: string = '',
|
||||||
|
public readAt: Date | null = null,
|
||||||
|
public createdAt: Date = new Date(),
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes all current notifications types.
|
||||||
|
*/
|
||||||
|
export enum NotificationTypes {
|
||||||
|
Custom = 0,
|
||||||
|
AuditCheckFailure = 1,
|
||||||
|
UptimeCheckFailure = 2,
|
||||||
|
Disqualification = 3,
|
||||||
|
Suspension = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes page offset for pagination.
|
||||||
|
*/
|
||||||
|
export class NotificationsCursor {
|
||||||
|
private DEFAULT_LIMIT: number = 7;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
public page: number = 0,
|
||||||
|
public limit: number = 0,
|
||||||
|
) {
|
||||||
|
if (!this.limit) {
|
||||||
|
this.limit = this.DEFAULT_LIMIT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes response object from server.
|
||||||
|
*/
|
||||||
|
export class NotificationsResponse {
|
||||||
|
public constructor(
|
||||||
|
public page: NotificationsPage = new NotificationsPage(),
|
||||||
|
public unreadCount: number = 0,
|
||||||
|
public totalCount: number = 0,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes page related notification information.
|
||||||
|
*/
|
||||||
|
export class NotificationsPage {
|
||||||
|
public constructor(
|
||||||
|
public notifications: Notification[] = [],
|
||||||
|
public pageCount: number = 0,
|
||||||
|
) {}
|
||||||
|
}
|
43
web/storagenode/src/storagenode/notifications/service.ts
Normal file
43
web/storagenode/src/storagenode/notifications/service.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
// Copyright (C) 2020 Storj Labs, Inc.
|
||||||
|
// See LICENSE for copying information.
|
||||||
|
|
||||||
|
import { NotificationsApi, NotificationsCursor, NotificationsResponse } from '@/storagenode/notifications/notifications';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PayoutService is used to store and handle node paystub information.
|
||||||
|
* PayoutService exposes a business logic related to payouts.
|
||||||
|
*/
|
||||||
|
export class NotificationsService {
|
||||||
|
private readonly api: NotificationsApi;
|
||||||
|
|
||||||
|
public constructor(api: NotificationsApi) {
|
||||||
|
this.api = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch notifications.
|
||||||
|
*
|
||||||
|
* @returns notifications response.
|
||||||
|
* @throws Error
|
||||||
|
*/
|
||||||
|
public async notifications(index: number, limit?: number): Promise<NotificationsResponse> {
|
||||||
|
const cursor = new NotificationsCursor(index, limit);
|
||||||
|
|
||||||
|
return await this.api.get(cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks single notification as read on server.
|
||||||
|
* @param id
|
||||||
|
*/
|
||||||
|
public async readSingeNotification(id: string): Promise<void> {
|
||||||
|
await this.api.read(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks all notifications as read on server.
|
||||||
|
*/
|
||||||
|
public async readAllNotifications(): Promise<void> {
|
||||||
|
await this.api.readAll();
|
||||||
|
}
|
||||||
|
}
|
147
web/storagenode/tests/unit/store/notifications.spec.ts
Normal file
147
web/storagenode/tests/unit/store/notifications.spec.ts
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
// Copyright (C) 2020 Storj Labs, Inc.
|
||||||
|
// See LICENSE for copying information.
|
||||||
|
|
||||||
|
import Vuex from 'vuex';
|
||||||
|
|
||||||
|
import { newNotificationsModule, NOTIFICATIONS_ACTIONS, NOTIFICATIONS_MUTATIONS } from '@/app/store/modules/notifications';
|
||||||
|
import { NotificationsState, UINotification } from '@/app/types/notifications';
|
||||||
|
import { NotificationsHttpApi } from '@/storagenode/api/notifications';
|
||||||
|
import {
|
||||||
|
Notification,
|
||||||
|
NotificationsPage,
|
||||||
|
NotificationsResponse,
|
||||||
|
NotificationTypes,
|
||||||
|
} from '@/storagenode/notifications/notifications';
|
||||||
|
import { NotificationsService } from '@/storagenode/notifications/service';
|
||||||
|
import { createLocalVue } from '@vue/test-utils';
|
||||||
|
|
||||||
|
const Vue = createLocalVue();
|
||||||
|
|
||||||
|
const notificationsApi = new NotificationsHttpApi();
|
||||||
|
const notificationsService = new NotificationsService(notificationsApi);
|
||||||
|
|
||||||
|
const notificationsModule = newNotificationsModule(notificationsService);
|
||||||
|
|
||||||
|
Vue.use(Vuex);
|
||||||
|
|
||||||
|
const store = new Vuex.Store({ modules: { notificationsModule } });
|
||||||
|
|
||||||
|
const state = store.state as any;
|
||||||
|
|
||||||
|
let notifications;
|
||||||
|
|
||||||
|
describe('mutations', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
createLocalVue().use(Vuex);
|
||||||
|
notifications = [
|
||||||
|
new UINotification(new Notification('1', '1', NotificationTypes.Disqualification, 'title1', 'message1', null)),
|
||||||
|
new UINotification(new Notification('2', '1', NotificationTypes.UptimeCheckFailure, 'title2', 'message2', null)),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets notification state', (): void => {
|
||||||
|
const notificationsState = new NotificationsState(notifications, 2, 1);
|
||||||
|
|
||||||
|
store.commit(NOTIFICATIONS_MUTATIONS.SET_NOTIFICATIONS, notificationsState);
|
||||||
|
|
||||||
|
expect(state.notificationsModule.notifications.length).toBe(notifications.length);
|
||||||
|
expect(state.notificationsModule.pageCount).toBe(2);
|
||||||
|
expect(state.notificationsModule.unreadCount).toBe(1);
|
||||||
|
|
||||||
|
store.commit(NOTIFICATIONS_MUTATIONS.SET_LATEST, notificationsState);
|
||||||
|
|
||||||
|
expect(state.notificationsModule.latestNotifications.length).toBe(notifications.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets single notification as read', (): void => {
|
||||||
|
store.commit(NOTIFICATIONS_MUTATIONS.MARK_AS_READ, '1');
|
||||||
|
|
||||||
|
const unreadNotificationsCount = state.notificationsModule.notifications.filter(e => !e.isRead).length;
|
||||||
|
|
||||||
|
expect(unreadNotificationsCount).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets all notification as read', (): void => {
|
||||||
|
store.commit(NOTIFICATIONS_MUTATIONS.READ_ALL);
|
||||||
|
|
||||||
|
const unreadNotificationsCount = state.notificationsModule.notifications.filter(e => !e.isRead).length;
|
||||||
|
|
||||||
|
expect(unreadNotificationsCount).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('actions', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
notifications = [
|
||||||
|
new UINotification(new Notification('1', '1', NotificationTypes.Disqualification, 'title1', 'message1', null)),
|
||||||
|
new UINotification(new Notification('2', '1', NotificationTypes.UptimeCheckFailure, 'title2', 'message2', null)),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws error on failed notifications fetch', async (): Promise<void> => {
|
||||||
|
jest.spyOn(notificationsApi, 'get').mockImplementation(() => { throw new Error(); });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await store.dispatch(NOTIFICATIONS_ACTIONS.GET_NOTIFICATIONS, 1);
|
||||||
|
expect(true).toBe(false);
|
||||||
|
} catch (error) {
|
||||||
|
expect(state.notificationsModule.latestNotifications.length).toBe(notifications.length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('success fetches notifications', async (): Promise<void> => {
|
||||||
|
jest.spyOn(notificationsService, 'notifications')
|
||||||
|
.mockReturnValue(Promise.resolve(new NotificationsResponse(new NotificationsPage(notifications, 1), 2, 1)));
|
||||||
|
|
||||||
|
await store.dispatch(NOTIFICATIONS_ACTIONS.GET_NOTIFICATIONS, 1);
|
||||||
|
|
||||||
|
expect(state.notificationsModule.latestNotifications.length).toBe(notifications.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws error on failed single notification read', async (): Promise<void> => {
|
||||||
|
jest.spyOn(notificationsApi, 'read').mockImplementation(() => { throw new Error(); });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await store.dispatch(NOTIFICATIONS_ACTIONS.MARK_AS_READ, '1');
|
||||||
|
expect(true).toBe(false);
|
||||||
|
} catch (error) {
|
||||||
|
const unreadNotificationsCount = state.notificationsModule.notifications.filter(e => !e.isRead).length;
|
||||||
|
|
||||||
|
expect(unreadNotificationsCount).toBe(notifications.length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('success marks single notification as read', async (): Promise<void> => {
|
||||||
|
jest.spyOn(notificationsService, 'notifications')
|
||||||
|
.mockReturnValue(Promise.resolve(new NotificationsResponse(new NotificationsPage(notifications, 1), 2, 1)));
|
||||||
|
|
||||||
|
await store.dispatch(NOTIFICATIONS_ACTIONS.GET_NOTIFICATIONS, 1);
|
||||||
|
await store.dispatch(NOTIFICATIONS_ACTIONS.MARK_AS_READ, '1');
|
||||||
|
|
||||||
|
const unreadNotificationsCount = state.notificationsModule.notifications.filter(e => !e.isRead).length;
|
||||||
|
|
||||||
|
expect(unreadNotificationsCount).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws error on failed all notifications read', async (): Promise<void> => {
|
||||||
|
jest.spyOn(notificationsApi, 'readAll').mockImplementation(() => { throw new Error(); });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await store.dispatch(NOTIFICATIONS_ACTIONS.READ_ALL);
|
||||||
|
expect(true).toBe(false);
|
||||||
|
} catch (error) {
|
||||||
|
const unreadNotificationsCount = state.notificationsModule.notifications.filter(e => !e.isRead).length;
|
||||||
|
|
||||||
|
expect(unreadNotificationsCount).toBe(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('success marks all notifications as read', async (): Promise<void> => {
|
||||||
|
await store.dispatch(NOTIFICATIONS_ACTIONS.READ_ALL);
|
||||||
|
|
||||||
|
const unreadNotificationsCount = state.notificationsModule.notifications.filter(e => !e.isRead).length;
|
||||||
|
|
||||||
|
expect(unreadNotificationsCount).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user