Commit e0191c18 authored by Filipa Lacerda's avatar Filipa Lacerda Committed by Phil Hughes

Updates empty text message

This commits updates the text in the
empty state to reflect it being used
in the user profile and the group settings
parent 7db21ae7
......@@ -51,6 +51,7 @@ export default class LinkedTabs {
this.defaultAction = this.options.defaultAction;
this.action = this.options.action || this.defaultAction;
this.hashedTabs = this.options.hashedTabs || false;
if (this.action === 'show') {
this.action = this.defaultAction;
......@@ -58,6 +59,10 @@ export default class LinkedTabs {
this.currentLocation = window.location;
if (this.hashedTabs) {
this.action = this.currentLocation.hash || this.action;
}
const tabSelector = `${this.options.parentEl} a[data-toggle="tab"]`;
// since this is a custom event we need jQuery :(
......@@ -91,7 +96,9 @@ export default class LinkedTabs {
copySource.replace(/\/+$/, '');
const newState = `${copySource}${this.currentLocation.search}${this.currentLocation.hash}`;
const newState = this.hashedTabs
? copySource
: `${copySource}${this.currentLocation.search}${this.currentLocation.hash}`;
window.history.replaceState(
{
......
......@@ -100,3 +100,9 @@ export function numberToHumanSize(size) {
* @returns {Float} The summed value
*/
export const sum = (a = 0, b = 0) => a + b;
/**
* Checks if the provided number is odd
* @param {Int} number
*/
export const isOdd = (number = 0) => number % 2;
import storageCounter from 'ee/storage_counter';
import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs';
document.addEventListener('DOMContentLoaded', () => {
if (document.querySelector('#js-storage-counter-app')) {
storageCounter();
// eslint-disable-next-line no-new
new LinkedTabs({
defaultAction: '#pipelines-quota-tab',
parentEl: '.js-storage-tabs',
hashedTabs: true,
});
}
});
<script>
import Project from './project.vue';
import query from '../queries/storage.graphql';
export default {
components: {
Project,
},
props: {
namespacePath: {
type: String,
required: true,
},
},
apollo: {
namespace: {
query,
variables() {
return {
fullPath: this.namespacePath,
};
},
update: data => ({
projects: data.namespace.projects.edges.map(({ node }) => node),
}),
},
},
data() {
return {
namespace: {},
};
},
};
</script>
<template>
<div class="ci-table" role="grid">
<div
class="gl-responsive-table-row table-row-header bg-gray-light pl-2 border-top mt-3 lh-100"
role="row"
>
<div class="table-section section-70 font-weight-bold" role="columnheader">
{{ __('Project') }}
</div>
<div class="table-section section-30 font-weight-bold" role="columnheader">
{{ __('Usage') }}
</div>
</div>
<project v-for="project in namespace.projects" :key="project.id" :project="project" />
</div>
</template>
<script>
import { GlButton, GlLink } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import ProjectAvatar from '~/vue_shared/components/project_avatar/default.vue';
import { numberToHumanSize, isOdd } from '~/lib/utils/number_utils';
import { s__ } from '~/locale';
import StorageRow from './storage_row.vue';
export default {
components: {
Icon,
GlButton,
GlLink,
ProjectAvatar,
StorageRow,
},
props: {
project: {
required: true,
type: Object,
},
},
data() {
return {
isOpen: false,
};
},
computed: {
projectAvatar() {
const { name, id, avatarUrl, webUrl } = this.project;
return {
name,
id: Number(id),
avatar_url: avatarUrl,
path: webUrl,
};
},
name() {
return this.project.nameWithNamespace;
},
storageSize() {
return numberToHumanSize(this.project.statistics.storageSize);
},
iconName() {
return this.isOpen ? 'angle-down' : 'angle-right';
},
statistics() {
const statisticsCopy = Object.assign({}, this.project.statistics);
delete statisticsCopy.storageSize;
// eslint-disable-next-line no-underscore-dangle
delete statisticsCopy.__typename;
return statisticsCopy;
},
},
methods: {
toggleProject() {
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: {
commitCount: s__('UsageQuota|Commit count'),
repositorySize: s__('UsageQuota|Repository'),
lfsObjectsSize: s__('UsageQuota|LFS Storage'),
buildArtifactsSize: s__('UsageQuota|Artifacts'),
packagesSize: s__('UsageQuota|Packages'),
wikiSize: s__('UsageQuota|Wiki'),
},
};
</script>
<template>
<div>
<div class="gl-responsive-table-row border-bottom" role="row">
<div class="table-section section-wrap section-70 text-truncate" role="gridcell">
<div class="table-mobile-header font-weight-bold" role="rowheader">{{ __('Project') }}</div>
<div class="table-mobile-content">
<gl-button
class="btn-transparent float-left p-0 mr-2"
:aria-label="__('Toggle project')"
@click="toggleProject"
>
<icon :name="iconName" class="folder-icon" />
</gl-button>
<project-avatar :project="projectAvatar" :size="20" />
<gl-link :href="project.webUrl" class="font-weight-bold">{{ name }}</gl-link>
</div>
</div>
<div class="table-section section-wrap section-30 text-truncate" role="gridcell">
<div class="table-mobile-header font-weight-bold" role="rowheader">{{ __('Usage') }}</div>
<div class="table-mobile-content">{{ 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="{ 'bg-gray-light': isOdd(index) }"
/>
</template>
</div>
</template>
<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 lh-100" 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>
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import App from './components/app.vue';
Vue.use(VueApollo);
export default () => {
const el = document.getElementById('js-storage-counter-app');
const { namespacePath } = el.dataset;
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
return new Vue({
el,
apolloProvider,
render(h) {
return h(App, {
props: {
namespacePath,
},
});
},
});
};
query getStorageCounter($fullPath: ID!) {
namespace(fullPath: $fullPath) {
id
projects(includeSubgroups: true) {
edges {
node {
id
fullPath
nameWithNamespace
avatarUrl
webUrl
name
statistics {
commitCount
storageSize
repositorySize
lfsObjectsSize
buildArtifactsSize
packagesSize
wikiSize
}
}
}
}
}
}
......@@ -22,7 +22,6 @@
%span
Audit Events
- if @group.shared_runners_enabled? && @group.shared_runners_minutes_limit_enabled?
= nav_link(path: 'usage_quota#index') do
= link_to group_usage_quotas_path(@group), title: s_('UsageQuota|Usage Quotas') do
%span
......
......@@ -8,14 +8,22 @@
= s_('UsageQuota|Usage of group resources across the projects in the %{strong_start}%{group_name}%{strong_end} group').html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe, group_name: @group.name }
.top-area.scrolling-tabs-container.inner-page-scroll-tabs
%ul.nav-links.scrolling-tabs.separator{ role: 'tablist' }
%ul.nav.nav-tabs.nav-links.scrolling-tabs.separator.js-storage-tabs{ role: 'tablist' }
%li.nav-item
%a.nav-link.active#pipelines-quota{ href: 'pipelines-quota-tab', data: { toggle: 'tab' }, 'aria-controls': 'pipelines-quota-tab', 'aria-selected': true }
%a.nav-link#pipelines-quota{ data: { toggle: "tab", action: '#pipelines-quota-tab' }, href: '#pipelines-quota-tab', 'aria-controls': '#pipelines-quota-tab', 'aria-selected': true }
= s_('UsageQuota|Pipelines')
- if Gitlab::Graphql.enabled?
%li.nav-item
%a.nav-link#storage-quota{ data: { toggle: "tab", action: '#storage-quota-tab' }, href: '#storage-quota-tab', 'aria-controls': '#storage-quota-tab', 'aria-selected': false }
= s_('UsageQuota|Storage')
.nav-controls
= link_to s_('UsageQuota|Buy additional minutes'), EE::SUBSCRIPTIONS_PLANS_URL, target: '_blank', class: 'btn btn-inverted btn-success float-right'
.tab-content
.tab-pane.show.active#pipelines-quota-tab{ role: 'tabpanel' }
.tab-pane#pipelines-quota-tab
= render "namespaces/pipelines_quota/list",
locals: { namespace: @group, projects: @projects }
.tab-pane#storage-quota-tab
- if Gitlab::Graphql.enabled?
#js-storage-counter-app{ data: { namespace_path: @group.full_path } }
......@@ -19,6 +19,8 @@
.col-sm-6.right
- if namespace.shared_runners_minutes_limit_enabled?
#{namespace_shared_runner_limits_percent_used(namespace)}% used
- elsif !namespace.shared_runners_enabled?
0% used
- else
= s_('UsageQuota|Unlimited')
......@@ -35,18 +37,26 @@
= _('Minutes')
%tbody
- projects.each do |project|
%tr
%td
.avatar-container.s20.d-none.d-sm-block
= project_icon(project, alt: '', class: 'avatar project-avatar s20')
%strong= link_to project.full_name, project
%td
= project.shared_runners_minutes
- if projects.blank?
- if !namespace.shared_runners_enabled?
%tr
%td{ colspan: 2 }
.nothing-here-block
= s_('UsageQuota|This group has no projects which use shared runners')
- runners_doc_path = help_page_path('ci/runners/README.html')
- help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: runners_doc_path }
= s_('UsageQuota|%{help_link_start}Shared runners%{help_link_end} are disabled, so there are no limits set on pipeline usage').html_safe % { help_link_start: help_link_start, help_link_end: '</a>'.html_safe }
- else
- projects.each do |project|
%tr
%td
.avatar-container.s20.d-none.d-sm-block
= project_icon(project, alt: '', class: 'avatar project-avatar s20')
%strong= link_to project.full_name, project
%td
= project.shared_runners_minutes
- if projects.blank?
%tr
%td{ colspan: 2 }
.nothing-here-block
= s_('UsageQuota|This namespace has no projects which use shared runners')
= paginate projects, theme: "gitlab"
---
title: Adds Storage Counter
merge_request: 13294
author:
type: added
......@@ -35,22 +35,22 @@ describe 'Groups > Usage Quotas' do
let(:group) { create(:group, :with_not_used_build_minutes_limit) }
let!(:project) { create(:project, namespace: group, shared_runners_enabled: false) }
it 'is not linked within the group settings dropdown' do
it 'is linked within the group settings dropdown' do
visit edit_group_path(group)
expect(page).not_to have_link('Usage Quotas')
expect(page).to have_link('Usage Quotas')
end
it 'shows correct group quota info' do
visit_pipeline_quota_page
page.within('.pipeline-quota') do
expect(page).to have_content("300 / Unlimited minutes")
expect(page).to have_content("0%")
expect(page).to have_selector('.bg-success')
end
page.within('.pipeline-project-metrics') do
expect(page).to have_content('This group has no projects which use shared runners')
expect(page).to have_content('Shared runners are disabled, so there are no limits set on pipeline usage')
end
end
end
......
......@@ -23,14 +23,14 @@ describe 'Profile > Pipeline Quota' do
describe 'shared runners use' do
where(:shared_runners_enabled, :used, :quota, :usage_class, :usage_text) do
false | 300 | 500 | 'success' | '300 / Unlimited minutes Unlimited'
false | 300 | 500 | 'success' | '300 / Unlimited minutes 0% used'
true | 300 | nil | 'success' | '300 / Unlimited minutes Unlimited'
true | 300 | 500 | 'success' | '300 / 500 minutes 60% used'
true | 1000 | 500 | 'danger' | '1000 / 500 minutes 200% used'
end
with_them do
let(:no_shared_runners_text) { 'This group has no projects which use shared runners' }
let(:no_shared_runners_text) { 'Shared runners are disabled, so there are no limits set on pipeline usage' }
before do
project.update!(shared_runners_enabled: shared_runners_enabled)
......
import { shallowMount } from '@vue/test-utils';
import StorageApp from 'ee/storage_counter/components/app.vue';
import Project from 'ee/storage_counter/components/project.vue';
import { projects } from '../data';
describe('Storage counter app', () => {
let wrapper;
function createComponent(loading = false) {
const $apollo = {
queries: {
namespace: {
loading,
},
},
};
wrapper = shallowMount(StorageApp, {
propsData: { namespacePath: 'h5bp' },
mocks: { $apollo },
});
}
beforeEach(() => {
createComponent();
wrapper.setData({
namespace: projects,
});
});
it('renders the 2 projects', () => {
expect(wrapper.findAll(Project).length).toEqual(2);
});
});
import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import Project from 'ee/storage_counter/components/project.vue';
import ProjectAvatar from '~/vue_shared/components/project_avatar/default.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
let wrapper;
const data = {
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: 1293346,
repositorySize: 0,
lfsObjectsSize: 0,
buildArtifactsSize: 1272375,
packagesSize: 0,
},
};
function factory(project) {
wrapper = shallowMount(Project, {
propsData: {
project,
},
});
}
describe('Storage Counter project component', () => {
beforeEach(() => {
factory(data);
});
it('renders project avatar', () => {
expect(wrapper.contains(ProjectAvatar)).toBe(true);
});
it('renders project name', () => {
expect(wrapper.text()).toContain(data.nameWithNamespace);
});
it('renders formatted storage size', () => {
expect(wrapper.text()).toContain(numberToHumanSize(data.statistics.storageSize));
});
describe('toggle row', () => {
describe('on click', () => {
it('toggles isOpen', () => {
expect(wrapper.vm.isOpen).toEqual(false);
wrapper.find(GlButton).vm.$emit('click');
expect(wrapper.vm.isOpen).toEqual(true);
wrapper.find(GlButton).vm.$emit('click');
expect(wrapper.vm.isOpen).toEqual(false);
});
});
});
});
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));
});
});
// eslint-disable-next-line import/prefer-default-export
export const projects = {
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,
},
},
{
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: 1293346,
repositorySize: 0,
lfsObjectsSize: 0,
buildArtifactsSize: 1272375,
packagesSize: 0,
},
},
],
};
......@@ -13861,6 +13861,9 @@ msgstr ""
msgid "Toggle navigation"
msgstr ""
msgid "Toggle project"
msgstr ""
msgid "Toggle sidebar"
msgstr ""
......@@ -14275,16 +14278,37 @@ msgstr ""
msgid "Usage statistics"
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|Artifacts"
msgstr ""
msgid "UsageQuota|Buy additional minutes"
msgstr ""
msgid "UsageQuota|Commit count"
msgstr ""
msgid "UsageQuota|Current period usage"
msgstr ""
msgid "UsageQuota|LFS Storage"
msgstr ""
msgid "UsageQuota|Packages"
msgstr ""
msgid "UsageQuota|Pipelines"
msgstr ""
msgid "UsageQuota|This group has no projects which use shared runners"
msgid "UsageQuota|Repository"
msgstr ""
msgid "UsageQuota|Storage"
msgstr ""
msgid "UsageQuota|This namespace has no projects which use shared runners"
msgstr ""
msgid "UsageQuota|Unlimited"
......@@ -14305,6 +14329,9 @@ msgstr ""
msgid "UsageQuota|Usage since"
msgstr ""
msgid "UsageQuota|Wiki"
msgstr ""
msgid "Use %{code_start}::%{code_end} to create a %{link_start}scoped label set%{link_end} (eg. %{code_start}priority::1%{code_end})"
msgstr ""
......
......@@ -5,6 +5,7 @@ import {
bytesToGiB,
numberToHumanSize,
sum,
isOdd,
} from '~/lib/utils/number_utils';
describe('Number Utils', () => {
......@@ -98,4 +99,14 @@ describe('Number Utils', () => {
expect([1, 2, 3, 4, 5].reduce(sum)).toEqual(15);
});
});
describe('isOdd', () => {
it('should return 0 with a even number', () => {
expect(isOdd(2)).toEqual(0);
});
it('should return 1 with a odd number', () => {
expect(isOdd(1)).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