web/satellite: initial setup of bucket's view of objects page

WHAT:
setup of objects store module,
setup of initial bucket's view,
generate special API key that will be used to generate gateway credentials

WHY:
initial setup of future bucket's management view

Change-Id: I0078869b95c04c0b142b2e112e93ff2332e8e90f
This commit is contained in:
Vitalii Shpital 2021-03-18 20:09:04 +02:00
parent 8e1aa4bb74
commit 6ddcacbe78
11 changed files with 385 additions and 168 deletions

View File

@ -46,6 +46,8 @@ export default class AccountDropdown extends Vue {
* Performs logout on backend than clears all user information from store and local storage.
*/
public async onLogoutClick(): Promise<void> {
await this.$router.push(RouteConfig.Login.path);
try {
await this.auth.logout();
} catch (error) {
@ -54,7 +56,6 @@ export default class AccountDropdown extends Vue {
return;
}
await this.$router.push(RouteConfig.Login.path);
await this.$store.dispatch(PM_ACTIONS.CLEAR);
await this.$store.dispatch(PROJECTS_ACTIONS.CLEAR);
await this.$store.dispatch(USER_ACTIONS.CLEAR);

View File

@ -0,0 +1,98 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="buckets-view">
<div class="buckets-view__title-area">
<h1 class="buckets-view__title-area__title">Buckets</h1>
</div>
<div class="buckets-view__loader" v-if="isLoading"></div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { RouteConfig } from '@/router';
import { ACCESS_GRANTS_ACTIONS } from '@/store/modules/accessGrants';
import { OBJECTS_ACTIONS } from '@/store/modules/objects';
@Component
export default class BucketsView extends Vue {
private readonly FILE_BROWSER_AG_NAME: string = 'Web file browser API key';
public isLoading: boolean = true;
/**
* Lifecycle hook after initial render.
* Setup gateway credentials.
*/
public async mounted(): Promise<void> {
if (!this.$route.params.passphrase) {
await this.$router.push(RouteConfig.Objects.path);
return;
}
try {
const accessGrant = await this.$store.dispatch(ACCESS_GRANTS_ACTIONS.CREATE, this.FILE_BROWSER_AG_NAME);
await this.$store.dispatch(OBJECTS_ACTIONS.SET_ACCESS_GRANT, accessGrant);
} catch (error) {
await this.$notify.error(error.message);
return;
}
}
/**
* Lifecycle hook before component destroying.
* Remove temporary created access grant.
*/
public async beforeDestroy(): Promise<void> {
try {
await this.$store.dispatch(ACCESS_GRANTS_ACTIONS.DELETE_BY_NAME_AND_PROJECT_ID, this.FILE_BROWSER_AG_NAME);
} catch (error) {
await this.$notify.error(error.message);
}
}
}
</script>
<style scoped lang="scss">
.buckets-view {
display: flex;
flex-direction: column;
align-items: center;
&__title-area {
margin-bottom: 100px;
width: 100%;
&__title {
font-family: 'font_medium', sans-serif;
font-style: normal;
font-weight: bold;
font-size: 18px;
line-height: 26px;
color: #232b34;
margin: 0;
width: 100%;
text-align: left;
}
}
&__loader {
border: 16px solid #f3f3f3;
border-top: 16px solid #3498db;
border-radius: 50%;
width: 120px;
height: 120px;
animation: spin 2s linear infinite;
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>

View File

@ -3,11 +3,14 @@
<template>
<div class="create-pass">
<GeneratePassphrase
:is-loading="isLoading"
:on-button-click="onNextClick"
:set-parent-passphrase="setPassphrase"
/>
<h1 class="create-pass__title">Objects</h1>
<div class="create-pass__container">
<GeneratePassphrase
:is-loading="isLoading"
:on-button-click="onNextClick"
:set-parent-passphrase="setPassphrase"
/>
</div>
</div>
</template>
@ -54,13 +57,36 @@ export default class CreatePassphrase extends Vue {
this.isLoading = false;
this.$router.push(RouteConfig.UploadFile.path);
this.$router.push({
name: RouteConfig.BucketsManagement.name,
params: {
passphrase: this.passphrase,
},
});
}
}
</script>
<style scoped lang="scss">
.create-pass {
margin-top: 100px;
display: flex;
flex-direction: column;
align-items: center;
&__title {
font-family: 'font_medium', sans-serif;
font-style: normal;
font-weight: bold;
font-size: 18px;
line-height: 26px;
color: #232b34;
margin: 0;
width: 100%;
text-align: left;
}
&__container {
margin-top: 100px;
}
}
</style>

View File

@ -3,61 +3,64 @@
<template>
<div class="enter-pass">
<h1 class="enter-pass__title">Access Data in Browser</h1>
<div class="enter-pass__warning">
<div class="enter-pass__warning__header">
<WarningIcon/>
<p class="enter-pass__warning__header__label">Would you like to access files in your browser?</p>
<h1 class="enter-pass__title">Objects</h1>
<div class="enter-pass__container">
<h1 class="enter-pass__container__title">Access Data in Browser</h1>
<div class="enter-pass__container__warning">
<div class="enter-pass__container__warning__header">
<WarningIcon/>
<p class="enter-pass__container__warning__header__label">Would you like to access files in your browser?</p>
</div>
<p class="enter-pass__container__warning__message">
Entering your encryption passphrase here will share encryption data with your browser.
<a
class="enter-pass__container__warning__message__link"
:href="docsLink"
target="_blank"
rel="noopener norefferer"
>
Learn More
</a>
</p>
</div>
<p class="enter-pass__warning__message">
Entering your encryption passphrase here will share encryption data with your browser.
<a
class="enter-pass__warning__message__link"
:href="docsLink"
target="_blank"
rel="noopener norefferer"
>
Learn More
</a>
</p>
</div>
<label class="enter-pass__textarea" for="enter-pass-textarea">
<p class="enter-pass__textarea__label">Encryption Passphrase</p>
<textarea
class="enter-pass__textarea__input"
:class="{ error: isError }"
id="enter-pass-textarea"
placeholder="Enter encryption passphrase here"
rows="2"
v-model="passphrase"
@input="resetErrors"
/>
</label>
<div class="enter-pass__error" v-if="isError">
<h2 class="enter-pass__error__title">Encryption Passphrase Does not Match</h2>
<p class="enter-pass__error__message">
This passphrase hasnt yet been used in the browser. Please ensure this is the encryption passphrase
used in libulink or the Uplink CLI.
</p>
<label class="enter-pass__error__check-area" :class="{ error: isCheckboxError }" for="error-checkbox">
<input
class="enter-pass__error__check-area__checkbox"
id="error-checkbox"
type="checkbox"
v-model="isCheckboxChecked"
@change="isCheckboxError = false"
>
I acknowledge this passphrase has not been used in this browser before.
<label class="enter-pass__container__textarea" for="enter-pass-textarea">
<p class="enter-pass__container__textarea__label">Encryption Passphrase</p>
<textarea
class="enter-pass__container__textarea__input"
:class="{ error: isError }"
id="enter-pass-textarea"
placeholder="Enter encryption passphrase here"
rows="2"
v-model="passphrase"
@input="resetErrors"
/>
</label>
<div class="enter-pass__container__error" v-if="isError">
<h2 class="enter-pass__container__error__title">Encryption Passphrase Does not Match</h2>
<p class="enter-pass__container__error__message">
This passphrase hasnt yet been used in the browser. Please ensure this is the encryption passphrase
used in libulink or the Uplink CLI.
</p>
<label class="enter-pass__container__error__check-area" :class="{ error: isCheckboxError }" for="error-checkbox">
<input
class="enter-pass__container__error__check-area__checkbox"
id="error-checkbox"
type="checkbox"
v-model="isCheckboxChecked"
@change="isCheckboxError = false"
>
I acknowledge this passphrase has not been used in this browser before.
</label>
</div>
<VButton
class="enter-pass__container__next-button"
label="Access Data"
width="100%"
height="48px"
:on-press="onAccessDataClick"
:is-disabled="!passphrase"
/>
</div>
<VButton
class="enter-pass__next-button"
label="Access Data"
width="100%"
height="48px"
:on-press="onAccessDataClick"
:is-disabled="!passphrase"
/>
</div>
</template>
@ -112,7 +115,12 @@ export default class EnterPassphrase extends Vue {
switch (true) {
case areHashesEqual() ||
!areHashesEqual() && this.isError && this.isCheckboxChecked:
this.$router.push(RouteConfig.UploadFile.path);
this.$router.push({
name: RouteConfig.BucketsManagement.name,
params: {
passphrase: this.passphrase,
},
});
return;
case !areHashesEqual() && this.isError && !this.isCheckboxChecked:
@ -141,114 +149,132 @@ export default class EnterPassphrase extends Vue {
<style scoped lang="scss">
.enter-pass {
padding: 45px 50px 60px 50px;
max-width: 515px;
min-width: 515px;
font-family: 'font_regular', sans-serif;
font-style: normal;
display: flex;
flex-direction: column;
align-items: center;
background-color: #fff;
border-radius: 6px;
margin: 100px 0 30px 0;
&__title {
font-family: 'font_bold', sans-serif;
font-family: 'font_medium', sans-serif;
font-style: normal;
font-weight: bold;
font-size: 22px;
line-height: 27px;
color: #000;
margin: 0 0 30px 0;
font-size: 18px;
line-height: 26px;
color: #232b34;
margin: 0;
width: 100%;
text-align: left;
}
&__warning {
&__container {
padding: 45px 50px 60px 50px;
max-width: 515px;
min-width: 515px;
font-family: 'font_regular', sans-serif;
font-style: normal;
display: flex;
flex-direction: column;
padding: 20px;
width: calc(100% - 40px);
background: #f5f6fa;
border: 1px solid #a9b5c1;
border-radius: 8px;
align-items: center;
background-color: #fff;
border-radius: 6px;
margin: 100px 0 30px 0;
&__header {
&__title {
font-family: 'font_bold', sans-serif;
font-weight: bold;
font-size: 22px;
line-height: 27px;
color: #000;
margin: 0 0 30px 0;
}
&__warning {
display: flex;
align-items: center;
flex-direction: column;
padding: 20px;
width: calc(100% - 40px);
background: #f5f6fa;
border: 1px solid #a9b5c1;
border-radius: 8px;
&__label {
font-style: normal;
font-family: 'font_bold', sans-serif;
&__header {
display: flex;
align-items: center;
&__label {
font-style: normal;
font-family: 'font_bold', sans-serif;
font-size: 16px;
line-height: 19px;
color: #1b2533;
margin: 0 0 0 15px;
}
}
&__message {
font-size: 16px;
line-height: 19px;
color: #1b2533;
margin: 0 0 0 15px;
margin: 10px 0 0 0;
&__link {
font-family: 'font_medium', sans-serif;
color: #0068dc;
text-decoration: underline;
}
}
}
&__message {
font-size: 16px;
line-height: 19px;
color: #1b2533;
margin: 10px 0 0 0;
&__link {
font-family: 'font_medium', sans-serif;
color: #0068dc;
text-decoration: underline;
}
}
}
&__textarea {
width: 100%;
font-size: 16px;
line-height: 21px;
color: #354049;
margin: 26px 0 10px 0;
&__label {
margin: 0 0 8px 0;
}
&__input {
padding: 15px 20px;
resize: none;
width: calc(100% - 42px);
font-size: 14px;
line-height: 25px;
border-radius: 6px;
}
}
&__error {
display: flex;
flex-direction: column;
align-items: flex-start;
color: #ce3030;
&__title {
font-family: 'font_medium', sans-serif;
&__textarea {
width: 100%;
font-size: 16px;
line-height: 21px;
margin: 0 0 5px 0;
color: #354049;
margin: 26px 0 10px 0;
&__label {
margin: 0 0 8px 0;
}
&__input {
padding: 15px 20px;
resize: none;
width: calc(100% - 42px);
font-size: 14px;
line-height: 25px;
border-radius: 6px;
}
}
&__message {
font-weight: normal;
margin: 0 0 20px 0;
}
&__check-area {
margin-bottom: 32px;
font-size: 14px;
line-height: 19px;
color: #1b2533;
&__error {
display: flex;
align-items: center;
cursor: pointer;
flex-direction: column;
align-items: flex-start;
color: #ce3030;
&__checkbox {
margin: 0 10px 0 0;
&__title {
font-family: 'font_medium', sans-serif;
font-size: 16px;
line-height: 21px;
margin: 0 0 5px 0;
}
&__message {
font-weight: normal;
margin: 0 0 20px 0;
}
&__check-area {
margin-bottom: 32px;
font-size: 14px;
line-height: 19px;
color: #1b2533;
display: flex;
align-items: center;
cursor: pointer;
&__checkbox {
margin: 0 10px 0 0;
}
}
}
}

View File

@ -3,9 +3,6 @@
<template>
<div class="objects-area">
<div class="objects-area__header">
<h1 class="objects-area__header__title">Objects</h1>
</div>
<router-view/>
</div>
</template>
@ -55,25 +52,5 @@ export default class ObjectsArea extends Vue {
<style scoped lang="scss">
.objects-area {
padding: 20px 45px;
display: flex;
flex-direction: column;
align-items: center;
&__header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
&__title {
font-family: 'font_medium', sans-serif;
font-style: normal;
font-weight: bold;
font-size: 18px;
line-height: 26px;
color: #232b34;
margin: 0;
}
}
}
</style>

View File

@ -18,6 +18,7 @@ import DetailedHistory from '@/components/account/billing/depositAndBillingHisto
import CreditsHistory from '@/components/account/billing/freeCredits/CreditsHistory.vue';
import SettingsArea from '@/components/account/SettingsArea.vue';
import Page404 from '@/components/errors/Page404.vue';
import BucketsView from '@/components/objects/BucketsView.vue';
import CreatePassphrase from '@/components/objects/CreatePassphrase.vue';
import EnterPassphrase from '@/components/objects/EnterPassphrase.vue';
import ObjectsArea from '@/components/objects/ObjectsArea.vue';
@ -90,6 +91,7 @@ export abstract class RouteConfig {
// objects child paths.
public static CreatePassphrase = new NavigationLink('create-passphrase', 'Objects Create Passphrase');
public static EnterPassphrase = new NavigationLink('enter-passphrase', 'Objects Enter Passphrase');
public static BucketsManagement = new NavigationLink('buckets', 'Buckets Management');
public static UploadFile = new NavigationLink('upload', 'Objects Upload');
}
@ -308,6 +310,12 @@ export const router = new Router({
name: RouteConfig.EnterPassphrase.name,
component: EnterPassphrase,
},
{
path: RouteConfig.BucketsManagement.path,
name: RouteConfig.BucketsManagement.name,
component: BucketsView,
props: true,
},
{
path: RouteConfig.UploadFile.path,
name: RouteConfig.UploadFile.name,

View File

@ -15,6 +15,7 @@ import { AccessGrantsState, makeAccessGrantsModule } from '@/store/modules/acces
import { appStateModule } from '@/store/modules/appState';
import { makeBucketsModule } from '@/store/modules/buckets';
import { makeNotificationsModule, NotificationsState } from '@/store/modules/notifications';
import { makeObjectsModule, ObjectsState } from '@/store/modules/objects';
import { makePaymentsModule, PaymentsState } from '@/store/modules/payments';
import { makeProjectMembersModule, ProjectMembersState } from '@/store/modules/projectMembers';
import { makeProjectsModule, ProjectsState, PROJECTS_MUTATIONS } from '@/store/modules/projects';
@ -46,6 +47,7 @@ class ModulesState {
public paymentsModule: PaymentsState;
public usersModule: User;
public projectsModule: ProjectsState;
public objectsModule: ObjectsState;
}
// Satellite store (vuex)
@ -59,6 +61,7 @@ export const store = new Vuex.Store<ModulesState>({
usersModule: makeUsersModule(authApi),
projectsModule: makeProjectsModule(projectsApi),
bucketUsageModule: makeBucketsModule(bucketsApi),
objectsModule: makeObjectsModule(),
},
});

View File

@ -17,6 +17,7 @@ export const ACCESS_GRANTS_ACTIONS = {
FETCH: 'fetchAccessGrants',
CREATE: 'createAccessGrant',
DELETE: 'deleteAccessGrants',
DELETE_BY_NAME_AND_PROJECT_ID: 'deleteAccessGrantsByNameAndProjectID',
CLEAR: 'clearAccessGrants',
GET_GATEWAY_CREDENTIALS: 'getGatewayCredentials',
SET_ACCESS_GRANTS_WEB_WORKER: 'setAccessGrantsWebWorker',
@ -210,10 +211,15 @@ export function makeAccessGrantsModule(api: AccessGrantsApi): StoreModule<Access
deleteAccessGrants: async function({state}: any): Promise<void> {
await api.delete(state.selectedAccessGrantsIds);
},
getGatewayCredentials: async function({state, commit}: any, accessGrant: string): Promise<void> {
deleteAccessGrantsByNameAndProjectID: async function({state, rootGetters}: any, name: string): Promise<void> {
await api.deleteByNameAndProjectID(name, rootGetters.selectedProject.id);
},
getGatewayCredentials: async function({state, commit}: any, accessGrant: string): Promise<GatewayCredentials> {
const credentials: GatewayCredentials = await api.getGatewayCredentials(accessGrant);
commit(SET_GATEWAY_CREDENTIALS, credentials);
return credentials;
},
setAccessGrantsSearchQuery: function ({commit}, search: string) {
commit(SET_SEARCH_QUERY, search);

View File

@ -0,0 +1,60 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
import { StoreModule } from '@/store';
import { GatewayCredentials } from '@/types/accessGrants';
export const OBJECTS_ACTIONS = {
CLEAR: 'clearObjects',
GET_GATEWAY_CREDENTIALS: 'getGatewayCredentials',
SET_ACCESS_GRANT: 'setAccessGrant',
};
export const OBJECTS_MUTATIONS = {
SET_GATEWAY_CREDENTIALS: 'setGatewayCredentials',
SET_ACCESS_GRANT: 'setAccessGrant',
CLEAR: 'clearObjects',
};
const {
CLEAR,
SET_ACCESS_GRANT,
SET_GATEWAY_CREDENTIALS,
} = OBJECTS_MUTATIONS;
export class ObjectsState {
public accessGrant: string = '';
public gatewayCredentials: GatewayCredentials = new GatewayCredentials();
}
/**
* Creates objects module with all dependencies.
*/
export function makeObjectsModule(): StoreModule<ObjectsState> {
return {
state: new ObjectsState(),
mutations: {
[SET_ACCESS_GRANT](state: ObjectsState, accessGrant: string) {
state.accessGrant = accessGrant;
},
[SET_GATEWAY_CREDENTIALS](state: ObjectsState, credentials: GatewayCredentials) {
state.gatewayCredentials = credentials;
},
[CLEAR](state: ObjectsState) {
state.accessGrant = '';
state.gatewayCredentials = new GatewayCredentials();
},
},
actions: {
setAccessGrant: function({commit}: any, accessGrant: string): void {
commit(SET_ACCESS_GRANT, accessGrant);
},
setGatewayCredentials: function({commit}: any, credentials: GatewayCredentials): void {
commit(SET_GATEWAY_CREDENTIALS, credentials);
},
clearObjects: function ({commit}: any): void {
commit(CLEAR);
},
},
};
}

View File

@ -33,6 +33,14 @@ export interface AccessGrantsApi {
*/
delete(ids: string[]): Promise<void>;
/**
* Delete existing access grant by name and project id
*
* @returns null
* @throws Error
*/
deleteByNameAndProjectID(name: string, projectID: string): Promise<void>;
/**
* Get gateway credentials using access grant
*

View File

@ -32,6 +32,10 @@ export class AccessGrantsMock implements AccessGrantsApi {
return Promise.resolve();
}
deleteByNameAndProjectID(name: string, projectID: string): Promise<void> {
return Promise.resolve();
}
getGatewayCredentials(accessGrant: string): Promise<GatewayCredentials> {
return Promise.resolve(new GatewayCredentials('testCredId', new Date(), 'testAccessKeyId', 'testSecret', 'testEndpoint'));
}