summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/pipelines/components
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/pipelines/components')
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_name_component.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines.vue44
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_filtered_search.vue91
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table_row.vue5
-rw-r--r--app/assets/javascripts/pipelines/components/stage.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_reports.vue3
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_summary.vue9
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue18
-rw-r--r--app/assets/javascripts/pipelines/components/tokens/pipeline_branch_name_token.vue70
-rw-r--r--app/assets/javascripts/pipelines/components/tokens/pipeline_trigger_author_token.vue114
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>