Skip to content

Commit

Permalink
satellite/admin/back-office/ui: Project view
Browse files Browse the repository at this point in the history
This commit map the get project API endpoint to the UI to show its
information in the project view.

issue: #6476

Change-Id: I50d065b65c2de2425d7e90720b18a3ea00fc7fcf
  • Loading branch information
cam-a authored and ifraixedes committed Jan 8, 2024
1 parent e86070c commit 63e67cf
Show file tree
Hide file tree
Showing 6 changed files with 234 additions and 50 deletions.
Expand Up @@ -28,8 +28,9 @@
<v-icon icon="mdi-dots-horizontal" />
</v-btn>
<v-chip
variant="text" color="default" size="small" router-link to="/project-details"
variant="text" color="default" size="small"
class="font-weight-bold pl-1 ml-1"
@click="selectProject(item.raw.id)"
>
<template #prepend>
<svg class="mr-2" width="24" height="24" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
Expand Down Expand Up @@ -134,6 +135,7 @@

<script setup lang="ts">
import { computed, ref } from 'vue';
import { useRouter } from 'vue-router';
import { VCard, VTextField, VBtn, VIcon, VTooltip, VChip } from 'vuetify/components';
import { VDataTable } from 'vuetify/labs/components';
Expand Down Expand Up @@ -165,6 +167,7 @@ type ProjectTableSlotProps = { item: { raw: ProjectTableItem } };
const search = ref<string>('');
const selected = ref<string[]>([]);
const sortBy = ref([{ key: 'name', order: 'asc' }]);
const router = useRouter();
const headers = [
{ title: 'Name', key: 'name' },
Expand Down Expand Up @@ -210,6 +213,14 @@ const projects = computed<ProjectTableItem[]>(() => {
}));
});
/**
* Selects the project and navigates to the project dashboard.
*/
async function selectProject(id: string): Promise<void> {
await appStore.selectProject(id);
router.push('/project-details');
}
function getPercentColor(percent: number) {
if (percent >= 99) {
return 'error';
Expand Down
Expand Up @@ -6,23 +6,23 @@
<v-card-item class="pt-1">
<v-row>
<v-col cols="12" class="pb-1">
<v-progress-linear :color="color" :model-value="progress" rounded height="6" />
<v-progress-linear :color="color" :model-value="percentage" rounded height="6" />
</v-col>

<v-col cols="6">
<p class="text-medium-emphasis">Used</p>
<h4>{{ used }}</h4>
<h4>{{ onlyLimit ? "---" : format(used || 0) }}</h4>
<v-divider class="my-3" />
<p class="text-medium-emphasis">Percentage</p>
<h4 class="">{{ percentage }}</h4>
<h4 class="">{{ onlyLimit ? "---" : percentage+'%' }}</h4>
</v-col>

<v-col cols="6">
<p class="text-right text-medium-emphasis">Available</p>
<h4 class="text-right">{{ available }}</h4>
<h4 class="text-right">{{ onlyLimit ? "---" : format(available) }}</h4>
<v-divider class="my-3" />
<p class="text-right text-medium-emphasis">Limit</p>
<h4 class="text-right">{{ limit }}</h4>
<h4 class="text-right">{{ format(limit) }}</h4>
</v-col>

<v-divider />
Expand All @@ -36,6 +36,7 @@
</template>

<script setup lang="ts">
import { computed } from 'vue';
import {
VCard,
VCardItem,
Expand All @@ -46,13 +47,50 @@ import {
VBtn,
} from 'vuetify/components';
import { Dimensions, Size } from '@/utils/bytesSize';
const props = defineProps<{
title: string;
progress: number;
used: string;
limit: string;
available: string;
percentage: string;
isBytes?: boolean;
onlyLimit?: boolean;
used?: number;
limit: number;
color: string;
}>();
</script>
const percentage = computed((): string => {
if (props.onlyLimit || !props.used) {
return '0';
}
const p = props.used/props.limit * 100;
return Math.round(p).toString();
});
const available = computed((): number => {
if (props.onlyLimit || !props.used) {
return 0;
}
return props.limit - props.used;
});
/**
* Returns a stringify val considering if val is expressed in bytes and it that case it returns the
* value in the best human readable memory size unit rounding down to 0 when its expressed in bytes
* and truncating the decimals when its expressed in other units.
*/
function format(val: number): string {
if (!props.isBytes) {
return val.toString();
}
const valmem = new Size(val, 2);
switch (valmem.label) {
case Dimensions.Bytes:
return '0';
default:
return `${valmem.formattedBytes.replace(/\.0+$/, '')}${valmem.label}`;
}
}
</script>
22 changes: 21 additions & 1 deletion satellite/admin/back-office/ui/src/store/app.ts
Expand Up @@ -4,18 +4,20 @@
import { reactive } from 'vue';
import { defineStore } from 'pinia';

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

class AppState {
public placements: PlacementInfo[];
public userAccount: UserAccount | null = null;
public selectedProject: Project | null = null;
}

export const useAppStore = defineStore('app', () => {
const state = reactive<AppState>(new AppState());

const userApi = new UserManagementHttpApiV1();
const placementApi = new PlacementManagementHttpApiV1();
const projectApi = new ProjectManagementHttpApiV1();

async function getUserByEmail(email: string): Promise<void> {
state.userAccount = await userApi.getUserByEmail(email);
Expand All @@ -29,10 +31,28 @@ export const useAppStore = defineStore('app', () => {
state.placements = await placementApi.getPlacements();
}

function getPlacementText(code: number): string {
for (const placement of state.placements) {
if (placement.id === code) {
if (placement.location) {
return placement.location;
}
break;
}
}
return `Unknown (${code})`;
}

async function selectProject(id: string): Promise<void> {
state.selectedProject = await projectApi.getProject(id);
}

return {
state,
getUserByEmail,
clearUser,
getPlacements,
getPlacementText,
selectProject,
};
});
94 changes: 94 additions & 0 deletions satellite/admin/back-office/ui/src/utils/bytesSize.ts
@@ -0,0 +1,94 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.

export enum Memory {
Bytes = 1e0,
KB = 1e3,
MB = 1e6,
GB = 1e9,
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 {
Bytes = 'B',
KB = 'KB',
MB = 'MB',
GB = 'GB',
TB = 'TB',
PB = 'PB',
}

export class Size {
private readonly precision: number;
public readonly bytes: number;
public readonly formattedBytes: string;
public readonly label: Dimensions;

public constructor(bytes: number, precision = 0) {
const _bytes = Math.ceil(bytes);
this.bytes = bytes;
this.precision = precision;

switch (true) {
case _bytes === 0:
this.formattedBytes = (bytes / Memory.Bytes).toFixed(this.precision);
this.label = Dimensions.Bytes;
break;
case _bytes < Memory.MB:
this.formattedBytes = (bytes / Memory.KB).toFixed(this.precision);
this.label = Dimensions.KB;
break;
case _bytes < Memory.GB:
this.formattedBytes = (bytes / Memory.MB).toFixed(this.precision);
this.label = Dimensions.MB;
break;
case _bytes < Memory.TB:
this.formattedBytes = (bytes / Memory.GB).toFixed(this.precision);
this.label = Dimensions.GB;
break;
case _bytes < Memory.PB:
this.formattedBytes = (bytes / Memory.TB).toFixed(this.precision);
this.label = Dimensions.TB;
break;
default:
this.formattedBytes = (bytes / Memory.PB).toFixed(this.precision);
this.label = Dimensions.PB;
}
}

/**
* Base10String converts size to a string using base-10 prefixes.
* @param size in bytes
*/
public static toBase10String(size: number): string {
const decimals = 2;

const _size = Math.abs(size);

switch (true) {
case _size >= Memory.EB * 2 / 3:
return `${parseFloat((size / Memory.EB).toFixed(decimals))}EB`;
case _size >= Memory.PB * 2 / 3:
return `${parseFloat((size / Memory.PB).toFixed(decimals))}PB`;
case _size >= Memory.TB * 2 / 3:
return `${parseFloat((size / Memory.TB).toFixed(decimals))}TB`;
case _size >= Memory.GB * 2 / 3:
return `${parseFloat((size / Memory.GB).toFixed(decimals))}GB`;
case _size >= Memory.MB * 2 / 3:
return `${parseFloat((size / Memory.MB).toFixed(decimals))}MB`;
case _size >= Memory.KB * 2 / 3:
return `${parseFloat((size / Memory.KB).toFixed(decimals))}KB`;
default:
return `${size}B`;
}
}
}
26 changes: 9 additions & 17 deletions satellite/admin/back-office/ui/src/views/AccountDetails.vue
Expand Up @@ -210,21 +210,6 @@ const userAccount = computed<UserAccount>(() => appStore.state.userAccount as Us
*/
const createdAt = computed<Date>(() => new Date(userAccount.value.createdAt));
/**
* Returns the string representation of the user's default placement.
*/
const placementText = computed<string>(() => {
for (const placement of appStore.state.placements) {
if (placement.id === userAccount.value.defaultPlacement) {
if (placement.location) {
return placement.location;
}
break;
}
}
return `Unknown (${userAccount.value.defaultPlacement})`;
});
type Usage = {
storage: number | null;
download: number;
Expand Down Expand Up @@ -258,17 +243,24 @@ const totalUsage = computed<Usage>(() => {
return total;
});
const placementText = computed<string>(() => {
return appStore.getPlacementText(userAccount.value.defaultPlacement);
});
/**
* Returns whether an error occurred retrieving usage data from the Redis live accounting cache.
*/
const usageCacheError = computed<boolean>(() => {
return !!userAccount.value.projects?.some(project =>
project.storageUsed === null ||
project.bandwidthUsed === null ||
project.segmentUsed === null,
);
});
onBeforeMount(() => !userAccount.value && router.push('/accounts'));
onUnmounted(appStore.clearUser);
onUnmounted(() => {
if (appStore.state.selectedProject === null) {
appStore.clearUser;
}
});
</script>

0 comments on commit 63e67cf

Please sign in to comment.