web/satellite/vuetify: added notifications

Added notifications to Vuetify app.
Populated existing functionality with notifications.

Issue:
https://github.com/storj/storj/issues/6087

Change-Id: I8339c372bb32fbf1e0ea136c92383494c129b4b6
This commit is contained in:
Vitalii 2023-08-04 16:12:43 +03:00 committed by Vitalii Shpital
parent f57bc81ce7
commit 185ebe3dcf
15 changed files with 216 additions and 33 deletions

View File

@ -3,12 +3,14 @@
<template>
<router-view />
<Notifications />
</template>
<script setup lang="ts">
import { onMounted } from 'vue';
import { useConfigStore } from '@/store/modules/configStore';
import Notifications from '@poc/layouts/default/Notifications.vue';
const configStore = useConfigStore();

View File

@ -99,7 +99,8 @@ import { ProjectInvitationResponse } from '@/types/projects';
import { ProjectRole } from '@/types/projectMembers';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import { AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { AnalyticsErrorEventSource, AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { useNotify } from '@/utils/hooks';
import IconProject from '@poc/components/icons/IconProject.vue';
import IconSettings from '@poc/components/icons/IconSettings.vue';
@ -116,6 +117,7 @@ const emit = defineEmits<{
const analyticsStore = useAnalyticsStore();
const projectsStore = useProjectsStore();
const router = useRouter();
const notify = useNotify();
const isDeclining = ref<boolean>(false);
@ -139,10 +141,18 @@ async function declineInvitation(): Promise<void> {
try {
await projectsStore.respondToInvitation(props.item.id, ProjectInvitationResponse.Decline);
analyticsStore.eventTriggered(AnalyticsEvent.PROJECT_INVITATION_DECLINED);
} catch { /* empty */ }
} catch (error) {
error.message = `Failed to decline project invitation. ${error.message}`;
notify.notifyError(error, AnalyticsErrorEventSource.PROJECT_INVITATION);
}
await projectsStore.getUserInvitations().catch(_ => {});
await projectsStore.getProjects().catch(_ => {});
try {
await projectsStore.getUserInvitations();
await projectsStore.getProjects();
} catch (error) {
error.message = `Failed to reload projects and invitations list. ${error.message}`;
notify.notifyError(error, AnalyticsErrorEventSource.PROJECT_INVITATION);
}
isDeclining.value = false;
}

View File

@ -131,7 +131,8 @@ import { ProjectRole } from '@/types/projectMembers';
import { SHORT_MONTHS_NAMES } from '@/utils/constants/date';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import { AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { AnalyticsErrorEventSource, AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { useNotify } from '@/utils/hooks';
import IconSettings from '@poc/components/icons/IconSettings.vue';
import IconTeam from '@poc/components/icons/IconTeam.vue';
@ -150,6 +151,7 @@ const decliningIds = ref(new Set<string>());
const analyticsStore = useAnalyticsStore();
const projectsStore = useProjectsStore();
const router = useRouter();
const notify = useNotify();
const sortBy = [{ key: 'name', order: 'asc' }];
const headers = [
@ -186,10 +188,18 @@ async function declineInvitation(item: ProjectItemModel): Promise<void> {
try {
await projectsStore.respondToInvitation(item.id, ProjectInvitationResponse.Decline);
analyticsStore.eventTriggered(AnalyticsEvent.PROJECT_INVITATION_DECLINED);
} catch { /* empty */ }
} catch (error) {
error.message = `Failed to decline project invitation. ${error.message}`;
notify.notifyError(error, AnalyticsErrorEventSource.PROJECT_INVITATION);
}
await projectsStore.getUserInvitations().catch(_ => {});
await projectsStore.getProjects().catch(_ => {});
try {
await projectsStore.getUserInvitations();
await projectsStore.getProjects();
} catch (error) {
error.message = `Failed to reload projects and invitations list. ${error.message}`;
notify.notifyError(error, AnalyticsErrorEventSource.PROJECT_INVITATION);
}
decliningIds.value.delete(item.id);
}

View File

@ -93,8 +93,9 @@ import {
import { useLoading } from '@/composables/useLoading';
import { useUsersStore } from '@/store/modules/usersStore';
import { UpdatedUser } from '@/types/users';
import { AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { AnalyticsErrorEventSource, AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import { useNotify } from '@/utils/hooks';
const rules = [
(value: string) => (!!value || 'Can\'t be empty'),
@ -103,6 +104,7 @@ const rules = [
const analyticsStore = useAnalyticsStore();
const userStore = useUsersStore();
const { isLoading, withLoading } = useLoading();
const notify = useNotify();
const props = defineProps<{
modelValue: boolean,
@ -129,8 +131,10 @@ async function onChangeName(): Promise<void> {
try {
await userStore.updateUser(new UpdatedUser(name.value, name.value));
notify.success('Account info successfully updated!');
analyticsStore.eventTriggered(AnalyticsEvent.PROFILE_UPDATED);
} catch (error) {
notify.notifyError(error, AnalyticsErrorEventSource.EDIT_PROFILE_MODAL);
return;
}

View File

@ -120,7 +120,8 @@ import { useConfigStore } from '@/store/modules/configStore';
import { AuthHttpApi } from '@/api/auth';
import { RouteConfig } from '@/types/router';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import { AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { AnalyticsErrorEventSource, AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { useNotify } from '@/utils/hooks';
const DELAY_BEFORE_REDIRECT = 2000; // 2 sec
const auth: AuthHttpApi = new AuthHttpApi();
@ -140,6 +141,7 @@ const analyticsStore = useAnalyticsStore();
const { config } = useConfigStore().state;
const { isLoading, withLoading } = useLoading();
const router = useRouter();
const notify = useNotify();
const props = defineProps<{
modelValue: boolean,
@ -167,8 +169,10 @@ async function onChangePassword(): Promise<void> {
try {
await auth.changePassword(oldPassword.value, newPassword.value);
notify.success('Password successfully changed!');
analyticsStore.eventTriggered(AnalyticsEvent.PASSWORD_CHANGED);
} catch (error) {
notify.notifyError(error, AnalyticsErrorEventSource.CHANGE_PASSWORD_MODAL);
return;
}
@ -180,7 +184,9 @@ async function onChangePassword(): Promise<void> {
// TODO: this reload will be unnecessary once vuetify poc has its own login and/or becomes the primary app
location.reload();
}, DELAY_BEFORE_REDIRECT);
} catch (error) { /* empty */ }
} catch (error) {
notify.notifyError(error, AnalyticsErrorEventSource.CHANGE_PASSWORD_MODAL);
}
emit('update:modelValue', false);
});

View File

@ -162,8 +162,9 @@ import QRCode from 'qrcode';
import { useLoading } from '@/composables/useLoading';
import { useConfigStore } from '@/store/modules/configStore';
import { useUsersStore } from '@/store/modules/usersStore';
import { AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { AnalyticsErrorEventSource, AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import { useNotify } from '@/utils/hooks';
const rules = [
(value: string) => (!!value || 'Can\'t be empty'),
@ -176,6 +177,7 @@ const analyticsStore = useAnalyticsStore();
const { config } = useConfigStore().state;
const usersStore = useUsersStore();
const { isLoading, withLoading } = useLoading();
const notify = useNotify();
const canvas = ref<HTMLCanvasElement>();
const innerContent = ref<Component | null>(null);
@ -240,6 +242,7 @@ function enable(): void {
analyticsStore.eventTriggered(AnalyticsEvent.MFA_ENABLED);
} catch (error) {
notify.notifyError(error, AnalyticsErrorEventSource.ENABLE_MFA_MODAL);
isError.value = true;
}
});
@ -253,7 +256,7 @@ async function showCodes() {
await usersStore.generateUserMFARecoveryCodes();
step.value = 2;
} catch (error) {
/* empty */
notify.notifyError(error, AnalyticsErrorEventSource.ENABLE_MFA_MODAL);
}
}
@ -267,7 +270,7 @@ watch(canvas, async val => {
try {
await QRCode.toCanvas(canvas.value, qrLink.value);
} catch (error) {
/* empty */
notify.notifyError(error, AnalyticsErrorEventSource.ENABLE_MFA_MODAL);
}
});

View File

@ -77,9 +77,9 @@ import {
import { ProjectInvitationResponse } from '@/types/projects';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { AnalyticsErrorEventSource, AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import { RouteConfig } from '@/types/router';
import { useNotify } from '@/utils/hooks';
const props = defineProps<{
modelValue: boolean,
@ -99,6 +99,7 @@ const emit = defineEmits<{
const analyticsStore = useAnalyticsStore();
const projectsStore = useProjectsStore();
const router = useRouter();
const notify = useNotify();
const isAccepting = ref<boolean>(false);
const isDeclining = ref<boolean>(false);
@ -108,6 +109,7 @@ const isDeclining = ref<boolean>(false);
*/
function openProject(): void {
projectsStore.selectProject(props.id);
notify.success('Invite accepted!');
router.push(`/projects/${props.id}/dashboard`);
analyticsStore.pageVisit('/projects/dashboard');
}
@ -118,7 +120,8 @@ function openProject(): void {
async function respondToInvitation(response: ProjectInvitationResponse): Promise<void> {
if (isDeclining.value || isAccepting.value) return;
const isLoading = response === ProjectInvitationResponse.Accept ? isAccepting : isDeclining;
const accepted = response === ProjectInvitationResponse.Accept;
const isLoading = accepted ? isAccepting : isDeclining;
isLoading.value = true;
let success = false;
@ -126,16 +129,26 @@ async function respondToInvitation(response: ProjectInvitationResponse): Promise
await projectsStore.respondToInvitation(props.id, response);
success = true;
analyticsStore.eventTriggered(
response === ProjectInvitationResponse.Accept ?
accepted ?
AnalyticsEvent.PROJECT_INVITATION_ACCEPTED :
AnalyticsEvent.PROJECT_INVITATION_DECLINED,
);
} catch { /* empty */ }
} catch (error) {
const action = accepted ? 'accept' : 'decline';
error.message = `Failed to ${action} project invitation. ${error.message}`;
notify.notifyError(error, AnalyticsErrorEventSource.JOIN_PROJECT_MODAL);
}
await projectsStore.getUserInvitations().catch(_ => {});
await projectsStore.getProjects().catch(_ => { success = false; });
try {
await projectsStore.getUserInvitations();
await projectsStore.getProjects();
} catch (error) {
success = false;
error.message = `Failed to reload projects and invitations list. ${error.message}`;
notify.notifyError(error, AnalyticsErrorEventSource.JOIN_PROJECT_MODAL);
}
if (response === ProjectInvitationResponse.Accept && success) openProject();
if (accepted && success) openProject();
isLoading.value = false;
}

View File

@ -95,9 +95,12 @@ import {
import { useLoading } from '@/composables/useLoading';
import { useUsersStore } from '@/store/modules/usersStore';
import { Duration } from '@/utils/time';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import { useNotify } from '@/utils/hooks';
const usersStore = useUsersStore();
const { isLoading, withLoading } = useLoading();
const notify = useNotify();
const props = defineProps<{
modelValue: boolean,
@ -137,7 +140,9 @@ async function onChangeTimeout(): Promise<void> {
await withLoading(async () => {
try {
await usersStore.updateSettings({ sessionDuration: duration.value.nanoseconds });
notify.success(`Session timeout changed successfully. Your session timeout is ${duration.value?.shortString}.`);
} catch (error) {
notify.notifyError(error, AnalyticsErrorEventSource.EDIT_TIMEOUT_MODAL);
return;
}

View File

@ -16,8 +16,11 @@ import DefaultBar from './AppBar.vue';
import DefaultView from './View.vue';
import { useUsersStore } from '@/store/modules/usersStore';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import { useNotify } from '@/utils/hooks';
const usersStore = useUsersStore();
const notify = useNotify();
/**
* Lifecycle hook after initial render.
@ -26,6 +29,8 @@ const usersStore = useUsersStore();
onBeforeMount(async () => {
try {
await usersStore.getSettings();
} catch (error) { /* empty */ }
} catch (error) {
notify.notifyError(error, AnalyticsErrorEventSource.ALL_PROJECT_DASHBOARD);
}
});
</script>

View File

@ -31,9 +31,12 @@ import { useABTestingStore } from '@/store/modules/abTestingStore';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { useAppStore } from '@poc/store/appStore';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import { useNotify } from '@/utils/hooks';
const router = useRouter();
const route = useRoute();
const notify = useNotify();
const analyticsStore = useAnalyticsStore();
const billingStore = useBillingStore();
@ -75,7 +78,7 @@ watch(() => route.params.projectId, async newId => selectProject(newId as string
/**
* Lifecycle hook after initial render.
* Pre fetches user`s and project information.
* Pre-fetches user`s and project information.
*/
onBeforeMount(async () => {
try {
@ -85,6 +88,7 @@ onBeforeMount(async () => {
usersStore.getSettings(),
]);
} catch (error) {
notify.notifyError(error, AnalyticsErrorEventSource.OVERALL_APP_WRAPPER_ERROR);
setTimeout(async () => await router.push(RouteConfig.Login.path), 1000);
return;
@ -92,11 +96,17 @@ onBeforeMount(async () => {
try {
await billingStore.setupAccount();
} catch (error) { /* empty */ }
} catch (error) {
error.message = `Unable to setup account. ${error.message}`;
notify.notifyError(error, AnalyticsErrorEventSource.OVERALL_APP_WRAPPER_ERROR);
}
try {
await billingStore.getCreditCards();
} catch (error) { /* empty */ }
} catch (error) {
error.message = `Unable to get credit cards. ${error.message}`;
notify.notifyError(error, AnalyticsErrorEventSource.OVERALL_APP_WRAPPER_ERROR);
}
selectProject(route.params.projectId as string);
});

View File

@ -0,0 +1,94 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<v-snackbar
v-model="doNotificationsExist"
position="fixed"
location="top right"
z-index="99999"
variant="text"
contained
>
<v-alert
v-for="item in notifications"
:key="item.id"
closable
variant="elevated"
:title="title(item.type)"
:text="item.message"
:type="getType(item.type)"
rounded="lg"
class="my-2"
border
@mouseover="() => onMouseOver(item.id)"
@mouseleave="() => onMouseLeave(item.id)"
/>
</v-snackbar>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { VAlert, VSnackbar } from 'vuetify/components';
import { useNotificationsStore } from '@/store/modules/notificationsStore';
import { DelayedNotification, NOTIFICATION_TYPES } from '@/types/DelayedNotification';
const notificationsStore = useNotificationsStore();
/**
* Indicates if any notifications are in queue.
*/
const doNotificationsExist = computed((): boolean => {
return notifications.value.length > 0;
});
/**
* Returns all notification queue from store.
*/
const notifications = computed((): DelayedNotification[] => {
return notificationsStore.state.notificationQueue as DelayedNotification[];
});
/**
* Returns notification title based on type.
* @param itemType
*/
function title(itemType: string): string {
const type = getType(itemType);
const [firstLetter, ...rest] = type;
return `${firstLetter.toUpperCase()}${rest.join('')}`;
}
/**
* Returns notification type.
* @param itemType
*/
function getType(itemType: string): string {
switch (itemType) {
case NOTIFICATION_TYPES.SUCCESS:
return 'success';
case NOTIFICATION_TYPES.ERROR:
return 'error';
case NOTIFICATION_TYPES.WARNING:
return 'warning';
default:
return 'info';
}
}
/**
* Forces notification to stay on page on mouse over it.
*/
function onMouseOver(id: string): void {
notificationsStore.pauseNotification(id);
}
/**
* Resume notification flow when mouse leaves notification.
*/
function onMouseLeave(id: string): void {
notificationsStore.resumeNotification(id);
}
</script>

View File

@ -8,6 +8,7 @@
*/
// Plugins
import { App } from 'vue';
import { createPinia, setActivePinia } from 'pinia';
import router from '../router';
@ -15,13 +16,16 @@ import router from '../router';
import { loadFonts } from './webfontloader';
import vuetify from './vuetify';
import NotificatorPlugin from '@/utils/plugins/notificator';
const pinia = createPinia();
setActivePinia(pinia);
export function registerPlugins (app) {
export function registerPlugins(app: App<Element>) {
loadFonts();
app
.use(vuetify)
.use(router)
.use(pinia);
.use(pinia)
.use(NotificatorPlugin);
}

View File

@ -175,6 +175,8 @@ import {
import { User, UserSettings } from '@/types/users';
import { useUsersStore } from '@/store/modules/usersStore';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import { useNotify } from '@/utils/hooks';
import { Duration } from '@/utils/time';
import ChangePasswordDialog from '@poc/components/dialogs/ChangePasswordDialog.vue';
@ -183,6 +185,7 @@ import EnableMFADialog from '@poc/components/dialogs/EnableMFADialog.vue';
import SetSessionTimeoutDialog from '@poc/components/dialogs/SetSessionTimeoutDialog.vue';
const usersStore = useUsersStore();
const notify = useNotify();
const isChangePasswordDialogShown = ref<boolean>(false);
const isChangeNameDialogShown = ref<boolean>(false);
@ -208,7 +211,7 @@ async function toggleEnableMFADialog() {
await usersStore.generateUserMFASecret();
isEnableMFADialogShown.value = true;
} catch (error) {
/* empty */
notify.notifyError(error, AnalyticsErrorEventSource.ACCOUNT_SETTINGS_AREA);
}
}

View File

@ -91,6 +91,8 @@ import { useBucketsStore } from '@/store/modules/bucketsStore';
import { DataStamp, Project, ProjectLimits } from '@/types/projects';
import { Dimensions, Size } from '@/utils/bytesSize';
import { ChartUtils } from '@/utils/chart';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import { useNotify } from '@/utils/hooks';
import PageTitleComponent from '@poc/components/PageTitleComponent.vue';
import PageSubtitleComponent from '@poc/components/PageSubtitleComponent.vue';
@ -106,6 +108,8 @@ const agStore = useAccessGrantsStore();
const billingStore = useBillingStore();
const bucketsStore = useBucketsStore();
const notify = useNotify();
const isDataFetching = ref<boolean>(true);
const chartWidth = ref<number>(0);
const chartContainer = ref<ComponentPublicInstance>();
@ -323,7 +327,9 @@ onMounted(async (): Promise<void> => {
agStore.getAccessGrants(FIRST_PAGE, projectID),
bucketsStore.getBuckets(FIRST_PAGE, projectID),
]);
} catch (error) { /* empty */ } finally {
} catch (error) {
notify.notifyError(error, AnalyticsErrorEventSource.PROJECT_DASHBOARD_PAGE);
} finally {
isDataFetching.value = false;
}
});

View File

@ -134,8 +134,9 @@ import {
import { useProjectMembersStore } from '@/store/modules/projectMembersStore';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { AnalyticsErrorEventSource, AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import { useNotify } from '@/utils/hooks';
import PageTitleComponent from '@poc/components/PageTitleComponent.vue';
import PageSubtitleComponent from '@poc/components/PageSubtitleComponent.vue';
@ -144,6 +145,7 @@ import TeamTableComponent from '@poc/components/TeamTableComponent.vue';
const analyticsStore = useAnalyticsStore();
const pmStore = useProjectMembersStore();
const projectsStore = useProjectsStore();
const notify = useNotify();
const isLoading = ref<boolean>(false);
const dialog = ref<boolean>(false);
@ -167,7 +169,10 @@ async function onAddUsersClick(): Promise<void> {
try {
await pmStore.inviteMembers([email.value], selectedProjectID.value);
} catch (_) {
notify.notify('Invites sent!');
} catch (error) {
error.message = `Error adding project members. ${error.message}`;
notify.notifyError(error, AnalyticsErrorEventSource.ADD_PROJECT_MEMBER_MODAL);
isLoading.value = false;
return;
}
@ -176,7 +181,10 @@ async function onAddUsersClick(): Promise<void> {
try {
await pmStore.getProjectMembers(1, selectedProjectID.value);
} catch (error) { /* empty */ }
} catch (error) {
error.message = `Unable to fetch project members. ${error.message}`;
notify.notifyError(error, AnalyticsErrorEventSource.ADD_PROJECT_MEMBER_MODAL);
}
dialog.value = false;
isLoading.value = false;