Skip to content

Commit

Permalink
satellite/admin/back-office/ui: implement project limits update
Browse files Browse the repository at this point in the history
Enable updating max buckets, storage, bandwidth, segment, rate, and
burst limits from the project page.

Change-Id: Iff10427aeaf8a9b145388054cf6f7ece4b03865f
  • Loading branch information
cam-a authored and ifraixedes committed Jan 9, 2024
1 parent 1a3704c commit ca42e2b
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 23 deletions.
111 changes: 89 additions & 22 deletions satellite/admin/back-office/ui/src/components/ProjectLimitsDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,40 +30,41 @@
<v-row>
<v-col cols="12" sm="6">
<v-text-field
label="Buckets" model-value="100" suffix="Buckets" variant="outlined"
v-model="buckets" :rules="limitRules" label="Buckets" suffix="Buckets" variant="outlined" :disabled="isLoading"
hide-details="auto"
/>
</v-col>
<!-- TODO: Implement unit selection (GB, TB, etc.) -->
<v-col cols="12" sm="6">
<v-text-field
label="Storage" model-value="100" suffix="TB" variant="outlined"
v-model="storage" :rules="limitRules" label="Storage" suffix="Bytes" variant="outlined" :disabled="isLoading"
hide-details="auto"
/>
</v-col>
<v-col cols="12" sm="6">
<v-text-field
label="Download per month" model-value="300" suffix="TB" variant="outlined"
v-model="egress" :rules="limitRules" label="Download per month" suffix="Bytes" variant="outlined" :disabled="isLoading"
hide-details="auto"
/>
</v-col>
<v-col cols="12" sm="6">
<v-text-field
label="Segments" model-value="100,000,000" variant="outlined"
v-model="segments" :rules="limitRules" label="Segments" variant="outlined" :disabled="isLoading"
hide-details="auto"
/>
</v-col>
<v-col cols="12" sm="6">
<v-text-field label="Rate" model-value="10,000" variant="outlined" hide-details="auto" />
<v-text-field v-model="rate" :rules="limitRules" label="Rate" variant="outlined" :disabled="isLoading" hide-details="auto" />
</v-col>
<v-col cols="12" sm="6">
<v-text-field label="Burst" model-value="50,000" variant="outlined" hide-details="auto" />
<v-text-field v-model="burst" :rules="limitRules" label="Burst" variant="outlined" :disabled="isLoading" hide-details="auto" />
</v-col>
</v-row>

<v-row>
<v-col cols="12">
<v-text-field
model-value="56F82SR21Q284" label="Project ID" variant="solo-filled" flat readonly
:model-value="appStore.state.selectedProject?.id" label="Project ID" variant="solo-filled" flat readonly
hide-details="auto"
/>
</v-col>
Expand All @@ -78,25 +79,17 @@
<v-btn variant="outlined" color="default" block @click="dialog = false">Cancel</v-btn>
</v-col>
<v-col>
<v-btn color="primary" variant="flat" block :loading="loading" @click="onButtonClick">Save</v-btn>
<v-btn color="primary" variant="flat" block :loading="isLoading" @click="updateLimits">Save</v-btn>
</v-col>
</v-row>
</v-card-actions>
</v-card>
</v-dialog>

<v-snackbar v-model="snackbar" :timeout="7000" color="error">
Error. Cannot change project limits.
<template #actions>
<v-btn color="default" variant="text" @click="snackbar = false">
Close
</v-btn>
</template>
</v-snackbar>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { onBeforeMount, ref, computed, watch } from 'vue';
import { useRouter } from 'vue-router';
import {
VDialog,
VCard,
Expand All @@ -110,14 +103,88 @@ import {
VCol,
VTextField,
VCardActions,
VSnackbar,
} from 'vuetify/components';
const snackbar = ref<boolean>(false);
import { useAppStore } from '@/store/app';
import { useNotificationsStore } from '@/store/notifications';
import { RequiredRule, ValidationRule } from '@/types/common';
const valid = ref<boolean>(false);
const isLoading = ref<boolean>(false);
const appStore = useAppStore();
const notify = useNotificationsStore();
const router = useRouter();
const dialog = ref<boolean>(false);
function onButtonClick() {
snackbar.value = true;
const buckets = ref<number>(0);
const storage = ref<number>(0);
const egress = ref<number>(0);
const segments = ref<number>(0);
const rate = ref<number>(0);
const burst = ref<number>(0);
async function updateLimits() {
if (!valid.value) {
return;
}
isLoading.value = true;
try {
const updateReq = {
maxBuckets: Number(buckets.value),
storageLimit: Number(storage.value),
bandwidthLimit: Number(egress.value),
segmentLimit: Number(segments.value),
rateLimit: Number(rate.value),
burstLimit: Number(burst.value),
};
await appStore.updateProjectLimits(appStore.state.selectedProject?.id || '', updateReq);
notify.notifySuccess('Successfully updated project limits.');
} catch (error) {
notify.notifyError(`Error updating project limits. ${error.message}`);
}
isLoading.value = false;
dialog.value = false;
}
/**
* Returns an array of validation rules applied to the text input.
*/
const limitRules = computed<ValidationRule<string>[]>(() => {
return [
RequiredRule,
v => !(isNaN(+v) || isNaN(parseFloat(v))) || 'Invalid number',
v => (parseFloat(v) >= 0) || 'Number must be zero or greater',
];
});
function setInputFields() {
const p = appStore.state.selectedProject;
if (!p) {
return;
}
buckets.value = p.maxBuckets || 0;
storage.value = p.storageLimit || 0;
egress.value = p.bandwidthLimit || 0;
segments.value = p.segmentLimit || 0;
rate.value = p.rateLimit || 0;
burst.value = p.burstLimit || 0;
}
watch(dialog, (shown) => {
if (!shown || !appStore.state.selectedProject) {
return;
}
setInputFields();
});
onBeforeMount(() => {
if (!appStore.state.selectedProject) {
router.push('/accounts');
return;
}
setInputFields();
});
</script>
23 changes: 22 additions & 1 deletion satellite/admin/back-office/ui/src/store/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { reactive } from 'vue';
import { defineStore } from 'pinia';

import { PlacementInfo, PlacementManagementHttpApiV1, Project, ProjectManagementHttpApiV1, UserAccount, UserManagementHttpApiV1 } from '@/api/client.gen';
import { PlacementInfo, PlacementManagementHttpApiV1, Project, ProjectLimitsUpdate, ProjectManagementHttpApiV1, UserAccount, UserManagementHttpApiV1 } from '@/api/client.gen';

class AppState {
public placements: PlacementInfo[];
Expand Down Expand Up @@ -47,12 +47,33 @@ export const useAppStore = defineStore('app', () => {
state.selectedProject = await projectApi.getProject(id);
}

async function updateProjectLimits(id: string, limits: ProjectLimitsUpdate): Promise<void> {
await projectApi.updateProjectLimits(limits, id);
if (state.selectedProject && state.selectedProject.id === id) {
state.selectedProject.maxBuckets = limits.maxBuckets;
state.selectedProject.storageLimit = limits.storageLimit;
state.selectedProject.bandwidthLimit = limits.bandwidthLimit;
state.selectedProject.segmentLimit = limits.segmentLimit;
state.selectedProject.rateLimit = limits.rateLimit;
state.selectedProject.burstLimit = limits.burstLimit;
}
if (state.userAccount && state.userAccount.projects) {
const updatedData = {
storageLimit: limits.storageLimit,
bandwidthLimit: limits.bandwidthLimit,
segmentLimit: limits.segmentLimit,
};
state.userAccount.projects.map((item) => (item.id === id ? { ...item, updatedData } : item));
}
}

return {
state,
getUserByEmail,
clearUser,
getPlacements,
getPlacementText,
selectProject,
updateProjectLimits,
};
});
5 changes: 5 additions & 0 deletions satellite/admin/back-office/ui/src/types/common.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.

export type ValidationRule<T> = string | boolean | ((value: T) => string | boolean);
export function RequiredRule(value: unknown): string | boolean {
return (Array.isArray(value) ? !!value.length : !!value || typeof value === 'number') || 'Required';
}

// TODO: fully implement these types and their methods according to their Go counterparts
export type UUID = string;
export type MemorySize = string;
Expand Down

0 comments on commit ca42e2b

Please sign in to comment.