diff --git a/web/satellite/src/App.vue b/web/satellite/src/App.vue index 0bfb1e0e5..a7ebc0166 100644 --- a/web/satellite/src/App.vue +++ b/web/satellite/src/App.vue @@ -2,39 +2,54 @@ // See LICENSE for copying information. + + diff --git a/web/satellite/src/components/notifications/Notification.vue b/web/satellite/src/components/notifications/Notification.vue index ff3548391..11dd748c9 100644 --- a/web/satellite/src/components/notifications/Notification.vue +++ b/web/satellite/src/components/notifications/Notification.vue @@ -2,43 +2,84 @@ // See LICENSE for copying information. \ No newline at end of file + diff --git a/web/satellite/src/components/notifications/NotificationArea.vue b/web/satellite/src/components/notifications/NotificationArea.vue new file mode 100644 index 000000000..463fea64e --- /dev/null +++ b/web/satellite/src/components/notifications/NotificationArea.vue @@ -0,0 +1,46 @@ +// Copyright (C) 2018 Storj Labs, Inc. +// See LICENSE for copying information. + + + + + + diff --git a/web/satellite/src/store/index.ts b/web/satellite/src/store/index.ts index 4ec18bca0..4cddd58ab 100644 --- a/web/satellite/src/store/index.ts +++ b/web/satellite/src/store/index.ts @@ -4,8 +4,9 @@ import Vue from 'vue'; import Vuex from 'vuex'; -import {authModule} from "@/store/modules/users"; -import {projectsModule} from "@/store/modules/projects"; +import { authModule } from "@/store/modules/users"; +import { projectsModule } from "@/store/modules/projects"; +import { notificationsModule } from '@/store/modules/notifications'; Vue.use(Vuex); @@ -13,8 +14,9 @@ Vue.use(Vuex); const store = new Vuex.Store({ modules: { authModule, - projectsModule - } + projectsModule, + notificationsModule, + }, }); export default store; diff --git a/web/satellite/src/store/modules/notifications.ts b/web/satellite/src/store/modules/notifications.ts new file mode 100644 index 000000000..e17db5f7d --- /dev/null +++ b/web/satellite/src/store/modules/notifications.ts @@ -0,0 +1,87 @@ +// Copyright (C) 2018 Storj Labs, Inc. +// See LICENSE for copying information. + +import { NOTIFICATION_MUTATIONS } from '../mutationConstants'; +import { NOTIFICATION_TYPES } from '@/utils/constants/notification'; +import { DelayedNotification } from '@/utils/entities/DelayedNotification'; + +export const notificationsModule = { + state: { + // Dynamic queue for displaying notifications + notificationQueue: [], + }, + mutations: { + // Mutaion for adding notification to queue + [NOTIFICATION_MUTATIONS.ADD](state: any, notification: any): void { + state.notificationQueue.push(notification); + + // Pause current notification if it`s not first + if (state.notificationQueue.length > 1) { + notification.pause(); + } + }, + // Mutaion for deleting notification to queue + [NOTIFICATION_MUTATIONS.DELETE](state: any): void { + state.notificationQueue[0].pause(); + state.notificationQueue.shift(); + + // Starts next notification in queue if it exist + if (state.notificationQueue[0]) { + state.notificationQueue[0].start(); + } + }, + [NOTIFICATION_MUTATIONS.PAUSE](state: any): void { + state.notificationQueue[0].pause(); + }, + [NOTIFICATION_MUTATIONS.RESUME](state: any): void { + state.notificationQueue[0].start(); + }, + }, + actions: { + // Commits muttation for adding success notification + success: function({commit}: any, message: string): void { + const notification = new DelayedNotification( + () => commit(NOTIFICATION_MUTATIONS.DELETE), + NOTIFICATION_TYPES.SUCCESS, + message, + ); + + commit(NOTIFICATION_MUTATIONS.ADD, notification); + }, + // Commits muttation for adding info notification + notify: function({commit}: any, message: string): void { + + const notification = new DelayedNotification( + () => commit(NOTIFICATION_MUTATIONS.DELETE), + NOTIFICATION_TYPES.NOTIFICATION, + message, + ); + + commit(NOTIFICATION_MUTATIONS.ADD, notification); + }, + // Commits muttation for adding error notification + error: function({commit}: any, message: string): void { + const notification = new DelayedNotification( + () => commit(NOTIFICATION_MUTATIONS.DELETE), + NOTIFICATION_TYPES.ERROR, + message, + ); + + commit(NOTIFICATION_MUTATIONS.ADD, notification); + }, + deleteNotification: function({commit}: any): void { + commit(NOTIFICATION_MUTATIONS.DELETE); + }, + pauseNotification: function({commit}: any): void { + commit(NOTIFICATION_MUTATIONS.PAUSE); + }, + resumeNotification: function({commit}: any): void { + commit(NOTIFICATION_MUTATIONS.RESUME); + }, + }, + getters: { + currentNotification: (state: any) => { + return state.notificationQueue[0] ? state.notificationQueue[0] : null; + }, + }, +}; diff --git a/web/satellite/src/store/mutationConstants.ts b/web/satellite/src/store/mutationConstants.ts index c36a926de..556575834 100644 --- a/web/satellite/src/store/mutationConstants.ts +++ b/web/satellite/src/store/mutationConstants.ts @@ -15,3 +15,10 @@ export const PROJECTS_MUTATIONS = { FETCH: "FETCH_PROJECTS", SELECT: "SELECT_PROJECT", }; + +export const NOTIFICATION_MUTATIONS = { + ADD: 'ADD_NOTIFICATION', + DELETE: 'DELETE_NOTIFICATION', + PAUSE: 'PAUSE_NOTIFICATION', + RESUME: 'RESUME_NOTIFICATION', +}; diff --git a/web/satellite/src/utils/constants/notification.ts b/web/satellite/src/utils/constants/notification.ts new file mode 100644 index 000000000..caa49d767 --- /dev/null +++ b/web/satellite/src/utils/constants/notification.ts @@ -0,0 +1,33 @@ +// Copyright (C) 2018 Storj Labs, Inc. +// See LICENSE for copying information. + +export const NOTIFICATION_IMAGES = { + NOTIFICATION: ` + + + + `, + SUCCESS: ` + + + + `, + ERROR: ` + + + + + `, + CLOSE: ` + + + ` + + +}; + +export const NOTIFICATION_TYPES = { + SUCCESS: 'SUCCESS', + NOTIFICATION: 'NOTIFICATION', + ERROR: 'ERROR', +}; diff --git a/web/satellite/src/utils/entities/DelayedNotification.ts b/web/satellite/src/utils/entities/DelayedNotification.ts new file mode 100644 index 000000000..8c2044557 --- /dev/null +++ b/web/satellite/src/utils/entities/DelayedNotification.ts @@ -0,0 +1,31 @@ +// Copyright (C) 2018 Storj Labs, Inc. +// See LICENSE for copying information. + +export class DelayedNotification { + public type: string; + public message: string; + public id: string; + private timerId: any = null; + private startTime: any; + private remainingTime: any; + private callback: Function; + + constructor(callback: Function, type: string, message: string) { + this.callback = callback; + this.type = type; + this.message = message; + this.id = Date.now().toString(); + this.remainingTime = 3000; + this.start(); + } + + public pause(): void { + clearTimeout(this.timerId); + this.remainingTime -= new Date().getMilliseconds() - this.startTime; + } + + public start(): void { + this.startTime = new Date().getMilliseconds(); + this.timerId = setTimeout(this.callback, this.remainingTime); + } +} diff --git a/web/satellite/src/utils/idGenerator.ts b/web/satellite/src/utils/idGenerator.ts new file mode 100644 index 000000000..80f48b1d3 --- /dev/null +++ b/web/satellite/src/utils/idGenerator.ts @@ -0,0 +1,7 @@ +// Copyright (C) 2018 Storj Labs, Inc. +// See LICENSE for copying information. + +// Custom string id generator +export function getId(): string { + return '_' + Math.random().toString(36).substr(2, 9); +} diff --git a/web/satellite/tests/unit/notifications/Notification.spec.ts b/web/satellite/tests/unit/notifications/Notification.spec.ts new file mode 100644 index 000000000..9eeeedc5e --- /dev/null +++ b/web/satellite/tests/unit/notifications/Notification.spec.ts @@ -0,0 +1,43 @@ +// Copyright (C) 2018 Storj Labs, Inc. +// See LICENSE for copying information. + +import { shallowMount, mount } from '@vue/test-utils'; +import Notification from '@/components/notifications/Notification.vue'; +import { NOTIFICATION_TYPES } from '@/utils/constants/notification'; + +describe('Notification.vue', () => { + + it('renders correctly', () => { + const wrapper = shallowMount(Notification); + + expect(wrapper).toMatchSnapshot(); + }); + + it('renders correctly with props', () => { + const testMessage = 'testMessage'; + + const wrapper = mount(Notification, { + propsData: { + type: NOTIFICATION_TYPES.SUCCESS, + message: testMessage, + }, + }); + + expect(wrapper).toMatchSnapshot(); + expect(wrapper.find(".notification-wrap__text").text()).toMatch(testMessage); + + wrapper.setProps({ + type: NOTIFICATION_TYPES.ERROR, + message: testMessage, + }); + + expect(wrapper).toMatchSnapshot(); + + wrapper.setProps({ + type: NOTIFICATION_TYPES.NOTIFICATION, + message: testMessage, + }); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/web/satellite/tests/unit/notifications/NotificationArea.spec.ts b/web/satellite/tests/unit/notifications/NotificationArea.spec.ts new file mode 100644 index 000000000..86ac5563d --- /dev/null +++ b/web/satellite/tests/unit/notifications/NotificationArea.spec.ts @@ -0,0 +1,44 @@ +// Copyright (C) 2018 Storj Labs, Inc. +// See LICENSE for copying information. + +import { shallowMount, mount } from '@vue/test-utils'; +import NotificationArea from '@/components/notifications/NotificationArea.vue'; +import { NOTIFICATION_TYPES } from '@/utils/constants/notification'; +import { DelayedNotification } from '@/utils/entities/DelayedNotification'; +import Vuex from 'vuex'; +import { createLocalVue } from 'vue-test-utils'; + + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('Notification.vue', () => { + + it('renders correctly', () => { + const wrapper = shallowMount(NotificationArea,{ + computed: { + currentNotification: jest.fn(), + }, + }); + + expect(wrapper).toMatchSnapshot(); + }); + + it('renders correctly with notification', () => { + const testMessage = 'testMessage'; + const notification = new DelayedNotification( + jest.fn(), + NOTIFICATION_TYPES.SUCCESS, + testMessage + ); + + const wrapper = mount(NotificationArea, { + localVue, + computed: { + currentNotification: () => notification, + } + }); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/web/satellite/tests/unit/notifications/__snapshots__/Notification.spec.ts.snap b/web/satellite/tests/unit/notifications/__snapshots__/Notification.spec.ts.snap new file mode 100644 index 000000000..96469c895 --- /dev/null +++ b/web/satellite/tests/unit/notifications/__snapshots__/Notification.spec.ts.snap @@ -0,0 +1,70 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Notification.vue renders correctly 1`] = ` +
+
+
+ + + +
+

+
+
+ + +
+
+`; + +exports[`Notification.vue renders correctly with props 1`] = ` +
+
+
+ + + +
+

testMessage

+
+
+ + +
+
+`; + +exports[`Notification.vue renders correctly with props 2`] = ` +
+
+
+ + + + +
+

testMessage

+
+
+ + +
+
+`; + +exports[`Notification.vue renders correctly with props 3`] = ` +
+
+
+ + + +
+

testMessage

+
+
+ + +
+
+`; diff --git a/web/satellite/tests/unit/notifications/__snapshots__/NotificationArea.spec.ts.snap b/web/satellite/tests/unit/notifications/__snapshots__/NotificationArea.spec.ts.snap new file mode 100644 index 000000000..cd38770fa --- /dev/null +++ b/web/satellite/tests/unit/notifications/__snapshots__/NotificationArea.spec.ts.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Notification.vue renders correctly 1`] = ``; + +exports[`Notification.vue renders correctly with notification 1`] = ` +
+
+
+
+ + + +
+

testMessage

+
+
+ + +
+
+
+`;