diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-20 14:34:42 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-20 14:34:42 +0000 |
commit | 9f46488805e86b1bc341ea1620b866016c2ce5ed (patch) | |
tree | f9748c7e287041e37d6da49e0a29c9511dc34768 /app/assets/javascripts/pipelines | |
parent | dfc92d081ea0332d69c8aca2f0e745cb48ae5e6d (diff) | |
download | gitlab-ce-9f46488805e86b1bc341ea1620b866016c2ce5ed.tar.gz |
Add latest changes from gitlab-org/gitlab@13-0-stable-ee
Diffstat (limited to 'app/assets/javascripts/pipelines')
16 files changed, 418 insertions, 41 deletions
diff --git a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue index ebd7a17040a..15c220a554d 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue @@ -69,7 +69,9 @@ export default { > <ci-icon :status="group.status" /> - <span class="ci-status-text text-truncate mw-70p gl-pl-1 d-inline-block align-bottom"> + <span + class="ci-status-text text-truncate mw-70p gl-pl-1-deprecated-no-really-do-not-use-me d-inline-block align-bottom" + > {{ group.name }} </span> diff --git a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue index 7125790ac3d..74a261f35d7 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue @@ -27,7 +27,9 @@ export default { <template> <span class="ci-job-name-component mw-100"> <ci-icon :status="status" /> - <span class="ci-status-text text-truncate mw-70p gl-pl-1 d-inline-block align-bottom"> + <span + class="ci-status-text text-truncate mw-70p gl-pl-1-deprecated-no-really-do-not-use-me d-inline-block align-bottom" + > {{ name }} </span> </span> diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue index 3d3dabbdf22..bed0ed51d5f 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -1,5 +1,5 @@ <script> -import { isEmpty, escape as esc } from 'lodash'; +import { isEmpty, escape } from 'lodash'; import stageColumnMixin from '../../mixins/stage_column_mixin'; import JobItem from './job_item.vue'; import JobGroupDropdown from './job_group_dropdown.vue'; @@ -44,7 +44,7 @@ export default { }, methods: { groupId(group) { - return `ci-badge-${esc(group.name)}`; + return `ci-badge-${escape(group.name)}`; }, pipelineActionRequestComplete() { this.$emit('refreshPipelineGraph'); diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue index d4f23697e09..fc93635bdb5 100644 --- a/app/assets/javascripts/pipelines/components/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines.vue @@ -9,14 +9,18 @@ import NavigationTabs from '../../vue_shared/components/navigation_tabs.vue'; import NavigationControls from './nav_controls.vue'; import { getParameterByName } from '../../lib/utils/common_utils'; import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; +import PipelinesFilteredSearch from './pipelines_filtered_search.vue'; +import { ANY_TRIGGER_AUTHOR, RAW_TEXT_WARNING } from '../constants'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { components: { TablePagination, NavigationTabs, NavigationControls, + PipelinesFilteredSearch, }, - mixins: [pipelinesMixin, CIPaginationMixin], + mixins: [pipelinesMixin, CIPaginationMixin, glFeatureFlagsMixin()], props: { store: { type: Object, @@ -78,6 +82,10 @@ export default { required: false, default: null, }, + projectId: { + type: String, + required: true, + }, }, data() { return { @@ -209,6 +217,9 @@ export default { }, ]; }, + canFilterPipelines() { + return this.glFeatures.filterPipelinesSearch; + }, }, created() { this.service = new PipelinesService(this.endpoint); @@ -238,6 +249,30 @@ export default { createFlash(s__('Pipelines|Something went wrong while cleaning runners cache.')); }); }, + resetRequestData() { + this.requestData = { page: this.page, scope: this.scope }; + }, + filterPipelines(filters) { + this.resetRequestData(); + + filters.forEach(filter => { + // do not add Any for username query param, so we + // can fetch all trigger authors + if (filter.type && filter.value.data !== ANY_TRIGGER_AUTHOR) { + this.requestData[filter.type] = filter.value.data; + } + + if (!filter.type) { + createFlash(RAW_TEXT_WARNING, 'warning'); + } + }); + + if (filters.length === 0) { + this.resetRequestData(); + } + + this.updateContent(this.requestData); + }, }, }; </script> @@ -267,6 +302,13 @@ export default { /> </div> + <pipelines-filtered-search + v-if="canFilterPipelines" + :pipelines="state.pipelines" + :project-id="projectId" + @filterPipelines="filterPipelines" + /> + <div class="content-list pipelines"> <gl-loading-icon v-if="stateToRender === $options.stateMap.loading" diff --git a/app/assets/javascripts/pipelines/components/pipelines_filtered_search.vue b/app/assets/javascripts/pipelines/components/pipelines_filtered_search.vue new file mode 100644 index 00000000000..8f9c3eb70a2 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipelines_filtered_search.vue @@ -0,0 +1,91 @@ +<script> +import { GlFilteredSearch } from '@gitlab/ui'; +import { __, s__ } from '~/locale'; +import PipelineTriggerAuthorToken from './tokens/pipeline_trigger_author_token.vue'; +import PipelineBranchNameToken from './tokens/pipeline_branch_name_token.vue'; +import Api from '~/api'; +import createFlash from '~/flash'; +import { FETCH_AUTHOR_ERROR_MESSAGE, FETCH_BRANCH_ERROR_MESSAGE } from '../constants'; + +export default { + components: { + GlFilteredSearch, + }, + props: { + pipelines: { + type: Array, + required: true, + }, + projectId: { + type: String, + required: true, + }, + }, + data() { + return { + projectUsers: null, + projectBranches: null, + }; + }, + computed: { + tokens() { + return [ + { + type: 'username', + icon: 'user', + title: s__('Pipeline|Trigger author'), + unique: true, + token: PipelineTriggerAuthorToken, + operators: [{ value: '=', description: __('is'), default: 'true' }], + triggerAuthors: this.projectUsers, + projectId: this.projectId, + }, + { + type: 'ref', + icon: 'branch', + title: s__('Pipeline|Branch name'), + unique: true, + token: PipelineBranchNameToken, + operators: [{ value: '=', description: __('is'), default: 'true' }], + branches: this.projectBranches, + projectId: this.projectId, + }, + ]; + }, + }, + created() { + Api.projectUsers(this.projectId) + .then(users => { + this.projectUsers = users; + }) + .catch(err => { + createFlash(FETCH_AUTHOR_ERROR_MESSAGE); + throw err; + }); + + Api.branches(this.projectId) + .then(({ data }) => { + this.projectBranches = data.map(branch => branch.name); + }) + .catch(err => { + createFlash(FETCH_BRANCH_ERROR_MESSAGE); + throw err; + }); + }, + methods: { + onSubmit(filters) { + this.$emit('filterPipelines', filters); + }, + }, +}; +</script> + +<template> + <div class="row-content-block"> + <gl-filtered-search + :placeholder="__('Filter pipelines')" + :available-tokens="tokens" + @submit="onSubmit" + /> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index e25f8ab4790..981914dd046 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -99,9 +99,10 @@ export default { // 3. If GitLab user does not have avatar, they might have a Gravatar } else if (this.pipeline.commit.author_gravatar_url) { - commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, { + commitAuthorInformation = { + ...this.pipeline.commit.author, avatar_url: this.pipeline.commit.author_gravatar_url, - }); + }; } // 4. If committer is not a GitLab User, they can have a Gravatar } else { diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue index 7426936515a..569920a4f31 100644 --- a/app/assets/javascripts/pipelines/components/stage.vue +++ b/app/assets/javascripts/pipelines/components/stage.vue @@ -137,7 +137,7 @@ export default { }, isDropdownOpen() { - return this.$el.classList.contains('open'); + return this.$el.classList.contains('show'); }, pipelineActionRequestComplete() { diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue b/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue index 388b300b39d..06ab45adf80 100644 --- a/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue +++ b/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue @@ -21,7 +21,8 @@ export default { return this.selectedSuite.total_count > 0; }, showTests() { - return this.testReports.total_count > 0; + const { test_suites: testSuites = [] } = this.testReports; + return testSuites.length > 0; }, }, methods: { diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue b/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue index 9739ef76867..80a1c83f171 100644 --- a/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue +++ b/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue @@ -29,7 +29,14 @@ export default { successPercentage() { // Returns a full number when the decimals equal .00. // Otherwise returns a float to two decimal points - return Number(((this.report.success_count / this.report.total_count) * 100 || 0).toFixed(2)); + // Do not include skipped tests as part of the total when doing success calculations. + + const totalCompletedCount = this.report.total_count - this.report.skipped_count; + + if (totalCompletedCount > 0) { + return Number(((this.report.success_count / totalCompletedCount) * 100 || 0).toFixed(2)); + } + return 0; }, formattedDuration() { return formatTime(secondsToMilliseconds(this.report.total_time)); diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue b/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue index 6effd6e949d..4dfb67dd8e8 100644 --- a/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue +++ b/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue @@ -1,14 +1,19 @@ <script> import { mapGetters } from 'vuex'; import { s__ } from '~/locale'; +import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; import store from '~/pipelines/stores/test_reports'; import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue'; export default { name: 'TestsSummaryTable', components: { + GlIcon, SmartVirtualList, }, + directives: { + GlTooltip: GlTooltipDirective, + }, store, props: { heading: { @@ -75,7 +80,10 @@ export default { v-for="(testSuite, index) in getTestSuites" :key="index" role="row" - class="gl-responsive-table-row gl-responsive-table-row-clickable test-reports-summary-row rounded cursor-pointer js-suite-row" + class="gl-responsive-table-row test-reports-summary-row rounded js-suite-row" + :class="{ + 'gl-responsive-table-row-clickable cursor-pointer': !testSuite.suite_error, + }" @click="tableRowClick(testSuite)" > <div class="table-section section-25"> @@ -84,6 +92,14 @@ export default { </div> <div class="table-mobile-content underline cgray pl-3"> {{ testSuite.name }} + <gl-icon + v-if="testSuite.suite_error" + ref="suiteErrorIcon" + v-gl-tooltip + name="error" + :title="testSuite.suite_error" + class="vertical-align-middle" + /> </div> </div> diff --git a/app/assets/javascripts/pipelines/components/tokens/pipeline_branch_name_token.vue b/app/assets/javascripts/pipelines/components/tokens/pipeline_branch_name_token.vue new file mode 100644 index 00000000000..a7a3f986255 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/tokens/pipeline_branch_name_token.vue @@ -0,0 +1,70 @@ +<script> +import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui'; +import Api from '~/api'; +import { FETCH_BRANCH_ERROR_MESSAGE, FILTER_PIPELINES_SEARCH_DELAY } from '../../constants'; +import createFlash from '~/flash'; +import { debounce } from 'lodash'; + +export default { + components: { + GlFilteredSearchToken, + GlFilteredSearchSuggestion, + GlLoadingIcon, + }, + props: { + config: { + type: Object, + required: true, + }, + value: { + type: Object, + required: true, + }, + }, + data() { + return { + branches: this.config.branches, + loading: true, + }; + }, + methods: { + fetchBranchBySearchTerm(searchTerm) { + Api.branches(this.config.projectId, searchTerm) + .then(res => { + this.branches = res.data.map(branch => branch.name); + this.loading = false; + }) + .catch(err => { + createFlash(FETCH_BRANCH_ERROR_MESSAGE); + this.loading = false; + throw err; + }); + }, + searchBranches: debounce(function debounceSearch({ data }) { + this.fetchBranchBySearchTerm(data); + }, FILTER_PIPELINES_SEARCH_DELAY), + }, +}; +</script> + +<template> + <gl-filtered-search-token + :config="config" + v-bind="{ ...$props, ...$attrs }" + v-on="$listeners" + @input="searchBranches" + > + <template #suggestions> + <gl-loading-icon v-if="loading" /> + <template v-else> + <gl-filtered-search-suggestion + v-for="(branch, index) in branches" + :key="index" + :value="branch" + > + {{ branch }} + </gl-filtered-search-suggestion> + </template> + </template> + </gl-filtered-search-token> +</template> diff --git a/app/assets/javascripts/pipelines/components/tokens/pipeline_trigger_author_token.vue b/app/assets/javascripts/pipelines/components/tokens/pipeline_trigger_author_token.vue new file mode 100644 index 00000000000..83e3558e1a1 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/tokens/pipeline_trigger_author_token.vue @@ -0,0 +1,114 @@ +<script> +import { + GlFilteredSearchToken, + GlAvatar, + GlFilteredSearchSuggestion, + GlDropdownDivider, + GlLoadingIcon, +} from '@gitlab/ui'; +import Api from '~/api'; +import createFlash from '~/flash'; +import { debounce } from 'lodash'; +import { + ANY_TRIGGER_AUTHOR, + FETCH_AUTHOR_ERROR_MESSAGE, + FILTER_PIPELINES_SEARCH_DELAY, +} from '../../constants'; + +export default { + anyTriggerAuthor: ANY_TRIGGER_AUTHOR, + components: { + GlFilteredSearchToken, + GlAvatar, + GlFilteredSearchSuggestion, + GlDropdownDivider, + GlLoadingIcon, + }, + props: { + config: { + type: Object, + required: true, + }, + value: { + type: Object, + required: true, + }, + }, + data() { + return { + users: this.config.triggerAuthors, + loading: true, + }; + }, + computed: { + currentValue() { + return this.value.data.toLowerCase(); + }, + activeUser() { + return this.users.find(user => { + return user.username.toLowerCase() === this.currentValue; + }); + }, + }, + methods: { + fetchAuthorBySearchTerm(searchTerm) { + Api.projectUsers(this.config.projectId, searchTerm) + .then(res => { + this.users = res; + this.loading = false; + }) + .catch(err => { + createFlash(FETCH_AUTHOR_ERROR_MESSAGE); + this.loading = false; + throw err; + }); + }, + searchAuthors: debounce(function debounceSearch({ data }) { + this.fetchAuthorBySearchTerm(data); + }, FILTER_PIPELINES_SEARCH_DELAY), + }, +}; +</script> + +<template> + <gl-filtered-search-token + :config="config" + v-bind="{ ...$props, ...$attrs }" + v-on="$listeners" + @input="searchAuthors" + > + <template #view="{inputValue}"> + <gl-avatar + v-if="activeUser" + :size="16" + :src="activeUser.avatar_url" + shape="circle" + class="gl-mr-2" + /> + <span>{{ activeUser ? activeUser.name : inputValue }}</span> + </template> + <template #suggestions> + <gl-filtered-search-suggestion :value="$options.anyTriggerAuthor">{{ + $options.anyTriggerAuthor + }}</gl-filtered-search-suggestion> + <gl-dropdown-divider /> + + <gl-loading-icon v-if="loading" /> + <template v-else> + <gl-filtered-search-suggestion + v-for="user in users" + :key="user.username" + :value="user.username" + > + <div class="d-flex"> + <gl-avatar :size="32" :src="user.avatar_url" /> + <div> + <div>{{ user.name }}</div> + <div>@{{ user.username }}</div> + </div> + </div> + </gl-filtered-search-suggestion> + </template> + </template> + </gl-filtered-search-token> +</template> diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js index c9655d18a04..d694430830b 100644 --- a/app/assets/javascripts/pipelines/constants.js +++ b/app/assets/javascripts/pipelines/constants.js @@ -1,9 +1,19 @@ +import { s__, __ } from '~/locale'; + export const CANCEL_REQUEST = 'CANCEL_REQUEST'; export const PIPELINES_TABLE = 'PIPELINES_TABLE'; export const LAYOUT_CHANGE_DELAY = 300; +export const FILTER_PIPELINES_SEARCH_DELAY = 200; +export const ANY_TRIGGER_AUTHOR = 'Any'; export const TestStatus = { FAILED: 'failed', SKIPPED: 'skipped', SUCCESS: 'success', }; + +export const FETCH_AUTHOR_ERROR_MESSAGE = __('There was a problem fetching project users.'); +export const FETCH_BRANCH_ERROR_MESSAGE = __('There was a problem fetching project branches.'); +export const RAW_TEXT_WARNING = s__( + 'Pipeline|Raw text search is not currently supported. Please use the available search tokens.', +); diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js index d76425c96b7..01295874e56 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -14,13 +14,7 @@ import axios from '~/lib/utils/axios_utils'; Vue.use(Translate); -export default () => { - const { dataset } = document.querySelector('.js-pipeline-details-vue'); - - const mediator = new PipelinesMediator({ endpoint: dataset.endpoint }); - - mediator.fetchPipeline(); - +const createPipelinesDetailApp = mediator => { // eslint-disable-next-line no-new new Vue({ el: '#js-pipeline-graph-vue', @@ -50,7 +44,9 @@ export default () => { }); }, }); +}; +const createPipelineHeaderApp = mediator => { // eslint-disable-next-line no-new new Vue({ el: '#js-pipeline-header-vue', @@ -94,7 +90,9 @@ export default () => { }); }, }); +}; +const createPipelinesTabs = dataset => { const tabsElement = document.querySelector('.pipelines-tabs'); const testReportsEnabled = window.gon && window.gon.features && window.gon.features.junitPipelineView; @@ -119,27 +117,40 @@ export default () => { tabsElement.addEventListener('click', tabClickHandler); } + } +}; - // eslint-disable-next-line no-new - new Vue({ - el: '#js-pipeline-tests-detail', - components: { - TestReports, - }, - render(createElement) { - return createElement('test-reports'); - }, - }); +const createTestDetails = detailsEndpoint => { + // eslint-disable-next-line no-new + new Vue({ + el: '#js-pipeline-tests-detail', + components: { + TestReports, + }, + render(createElement) { + return createElement('test-reports'); + }, + }); - axios - .get(dataset.testReportsCountEndpoint) - .then(({ data }) => { - if (!data.total_count) { - return; - } + axios + .get(detailsEndpoint) + .then(({ data }) => { + if (!data.total_count) { + return; + } - document.querySelector('.js-test-report-badge-counter').innerHTML = data.total_count; - }) - .catch(() => {}); - } + document.querySelector('.js-test-report-badge-counter').innerHTML = data.total_count; + }) + .catch(() => {}); +}; + +export default () => { + const { dataset } = document.querySelector('.js-pipeline-details-vue'); + const mediator = new PipelinesMediator({ endpoint: dataset.endpoint }); + mediator.fetchPipeline(); + + createPipelinesDetailApp(mediator); + createPipelineHeaderApp(mediator); + createPipelinesTabs(dataset); + createTestDetails(dataset.testReportsCountEndpoint); }; diff --git a/app/assets/javascripts/pipelines/services/pipelines_service.js b/app/assets/javascripts/pipelines/services/pipelines_service.js index 3c755db23dc..ae94d7a7ca0 100644 --- a/app/assets/javascripts/pipelines/services/pipelines_service.js +++ b/app/assets/javascripts/pipelines/services/pipelines_service.js @@ -19,13 +19,23 @@ export default class PipelinesService { } getPipelines(data = {}) { - const { scope, page } = data; + const { scope, page, username, ref } = data; const { CancelToken } = axios; + const queryParams = { scope, page }; + + if (username) { + queryParams.username = username; + } + + if (ref) { + queryParams.ref = ref; + } + this.cancelationSource = CancelToken.source(); return axios.get(this.endpoint, { - params: { scope, page }, + params: queryParams, cancelToken: this.cancelationSource.token, }); } diff --git a/app/assets/javascripts/pipelines/stores/pipeline_store.js b/app/assets/javascripts/pipelines/stores/pipeline_store.js index 1ef73760e02..c6f65277c8d 100644 --- a/app/assets/javascripts/pipelines/stores/pipeline_store.js +++ b/app/assets/javascripts/pipelines/stores/pipeline_store.js @@ -15,7 +15,7 @@ export default class PipelineStore { * @param {Object} pipeline */ storePipeline(pipeline = {}) { - const pipelineCopy = Object.assign({}, pipeline); + const pipelineCopy = { ...pipeline }; if (pipelineCopy.triggered_by) { pipelineCopy.triggered_by = [pipelineCopy.triggered_by]; |