Commit 1e2c19b4 authored by Brandon Labuschagne's avatar Brandon Labuschagne

Merge branch 'led/fe-usage-quotas-refactor-1' into 'master'

Move project related usage quotas code to a shared folder

See merge request gitlab-org/gitlab!75351
parents 349b985e 6d27b228
import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs';
import storageCounter from '~/projects/storage_counter';
import initProjectStorage from 'ee/usage_quotas/storage/init_project_storage';
import initSearchSettings from '~/search_settings';
const initLinkedTabs = () => {
......@@ -15,7 +15,7 @@ const initLinkedTabs = () => {
};
const initVueApp = () => {
storageCounter('js-project-storage-count-app');
initProjectStorage('js-project-storage-count-app');
};
initVueApp();
......
......@@ -10,10 +10,10 @@ import {
import { parseBoolean } from '~/lib/utils/common_utils';
import UsageGraph from '~/vue_shared/components/storage_counter/usage_graph.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ProjectList from 'ee/usage_quotas/storage/components/project_list.vue';
import { PROJECTS_PER_PAGE } from '../constants';
import query from '../queries/storage.query.graphql';
import { formatUsageSize, parseGetStorageResults } from '../utils';
import ProjectsTable from './projects_table.vue';
import StorageInlineAlert from './storage_inline_alert.vue';
import TemporaryStorageIncreaseModal from './temporary_storage_increase_modal.vue';
import UsageStatistics from './usage_statistics.vue';
......@@ -26,7 +26,7 @@ export default {
GlButton,
GlSprintf,
UsageGraph,
ProjectsTable,
ProjectList,
UsageStatistics,
StorageInlineAlert,
GlKeysetPagination,
......@@ -230,7 +230,7 @@ export default {
/>
</div>
</div>
<projects-table
<project-list
:projects="namespaceProjects"
:is-loading="isQueryLoading"
:additional-purchased-storage-size="namespace.additionalPurchasedStorageSize || 0"
......
<script>
import { numberToHumanSize } from '~/lib/utils/number_utils';
export default {
props: {
name: {
type: String,
required: true,
},
value: {
type: Number,
required: true,
},
},
computed: {
formattedValue() {
return numberToHumanSize(this.value);
},
},
};
</script>
<template>
<div class="gl-responsive-table-row gl-line-height-normal" role="row">
<div class="table-section section-wrap section-70 text-truncate pl-2 ml-3" role="gridcell">
<div class="table-mobile-header" role="rowheader"></div>
<div class="table-mobile-content ml-1">{{ name }}</div>
</div>
<div class="table-section section-wrap section-30 text-truncate" role="gridcell">
<div class="table-mobile-header" role="rowheader"></div>
<div class="table-mobile-content">{{ formattedValue }}</div>
</div>
</div>
</template>
......@@ -13,5 +13,3 @@ export const STORAGE_USAGE_THRESHOLDS = {
};
export const PROJECTS_PER_PAGE = 20;
export const SKELETON_LOADER_ROWS = 5;
<script>
import { GlLink, GlIcon } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { numberToHumanSize, isOdd } from '~/lib/utils/number_utils';
import { s__ } from '~/locale';
import ProjectAvatar from '~/vue_shared/components/deprecated_project_avatar/default.vue';
import StorageRow from './storage_row.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import ProjectAvatar from '~/vue_shared/components/project_avatar.vue';
import { getStorageTypesFromProjectStatistics } from '../utils';
import { PROJECT_TABLE_LABEL_PROJECT, PROJECT_TABLE_LABEL_USAGE } from '../constants';
import ProjectStorageDetail from './project_storage_detail.vue';
export default {
name: 'CollapsibleProjectStorageDetail',
components: {
GlIcon,
GlLink,
ProjectAvatar,
StorageRow,
ProjectStorageDetail,
},
props: {
project: {
......@@ -25,15 +26,6 @@ export default {
};
},
computed: {
projectAvatar() {
const { name, id, avatarUrl, webUrl } = this.project;
return {
name,
id: Number(getIdFromGraphQLId(id)),
avatar_url: avatarUrl,
path: webUrl,
};
},
name() {
return this.project.nameWithNamespace;
},
......@@ -43,14 +35,8 @@ export default {
iconName() {
return this.isOpen ? 'angle-down' : 'angle-right';
},
statistics() {
const statisticsCopy = { ...this.project.statistics };
delete statisticsCopy.storageSize;
// eslint-disable-next-line no-underscore-dangle
delete statisticsCopy.__typename;
delete statisticsCopy.commitCount;
return statisticsCopy;
projectStorageTypes() {
return getStorageTypesFromProjectStatistics(this.project.statistics);
},
},
methods: {
......@@ -63,28 +49,10 @@ export default {
}
this.isOpen = !this.isOpen;
},
getFormattedName(name) {
return this.$options.i18nStatisticsMap[name];
},
isOdd(num) {
return isOdd(num);
},
/**
* Some values can be `nil`
* for those, we send 0 instead
*/
getValue(val) {
return val || 0;
},
},
i18nStatisticsMap: {
repositorySize: s__('UsageQuota|Repository'),
lfsObjectsSize: s__('UsageQuota|LFS Storage'),
buildArtifactsSize: s__('UsageQuota|Artifacts'),
packagesSize: s__('UsageQuota|Packages'),
wikiSize: s__('UsageQuota|Wiki'),
snippetsSize: s__('UsageQuota|Snippets'),
uploadsSize: s__('UsageQuota|Uploads'),
i18n: {
PROJECT_TABLE_LABEL_PROJECT,
PROJECT_TABLE_LABEL_USAGE,
},
};
</script>
......@@ -101,16 +69,20 @@ export default {
role="gridcell"
>
<div class="table-mobile-header gl-font-weight-bold" role="rowheader">
{{ __('Project') }}
{{ $options.i18n.PROJECT_TABLE_LABEL_PROJECT }}
</div>
<div class="table-mobile-content gl-display-flex gl-align-items-center">
<div class="gl-display-flex gl-mr-3 gl-align-items-center">
<gl-icon :size="12" :name="iconName" class="gl-mr-2" />
<gl-icon :size="12" :name="iconName" />
<gl-icon name="bookmark" />
</div>
<div>
<project-avatar :project="projectAvatar" :size="32" />
</div>
<project-avatar
:project-name="project.name"
:project-avatar-url="project.avatarUrl"
:size="32"
:alt="project.name"
class="gl-mr-3"
/>
<gl-link
:href="project.webUrl"
class="js-project-link gl-font-weight-bold gl-text-gray-900!"
......@@ -123,20 +95,11 @@ export default {
role="gridcell"
>
<div class="table-mobile-header gl-font-weight-bold" role="rowheader">
{{ __('Usage') }}
{{ $options.i18n.PROJECT_TABLE_LABEL_USAGE }}
</div>
<div class="table-mobile-content gl-text-gray-900">{{ storageSize }}</div>
</div>
</div>
<template v-if="isOpen">
<storage-row
v-for="(value, statisticsName, index) in statistics"
:key="index"
:name="getFormattedName(statisticsName)"
:value="getValue(value)"
:class="{ 'gl-bg-gray-10': isOdd(index) }"
/>
</template>
<project-storage-detail v-if="isOpen" :storage-types="projectStorageTypes" />
</div>
</template>
<script>
import { SEARCH_DEBOUNCE_MS } from '~/ref/constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import Project from './project.vue';
import { PROJECT_TABLE_LABEL_PROJECT, PROJECT_TABLE_LABEL_USAGE } from '../constants';
import CollapsibleProjectStorageDetail from './collapsible_project_storage_detail.vue';
import ProjectsSkeletonLoader from './projects_skeleton_loader.vue';
export default {
name: 'ProjectList',
components: {
Project,
CollapsibleProjectStorageDetail,
ProjectsSkeletonLoader,
},
mixins: [glFeatureFlagsMixin()],
props: {
projects: {
type: Array,
......@@ -25,7 +24,10 @@ export default {
default: false,
},
},
searchDebounceValue: SEARCH_DEBOUNCE_MS,
i18n: {
PROJECT_TABLE_LABEL_PROJECT,
PROJECT_TABLE_LABEL_USAGE,
},
};
</script>
......@@ -36,20 +38,19 @@ export default {
role="row"
>
<div class="table-section section-70 gl-font-weight-bold" role="columnheader">
{{ __('Project') }}
{{ $options.i18n.PROJECT_TABLE_LABEL_PROJECT }}
</div>
<div class="table-section section-30 gl-font-weight-bold" role="columnheader">
{{ __('Usage') }}
{{ $options.i18n.PROJECT_TABLE_LABEL_USAGE }}
</div>
</div>
<projects-skeleton-loader v-if="isLoading" />
<template v-else>
<project
v-for="project in projects"
:key="project.id"
:project="project"
:additional-purchased-storage-size="additionalPurchasedStorageSize"
/>
</template>
<collapsible-project-storage-detail
v-for="project in projects"
v-else
:key="project.id"
:project="project"
:additional-purchased-storage-size="additionalPurchasedStorageSize"
/>
</div>
</template>
......@@ -11,23 +11,23 @@ import {
TOTAL_USAGE_DEFAULT_TEXT,
HELP_LINK_ARIA_LABEL,
} from '../constants';
import getProjectStorageCount from '../queries/project_storage.query.graphql';
import getProjectStorageStatistics from '../queries/project_storage.query.graphql';
import { parseGetProjectStorageResults } from '../utils';
import StorageTable from './storage_table.vue';
import ProjectStorageDetail from './project_storage_detail.vue';
export default {
name: 'StorageCounterApp',
name: 'ProjectStorageApp',
components: {
GlAlert,
GlLink,
GlLoadingIcon,
StorageTable,
UsageGraph,
ProjectStorageDetail,
},
inject: ['projectPath', 'helpLinks'],
apollo: {
project: {
query: getProjectStorageCount,
query: getProjectStorageStatistics,
variables() {
return {
fullPath: this.projectPath,
......@@ -101,6 +101,6 @@ export default {
<div v-if="project.statistics" class="gl-w-full">
<usage-graph :root-storage-statistics="project.statistics" :limit="0" />
</div>
<storage-table :storage-types="storageTypes" />
<project-storage-detail :storage-types="storageTypes" />
</div>
</template>
......@@ -3,11 +3,15 @@ import { GlLink, GlIcon, GlTableLite as GlTable, GlSprintf } from '@gitlab/ui';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { thWidthClass } from '~/lib/utils/table_utility';
import { sprintf } from '~/locale';
import { PROJECT_TABLE_LABELS, HELP_LINK_ARIA_LABEL } from '../constants';
import {
PROJECT_TABLE_LABEL_STORAGE_TYPE,
PROJECT_TABLE_LABEL_USAGE,
HELP_LINK_ARIA_LABEL,
} from '../constants';
import StorageTypeIcon from './storage_type_icon.vue';
export default {
name: 'StorageTable',
name: 'ProjectStorageDetail',
components: {
GlLink,
GlIcon,
......@@ -31,13 +35,13 @@ export default {
projectTableFields: [
{
key: 'storageType',
label: PROJECT_TABLE_LABELS.STORAGE_TYPE,
label: PROJECT_TABLE_LABEL_STORAGE_TYPE,
thClass: thWidthClass(90),
sortable: true,
},
{
key: 'value',
label: PROJECT_TABLE_LABELS.VALUE,
label: PROJECT_TABLE_LABEL_USAGE,
thClass: thWidthClass(10),
sortable: true,
formatter: (value) => {
......
import { s__, __ } from '~/locale';
export const ERROR_MESSAGE = s__(
'UsageQuota|Something went wrong while fetching project storage statistics',
);
export const LEARN_MORE_LABEL = __('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 artifacts, repositories, wiki, uploads, and other items.',
);
export const PROJECT_STORAGE_TYPES = [
{
id: 'buildArtifactsSize',
......@@ -42,20 +55,8 @@ export const PROJECT_STORAGE_TYPES = [
},
];
export const PROJECT_TABLE_LABELS = {
STORAGE_TYPE: s__('UsageQuota|Storage type'),
VALUE: s__('UsageQuota|Usage'),
};
export const PROJECT_TABLE_LABEL_PROJECT = __('Project');
export const PROJECT_TABLE_LABEL_STORAGE_TYPE = s__('UsageQuota|Storage type');
export const PROJECT_TABLE_LABEL_USAGE = s__('UsageQuota|Usage');
export const ERROR_MESSAGE = s__(
'UsageQuota|Something went wrong while fetching project storage statistics',
);
export const LEARN_MORE_LABEL = __('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 artifacts, repositories, wiki, uploads, and other items.',
);
export const SKELETON_LOADER_ROWS = 5;
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import StorageCounterApp from './components/app.vue';
import ProjectStorageApp from './components/project_storage_app.vue';
Vue.use(VueApollo);
......@@ -45,7 +45,7 @@ export default (containerId = 'js-project-storage-count-app') => {
},
},
render(createElement) {
return createElement(StorageCounterApp);
return createElement(ProjectStorageApp);
},
});
};
query getProjectStorageCount($fullPath: ID!) {
query getProjectStorageStatistics($fullPath: ID!) {
project(fullPath: $fullPath) {
id
statistics {
......
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { PROJECT_STORAGE_TYPES } from './constants';
/**
* This method parses the results from `getProjectStorageCount` call.
*
* @param {Object} data graphql result
* @returns {Object}
*/
export const parseGetProjectStorageResults = (data, helpLinks) => {
const projectStatistics = data?.project?.statistics;
if (!projectStatistics) {
return {};
}
const { storageSize, ...storageStatistics } = projectStatistics;
const storageTypes = PROJECT_STORAGE_TYPES.reduce((types, currentType) => {
export const getStorageTypesFromProjectStatistics = (projectStatistics, helpLinks = {}) =>
PROJECT_STORAGE_TYPES.reduce((types, currentType) => {
const helpPathKey = currentType.id.replace(`Size`, `HelpPagePath`);
const helpPath = helpLinks[helpPathKey];
......@@ -22,10 +11,24 @@ export const parseGetProjectStorageResults = (data, helpLinks) => {
...currentType,
helpPath,
},
value: storageStatistics[currentType.id],
value: projectStatistics[currentType.id],
});
}, []);
/**
* This method parses the results from `getProjectStorageStatistics` call.
*
* @param {Object} data graphql result
* @returns {Object}
*/
export const parseGetProjectStorageResults = (data, helpLinks) => {
const projectStatistics = data?.project?.statistics;
if (!projectStatistics) {
return {};
}
const { storageSize } = projectStatistics;
const storageTypes = getStorageTypesFromProjectStatistics(projectStatistics, helpLinks);
return {
storage: {
totalUsage: numberToHumanSize(storageSize, 1),
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Projects (JavaScript fixtures)', type: :controller do
include ApiHelpers
include JavaScriptFixturesHelpers
runners_token = 'runnerstoken:intabulasreferre'
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project, namespace: namespace, path: 'builds-project', runners_token: runners_token, avatar: fixture_file_upload('spec/fixtures/dk.png', 'image/png')) }
let(:user) { project.owner }
describe GraphQL::Query, type: :request do
include GraphqlHelpers
context 'project storage statistics query' do
before do
project.statistics.update!(
repository_size: 3900000,
lfs_objects_size: 4800000,
build_artifacts_size: 400000,
pipeline_artifacts_size: 400000,
wiki_size: 300000,
packages_size: 3800000,
uploads_size: 900000
)
end
base_input_path = 'usage_quotas/storage/queries/'
base_output_path = 'graphql/usage_quotas/storage/'
query_name = 'project_storage.query.graphql'
it "#{base_output_path}#{query_name}.json" do
query = get_graphql_query_as_string("#{base_input_path}#{query_name}", ee: true)
post_graphql(query, current_user: user, variables: { fullPath: project.full_path })
expect_graphql_errors_to_be_empty
end
end
end
end
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 ProjectsTable from 'ee/storage_counter/components/projects_table.vue';
import CollapsibleProjectStorageDetail from 'ee/usage_quotas/storage/components/collapsible_project_storage_detail.vue';
import ProjectList from 'ee/usage_quotas/storage/components/project_list.vue';
import StorageInlineAlert from 'ee/storage_counter/components/storage_inline_alert.vue';
import TemporaryStorageIncreaseModal from 'ee/storage_counter/components/temporary_storage_increase_modal.vue';
import UsageStatistics from 'ee/storage_counter/components/usage_statistics.vue';
......@@ -22,7 +22,7 @@ describe('Storage counter app', () => {
const findUsageGraph = () => wrapper.find(UsageGraph);
const findUsageStatistics = () => wrapper.find(UsageStatistics);
const findStorageInlineAlert = () => wrapper.find(StorageInlineAlert);
const findProjectsTable = () => wrapper.find(ProjectsTable);
const findProjectList = () => wrapper.find(ProjectList);
const findPrevButton = () => wrapper.find('[data-testid="prevButton"]');
const findNextButton = () => wrapper.find('[data-testid="nextButton"]');
......@@ -74,7 +74,7 @@ describe('Storage counter app', () => {
await wrapper.vm.$nextTick();
expect(wrapper.findAll(Project)).toHaveLength(3);
expect(wrapper.findAll(CollapsibleProjectStorageDetail)).toHaveLength(3);
});
describe('limit', () => {
......@@ -232,21 +232,21 @@ describe('Storage counter app', () => {
it('triggers search if user enters search input', () => {
expect(wrapper.vm.searchTerm).toBe('');
findProjectsTable().vm.$emit('search', sampleSearchTerm);
findProjectList().vm.$emit('search', sampleSearchTerm);
expect(wrapper.vm.searchTerm).toBe(sampleSearchTerm);
});
it('triggers search if user clears the entered search input', () => {
const projectsTable = findProjectsTable();
const projectList = findProjectList();
expect(wrapper.vm.searchTerm).toBe('');
projectsTable.vm.$emit('search', sampleSearchTerm);
projectList.vm.$emit('search', sampleSearchTerm);
expect(wrapper.vm.searchTerm).toBe(sampleSearchTerm);
projectsTable.vm.$emit('search', '');
projectList.vm.$emit('search', '');
expect(wrapper.vm.searchTerm).toBe('');
});
......@@ -254,7 +254,7 @@ describe('Storage counter app', () => {
it('does not trigger search if user enters short search input', () => {
expect(wrapper.vm.searchTerm).toBe('');
findProjectsTable().vm.$emit('search', sampleShortSearchTerm);
findProjectList().vm.$emit('search', sampleShortSearchTerm);
expect(wrapper.vm.searchTerm).toBe('');
});
......
import { shallowMount } from '@vue/test-utils';
import StorageRow from 'ee/storage_counter/components/storage_row.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
let wrapper;
const data = {
name: 'LFS Package',
value: 1293346,
};
function factory({ name, value }) {
wrapper = shallowMount(StorageRow, {
propsData: {
name,
value,
},
});
}
describe('Storage Counter row component', () => {
beforeEach(() => {
factory(data);
});
it('renders provided name', () => {
expect(wrapper.text()).toContain(data.name);
});
it('renders formatted value', () => {
expect(wrapper.text()).toContain(numberToHumanSize(data.value));
});
});
export const projects = [
{
id: '24',
fullPath: 'h5bp/dummy-project',
nameWithNamespace: 'H5bp / dummy project',
avatarUrl: null,
webUrl: 'http://localhost:3001/h5bp/dummy-project',
name: 'dummy project',
statistics: {
commitCount: 1,
storageSize: 41943,
repositorySize: 41943,
lfsObjectsSize: 0,
buildArtifactsSize: 0,
packagesSize: 0,
},
actualRepositorySizeLimit: 100000,
totalCalculatedUsedStorage: 41943,
totalCalculatedStorageLimit: 41943000,
},
{
id: '8',
fullPath: 'h5bp/html5-boilerplate',
nameWithNamespace: 'H5bp / Html5 Boilerplate',
avatarUrl: null,
webUrl: 'http://localhost:3001/h5bp/html5-boilerplate',
name: 'Html5 Boilerplate',
statistics: {
commitCount: 0,
storageSize: 99000,
repositorySize: 0,
lfsObjectsSize: 0,
buildArtifactsSize: 1272375,
packagesSize: 0,
},
actualRepositorySizeLimit: 100000,
totalCalculatedUsedStorage: 89000,
totalCalculatedStorageLimit: 99430,
},
{
id: '80',
fullPath: 'twit/twitter',
nameWithNamespace: 'Twitter',
avatarUrl: null,
webUrl: 'http://localhost:3001/twit/twitter',
name: 'Twitter',
statistics: {
commitCount: 0,
storageSize: 12933460,
repositorySize: 209710,
lfsObjectsSize: 209720,
buildArtifactsSize: 1272375,
packagesSize: 0,
},
actualRepositorySizeLimit: 100000,
totalCalculatedUsedStorage: 13143170,
totalCalculatedStorageLimit: 12143170,
},
];
import { projects } from 'ee_jest/usage_quotas/storage/mock_data';
export const namespaceData = {
totalUsage: 'N/A',
......
......@@ -4,7 +4,8 @@ import {
parseProjects,
calculateUsedAndRemStorage,
} from 'ee/storage_counter/utils';
import { projects as mockProjectsData, mockGetStorageCounterGraphQLResponse } from './mock_data';
import { projects as mockProjectsData } from 'ee_jest/usage_quotas/storage/mock_data';
import { mockGetStorageCounterGraphQLResponse } from './mock_data';
describe('UsageThreshold', () => {
it.each`
......
import { shallowMount } from '@vue/test-utils';
import Project from 'ee/storage_counter/components/project.vue';
import StorageRow from 'ee/storage_counter/components/storage_row.vue';
import CollapsibleProjectStorageDetail from 'ee/usage_quotas/storage/components/collapsible_project_storage_detail.vue';
import ProjectStorageDetail from 'ee/usage_quotas/storage/components/project_storage_detail.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import ProjectAvatar from '~/vue_shared/components/deprecated_project_avatar/default.vue';
import ProjectAvatar from '~/vue_shared/components/project_avatar.vue';
import { projects } from '../mock_data';
let wrapper;
const createComponent = () => {
wrapper = shallowMount(Project, {
wrapper = shallowMount(CollapsibleProjectStorageDetail, {
propsData: {
project: projects[1],
},
......@@ -15,9 +15,9 @@ const createComponent = () => {
};
const findTableRow = () => wrapper.find('[data-testid="projectTableRow"]');
const findStorageRow = () => wrapper.find(StorageRow);
const findProjectStorageDetail = () => wrapper.find(ProjectStorageDetail);
describe('Storage Counter project component', () => {
describe('CollapsibleProjectStorageDetail', () => {
beforeEach(() => {
createComponent();
});
......@@ -37,16 +37,16 @@ describe('Storage Counter project component', () => {
describe('toggle row', () => {
describe('on click', () => {
it('toggles isOpen', () => {
expect(findStorageRow().exists()).toBe(false);
expect(findProjectStorageDetail().exists()).toBe(false);
findTableRow().trigger('click');
wrapper.vm.$nextTick(() => {
expect(findStorageRow().exists()).toBe(true);
expect(findProjectStorageDetail().exists()).toBe(true);
findTableRow().trigger('click');
wrapper.vm.$nextTick(() => {
expect(findStorageRow().exists()).toBe(false);
expect(findProjectStorageDetail().exists()).toBe(false);
});
});
});
......
import { shallowMount } from '@vue/test-utils';
import Project from 'ee/storage_counter/components/project.vue';
import ProjectsTable from 'ee/storage_counter/components/projects_table.vue';
import CollapsibleProjectStorageDetail from 'ee/usage_quotas/storage/components/collapsible_project_storage_detail.vue';
import ProjectList from 'ee/usage_quotas/storage/components/project_list.vue';
import { projects } from '../mock_data';
let wrapper;
const createComponent = ({ additionalRepoStorageByNamespace = false } = {}) => {
wrapper = shallowMount(ProjectsTable, {
wrapper = shallowMount(ProjectList, {
propsData: {
projects,
additionalPurchasedStorageSize: 0,
......@@ -19,9 +19,9 @@ const createComponent = ({ additionalRepoStorageByNamespace = false } = {}) => {
});
};
const findTableRows = () => wrapper.findAll(Project);
const findTableRows = () => wrapper.findAll(CollapsibleProjectStorageDetail);
describe('Usage Quotas project table component', () => {
describe('ProjectList', () => {
beforeEach(() => {
createComponent();
});
......
......@@ -4,21 +4,21 @@ 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 ProjectStorageApp from 'ee/usage_quotas/storage/components/project_storage_app.vue';
import { TOTAL_USAGE_DEFAULT_TEXT } from 'ee/usage_quotas/storage/constants';
import getProjectStorageStatistics from 'ee/usage_quotas/storage/queries/project_storage.query.graphql';
import UsageGraph from '~/vue_shared/components/storage_counter/usage_graph.vue';
import {
mockGetProjectStorageCountGraphQLResponse,
mockEmptyResponse,
projectData,
defaultProvideValues,
mockGetProjectStorageStatisticsGraphQLResponse,
mockEmptyResponse,
defaultProjectProvideValues,
} from '../mock_data';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('Storage counter app', () => {
describe('ProjectStorageApp', () => {
let wrapper;
const createMockApolloProvider = ({ reject = false, mockedValue } = {}) => {
......@@ -30,18 +30,18 @@ describe('Storage counter app', () => {
response = jest.fn().mockResolvedValue(mockedValue);
}
const requestHandlers = [[getProjectStorageCount, response]];
const requestHandlers = [[getProjectStorageStatistics, response]];
return createMockApollo(requestHandlers);
};
const createComponent = ({ provide = {}, mockApollo } = {}) => {
wrapper = extendedWrapper(
shallowMount(StorageCounterApp, {
shallowMount(ProjectStorageApp, {
localVue,
apolloProvider: mockApollo,
provide: {
...defaultProvideValues,
...defaultProjectProvideValues,
...provide,
},
}),
......@@ -63,7 +63,7 @@ describe('Storage counter app', () => {
beforeEach(async () => {
mockApollo = createMockApolloProvider({
mockedValue: mockGetProjectStorageCountGraphQLResponse,
mockedValue: mockGetProjectStorageStatisticsGraphQLResponse,
});
createComponent({ mockApollo });
await waitForPromises();
......@@ -75,7 +75,7 @@ describe('Storage counter app', () => {
it('renders correct usage quotas help link', () => {
expect(findUsageQuotasHelpLink().attributes('href')).toBe(
defaultProvideValues.helpLinks.usageQuotasHelpPagePath,
defaultProjectProvideValues.helpLinks.usageQuotasHelpPagePath,
);
});
});
......@@ -129,7 +129,7 @@ describe('Storage counter app', () => {
beforeEach(async () => {
mockApollo = createMockApolloProvider({
mockedValue: mockGetProjectStorageCountGraphQLResponse,
mockedValue: mockGetProjectStorageStatisticsGraphQLResponse,
});
createComponent({ mockApollo });
await waitForPromises();
......@@ -143,7 +143,7 @@ describe('Storage counter app', () => {
const {
__typename,
...statistics
} = mockGetProjectStorageCountGraphQLResponse.data.project.statistics;
} = mockGetProjectStorageStatisticsGraphQLResponse.data.project.statistics;
expect(findUsageGraph().props('rootStorageStatistics')).toMatchObject(statistics);
});
});
......
import { GlTableLite } 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';
import ProjectStorageDetail from 'ee/usage_quotas/storage/components/project_storage_detail.vue';
import { projectData, projectHelpLinks } from '../mock_data';
describe('StorageTable', () => {
describe('ProjectStorageDetail', () => {
let wrapper;
const defaultProps = {
......@@ -13,7 +13,7 @@ describe('StorageTable', () => {
const createComponent = (props = {}) => {
wrapper = extendedWrapper(
mount(StorageTable, {
mount(ProjectStorageDetail, {
propsData: {
...defaultProps,
...props,
......@@ -39,7 +39,7 @@ describe('StorageTable', () => {
expect(wrapper.findByTestId(`${id}-description`).text()).toBe(description);
expect(wrapper.findByTestId(`${id}-icon`).props('name')).toBe(id);
expect(wrapper.findByTestId(`${id}-help-link`).attributes('href')).toBe(
defaultProvideValues.helpLinks[id.replace(`Size`, `HelpPagePath`)]
projectHelpLinks[id.replace(`Size`, `HelpPagePath`)]
.replace(`Size`, ``)
.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`),
);
......
import { mount } from '@vue/test-utils';
import ProjectsSkeletonLoader from 'ee/storage_counter/components/projects_skeleton_loader.vue';
import ProjectsSkeletonLoader from 'ee/usage_quotas/storage/components/projects_skeleton_loader.vue';
describe('ProjectsSkeletonLoader', () => {
let wrapper;
......
import { mount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
import StorageTypeIcon from '~/projects/storage_counter/components/storage_type_icon.vue';
import StorageTypeIcon from 'ee/usage_quotas/storage/components/storage_type_icon.vue';
describe('StorageTypeIcon', () => {
let wrapper;
......
import mockGetProjectStorageCountGraphQLResponse from 'test_fixtures/graphql/projects/storage_counter/project_storage.query.graphql.json';
import mockGetProjectStorageStatisticsGraphQLResponse from 'test_fixtures/graphql/usage_quotas/storage/project_storage.query.graphql.json';
export { mockGetProjectStorageCountGraphQLResponse };
export { mockGetProjectStorageStatisticsGraphQLResponse };
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 projects = [
{
id: '24',
fullPath: 'h5bp/dummy-project',
nameWithNamespace: 'H5bp / dummy project',
avatarUrl: null,
webUrl: 'http://localhost:3001/h5bp/dummy-project',
name: 'dummy project',
statistics: {
commitCount: 1,
storageSize: 41943,
repositorySize: 41943,
lfsObjectsSize: 0,
buildArtifactsSize: 0,
packagesSize: 0,
},
actualRepositorySizeLimit: 100000,
totalCalculatedUsedStorage: 41943,
totalCalculatedStorageLimit: 41943000,
},
};
{
id: '8',
fullPath: 'h5bp/html5-boilerplate',
nameWithNamespace: 'H5bp / Html5 Boilerplate',
avatarUrl: null,
webUrl: 'http://localhost:3001/h5bp/html5-boilerplate',
name: 'Html5 Boilerplate',
statistics: {
commitCount: 0,
storageSize: 99000,
repositorySize: 0,
lfsObjectsSize: 0,
buildArtifactsSize: 1272375,
packagesSize: 0,
},
actualRepositorySizeLimit: 100000,
totalCalculatedUsedStorage: 89000,
totalCalculatedStorageLimit: 99430,
},
{
id: '80',
fullPath: 'twit/twitter',
nameWithNamespace: 'Twitter',
avatarUrl: null,
webUrl: 'http://localhost:3001/twit/twitter',
name: 'Twitter',
statistics: {
commitCount: 0,
storageSize: 12933460,
repositorySize: 209710,
lfsObjectsSize: 209720,
buildArtifactsSize: 1272375,
packagesSize: 0,
},
actualRepositorySizeLimit: 100000,
totalCalculatedUsedStorage: 13143170,
totalCalculatedStorageLimit: 12143170,
},
];
export const projectData = {
storage: {
......@@ -90,3 +136,19 @@ export const projectData = {
],
},
};
export const projectHelpLinks = {
usageQuotasHelpPagePath: '/usage-quotas',
buildArtifactsHelpPagePath: '/build-artifacts',
lfsObjectsHelpPagePath: '/lsf-objects',
packagesHelpPagePath: '/packages',
repositoryHelpPagePath: '/repository',
snippetsHelpPagePath: '/snippets',
uploadsHelpPagePath: '/uploads',
wikiHelpPagePath: '/wiki',
};
export const defaultProjectProvideValues = {
projectPath: '/project-path',
helpLinks: projectHelpLinks,
};
import { parseGetProjectStorageResults } from '~/projects/storage_counter/utils';
import { parseGetProjectStorageResults } from 'ee/usage_quotas/storage/utils';
import {
mockGetProjectStorageCountGraphQLResponse,
projectData,
defaultProvideValues,
mockGetProjectStorageStatisticsGraphQLResponse,
defaultProjectProvideValues,
} from './mock_data';
describe('parseGetProjectStorageResults', () => {
it('parses project statistics correctly', () => {
expect(
parseGetProjectStorageResults(
mockGetProjectStorageCountGraphQLResponse.data,
defaultProvideValues.helpLinks,
mockGetProjectStorageStatisticsGraphQLResponse.data,
defaultProjectProvideValues.helpLinks,
),
).toMatchObject(projectData);
});
it('includes storage type with size of 0 in returned value', () => {
const mockedResponse = mockGetProjectStorageCountGraphQLResponse.data;
const mockedResponse = mockGetProjectStorageStatisticsGraphQLResponse.data;
// ensuring a specific storage type item has size of 0
mockedResponse.project.statistics.repositorySize = 0;
const response = parseGetProjectStorageResults(mockedResponse, defaultProvideValues.helpLinks);
const response = parseGetProjectStorageResults(
mockedResponse,
defaultProjectProvideValues.helpLinks,
);
expect(response.storage.storageTypes).toEqual(
expect.arrayContaining([
......
......@@ -37424,9 +37424,6 @@ msgstr ""
msgid "Upvotes"
msgstr ""
msgid "Usage"
msgstr ""
msgid "Usage Trends"
msgstr ""
......@@ -37484,9 +37481,6 @@ msgstr ""
msgid "UsageQuota|LFS Objects"
msgstr ""
msgid "UsageQuota|LFS Storage"
msgstr ""
msgid "UsageQuota|LFS storage"
msgstr ""
......
......@@ -65,31 +65,5 @@ RSpec.describe 'Projects (JavaScript fixtures)', type: :controller do
expect_graphql_errors_to_be_empty
end
end
context 'project storage count query' do
before do
project.statistics.update!(
repository_size: 3900000,
lfs_objects_size: 4800000,
build_artifacts_size: 400000,
pipeline_artifacts_size: 400000,
wiki_size: 300000,
packages_size: 3800000,
uploads_size: 900000
)
end
base_input_path = 'projects/storage_counter/queries/'
base_output_path = 'graphql/projects/storage_counter/'
query_name = 'project_storage.query.graphql'
it "#{base_output_path}#{query_name}.json" do
query = get_graphql_query_as_string("#{base_input_path}#{query_name}")
post_graphql(query, current_user: user, variables: { fullPath: project.full_path })
expect_graphql_errors_to_be_empty
end
end
end
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