Commit ff3655a6 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch 'led/initial-project-storage-ui' into 'master'

Add initial project storage ui

See merge request gitlab-org/gitlab!68898
parents a1d583e1 af4d71b5
......@@ -69,19 +69,20 @@ export function bytesToGiB(number) {
* representation (e.g., giving it 1500 yields 1.5 KB).
*
* @param {Number} size
* @param {Number} digits - The number of digits to appear after the decimal point
* @returns {String}
*/
export function numberToHumanSize(size) {
export function numberToHumanSize(size, digits = 2) {
const abs = Math.abs(size);
if (abs < BYTES_IN_KIB) {
return sprintf(__('%{size} bytes'), { size });
} else if (abs < BYTES_IN_KIB ** 2) {
return sprintf(__('%{size} KiB'), { size: bytesToKiB(size).toFixed(2) });
return sprintf(__('%{size} KiB'), { size: bytesToKiB(size).toFixed(digits) });
} else if (abs < BYTES_IN_KIB ** 3) {
return sprintf(__('%{size} MiB'), { size: bytesToMiB(size).toFixed(2) });
return sprintf(__('%{size} MiB'), { size: bytesToMiB(size).toFixed(digits) });
}
return sprintf(__('%{size} GiB'), { size: bytesToGiB(size).toFixed(2) });
return sprintf(__('%{size} GiB'), { size: bytesToGiB(size).toFixed(digits) });
}
/**
......
<script>
import { GlAlert } from '@gitlab/ui';
import { s__ } from '~/locale';
import { GlAlert, GlLink, GlLoadingIcon } from '@gitlab/ui';
import { sprintf } from '~/locale';
import UsageGraph from '~/vue_shared/components/storage_counter/usage_graph.vue';
import {
ERROR_MESSAGE,
LEARN_MORE_LABEL,
USAGE_QUOTAS_LABEL,
TOTAL_USAGE_TITLE,
TOTAL_USAGE_SUBTITLE,
TOTAL_USAGE_DEFAULT_TEXT,
HELP_LINK_ARIA_LABEL,
} from '../constants';
import getProjectStorageCount from '../queries/project_storage.query.graphql';
import { parseGetProjectStorageResults } from '../utils';
import StorageTable from './storage_table.vue';
export default {
name: 'StorageCounterApp',
components: {
GlAlert,
GlLink,
GlLoadingIcon,
StorageTable,
UsageGraph,
},
inject: ['projectPath'],
inject: ['projectPath', 'helpLinks'],
apollo: {
project: {
query: getProjectStorageCount,
......@@ -20,11 +33,11 @@ export default {
fullPath: this.projectPath,
};
},
update: parseGetProjectStorageResults,
update(data) {
return parseGetProjectStorageResults(data, this.helpLinks);
},
error() {
this.error = s__(
'UsageQuota|Something went wrong while fetching project storage statistics',
);
this.error = ERROR_MESSAGE;
},
},
},
......@@ -34,24 +47,60 @@ export default {
error: '',
};
},
computed: {
totalUsage() {
return this.project?.storage?.totalUsage || TOTAL_USAGE_DEFAULT_TEXT;
},
storageTypes() {
return this.project?.storage?.storageTypes || [];
},
},
methods: {
clearError() {
this.error = '';
},
helpLinkAriaLabel(linkTitle) {
return sprintf(HELP_LINK_ARIA_LABEL, {
linkTitle,
});
},
},
i18n: {
placeholder: s__('UsageQuota|Usage'),
},
LEARN_MORE_LABEL,
USAGE_QUOTAS_LABEL,
TOTAL_USAGE_TITLE,
TOTAL_USAGE_SUBTITLE,
};
</script>
<template>
<div>
<gl-alert v-if="error" variant="danger" @dismiss="clearError">
{{ error }}
</gl-alert>
<div v-else>{{ $options.i18n.placeholder }}</div>
<gl-loading-icon v-if="$apollo.queries.project.loading" class="gl-mt-5" size="md" />
<gl-alert v-else-if="error" variant="danger" @dismiss="clearError">
{{ error }}
</gl-alert>
<div v-else>
<div class="gl-pt-5 gl-px-3">
<div class="gl-display-flex gl-justify-content-space-between gl-align-items-center">
<div>
<p class="gl-m-0 gl-font-lg gl-font-weight-bold">{{ $options.TOTAL_USAGE_TITLE }}</p>
<p class="gl-m-0 gl-text-gray-400">
{{ $options.TOTAL_USAGE_SUBTITLE }}
<gl-link
:href="helpLinks.usageQuotasHelpPagePath"
target="_blank"
:aria-label="helpLinkAriaLabel($options.USAGE_QUOTAS_LABEL)"
data-testid="usage-quotas-help-link"
>
{{ $options.LEARN_MORE_LABEL }}
</gl-link>
</p>
</div>
<p class="gl-m-0 gl-font-size-h-display gl-font-weight-bold" data-testid="total-usage">
{{ totalUsage }}
</p>
</div>
</div>
<div v-if="project.statistics" class="gl-w-full">
<usage-graph :root-storage-statistics="project.statistics" :limit="0" />
</div>
<storage-table :storage-types="storageTypes" />
</div>
</template>
<script>
import { GlLink, GlIcon, GlTable, GlSprintf } from '@gitlab/ui';
import { thWidthClass } from '~/lib/utils/table_utility';
import { sprintf } from '~/locale';
import { PROJECT_TABLE_LABELS, HELP_LINK_ARIA_LABEL } from '../constants';
export default {
name: 'StorageTable',
components: {
GlLink,
GlIcon,
GlTable,
GlSprintf,
},
props: {
storageTypes: {
type: Array,
required: true,
},
},
methods: {
helpLinkAriaLabel(linkTitle) {
return sprintf(HELP_LINK_ARIA_LABEL, {
linkTitle,
});
},
},
projectTableFields: [
{
key: 'storageType',
label: PROJECT_TABLE_LABELS.STORAGE_TYPE,
thClass: thWidthClass(90),
sortable: true,
},
{
key: 'value',
label: PROJECT_TABLE_LABELS.VALUE,
thClass: thWidthClass(10),
sortable: true,
},
],
};
</script>
<template>
<gl-table :items="storageTypes" :fields="$options.projectTableFields">
<template #cell(storageType)="{ item }">
<p class="gl-font-weight-bold gl-mb-0" :data-testid="`${item.storageType.id}-name`">
{{ item.storageType.name }}
<gl-link
v-if="item.storageType.helpPath"
:href="item.storageType.helpPath"
target="_blank"
:aria-label="helpLinkAriaLabel(item.storageType.name)"
:data-testid="`${item.storageType.id}-help-link`"
>
<gl-icon name="question" :size="12" />
</gl-link>
</p>
<p class="gl-mb-0" :data-testid="`${item.storageType.id}-description`">
{{ item.storageType.description }}
</p>
<p v-if="item.storageType.warningMessage" class="gl-mb-0 gl-font-sm">
<gl-icon name="warning" :size="12" />
<gl-sprintf :message="item.storageType.warningMessage">
<template #warningLink="{ content }">
<gl-link :href="item.storageType.warningLink" target="_blank" class="gl-font-sm">{{
content
}}</gl-link>
</template>
</gl-sprintf>
</p>
</template>
</gl-table>
</template>
import { s__, __ } from '~/locale';
export const PROJECT_STORAGE_TYPES = [
{
id: 'buildArtifactsSize',
name: s__('UsageQuota|Artifacts'),
description: s__('UsageQuota|Pipeline artifacts and job artifacts, created with CI/CD.'),
warningMessage: s__(
'UsageQuota|There is a known issue with Artifact storage where the total could be incorrect for some projects. More details and progress are available in %{warningLinkStart}the epic%{warningLinkEnd}.',
),
warningLink: 'https://gitlab.com/groups/gitlab-org/-/epics/5380',
},
{
id: 'lfsObjectsSize',
name: s__('UsageQuota|LFS Storage'),
description: s__('UsageQuota|Audio samples, videos, datasets, and graphics.'),
},
{
id: 'packagesSize',
name: s__('UsageQuota|Packages'),
description: s__('UsageQuota|Code packages and container images.'),
},
{
id: 'repositorySize',
name: s__('UsageQuota|Repository'),
description: s__('UsageQuota|Git repository, managed by the Gitaly service.'),
},
{
id: 'snippetsSize',
name: s__('UsageQuota|Snippets'),
description: s__('UsageQuota|Shared bits of code and text.'),
},
{
id: 'uploadsSize',
name: s__('UsageQuota|Uploads'),
description: s__('UsageQuota|File attachments and smaller design graphics.'),
},
{
id: 'wikiSize',
name: s__('UsageQuota|Wiki'),
description: s__('UsageQuota|Wiki content.'),
},
];
export const PROJECT_TABLE_LABELS = {
STORAGE_TYPE: s__('UsageQuota|Storage type'),
VALUE: s__('UsageQuota|Usage'),
};
export const ERROR_MESSAGE = s__(
'UsageQuota|Something went wrong while fetching project storage statistics',
);
export const LEARN_MORE_LABEL = s__('Learn more.');
export const USAGE_QUOTAS_LABEL = s__('UsageQuota|Usage Quotas');
export const HELP_LINK_ARIA_LABEL = s__('UsageQuota|%{linkTitle} help link');
export const TOTAL_USAGE_DEFAULT_TEXT = __('N/A');
export const TOTAL_USAGE_TITLE = s__('UsageQuota|Usage Breakdown');
export const TOTAL_USAGE_SUBTITLE = s__(
'UsageQuota|Includes project registry, artifacts, packages, wiki, uploads and other items.',
);
......@@ -12,7 +12,17 @@ export default (containerId = 'js-project-storage-count-app') => {
return false;
}
const { projectPath } = el.dataset;
const {
projectPath,
usageQuotasHelpPagePath,
buildArtifactsHelpPagePath,
lfsObjectsHelpPagePath,
packagesHelpPagePath,
repositoryHelpPagePath,
snippetsHelpPagePath,
uploadsHelpPagePath,
wikiHelpPagePath,
} = el.dataset;
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
......@@ -23,6 +33,16 @@ export default (containerId = 'js-project-storage-count-app') => {
apolloProvider,
provide: {
projectPath,
helpLinks: {
usageQuotasHelpPagePath,
buildArtifactsHelpPagePath,
lfsObjectsHelpPagePath,
packagesHelpPagePath,
repositoryHelpPagePath,
snippetsHelpPagePath,
uploadsHelpPagePath,
wikiHelpPagePath,
},
},
render(createElement) {
return createElement(StorageCounterApp);
......
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { s__ } from '~/locale';
const projectStorageTypes = [
{
id: 'buildArtifactsSize',
name: s__('UsageQuota|Artifacts'),
},
{
id: 'lfsObjectsSize',
name: s__('UsageQuota|LFS Storage'),
},
{
id: 'packagesSize',
name: s__('UsageQuota|Packages'),
},
{
id: 'repositorySize',
name: s__('UsageQuota|Repository'),
},
{
id: 'snippetsSize',
name: s__('UsageQuota|Snippets'),
},
{
id: 'uploadsSize',
name: s__('UsageQuota|Uploads'),
},
{
id: 'wikiSize',
name: s__('UsageQuota|Wiki'),
},
];
import { PROJECT_STORAGE_TYPES } from './constants';
/**
* This method parses the results from `getProjectStorageCount` call.
......@@ -38,26 +7,32 @@ const projectStorageTypes = [
* @param {Object} data graphql result
* @returns {Object}
*/
export const parseGetProjectStorageResults = (data) => {
export const parseGetProjectStorageResults = (data, helpLinks) => {
const projectStatistics = data?.project?.statistics;
if (!projectStatistics) {
return {};
}
const { storageSize, ...storageStatistics } = projectStatistics;
const storageTypes = projectStorageTypes.reduce((types, currentType) => {
const storageTypes = PROJECT_STORAGE_TYPES.reduce((types, currentType) => {
if (!storageStatistics[currentType.id]) {
return types;
}
const helpPathKey = currentType.id.replace(`Size`, `HelpPagePath`);
const helpPath = helpLinks[helpPathKey];
return types.concat({
...currentType,
value: numberToHumanSize(storageStatistics[currentType.id]),
storageType: {
...currentType,
helpPath,
},
value: numberToHumanSize(storageStatistics[currentType.id], 1),
});
}, []);
return {
storage: {
totalUsage: numberToHumanSize(storageSize),
totalUsage: numberToHumanSize(storageSize, 1),
storageTypes,
},
statistics: projectStatistics,
......
......@@ -8,6 +8,18 @@ class Projects::UsageQuotasController < Projects::ApplicationController
feature_category :utilization
def index
@storage_app_data = {
project_path: @project.full_path,
usage_quotas_help_page_path: help_page_path('user/usage_quotas'),
build_artifacts_help_page_path: help_page_path('ci/pipelines/job_artifacts', anchor: 'when-job-artifacts-are-deleted'),
packages_help_page_path: help_page_path('user/packages/package_registry/index.md', anchor: 'delete-a-package'),
repository_help_page_path: help_page_path('user/project/repository/reducing_the_repo_size_using_git'),
snippets_help_page_path: help_page_path('user/snippets', anchor: 'reduce-snippets-repository-size'),
wiki_help_page_path: help_page_path('administration/wikis/index.md', anchor: 'reduce-wiki-repository-size')
}
end
private
def verify_usage_quotas_enabled!
......
......@@ -4,8 +4,10 @@
= s_('UsageQuota|Usage Quotas')
.row
.col-sm-6
= s_('UsageQuota|Usage of project resources across the %{strong_start}%{project_name}%{strong_end} project').html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe, project_name: @project.name }
.col-sm-12
= s_('UsageQuota|Usage of project resources across the %{strong_start}%{project_name}%{strong_end} project').html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe, project_name: @project.name } + '.'
%a{ href: help_page_path('user/usage_quotas.md') }
= s_('UsageQuota|Learn more about usage quotas') + '.'
.top-area.scrolling-tabs-container.inner-page-scroll-tabs
%ul.nav.nav-tabs.nav-links.scrolling-tabs.separator.js-usage-quota-tabs{ role: 'tablist' }
......@@ -14,4 +16,4 @@
= s_('UsageQuota|Storage')
.tab-content
.tab-pane#storage-quota-tab
#js-project-storage-count-app{ data: { project_path: @project.full_path } }
#js-project-storage-count-app{ data: @storage_app_data }
......@@ -33,7 +33,7 @@ export default {
'UsageQuota|This is the total amount of storage used across your projects within this namespace.',
),
link: {
text: s__('UsageQuota|Learn more about usage quotas'),
text: `${s__('UsageQuota|Learn more about usage quotas')}.`,
url: helpPagePath('user/usage_quotas'),
},
};
......
......@@ -36156,6 +36156,9 @@ msgstr ""
msgid "UsageQuota|%{help_link_start}Shared runners%{help_link_end} are disabled, so there are no limits set on pipeline usage"
msgstr ""
msgid "UsageQuota|%{linkTitle} help link"
msgstr ""
msgid "UsageQuota|%{percentageLeft} of purchased storage is available"
msgstr ""
......@@ -36165,6 +36168,9 @@ msgstr ""
msgid "UsageQuota|Artifacts is a sum of build and pipeline artifacts."
msgstr ""
msgid "UsageQuota|Audio samples, videos, datasets, and graphics."
msgstr ""
msgid "UsageQuota|Buy additional minutes"
msgstr ""
......@@ -36174,9 +36180,21 @@ msgstr ""
msgid "UsageQuota|CI minutes usage by project"
msgstr ""
msgid "UsageQuota|Code packages and container images."
msgstr ""
msgid "UsageQuota|Current period usage"
msgstr ""
msgid "UsageQuota|File attachments and smaller design graphics."
msgstr ""
msgid "UsageQuota|Git repository, managed by the Gitaly service."
msgstr ""
msgid "UsageQuota|Includes project registry, artifacts, packages, wiki, uploads and other items."
msgstr ""
msgid "UsageQuota|Increase storage temporarily"
msgstr ""
......@@ -36195,6 +36213,9 @@ msgstr ""
msgid "UsageQuota|Packages"
msgstr ""
msgid "UsageQuota|Pipeline artifacts and job artifacts, created with CI/CD."
msgstr ""
msgid "UsageQuota|Pipelines"
msgstr ""
......@@ -36213,6 +36234,9 @@ msgstr ""
msgid "UsageQuota|Seats"
msgstr ""
msgid "UsageQuota|Shared bits of code and text."
msgstr ""
msgid "UsageQuota|Snippets"
msgstr ""
......@@ -36222,6 +36246,12 @@ msgstr ""
msgid "UsageQuota|Storage"
msgstr ""
msgid "UsageQuota|Storage type"
msgstr ""
msgid "UsageQuota|There is a known issue with Artifact storage where the total could be incorrect for some projects. More details and progress are available in %{warningLinkStart}the epic%{warningLinkEnd}."
msgstr ""
msgid "UsageQuota|This is the total amount of storage used across your projects within this namespace."
msgstr ""
......@@ -36261,6 +36291,9 @@ msgstr ""
msgid "UsageQuota|Usage"
msgstr ""
msgid "UsageQuota|Usage Breakdown"
msgstr ""
msgid "UsageQuota|Usage Quotas"
msgstr ""
......@@ -36285,6 +36318,9 @@ msgstr ""
msgid "UsageQuota|Wiki"
msgstr ""
msgid "UsageQuota|Wiki content."
msgstr ""
msgid "UsageQuota|Wikis"
msgstr ""
......
import { GlAlert } from '@gitlab/ui';
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import StorageCounterApp from '~/projects/storage_counter/components/app.vue';
import { TOTAL_USAGE_DEFAULT_TEXT } from '~/projects/storage_counter/constants';
import getProjectStorageCount from '~/projects/storage_counter/queries/project_storage.query.graphql';
import UsageGraph from '~/vue_shared/components/storage_counter/usage_graph.vue';
import { projectStorageCountResponse } from './mock_data';
import {
mockGetProjectStorageCountGraphQLResponse,
mockEmptyResponse,
projectData,
defaultProvideValues,
} from '../mock_data';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('Storage counter app', () => {
let wrapper;
const createMockApolloProvider = ({ mutationMock }) => {
localVue.use(VueApollo);
const createMockApolloProvider = ({ reject = false, mockedValue } = {}) => {
let response;
const requestHandlers = [[getProjectStorageCount, mutationMock]];
if (reject) {
response = jest.fn().mockRejectedValue(mockedValue || new Error('GraphQL error'));
} else {
response = jest.fn().mockResolvedValue(mockedValue);
}
const requestHandlers = [[getProjectStorageCount, response]];
return createMockApollo(requestHandlers);
};
const createComponent = ({ provide = {}, mockApollo } = {}) => {
const defaultProvideValues = {
projectPath: 'test-project',
};
wrapper = shallowMount(StorageCounterApp, {
localVue,
apolloProvider: mockApollo,
provide: {
...defaultProvideValues,
...provide,
},
});
wrapper = extendedWrapper(
shallowMount(StorageCounterApp, {
localVue,
apolloProvider: mockApollo,
provide: {
...defaultProvideValues,
...provide,
},
}),
);
};
const findAlert = () => wrapper.findComponent(GlAlert);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findUsagePercentage = () => wrapper.findByTestId('total-usage');
const findUsageQuotasHelpLink = () => wrapper.findByTestId('usage-quotas-help-link');
const findUsageGraph = () => wrapper.findComponent(UsageGraph);
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('renders app successfully', () => {
expect(wrapper.text()).toBe('Usage');
describe('with apollo fetching successful', () => {
let mockApollo;
beforeEach(async () => {
mockApollo = createMockApolloProvider({
mockedValue: mockGetProjectStorageCountGraphQLResponse,
});
createComponent({ mockApollo });
await waitForPromises();
});
it('renders correct total usage', () => {
expect(findUsagePercentage().text()).toBe(projectData.storage.totalUsage);
});
it('renders correct usage quotas help link', () => {
expect(findUsageQuotasHelpLink().attributes('href')).toBe(
defaultProvideValues.helpLinks.usageQuotasHelpPagePath,
);
});
});
describe('handling apollo fetching error', () => {
const mutationMock = jest.fn().mockRejectedValue(new Error('GraphQL error'));
describe('with apollo loading', () => {
let mockApollo;
beforeEach(() => {
const mockApollo = createMockApolloProvider({ mutationMock });
mockApollo = createMockApolloProvider({
mockedValue: new Promise(() => {}),
});
createComponent({ mockApollo });
});
it('should show loading icon', () => {
expect(findLoadingIcon().exists()).toBe(true);
});
});
describe('with apollo returning empty data', () => {
let mockApollo;
beforeEach(async () => {
mockApollo = createMockApolloProvider({
mockedValue: mockEmptyResponse,
});
createComponent({ mockApollo });
await waitForPromises();
});
it('renders gl-alert if there is an error', () => {
it('shows default text for total usage', () => {
expect(findUsagePercentage().text()).toBe(TOTAL_USAGE_DEFAULT_TEXT);
});
});
describe('with apollo fetching error', () => {
let mockApollo;
beforeEach(() => {
mockApollo = createMockApolloProvider();
createComponent({ mockApollo, reject: true });
});
it('renders gl-alert', () => {
expect(findAlert().exists()).toBe(true);
});
});
describe('rendering <usage-graph />', () => {
const mutationMock = jest.fn().mockResolvedValue(projectStorageCountResponse);
let mockApollo;
beforeEach(() => {
const mockApollo = createMockApolloProvider({ mutationMock });
beforeEach(async () => {
mockApollo = createMockApolloProvider({
mockedValue: mockGetProjectStorageCountGraphQLResponse,
});
createComponent({ mockApollo });
await waitForPromises();
});
it('renders usage-graph component if project.statistics exists', () => {
......@@ -76,7 +140,10 @@ describe('Storage counter app', () => {
});
it('passes project.statistics to usage-graph component', () => {
const { __typename, ...statistics } = projectStorageCountResponse.data.project.statistics;
const {
__typename,
...statistics
} = mockGetProjectStorageCountGraphQLResponse.data.project.statistics;
expect(findUsageGraph().props('rootStorageStatistics')).toMatchObject(statistics);
});
});
......
import { GlTable } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import StorageTable from '~/projects/storage_counter/components/storage_table.vue';
import { projectData, defaultProvideValues } from '../mock_data';
describe('StorageTable', () => {
let wrapper;
const defaultProps = {
storageTypes: projectData.storage.storageTypes,
};
const createComponent = (props = {}) => {
wrapper = extendedWrapper(
mount(StorageTable, {
propsData: {
...defaultProps,
...props,
},
}),
);
};
const findTable = () => wrapper.findComponent(GlTable);
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('with storage types', () => {
it.each(projectData.storage.storageTypes)(
'renders table row correctly %o',
({ storageType: { id, name, description } }) => {
expect(wrapper.findByTestId(`${id}-name`).text()).toBe(name);
expect(wrapper.findByTestId(`${id}-description`).text()).toBe(description);
expect(wrapper.findByTestId(`${id}-help-link`).attributes('href')).toBe(
defaultProvideValues.helpLinks[id.replace(`Size`, `HelpPagePath`)]
.replace(`Size`, ``)
.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`),
);
},
);
});
describe('without storage types', () => {
beforeEach(() => {
createComponent({ storageTypes: [] });
});
it('should render the table header <th>', () => {
expect(findTable().find('th').exists()).toBe(true);
});
it('should not render any table data <td>', () => {
expect(findTable().find('td').exists()).toBe(false);
});
});
});
export const projectStorageCountResponse = {
export const mockGetProjectStorageCountGraphQLResponse = {
data: {
project: {
id: 'gid://gitlab/Project/20',
statistics: {
buildArtifactsSize: 400000,
lfsObjectsSize: 4800000,
packagesSize: 3800000,
repositorySize: 39000000,
snippetsSize: 0,
storageSize: 39930000,
uploadsSize: 0,
wikiSize: 300000,
buildArtifactsSize: 400000.0,
lfsObjectsSize: 4800000.0,
packagesSize: 3800000.0,
repositorySize: 3900000.0,
snippetsSize: 1200000.0,
storageSize: 15300000.0,
uploadsSize: 900000.0,
wikiSize: 300000.0,
__typename: 'ProjectStatistics',
},
__typename: 'Project',
},
},
};
export const mockEmptyResponse = { data: { project: null } };
export const defaultProvideValues = {
projectPath: '/project-path',
helpLinks: {
usageQuotasHelpPagePath: '/usage-quotas',
buildArtifactsHelpPagePath: '/build-artifacts',
lfsObjectsHelpPagePath: '/lsf-objects',
packagesHelpPagePath: '/packages',
repositoryHelpPagePath: '/repository',
snippetsHelpPagePath: '/snippets',
uploadsHelpPagePath: '/uploads',
wikiHelpPagePath: '/wiki',
},
};
export const projectData = {
storage: {
totalUsage: '14.6 MiB',
storageTypes: [
{
storageType: {
id: 'buildArtifactsSize',
name: 'Artifacts',
description: 'Pipeline artifacts and job artifacts, created with CI/CD.',
warningMessage:
'There is a known issue with Artifact storage where the total could be incorrect for some projects. More details and progress are available in %{warningLinkStart}the epic%{warningLinkEnd}.',
helpPath: '/build-artifacts',
},
value: '390.6 KiB',
},
{
storageType: {
id: 'lfsObjectsSize',
name: 'LFS Storage',
description: 'Audio samples, videos, datasets, and graphics.',
helpPath: '/lsf-objects',
},
value: '4.6 MiB',
},
{
storageType: {
id: 'packagesSize',
name: 'Packages',
description: 'Code packages and container images.',
helpPath: '/packages',
},
value: '3.6 MiB',
},
{
storageType: {
id: 'repositorySize',
name: 'Repository',
description: 'Git repository, managed by the Gitaly service.',
helpPath: '/repository',
},
value: '3.7 MiB',
},
{
storageType: {
id: 'snippetsSize',
name: 'Snippets',
description: 'Shared bits of code and text.',
helpPath: '/snippets',
},
value: '1.1 MiB',
},
{
storageType: {
id: 'uploadsSize',
name: 'Uploads',
description: 'File attachments and smaller design graphics.',
helpPath: '/uploads',
},
value: '878.9 KiB',
},
{
storageType: {
id: 'wikiSize',
name: 'Wiki',
description: 'Wiki content.',
helpPath: '/wiki',
},
value: '293.0 KiB',
},
],
},
};
import { parseGetProjectStorageResults } from '~/projects/storage_counter/utils';
import {
mockGetProjectStorageCountGraphQLResponse,
projectData,
defaultProvideValues,
} from './mock_data';
describe('parseGetProjectStorageResults', () => {
it('parses project statistics correctly', () => {
expect(
parseGetProjectStorageResults(
mockGetProjectStorageCountGraphQLResponse.data,
defaultProvideValues.helpLinks,
),
).toMatchObject(projectData);
});
});
......@@ -28,10 +28,20 @@ RSpec.describe 'Project Usage Quotas' do
end
it 'renders usage quotas path' do
mock_storage_app_data = {
project_path: project.full_path,
usage_quotas_help_page_path: help_page_path('user/usage_quotas'),
build_artifacts_help_page_path: help_page_path('ci/pipelines/job_artifacts', anchor: 'when-job-artifacts-are-deleted'),
packages_help_page_path: help_page_path('user/packages/package_registry/index.md', anchor: 'delete-a-package'),
repository_help_page_path: help_page_path('user/project/repository/reducing_the_repo_size_using_git'),
snippets_help_page_path: help_page_path('user/snippets', anchor: 'reduce-snippets-repository-size'),
wiki_help_page_path: help_page_path('administration/wikis/index.md', anchor: 'reduce-wiki-repository-size')
}
get project_usage_quotas_path(project)
expect(response).to have_gitlab_http_status(:ok)
expect(response.body).to include(project_usage_quotas_path(project))
expect(assigns[:storage_app_data]).to eq(mock_storage_app_data)
expect(response.body).to include("Usage of project resources across the <strong>#{project.name}</strong> project")
end
......
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