web/satellite/vuetify-poc: add project settings page
This change implements the Project Settings page in the Vuetify project. It allows users to view and change a project's name, description, storage limit, and bandwidth limit. Resolves #6176 Change-Id: Ibcc56d2bb7c71e818390e69eec197508e3551803
This commit is contained in:
parent
de7aabc8c9
commit
80186ecc67
@ -278,3 +278,8 @@ export enum LimitToChange {
|
||||
Storage = 'Storage',
|
||||
Bandwidth = 'Bandwidth',
|
||||
}
|
||||
|
||||
export enum FieldToChange {
|
||||
Name = 'Name',
|
||||
Description = 'Description',
|
||||
}
|
||||
|
@ -9,6 +9,13 @@ export enum Memory {
|
||||
TB = 1e12,
|
||||
PB = 1e15,
|
||||
EB = 1e18,
|
||||
|
||||
KiB = 2 ** 10,
|
||||
MiB = 2 ** 20,
|
||||
GiB = 2 ** 30,
|
||||
TiB = 2 ** 40,
|
||||
PiB = 2 ** 50,
|
||||
EiB = 2 ** 60,
|
||||
}
|
||||
|
||||
export enum Dimensions {
|
||||
|
@ -106,7 +106,9 @@ export enum AnalyticsErrorEventSource {
|
||||
ONBOARDING_NAME_STEP = 'Onboarding name step',
|
||||
ONBOARDING_PERMISSIONS_STEP = 'Onboarding permissions step',
|
||||
PROJECT_DASHBOARD_PAGE = 'Project dashboard page',
|
||||
PROJECT_SETTINGS_AREA = 'Project settings area',
|
||||
EDIT_PROJECT_DETAILS = 'Edit project details',
|
||||
EDIT_PROJECT_LIMIT = 'Edit project limit',
|
||||
PROJECTS_LIST = 'Projects list',
|
||||
PROJECT_MEMBERS_HEADER = 'Project members page header',
|
||||
PROJECT_MEMBERS_PAGE = 'Project members page',
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
<v-menu activator="parent" location="end" transition="scale-transition">
|
||||
<v-list class="pa-2">
|
||||
<v-list-item link rounded="lg">
|
||||
<v-list-item link rounded="lg" :to="`/projects/${item.id}/settings`">
|
||||
<template #prepend>
|
||||
<icon-settings />
|
||||
</template>
|
||||
|
@ -72,7 +72,7 @@
|
||||
<v-menu activator="parent" location="bottom end" transition="scale-transition">
|
||||
<v-list class="pa-0">
|
||||
<template v-if="item.raw.role === ProjectRole.Owner">
|
||||
<v-list-item link>
|
||||
<v-list-item link :to="`/projects/${item.raw.id}/settings`">
|
||||
<template #prepend>
|
||||
<icon-settings />
|
||||
</template>
|
||||
|
@ -0,0 +1,165 @@
|
||||
// Copyright (C) 2023 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<v-dialog
|
||||
v-model="model"
|
||||
width="410px"
|
||||
transition="fade-transition"
|
||||
:persistent="isLoading"
|
||||
>
|
||||
<v-card rounded="xlg">
|
||||
<v-card-item class="pl-7 py-4">
|
||||
<template #prepend>
|
||||
<img class="d-block" src="@/../static/images/modals/boxesIcon.svg" alt="Boxes">
|
||||
</template>
|
||||
<v-card-title class="font-weight-bold">Edit Project {{ field }}</v-card-title>
|
||||
<template #append>
|
||||
<v-btn
|
||||
icon="$close"
|
||||
variant="text"
|
||||
size="small"
|
||||
color="default"
|
||||
:disabled="isLoading"
|
||||
@click="model = false"
|
||||
/>
|
||||
</template>
|
||||
</v-card-item>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-form v-model="formValid" class="pa-7" @submit.prevent>
|
||||
<v-text-field
|
||||
v-model="input"
|
||||
class="py-4"
|
||||
variant="outlined"
|
||||
:rules="rules"
|
||||
:label="`Project ${field}`"
|
||||
:counter="maxLength"
|
||||
persistent-counter
|
||||
autofocus
|
||||
/>
|
||||
</v-form>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-actions class="pa-7">
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-btn variant="outlined" color="default" block :disabled="isLoading" @click="model = false">
|
||||
Cancel
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-btn color="primary" variant="flat" block :loading="isLoading" @click="onSaveClick">
|
||||
Save
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import {
|
||||
VDialog,
|
||||
VCard,
|
||||
VCardItem,
|
||||
VCardTitle,
|
||||
VDivider,
|
||||
VCardActions,
|
||||
VRow,
|
||||
VCol,
|
||||
VBtn,
|
||||
VForm,
|
||||
VTextField,
|
||||
} from 'vuetify/components';
|
||||
|
||||
import { useLoading } from '@/composables/useLoading';
|
||||
import { AnalyticsErrorEventSource, AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
|
||||
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
|
||||
import { useNotify } from '@/utils/hooks';
|
||||
import { useProjectsStore } from '@/store/modules/projectsStore';
|
||||
import { ValidationRule } from '@poc/types/common';
|
||||
import { FieldToChange, ProjectFields, MAX_NAME_LENGTH, MAX_DESCRIPTION_LENGTH } from '@/types/projects';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean,
|
||||
field: FieldToChange,
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean],
|
||||
}>();
|
||||
|
||||
const model = computed<boolean>({
|
||||
get: () => props.modelValue,
|
||||
set: value => emit('update:modelValue', value),
|
||||
});
|
||||
|
||||
const projectsStore = useProjectsStore();
|
||||
const analyticsStore = useAnalyticsStore();
|
||||
const { isLoading, withLoading } = useLoading();
|
||||
const notify = useNotify();
|
||||
|
||||
const formValid = ref<boolean>(false);
|
||||
const input = ref<string>('');
|
||||
|
||||
/**
|
||||
* Returns the maximum input length.
|
||||
*/
|
||||
const maxLength = computed<number>(() => {
|
||||
return props.field === FieldToChange.Name ? MAX_NAME_LENGTH : MAX_DESCRIPTION_LENGTH;
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns an array of validation rules applied to the input.
|
||||
*/
|
||||
const rules = computed<ValidationRule<string>[]>(() => {
|
||||
const max = maxLength.value;
|
||||
const required = props.field === FieldToChange.Name;
|
||||
return [
|
||||
v => (!!v || !required) || 'Required',
|
||||
v => v.length <= max || 'Input is too long.',
|
||||
];
|
||||
});
|
||||
|
||||
/**
|
||||
* Updates project field.
|
||||
*/
|
||||
async function onSaveClick(): Promise<void> {
|
||||
if (!formValid.value) return;
|
||||
await withLoading(async () => {
|
||||
try {
|
||||
if (props.field === FieldToChange.Name) {
|
||||
await projectsStore.updateProjectName(new ProjectFields(input.value, ''));
|
||||
} else {
|
||||
await projectsStore.updateProjectDescription(new ProjectFields('', input.value));
|
||||
}
|
||||
} catch (error) {
|
||||
notify.error(
|
||||
`Error updating project ${props.field.toLowerCase()}. ${error.message}`,
|
||||
AnalyticsErrorEventSource.EDIT_PROJECT_DETAILS,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
analyticsStore.eventTriggered(
|
||||
props.field === FieldToChange.Name
|
||||
? AnalyticsEvent.PROJECT_NAME_UPDATED
|
||||
: AnalyticsEvent.PROJECT_DESCRIPTION_UPDATED,
|
||||
);
|
||||
notify.success(`Project ${props.field.toLowerCase()} updated.`);
|
||||
|
||||
model.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
watch(() => model.value, shown => {
|
||||
if (!shown) return;
|
||||
const project = projectsStore.state.selectedProject;
|
||||
input.value = props.field === FieldToChange.Name ? project.name : project.description;
|
||||
}, { immediate: true });
|
||||
</script>
|
@ -0,0 +1,333 @@
|
||||
// Copyright (C) 2023 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<v-dialog
|
||||
v-model="model"
|
||||
width="410px"
|
||||
transition="fade-transition"
|
||||
:persistent="isLoading"
|
||||
>
|
||||
<v-card rounded="xlg">
|
||||
<v-card-item class="pl-7 py-4">
|
||||
<template #prepend>
|
||||
<img class="d-block" src="@/../static/images/modals/limit.svg" alt="Speedometer">
|
||||
</template>
|
||||
<v-card-title class="font-weight-bold">Edit {{ limitType }} Limit</v-card-title>
|
||||
<template #append>
|
||||
<v-btn
|
||||
icon="$close"
|
||||
variant="text"
|
||||
size="small"
|
||||
color="default"
|
||||
:disabled="isLoading"
|
||||
@click="model = false"
|
||||
/>
|
||||
</template>
|
||||
</v-card-item>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-form v-model="formValid" class="pa-7" @submit.prevent>
|
||||
<v-row>
|
||||
<v-col cols="6">
|
||||
<p class="text-subtitle-2 mb-1">Set {{ limitType }} Limit</p>
|
||||
<v-text-field
|
||||
class="edit-project-limit__text-field"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
type="number"
|
||||
:rules="rules"
|
||||
:model-value="inputText"
|
||||
@update:model-value="updateInputText"
|
||||
>
|
||||
<template #append-inner>
|
||||
<v-menu>
|
||||
<template #activator="{ props: slotProps, isActive }">
|
||||
<v-btn
|
||||
class="h-100 text-medium-emphasis"
|
||||
variant="text"
|
||||
density="compact"
|
||||
color="default"
|
||||
:append-icon="isActive ? 'mdi-menu-up' : 'mdi-menu-down'"
|
||||
v-bind="slotProps"
|
||||
@mousedown.stop
|
||||
@click.stop
|
||||
>
|
||||
<span class="font-weight-regular">{{ activeMeasurement }}</span>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list v-model:selected="dropdownModel" density="compact">
|
||||
<v-list-item :title="Dimensions.TB" :value="Dimensions.TB" />
|
||||
<v-list-item :title="Dimensions.GB" :value="Dimensions.GB" />
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<p class="text-subtitle-2 mb-1">Available {{ limitType }}</p>
|
||||
<v-text-field
|
||||
class="edit-project-limit__text-field"
|
||||
variant="solo-filled"
|
||||
density="compact"
|
||||
flat
|
||||
readonly
|
||||
:model-value="availableUsageFormatted"
|
||||
>
|
||||
<template #append-inner>
|
||||
<v-menu>
|
||||
<template #activator="{ props: slotProps, isActive }">
|
||||
<v-btn
|
||||
class="h-100 text-medium-emphasis"
|
||||
variant="text"
|
||||
density="compact"
|
||||
color="default"
|
||||
:append-icon="isActive ? 'mdi-menu-up' : 'mdi-menu-down'"
|
||||
v-bind="slotProps"
|
||||
@mousedown.stop
|
||||
@click.stop
|
||||
>
|
||||
<span class="font-weight-regular">{{ activeMeasurement }}</span>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list v-model:selected="dropdownModel" density="compact">
|
||||
<v-list-item :title="Dimensions.TB" :value="Dimensions.TB" />
|
||||
<v-list-item :title="Dimensions.GB" :value="Dimensions.GB" />
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12">
|
||||
<v-card class="pa-3">
|
||||
<div class="d-flex mx-2 text-subtitle-2 font-weight-bold text-medium-emphasis">
|
||||
0 {{ activeMeasurement }}
|
||||
<v-spacer />
|
||||
{{ availableUsageFormatted }} {{ activeMeasurement }}
|
||||
</div>
|
||||
<v-slider
|
||||
min="0"
|
||||
:max="availableUsage"
|
||||
:step="Memory[activeMeasurement]"
|
||||
color="success"
|
||||
:model-value="input"
|
||||
@update:model-value="updateInput"
|
||||
/>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-actions class="pa-7">
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-btn variant="outlined" color="default" block :disabled="isLoading" @click="model = false">
|
||||
Cancel
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-btn color="primary" variant="flat" block :loading="isLoading" @click="onSaveClick">
|
||||
Save
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import {
|
||||
VDialog,
|
||||
VCard,
|
||||
VCardItem,
|
||||
VCardTitle,
|
||||
VDivider,
|
||||
VCardActions,
|
||||
VRow,
|
||||
VCol,
|
||||
VBtn,
|
||||
VForm,
|
||||
VTextField,
|
||||
VSpacer,
|
||||
VSlider,
|
||||
VMenu,
|
||||
VList,
|
||||
VListItem,
|
||||
} from 'vuetify/components';
|
||||
|
||||
import { AnalyticsErrorEventSource, AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
|
||||
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
|
||||
import { useProjectsStore } from '@/store/modules/projectsStore';
|
||||
import { useConfigStore } from '@/store/modules/configStore';
|
||||
import { useNotify } from '@/utils/hooks';
|
||||
import { useLoading } from '@/composables/useLoading';
|
||||
import { RequiredRule, ValidationRule } from '@poc/types/common';
|
||||
import { LimitToChange, ProjectLimits } from '@/types/projects';
|
||||
import { Dimensions, Memory } from '@/utils/bytesSize';
|
||||
import { decimalShift } from '@/utils/strings';
|
||||
|
||||
const projectsStore = useProjectsStore();
|
||||
const analyticsStore = useAnalyticsStore();
|
||||
const configStore = useConfigStore();
|
||||
|
||||
const { isLoading, withLoading } = useLoading();
|
||||
const notify = useNotify();
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean,
|
||||
limitType: LimitToChange,
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean],
|
||||
}>();
|
||||
|
||||
const formValid = ref<boolean>(false);
|
||||
const activeMeasurement = ref<Dimensions.GB | Dimensions.TB>(Dimensions.TB);
|
||||
const inputText = ref<string>('0');
|
||||
const input = ref<number>(0);
|
||||
|
||||
const model = computed<boolean>({
|
||||
get: () => props.modelValue,
|
||||
set: value => emit('update:modelValue', value),
|
||||
});
|
||||
|
||||
const dropdownModel = computed<(Dimensions.GB | Dimensions.TB)[]>({
|
||||
get: () => [ activeMeasurement.value ],
|
||||
set: value => activeMeasurement.value = value[0],
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the maximum amount of bytes that the usage limit can be set to.
|
||||
*/
|
||||
const availableUsage = computed<number>(() => {
|
||||
if (props.limitType === LimitToChange.Storage) {
|
||||
return Math.max(
|
||||
projectsStore.state.currentLimits.storageLimit,
|
||||
parseConfigLimit(configStore.state.config.defaultPaidStorageLimit),
|
||||
);
|
||||
}
|
||||
return Math.max(
|
||||
projectsStore.state.currentLimits.bandwidthLimit,
|
||||
parseConfigLimit(configStore.state.config.defaultPaidBandwidthLimit),
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the maximum amount of active measurement units that the usage limit can be set to.
|
||||
*/
|
||||
const availableUsageFormatted = computed<string>(() => {
|
||||
return decimalShift((availableUsage.value / Memory[activeMeasurement.value]).toFixed(2), 0);
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns an array of validation rules applied to the text input.
|
||||
*/
|
||||
const rules = computed<ValidationRule<string>[]>(() => {
|
||||
const max = availableUsage.value;
|
||||
return [
|
||||
RequiredRule,
|
||||
v => !(isNaN(+v) || isNaN(parseFloat(v))) || 'Invalid number',
|
||||
v => (parseFloat(v) > 0) || 'Number must be positive',
|
||||
v => (parseFloat(v) <= max) || 'Number is too large',
|
||||
];
|
||||
});
|
||||
|
||||
/**
|
||||
* Parses limit value from config, returning it as a byte amount.
|
||||
*/
|
||||
function parseConfigLimit(limit: string): number {
|
||||
const [value, unit] = limit.split(' ');
|
||||
return parseFloat(value) * Memory[unit === 'B' ? 'Bytes' : unit];
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates project limit.
|
||||
*/
|
||||
async function onSaveClick(): Promise<void> {
|
||||
if (!formValid.value) return;
|
||||
await withLoading(async () => {
|
||||
try {
|
||||
if (props.limitType === LimitToChange.Storage) {
|
||||
await projectsStore.updateProjectStorageLimit(new ProjectLimits(0, 0, input.value));
|
||||
} else {
|
||||
await projectsStore.updateProjectBandwidthLimit(new ProjectLimits(input.value));
|
||||
}
|
||||
} catch (error) {
|
||||
notify.error(error.message, AnalyticsErrorEventSource.EDIT_PROJECT_LIMIT);
|
||||
return;
|
||||
}
|
||||
|
||||
analyticsStore.eventTriggered(
|
||||
props.limitType === LimitToChange.Storage
|
||||
? AnalyticsEvent.PROJECT_STORAGE_LIMIT_UPDATED
|
||||
: AnalyticsEvent.PROJECT_BANDWIDTH_LIMIT_UPDATED,
|
||||
);
|
||||
notify.success('Limit updated successfully.');
|
||||
|
||||
model.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates input refs with value from text field.
|
||||
*/
|
||||
function updateInputText(value: string): void {
|
||||
inputText.value = value;
|
||||
const num = +value;
|
||||
if (isNaN(num) || isNaN(parseFloat(value))) return;
|
||||
input.value = Math.floor(num * Memory[activeMeasurement.value]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates input refs with value from slider.
|
||||
*/
|
||||
function updateInput(value: number): void {
|
||||
input.value = value;
|
||||
inputText.value = (value / Memory[activeMeasurement.value]).toString();
|
||||
}
|
||||
|
||||
watch(() => model.value, shown => {
|
||||
if (!shown) return;
|
||||
const project = projectsStore.state.selectedProject;
|
||||
updateInput(
|
||||
props.limitType === LimitToChange.Storage
|
||||
? projectsStore.state.currentLimits.storageLimit
|
||||
: projectsStore.state.currentLimits.bandwidthLimit,
|
||||
);
|
||||
}, { immediate: true });
|
||||
|
||||
watch(() => activeMeasurement.value, unit => {
|
||||
inputText.value = (input.value / Memory[unit]).toString();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.edit-project-limit__text-field {
|
||||
|
||||
:deep(.v-field) {
|
||||
padding-inline-end: 0;
|
||||
}
|
||||
|
||||
:deep(input) {
|
||||
text-overflow: ellipsis;
|
||||
|
||||
/* Firefox */
|
||||
appearance: textfield;
|
||||
|
||||
/* Chrome, Safari, Edge, Opera */
|
||||
|
||||
&::-webkit-outer-spin-button,
|
||||
&::-webkit-inner-spin-button {
|
||||
appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -57,7 +57,7 @@
|
||||
<v-divider class="my-2" />
|
||||
|
||||
<!-- Project Settings -->
|
||||
<v-list-item link rounded="lg">
|
||||
<v-list-item link rounded="lg" :to="`/projects/${selectedProject.id}/settings`">
|
||||
<template #prepend>
|
||||
<IconSettings />
|
||||
</template>
|
||||
|
@ -72,6 +72,11 @@ const routes: RouteRecordRaw[] = [
|
||||
name: 'Team',
|
||||
component: () => import(/* webpackChunkName: "Team" */ '@poc/views/Team.vue'),
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
name: 'Project Settings',
|
||||
component: () => import(/* webpackChunkName: "ProjectSettings" */ '@poc/views/ProjectSettings.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
279
web/satellite/vuetify-poc/src/views/ProjectSettings.vue
Normal file
279
web/satellite/vuetify-poc/src/views/ProjectSettings.vue
Normal file
@ -0,0 +1,279 @@
|
||||
// Copyright (C) 2023 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<v-container>
|
||||
<h1 class="text-h5 font-weight-bold mb-2">Project Settings</h1>
|
||||
|
||||
<v-card class="my-6">
|
||||
<v-list lines="three">
|
||||
<v-list-subheader class="mb-2">Details</v-list-subheader>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-list-item>
|
||||
<v-list-item-title>Project Name</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ project.name }}</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-list-item-action>
|
||||
<v-btn variant="outlined" color="default" size="small" @click="showEditNameDialog">
|
||||
Edit Project Name
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-list-item>
|
||||
<v-list-item-title>Description</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ project.description }}</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-list-item-action>
|
||||
<v-btn variant="outlined" color="default" size="small" @click="showEditDescriptionDialog">
|
||||
Edit Description
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
|
||||
<v-card class="my-6">
|
||||
<v-list lines="three">
|
||||
<v-list-subheader class="mb-2">Limits</v-list-subheader>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-list-item v-if="!isPaidTier">
|
||||
<v-list-item-title>Free Account</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ storageLimitFormatted }} Storage / {{ bandwidthLimitFormatted }} Bandwidth.
|
||||
Need more? Upgrade now.
|
||||
</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-list-item-action>
|
||||
<v-btn variant="flat" color="primary" size="small">
|
||||
Upgrade to Pro
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<template v-else>
|
||||
<v-list-item>
|
||||
<v-list-item-title>Storage</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ storageLimitFormatted }} of {{ paidStorageLimitFormatted }} available storage.
|
||||
</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-list-item-action>
|
||||
<v-btn variant="outlined" color="default" size="small" @click="showStorageLimitDialog">
|
||||
Edit Storage Limit
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-list-item>
|
||||
<v-list-item-title>Bandwidth</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ bandwidthLimitFormatted }} of {{ paidBandwidthLimitFormatted }} available bandwidth.
|
||||
</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-list-item-action>
|
||||
<v-btn variant="outlined" color="default" size="small" @click="showBandwidthLimitDialog">
|
||||
Edit Bandwidth Limit
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-list-item>
|
||||
<v-list-item-title>Account</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ paidStorageLimitFormatted }} storage and {{ paidBandwidthLimitFormatted }} bandwidth per month.
|
||||
</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-list-item-action>
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
color="default"
|
||||
size="small"
|
||||
:href="projectLimitsIncreaseRequestURL"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Request Increase
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-container>
|
||||
|
||||
<edit-project-details-dialog v-model="isEditDetailsDialogShown" :field="fieldToChange" />
|
||||
<edit-project-limit-dialog v-model="isEditLimitDialogShown" :limit-type="limitToChange" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import {
|
||||
VContainer,
|
||||
VCard,
|
||||
VList,
|
||||
VListSubheader,
|
||||
VDivider,
|
||||
VListItem,
|
||||
VListItemTitle,
|
||||
VListItemSubtitle,
|
||||
VListItemAction,
|
||||
VBtn,
|
||||
} from 'vuetify/components';
|
||||
|
||||
import { useProjectsStore } from '@/store/modules/projectsStore';
|
||||
import { FieldToChange, LimitToChange, Project } from '@/types/projects';
|
||||
import { useUsersStore } from '@/store/modules/usersStore';
|
||||
import { Memory, Size } from '@/utils/bytesSize';
|
||||
import { useConfigStore } from '@/store/modules/configStore';
|
||||
import { decimalShift } from '@/utils/strings';
|
||||
import { useNotify } from '@/utils/hooks';
|
||||
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
|
||||
|
||||
import EditProjectDetailsDialog from '@poc/components/dialogs/EditProjectDetailsDialog.vue';
|
||||
import EditProjectLimitDialog from '@poc/components/dialogs/EditProjectLimitDialog.vue';
|
||||
|
||||
const isEditDetailsDialogShown = ref<boolean>(false);
|
||||
const isEditLimitDialogShown = ref<boolean>(false);
|
||||
const fieldToChange = ref<FieldToChange>(FieldToChange.Name);
|
||||
const limitToChange = ref<LimitToChange>(LimitToChange.Storage);
|
||||
|
||||
const projectsStore = useProjectsStore();
|
||||
const usersStore = useUsersStore();
|
||||
const configStore = useConfigStore();
|
||||
|
||||
const notify = useNotify();
|
||||
|
||||
/**
|
||||
* Returns selected project from the store.
|
||||
*/
|
||||
const project = computed<Project>(() => {
|
||||
return projectsStore.state.selectedProject;
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns user's paid tier status from store.
|
||||
*/
|
||||
const isPaidTier = computed<boolean>(() => {
|
||||
return usersStore.state.user.paidTier;
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns formatted storage limit.
|
||||
*/
|
||||
const storageLimitFormatted = computed<string>(() => {
|
||||
return formatLimit(projectsStore.state.currentLimits.storageLimit);
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns formatted bandwidth limit.
|
||||
*/
|
||||
const bandwidthLimitFormatted = computed<string>(() => {
|
||||
return formatLimit(projectsStore.state.currentLimits.bandwidthLimit);
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns formatted paid tier storage limit.
|
||||
*/
|
||||
const paidStorageLimitFormatted = computed<string>(() => {
|
||||
const limit = Math.max(
|
||||
projectsStore.state.currentLimits.storageLimit,
|
||||
parseConfigLimit(configStore.state.config.defaultPaidStorageLimit),
|
||||
);
|
||||
return formatLimit(limit);
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns formatted paid tier bandwidth limit.
|
||||
*/
|
||||
const paidBandwidthLimitFormatted = computed<string>(() => {
|
||||
const limit = Math.max(
|
||||
projectsStore.state.currentLimits.bandwidthLimit,
|
||||
parseConfigLimit(configStore.state.config.defaultPaidBandwidthLimit),
|
||||
);
|
||||
return formatLimit(limit);
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns project limits increase request URL from config.
|
||||
*/
|
||||
const projectLimitsIncreaseRequestURL = computed((): string => {
|
||||
return configStore.state.config.projectLimitsIncreaseRequestURL;
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns formatted limit value.
|
||||
*/
|
||||
function formatLimit(limit: number): string {
|
||||
const size = new Size(limit, 2);
|
||||
return `${decimalShift(size.formattedBytes, 0)} ${size.label}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the Edit Project Name dialog.
|
||||
*/
|
||||
function showEditNameDialog(): void {
|
||||
fieldToChange.value = FieldToChange.Name;
|
||||
isEditDetailsDialogShown.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the Edit Description dialog.
|
||||
*/
|
||||
function showEditDescriptionDialog(): void {
|
||||
fieldToChange.value = FieldToChange.Description;
|
||||
isEditDetailsDialogShown.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the Storage Limit dialog.
|
||||
*/
|
||||
function showStorageLimitDialog(): void {
|
||||
limitToChange.value = LimitToChange.Storage;
|
||||
isEditLimitDialogShown.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the Bandwidth Limit dialog.
|
||||
*/
|
||||
function showBandwidthLimitDialog(): void {
|
||||
limitToChange.value = LimitToChange.Bandwidth;
|
||||
isEditLimitDialogShown.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses limit value from config, returning it as a byte amount.
|
||||
*/
|
||||
function parseConfigLimit(limit: string): number {
|
||||
const [value, unit] = limit.split(' ');
|
||||
return parseFloat(value) * Memory[unit === 'B' ? 'Bytes' : unit];
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle hook after initial render.
|
||||
* Fetches project limits.
|
||||
*/
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await projectsStore.getProjectLimits(project.value.id);
|
||||
} catch (error) {
|
||||
notify.error(`Error fetching project limits. ${error.message}`, AnalyticsErrorEventSource.PROJECT_SETTINGS_AREA);
|
||||
}
|
||||
});
|
||||
</script>
|
Loading…
Reference in New Issue
Block a user