diff options
Diffstat (limited to 'app/assets/javascripts')
28 files changed, 462 insertions, 302 deletions
diff --git a/app/assets/javascripts/batch_comments/components/review_bar.vue b/app/assets/javascripts/batch_comments/components/review_bar.vue index 080a5543e53..158b5f45d1c 100644 --- a/app/assets/javascripts/batch_comments/components/review_bar.vue +++ b/app/assets/javascripts/batch_comments/components/review_bar.vue @@ -10,7 +10,6 @@ export default { }, computed: { ...mapGetters(['isNotesFetched']), - ...mapGetters('batchComments', ['draftsCount']), }, watch: { isNotesFetched() { @@ -25,7 +24,7 @@ export default { }; </script> <template> - <div v-show="draftsCount > 0"> + <div> <nav class="review-bar-component" data-testid="review_bar_component"> <div class="review-bar-content d-flex gl-justify-content-end" diff --git a/app/assets/javascripts/batch_comments/index.js b/app/assets/javascripts/batch_comments/index.js index 9c763e70d63..65fd34dcb00 100644 --- a/app/assets/javascripts/batch_comments/index.js +++ b/app/assets/javascripts/batch_comments/index.js @@ -1,7 +1,6 @@ import Vue from 'vue'; -import { mapActions } from 'vuex'; +import { mapActions, mapGetters } from 'vuex'; import store from '~/mr_notes/stores'; -import ReviewBar from './components/review_bar.vue'; export const initReviewBar = () => { const el = document.getElementById('js-review-bar'); @@ -10,6 +9,12 @@ export const initReviewBar = () => { new Vue({ el, store, + components: { + ReviewBar: () => import('./components/review_bar.vue'), + }, + computed: { + ...mapGetters('batchComments', ['draftsCount']), + }, mounted() { this.fetchDrafts(); }, @@ -17,7 +22,9 @@ export const initReviewBar = () => { ...mapActions('batchComments', ['fetchDrafts']), }, render(createElement) { - return createElement(ReviewBar); + if (this.draftsCount === 0) return null; + + return createElement('review-bar'); }, }); }; diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js index 8d88b682df2..2109aecdf03 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js +++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import { initPipelineCountListener } from './utils'; /** * Used in: @@ -12,13 +13,7 @@ export default () => { if (pipelineTableViewEl) { // Update MR and Commits tabs - pipelineTableViewEl.addEventListener('update-pipelines-count', (event) => { - if (event.detail.pipelineCount) { - const badge = document.querySelector('.js-pipelines-mr-count'); - - badge.textContent = event.detail.pipelineCount; - } - }); + initPipelineCountListener(pipelineTableViewEl); if (pipelineTableViewEl.dataset.disableInitialization === undefined) { const table = new Vue({ diff --git a/app/assets/javascripts/commit/pipelines/utils.js b/app/assets/javascripts/commit/pipelines/utils.js new file mode 100644 index 00000000000..52cbe52fa9b --- /dev/null +++ b/app/assets/javascripts/commit/pipelines/utils.js @@ -0,0 +1,11 @@ +export function initPipelineCountListener(el) { + if (!el) return; + + el.addEventListener('update-pipelines-count', (event) => { + if (event.detail.pipelineCount) { + const badge = document.querySelector('.js-pipelines-mr-count'); + + badge.textContent = event.detail.pipelineCount; + } + }); +} diff --git a/app/assets/javascripts/content_editor/content_editor.stories.js b/app/assets/javascripts/content_editor/content_editor.stories.js new file mode 100644 index 00000000000..8f2ce8feb5d --- /dev/null +++ b/app/assets/javascripts/content_editor/content_editor.stories.js @@ -0,0 +1,27 @@ +import { ContentEditor } from './index'; + +export default { + component: ContentEditor, + title: 'Components/Content Editor', +}; + +const Template = (_, { argTypes }) => ({ + components: { ContentEditor }, + props: Object.keys(argTypes), + template: '<content-editor v-bind="$props" @initialized="loadContent" />', + methods: { + loadContent(contentEditor) { + // eslint-disable-next-line @gitlab/require-i18n-strings + contentEditor.setSerializedContent('Hello content editor'); + }, + }, +}); + +export const Default = Template.bind({}); + +Default.args = { + renderMarkdown: () => '<p>Hello content editor</p>', + uploadsPath: '/uploads/', + serializerConfig: {}, + extensions: [], +}; diff --git a/app/assets/javascripts/diffs/components/pre_renderer.vue b/app/assets/javascripts/diffs/components/pre_renderer.vue index c357aa2d924..e4320c40d2c 100644 --- a/app/assets/javascripts/diffs/components/pre_renderer.vue +++ b/app/assets/javascripts/diffs/components/pre_renderer.vue @@ -17,7 +17,6 @@ export default { }, mounted() { this.width = this.$el.parentNode.offsetWidth; - window.test = this; this.$_itemsWithSizeWatcher = this.$watch('vscrollParent.itemsWithSize', async () => { await this.$nextTick(); diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js index bddc28c4758..1b1ab59b2b4 100644 --- a/app/assets/javascripts/diffs/index.js +++ b/app/assets/javascripts/diffs/index.js @@ -3,7 +3,6 @@ import Vue from 'vue'; import { mapActions, mapState, mapGetters } from 'vuex'; import { parseBoolean } from '~/lib/utils/common_utils'; import { getParameterValues } from '~/lib/utils/url_utility'; -import FindFile from '~/vue_shared/components/file_finder/index.vue'; import eventHub from '../notes/event_hub'; import diffsApp from './components/app.vue'; @@ -12,51 +11,7 @@ import { getReviewsForMergeRequest } from './utils/file_reviews'; import { getDerivedMergeRequestInformation } from './utils/merge_request'; export default function initDiffsApp(store) { - const fileFinderEl = document.getElementById('js-diff-file-finder'); - - if (fileFinderEl) { - // eslint-disable-next-line no-new - new Vue({ - el: fileFinderEl, - store, - computed: { - ...mapState('diffs', ['fileFinderVisible', 'isLoading']), - ...mapGetters('diffs', ['flatBlobsList']), - }, - watch: { - fileFinderVisible(newVal, oldVal) { - if (newVal && !oldVal && !this.flatBlobsList.length) { - eventHub.$emit('fetchDiffData'); - } - }, - }, - methods: { - ...mapActions('diffs', ['toggleFileFinder', 'scrollToFile']), - openFile(file) { - window.mrTabs.tabShown('diffs'); - this.scrollToFile(file.path); - }, - }, - render(createElement) { - return createElement(FindFile, { - props: { - files: this.flatBlobsList, - visible: this.fileFinderVisible, - loading: this.isLoading, - showDiffStats: true, - clearSearchOnClose: false, - }, - on: { - toggle: this.toggleFileFinder, - click: this.openFile, - }, - class: ['diff-file-finder'], - }); - }, - }); - } - - return new Vue({ + const vm = new Vue({ el: '#js-diffs-app', name: 'MergeRequestDiffs', components: { @@ -157,4 +112,53 @@ export default function initDiffsApp(store) { }); }, }); + + const fileFinderEl = document.getElementById('js-diff-file-finder'); + + if (fileFinderEl) { + // eslint-disable-next-line no-new + new Vue({ + el: fileFinderEl, + store, + components: { + FindFile: () => import('~/vue_shared/components/file_finder/index.vue'), + }, + computed: { + ...mapState('diffs', ['fileFinderVisible', 'isLoading']), + ...mapGetters('diffs', ['flatBlobsList']), + }, + watch: { + fileFinderVisible(newVal, oldVal) { + if (newVal && !oldVal && !this.flatBlobsList.length) { + eventHub.$emit('fetchDiffData'); + } + }, + }, + methods: { + ...mapActions('diffs', ['toggleFileFinder', 'scrollToFile']), + openFile(file) { + window.mrTabs.tabShown('diffs'); + this.scrollToFile(file.path); + }, + }, + render(createElement) { + return createElement('find-file', { + props: { + files: this.flatBlobsList, + visible: this.fileFinderVisible, + loading: this.isLoading, + showDiffStats: true, + clearSearchOnClose: false, + }, + on: { + toggle: this.toggleFileFinder, + click: this.openFile, + }, + class: ['diff-file-finder'], + }); + }, + }); + } + + return vm; } diff --git a/app/assets/javascripts/init_issuable_sidebar.js b/app/assets/javascripts/init_issuable_sidebar.js index 17c73fdf1c3..7a70d893008 100644 --- a/app/assets/javascripts/init_issuable_sidebar.js +++ b/app/assets/javascripts/init_issuable_sidebar.js @@ -1,9 +1,7 @@ /* eslint-disable no-new */ -import { mountSidebarLabels, getSidebarOptions } from '~/sidebar/mount_sidebar'; +import { getSidebarOptions } from '~/sidebar/mount_sidebar'; import IssuableContext from './issuable_context'; -import LabelsSelect from './labels_select'; -import MilestoneSelect from './milestone_select'; import Sidebar from './right_sidebar'; export default () => { @@ -13,12 +11,6 @@ export default () => { const sidebarOptions = getSidebarOptions(sidebarOptEl); - new MilestoneSelect({ - full_path: sidebarOptions.fullPath, - }); - new LabelsSelect(); new IssuableContext(sidebarOptions.currentUser); Sidebar.initialize(); - - mountSidebarLabels(); }; diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js index f3dedb7726a..f46263c0e4d 100644 --- a/app/assets/javascripts/lib/utils/number_utils.js +++ b/app/assets/javascripts/lib/utils/number_utils.js @@ -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) }); } /** diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index 0ddb2c2334c..e89b886df83 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -19,11 +19,9 @@ function MergeRequest(opts) { this.opts = opts != null ? opts : {}; this.submitNoteForm = this.submitNoteForm.bind(this); this.$el = $('.merge-request'); - this.$('.show-all-commits').on('click', () => this.showAllCommits()); this.initTabs(); this.initMRBtnListeners(); - this.initCommitMessageListeners(); if ($('.description.js-task-list-container').length) { this.taskList = new TaskList({ @@ -59,11 +57,6 @@ MergeRequest.prototype.initTabs = function () { window.mrTabs = new MergeRequestTabs(this.opts); }; -MergeRequest.prototype.showAllCommits = function () { - this.$('.first-commits').remove(); - return this.$('.all-commits').removeClass('hide'); -}; - MergeRequest.prototype.initMRBtnListeners = function () { const _this = this; const draftToggles = document.querySelectorAll('.js-draft-toggle-button'); @@ -128,26 +121,6 @@ MergeRequest.prototype.submitNoteForm = function (form, $button) { } }; -MergeRequest.prototype.initCommitMessageListeners = function () { - $(document).on('click', 'a.js-with-description-link', (e) => { - const textarea = $('textarea.js-commit-message'); - e.preventDefault(); - - textarea.val(textarea.data('messageWithDescription')); - $('.js-with-description-hint').hide(); - $('.js-without-description-hint').show(); - }); - - $(document).on('click', 'a.js-without-description-link', (e) => { - const textarea = $('textarea.js-commit-message'); - e.preventDefault(); - - textarea.val(textarea.data('messageWithoutDescription')); - $('.js-with-description-hint').show(); - $('.js-without-description-hint').hide(); - }); -}; - MergeRequest.decreaseCounter = function (by = 1) { const $el = $('.js-merge-counter'); const count = Math.max(parseInt($el.text().replace(/[^\d]/, ''), 10) - by, 0); diff --git a/app/assets/javascripts/mr_notes/index.js b/app/assets/javascripts/mr_notes/index.js index a7696a716d0..ea3e4e5604c 100644 --- a/app/assets/javascripts/mr_notes/index.js +++ b/app/assets/javascripts/mr_notes/index.js @@ -19,6 +19,7 @@ export default function initMrNotes() { action: mrShowNode.dataset.mrAction, }); + initDiffsApp(store); initNotesApp(); document.addEventListener('merged:UpdateActions', () => { @@ -26,20 +27,25 @@ export default function initMrNotes() { initCherryPickCommitModal(); }); - // eslint-disable-next-line no-new - new Vue({ - el: '#js-vue-discussion-counter', - name: 'DiscussionCounter', - components: { - discussionCounter, - }, - store, - render(createElement) { - return createElement('discussion-counter'); - }, - }); + requestIdleCallback(() => { + const el = document.getElementById('js-vue-discussion-counter'); - initDiscussionFilters(store); - initSortDiscussions(store); - initDiffsApp(store); + if (el) { + // eslint-disable-next-line no-new + new Vue({ + el, + name: 'DiscussionCounter', + components: { + discussionCounter, + }, + store, + render(createElement) { + return createElement('discussion-counter'); + }, + }); + } + + initDiscussionFilters(store); + initSortDiscussions(store); + }); } diff --git a/app/assets/javascripts/pages/projects/issues/show/index.js b/app/assets/javascripts/pages/projects/issues/show/index.js index e4f99d1e7fd..1282d2aa303 100644 --- a/app/assets/javascripts/pages/projects/issues/show/index.js +++ b/app/assets/javascripts/pages/projects/issues/show/index.js @@ -1,7 +1,8 @@ +import { store } from '~/notes/stores'; import initRelatedIssues from '~/related_issues'; import initSidebarBundle from '~/sidebar/sidebar_bundle'; import initShow from '../show'; initShow(); -initSidebarBundle(); +initSidebarBundle(store); initRelatedIssues(); diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js index 25864149378..dadf0988582 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js +++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js @@ -2,11 +2,10 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import loadAwardsHandler from '~/awards_handler'; import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable'; -import initPipelines from '~/commit/pipelines/pipelines_bundle'; +import { initPipelineCountListener } from '~/commit/pipelines/utils'; import initIssuableSidebar from '~/init_issuable_sidebar'; import StatusBox from '~/issuable/components/status_box.vue'; import createDefaultClient from '~/lib/graphql'; -import { handleLocationHash } from '~/lib/utils/common_utils'; import initSourcegraph from '~/sourcegraph'; import ZenMode from '~/zen_mode'; import getStateQuery from './queries/get_state.query.graphql'; @@ -15,11 +14,10 @@ export default function initMergeRequestShow() { const awardEmojiEl = document.getElementById('js-vue-awards-block'); new ZenMode(); // eslint-disable-line no-new - initIssuableSidebar(); - initPipelines(); + initPipelineCountListener(document.querySelector('#commit-pipeline-table-view')); new ShortcutsIssuable(true); // eslint-disable-line no-new - handleLocationHash(); initSourcegraph(); + initIssuableSidebar(); if (awardEmojiEl) { import('~/emoji/awards_app') .then((m) => m.default(awardEmojiEl)) diff --git a/app/assets/javascripts/pages/projects/merge_requests/show/index.js b/app/assets/javascripts/pages/projects/merge_requests/show/index.js index 546fa66eda6..25dede33880 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/show/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/show/index.js @@ -5,8 +5,11 @@ import initSidebarBundle from '~/sidebar/sidebar_bundle'; import initIssuableHeaderWarning from '~/vue_shared/components/issuable/init_issuable_header_warning'; import initShow from '../init_merge_request_show'; -initShow(); -initSidebarBundle(); initMrNotes(); -initReviewBar(); -initIssuableHeaderWarning(store); +initShow(); + +requestIdleCallback(() => { + initSidebarBundle(store); + initReviewBar(); + initIssuableHeaderWarning(store); +}); diff --git a/app/assets/javascripts/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue b/app/assets/javascripts/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue index ebe73bdcec3..551a0430fbf 100644 --- a/app/assets/javascripts/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue +++ b/app/assets/javascripts/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue @@ -1,21 +1,14 @@ <script> -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import BranchSwitcher from './branch_switcher.vue'; export default { components: { BranchSwitcher, }, - mixins: [glFeatureFlagsMixin()], - computed: { - showBranchSwitcher() { - return this.glFeatures.pipelineEditorBranchSwitcher; - }, - }, }; </script> <template> <div class="gl-mb-4"> - <branch-switcher v-if="showBranchSwitcher" v-on="$listeners" /> + <branch-switcher v-on="$listeners" /> </div> </template> diff --git a/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue b/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue index 0ac4a40ff4a..fbb66231f16 100644 --- a/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue +++ b/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue @@ -24,9 +24,6 @@ export default { }, }, computed: { - showFileNav() { - return this.glFeatures.pipelineEditorBranchSwitcher; - }, showCTAButton() { return this.glFeatures.pipelineEditorEmptyStateAction; }, @@ -40,7 +37,7 @@ export default { </script> <template> <div> - <pipeline-editor-file-nav v-if="showFileNav" v-on="$listeners" /> + <pipeline-editor-file-nav v-on="$listeners" /> <div class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-mt-11"> <img :src="emptyStateIllustrationPath" /> <h1 class="gl-font-size-h1">{{ $options.i18n.title }}</h1> diff --git a/app/assets/javascripts/pipelines/components/parsing_utils.js b/app/assets/javascripts/pipelines/components/parsing_utils.js index 48658c7ffe0..fa7330ce890 100644 --- a/app/assets/javascripts/pipelines/components/parsing_utils.js +++ b/app/assets/javascripts/pipelines/components/parsing_utils.js @@ -1,58 +1,8 @@ import { memoize } from 'lodash'; +import { createNodeDict } from '../utils'; import { createSankey } from './dag/drawing_utils'; /* - The following functions are the main engine in transforming the data as - received from the endpoint into the format the d3 graph expects. - - Input is of the form: - [nodes] - nodes: [{category, name, jobs, size}] - category is the stage name - name is a group name; in the case that the group has one job, it is - also the job name - size is the number of parallel jobs - jobs: [{ name, needs}] - job name is either the same as the group name or group x/y - needs: [job-names] - needs is an array of job-name strings - - Output is of the form: - { nodes: [node], links: [link] } - node: { name, category }, + unused info passed through - link: { source, target, value }, with source & target being node names - and value being a constant - - We create nodes in the GraphQL update function, and then here we create the node dictionary, - then create links, and then dedupe the links, so that in the case where - job 4 depends on job 1 and job 2, and job 2 depends on job 1, we show only a single link - from job 1 to job 2 then another from job 2 to job 4. - - CREATE LINKS - nodes.name -> target - nodes.name.needs.each -> source (source is the name of the group, not the parallel job) - 10 -> value (constant) - */ - -export const createNodeDict = (nodes) => { - return nodes.reduce((acc, node) => { - const newNode = { - ...node, - needs: node.jobs.map((job) => job.needs || []).flat(), - }; - - if (node.size > 1) { - node.jobs.forEach((job) => { - acc[job.name] = newNode; - }); - } - - acc[node.name] = newNode; - return acc; - }, {}); -}; - -/* A peformant alternative to lodash's isEqual. Because findIndex always finds the first instance of a match, if the found index is not the first, we know it is in fact a duplicate. diff --git a/app/assets/javascripts/pipelines/utils.js b/app/assets/javascripts/pipelines/utils.js index 02a9e5b7fc6..e28eb74fb1b 100644 --- a/app/assets/javascripts/pipelines/utils.js +++ b/app/assets/javascripts/pipelines/utils.js @@ -1,8 +1,58 @@ import * as Sentry from '@sentry/browser'; import { pickBy } from 'lodash'; -import { createNodeDict } from './components/parsing_utils'; import { SUPPORTED_FILTER_PARAMETERS } from './constants'; +/* + The following functions are the main engine in transforming the data as + received from the endpoint into the format the d3 graph expects. + + Input is of the form: + [nodes] + nodes: [{category, name, jobs, size}] + category is the stage name + name is a group name; in the case that the group has one job, it is + also the job name + size is the number of parallel jobs + jobs: [{ name, needs}] + job name is either the same as the group name or group x/y + needs: [job-names] + needs is an array of job-name strings + + Output is of the form: + { nodes: [node], links: [link] } + node: { name, category }, + unused info passed through + link: { source, target, value }, with source & target being node names + and value being a constant + + We create nodes in the GraphQL update function, and then here we create the node dictionary, + then create links, and then dedupe the links, so that in the case where + job 4 depends on job 1 and job 2, and job 2 depends on job 1, we show only a single link + from job 1 to job 2 then another from job 2 to job 4. + + CREATE LINKS + nodes.name -> target + nodes.name.needs.each -> source (source is the name of the group, not the parallel job) + 10 -> value (constant) + */ + +export const createNodeDict = (nodes) => { + return nodes.reduce((acc, node) => { + const newNode = { + ...node, + needs: node.jobs.map((job) => job.needs || []).flat(), + }; + + if (node.size > 1) { + node.jobs.forEach((job) => { + acc[job.name] = newNode; + }); + } + + acc[node.name] = newNode; + return acc; + }, {}); +}; + export const validateParams = (params) => { return pickBy(params, (val, key) => SUPPORTED_FILTER_PARAMETERS.includes(key) && val); }; diff --git a/app/assets/javascripts/projects/storage_counter/components/app.vue b/app/assets/javascripts/projects/storage_counter/components/app.vue index cfb08c11f0e..1a911ea3d9b 100644 --- a/app/assets/javascripts/projects/storage_counter/components/app.vue +++ b/app/assets/javascripts/projects/storage_counter/components/app.vue @@ -1,17 +1,30 @@ <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> diff --git a/app/assets/javascripts/projects/storage_counter/components/storage_table.vue b/app/assets/javascripts/projects/storage_counter/components/storage_table.vue new file mode 100644 index 00000000000..50aa70ecc9d --- /dev/null +++ b/app/assets/javascripts/projects/storage_counter/components/storage_table.vue @@ -0,0 +1,74 @@ +<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> diff --git a/app/assets/javascripts/projects/storage_counter/constants.js b/app/assets/javascripts/projects/storage_counter/constants.js new file mode 100644 index 00000000000..d9b28abfbe7 --- /dev/null +++ b/app/assets/javascripts/projects/storage_counter/constants.js @@ -0,0 +1,61 @@ +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.', +); diff --git a/app/assets/javascripts/projects/storage_counter/index.js b/app/assets/javascripts/projects/storage_counter/index.js index 8812bfb56a2..10668f08402 100644 --- a/app/assets/javascripts/projects/storage_counter/index.js +++ b/app/assets/javascripts/projects/storage_counter/index.js @@ -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); diff --git a/app/assets/javascripts/projects/storage_counter/utils.js b/app/assets/javascripts/projects/storage_counter/utils.js index cfc5f14b26a..7b7182cbbf0 100644 --- a/app/assets/javascripts/projects/storage_counter/utils.js +++ b/app/assets/javascripts/projects/storage_counter/utils.js @@ -1,36 +1,5 @@ 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, diff --git a/app/assets/javascripts/reports/components/issue_body.js b/app/assets/javascripts/reports/components/issue_body.js index 6014d9d6ad8..04e72809e62 100644 --- a/app/assets/javascripts/reports/components/issue_body.js +++ b/app/assets/javascripts/reports/components/issue_body.js @@ -1,18 +1,16 @@ import IssueStatusIcon from '~/reports/components/issue_status_icon.vue'; -import AccessibilityIssueBody from '../accessibility_report/components/accessibility_issue_body.vue'; -import CodequalityIssueBody from '../codequality_report/components/codequality_issue_body.vue'; -import TestIssueBody from '../grouped_test_report/components/test_issue_body.vue'; export const components = { - AccessibilityIssueBody, - CodequalityIssueBody, - TestIssueBody, + AccessibilityIssueBody: () => + import('../accessibility_report/components/accessibility_issue_body.vue'), + CodequalityIssueBody: () => import('../codequality_report/components/codequality_issue_body.vue'), + TestIssueBody: () => import('../grouped_test_report/components/test_issue_body.vue'), }; export const componentNames = { - AccessibilityIssueBody: AccessibilityIssueBody.name, - CodequalityIssueBody: CodequalityIssueBody.name, - TestIssueBody: TestIssueBody.name, + AccessibilityIssueBody: 'AccessibilityIssueBody', + CodequalityIssueBody: 'CodequalityIssueBody', + TestIssueBody: 'TestIssueBody', }; export const iconComponents = { diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js index 12d9d9ab222..10ab80f4ec2 100644 --- a/app/assets/javascripts/sidebar/mount_sidebar.js +++ b/app/assets/javascripts/sidebar/mount_sidebar.js @@ -1,7 +1,6 @@ import $ from 'jquery'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; -import createFlash from '~/flash'; import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/graphql_shared/constants'; import { convertToGraphQLId } from '~/graphql_shared/utils'; import initInviteMembersModal from '~/invite_members/init_invite_members_modal'; @@ -13,7 +12,6 @@ import { isInIncidentPage, parseBoolean, } from '~/lib/utils/common_utils'; -import { __ } from '~/locale'; import CollapsedAssigneeList from '~/sidebar/components/assignees/collapsed_assignee_list.vue'; import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue'; import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue'; @@ -363,10 +361,10 @@ function mountReferenceComponent() { }); } -function mountLockComponent() { +function mountLockComponent(store) { const el = document.getElementById('js-lock-entry-point'); - if (!el) { + if (!el || !store) { return; } @@ -375,37 +373,20 @@ function mountLockComponent() { const dataNode = document.getElementById('js-lock-issue-data'); const initialData = JSON.parse(dataNode.innerHTML); - let importStore; - if (isInIssuePage() || isInIncidentPage()) { - importStore = import(/* webpackChunkName: 'notesStore' */ '~/notes/stores').then( - ({ store }) => store, - ); - } else { - importStore = import(/* webpackChunkName: 'mrNotesStore' */ '~/mr_notes/stores').then( - (store) => store.default, - ); - } - - importStore - .then( - (store) => - new Vue({ - el, - store, - provide: { - fullPath, - }, - render: (createElement) => - createElement(IssuableLockForm, { - props: { - isEditable: initialData.is_editable, - }, - }), - }), - ) - .catch(() => { - createFlash({ message: __('Failed to load sidebar lock status') }); - }); + // eslint-disable-next-line no-new + new Vue({ + el, + store, + provide: { + fullPath, + }, + render: (createElement) => + createElement(IssuableLockForm, { + props: { + isEditable: initialData.is_editable, + }, + }), + }); } function mountParticipantsComponent() { @@ -537,7 +518,7 @@ function mountCopyEmailComponent() { const isAssigneesWidgetShown = (isInIssuePage() || isInDesignPage()) && gon.features.issueAssigneesWidget; -export function mountSidebar(mediator) { +export function mountSidebar(mediator, store) { initInviteMembersModal(); initInviteMembersTrigger(); @@ -548,11 +529,12 @@ export function mountSidebar(mediator) { mountAssigneesComponentDeprecated(mediator); } mountReviewersComponent(mediator); + mountSidebarLabels(); mountMilestoneSelect(); mountConfidentialComponent(mediator); mountDueDateComponent(mediator); mountReferenceComponent(mediator); - mountLockComponent(); + mountLockComponent(store); mountParticipantsComponent(); mountSubscriptionsComponent(); mountCopyEmailComponent(); diff --git a/app/assets/javascripts/sidebar/sidebar_bundle.js b/app/assets/javascripts/sidebar/sidebar_bundle.js index 063e3313a3c..1be670f7590 100644 --- a/app/assets/javascripts/sidebar/sidebar_bundle.js +++ b/app/assets/javascripts/sidebar/sidebar_bundle.js @@ -1,9 +1,9 @@ -import { mountSidebar, getSidebarOptions } from './mount_sidebar'; +import { mountSidebar, getSidebarOptions } from 'ee_else_ce/sidebar/mount_sidebar'; import Mediator from './sidebar_mediator'; -export default () => { +export default (store) => { const mediator = new Mediator(getSidebarOptions()); mediator.fetch(); - mountSidebar(mediator); + mountSidebar(mediator, store); }; diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js index 3a3a1329483..b10e0e2bc88 100644 --- a/app/assets/javascripts/vue_merge_request_widget/index.js +++ b/app/assets/javascripts/vue_merge_request_widget/index.js @@ -7,8 +7,6 @@ import VueApollo from 'vue-apollo'; import MrWidgetOptions from 'ee_else_ce/vue_merge_request_widget/mr_widget_options.vue'; import createDefaultClient from '~/lib/graphql'; import Translate from '../vue_shared/translate'; -import { registerExtension } from './components/extensions'; -import issueExtension from './extensions/issues'; Vue.use(Translate); Vue.use(VueApollo); @@ -28,8 +26,6 @@ export default () => { gl.mrWidgetData.gitlabLogo = gon.gitlab_logo; gl.mrWidgetData.defaultAvatarUrl = gon.default_avatar_url; - registerExtension(issueExtension); - const vm = new Vue({ el: '#js-vue-mr-widget', provide: { diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index c17fdbe1e47..f8e6dcf7ec0 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -12,9 +12,6 @@ import { sprintf, s__, __ } from '~/locale'; import Project from '~/pages/projects/project'; import SmartInterval from '~/smart_interval'; import { setFaviconOverlay } from '../lib/utils/favicon'; -import GroupedAccessibilityReportsApp from '../reports/accessibility_report/grouped_accessibility_reports_app.vue'; -import GroupedCodequalityReportsApp from '../reports/codequality_report/grouped_codequality_reports_app.vue'; -import GroupedTestReportsApp from '../reports/grouped_test_report/grouped_test_reports_app.vue'; import Loading from './components/loading.vue'; import MrWidgetAlertMessage from './components/mr_widget_alert_message.vue'; import WidgetHeader from './components/mr_widget_header.vue'; @@ -42,7 +39,6 @@ import ShaMismatch from './components/states/sha_mismatch.vue'; import UnresolvedDiscussionsState from './components/states/unresolved_discussions.vue'; import WorkInProgressState from './components/states/work_in_progress.vue'; // import ExtensionsContainer from './components/extensions/container'; -import TerraformPlan from './components/terraform/mr_widget_terraform_container.vue'; import eventHub from './event_hub'; import mergeRequestQueryVariablesMixin from './mixins/merge_request_query_variables'; import getStateQuery from './queries/get_state.query.graphql'; @@ -84,10 +80,13 @@ export default { 'mr-widget-auto-merge-failed': AutoMergeFailed, 'mr-widget-rebase': RebaseState, SourceBranchRemovalStatus, - GroupedCodequalityReportsApp, - GroupedTestReportsApp, - TerraformPlan, - GroupedAccessibilityReportsApp, + GroupedCodequalityReportsApp: () => + import('../reports/codequality_report/grouped_codequality_reports_app.vue'), + GroupedTestReportsApp: () => + import('../reports/grouped_test_report/grouped_test_reports_app.vue'), + TerraformPlan: () => import('./components/terraform/mr_widget_terraform_container.vue'), + GroupedAccessibilityReportsApp: () => + import('../reports/accessibility_report/grouped_accessibility_reports_app.vue'), MrWidgetApprovals, SecurityReportsApp: () => import('~/vue_shared/security_reports/security_reports_app.vue'), }, |