From 3c8f68fed95fbfe98ee4ffcfdbd2fb7ba07ab9a5 Mon Sep 17 00:00:00 2001 From: NickolaiYurchenko Date: Wed, 2 Nov 2022 17:25:44 +0200 Subject: [PATCH] web/satellite: pinia package added added to replace vuex in future buckets, projects and users modules pinia analogues created have no pretty and straight-forward ways to work with option api + ts files so it is better to use with composition api components Change-Id: Ia8acc491c0e76e01bf6d533747d186257680e5c9 --- web/satellite/package-lock.json | 82 ++++- web/satellite/package.json | 1 + web/satellite/src/main.ts | 4 + .../src/store/modules/bucketsStore.ts | 62 ++++ .../src/store/modules/projectsStore.ts | 293 ++++++++++++++++++ web/satellite/src/store/modules/usersStore.ts | 88 ++++++ 6 files changed, 528 insertions(+), 2 deletions(-) create mode 100644 web/satellite/src/store/modules/bucketsStore.ts create mode 100644 web/satellite/src/store/modules/projectsStore.ts create mode 100644 web/satellite/src/store/modules/usersStore.ts diff --git a/web/satellite/package-lock.json b/web/satellite/package-lock.json index 9b490ac19..34e8a9363 100644 --- a/web/satellite/package-lock.json +++ b/web/satellite/package-lock.json @@ -23,6 +23,7 @@ "graphql-tag": "2.12.6", "load-script": "1.0.0", "pbkdf2": "3.1.2", + "pinia": "^2.0.23", "pretty-bytes": "5.6.0", "qrcode": "1.5.0", "stream-browserify": "3.0.0", @@ -4036,6 +4037,11 @@ "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", "dev": true }, + "node_modules/@vue/devtools-api": { + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.4.5.tgz", + "integrity": "sha512-JD5fcdIuFxU4fQyXUu3w2KpAJHzTVdN+p4iOX2lMWSHMOoQdMAcpFLZzm9Z/2nmsoZ1a96QEhZ26e50xLBsgOQ==" + }, "node_modules/@vue/eslint-config-typescript": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-10.0.0.tgz", @@ -14220,6 +14226,56 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pinia": { + "version": "2.0.23", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.0.23.tgz", + "integrity": "sha512-N15hFf4o5STrxpNrib1IEb1GOArvPYf1zPvQVRGOO1G1d74Ak0J0lVyalX/SmrzdT4Q0nlEFjbURsmBmIGUR5Q==", + "dependencies": { + "@vue/devtools-api": "^6.4.4", + "vue-demi": "*" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "@vue/composition-api": "^1.4.0", + "typescript": ">=4.4.4", + "vue": "^2.6.14 || ^3.2.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/pinia/node_modules/vue-demi": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz", + "integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, "node_modules/pirates": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", @@ -17570,7 +17626,7 @@ "version": "4.6.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz", "integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -22135,6 +22191,11 @@ } } }, + "@vue/devtools-api": { + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.4.5.tgz", + "integrity": "sha512-JD5fcdIuFxU4fQyXUu3w2KpAJHzTVdN+p4iOX2lMWSHMOoQdMAcpFLZzm9Z/2nmsoZ1a96QEhZ26e50xLBsgOQ==" + }, "@vue/eslint-config-typescript": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-10.0.0.tgz", @@ -29788,6 +29849,23 @@ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, + "pinia": { + "version": "2.0.23", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.0.23.tgz", + "integrity": "sha512-N15hFf4o5STrxpNrib1IEb1GOArvPYf1zPvQVRGOO1G1d74Ak0J0lVyalX/SmrzdT4Q0nlEFjbURsmBmIGUR5Q==", + "requires": { + "@vue/devtools-api": "^6.4.4", + "vue-demi": "*" + }, + "dependencies": { + "vue-demi": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz", + "integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==", + "requires": {} + } + } + }, "pirates": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", @@ -32225,7 +32303,7 @@ "version": "4.6.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz", "integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==", - "dev": true + "devOptional": true }, "unbox-primitive": { "version": "1.0.2", diff --git a/web/satellite/package.json b/web/satellite/package.json index 74b6aff90..02214b06d 100644 --- a/web/satellite/package.json +++ b/web/satellite/package.json @@ -28,6 +28,7 @@ "graphql-tag": "2.12.6", "load-script": "1.0.0", "pbkdf2": "3.1.2", + "pinia": "2.0.23", "pretty-bytes": "5.6.0", "qrcode": "1.5.0", "stream-browserify": "3.0.0", diff --git a/web/satellite/src/main.ts b/web/satellite/src/main.ts index 6ce6a1420..cce60c86e 100644 --- a/web/satellite/src/main.ts +++ b/web/satellite/src/main.ts @@ -4,6 +4,7 @@ import Vue from 'vue'; import VueClipboard from 'vue-clipboard2'; import VueSanitize from 'vue-sanitize'; +import { createPinia, PiniaVuePlugin } from 'pinia'; import App from './App.vue'; import { router } from './router'; @@ -23,6 +24,8 @@ Vue.config.productionTip = false; Vue.use(new NotificatorPlugin(store)); Vue.use(VueClipboard); Vue.use(VueSanitize); +Vue.use(PiniaVuePlugin); +const pinia = createPinia(); /** * Click outside handlers. @@ -87,5 +90,6 @@ Vue.filter('bytesToBase10String', (amountInBytes: number): string => { new Vue({ router, store, + pinia, render: (h) => h(App), }).$mount('#app'); diff --git a/web/satellite/src/store/modules/bucketsStore.ts b/web/satellite/src/store/modules/bucketsStore.ts new file mode 100644 index 000000000..639af1d83 --- /dev/null +++ b/web/satellite/src/store/modules/bucketsStore.ts @@ -0,0 +1,62 @@ +// Copyright (C) 2022 Storj Labs, Inc. +// See LICENSE for copying information. + +import { defineStore } from 'pinia'; +import { reactive } from 'vue'; + +import { Bucket, BucketCursor, BucketPage, BucketsApi } from '@/types/buckets'; +import { BucketsApiGql } from '@/api/buckets'; +import { useProjectsStore } from '@/store/modules/projectsStore'; + +const BUCKETS_PAGE_LIMIT = 7; +const FIRST_PAGE = 1; + +export class BucketsState { + public allBucketNames: string[] = []; + public cursor: BucketCursor = { limit: BUCKETS_PAGE_LIMIT, search: '', page: FIRST_PAGE }; + public page: BucketPage = { buckets: new Array(), currentPage: 1, pageCount: 1, offset: 0, limit: BUCKETS_PAGE_LIMIT, search: '', totalCount: 0 }; +} + +export const useBucketsStore = defineStore('buckets', () => { + const bucketsState = reactive({ + allBucketNames: [], + cursor: { limit: BUCKETS_PAGE_LIMIT, search: '', page: FIRST_PAGE }, + page: { buckets: new Array(), currentPage: 1, pageCount: 1, offset: 0, limit: BUCKETS_PAGE_LIMIT, search: '', totalCount: 0 }, + }); + + const api: BucketsApi = new BucketsApiGql(); + + function setBucketsSearch(search: string): void { + bucketsState.cursor.search = search; + } + + function clearBucketsState(): void { + bucketsState.allBucketNames = []; + bucketsState.cursor = new BucketCursor('', BUCKETS_PAGE_LIMIT, FIRST_PAGE); + bucketsState.page = new BucketPage([], '', BUCKETS_PAGE_LIMIT, 0, 1, 1, 0); + } + + async function fetchBuckets(page: number): Promise { + const { projectsStore } = useProjectsStore(); + const projectID = projectsStore.selectedProject.id; + const before = new Date(); + bucketsState.cursor.page = page; + + bucketsState.page = await api.get(projectID, before, bucketsState.cursor); + } + + async function fetchAllBucketsNames(): Promise { + const { projectsStore } = useProjectsStore(); + const projectID = projectsStore.selectedProject.id; + + bucketsState.allBucketNames = await api.getAllBucketNames(projectID); + } + + return { + bucketsState, + setBucketsSearch, + clearBucketsState, + fetchBuckets, + fetchAllBucketsNames, + }; +}); diff --git a/web/satellite/src/store/modules/projectsStore.ts b/web/satellite/src/store/modules/projectsStore.ts new file mode 100644 index 000000000..81094238e --- /dev/null +++ b/web/satellite/src/store/modules/projectsStore.ts @@ -0,0 +1,293 @@ +// Copyright (C) 2019 Storj Labs, Inc. +// See LICENSE for copying information. + +import { defineStore } from 'pinia'; +import { computed, reactive } from 'vue'; + +import { + DataStamp, + Project, + ProjectFields, + ProjectLimits, + ProjectsApi, + ProjectsCursor, + ProjectsPage, + ProjectsStorageBandwidthDaily, + ProjectUsageDateRange, +} from '@/types/projects'; +import { ProjectsApiGql } from '@/api/projects'; +import { useUsersStore } from '@/store/modules/usersStore'; + +const defaultSelectedProject = new Project('', '', '', '', '', true, 0); + +export class ProjectsState { + public projects: Project[] = []; + public selectedProject: Project = defaultSelectedProject; + public currentLimits: ProjectLimits = new ProjectLimits(); + public totalLimits: ProjectLimits = new ProjectLimits(); + public cursor: ProjectsCursor = new ProjectsCursor(); + public page: ProjectsPage = new ProjectsPage(); + public allocatedBandwidthChartData: DataStamp[] = []; + public settledBandwidthChartData: DataStamp[] = []; + public storageChartData: DataStamp[] = []; + public chartDataSince: Date = new Date(); + public chartDataBefore: Date = new Date(); +} + +const PROJECT_PAGE_LIMIT = 7; + +export const useProjectsStore = defineStore('projects', () => { + const projectsStore = reactive({ + projects: [], + selectedProject: defaultSelectedProject, + currentLimits: new ProjectLimits(), + totalLimits: new ProjectLimits(), + cursor: new ProjectsCursor(), + page: new ProjectsPage(), + allocatedBandwidthChartData: [], + settledBandwidthChartData: [], + storageChartData: [], + chartDataSince: new Date(), + chartDataBefore: new Date(), + }); + + const api: ProjectsApi = new ProjectsApiGql(); + + async function fetchProjects(): Promise { + const projects = await api.get(); + + setProjects(projects); + + return projects; + } + + function setProjects(projects: Project[]): void { + projectsStore.projects = projects; + + if (!projectsStore.selectedProject.id) { + return; + } + + const projectsCount = projectsStore.projects.length; + + for (let i = 0; i < projectsCount; i++) { + const project = projectsStore.projects[i]; + + if (project.id !== projectsStore.selectedProject.id) { + continue; + } + + projectsStore.selectedProject = project; + + return; + } + + projectsStore.selectedProject = defaultSelectedProject; + } + + async function fetchOwnedProjects(pageNumber: number): Promise { + projectsStore.cursor.page = pageNumber; + projectsStore.cursor.limit = PROJECT_PAGE_LIMIT; + + projectsStore.page = await api.getOwnedProjects(projectsStore.cursor); + } + + async function fetchDailyProjectData(payload: ProjectUsageDateRange): Promise { + const usage: ProjectsStorageBandwidthDaily = await api.getDailyUsage(projectsStore.selectedProject.id, payload.since, payload.before); + + projectsStore.allocatedBandwidthChartData = usage.allocatedBandwidth; + projectsStore.settledBandwidthChartData = usage.settledBandwidth; + projectsStore.storageChartData = usage.storage; + projectsStore.chartDataSince = payload.since; + projectsStore.chartDataBefore = payload.before; + } + + async function createProject(createProjectFields: ProjectFields): Promise { + const createdProject = await api.create(createProjectFields); + + projectsStore.projects.push(createdProject); + + return createdProject.id; + } + + async function createDefaultProject(): Promise { + const UNTITLED_PROJECT_NAME = 'My First Project'; + const UNTITLED_PROJECT_DESCRIPTION = '___'; + const { usersState } = useUsersStore(); + + const project = new ProjectFields( + UNTITLED_PROJECT_NAME, + UNTITLED_PROJECT_DESCRIPTION, + usersState.user.id, + ); + + const createdProjectId = await createProject(project); + + selectProject(createdProjectId); + } + + function selectProject(projectID: string): void { + const selected = projectsStore.projects.find((project: Project) => project.id === projectID); + + if (!selected) { + return; + } + + projectsStore.selectedProject = selected; + } + + async function updateProjectName(fieldsToUpdate: ProjectFields): Promise { + const project = new ProjectFields( + fieldsToUpdate.name, + projectsStore.selectedProject.description, + projectsStore.selectedProject.id, + ); + const limit = new ProjectLimits( + projectsStore.currentLimits.bandwidthLimit, + projectsStore.currentLimits.bandwidthUsed, + projectsStore.currentLimits.storageLimit, + projectsStore.currentLimits.storageUsed, + ); + + await api.update(projectsStore.selectedProject.id, project, limit); + + projectsStore.selectedProject.name = fieldsToUpdate.name; + } + + async function updateProjectDescription(fieldsToUpdate: ProjectFields): Promise { + const project = new ProjectFields( + projectsStore.selectedProject.name, + fieldsToUpdate.description, + projectsStore.selectedProject.id, + ); + const limit = new ProjectLimits( + projectsStore.currentLimits.bandwidthLimit, + projectsStore.currentLimits.bandwidthUsed, + projectsStore.currentLimits.storageLimit, + projectsStore.currentLimits.storageUsed, + ); + await api.update(projectsStore.selectedProject.id, project, limit); + + projectsStore.selectedProject.description = fieldsToUpdate.description; + } + + async function updateProjectStorageLimit(limitsToUpdate: ProjectLimits): Promise { + const project = new ProjectFields( + projectsStore.selectedProject.name, + projectsStore.selectedProject.description, + projectsStore.selectedProject.id, + ); + const limit = new ProjectLimits( + projectsStore.currentLimits.bandwidthLimit, + projectsStore.currentLimits.bandwidthUsed, + limitsToUpdate.storageLimit, + projectsStore.currentLimits.storageUsed, + ); + await api.update(projectsStore.selectedProject.id, project, limit); + + projectsStore.currentLimits.storageLimit = limitsToUpdate.storageLimit; + } + + async function updateProjectBandwidthLimit(limitsToUpdate: ProjectLimits): Promise { + const project = new ProjectFields( + projectsStore.selectedProject.name, + projectsStore.selectedProject.description, + projectsStore.selectedProject.id, + ); + const limit = new ProjectLimits( + limitsToUpdate.bandwidthLimit, + projectsStore.currentLimits.bandwidthUsed, + projectsStore.currentLimits.storageLimit, + projectsStore.currentLimits.storageUsed, + ); + await api.update(projectsStore.selectedProject.id, project, limit); + + projectsStore.currentLimits.bandwidthLimit = limitsToUpdate.bandwidthLimit; + } + + async function deleteProject(projectID: string): Promise { + await api.delete(projectID); + + projectsStore.projects = projectsStore.projects.filter(project => project.id !== projectID); + + if (projectsStore.selectedProject.id === projectID) { + projectsStore.selectedProject = new Project(); + } + } + + async function fetchProjectLimits(projectID: string): Promise { + projectsStore.currentLimits = await api.getLimits(projectID); + } + + async function fetchTotalLimits(): Promise { + projectsStore.totalLimits = await api.getTotalLimits(); + } + + async function getProjectSalt(projectID: string): Promise { + return await api.getSalt(projectID); + } + + function clearProjectState(): void { + projectsStore.projects = []; + projectsStore.selectedProject = defaultSelectedProject; + projectsStore.currentLimits = new ProjectLimits(); + projectsStore.totalLimits = new ProjectLimits(); + projectsStore.storageChartData = []; + projectsStore.allocatedBandwidthChartData = []; + projectsStore.settledBandwidthChartData = []; + projectsStore.chartDataSince = new Date(); + projectsStore.chartDataBefore = new Date(); + } + + const projects = computed(() => { + return projectsStore.projects.map((project: Project) => { + if (project.id === projectsStore.selectedProject.id) { + project.isSelected = true; + } + + return project; + }); + }); + + const projectsWithoutSelected = computed(() => { + return projectsStore.projects.filter((project: Project) => { + return project.id !== projectsStore.selectedProject.id; + }); + }); + + const projectsCount = computed(() => { + let projectsCount = 0; + + const { usersState } = useUsersStore(); + + projectsStore.projects.forEach((project: Project) => { + if (project.ownerId === usersState.user.id) { + projectsCount++; + } + }); + + return projectsCount; + }); + + return { + projectsStore, + fetchProjects, + fetchOwnedProjects, + fetchDailyProjectData, + createProject, + createDefaultProject, + selectProject, + updateProjectName, + updateProjectDescription, + updateProjectStorageLimit, + updateProjectBandwidthLimit, + deleteProject, + fetchProjectLimits, + fetchTotalLimits, + getProjectSalt, + clearProjectState, + projects, + projectsWithoutSelected, + projectsCount, + }; +}); diff --git a/web/satellite/src/store/modules/usersStore.ts b/web/satellite/src/store/modules/usersStore.ts new file mode 100644 index 000000000..a8fc82b0f --- /dev/null +++ b/web/satellite/src/store/modules/usersStore.ts @@ -0,0 +1,88 @@ +// Copyright (C) 2022 Storj Labs, Inc. +// See LICENSE for copying information. + +import { defineStore } from 'pinia'; +import { computed, reactive } from 'vue'; + +import { DisableMFARequest, UpdatedUser, User, UsersApi } from '@/types/users'; +import { MetaUtils } from '@/utils/meta'; +import { AuthHttpApi } from '@/api/auth'; + +export class UsersState { + public user: User = new User(); + public userMFASecret = ''; + public userMFARecoveryCodes: string[] = []; +} + +export const useUsersStore = defineStore('users', () => { + const usersState = reactive({ + user: new User(), + userMFASecret: '', + userMFARecoveryCodes: [], + }); + + const userName = computed(() => { + return usersState.user.getFullName(); + }); + + const api: UsersApi = new AuthHttpApi(); + + async function updateUserInfo(userInfo: UpdatedUser): Promise { + await api.update(userInfo); + + usersState.user.fullName = userInfo.fullName; + usersState.user.shortName = userInfo.shortName; + } + + async function fetchUserInfo(): Promise { + const user = await api.get(); + + usersState.user = user; + + if (user.projectLimit === 0) { + const limitFromConfig = MetaUtils.getMetaContent('default-project-limit'); + + usersState.user.projectLimit = parseInt(limitFromConfig); + + return; + } + + usersState.user.projectLimit = user.projectLimit; + } + + async function disableUserMFA(request: DisableMFARequest): Promise { + await api.disableUserMFA(request.passcode, request.recoveryCode); + } + + async function enableUserMFA(passcode: string): Promise { + await api.enableUserMFA(passcode); + } + + async function generateUserMFASecret(): Promise { + usersState.userMFASecret = await api.generateUserMFASecret(); + } + + async function generateUserMFARecoveryCodes(): Promise { + const codes = await api.generateUserMFARecoveryCodes(); + + usersState.userMFARecoveryCodes = codes; + usersState.user.mfaRecoveryCodeCount = codes.length; + } + + function clearUserInfo() { + usersState.user = new User(); + usersState.user.projectLimit = 1; + } + + return { + usersState, + userName, + updateUserInfo, + fetchUserInfo, + disableUserMFA, + enableUserMFA, + generateUserMFASecret, + generateUserMFARecoveryCodes, + clearUserInfo, + }; +});