diff options
Diffstat (limited to 'app/assets/javascripts/pipelines/components')
12 files changed, 357 insertions, 11 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> |