Commit 2c029fcf authored by Dhiraj Bodicherla's avatar Dhiraj Bodicherla Committed by Vitaly Slobodin

Add usage stats to quotas page

This MR adds more info about usage statistics
in the Usage Quotas page
parent 83fe18a3
export const BYTES_IN_KIB = 1024;
export const BYTES_IN_KB = 1000;
export const HIDDEN_CLASS = 'hidden';
export const TRUNCATE_WIDTH_DEFAULT_WIDTH = 80;
export const TRUNCATE_WIDTH_DEFAULT_FONT_SIZE = 12;
......
import { BYTES_IN_KIB } from './constants';
import { BYTES_IN_KIB, BYTES_IN_KB } from './constants';
import { sprintf, __ } from '~/locale';
/**
......@@ -34,6 +34,18 @@ export function formatRelevantDigits(number) {
return formattedNumber;
}
/**
* Utility function that calculates KB of the given bytes.
* Note: This method calculates KiloBytes as opposed to
* Kibibytes. For Kibibytes, bytesToKiB should be used.
*
* @param {Number} number bytes
* @return {Number} KiB
*/
export function bytesToKB(number) {
return number / BYTES_IN_KB;
}
/**
* Utility function that calculates KiB of the given bytes.
*
......
<script>
import { GlLink, GlSprintf, GlModalDirective, GlButton, GlIcon } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ProjectsTable from './projects_table.vue';
import UsageGraph from './usage_graph.vue';
import UsageStatistics from './usage_statistics.vue';
import query from '../queries/storage.query.graphql';
import TemporaryStorageIncreaseModal from './temporary_storage_increase_modal.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
......@@ -16,11 +18,13 @@ export default {
GlSprintf,
GlIcon,
UsageGraph,
UsageStatistics,
TemporaryStorageIncreaseModal,
},
directives: {
GlModalDirective,
},
mixins: [glFeatureFlagsMixin()],
props: {
namespacePath: {
type: String,
......@@ -78,6 +82,9 @@ export default {
isStorageIncreaseModalVisible() {
return parseBoolean(this.isTemporaryStorageIncreaseVisible);
},
isAdditionalStorageFlagEnabled() {
return this.glFeatures.additionalRepoStorageByNamespace;
},
},
methods: {
formatSize(size) {
......@@ -89,9 +96,12 @@ export default {
</script>
<template>
<div>
<div class="pipeline-quota container-fluid py-4 px-2 m-0">
<div class="row py-0 d-flex align-items-center">
<div class="col-lg-6">
<div v-if="isAdditionalStorageFlagEnabled && namespace.rootStorageStatistics">
<usage-statistics :root-storage-statistics="namespace.rootStorageStatistics" />
</div>
<div v-else class="gl-py-4 gl-px-2 gl-m-0">
<div class="gl-display-flex gl-align-items-center">
<div class="gl-w-half">
<gl-sprintf :message="s__('UsageQuota|You used: %{usage} %{limit}')">
<template #usage>
<span class="gl-font-weight-bold" data-testid="total-usage">
......@@ -117,7 +127,7 @@ export default {
<gl-icon name="question" :size="12" />
</gl-link>
</div>
<div class="col-lg-6 text-lg-right">
<div class="gl-w-half gl-text-right">
<gl-button
v-if="isStorageIncreaseModalVisible"
v-gl-modal-directive="$options.modalId"
......@@ -136,14 +146,11 @@ export default {
>
</div>
</div>
<div class="row py-0">
<div class="col-sm-12">
<usage-graph
v-if="namespace.rootStorageStatistics"
:root-storage-statistics="namespace.rootStorageStatistics"
:limit="namespace.limit"
/>
</div>
<div v-if="namespace.rootStorageStatistics" class="gl-w-full">
<usage-graph
:root-storage-statistics="namespace.rootStorageStatistics"
:limit="namespace.limit"
/>
</div>
</div>
<projects-table :projects="namespaceProjects" />
......
<script>
import { GlButton } from '@gitlab/ui';
import UsageStatisticsCard from './usage_statistics_card.vue';
import { s__ } from '~/locale';
import { bytesToKB } from '~/lib/utils/number_utils';
import { getFormatter, SUPPORTED_FORMATS } from '~/lib/utils/unit_format';
export default {
components: {
GlButton,
UsageStatisticsCard,
},
props: {
rootStorageStatistics: {
required: true,
type: Object,
},
},
computed: {
totalUsage() {
const { repositorySize = 0, lfsObjectsSize = 0 } = this.rootStorageStatistics;
return {
usage: this.formatSize(repositorySize + lfsObjectsSize),
description: s__('UsageQuota|Total namespace storage used'),
link: {
text: s__('UsageQuota|Learn more about usage quotas'),
url: '#',
},
};
},
excessUsage() {
return {
usage: this.formatSize(0),
description: s__('UsageQuota|Total excess storage used'),
link: {
text: s__('UsageQuota|Learn more about excess storage usage'),
url: '#',
},
};
},
purchasedUsage() {
return {
usage: this.formatSize(0),
description: s__('UsageQuota|Purchased storage available'),
link: {
text: s__('UsageQuota|Purchase more storage'),
url: '#',
},
};
},
},
methods: {
/**
* The formatDecimalBytes method returns
* value along with the unit. However, the unit
* and the value needs to be separated so that
* they can have different styles. The method
* splits the value into value and unit.
*
* We want to display all units above bytes. Hence
* converting bytesToKB before passing it to
* `getFormatter`
*
* @params {Number} size size in bytes
* @returns {Object} value and unit of formatted size
*/
formatSize(size) {
const formatDecimalBytes = getFormatter(SUPPORTED_FORMATS.kilobytes);
const formattedSize = formatDecimalBytes(bytesToKB(size), 1);
return {
value: formattedSize.slice(0, -2),
unit: formattedSize.slice(-2),
};
},
},
};
</script>
<template>
<div class="gl-display-flex gl-sm-flex-direction-column">
<usage-statistics-card
data-testid="totalUsage"
:usage="totalUsage.usage"
:link="totalUsage.link"
:description="totalUsage.description"
css-class="gl-mr-4"
/>
<usage-statistics-card
data-testid="excessUsage"
:usage="excessUsage.usage"
:link="excessUsage.link"
:description="excessUsage.description"
css-class="gl-mx-4"
/>
<usage-statistics-card
data-testid="purchasedUsage"
:usage="purchasedUsage.usage"
:link="purchasedUsage.link"
:description="purchasedUsage.description"
css-class="gl-ml-4"
>
<template #link="{link}">
<gl-button
target="_blank"
:href="link.url"
class="mb-0"
variant="success"
category="primary"
block
>
{{ link.text }}
</gl-button>
</template>
</usage-statistics-card>
</div>
</template>
<script>
import { GlLink, GlIcon, GlSprintf } from '@gitlab/ui';
export default {
components: {
GlIcon,
GlLink,
GlSprintf,
},
props: {
link: {
type: Object,
required: false,
default: () => ({ text: '', url: '' }),
},
description: {
type: String,
required: true,
},
usage: {
type: Object,
required: true,
},
cssClass: {
type: String,
required: false,
default: '',
},
},
};
</script>
<template>
<div class="gl-p-5 gl-my-5 gl-bg-gray-10 gl-flex-fill-1 gl-white-space-nowrap" :class="cssClass">
<p class="mb-2">
<gl-sprintf :message="__('%{size} %{unit}')">
<template #size>
<span class="gl-font-size-h-display gl-font-weight-bold">{{ usage.value }}</span>
</template>
<template #unit>
<span class="gl-font-lg gl-font-weight-bold">{{ usage.unit }}</span>
</template>
</gl-sprintf>
</p>
<p class="gl-border-b-2 gl-border-b-solid gl-border-b-gray-100 gl-font-weight-bold gl-pb-3">
{{ description }}
</p>
<p class="gl-mb-0">
<slot v-bind="{ link }" name="link">
<gl-link target="_blank" :href="link.url">
<span class="text-truncate">{{ link.text }}</span>
<gl-icon name="external-link" class="gl-ml-2 gl-flex-shrink-0 gl-text-black-normal" />
</gl-link>
</slot>
</p>
</div>
</template>
import { mount } from '@vue/test-utils';
import StorageApp from 'ee/storage_counter/components/app.vue';
import Project from 'ee/storage_counter/components/project.vue';
import UsageGraph from 'ee/storage_counter/components/usage_graph.vue';
import UsageStatistics from 'ee/storage_counter/components/usage_statistics.vue';
import TemporaryStorageIncreaseModal from 'ee/storage_counter/components/temporary_storage_increase_modal.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { namespaceData, withRootStorageStatistics } from '../mock_data';
......@@ -15,8 +17,14 @@ describe('Storage counter app', () => {
const findPurchaseStorageLink = () => wrapper.find("[data-testid='purchase-storage-link']");
const findTemporaryStorageIncreaseButton = () =>
wrapper.find("[data-testid='temporary-storage-increase-button']");
function createComponent(props = {}, loading = false) {
const findUsageGraph = () => wrapper.find(UsageGraph);
const findUsageStatistics = () => wrapper.find(UsageStatistics);
const createComponent = ({
props = {},
loading = false,
additionalRepoStorageByNamespace = false,
} = {}) => {
const $apollo = {
queries: {
namespace: {
......@@ -31,8 +39,13 @@ describe('Storage counter app', () => {
directives: {
GlModalDirective: createMockDirective(),
},
provide: {
glFeatures: {
additionalRepoStorageByNamespace,
},
},
});
}
};
beforeEach(() => {
createComponent();
......@@ -86,6 +99,34 @@ describe('Storage counter app', () => {
});
});
describe('with additional_repo_storage_by_namespace feature flag', () => {
it('usage_graph component hidden is when flag is false', async () => {
wrapper.setData({
namespace: withRootStorageStatistics,
});
await wrapper.vm.$nextTick();
expect(findUsageGraph().exists()).toBe(true);
expect(findUsageStatistics().exists()).toBe(false);
});
it('usage_statistics component is rendered when flag is true', async () => {
createComponent({
additionalRepoStorageByNamespace: true,
});
wrapper.setData({
namespace: withRootStorageStatistics,
});
await wrapper.vm.$nextTick();
expect(findUsageStatistics().exists()).toBe(true);
expect(findUsageGraph().exists()).toBe(false);
});
});
describe('without rootStorageStatistics information', () => {
it('renders N/A', async () => {
wrapper.setData({
......@@ -107,7 +148,7 @@ describe('Storage counter app', () => {
describe('when purchaseStorageUrl is set', () => {
beforeEach(() => {
createComponent({ purchaseStorageUrl: 'customers.gitlab.com' });
createComponent({ props: { purchaseStorageUrl: 'customers.gitlab.com' } });
});
it('does render link', () => {
......@@ -127,7 +168,7 @@ describe('Storage counter app', () => {
${{ isTemporaryStorageIncreaseVisible: 'true' }} | ${true}
`('with $props', ({ props, isVisible }) => {
beforeEach(() => {
createComponent(props);
createComponent({ props });
});
it(`renders button = ${isVisible}`, () => {
......@@ -137,7 +178,7 @@ describe('Storage counter app', () => {
describe('when temporary storage increase is visible', () => {
beforeEach(() => {
createComponent({ isTemporaryStorageIncreaseVisible: 'true' });
createComponent({ props: { isTemporaryStorageIncreaseVisible: 'true' } });
wrapper.setData({
namespace: {
...namespaceData,
......
import { shallowMount } from '@vue/test-utils';
import { GlButton, GlLink } from '@gitlab/ui';
import UsageStatistics from 'ee/storage_counter/components/usage_statistics.vue';
import UsageStatisticsCard from 'ee/storage_counter/components/usage_statistics_card.vue';
import { withRootStorageStatistics } from '../mock_data';
describe('Usage Statistics component', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMount(UsageStatistics, {
propsData: {
rootStorageStatistics: withRootStorageStatistics.rootStorageStatistics,
},
stubs: {
UsageStatisticsCard,
GlLink,
},
});
};
const getStatisticsCards = () => wrapper.findAll(UsageStatisticsCard);
const getStatisticsCard = testId => wrapper.find(`[data-testid="${testId}"]`);
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('renders three statistics cards', () => {
expect(getStatisticsCards()).toHaveLength(3);
});
it.each`
cardName | componentName | componentType
${'totalUsage'} | ${'GlLink'} | ${GlLink}
${'excessUsage'} | ${'GlLink'} | ${GlLink}
${'purchasedUsage'} | ${'GlButton'} | ${GlButton}
`('renders $componentName in $cardName', ({ cardName, componentType }) => {
expect(
getStatisticsCard(cardName)
.find(componentType)
.exists(),
).toBe(true);
});
});
......@@ -55,4 +55,17 @@ export const namespaceData = {
projects,
};
export const withRootStorageStatistics = { ...projects, totalUsage: 3261070 };
export const withRootStorageStatistics = {
projects,
limit: 10000000,
totalUsage: 129334601,
rootStorageStatistics: {
storageSize: 129334601,
repositorySize: 46012030,
lfsObjectsSize: 4329334601203,
buildArtifactsSize: 1272375,
packagesSize: 123123120,
wikiSize: 1000,
snippetsSize: 10000,
},
};
......@@ -746,6 +746,9 @@ msgid_plural "%{securityScanner} results are not available because a pipeline ha
msgstr[0] ""
msgstr[1] ""
msgid "%{size} %{unit}"
msgstr ""
msgid "%{size} GiB"
msgstr ""
......@@ -28072,6 +28075,12 @@ msgstr ""
msgid "UsageQuota|LFS Storage"
msgstr ""
msgid "UsageQuota|Learn more about excess storage usage"
msgstr ""
msgid "UsageQuota|Learn more about usage quotas"
msgstr ""
msgid "UsageQuota|Packages"
msgstr ""
......@@ -28081,6 +28090,9 @@ msgstr ""
msgid "UsageQuota|Purchase more storage"
msgstr ""
msgid "UsageQuota|Purchased storage available"
msgstr ""
msgid "UsageQuota|Repositories"
msgstr ""
......@@ -28102,6 +28114,12 @@ msgstr ""
msgid "UsageQuota|This project is locked."
msgstr ""
msgid "UsageQuota|Total excess storage used"
msgstr ""
msgid "UsageQuota|Total namespace storage used"
msgstr ""
msgid "UsageQuota|Unlimited"
msgstr ""
......
import {
formatRelevantDigits,
bytesToKB,
bytesToKiB,
bytesToMiB,
bytesToGiB,
......@@ -54,6 +55,16 @@ describe('Number Utils', () => {
});
});
describe('bytesToKB', () => {
it.each`
input | output
${1000} | ${1}
${1024} | ${1.024}
`('returns $output KB for $input bytes', ({ input, output }) => {
expect(bytesToKB(input)).toBe(output);
});
});
describe('bytesToKiB', () => {
it('calculates KiB for the given bytes', () => {
expect(bytesToKiB(1024)).toEqual(1);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment