Commit 5f225a35 authored by Timothy Noah's avatar Timothy Noah Committed by Dhiraj Bodicherla

Update usage quotas table tooltips

This MR updates the tooltips in usage quotas
table rows. The tooltip will now have more info
about why a project is locked or in warning state
parent 009b3788
...@@ -166,7 +166,10 @@ export default { ...@@ -166,7 +166,10 @@ export default {
/> />
</div> </div>
</div> </div>
<projects-table :projects="namespaceProjects" /> <projects-table
:projects="namespaceProjects"
:additional-purchased-storage-size="namespace.additionalPurchasedStorageSize || 0"
/>
<temporary-storage-increase-modal <temporary-storage-increase-modal
v-if="isStorageIncreaseModalVisible" v-if="isStorageIncreaseModalVisible"
:limit="formattedNamespaceLimit" :limit="formattedNamespaceLimit"
......
...@@ -6,13 +6,27 @@ ...@@ -6,13 +6,27 @@
* lifted this component could replace and be used mainstream. * lifted this component could replace and be used mainstream.
*/ */
import { GlLink, GlIcon, GlTooltipDirective } from '@gitlab/ui'; import { GlLink, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__, sprintf } from '~/locale';
import ProjectAvatar from '~/vue_shared/components/project_avatar/default.vue'; import ProjectAvatar from '~/vue_shared/components/project_avatar/default.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { formatUsageSize, usageRatioToThresholdLevel } from '../utils'; import { formatUsageSize, usageRatioToThresholdLevel } from '../utils';
import { ALERT_THRESHOLD, ERROR_THRESHOLD, WARNING_THRESHOLD } from '../constants'; import { ALERT_THRESHOLD, ERROR_THRESHOLD, WARNING_THRESHOLD } from '../constants';
export default { export default {
i18n: {
warningWithNoPurchasedStorageText: s__(
'UsageQuota|This project is near the free %{actualRepositorySizeLimit} limit and at risk of being locked.',
),
lockedWithNoPurchasedStorageText: s__(
'UsageQuota|This project is locked because it is using %{actualRepositorySizeLimit} of free storage and there is no purchased storage available.',
),
warningWithPurchasedStorageText: s__(
'UsageQuota|This project is at risk of being locked because purchased storage is running low.',
),
lockedWithPurchasedStorageText: s__(
'UsageQuota|This project is locked because it used %{actualRepositorySizeLimit} of free storage and all the purchased storage.',
),
},
components: { components: {
GlIcon, GlIcon,
GlLink, GlLink,
...@@ -26,6 +40,10 @@ export default { ...@@ -26,6 +40,10 @@ export default {
required: true, required: true,
type: Object, type: Object,
}, },
additionalPurchasedStorageSize: {
type: Number,
required: true,
},
}, },
computed: { computed: {
projectAvatar() { projectAvatar() {
...@@ -40,6 +58,9 @@ export default { ...@@ -40,6 +58,9 @@ export default {
name() { name() {
return this.project.nameWithNamespace; return this.project.nameWithNamespace;
}, },
hasPurchasedStorage() {
return this.additionalPurchasedStorageSize > 0;
},
storageSize() { storageSize() {
return formatUsageSize(this.project.totalCalculatedUsedStorage); return formatUsageSize(this.project.totalCalculatedUsedStorage);
}, },
...@@ -53,21 +74,32 @@ export default { ...@@ -53,21 +74,32 @@ export default {
return usageRatioToThresholdLevel(this.excessStorageRatio); return usageRatioToThresholdLevel(this.excessStorageRatio);
}, },
status() { status() {
const i18nTextOpts = {
actualRepositorySizeLimit: formatUsageSize(this.project.actualRepositorySizeLimit),
};
if (this.thresholdLevel === ERROR_THRESHOLD) { if (this.thresholdLevel === ERROR_THRESHOLD) {
const tooltipText = this.hasPurchasedStorage
? this.$options.i18n.lockedWithPurchasedStorageText
: this.$options.i18n.lockedWithNoPurchasedStorageText;
return { return {
bgColor: { 'gl-bg-red-50': true }, bgColor: { 'gl-bg-red-50': true },
iconClass: { 'gl-text-red-500': true }, iconClass: { 'gl-text-red-500': true },
linkClass: 'gl-text-red-500!', linkClass: 'gl-text-red-500!',
tooltipText: s__('UsageQuota|This project is locked.'), tooltipText: sprintf(tooltipText, i18nTextOpts),
}; };
} else if ( } else if (
this.thresholdLevel === WARNING_THRESHOLD || this.thresholdLevel === WARNING_THRESHOLD ||
this.thresholdLevel === ALERT_THRESHOLD this.thresholdLevel === ALERT_THRESHOLD
) { ) {
const tooltipText = this.hasPurchasedStorage
? this.$options.i18n.warningWithPurchasedStorageText
: this.$options.i18n.warningWithNoPurchasedStorageText;
return { return {
bgColor: { 'gl-bg-orange-50': true }, bgColor: { 'gl-bg-orange-50': true },
iconClass: 'gl-text-orange-500', iconClass: 'gl-text-orange-500',
tooltipText: s__('UsageQuota|This project is at risk of being locked.'), tooltipText: sprintf(tooltipText, i18nTextOpts),
}; };
} }
......
...@@ -14,6 +14,10 @@ export default { ...@@ -14,6 +14,10 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
additionalPurchasedStorageSize: {
type: Number,
required: true,
},
}, },
computed: { computed: {
isAdditionalStorageFlagEnabled() { isAdditionalStorageFlagEnabled() {
...@@ -61,6 +65,7 @@ export default { ...@@ -61,6 +65,7 @@ export default {
v-for="project in projects" v-for="project in projects"
:key="project.id" :key="project.id"
:project="project" :project="project"
:additional-purchased-storage-size="additionalPurchasedStorageSize"
/> />
</div> </div>
</template> </template>
...@@ -12,6 +12,7 @@ const createComponent = (propsData = {}) => { ...@@ -12,6 +12,7 @@ const createComponent = (propsData = {}) => {
wrapper = shallowMount(ProjectWithExcessStorage, { wrapper = shallowMount(ProjectWithExcessStorage, {
propsData: { propsData: {
project: projects[0], project: projects[0],
additionalPurchasedStorageSize: 0,
...propsData, ...propsData,
}, },
directives: { directives: {
...@@ -34,67 +35,113 @@ describe('Storage Counter project component', () => { ...@@ -34,67 +35,113 @@ describe('Storage Counter project component', () => {
wrapper.destroy(); wrapper.destroy();
}); });
it('renders project avatar', () => { describe('without extra storage purchased', () => {
expect(wrapper.find(ProjectAvatar).exists()).toBe(true); it('renders project avatar', () => {
}); expect(wrapper.find(ProjectAvatar).exists()).toBe(true);
it('renders project name', () => {
expect(wrapper.text()).toContain(projects[0].nameWithNamespace);
});
it('renders formatted storage size', () => {
expect(wrapper.text()).toContain(formatUsageSize(projects[0].statistics.storageSize));
});
it('does not render the warning icon if project is not in error state', () => {
expect(findWarningIcon().exists()).toBe(false);
});
it('render row without error state background', () => {
expect(findTableRow().classes('gl-bg-red-50')).toBe(false);
});
describe('renders the row in error state', () => {
beforeEach(() => {
createComponent({ project: projects[2] });
}); });
it('with error state background', () => { it('renders project name', () => {
expect(findTableRow().classes('gl-bg-red-50')).toBe(true); expect(wrapper.text()).toContain(projects[0].nameWithNamespace);
}); });
it('with project link in error state', () => { it('renders formatted storage size', () => {
expect(findProjectLink().classes('gl-text-red-500!')).toBe(true); expect(wrapper.text()).toContain(formatUsageSize(projects[0].statistics.storageSize));
}); });
it('with error icon', () => { it('does not render the warning icon if project is not in error state', () => {
expect(findWarningIcon().exists()).toBe(true); expect(findWarningIcon().exists()).toBe(false);
}); });
it('with tooltip', () => { it('render row without error state background', () => {
expect(getWarningIconTooltipText().title).toBe('This project is locked.'); expect(findTableRow().classes('gl-bg-red-50')).toBe(false);
}); });
});
describe('renders the row in warning state', () => { describe('renders the row in error state', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ project: projects[1] }); createComponent({ project: projects[2] });
}); });
it('with error state background', () => {
expect(findTableRow().classes('gl-bg-red-50')).toBe(true);
});
it('with project link in error state', () => {
expect(findProjectLink().classes('gl-text-red-500!')).toBe(true);
});
it('with error icon', () => {
expect(findWarningIcon().exists()).toBe(true);
});
it('with warning state background', () => { it('with tooltip', () => {
expect(findTableRow().classes('gl-bg-orange-50')).toBe(true); expect(getWarningIconTooltipText().title).toBe(
'This project is locked because it is using 97.7KiB of free storage and there is no purchased storage available.',
);
});
}); });
it('with project link in default gray state', () => { describe('renders the row in warning state', () => {
expect(findProjectLink().classes('gl-text-gray-900!')).toBe(true); beforeEach(() => {
createComponent({ project: projects[1] });
});
it('with warning state background', () => {
expect(findTableRow().classes('gl-bg-orange-50')).toBe(true);
});
it('with project link in default gray state', () => {
expect(findProjectLink().classes('gl-text-gray-900!')).toBe(true);
});
it('with warning icon', () => {
expect(findWarningIcon().exists()).toBe(true);
});
it('with tooltip', () => {
expect(getWarningIconTooltipText().title).toBe(
'This project is near the free 97.7KiB limit and at risk of being locked.',
);
});
}); });
});
it('with warning icon', () => { describe('with extra storage purchased', () => {
expect(findWarningIcon().exists()).toBe(true); describe('if projects is in error state', () => {
beforeEach(() => {
createComponent({
project: projects[2],
additionalPurchasedStorageSize: 100000,
});
});
afterEach(() => {
wrapper.destroy();
});
it('renders purchased storage specific error tooltip ', () => {
expect(getWarningIconTooltipText().title).toBe(
'This project is locked because it used 97.7KiB of free storage and all the purchased storage.',
);
});
}); });
it('with tooltip', () => { describe('if projects is in warning state', () => {
expect(getWarningIconTooltipText().title).toBe('This project is at risk of being locked.'); beforeEach(() => {
createComponent({
project: projects[1],
additionalPurchasedStorageSize: 100000,
});
});
afterEach(() => {
wrapper.destroy();
});
it('renders purchased storage specific warning tooltip ', () => {
expect(getWarningIconTooltipText().title).toBe(
'This project is at risk of being locked because purchased storage is running low.',
);
});
}); });
}); });
}); });
...@@ -14,6 +14,7 @@ const createComponent = ({ additionalRepoStorageByNamespace = false } = {}) => { ...@@ -14,6 +14,7 @@ const createComponent = ({ additionalRepoStorageByNamespace = false } = {}) => {
wrapper = shallowMount(ProjectsTable, { wrapper = shallowMount(ProjectsTable, {
propsData: { propsData: {
projects, projects,
additionalPurchasedStorageSize: 0,
}, },
stubs, stubs,
provide: { provide: {
......
...@@ -28714,10 +28714,16 @@ msgstr "" ...@@ -28714,10 +28714,16 @@ 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 ""
msgid "UsageQuota|This project is at risk of being locked." msgid "UsageQuota|This project is at risk of being locked because purchased storage is running low."
msgstr "" msgstr ""
msgid "UsageQuota|This project is locked." msgid "UsageQuota|This project is locked because it is using %{actualRepositorySizeLimit} of free storage and there is no purchased storage available."
msgstr ""
msgid "UsageQuota|This project is locked because it used %{actualRepositorySizeLimit} of free storage and all the purchased storage."
msgstr ""
msgid "UsageQuota|This project is near the free %{actualRepositorySizeLimit} limit and at risk of being locked."
msgstr "" msgstr ""
msgid "UsageQuota|Total excess storage used" msgid "UsageQuota|Total excess storage used"
......
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