Skip to content

Commit

Permalink
web/satellite: implemented Emissions dialog for project dashboard
Browse files Browse the repository at this point in the history
Added new dialog to show detailed info about emission impact calculations.
Dialog is shown on emission-related cards click on project dashboard.

Issue:
#6694
#6793

Change-Id: Iac287d75dae84df9b33c94023ed45d5452bc0fec
  • Loading branch information
VitaliiShpital authored and Storj Robot committed Feb 21, 2024
1 parent e6ac834 commit 699d267
Show file tree
Hide file tree
Showing 4 changed files with 189 additions and 70 deletions.
12 changes: 12 additions & 0 deletions web/satellite/src/assets/icon-color-globe.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 9 additions & 32 deletions web/satellite/src/components/CardStatsComponent.vue
Expand Up @@ -2,47 +2,24 @@
// See LICENSE for copying information.

<template>
<v-card :subtitle="subtitle" variant="outlined" border rounded="xlg" :to="to">
<template #title>
<v-card-title class="d-flex align-center">
<component :is="iconComponent" v-if="icon" v-bind="iconProps" class="mr-2" width="16" height="16" />
{{ title }}
</v-card-title>
</template>
<v-card :title="title" :subtitle="subtitle" variant="outlined" border rounded="xlg" :to="to">
<v-card-text>
<v-chip rounded color="default" variant="tonal" class="font-weight-bold" :to="to">{{ data }}</v-chip>
<v-chip :color="color" variant="tonal" class="font-weight-bold mt-n1" :to="to">{{ data }}</v-chip>
</v-card-text>
</v-card>
</template>

<script setup lang="ts">
import { computed, Component } from 'vue';
import { VCard, VCardText, VChip, VCardTitle } from 'vuetify/components';
import { VCard, VCardText, VChip } from 'vuetify/components';
import IconFile from '@/components/icons/IconFile.vue';
import IconGlobe from '@/components/icons/IconGlobe.vue';
import IconBucket from '@/components/icons/IconBucket.vue';
import IconAccess from '@/components/icons/IconAccess.vue';
import IconTeam from '@/components/icons/IconTeam.vue';
import IconCard from '@/components/icons/IconCard.vue';
const props = defineProps<{
const props = withDefaults(defineProps<{
title: string;
subtitle: string;
data: string;
color?: string;
to?: string;
icon?: keyof typeof iconComponents;
}>();
const iconComponents = {
file: IconFile,
globe: IconGlobe,
bucket: IconBucket,
access: IconAccess,
team: IconTeam,
card: IconCard,
};
const iconComponent = computed<Component | null>(() => props.icon ? iconComponents[props.icon] : null);
const iconProps = computed<object | null>(() => iconComponent.value?.['props']?.['bold'] ? { bold: true } : null);
}>(), {
color: 'info',
to: undefined,
});
</script>
119 changes: 119 additions & 0 deletions web/satellite/src/components/dialogs/EmissionsDialog.vue
@@ -0,0 +1,119 @@
// Copyright (C) 2024 Storj Labs, Inc.
// See LICENSE for copying information.

<template>
<v-dialog
v-model="model"
activator="parent"
width="auto"
min-width="400px"
max-width="450px"
transition="fade-transition"
>
<v-card rounded="xlg">
<v-sheet>
<v-card-item class="py-4 pl-6">
<template #prepend>
<img src="@/assets/icon-color-globe.svg" alt="Earth" width="40" class="mt-1">
</template>
<v-card-title class="font-weight-bold">
Storj Sustainability
</v-card-title>
<template #append>
<v-btn
icon="$close"
variant="text"
size="small"
color="default"
@click="model = false"
/>
</template>
</v-card-item>
</v-sheet>

<v-card-text class="mt-n4 px-6">
<v-card class="pa-4 mb-4">
<p class="text-body-2 font-weight-bold mb-2">Carbon Emissions</p>
<v-chip variant="tonal" color="info" class="font-weight-bold">
{{ emission.storjImpact.toLocaleString(undefined, { maximumFractionDigits: 0 }) }} kg CO₂e
</v-chip>
<p class="text-body-2 mt-2">Estimated for this Storj project. <a href="https://www.storj.io/documents/storj-sustainability-whitepaper.pdf" target="_blank" rel="noopener noreferrer" class="link font-weight-bold">Learn more</a></p>
</v-card>
<v-card class="pa-4 mb-4">
<p class="text-body-2 font-weight-bold mb-2">Carbon Comparison</p>
<v-chip variant="tonal" color="warning" class="font-weight-bold">
{{ emission.hyperscalerImpact.toLocaleString(undefined, { maximumFractionDigits: 0 }) }} kg CO₂e
</v-chip>
<p class="text-body-2 mt-2">By using traditional cloud storage. <a href="https://www.storj.io/documents/storj-sustainability-whitepaper.pdf" target="_blank" rel="noopener noreferrer" class="link font-weight-bold">Learn more</a></p>
</v-card>
<v-card class="pa-4 mb-4">
<p class="text-body-2 font-weight-bold mb-2">Total Carbon Avoided</p>
<v-chip variant="tonal" color="green" class="font-weight-bold">
{{ co2Savings }} kg CO₂e
</v-chip>
<p class="text-body-2 mt-2">Estimated by using Storj. <a href="https://www.storj.io/documents/storj-sustainability-whitepaper.pdf" target="_blank" rel="noopener noreferrer" class="link font-weight-bold">Learn more</a></p>
</v-card>
<v-card class="pa-4 mb-2">
<p class="text-body-2 font-weight-bold mb-2">Carbon Avoided Equals To</p>
<v-chip variant="tonal" color="green" class="font-weight-bold">
{{ emission.savedTrees.toLocaleString() }} trees grown for 10 years
</v-chip>
<p class="text-body-2 mt-2">Estimated equivalencies. <a href="https://www.epa.gov/energy/greenhouse-gases-equivalencies-calculator-calculations-and-references#seedlings" target="_blank" rel="noopener noreferrer" class="link font-weight-bold">Learn more</a></p>
</v-card>
</v-card-text>

<v-card-actions class="pa-6 pt-2">
<v-row>
<v-col>
<v-btn color="primary" variant="flat" block link href="https://www.storj.io/documents/storj-sustainability-whitepaper.pdf" target="_blank" rel="noopener noreferrer">
Sustainability Whitepaper <v-icon :icon="mdiOpenInNew" class="ml-2" />
</v-btn>
</v-col>
</v-row>
</v-card-actions>
</v-card>
</v-dialog>
</template>

<script setup lang="ts">
import { computed, ref } from 'vue';
import {
VDialog,
VCard,
VSheet,
VCardItem,
VCardActions,
VCardTitle,
VCardText,
VBtn,
VChip,
VRow,
VCol,
VIcon,
} from 'vuetify/components';
import { mdiOpenInNew } from '@mdi/js';
import { Emission } from '@/types/projects';
import { useProjectsStore } from '@/store/modules/projectsStore';
const projectsStore = useProjectsStore();
const model = ref<boolean>(false);
/**
* Returns project's emission impact.
*/
const emission = computed<Emission>(() => {
return projectsStore.state.emission;
});
/**
* Returns calculated and formatted CO2 savings info.
*/
const co2Savings = computed<string>(() => {
let saved = emission.value.hyperscalerImpact - emission.value.storjImpact;
if (saved < 0) saved = 0;
return saved.toLocaleString(undefined, { maximumFractionDigits: 0 });
});
</script>
87 changes: 49 additions & 38 deletions web/satellite/src/views/Dashboard.vue
Expand Up @@ -36,62 +36,48 @@

<v-row class="d-flex align-center mt-2">
<v-col cols="6" md="4" lg="2">
<CardStatsComponent icon="file" title="Files" subtitle="Total files stored" :data="limits.objectCount.toLocaleString()" :to="ROUTES.Buckets.path" />
<CardStatsComponent title="Files" subtitle="Project files" :data="limits.objectCount.toLocaleString()" :to="ROUTES.Buckets.path" />
</v-col>
<v-col v-if="!emissionImpactViewEnabled" cols="6" md="4" lg="2">
<CardStatsComponent icon="globe" title="Segments" subtitle="All file pieces" :data="limits.segmentCount.toLocaleString()" :to="ROUTES.Buckets.path" />
<CardStatsComponent title="Segments" subtitle="All file pieces" :data="limits.segmentCount.toLocaleString()" :to="ROUTES.Buckets.path" />
</v-col>
<v-col cols="6" md="4" lg="2">
<CardStatsComponent icon="bucket" title="Buckets" subtitle="Storage buckets" :data="bucketsCount.toLocaleString()" :to="ROUTES.Buckets.path" />
<CardStatsComponent title="Buckets" subtitle="Project buckets" :data="bucketsCount.toLocaleString()" :to="ROUTES.Buckets.path" />
</v-col>
<v-col cols="6" md="4" lg="2">
<CardStatsComponent icon="access" title="Access" subtitle="Project keys" :data="accessGrantsCount.toLocaleString()" :to="ROUTES.Access.path" />
<CardStatsComponent title="Access" subtitle="Project accesses" :data="accessGrantsCount.toLocaleString()" :to="ROUTES.Access.path" />
</v-col>
<v-col cols="6" md="4" lg="2">
<CardStatsComponent icon="team" title="Team" subtitle="Project members" :data="teamSize.toLocaleString()" :to="ROUTES.Team.path" />
<CardStatsComponent title="Team" subtitle="Project members" :data="teamSize.toLocaleString()" :to="ROUTES.Team.path" />
</v-col>
<template v-if="emissionImpactViewEnabled">
<v-col cols="12" sm="6" md="4" lg="2">
<emissions-dialog />
<v-tooltip
activator="parent"
location="bottom"
location="top"
offset="-20"
opacity="80"
>
{{ emission.storjImpact.toLocaleString(undefined, { maximumFractionDigits: 3 }) }} kg CO2e estimated if using Storj
<br>
{{ emission.hyperscalerImpact.toLocaleString(undefined, { maximumFractionDigits: 3 }) }} kg CO2e estimated if using traditional cloud storage
<br>
<a
href="https://www.storj.io/documents/storj-sustainability-whitepaper.pdf"
target="_blank"
rel="noopener noreferrer"
>
More info about our methodology
</a>
Click to learn more
</v-tooltip>
<CardStatsComponent icon="globe" title="CO2 Saved" subtitle="By using Storj" :data="co2Savings" />
<CardStatsComponent title="CO₂ Estimated" subtitle="For this project" :data="co2Estimated" link />
</v-col>
<v-col cols="12" sm="6" md="4" lg="2">
<emissions-dialog />
<v-tooltip
activator="parent"
location="bottom"
location="top"
offset="-20"
opacity="80"
>
Number of urban tree seedlings grown for 10 years.
<br>
<a
href="https://www.epa.gov/energy/greenhouse-gases-equivalencies-calculator-calculations-and-references#seedlings"
target="_blank"
rel="noopener noreferrer"
>
Learn more about Greenhouse Gases Equivalencies
</a>
Click to learn more
</v-tooltip>
<CardStatsComponent icon="globe" title="Savings" subtitle="Equivalent" :data="`${emission.savedTrees.toLocaleString()} trees`" />
<CardStatsComponent title="CO₂ Avoided" subtitle="By using Storj" :data="co2Saved" color="green" link />
</v-col>
</template>
<v-col v-if="billingEnabled && !emissionImpactViewEnabled" cols="6" md="4" lg="2">
<CardStatsComponent icon="card" title="Billing" :subtitle="`${paidTierString} account`" :data="paidTierString" :to="ROUTES.Account.with(ROUTES.Billing).path" />
<CardStatsComponent title="Billing" :subtitle="`${paidTierString} account`" :data="paidTierString" :to="ROUTES.Account.with(ROUTES.Billing).path" />
</v-col>
</v-row>

Expand Down Expand Up @@ -286,7 +272,7 @@ import {
} from 'vuetify/components';
import { ComponentPublicInstance } from '@vue/runtime-core';
import { useRouter } from 'vue-router';
import { mdiChevronRight, mdiInformationOutline } from '@mdi/js';
import { mdiInformationOutline } from '@mdi/js';
import { useUsersStore } from '@/store/modules/usersStore';
import { useProjectsStore } from '@/store/modules/projectsStore';
Expand All @@ -300,7 +286,6 @@ import { ChartUtils } from '@/utils/chart';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import { useNotify } from '@/utils/hooks';
import { useAppStore } from '@/store/modules/appStore';
import { LocalData } from '@/utils/localData';
import { ProjectMembersPage } from '@/types/projectMembers';
import { AccessGrantsPage } from '@/types/accessGrants';
import { useConfigStore } from '@/store/modules/configStore';
Expand All @@ -318,7 +303,6 @@ import StorageChart from '@/components/StorageChart.vue';
import BucketsDataTable from '@/components/BucketsDataTable.vue';
import EditProjectLimitDialog from '@/components/dialogs/EditProjectLimitDialog.vue';
import CreateBucketDialog from '@/components/dialogs/CreateBucketDialog.vue';
import ManagePassphraseDialog from '@/components/dialogs/ManagePassphraseDialog.vue';
import IconCloud from '@/components/icons/IconCloud.vue';
import IconArrowDown from '@/components/icons/IconArrowDown.vue';
import LimitWarningBanners from '@/components/LimitWarningBanners.vue';
Expand All @@ -327,6 +311,12 @@ import IconUpgrade from '@/components/icons/IconUpgrade.vue';
import IconCirclePlus from '@/components/icons/IconCirclePlus.vue';
import NextStepsContainer from '@/components/onboarding/NextStepsContainer.vue';
import TeamPassphraseBanner from '@/components/TeamPassphraseBanner.vue';
import EmissionsDialog from '@/components/dialogs/EmissionsDialog.vue';
type ValueUnit = {
value: number
unit: string
}
const appStore = useAppStore();
const usersStore = useUsersStore();
Expand All @@ -352,13 +342,24 @@ const isDatePicker = ref<boolean>(false);
const datePickerModel = ref<Date[]>([]);
/**
* Returns calculated and formatted CO2 savings info.
* Returns formatted CO2 estimated info.
*/
const co2Savings = computed<string>(() => {
let saved = emission.value.hyperscalerImpact - emission.value.storjImpact;
if (saved < 0) saved = 0;
const co2Estimated = computed<string>(() => {
const formatted = getValueAndUnit(emission.value.storjImpact);
return `${saved.toLocaleString(undefined, { maximumFractionDigits: 3 })} kg CO2e`;
return `${formatted.value.toLocaleString(undefined, { maximumFractionDigits: 0 })} ${formatted.unit} CO₂e`;
});
/**
* Returns formatted CO2 save info.
*/
const co2Saved = computed<string>(() => {
let value = emission.value.hyperscalerImpact - emission.value.storjImpact;
if (value < 0) value = 0;
const formatted = getValueAndUnit(value);
return `${formatted.value.toLocaleString(undefined, { maximumFractionDigits: 0 })} ${formatted.unit} CO₂e`;
});
/**
Expand Down Expand Up @@ -589,6 +590,16 @@ const emission = computed<Emission>(() => {
return projectsStore.state.emission;
});
/**
* Returns adjusted value and unit.
*/
function getValueAndUnit(value: number): ValueUnit {
const unitUpgradeThreshold = 999999;
const [newValue, unit] = value > unitUpgradeThreshold ? [value / 1000, 't'] : [value, 'kg'];
return { value: newValue, unit };
}
/**
* Returns formatted amount.
*/
Expand Down

0 comments on commit 699d267

Please sign in to comment.