web/satellite: buckets view for objects page

WHAT:
buckets management view for objects page

WHY:
to be able to create and delete buckets

Change-Id: I6df986b52928433f7a0a4c4772d3064c4f1a1516
This commit is contained in:
Vitalii Shpital 2021-03-23 22:23:27 +02:00
parent ebf6bee0d4
commit c4b2d76d1c
19 changed files with 691 additions and 30 deletions

View File

@ -60,6 +60,11 @@ func (keys *APIKeys) DeleteByNameAndProjectID(w http.ResponseWriter, r *http.Req
return
}
if console.ErrNoAPIKey.Has(err) {
keys.serveJSONError(w, http.StatusNoContent, err)
return
}
keys.serveJSONError(w, http.StatusInternalServerError, err)
return
}
@ -67,14 +72,18 @@ func (keys *APIKeys) DeleteByNameAndProjectID(w http.ResponseWriter, r *http.Req
// serveJSONError writes JSON error to response output stream.
func (keys *APIKeys) serveJSONError(w http.ResponseWriter, status int, err error) {
w.WriteHeader(status)
if status == http.StatusNoContent {
return
}
if status == http.StatusInternalServerError {
keys.log.Error("returning internal server error to client", zap.Int("code", status), zap.Error(err))
} else {
keys.log.Debug("returning error to client", zap.Int("code", status), zap.Error(err))
}
w.WriteHeader(status)
var response struct {
Error string `json:"error"`
}

View File

@ -272,7 +272,7 @@ func (server *Server) appHandler(w http.ResponseWriter, r *http.Request) {
cspValues := []string{
"default-src 'self'",
"connect-src 'self' api.segment.io *.google-analytics.com " + server.config.GatewayCredentialsRequestURL,
"connect-src 'self' api.segment.io *.google-analytics.com *.tardigradeshare.io " + server.config.GatewayCredentialsRequestURL,
"frame-ancestors " + server.config.FrameAncestors,
"frame-src 'self' *.stripe.com *.googletagmanager.com",
"img-src 'self' data: *.customer.io *.googletagmanager.com *.google-analytics.com",

View File

@ -49,6 +49,7 @@ const (
passwordIncorrectErrMsg = "Your password needs at least %d characters long"
projectOwnerDeletionForbiddenErrMsg = "%s is a project owner and can not be deleted"
apiKeyWithNameExistsErrMsg = "An API Key with this name already exists in this project, please use a different name"
apiKeyWithNameDoesntExistErrMsg = "An API Key with this name doesn't exist in this project."
teamMemberDoesNotExistErrMsg = `There is no account on this Satellite for the user(s) you have entered.
Please add team members with active accounts`
@ -74,6 +75,9 @@ var (
// ErrEmailUsed is error type that occurs on repeating auth attempts with email.
ErrEmailUsed = errs.Class("email used")
// ErrNoAPIKey is error type that occurs when there is no api key found.
ErrNoAPIKey = errs.Class("no api key found")
)
// Service is handling accounts related logic.
@ -1346,7 +1350,7 @@ func (s *Service) DeleteAPIKeyByNameAndProjectID(ctx context.Context, name strin
key, err := s.store.APIKeys().GetByNameAndProjectID(ctx, name, projectID)
if err != nil {
return Error.Wrap(err)
return ErrNoAPIKey.New(apiKeyWithNameDoesntExistErrMsg)
}
err = s.store.APIKeys().Delete(ctx, key.ID)

View File

@ -141,7 +141,7 @@ export class AccessGrantsApiGql extends BaseGql implements AccessGrantsApi {
const path = `${this.ROOT_PATH}/delete-by-name?name=${name}&projectID=${projectID}`;
const response = await this.client.delete(path);
if (response.ok) {
if (response.ok || response.status === 204) {
return;
}

View File

@ -9,6 +9,7 @@
<h3 v-if="!error" class="label-container__main__label">{{label}}</h3>
<h3 v-if="!error" class="label-container__main__label add-label">{{additionalLabel}}</h3>
<h3 class="label-container__main__error" v-if="error">{{error}}</h3>
<div v-if="isLoading" class="loader"/>
</div>
<h3 v-if="isLimitShown" class="label-container__limit">{{currentLimit}}/{{maxSymbols}}</h3>
</div>
@ -69,6 +70,8 @@ export default class HeaderedInput extends HeaderlessInput {
private readonly isLimitShown: boolean;
@Prop({default: false})
private readonly isMultiline: boolean;
@Prop({default: false})
private readonly isLoading: boolean;
public value: string;
@ -160,4 +163,19 @@ export default class HeaderedInput extends HeaderlessInput {
margin-left: 5px;
color: rgba(56, 75, 101, 0.4);
}
.loader {
margin-left: 10px;
border: 5px solid #f3f3f3;
border-top: 5px solid #3498db;
border-radius: 50%;
width: 15px;
height: 15px;
animation: spin 2s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>

View File

@ -93,8 +93,8 @@ export default class HeaderlessInput extends Vue {
protected readonly error: string;
@Prop({default: Number.MAX_SAFE_INTEGER})
protected readonly maxSymbols: number;
@Prop({default: []})
protected readonly optionsList: [string];
@Prop({default: () => []})
protected readonly optionsList: string[];
@Prop({default: false})
protected optionsShown: boolean;
@Prop({default: false})

View File

@ -0,0 +1,153 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="bucket-item">
<div class="bucket-item__name">
<BucketIcon/>
<p class="bucket-item__name__value">{{itemData.Name}}</p>
</div>
<p class="bucket-item__date">{{formattedDate}}</p>
<div class="bucket-item__functional" @click.stop="openDropdown(dropdownKey)" v-click-outside="closeDropdown">
<DotsIcon/>
<div ref="dropdown" class="bucket-item__functional__dropdown" v-if="isDropdownOpen">
<div class="bucket-item__functional__dropdown__item" @click.stop="onDeleteClick">
<DeleteIcon/>
<p class="bucket-item__functional__dropdown__item__label">Delete</p>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import ObjectsPopup from '@/components/objects/ObjectsPopup.vue';
import BucketIcon from '@/../static/images/objects/bucketItem.svg';
import DeleteIcon from '@/../static/images/objects/delete.svg';
import DotsIcon from '@/../static/images/objects/dots.svg';
import { Bucket } from '@aws-sdk/client-s3';
@Component({
components: {
BucketIcon,
DotsIcon,
DeleteIcon,
ObjectsPopup,
},
})
export default class BucketItem extends Vue {
@Prop({ default: null })
public readonly itemData: Bucket;
@Prop({ default: () => '' })
public readonly showDeleteBucketPopup;
@Prop({ default: () => '' })
public readonly openDropdown;
@Prop({ default: false })
public readonly isDropdownOpen: boolean;
@Prop({ default: -1 })
public readonly dropdownKey: number;
public isRequestProcessing: boolean = false;
public errorMessage: string = '';
/**
* Returns formatted date.
*/
public get formattedDate(): string | undefined {
return this.itemData.CreationDate?.toLocaleString();
}
/**
* Closes dropdown.
*/
public closeDropdown(): void {
this.openDropdown(-1);
}
/**
* Holds on delete click logic.
*/
public onDeleteClick(): void {
this.showDeleteBucketPopup(this.itemData.Name);
this.closeDropdown();
}
}
</script>
<style scoped lang="scss">
.bucket-item {
font-family: 'font_regular', sans-serif;
font-style: normal;
display: flex;
align-items: center;
padding: 25px 20px;
width: calc(100% - 40px);
font-weight: normal;
font-size: 14px;
line-height: 19px;
color: #1b2533;
cursor: pointer;
&__name {
display: flex;
align-items: center;
width: 70%;
&__value {
margin: 0 0 0 17px;
}
}
&__date {
width: 30%;
margin: 0;
}
&__functional {
padding: 0 10px;
position: relative;
cursor: pointer;
&__dropdown {
position: absolute;
top: 25px;
right: 15px;
background: #fff;
box-shadow: 0 20px 34px rgba(10, 27, 44, 0.28);
border-radius: 6px;
width: 255px;
padding: 10px 0;
z-index: 100;
&__item {
display: flex;
align-items: center;
padding: 20px 25px;
width: calc(100% - 50px);
&__label {
margin: 0 0 0 10px;
}
&:hover {
background-color: #f4f5f7;
font-family: 'font_medium', sans-serif;
.bucket-delete-path {
fill: #0068dc;
stroke: #0068dc;
}
}
}
}
}
&:hover {
background-color: #e6e9ef;
}
}
</style>

View File

@ -5,40 +5,105 @@
<div class="buckets-view">
<div class="buckets-view__title-area">
<h1 class="buckets-view__title-area__title">Buckets</h1>
<div class="buckets-view__title-area__button" @click="showCreateBucketPopup">
<BucketIcon/>
<p class="buckets-view__title-area__button__label">New Bucket</p>
</div>
</div>
<div class="buckets-view__loader" v-if="isLoading"/>
<p class="buckets-view__no-buckets" v-if="!(isLoading || buckets.length)">No Buckets</p>
<div class="buckets-view__list" v-if="!isLoading && buckets.length">
<div class="buckets-view__list__sorting-header">
<p class="buckets-view__list__sorting-header__name">Name</p>
<p class="buckets-view__list__sorting-header__date">Date Added</p>
<p class="buckets-view__list__sorting-header__empty"/>
</div>
<div class="buckets-view__list__item" v-for="(bucket, key) in buckets" :key="key" @click.stop="openBucket">
<BucketItem
:item-data="bucket"
:show-delete-bucket-popup="showDeleteBucketPopup"
:dropdown-key="key"
:open-dropdown="openDropdown"
:is-dropdown-open="activeDropdown === key"
/>
</div>
</div>
<ObjectsPopup
v-if="isCreatePopupVisible"
@setName="setCreateBucketName"
@close="hideCreateBucketPopup"
:on-click="onCreateBucketClick"
title="Create Bucket"
sub-title="Buckets are simply containers that store objects and their metadata within a project."
button-label="Create Bucket"
:error-message="errorMessage"
:is-loading="isRequestProcessing"
/>
<ObjectsPopup
v-if="isDeletePopupVisible"
@setName="setDeleteBucketName"
@close="hideDeleteBucketPopup"
:on-click="onDeleteBucketClick"
title="Are you sure?"
sub-title="Deleting this bucket will delete all metadata related to this bucket."
button-label="Confirm Delete Bucket"
:default-input-value="deleteBucketName"
:error-message="errorMessage"
:is-loading="isRequestProcessing"
/>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import BucketItem from '@/components/objects/BucketItem.vue';
import ObjectsPopup from '@/components/objects/ObjectsPopup.vue';
import BucketIcon from '@/../static/images/objects/bucket.svg';
import { RouteConfig } from '@/router';
import { ACCESS_GRANTS_ACTIONS } from '@/store/modules/accessGrants';
import { OBJECTS_ACTIONS } from '@/store/modules/objects';
import { AccessGrant, GatewayCredentials } from '@/types/accessGrants';
import { MetaUtils } from '@/utils/meta';
import { Bucket } from '@aws-sdk/client-s3';
@Component
@Component({
components: {
BucketIcon,
ObjectsPopup,
BucketItem,
},
})
export default class BucketsView extends Vue {
private readonly FILE_BROWSER_AG_NAME: string = 'Web file browser API key';
private worker: Worker;
private grantWithPermissions: string = '';
private accessGrant: string = '';
private createBucketName: string = '';
private deleteBucketName: string = '';
public isLoading: boolean = true;
public isCreatePopupVisible: boolean = false;
public isDeletePopupVisible: boolean = false;
public isRequestProcessing: boolean = false;
public errorMessage: string = '';
public activeDropdown: number = -1;
/**
* 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);
if (!this.$store.state.objectsModule.passphrase) {
await this.$router.push(RouteConfig.Objects.with(RouteConfig.EnterPassphrase).path);
return;
}
await this.removeTemporaryAccessGrant();
try {
const cleanAPIKey: AccessGrant = await this.$store.dispatch(ACCESS_GRANTS_ACTIONS.CREATE, this.FILE_BROWSER_AG_NAME);
@ -96,6 +161,11 @@ export default class BucketsView extends Vue {
const gatewayCredentials: GatewayCredentials = await this.$store.dispatch(ACCESS_GRANTS_ACTIONS.GET_GATEWAY_CREDENTIALS, {accessGrant: this.accessGrant, optionalURL: gatewayURL});
await this.$store.dispatch(OBJECTS_ACTIONS.SET_GATEWAY_CREDENTIALS, gatewayCredentials);
await this.$store.dispatch(OBJECTS_ACTIONS.SET_S3_CLIENT);
await this.$store.dispatch(OBJECTS_ACTIONS.FETCH_BUCKETS);
this.isLoading = false;
if (!this.buckets.length) this.showCreateBucketPopup();
}, 1000);
}, 1000);
} catch (error) {
@ -110,12 +180,130 @@ export default class BucketsView extends Vue {
* Remove temporary created access grant.
*/
public async beforeDestroy(): Promise<void> {
await this.removeTemporaryAccessGrant();
}
/**
* Holds create bucket click logic.
*/
public async onCreateBucketClick(): Promise<void> {
if (this.isRequestProcessing) return;
if (!this.createBucketName) {
this.errorMessage = 'Bucket name can\'t be empty';
}
this.isRequestProcessing = true;
try {
await this.$store.dispatch(OBJECTS_ACTIONS.CREATE_BUCKET, this.createBucketName);
await this.$store.dispatch(OBJECTS_ACTIONS.FETCH_BUCKETS);
} catch (error) {
await this.$notify.error(error.message);
}
this.isRequestProcessing = false;
this.createBucketName = '';
this.hideCreateBucketPopup();
}
/**
* Holds delete bucket click logic.
*/
public async onDeleteBucketClick(): Promise<void> {
if (this.isRequestProcessing) return;
if (!this.deleteBucketName) {
this.errorMessage = 'Bucket name can\'t be empty';
}
this.isRequestProcessing = true;
try {
await this.$store.dispatch(OBJECTS_ACTIONS.DELETE_BUCKET, this.deleteBucketName);
await this.$store.dispatch(OBJECTS_ACTIONS.FETCH_BUCKETS);
} catch (error) {
await this.$notify.error(error.message);
}
this.isRequestProcessing = false;
this.deleteBucketName = '';
this.hideDeleteBucketPopup();
}
/**
* Opens utils dropdown.
*/
public openDropdown(key: number): void {
this.activeDropdown = key;
}
/**
* Makes delete bucket popup visible.
*/
public showDeleteBucketPopup(name: string): void {
this.deleteBucketName = name;
this.isDeletePopupVisible = true;
}
/**
* Hides delete bucket popup.
*/
public hideDeleteBucketPopup(): void {
this.isDeletePopupVisible = false;
}
/**
* Set delete bucket name form input.
*/
public setDeleteBucketName(name: string): void {
this.errorMessage = '';
this.deleteBucketName = name;
}
/**
* Makes create bucket popup visible.
*/
public showCreateBucketPopup(): void {
this.isCreatePopupVisible = true;
}
/**
* Hides create bucket popup.
*/
public hideCreateBucketPopup(): void {
this.isCreatePopupVisible = false;
}
/**
* Set create bucket name form input.
*/
public setCreateBucketName(name: string): void {
this.errorMessage = '';
this.createBucketName = name;
}
/**
* Removes temporary created access grant.
*/
public async removeTemporaryAccessGrant(): 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);
}
}
public openBucket(): void {
this.$router.push(RouteConfig.Objects.with(RouteConfig.UploadFile).path);
}
/**
* Returns fetched buckets from store.
*/
public get buckets(): Bucket[] {
return this.$store.state.objectsModule.buckets;
}
}
</script>
@ -124,14 +312,18 @@ export default class BucketsView extends Vue {
display: flex;
flex-direction: column;
align-items: center;
font-family: 'font_regular', sans-serif;
font-style: normal;
background-color: #f5f6fa;
&__title-area {
margin-bottom: 100px;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
&__title {
font-family: 'font_medium', sans-serif;
font-style: normal;
font-weight: bold;
font-size: 18px;
line-height: 26px;
@ -140,9 +332,33 @@ export default class BucketsView extends Vue {
width: 100%;
text-align: left;
}
&__button {
width: 154px;
height: 46px;
display: flex;
align-items: center;
justify-content: center;
background-color: #0068dc;
border-radius: 4px;
cursor: pointer;
&__label {
font-weight: normal;
font-size: 12px;
line-height: 17px;
color: #fff;
margin: 0 0 0 5px;
}
&:hover {
background-color: #0000c2;
}
}
}
&__loader {
margin-top: 100px;
border: 16px solid #f3f3f3;
border-top: 16px solid #3498db;
border-radius: 50%;
@ -150,6 +366,49 @@ export default class BucketsView extends Vue {
height: 120px;
animation: spin 2s linear infinite;
}
&__no-buckets {
width: 100%;
text-align: center;
font-size: 30px;
line-height: 42px;
margin: 100px 0 0 0;
}
&__list {
margin-top: 40px;
width: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
padding-bottom: 100px;
&__sorting-header {
display: flex;
align-items: center;
padding: 0 20px 5px 20px;
width: calc(100% - 40px);
font-weight: bold;
font-size: 14px;
line-height: 20px;
color: #768394;
border-bottom: 1px solid rgba(169, 181, 193, 0.4);
&__name {
width: calc(70% - 16px);
margin: 0;
}
&__date {
width: 30%;
margin: 0;
}
&__empty {
margin: 0;
}
}
}
}
@keyframes spin {

View File

@ -21,6 +21,7 @@ import { Component, Vue } from 'vue-property-decorator';
import GeneratePassphrase from '@/components/common/GeneratePassphrase.vue';
import { RouteConfig } from '@/router';
import { OBJECTS_ACTIONS } from '@/store/modules/objects';
import { LocalData } from '@/utils/localData';
@Component({
@ -57,12 +58,8 @@ export default class CreatePassphrase extends Vue {
this.isLoading = false;
this.$router.push({
name: RouteConfig.BucketsManagement.name,
params: {
passphrase: this.passphrase,
},
});
this.$store.dispatch(OBJECTS_ACTIONS.SET_PASSPHRASE, this.passphrase);
this.$router.push({name: RouteConfig.BucketsManagement.name});
}
}
</script>

View File

@ -73,6 +73,7 @@ import VButton from '@/components/common/VButton.vue';
import WarningIcon from '@/../static/images/common/greyWarning.svg';
import { RouteConfig } from '@/router';
import { OBJECTS_ACTIONS } from '@/store/modules/objects';
import { LocalData, UserIDPassSalt } from '@/utils/localData';
import { MetaUtils } from '@/utils/meta';
@ -115,12 +116,8 @@ export default class EnterPassphrase extends Vue {
switch (true) {
case areHashesEqual() ||
!areHashesEqual() && this.isError && this.isCheckboxChecked:
this.$router.push({
name: RouteConfig.BucketsManagement.name,
params: {
passphrase: this.passphrase,
},
});
this.$store.dispatch(OBJECTS_ACTIONS.SET_PASSPHRASE, this.passphrase);
this.$router.push({name: RouteConfig.BucketsManagement.name});
return;
case !areHashesEqual() && this.isError && !this.isCheckboxChecked:

View File

@ -0,0 +1,143 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="objects-popup">
<div class="objects-popup__container">
<h1 class="objects-popup__container__title">{{title}}</h1>
<p class="objects-popup__container__sub-title">{{subTitle}}</p>
<HeaderedInput
class="objects-popup__container__input"
label="Bucket Name (lowercase letters)"
placeholder="Enter bucket name"
@setData="onChangeName"
:init-value="defaultInputValue"
:error="errorMessage"
:is-loading="isLoading"
/>
<VButton
:label="buttonLabel"
width="100%"
height="48px"
:on-press="onClick"
:is-disabled="isLoading"
/>
<div class="objects-popup__container__close-cross-container" @click="onCloseClick">
<CloseCrossIcon />
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import HeaderedInput from '@/components/common/HeaderedInput.vue';
import VButton from '@/components/common/VButton.vue';
import CloseCrossIcon from '@/../static/images/common/closeCross.svg';
@Component({
components: {
HeaderedInput,
VButton,
CloseCrossIcon,
},
})
export default class ObjectsPopup extends Vue {
@Prop({ default: () => ''})
public readonly onClick;
@Prop({ default: ''})
public readonly title: string;
@Prop({ default: ''})
public readonly subTitle: string;
@Prop({ default: ''})
public readonly defaultInputValue: string;
@Prop({ default: ''})
public readonly buttonLabel: string;
@Prop({ default: ''})
public readonly errorMessage: string;
@Prop({ default: false})
public readonly isLoading: boolean;
/**
* Sets bucket name from input.
*/
public onChangeName(value: string): void {
this.$emit('setName', value);
}
/**
* Closes popup.
*/
public onCloseClick(): void {
this.$emit('close');
}
}
</script>
<style scoped lang="scss">
.objects-popup {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: rgba(27, 37, 51, 0.75);
display: flex;
align-items: center;
justify-content: center;
&__container {
padding: 45px 70px;
border-radius: 10px;
font-family: 'font_regular', sans-serif;
font-style: normal;
display: flex;
flex-direction: column;
align-items: center;
background-color: #fff;
max-width: 480px;
position: relative;
&__title {
font-family: 'font_bold', sans-serif;
font-size: 22px;
line-height: 27px;
color: #000;
margin: 0 0 18px 0;
}
&__sub-title {
font-weight: normal;
font-size: 18px;
line-height: 30px;
text-align: center;
letter-spacing: -0.100741px;
color: rgba(37, 37, 37, 0.7);
margin: 0;
}
&__input {
width: calc(100% - 4px);
margin-bottom: 18px;
}
&__close-cross-container {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
right: 30px;
top: 30px;
height: 24px;
width: 24px;
cursor: pointer;
&:hover .close-cross-svg-path {
fill: #2683ff;
}
}
}
}
</style>

View File

@ -92,7 +92,7 @@ export abstract class RouteConfig {
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');
public static UploadFile = new NavigationLink('upload/', 'Objects Upload');
}
export const notProjectRelatedRoutes = [
@ -320,6 +320,12 @@ export const router = new Router({
path: RouteConfig.UploadFile.path,
name: RouteConfig.UploadFile.name,
component: UploadFile,
children: [
{
path: '*',
component: UploadFile,
},
],
},
],
},

View File

@ -3,13 +3,24 @@
import { StoreModule } from '@/store';
import { GatewayCredentials } from '@/types/accessGrants';
import * as AWS from '@aws-sdk/client-s3';
import {
Bucket,
CreateBucketCommand,
DeleteBucketCommand,
ListBucketsCommand,
ListBucketsOutput,
S3Client,
} from '@aws-sdk/client-s3';
export const OBJECTS_ACTIONS = {
CLEAR: 'clearObjects',
SET_GATEWAY_CREDENTIALS: 'setGatewayCredentials',
SET_ACCESS_GRANT: 'setAccessGrant',
SET_S3_CLIENT: 'setS3Client',
SET_PASSPHRASE: 'setPassphrase',
FETCH_BUCKETS: 'fetchBuckets',
CREATE_BUCKET: 'createBucket',
DELETE_BUCKET: 'deleteBucket',
};
export const OBJECTS_MUTATIONS = {
@ -17,6 +28,8 @@ export const OBJECTS_MUTATIONS = {
SET_ACCESS_GRANT: 'setAccessGrant',
CLEAR: 'clearObjects',
SET_S3_CLIENT: 'setS3Client',
SET_BUCKETS: 'setBuckets',
SET_PASSPHRASE: 'setPassphrase',
};
const {
@ -24,12 +37,16 @@ const {
SET_ACCESS_GRANT,
SET_GATEWAY_CREDENTIALS,
SET_S3_CLIENT,
SET_BUCKETS,
SET_PASSPHRASE,
} = OBJECTS_MUTATIONS;
export class ObjectsState {
public accessGrant: string = '';
public gatewayCredentials: GatewayCredentials = new GatewayCredentials();
public s3Client: AWS.S3 = new AWS.S3({});
public s3Client: S3Client = new S3Client({});
public buckets: Bucket[] = [];
public passphrase: string = '';
}
/**
@ -46,15 +63,29 @@ export function makeObjectsModule(): StoreModule<ObjectsState> {
state.gatewayCredentials = credentials;
},
[SET_S3_CLIENT](state: ObjectsState) {
// TODO: use this for local testing. Remove after final implementation.
// state.gatewayCredentials.accessKeyId = 'jwitszrc76z4amjcrinv4zjpnlia';
// state.gatewayCredentials.secretKey = 'jyjufay7ddmwj6tlboyuj23yy4lqigfqa2ie25y526qmjj65khxzw';
// state.gatewayCredentials.endpoint = 'https://gateway.tardigradeshare.io';
const s3Config = {
accessKeyId: state.gatewayCredentials.accessKeyId,
secretAccessKey: state.gatewayCredentials.secretKey,
credentials: {
accessKeyId: state.gatewayCredentials.accessKeyId,
secretAccessKey: state.gatewayCredentials.secretKey,
},
endpoint: state.gatewayCredentials.endpoint,
s3ForcePathStyle: true,
signatureVersion: 'v4',
region: 'REGION',
};
state.s3Client = new AWS.S3(s3Config);
state.s3Client = new S3Client(s3Config);
},
[SET_BUCKETS](state: ObjectsState, buckets: Bucket[]) {
state.buckets = buckets;
},
[SET_PASSPHRASE](state: ObjectsState, passphrase: string) {
state.passphrase = passphrase;
},
[CLEAR](state: ObjectsState) {
state.accessGrant = '';
@ -71,6 +102,29 @@ export function makeObjectsModule(): StoreModule<ObjectsState> {
setS3Client: function({commit}: any): void {
commit(SET_S3_CLIENT);
},
setPassphrase: function({commit}: any, passphrase: string): void {
commit(SET_PASSPHRASE, passphrase);
},
fetchBuckets: async function(ctx): Promise<void> {
const bucketsOutput: ListBucketsOutput = await ctx.state.s3Client.send(new ListBucketsCommand({
credentials: {
accessKeyId: ctx.state.gatewayCredentials.accessKeyId,
secretAccessKey: ctx.state.gatewayCredentials.secretKey,
},
}));
ctx.commit(SET_BUCKETS, bucketsOutput.Buckets);
},
createBucket: async function(ctx, name: string): Promise<void> {
await ctx.state.s3Client.send(new CreateBucketCommand({
Bucket: name,
}));
},
deleteBucket: async function(ctx, name: string): Promise<void> {
await ctx.state.s3Client.send(new DeleteBucketCommand({
Bucket: name,
}));
},
clearObjects: function ({commit}: any): void {
commit(CLEAR);
},

View File

@ -0,0 +1,5 @@
<svg width="22" height="20" viewBox="0 0 22 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.9414 6.37625C11.9801 6.08967 12 5.79716 12 5.5C12 4.27554 11.6614 3.13015 11.0728 2.15236C11.8914 2.054 12.7826 2 13.7149 2C17.7162 2 20.9599 2.99466 20.9599 4.22165C20.9599 5.44863 17.7162 6.44329 13.7149 6.44329C13.1031 6.44329 12.509 6.42003 11.9414 6.37625ZM6.5 11.9236C8.71728 11.5812 10.5646 10.1182 11.4453 8.13147C12.1494 8.19493 12.8858 8.22901 13.6386 8.23117L13.7149 8.23128C15.8794 8.23128 17.914 7.96736 19.4379 7.50003C20.2135 7.26217 20.8458 6.97514 21.3041 6.64285C21.3439 6.61398 21.3826 6.58466 21.42 6.55488L20.3732 18.0185L20.3704 18.0186C20.2668 19.1183 17.3267 20 13.7149 20C10.1031 20 7.16303 19.1183 7.05939 18.0186L7.05656 18.0185L6.5 11.9236Z" fill="white"/>
<circle cx="5.5" cy="5.5" r="5.5" fill="white"/>
<path d="M7.60876 5.69568H3.39124M5.5 3.58691V7.80444" stroke="#0068DC" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1023 B

View File

@ -0,0 +1,3 @@
<svg width="16" height="18" viewBox="0 0 16 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.42 4.55488L14.3732 16.0185L14.3704 16.0186C14.2668 17.1183 11.3267 18 7.7149 18C4.10314 18 1.16304 17.1183 1.05939 16.0186L1.05656 16.0185L0.00976562 4.55488C0.0472251 4.58465 0.0858874 4.61398 0.125716 4.64285C0.584003 4.97514 1.21629 5.26217 1.9919 5.50003C3.49783 5.96186 5.5026 6.22504 7.63856 6.23117L7.7149 6.23128C9.8794 6.23128 11.914 5.96736 13.4379 5.50003C14.2135 5.26217 14.8458 4.97514 15.3041 4.64285C15.3439 4.61398 15.3826 4.58466 15.42 4.55488ZM7.7149 0C11.7162 0 14.9599 0.994665 14.9599 2.22165C14.9599 3.44863 11.7162 4.44329 7.7149 4.44329C3.71361 4.44329 0.469924 3.44863 0.469924 2.22165C0.469924 0.994665 3.71361 0 7.7149 0Z" fill="#768394"/>
</svg>

After

Width:  |  Height:  |  Size: 827 B

View File

@ -0,0 +1,3 @@
<svg width="17" height="19" viewBox="0 0 17 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path class="bucket-delete-path" d="M11.1921 2.84172H15.3004C15.5351 2.84172 15.7254 3.032 15.7254 3.26672C15.7254 3.50144 15.5351 3.69172 15.3004 3.69172H14.5594L13.4991 16.9458C13.4579 17.4612 13.0276 17.8584 12.5106 17.8584H4.4902C3.97319 17.8584 3.54292 17.4612 3.50169 16.9458L2.44137 3.69172H1.70039C1.46567 3.69172 1.27539 3.50144 1.27539 3.26672C1.27539 3.032 1.46567 2.84172 1.70039 2.84172H5.80872V1.99172C5.80872 1.51928 6.20447 1.14172 6.68706 1.14172H10.3137C10.7963 1.14172 11.1921 1.51928 11.1921 1.99172V2.84172ZM3.29408 3.69172L4.34899 16.878C4.35488 16.9516 4.41634 17.0084 4.4902 17.0084H12.5106C12.5844 17.0084 12.6459 16.9516 12.6518 16.878L13.7067 3.69172H3.29408ZM10.3421 2.84172V1.99172C10.3421 1.99828 10.3345 1.99172 10.3137 1.99172H6.68706C6.66631 1.99172 6.65872 1.99828 6.65872 1.99172V2.84172H10.3421Z" fill="#768394" stroke="#768394"/>
</svg>

After

Width:  |  Height:  |  Size: 974 B

View File

@ -0,0 +1,5 @@
<svg width="4" height="16" viewBox="0 0 4 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.2 1.6C3.2 2.48366 2.48366 3.2 1.6 3.2C0.716344 3.2 0 2.48366 0 1.6C0 0.716344 0.716344 0 1.6 0C2.48366 0 3.2 0.716344 3.2 1.6Z" fill="#7C8794"/>
<path d="M3.2 8C3.2 8.88366 2.48366 9.6 1.6 9.6C0.716344 9.6 0 8.88366 0 8C0 7.11634 0.716344 6.4 1.6 6.4C2.48366 6.4 3.2 7.11634 3.2 8Z" fill="#7C8794"/>
<path d="M1.6 16C2.48366 16 3.2 15.2837 3.2 14.4C3.2 13.5163 2.48366 12.8 1.6 12.8C0.716344 12.8 0 13.5163 0 14.4C0 15.2837 0.716344 16 1.6 16Z" fill="#7C8794"/>
</svg>

After

Width:  |  Height:  |  Size: 587 B

View File

@ -8,6 +8,7 @@ exports[`HeaderedInput.vue renders correctly with default props 1`] = `
<h3 class="label-container__main__label"></h3>
<h3 class="label-container__main__label add-label"></h3>
<!---->
<!---->
</div>
<!---->
</div>
@ -24,6 +25,7 @@ exports[`HeaderedInput.vue renders correctly with input error 1`] = `
<!---->
<!---->
<h3 class="label-container__main__error">testError</h3>
<!---->
</div>
<!---->
</div>
@ -40,6 +42,7 @@ exports[`HeaderedInput.vue renders correctly with isMultiline props 1`] = `
<h3 class="label-container__main__label"></h3>
<h3 class="label-container__main__label add-label"></h3>
<!---->
<!---->
</div>
<!---->
</div>

View File

@ -11,6 +11,7 @@ exports[`CreateProject.vue renders correctly 1`] = `
<h3 class="label-container__main__label">Project Name</h3>
<h3 class="label-container__main__label add-label">Up To 20 Characters</h3>
<!---->
<!---->
</div>
<h3 class="label-container__limit">0/20</h3>
</div>
@ -24,6 +25,7 @@ exports[`CreateProject.vue renders correctly 1`] = `
<h3 class="label-container__main__label">Description</h3>
<h3 class="label-container__main__label add-label">Optional</h3>
<!---->
<!---->
</div>
<h3 class="label-container__limit">0/100</h3>
</div>