Commit 3632af7f authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch 'nicolasdular/storae-graph-with-limits' into 'master'

Show storage graph with limits

See merge request gitlab-org/gitlab!34931
parents fc0beec9 dbf27467
<script> <script>
import { GlLink } from '@gitlab/ui'; import { GlLink, GlSprintf } from '@gitlab/ui';
import Project from './project.vue'; import Project from './project.vue';
import UsageGraph from './usage_graph.vue'; import UsageGraph from './usage_graph.vue';
import query from '../queries/storage.graphql'; import query from '../queries/storage.graphql';
...@@ -10,6 +10,7 @@ export default { ...@@ -10,6 +10,7 @@ export default {
components: { components: {
Project, Project,
GlLink, GlLink,
GlSprintf,
Icon, Icon,
UsageGraph, UsageGraph,
}, },
...@@ -44,6 +45,7 @@ export default { ...@@ -44,6 +45,7 @@ export default {
? numberToHumanSize(data.namespace.rootStorageStatistics.storageSize) ? numberToHumanSize(data.namespace.rootStorageStatistics.storageSize)
: 'N/A', : 'N/A',
rootStorageStatistics: data.namespace.rootStorageStatistics, rootStorageStatistics: data.namespace.rootStorageStatistics,
limit: data.namespace.storageSizeLimit,
}), }),
}, },
}, },
...@@ -52,6 +54,11 @@ export default { ...@@ -52,6 +54,11 @@ export default {
namespace: {}, namespace: {},
}; };
}, },
methods: {
formatSize(size) {
return numberToHumanSize(size);
},
},
}; };
</script> </script>
<template> <template>
...@@ -59,17 +66,30 @@ export default { ...@@ -59,17 +66,30 @@ export default {
<div class="pipeline-quota container-fluid py-4 px-2 m-0"> <div class="pipeline-quota container-fluid py-4 px-2 m-0">
<div class="row py-0"> <div class="row py-0">
<div class="col-sm-12"> <div class="col-sm-12">
<strong>{{ s__('UsageQuota|Storage usage:') }}</strong> <gl-sprintf :message="s__('UsageQuota|You used: %{usage} %{limit}')">
<span data-testid="total-usage"> <template #usage>
{{ namespace.totalUsage }} <span class="gl-font-weight-bold" data-testid="total-usage">
<gl-link {{ namespace.totalUsage }}
:href="helpPagePath" </span>
target="_blank" </template>
:aria-label="s__('UsageQuota|Usage quotas help link')" <template #limit>
> <gl-sprintf
<icon name="question" :size="12" /> v-if="namespace.limit"
</gl-link> :message="s__('UsageQuota|out of %{formattedLimit} of your namespace storage')"
</span> >
<template #formattedLimit>
<span class="gl-font-weight-bold">{{ formatSize(namespace.limit) }}</span>
</template>
</gl-sprintf>
</template>
</gl-sprintf>
<gl-link
:href="helpPagePath"
target="_blank"
:aria-label="s__('UsageQuota|Usage quotas help link')"
>
<icon name="question" :size="12" />
</gl-link>
</div> </div>
</div> </div>
<div class="row py-0"> <div class="row py-0">
...@@ -77,6 +97,7 @@ export default { ...@@ -77,6 +97,7 @@ export default {
<usage-graph <usage-graph
v-if="namespace.rootStorageStatistics" v-if="namespace.rootStorageStatistics"
:root-storage-statistics="namespace.rootStorageStatistics" :root-storage-statistics="namespace.rootStorageStatistics"
:limit="namespace.limit"
/> />
</div> </div>
</div> </div>
......
...@@ -8,6 +8,10 @@ export default { ...@@ -8,6 +8,10 @@ export default {
required: true, required: true,
type: Object, type: Object,
}, },
limit: {
required: true,
type: Number,
},
}, },
computed: { computed: {
storageTypes() { storageTypes() {
...@@ -27,31 +31,31 @@ export default { ...@@ -27,31 +31,31 @@ export default {
return [ return [
{ {
name: s__('UsageQuota|Repositories'), name: s__('UsageQuota|Repositories'),
percentage: this.sizePercentage(repositorySize), style: this.usageStyle(this.sizePercentage(repositorySize)),
class: 'gl-bg-data-viz-blue-500', class: 'gl-bg-data-viz-blue-500',
size: repositorySize, size: repositorySize,
}, },
{ {
name: s__('UsageQuota|LFS Objects'), name: s__('UsageQuota|LFS Objects'),
percentage: this.sizePercentage(lfsObjectsSize), style: this.usageStyle(this.sizePercentage(lfsObjectsSize)),
class: 'gl-bg-data-viz-orange-600', class: 'gl-bg-data-viz-orange-600',
size: lfsObjectsSize, size: lfsObjectsSize,
}, },
{ {
name: s__('UsageQuota|Packages'), name: s__('UsageQuota|Packages'),
percentage: this.sizePercentage(packagesSize), style: this.usageStyle(this.sizePercentage(packagesSize)),
class: 'gl-bg-data-viz-aqua-500', class: 'gl-bg-data-viz-aqua-500',
size: packagesSize, size: packagesSize,
}, },
{ {
name: s__('UsageQuota|Build Artifacts'), name: s__('UsageQuota|Build Artifacts'),
percentage: this.sizePercentage(buildArtifactsSize), style: this.usageStyle(this.sizePercentage(buildArtifactsSize)),
class: 'gl-bg-data-viz-green-600', class: 'gl-bg-data-viz-green-600',
size: buildArtifactsSize, size: buildArtifactsSize,
}, },
{ {
name: s__('UsageQuota|Wikis'), name: s__('UsageQuota|Wikis'),
percentage: this.sizePercentage(wikiSize), style: this.usageStyle(this.sizePercentage(wikiSize)),
class: 'gl-bg-data-viz-magenta-500', class: 'gl-bg-data-viz-magenta-500',
size: wikiSize, size: wikiSize,
}, },
...@@ -64,23 +68,31 @@ export default { ...@@ -64,23 +68,31 @@ export default {
formatSize(size) { formatSize(size) {
return numberToHumanSize(size); return numberToHumanSize(size);
}, },
usageStyle(percentage) {
return { width: `${percentage.toFixed()}%` };
},
sizePercentage(size) { sizePercentage(size) {
const { storageSize } = this.rootStorageStatistics; let max = this.rootStorageStatistics.storageSize;
if (this.limit !== 0 && max <= this.limit) {
max = this.limit;
}
return (size / storageSize) * 100; return (size / max) * 100;
}, },
}, },
}; };
</script> </script>
<template> <template>
<div v-if="storageTypes" class="gl-display-flex gl-flex-direction-column w-100"> <div v-if="storageTypes" class="gl-display-flex gl-flex-direction-column w-100">
<div class="gl-h-6 my-3"> <div class="gl-h-6 my-3 gl-bg-gray-50 gl-rounded-base">
<div <div
v-for="storageType in storageTypes" v-for="storageType in storageTypes"
:key="storageType.name" :key="storageType.name"
class="storage-type-usage gl-h-full gl-display-inline-block" class="storage-type-usage gl-h-full gl-display-inline-block"
:class="storageType.class" :class="storageType.class"
:style="{ width: `${storageType.percentage}%` }" :style="storageType.style"
data-testid="storage-type-usage"
></div> ></div>
</div> </div>
<div class="row py-0"> <div class="row py-0">
...@@ -88,7 +100,7 @@ export default { ...@@ -88,7 +100,7 @@ export default {
v-for="storageType in storageTypes" v-for="storageType in storageTypes"
:key="storageType.name" :key="storageType.name"
class="col-md-auto gl-display-flex gl-align-items-center" class="col-md-auto gl-display-flex gl-align-items-center"
data-testid="storage-type" data-testid="storage-type-legend"
> >
<div class="gl-h-2 gl-w-5 gl-mr-2 gl-display-inline-block" :class="storageType.class"></div> <div class="gl-h-2 gl-w-5 gl-mr-2 gl-display-inline-block" :class="storageType.class"></div>
<span class="gl-mr-2 gl-font-weight-bold gl-font-sm"> <span class="gl-mr-2 gl-font-weight-bold gl-font-sm">
......
query getStorageCounter($fullPath: ID!) { query getStorageCounter($fullPath: ID!) {
namespace(fullPath: $fullPath) { namespace(fullPath: $fullPath) {
id id
storageSizeLimit
rootStorageStatistics { rootStorageStatistics {
storageSize storageSize
repositorySize repositorySize
......
---
title: Render storage graph when there is a limit set
merge_request: 34931
author:
type: changed
import { shallowMount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import StorageApp from 'ee/storage_counter/components/app.vue'; import StorageApp from 'ee/storage_counter/components/app.vue';
import Project from 'ee/storage_counter/components/project.vue'; import Project from 'ee/storage_counter/components/project.vue';
import { projects, withRootStorageStatistics } from '../data'; import { projects, withRootStorageStatistics } from '../data';
import { numberToHumanSize } from '~/lib/utils/number_utils';
describe('Storage counter app', () => { describe('Storage counter app', () => {
let wrapper; let wrapper;
const findTotalUsage = () => wrapper.find("[data-testid='total-usage']");
function createComponent(loading = false) { function createComponent(loading = false) {
const $apollo = { const $apollo = {
queries: { queries: {
...@@ -15,7 +18,7 @@ describe('Storage counter app', () => { ...@@ -15,7 +18,7 @@ describe('Storage counter app', () => {
}, },
}; };
wrapper = shallowMount(StorageApp, { wrapper = mount(StorageApp, {
propsData: { namespacePath: 'h5bp', helpPagePath: 'help' }, propsData: { namespacePath: 'h5bp', helpPagePath: 'help' },
mocks: { $apollo }, mocks: { $apollo },
}); });
...@@ -29,51 +32,59 @@ describe('Storage counter app', () => { ...@@ -29,51 +32,59 @@ describe('Storage counter app', () => {
wrapper.destroy(); wrapper.destroy();
}); });
it('renders the 2 projects', done => { it('renders the 2 projects', async () => {
wrapper.setData({ wrapper.setData({
namespace: projects, namespace: projects,
}); });
wrapper.vm await wrapper.vm.$nextTick();
.$nextTick()
.then(() => { expect(wrapper.findAll(Project)).toHaveLength(2);
expect(wrapper.findAll(Project)).toHaveLength(2); });
})
.then(done) describe('limit', () => {
.catch(done.fail); it('when limit is set it renders limit information', async () => {
wrapper.setData({
namespace: projects,
});
await wrapper.vm.$nextTick();
expect(wrapper.text()).toContain(numberToHumanSize(projects.limit));
});
it('when limit is 0 it does not render limit information', async () => {
wrapper.setData({
namespace: { ...projects, limit: 0 },
});
await wrapper.vm.$nextTick();
expect(wrapper.text()).not.toContain(numberToHumanSize(0));
});
}); });
describe('with rootStorageStatistics information', () => { describe('with rootStorageStatistics information', () => {
it('renders total usage', done => { it('renders total usage', async () => {
wrapper.setData({ wrapper.setData({
namespace: withRootStorageStatistics, namespace: withRootStorageStatistics,
}); });
wrapper.vm await wrapper.vm.$nextTick();
.$nextTick()
.then(() => { expect(findTotalUsage().text()).toContain(withRootStorageStatistics.totalUsage);
expect(wrapper.find("[data-testid='total-usage']").text()).toContain(
withRootStorageStatistics.totalUsage,
);
})
.then(done)
.catch(done.fail);
}); });
}); });
describe('without rootStorageStatistics information', () => { describe('without rootStorageStatistics information', () => {
it('renders N/A', done => { it('renders N/A', async () => {
wrapper.setData({ wrapper.setData({
namespace: projects, namespace: projects,
}); });
wrapper.vm await wrapper.vm.$nextTick();
.$nextTick()
.then(() => { expect(findTotalUsage().text()).toContain('N/A');
expect(wrapper.find("[data-testid='total-usage']").text()).toContain('N/A');
})
.then(done)
.catch(done.fail);
}); });
}); });
}); });
...@@ -2,26 +2,36 @@ import { shallowMount } from '@vue/test-utils'; ...@@ -2,26 +2,36 @@ import { shallowMount } from '@vue/test-utils';
import UsageGraph from 'ee/storage_counter/components/usage_graph.vue'; import UsageGraph from 'ee/storage_counter/components/usage_graph.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils'; import { numberToHumanSize } from '~/lib/utils/number_utils';
const data = { let data;
wikiSize: 5000,
repositorySize: 4000,
packagesSize: 3000,
lfsObjectsSize: 2000,
buildArtifactsSize: 1000,
storageSize: 15000,
};
let wrapper; let wrapper;
function mountComponent(rootStorageStatistics) {
function mountComponent({ rootStorageStatistics, limit }) {
wrapper = shallowMount(UsageGraph, { wrapper = shallowMount(UsageGraph, {
propsData: { propsData: {
rootStorageStatistics, rootStorageStatistics,
limit,
}, },
}); });
} }
function findStorageTypeUsagesSerialized() {
return wrapper
.findAll('[data-testid="storage-type-usage"]')
.wrappers.map(wp => wp.element.style.width);
}
describe('Storage Counter usage graph component', () => { describe('Storage Counter usage graph component', () => {
beforeEach(() => { beforeEach(() => {
data = {
rootStorageStatistics: {
wikiSize: 5000,
repositorySize: 4000,
packagesSize: 3000,
lfsObjectsSize: 2000,
buildArtifactsSize: 1000,
storageSize: 15000,
},
limit: 2000,
};
mountComponent(data); mountComponent(data);
}); });
...@@ -30,26 +40,34 @@ describe('Storage Counter usage graph component', () => { ...@@ -30,26 +40,34 @@ describe('Storage Counter usage graph component', () => {
}); });
it('renders the legend in order', () => { it('renders the legend in order', () => {
const types = wrapper.findAll('[data-testid="storage-type"]'); const types = wrapper.findAll('[data-testid="storage-type-legend"]');
expect(types.at(0).text()).toContain('Wikis'); const {
expect(types.at(1).text()).toContain('Repositories'); buildArtifactsSize,
expect(types.at(2).text()).toContain('Packages'); lfsObjectsSize,
expect(types.at(3).text()).toContain('LFS Objects'); packagesSize,
expect(types.at(4).text()).toContain('Build Artifacts'); repositorySize,
}); wikiSize,
} = data.rootStorageStatistics;
it('renders formatted data in the legend', () => { expect(types.at(0).text()).toMatchInterpolatedText(`Wikis ${numberToHumanSize(wikiSize)}`);
expect(wrapper.text()).toContain(numberToHumanSize(data.buildArtifactsSize)); expect(types.at(1).text()).toMatchInterpolatedText(
expect(wrapper.text()).toContain(numberToHumanSize(data.lfsObjectsSize)); `Repositories ${numberToHumanSize(repositorySize)}`,
expect(wrapper.text()).toContain(numberToHumanSize(data.packagesSize)); );
expect(wrapper.text()).toContain(numberToHumanSize(data.repositorySize)); expect(types.at(2).text()).toMatchInterpolatedText(
expect(wrapper.text()).toContain(numberToHumanSize(data.wikiSize)); `Packages ${numberToHumanSize(packagesSize)}`,
);
expect(types.at(3).text()).toMatchInterpolatedText(
`LFS Objects ${numberToHumanSize(lfsObjectsSize)}`,
);
expect(types.at(4).text()).toMatchInterpolatedText(
`Build Artifacts ${numberToHumanSize(buildArtifactsSize)}`,
);
}); });
describe('when storage type is not used', () => { describe('when storage type is not used', () => {
beforeEach(() => { beforeEach(() => {
data.wikiSize = 0; data.rootStorageStatistics.wikiSize = 0;
mountComponent(data); mountComponent(data);
}); });
...@@ -60,11 +78,34 @@ describe('Storage Counter usage graph component', () => { ...@@ -60,11 +78,34 @@ describe('Storage Counter usage graph component', () => {
describe('when there is no storage usage', () => { describe('when there is no storage usage', () => {
beforeEach(() => { beforeEach(() => {
mountComponent({ storageSize: 0 }); data.rootStorageStatistics.storageSize = 0;
mountComponent(data);
}); });
it('it does not render', () => { it('it does not render', () => {
expect(wrapper.html()).toEqual(''); expect(wrapper.html()).toEqual('');
}); });
}); });
describe('when limit is 0', () => {
beforeEach(() => {
data.limit = 0;
mountComponent(data);
});
it('sets correct width values', () => {
expect(findStorageTypeUsagesSerialized()).toStrictEqual(['33%', '27%', '20%', '13%', '7%']);
});
});
describe('when storage exceeds limit', () => {
beforeEach(() => {
data.limit = data.rootStorageStatistics.storageSize - 1;
mountComponent(data);
});
it('it does render correclty', () => {
expect(findStorageTypeUsagesSerialized()).toStrictEqual(['33%', '27%', '20%', '13%', '7%']);
});
});
}); });
export const projects = { export const projects = {
totalUsage: 'N/A', totalUsage: 'N/A',
limit: 10000000,
projects: [ projects: [
{ {
id: '24', id: '24',
......
...@@ -24656,9 +24656,6 @@ msgstr "" ...@@ -24656,9 +24656,6 @@ msgstr ""
msgid "UsageQuota|Storage" msgid "UsageQuota|Storage"
msgstr "" msgstr ""
msgid "UsageQuota|Storage usage:"
msgstr ""
msgid "UsageQuota|This namespace has no projects which use shared runners" msgid "UsageQuota|This namespace has no projects which use shared runners"
msgstr "" msgstr ""
...@@ -24689,6 +24686,12 @@ msgstr "" ...@@ -24689,6 +24686,12 @@ msgstr ""
msgid "UsageQuota|Wikis" msgid "UsageQuota|Wikis"
msgstr "" msgstr ""
msgid "UsageQuota|You used: %{usage} %{limit}"
msgstr ""
msgid "UsageQuota|out of %{formattedLimit} of your namespace storage"
msgstr ""
msgid "Use %{code_start}::%{code_end} to create a %{link_start}scoped label set%{link_end} (eg. %{code_start}priority::1%{code_end})" msgid "Use %{code_start}::%{code_end} to create a %{link_start}scoped label set%{link_end} (eg. %{code_start}priority::1%{code_end})"
msgstr "" msgstr ""
......
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