web/satellite,satellite/console: Allow paid tier users to edit limits

Added editable fields to the project details page for Storage Limit and Bandwidth limit. Leveraged existing types when possible.

Added fixed checking into the limits to prevent reducing limits beyond current usage, as well as limiting usage to less than the default paid tier maximum.

Change-Id: I07ce53470919a8a9d4dce56ade6904ede8daf34c
This commit is contained in:
dlamarmorgan 2021-08-02 15:06:15 -07:00 committed by Damein Morgan
parent 1fa0cfbfe0
commit cc083dbdc9
14 changed files with 447 additions and 39 deletions

View File

@ -43,6 +43,10 @@ const (
// InputArg is argument name for all input types.
InputArg = "input"
// ProjectFields is a field name for project specific fields.
ProjectFields = "projectFields"
// ProjectLimits is a field name for project specific limits.
ProjectLimits = "projectLimits"
// FieldProjectID is field name for projectID.
FieldProjectID = "projectID"
// FieldNewPassword is a field name for new password.
@ -96,16 +100,15 @@ func rootMutation(log *zap.Logger, service *console.Service, mailService *mailse
FieldID: &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
FieldName: &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
ProjectFields: &graphql.ArgumentConfig{
Type: graphql.NewNonNull(types.projectInput),
},
FieldDescription: &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
ProjectLimits: &graphql.ArgumentConfig{
Type: graphql.NewNonNull(types.projectLimit),
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
name := p.Args[FieldName].(string)
description := p.Args[FieldDescription].(string)
var projectInput = fromMapProjectInfoProjectLimits(p.Args[ProjectFields].(map[string]interface{}), p.Args[ProjectLimits].(map[string]interface{}))
inputID := p.Args[FieldID].(string)
projectID, err := uuid.FromString(inputID)
@ -113,7 +116,7 @@ func rootMutation(log *zap.Logger, service *console.Service, mailService *mailse
return nil, err
}
project, err := service.UpdateProject(p.Context, projectID, name, description)
project, err := service.UpdateProject(p.Context, projectID, projectInput)
if err != nil {
return nil, err
}

View File

@ -366,13 +366,17 @@ func TestGraphqlMutation(t *testing.T) {
const testName = "testName"
const testDescription = "test description"
const StorageLimit = "100"
const BandwidthLimit = "100"
t.Run("Update project mutation", func(t *testing.T) {
query := fmt.Sprintf(
"mutation {updateProject(id:\"%s\",name:\"%s\",description:\"%s\"){id,name,description}}",
"mutation {updateProject(id:\"%s\",projectFields:{name:\"%s\",description:\"%s\"},projectLimits:{storageLimit:\"%s\",bandwidthLimit:\"%s\"}){id,name,description}}",
project.ID.String(),
testName,
testDescription,
StorageLimit,
BandwidthLimit,
)
result, err := testQuery(t, query)

View File

@ -4,10 +4,12 @@
package consoleql
import (
"strconv"
"time"
"github.com/graphql-go/graphql"
"storj.io/common/memory"
"storj.io/storj/satellite/accounting"
"storj.io/storj/satellite/console"
)
@ -17,6 +19,8 @@ const (
ProjectType = "project"
// ProjectInputType is a graphql type name for project input.
ProjectInputType = "projectInput"
// ProjectLimitType is a graphql type name for project limit.
ProjectLimitType = "projectLimit"
// ProjectUsageType is a graphql type name for project usage.
ProjectUsageType = "projectUsage"
// ProjectsCursorInputType is a graphql input type name for projects cursor.
@ -54,6 +58,10 @@ const (
FieldUsage = "usage"
// FieldBucketUsages is a field name for bucket usages.
FieldBucketUsages = "bucketUsages"
// FieldStorageLimit is a field name for the storage limit.
FieldStorageLimit = "storageLimit"
// FieldBandwidthLimit is a field name for bandwidth limit.
FieldBandwidthLimit = "bandwidthLimit"
// FieldStorage is a field name for storage total.
FieldStorage = "storage"
// FieldEgress is a field name for egress total.
@ -266,6 +274,21 @@ func graphqlProjectInput() *graphql.InputObject {
})
}
// graphqlProjectLimit creates graphql.InputObject type needed to create/update satellite.Project.
func graphqlProjectLimit() *graphql.InputObject {
return graphql.NewInputObject(graphql.InputObjectConfig{
Name: ProjectLimitType,
Fields: graphql.InputObjectConfigFieldMap{
FieldStorageLimit: &graphql.InputObjectFieldConfig{
Type: graphql.String,
},
FieldBandwidthLimit: &graphql.InputObjectFieldConfig{
Type: graphql.String,
},
},
})
}
// graphqlBucketUsageCursor creates bucket usage cursor graphql input type.
func graphqlProjectsCursor() *graphql.InputObject {
return graphql.NewInputObject(graphql.InputObjectConfig{
@ -415,6 +438,18 @@ func fromMapProjectInfo(args map[string]interface{}) (project console.ProjectInf
return
}
// fromMapProjectInfoProjectLimits creates console.ProjectInfo from input args.
func fromMapProjectInfoProjectLimits(projectInfo, projectLimits map[string]interface{}) (project console.ProjectInfo) {
project.Name, _ = projectInfo[FieldName].(string)
project.Description, _ = projectInfo[FieldDescription].(string)
storageLimit, _ := strconv.Atoi(projectLimits[FieldStorageLimit].(string))
project.StorageLimit = memory.Size(storageLimit)
bandwidthLimit, _ := strconv.Atoi(projectLimits[FieldBandwidthLimit].(string))
project.BandwidthLimit = memory.Size(bandwidthLimit)
return
}
// fromMapProjectsCursor creates console.ProjectsCursor from input args.
func fromMapProjectsCursor(args map[string]interface{}) (cursor console.ProjectsCursor) {
cursor.Limit = args[LimitArg].(int)

View File

@ -31,6 +31,7 @@ type TypeCreator struct {
userInput *graphql.InputObject
projectInput *graphql.InputObject
projectLimit *graphql.InputObject
projectsCursor *graphql.InputObject
bucketUsageCursor *graphql.InputObject
projectMembersCursor *graphql.InputObject
@ -50,6 +51,11 @@ func (c *TypeCreator) Create(log *zap.Logger, service *console.Service, mailServ
return err
}
c.projectLimit = graphqlProjectLimit()
if err := c.projectLimit.Error(); err != nil {
return err
}
c.bucketUsageCursor = graphqlBucketUsageCursor()
if err := c.bucketUsageCursor.Error(); err != nil {
return err

View File

@ -82,10 +82,11 @@ type Project struct {
// ProjectInfo holds data needed to create/update Project.
type ProjectInfo struct {
Name string `json:"name"`
Description string `json:"description"`
CreatedAt time.Time `json:"createdAt"`
Name string `json:"name"`
Description string `json:"description"`
StorageLimit memory.Size `json:"project specific storage limit"`
BandwidthLimit memory.Size `json:"project specific bandwidth limit"`
CreatedAt time.Time `json:"createdAt"`
}
// ProjectsCursor holds info for project

View File

@ -1181,7 +1181,7 @@ func (s *Service) DeleteProject(ctx context.Context, projectID uuid.UUID) (err e
}
// UpdateProject is a method for updating project name and description by id.
func (s *Service) UpdateProject(ctx context.Context, projectID uuid.UUID, name string, description string) (p *Project, err error) {
func (s *Service) UpdateProject(ctx context.Context, projectID uuid.UUID, projectInfo ProjectInfo) (p *Project, err error) {
defer mon.Task()(&ctx)(&err)
auth, err := s.getAuthAndAuditLog(ctx, "update project name and description", zap.String("projectID", projectID.String()))
@ -1189,7 +1189,7 @@ func (s *Service) UpdateProject(ctx context.Context, projectID uuid.UUID, name s
return nil, Error.Wrap(err)
}
err = ValidateNameAndDescription(name, description)
err = ValidateNameAndDescription(projectInfo.Name, projectInfo.Description)
if err != nil {
return nil, Error.Wrap(err)
}
@ -1199,8 +1199,43 @@ func (s *Service) UpdateProject(ctx context.Context, projectID uuid.UUID, name s
return nil, Error.Wrap(err)
}
project := isMember.project
project.Name = name
project.Description = description
project.Name = projectInfo.Name
project.Description = projectInfo.Description
if auth.User.PaidTier {
if projectInfo.StorageLimit.Int64() <= 0 || projectInfo.BandwidthLimit.Int64() <= 0 {
return project, errors.New("project limits must be greater than 0")
}
if projectInfo.StorageLimit.Int64() > s.config.UsageLimits.Storage.Paid.Int64() {
return project, errors.New("specified storage limit exceeds allowed maximum for current tier")
}
if projectInfo.BandwidthLimit.Int64() > s.config.UsageLimits.Bandwidth.Paid.Int64() {
return project, errors.New("specified bandwidth limit exceeds allowed maximum for current tier")
}
storageUsed, err := s.projectUsage.GetProjectStorageTotals(ctx, projectID)
if err != nil {
return nil, Error.Wrap(err)
}
if projectInfo.StorageLimit.Int64() < storageUsed {
return project, errors.New("cannot set storage limit below current usage")
}
bandwidthUsed, err := s.projectUsage.GetProjectBandwidthTotals(ctx, projectID)
if err != nil {
return nil, Error.Wrap(err)
}
if projectInfo.BandwidthLimit.Int64() < bandwidthUsed {
return project, errors.New("cannot set bandwidth limit below current usage")
}
project.StorageLimit = new(memory.Size)
*project.StorageLimit = projectInfo.StorageLimit
project.BandwidthLimit = new(memory.Size)
*project.BandwidthLimit = projectInfo.BandwidthLimit
}
err = s.store.Projects().Update(ctx, project)
if err != nil {

View File

@ -59,15 +59,51 @@ func TestService(t *testing.T) {
})
t.Run("TestUpdateProject", func(t *testing.T) {
// Updating own project should work
updatedPro, err := service.UpdateProject(authCtx1, up1Pro1.ID, "newName", "TestUpdate")
updatedName := "newName"
updatedDescription := "newDescription"
updatedStorageLimit := memory.Size(100)
updatedBandwidthLimit := memory.Size(100)
// user should be in free tier
user, err := service.GetUser(ctx, up1Pro1.OwnerID)
require.NoError(t, err)
require.NotEqual(t, up1Pro1.Name, updatedPro.Name)
require.False(t, user.PaidTier)
// get context
authCtx1, err := sat.AuthenticatedContext(ctx, user.ID)
require.NoError(t, err)
// add a credit card to put the user in the paid tier
err = service.Payments().AddCreditCard(authCtx1, "test-cc-token")
require.NoError(t, err)
// update auth ctx
authCtx1, err = sat.AuthenticatedContext(ctx, user.ID)
require.NoError(t, err)
// Updating own project should work
updatedProject, err := service.UpdateProject(authCtx1, up1Pro1.ID, console.ProjectInfo{
Name: updatedName,
Description: updatedDescription,
StorageLimit: updatedStorageLimit,
BandwidthLimit: updatedBandwidthLimit,
})
require.NoError(t, err)
require.NotEqual(t, up1Pro1.Name, updatedProject.Name)
require.Equal(t, updatedName, updatedProject.Name)
require.NotEqual(t, up1Pro1.Description, updatedProject.Description)
require.Equal(t, updatedDescription, updatedProject.Description)
require.NotEqual(t, *up1Pro1.StorageLimit, *updatedProject.StorageLimit)
require.Equal(t, updatedStorageLimit, *updatedProject.StorageLimit)
require.NotEqual(t, *up1Pro1.BandwidthLimit, *updatedProject.BandwidthLimit)
require.Equal(t, updatedBandwidthLimit, *updatedProject.BandwidthLimit)
// Updating someone else project details should not work
updatedPro, err = service.UpdateProject(authCtx1, up2Pro1.ID, "newName", "TestUpdate")
updatedProject, err = service.UpdateProject(authCtx1, up2Pro1.ID, console.ProjectInfo{
Name: "newName",
Description: "TestUpdate",
StorageLimit: memory.Size(100),
BandwidthLimit: memory.Size(100),
})
require.Error(t, err)
require.Nil(t, updatedPro)
require.Nil(t, updatedProject)
})
t.Run("TestAddProjectMembers", func(t *testing.T) {

View File

@ -83,20 +83,28 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
* @returns Project[]
* @throws Error
*/
public async update(projectId: string, name: string, description: string): Promise<void> {
public async update(projectId: string, projectFields: ProjectFields, projectLimits: ProjectLimits): Promise<void> {
const query =
`mutation($projectId: String!, $name: String!, $description: String!) {
`mutation($projectId: String!, $name: String!, $description: String!, $storageLimit: String!, $bandwidthLimit: String!) {
updateProject(
id: $projectId,
name: $name,
description: $description
projectFields: {
name: $name,
description: $description,
},
projectLimits: {
storageLimit: $storageLimit,
bandwidthLimit: $bandwidthLimit,
}
) {name}
}`;
const variables = {
projectId: projectId,
name: name,
description: description,
name: projectFields.name,
description: projectFields.description,
storageLimit: projectLimits.storageLimit.toString(),
bandwidthLimit: projectLimits.bandwidthLimit.toString(),
};
await this.mutate(query, variables);

View File

@ -63,19 +63,83 @@
:on-press="onSaveDescriptionButtonClick"
/>
</div>
<div v-if="isPaidTier" class="project-details__wrapper__container__limits">
<p class="project-details__wrapper__container__limits__label">Storage Limit</p>
<div v-if="!isStorageLimitEditing" class="project-details__wrapper__container__limits__storagelimit-area">
<p class="project-details__wrapper__container__limits__storagelimit-area__storagelimit">{{ storageLimitFormatted }}</p>
<VButton
label="Edit"
width="64px"
height="28px"
:on-press="toggleStorageLimitEditing"
is-white="true"
/>
</div>
<div v-if="isStorageLimitEditing" class="project-details__wrapper__container__limits__storagelimit-editing">
<input
v-model="storageLimitValue"
class="project-details__wrapper__container__limits__storagelimit-editing__input"
placeholder="Enter a storage limit for your project"
@input="onStorageLimitInput"
@change="onStorageLimitInput"
>
<span class="project-details__wrapper__container__limits__storagelimit-editing__limit">{{ nameValue.length }}/{{ nameLength }}</span>
<VButton
class="project-details__wrapper__container__limits__storagelimit-editing__save-button"
label="Save"
width="66px"
height="30px"
:on-press="onSaveStorageLimitButtonClick"
/>
</div>
<p class="project-details__wrapper__container__limits__label">Bandwidth Limit</p>
<div v-if="!isBandwidthLimitEditing" class="project-details__wrapper__container__limits__bandwidthlimit-area">
<p class="project-details__wrapper__container__limits__bandwidthlimit-area__bandwidthlimit">{{ bandwidthLimitFormatted }}</p>
<VButton
label="Edit"
width="64px"
height="28px"
:on-press="toggleBandwidthLimitEditing"
is-white="true"
/>
</div>
<div v-if="isBandwidthLimitEditing" class="project-details__wrapper__container__limits__bandwidthlimit-editing">
<input
v-model="bandwidthLimitValue"
class="project-details__wrapper__container__limits__bandwidthlimit-editing__input"
placeholder="Enter a bandwidth limit for your project"
@input="onBandwidthLimitInput"
@change="onBandwidthLimitInput"
>
<span class="project-details__wrapper__container__limits__bandwidthlimit-editing__limit">{{ nameValue.length }}/{{ nameLength }}</span>
<VButton
class="project-details__wrapper__container__limits__bandwidthlimit-editing__save-button"
label="Save"
width="66px"
height="30px"
:on-press="onSaveBandwidthLimitButtonClick"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Dimensions, Size } from '@/utils/bytesSize';
import { Component, Vue } from 'vue-property-decorator';
import HeaderedInput from '@/components/common/HeaderedInput.vue';
import VButton from '@/components/common/VButton.vue';
import { PROJECTS_ACTIONS } from '@/store/modules/projects';
import { MAX_DESCRIPTION_LENGTH, MAX_NAME_LENGTH, Project, ProjectFields } from '@/types/projects';
import {
MAX_DESCRIPTION_LENGTH,
MAX_NAME_LENGTH,
Project,
ProjectFields, ProjectLimits
} from '@/types/projects';
@Component({
components: {
@ -86,8 +150,13 @@ import { MAX_DESCRIPTION_LENGTH, MAX_NAME_LENGTH, Project, ProjectFields } from
export default class EditProjectDetails extends Vue {
public isNameEditing = false;
public isDescriptionEditing = false;
public isStorageLimitEditing = false;
public isBandwidthLimitEditing = false;
public isPaidTier = false;
public nameValue = '';
public descriptionValue = '';
public storageLimitValue = 0;
public bandwidthLimitValue = 0;
public nameLength: number = MAX_NAME_LENGTH;
public descriptionLength: number = MAX_DESCRIPTION_LENGTH;
@ -98,6 +167,34 @@ export default class EditProjectDetails extends Vue {
return this.$store.getters.selectedProject;
}
/**
* Lifecycle hook after initial render.
* Fetches project limits and paid tier status.
*/
public async mounted(): Promise<void> {
if (!this.$store.getters.selectedProject.id) {
return;
}
if (this.$store.state.usersModule.user.paidTier)
{
this.isPaidTier = true;
}
try {
await this.$store.dispatch(PROJECTS_ACTIONS.GET_LIMITS, this.$store.getters.selectedProject.id);
} catch (error) {
await this.$notify.error(error.message);
}
}
/**
* Returns current limits from store.
*/
public get currentLimits(): ProjectLimits {
return this.$store.state.projectsModule.currentLimits;
}
/**
* Returns displayed project description on UI.
*/
@ -133,6 +230,49 @@ export default class EditProjectDetails extends Vue {
this.descriptionValue = target.value.slice(0, MAX_DESCRIPTION_LENGTH);
}
/**
* Returns formatted limit amount.
*/
public get storageLimitFormatted(): string {
return this.formattedValue(new Size(this.currentLimits.storageLimit, 2));
}
/**
* Triggers on storage limit input.
*/
public onStorageLimitInput(event: Event): void {
const target = event.target as HTMLInputElement;
this.storageLimitValue = parseInt(target.value);
}
/**
* Returns formatted limit amount.
*/
public get bandwidthLimitFormatted(): string {
return this.formattedValue(new Size(this.currentLimits.bandwidthLimit, 2));
}
/**
* Triggers on bandwidth limit input.
*/
public onBandwidthLimitInput(event: Event): void {
const target = event.target as HTMLInputElement;
this.bandwidthLimitValue = parseInt(target.value);
}
/**
* Formats value to needed form and returns it.
*/
private formattedValue(value: Size): string {
switch (value.label) {
case Dimensions.Bytes:
case Dimensions.KB:
return '0';
default:
return `${value.formattedBytes.replace(/\\.0+$/, '')}${value.label}`;
}
}
/**
* Updates project name.
*/
@ -169,6 +309,40 @@ export default class EditProjectDetails extends Vue {
await this.$notify.success('Project description updated successfully!');
}
/**
* Updates project storage limit.
*/
public async onSaveStorageLimitButtonClick(): Promise<void> {
try {
const updatedProject = new ProjectLimits(0, 0, this.storageLimitValue);
await this.$store.dispatch(PROJECTS_ACTIONS.UPDATE_STORAGE_LIMIT, updatedProject);
} catch (error) {
await this.$notify.error(`Unable to update project storage limit. ${error.message}`);
return;
}
this.toggleStorageLimitEditing();
await this.$notify.success('Project storage limit updated successfully!');
}
/**
* Updates project bandwidth limit.
*/
public async onSaveBandwidthLimitButtonClick(): Promise<void> {
try {
const updatedProject = new ProjectLimits(this.bandwidthLimitValue);
await this.$store.dispatch(PROJECTS_ACTIONS.UPDATE_BANDWIDTH_LIMIT, updatedProject);
} catch (error) {
await this.$notify.error(`Unable to update project bandwidth limit. ${error.message}`);
return;
}
this.toggleBandwidthLimitEditing();
await this.$notify.success('Project bandwidth limit updated successfully!');
}
/**
* Toggles project name editing state.
*/
@ -185,6 +359,26 @@ export default class EditProjectDetails extends Vue {
this.descriptionValue = this.storedProject.description;
}
/**
* Toggles project storage limit editing state.
*/
public toggleStorageLimitEditing(): void {
if (this.$store.state.usersModule.user.paidTier) {
this.isStorageLimitEditing = !this.isStorageLimitEditing;
this.storageLimitValue = this.currentLimits.storageLimit;
}
}
/**
* Toggles project bandwidth limit editing state.
*/
public toggleBandwidthLimitEditing(): void {
if (this.$store.state.usersModule.user.paidTier) {
this.isBandwidthLimitEditing = !this.isBandwidthLimitEditing;
this.bandwidthLimitValue = this.currentLimits.bandwidthLimit;
}
}
/**
* Redirects to previous route.
*/
@ -240,7 +434,9 @@ export default class EditProjectDetails extends Vue {
}
&__name-area,
&__description-area {
&__description-area,
&__limits__storagelimit-area,
&__limits__bandwidthlimit-area {
display: flex;
align-items: center;
justify-content: space-between;
@ -250,7 +446,9 @@ export default class EditProjectDetails extends Vue {
border-radius: 6px;
&__name,
&__description {
&__description,
&__limits__storagelimit,
&__limits__bandwidthlimit {
font-weight: normal;
font-size: 16px;
line-height: 19px;
@ -264,12 +462,16 @@ export default class EditProjectDetails extends Vue {
}
}
&__name-area {
&__name-area,
&__description-area,
&__limits__storagelimit-area {
margin-bottom: 35px;
}
&__name-editing,
&__description-editing {
&__description-editing,
&__limits__storagelimit-editing,
&__limits__bandwidthlimit-editing {
display: flex;
align-items: center;
width: calc(100% - 7px);
@ -308,7 +510,9 @@ export default class EditProjectDetails extends Vue {
}
}
&__name-editing {
&__name-editing,
&__description-editing,
&___limits_storagelimit-editing {
margin-bottom: 35px;
}
}

View File

@ -19,6 +19,8 @@ export const PROJECTS_ACTIONS = {
SELECT: 'selectProject',
UPDATE_NAME: 'updateProjectName',
UPDATE_DESCRIPTION: 'updateProjectDescription',
UPDATE_STORAGE_LIMIT: 'updateProjectStorageLimit',
UPDATE_BANDWIDTH_LIMIT: 'updateProjectBandwidthLimit',
DELETE: 'deleteProject',
CLEAR: 'clearProjects',
GET_LIMITS: 'getProjectLimits',
@ -30,6 +32,8 @@ export const PROJECTS_MUTATIONS = {
REMOVE: 'DELETE_PROJECT',
UPDATE_PROJECT_NAME: 'UPDATE_PROJECT_NAME',
UPDATE_PROJECT_DESCRIPTION: 'UPDATE_PROJECT_DESCRIPTION',
UPDATE_PROJECT_STORAGE_LIMIT: 'UPDATE_STORAGE_LIMIT',
UPDATE_PROJECT_BANDWIDTH_LIMIT: 'UPDATE_BANDWIDTH_LIMIT',
SET_PROJECTS: 'SET_PROJECTS',
SELECT_PROJECT: 'SELECT_PROJECT',
CLEAR_PROJECTS: 'CLEAR_PROJECTS',
@ -64,6 +68,8 @@ const {
SELECT,
UPDATE_NAME,
UPDATE_DESCRIPTION,
UPDATE_STORAGE_LIMIT,
UPDATE_BANDWIDTH_LIMIT,
DELETE,
CLEAR,
GET_LIMITS,
@ -76,6 +82,8 @@ const {
REMOVE,
UPDATE_PROJECT_NAME,
UPDATE_PROJECT_DESCRIPTION,
UPDATE_PROJECT_STORAGE_LIMIT,
UPDATE_PROJECT_BANDWIDTH_LIMIT,
SET_PROJECTS,
SELECT_PROJECT,
CLEAR_PROJECTS,
@ -131,6 +139,12 @@ export function makeProjectsModule(api: ProjectsApi): StoreModule<ProjectsState>
[UPDATE_PROJECT_DESCRIPTION](state: ProjectsState, fieldsToUpdate: ProjectFields): void {
state.selectedProject.description = fieldsToUpdate.description;
},
[UPDATE_PROJECT_STORAGE_LIMIT](state: ProjectsState, limitsToUpdate: ProjectLimits): void {
state.currentLimits.storageLimit = limitsToUpdate.storageLimit;
},
[UPDATE_PROJECT_BANDWIDTH_LIMIT](state: ProjectsState, limitsToUpdate: ProjectLimits): void {
state.currentLimits.bandwidthLimit = limitsToUpdate.bandwidthLimit;
},
[REMOVE](state: ProjectsState, projectID: string): void {
state.projects = state.projects.filter(project => project.id !== projectID);
@ -196,16 +210,70 @@ export function makeProjectsModule(api: ProjectsApi): StoreModule<ProjectsState>
commit(SELECT_PROJECT, projectID);
},
[UPDATE_NAME]: async function ({commit, state}: ProjectsContext, fieldsToUpdate: ProjectFields): Promise<void> {
await api.update(state.selectedProject.id, fieldsToUpdate.name, state.selectedProject.description);
const project = new ProjectFields(
fieldsToUpdate.name,
state.selectedProject.description,
state.selectedProject.id,
);
const limit = new ProjectLimits(
state.currentLimits.bandwidthLimit,
state.currentLimits.bandwidthUsed,
state.currentLimits.storageLimit,
state.currentLimits.storageUsed,
);
await api.update(state.selectedProject.id, project, limit);
commit(UPDATE_PROJECT_NAME, fieldsToUpdate);
},
[UPDATE_DESCRIPTION]: async function ({commit, state}: ProjectsContext, fieldsToUpdate: ProjectFields): Promise<void> {
await api.update(state.selectedProject.id, state.selectedProject.name, fieldsToUpdate.description);
const project = new ProjectFields(
state.selectedProject.name,
fieldsToUpdate.description,
state.selectedProject.id,
);
const limit = new ProjectLimits(
state.currentLimits.bandwidthLimit,
state.currentLimits.bandwidthUsed,
state.currentLimits.storageLimit,
state.currentLimits.storageUsed,
);
await api.update(state.selectedProject.id, project, limit);
commit(UPDATE_PROJECT_DESCRIPTION, fieldsToUpdate);
},
[DELETE]: async function ({commit}: ProjectsContext, projectID: string): Promise<void> {
[UPDATE_STORAGE_LIMIT]: async function ({commit, state}: any, limitsToUpdate: ProjectLimits): Promise<void> {
const project = new ProjectFields(
state.selectedProject.name,
state.selectedProject.description,
state.selectedProject.id,
);
const limit = new ProjectLimits(
state.currentLimits.bandwidthLimit,
state.currentLimits.bandwidthUsed,
limitsToUpdate.storageLimit,
state.currentLimits.storageUsed,
);
await api.update(state.selectedProject.id, project, limit);
commit(UPDATE_PROJECT_STORAGE_LIMIT, limitsToUpdate);
},
[UPDATE_BANDWIDTH_LIMIT]: async function ({commit, state}: any, limitsToUpdate: ProjectLimits): Promise<void> {
const project = new ProjectFields(
state.selectedProject.name,
state.selectedProject.description,
state.selectedProject.id,
);
const limit = new ProjectLimits(
limitsToUpdate.bandwidthLimit,
state.currentLimits.bandwidthUsed,
state.currentLimits.storageLimit,
state.currentLimits.storageUsed,
);
await api.update(state.selectedProject.id, project, limit);
commit(UPDATE_PROJECT_BANDWIDTH_LIMIT, limitsToUpdate);
},
[DELETE]: async function ({commit}: any, projectID: string): Promise<void> {
await api.delete(projectID);
commit(REMOVE, projectID);

View File

@ -28,7 +28,7 @@ export interface ProjectsApi {
* @returns Project[]
* @throws Error
*/
update(projectId: string, name: string, description: string): Promise<void>;
update(projectId: string, updateProjectFields: ProjectFields, updateProjectLimits: ProjectLimits): Promise<void>;
/**
* Delete project.
*

View File

@ -35,7 +35,7 @@ export class ProjectsApiMock implements ProjectsApi {
return Promise.resolve(this.mockProjectsPage);
}
update(_projectId: string, _name: string, _description: string): Promise<void> {
update(_projectId: string, _projectFields: ProjectFields, _projectLimits: ProjectLimits): Promise<void> {
return Promise.resolve();
}

View File

@ -6,7 +6,7 @@ import Vuex from 'vuex';
import EditProjectDetails from '@/components/project/EditProjectDetails.vue';
import { makeProjectsModule, PROJECTS_MUTATIONS } from '@/store/modules/projects';
import { Project } from '@/types/projects';
import { Project, ProjectLimits } from '@/types/projects';
import { NotificatorPlugin } from '@/utils/plugins/notificator';
import { createLocalVue, mount } from '@vue/test-utils';
@ -18,7 +18,9 @@ const localVue = createLocalVue();
localVue.use(Vuex);
localVue.use(notificationPlugin);
const projectLimits = new ProjectLimits(1000, 100, 1000, 100);
const projectsApi = new ProjectsApiMock();
projectsApi.setMockLimits(projectLimits);
const projectsModule = makeProjectsModule(projectsApi);
const store = new Vuex.Store({ modules: { projectsModule }});
@ -28,6 +30,7 @@ describe('EditProjectDetails.vue', () => {
it('renders correctly', (): void => {
store.commit(PROJECTS_MUTATIONS.ADD, project);
store.commit(PROJECTS_MUTATIONS.SELECT_PROJECT, project.id);
store.commit(PROJECTS_MUTATIONS.SET_LIMITS, projectLimits);
const wrapper = mount(EditProjectDetails, {
store,

View File

@ -17,6 +17,7 @@ exports[`EditProjectDetails.vue editing description works correctly 1`] = `
<div class="project-details__wrapper__container__description-editing"><input placeholder="Enter a description for your project" class="project-details__wrapper__container__description-editing__input"> <span class="project-details__wrapper__container__description-editing__limit">4/100</span>
<div class="project-details__wrapper__container__description-editing__save-button container" style="width: 66px; height: 30px; border-radius: 6px;"><span class="label">Save</span></div>
</div>
<!---->
</div>
</div>
</div>
@ -40,6 +41,7 @@ exports[`EditProjectDetails.vue editing description works correctly 2`] = `
<div class="container white" style="width: 64px; height: 28px; border-radius: 6px;"><span class="label">Edit</span></div>
</div>
<!---->
<!---->
</div>
</div>
</div>
@ -62,6 +64,7 @@ exports[`EditProjectDetails.vue editing name works correctly 1`] = `
<div class="container white" style="width: 64px; height: 28px; border-radius: 6px;"><span class="label">Edit</span></div>
</div>
<!---->
<!---->
</div>
</div>
</div>
@ -85,6 +88,7 @@ exports[`EditProjectDetails.vue editing name works correctly 2`] = `
<div class="container white" style="width: 64px; height: 28px; border-radius: 6px;"><span class="label">Edit</span></div>
</div>
<!---->
<!---->
</div>
</div>
</div>
@ -108,6 +112,7 @@ exports[`EditProjectDetails.vue renders correctly 1`] = `
<div class="container white" style="width: 64px; height: 28px; border-radius: 6px;"><span class="label">Edit</span></div>
</div>
<!---->
<!---->
</div>
</div>
</div>