From de4a8552486a63c708b8e81c4f9b63be0308ac24 Mon Sep 17 00:00:00 2001 From: Wilfred Asomani Date: Tue, 5 Mar 2024 10:25:41 +0000 Subject: [PATCH] web/satellite: enable versioning on bucket creation This change updates the bucket creation dialog to allow projects with versioning UI config enabled and default versioning not enabled to enable(or not) versioning during bucket creation. Issue: https://github.com/storj/storj/issues/6813 Change-Id: Ic96649317a5dbe4b68884dd299fac5af08b9e7d5 --- web/satellite/src/api/projects.ts | 24 +++ .../components/dialogs/CreateBucketDialog.vue | 159 ++++++++++++++---- web/satellite/src/layouts/default/Default.vue | 5 + .../src/store/modules/bucketsStore.ts | 22 ++- .../src/store/modules/projectsStore.ts | 13 +- web/satellite/src/types/projects.ts | 23 ++- 6 files changed, 202 insertions(+), 44 deletions(-) diff --git a/web/satellite/src/api/projects.ts b/web/satellite/src/api/projects.ts index f25bc324f239..f5a2bb740462 100644 --- a/web/satellite/src/api/projects.ts +++ b/web/satellite/src/api/projects.ts @@ -14,6 +14,7 @@ import { ProjectsStorageBandwidthDaily, ProjectInvitationResponse, Emission, + ProjectConfig, } from '@/types/projects'; import { HttpClient } from '@/utils/httpClient'; import { Time } from '@/utils/time'; @@ -92,6 +93,29 @@ export class ProjectsHttpApi implements ProjectsApi { )); } + /** + * Fetch config for project. + * + * @param projectId - the project's ID + * @returns ProjectConfig + * @throws Error + */ + public async getConfig(projectId: string): Promise { + const response = await this.http.get(`${this.ROOT_PATH}/${projectId}/config`); + const result = await response.json(); + if (response.ok) { + return new ProjectConfig( + result.versioningUIEnabled, + ); + } + + throw new APIError({ + status: response.status, + message: result.error || 'Could not get project config', + requestID: response.headers.get('x-request-id'), + }); + } + /** * Update project name and description. * diff --git a/web/satellite/src/components/dialogs/CreateBucketDialog.vue b/web/satellite/src/components/dialogs/CreateBucketDialog.vue index fdc6b7cc527c..314910087533 100644 --- a/web/satellite/src/components/dialogs/CreateBucketDialog.vue +++ b/web/satellite/src/components/dialogs/CreateBucketDialog.vue @@ -44,39 +44,83 @@ - - - -

Buckets are used to store and organize your files. Enter a bucket name using lowercase characters.

- -
-
-
- + + + + + +

Buckets are used to store and organize your files. Enter a bucket name using lowercase characters.

+ +
+
+
+
+ + + + +

Do you want to enable versioning?

+

Enabling object versioning allows you to preserve, retrieve, and restore previous versions of a file, offering protection against unintentional modifications or deletions.

+ + + Disabled + + + Enabled + + + +

Keep multiple versions of each file in the same bucket. Additional storage costs apply for each version.

+
+ +

Uploading a file with the same name will overwrite the existing file in this bucket.

+
+
+
+
+
+
- Cancel + + {{ step === CreateStep.Name ? 'Cancel' : 'Back' }} + - - Create Bucket + + {{ !allowCreateVersionedBucket || step === CreateStep.Versioning ? 'Create Bucket' : 'Next' }} @@ -87,19 +131,24 @@ diff --git a/web/satellite/src/layouts/default/Default.vue b/web/satellite/src/layouts/default/Default.vue index 4e7d6911e75b..6ea12f2274b1 100644 --- a/web/satellite/src/layouts/default/Default.vue +++ b/web/satellite/src/layouts/default/Default.vue @@ -87,6 +87,11 @@ async function selectProject(urlId: string): Promise { return; } projectsStore.selectProject(project.id); + try { + await projectsStore.getProjectConfig(); + } catch (error) { + notify.notifyError(error, AnalyticsErrorEventSource.OVERALL_APP_WRAPPER_ERROR); + } } watch(() => route.params.id, async newId => { diff --git a/web/satellite/src/store/modules/bucketsStore.ts b/web/satellite/src/store/modules/bucketsStore.ts index c7d6442bcddb..1d03941f6737 100644 --- a/web/satellite/src/store/modules/bucketsStore.ts +++ b/web/satellite/src/store/modules/bucketsStore.ts @@ -9,6 +9,8 @@ import { CreateBucketCommand, DeleteBucketCommand, ListObjectsV2Command, + PutBucketVersioningCommand, + BucketVersioningStatus, } from '@aws-sdk/client-s3'; import { SignatureV4 } from '@smithy/signature-v4'; @@ -225,16 +227,32 @@ export const useBucketsStore = defineStore('buckets', () => { state.enterPassphraseCallback = fn; } - async function createBucket(name: string): Promise { + async function createBucket(name: string, enableBucketVersioning: boolean): Promise { await state.s3Client.send(new CreateBucketCommand({ Bucket: name, })); + if (enableBucketVersioning) { + await state.s3Client.send(new PutBucketVersioningCommand({ + Bucket: name, + VersioningConfiguration: { + Status: BucketVersioningStatus.Enabled, + }, + })); + } } - async function createBucketWithNoPassphrase(name: string): Promise { + async function createBucketWithNoPassphrase(name: string, enableBucketVersioning: boolean): Promise { await state.s3ClientForCreate.send(new CreateBucketCommand({ Bucket: name, })); + if (enableBucketVersioning) { + await state.s3ClientForCreate.send(new PutBucketVersioningCommand({ + Bucket: name, + VersioningConfiguration: { + Status: BucketVersioningStatus.Enabled, + }, + })); + } } async function deleteBucket(name: string): Promise { diff --git a/web/satellite/src/store/modules/projectsStore.ts b/web/satellite/src/store/modules/projectsStore.ts index e569df5df368..83f9f6672a0a 100644 --- a/web/satellite/src/store/modules/projectsStore.ts +++ b/web/satellite/src/store/modules/projectsStore.ts @@ -2,7 +2,7 @@ // See LICENSE for copying information. import { defineStore } from 'pinia'; -import { computed, reactive, readonly } from 'vue'; +import { computed, ComputedRef, reactive, readonly } from 'vue'; import { DataStamp, @@ -18,12 +18,14 @@ import { ProjectInvitation, ProjectInvitationResponse, Emission, + ProjectConfig, } from '@/types/projects'; import { ProjectsHttpApi } from '@/api/projects'; import { DEFAULT_PAGE_LIMIT } from '@/types/pagination'; import { hexToBase64 } from '@/utils/strings'; import { Duration, Time } from '@/utils/time'; import { useConfigStore } from '@/store/modules/configStore'; +import { Versioning } from '@/types/versioning'; const DEFAULT_PROJECT = new Project('', '', '', '', '', true, 0); const DEFAULT_INVITATION = new ProjectInvitation('', '', '', '', new Date()); @@ -35,6 +37,7 @@ export const DEFAULT_PROJECT_LIMITS = readonly(new ProjectLimits()); export class ProjectsState { public projects: Project[] = []; public selectedProject: Project = DEFAULT_PROJECT; + public selectedProjectConfig: ProjectConfig = new ProjectConfig(); public currentLimits: Readonly = DEFAULT_PROJECT_LIMITS; public totalLimits: Readonly = DEFAULT_PROJECT_LIMITS; public cursor: ProjectsCursor = new ProjectsCursor(); @@ -53,6 +56,8 @@ export const useProjectsStore = defineStore('projects', () => { const api: ProjectsApi = new ProjectsHttpApi(); + const versioningUIEnabled: ComputedRef = computed(() => state.selectedProjectConfig.versioningUIEnabled); + function getUsageReportLink(startUTC: Date, endUTC: Date, projectID = ''): string { const since = Time.toUnixTimestamp(startUTC); const before = Time.toUnixTimestamp(endUTC); @@ -187,6 +192,10 @@ export const useProjectsStore = defineStore('projects', () => { state.selectedProject = selected; } + async function getProjectConfig(): Promise { + state.selectedProjectConfig = await api.getConfig(state.selectedProject.id); + } + async function updateProjectName(fieldsToUpdate: ProjectFields): Promise { const project = new ProjectFields( fieldsToUpdate.name, @@ -349,12 +358,14 @@ export const useProjectsStore = defineStore('projects', () => { return { state, + versioningUIEnabled, getProjects, getOwnedProjects, getDailyProjectData, createProject, createDefaultProject, selectProject, + getProjectConfig, updateProjectName, updateProjectDescription, updateProjectStorageLimit, diff --git a/web/satellite/src/types/projects.ts b/web/satellite/src/types/projects.ts index 6c14d62a1e5b..085901a17053 100644 --- a/web/satellite/src/types/projects.ts +++ b/web/satellite/src/types/projects.ts @@ -23,6 +23,14 @@ export interface ProjectsApi { * @throws Error */ get(): Promise; + /** + * Fetch config for project. + * + * @param projectId - the project's ID + * @returns ProjectConfig + * @throws Error + */ + getConfig(projectId: string): Promise; /** * Update project name and description. * @@ -140,14 +148,15 @@ export class Project { public storageUsed: number = 0, public bandwidthUsed: number = 0, ) {} +} - /** - * Returns created date as a local string. - */ - public createdDate(): string { - const createdAt = new Date(this.createdAt); - return createdAt.toLocaleString('en-US', { year: 'numeric', month: '2-digit', day: 'numeric' }); - } +/** + * ProjectConfig is a type, used for project configuration. + */ +export class ProjectConfig { + public constructor( + public versioningUIEnabled: boolean = false, + ) {} } /**