Commit 411b8819 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch 'pb-pipelines-table-refactor-build-table' into 'master'

Refactor pipelines table to GlTable

See merge request gitlab-org/gitlab!55392
parents de57a7e3 3868d525
<script>
import { GlButton, GlTooltipDirective, GlModalDirective } from '@gitlab/ui';
import { __ } from '~/locale';
import eventHub from '../../event_hub';
import PipelinesArtifactsComponent from './pipelines_artifacts.vue';
import PipelinesManualActions from './pipelines_manual_actions.vue';
export default {
i18n: {
cancelTitle: __('Cancel'),
redeployTitle: __('Retry'),
},
directives: {
GlTooltip: GlTooltipDirective,
GlModalDirective,
},
components: {
GlButton,
PipelinesManualActions,
PipelinesArtifactsComponent,
},
props: {
pipeline: {
type: Object,
required: true,
},
cancelingPipeline: {
type: Number,
required: false,
default: null,
},
},
data() {
return {
isRetrying: false,
};
},
computed: {
displayPipelineActions() {
return (
this.pipeline.flags.retryable ||
this.pipeline.flags.cancelable ||
this.pipeline.details.manual_actions.length ||
this.pipeline.details.artifacts.length
);
},
actions() {
if (!this.pipeline || !this.pipeline.details) {
return [];
}
const { details } = this.pipeline;
return [...(details.manual_actions || []), ...(details.scheduled_actions || [])];
},
isCancelling() {
return this.cancelingPipeline === this.pipeline.id;
},
},
watch: {
pipeline() {
this.isRetrying = false;
},
},
methods: {
handleCancelClick() {
eventHub.$emit('openConfirmationModal', {
pipeline: this.pipeline,
endpoint: this.pipeline.cancel_path,
});
},
handleRetryClick() {
this.isRetrying = true;
eventHub.$emit('retryPipeline', this.pipeline.retry_path);
},
},
};
</script>
<template>
<div v-if="displayPipelineActions" class="gl-text-right">
<div class="btn-group">
<pipelines-manual-actions v-if="actions.length > 0" :actions="actions" />
<pipelines-artifacts-component
v-if="pipeline.details.artifacts.length"
:artifacts="pipeline.details.artifacts"
/>
<gl-button
v-if="pipeline.flags.retryable"
v-gl-tooltip.hover
:aria-label="$options.i18n.redeployTitle"
:title="$options.i18n.redeployTitle"
:disabled="isRetrying"
:loading="isRetrying"
class="js-pipelines-retry-button"
data-qa-selector="pipeline_retry_button"
icon="repeat"
variant="default"
category="secondary"
@click="handleRetryClick"
/>
<gl-button
v-if="pipeline.flags.cancelable"
v-gl-tooltip.hover
v-gl-modal-directive="'confirmation-modal'"
:aria-label="$options.i18n.cancelTitle"
:title="$options.i18n.cancelTitle"
:loading="isCancelling"
:disabled="isCancelling"
icon="close"
variant="danger"
category="primary"
class="js-pipelines-cancel-button"
@click="handleCancelClick"
/>
</div>
</div>
</template>
...@@ -29,7 +29,7 @@ export default { ...@@ -29,7 +29,7 @@ export default {
}; };
</script> </script>
<template> <template>
<div :class="classes"> <div :class="classes" data-testid="pipeline-triggerer">
<user-avatar-link <user-avatar-link
v-if="user" v-if="user"
:link-href="user.path" :link-href="user.path"
......
...@@ -61,7 +61,7 @@ export default { ...@@ -61,7 +61,7 @@ export default {
}; };
</script> </script>
<template> <template>
<div :class="classes"> <div :class="classes" data-testid="pipeline-url-table-cell">
<gl-link <gl-link
:href="pipeline.path" :href="pipeline.path"
data-testid="pipeline-url-link" data-testid="pipeline-url-link"
......
<script>
import { CHILD_VIEW } from '~/pipelines/constants';
import CommitComponent from '~/vue_shared/components/commit.vue';
export default {
components: {
CommitComponent,
},
props: {
pipeline: {
type: Object,
required: true,
},
viewType: {
type: String,
required: true,
},
},
computed: {
commitAuthor() {
let commitAuthorInformation;
if (!this.pipeline || !this.pipeline.commit) {
return null;
}
// 1. person who is an author of a commit might be a GitLab user
if (this.pipeline.commit.author) {
// 2. if person who is an author of a commit is a GitLab user
// they can have a GitLab avatar
if (this.pipeline.commit.author.avatar_url) {
commitAuthorInformation = this.pipeline.commit.author;
// 3. If GitLab user does not have avatar, they might have a Gravatar
} else if (this.pipeline.commit.author_gravatar_url) {
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 {
commitAuthorInformation = {
avatar_url: this.pipeline.commit.author_gravatar_url,
path: `mailto:${this.pipeline.commit.author_email}`,
username: this.pipeline.commit.author_name,
};
}
return commitAuthorInformation;
},
commitTag() {
return this.pipeline?.ref?.tag;
},
commitRef() {
return this.pipeline?.ref;
},
commitUrl() {
return this.pipeline?.commit?.commit_path;
},
commitShortSha() {
return this.pipeline?.commit?.short_id;
},
commitTitle() {
return this.pipeline?.commit?.title;
},
isChildView() {
return this.viewType === CHILD_VIEW;
},
},
};
</script>
<template>
<commit-component
:tag="commitTag"
:commit-ref="commitRef"
:commit-url="commitUrl"
:merge-request-ref="pipeline.merge_request"
:short-sha="commitShortSha"
:title="commitTitle"
:author="commitAuthor"
:show-ref-info="!isChildView"
/>
</template>
<script>
import { CHILD_VIEW } from '~/pipelines/constants';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
export default {
components: {
CiBadge,
},
props: {
pipeline: {
type: Object,
required: true,
},
viewType: {
type: String,
required: true,
},
},
computed: {
pipelineStatus() {
return this.pipeline?.details?.status ?? {};
},
isChildView() {
return this.viewType === CHILD_VIEW;
},
},
};
</script>
<template>
<ci-badge
:status="pipelineStatus"
:show-text="!isChildView"
:icon-classes="'gl-vertical-align-middle!'"
data-qa-selector="pipeline_commit_status"
/>
</template>
<script> <script>
import { GlTable, GlTooltipDirective } from '@gitlab/ui'; import { GlTable, GlTooltipDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
import PipelineOperations from './pipeline_operations.vue';
import PipelineStopModal from './pipeline_stop_modal.vue'; import PipelineStopModal from './pipeline_stop_modal.vue';
import PipelineTriggerer from './pipeline_triggerer.vue';
import PipelineUrl from './pipeline_url.vue';
import PipelinesCommit from './pipelines_commit.vue';
import PipelinesStatusBadge from './pipelines_status_badge.vue';
import PipelinesTableRowComponent from './pipelines_table_row.vue'; import PipelinesTableRowComponent from './pipelines_table_row.vue';
import PipelineStage from './stage.vue';
import PipelinesTimeago from './time_ago.vue';
const DEFAULT_TD_CLASS = 'gl-p-5!';
const HIDE_TD_ON_MOBILE = 'gl-display-none! gl-lg-display-table-cell!';
const DEFAULT_TH_CLASSES =
'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1! gl-font-sm!';
export default { export default {
fields: [
{
key: 'status',
label: s__('Pipeline|Status'),
thClass: DEFAULT_TH_CLASSES,
columnClass: 'gl-w-10p',
tdClass: DEFAULT_TD_CLASS,
thAttr: { 'data-testid': 'status-th' },
},
{
key: 'pipeline',
label: s__('Pipeline|Pipeline'),
thClass: DEFAULT_TH_CLASSES,
tdClass: `${DEFAULT_TD_CLASS} ${HIDE_TD_ON_MOBILE}`,
columnClass: 'gl-w-10p',
thAttr: { 'data-testid': 'pipeline-th' },
},
{
key: 'triggerer',
label: s__('Pipeline|Triggerer'),
thClass: DEFAULT_TH_CLASSES,
tdClass: `${DEFAULT_TD_CLASS} ${HIDE_TD_ON_MOBILE}`,
columnClass: 'gl-w-10p',
thAttr: { 'data-testid': 'triggerer-th' },
},
{
key: 'commit',
label: s__('Pipeline|Commit'),
thClass: DEFAULT_TH_CLASSES,
tdClass: DEFAULT_TD_CLASS,
columnClass: 'gl-w-20p',
thAttr: { 'data-testid': 'commit-th' },
},
{
key: 'stages',
label: s__('Pipeline|Stages'),
thClass: DEFAULT_TH_CLASSES,
tdClass: DEFAULT_TD_CLASS,
columnClass: 'gl-w-15p',
thAttr: { 'data-testid': 'stages-th' },
},
{
key: 'timeago',
label: s__('Pipeline|Duration'),
thClass: DEFAULT_TH_CLASSES,
tdClass: DEFAULT_TD_CLASS,
columnClass: 'gl-w-15p',
thAttr: { 'data-testid': 'timeago-th' },
},
{
key: 'actions',
label: '',
thClass: DEFAULT_TH_CLASSES,
tdClass: DEFAULT_TD_CLASS,
columnClass: 'gl-w-20p',
thAttr: { 'data-testid': 'actions-th' },
},
],
components: { components: {
GlTable, GlTable,
PipelinesTableRowComponent, PipelinesCommit,
PipelineOperations,
PipelineStage,
PipelinesStatusBadge,
PipelineStopModal, PipelineStopModal,
PipelinesTableRowComponent,
PipelinesTimeago,
PipelineTriggerer,
PipelineUrl,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -43,11 +121,6 @@ export default { ...@@ -43,11 +121,6 @@ export default {
cancelingPipeline: null, cancelingPipeline: null,
}; };
}, },
computed: {
legacyTableClass() {
return !this.glFeatures.newPipelinesTable ? 'ci-table' : '';
},
},
watch: { watch: {
pipelines() { pipelines() {
this.cancelingPipeline = null; this.cancelingPipeline = null;
...@@ -73,8 +146,8 @@ export default { ...@@ -73,8 +146,8 @@ export default {
}; };
</script> </script>
<template> <template>
<div :class="legacyTableClass"> <div class="ci-table">
<div v-if="!glFeatures.newPipelinesTable" data-testid="ci-table"> <div v-if="!glFeatures.newPipelinesTable" data-testid="legacy-ci-table">
<div class="gl-responsive-table-row table-row-header" role="row"> <div class="gl-responsive-table-row table-row-header" role="row">
<div class="table-section section-10 js-pipeline-status" role="rowheader"> <div class="table-section section-10 js-pipeline-status" role="rowheader">
{{ s__('Pipeline|Status') }} {{ s__('Pipeline|Status') }}
...@@ -107,7 +180,71 @@ export default { ...@@ -107,7 +180,71 @@ export default {
/> />
</div> </div>
<gl-table v-else /> <gl-table
v-else
:fields="$options.fields"
:items="pipelines"
tbody-tr-class="commit"
:tbody-tr-attr="{ 'data-testid': 'pipeline-table-row' }"
stacked="lg"
fixed
>
<template #head(actions)>
<slot name="table-header-actions"></slot>
</template>
<template #table-colgroup="{ fields }">
<col v-for="field in fields" :key="field.key" :class="field.columnClass" />
</template>
<template #cell(status)="{ item }">
<pipelines-status-badge :pipeline="item" :view-type="viewType" />
</template>
<template #cell(pipeline)="{ item }">
<pipeline-url
class="gl-text-truncate"
:pipeline="item"
:pipeline-schedule-url="pipelineScheduleUrl"
/>
</template>
<template #cell(triggerer)="{ item }">
<pipeline-triggerer :pipeline="item" />
</template>
<template #cell(commit)="{ item }">
<pipelines-commit :pipeline="item" :view-type="viewType" />
</template>
<template #cell(stages)="{ item }">
<div class="stage-cell">
<div></div>
<template v-if="item.details.stages.length > 0">
<div
v-for="(stage, index) in item.details.stages"
:key="index"
class="stage-container dropdown"
data-testid="widget-mini-pipeline-graph"
>
<pipeline-stage
:type="$options.pipelinesTable"
:stage="stage"
:update-dropdown="updateGraphDropdown"
/>
</div>
</template>
</div>
</template>
<template #cell(timeago)="{ item }">
<pipelines-timeago :pipeline="item" />
</template>
<template #cell(actions)="{ item }">
<pipeline-operations :pipeline="item" :canceling-pipeline="cancelingPipeline" />
</template>
</gl-table>
<pipeline-stop-modal :pipeline="pipeline" @submit="onSubmit" /> <pipeline-stop-modal :pipeline="pipeline" @submit="onSubmit" />
</div> </div>
......
...@@ -33,3 +33,5 @@ export const LOAD_FAILURE = 'load_failure'; ...@@ -33,3 +33,5 @@ export const LOAD_FAILURE = 'load_failure';
export const PARSE_FAILURE = 'parse_failure'; export const PARSE_FAILURE = 'parse_failure';
export const POST_FAILURE = 'post_failure'; export const POST_FAILURE = 'post_failure';
export const UNSUPPORTED_DATA = 'unsupported_data'; export const UNSUPPORTED_DATA = 'unsupported_data';
export const CHILD_VIEW = 'child';
...@@ -42,6 +42,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -42,6 +42,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:codequality_backend_comparison, @project, default_enabled: :yaml) push_frontend_feature_flag(:codequality_backend_comparison, @project, default_enabled: :yaml)
push_frontend_feature_flag(:local_file_reviews, default_enabled: :yaml) push_frontend_feature_flag(:local_file_reviews, default_enabled: :yaml)
push_frontend_feature_flag(:paginated_notes, @project, default_enabled: :yaml) push_frontend_feature_flag(:paginated_notes, @project, default_enabled: :yaml)
push_frontend_feature_flag(:new_pipelines_table, @project, default_enabled: :yaml)
record_experiment_user(:invite_members_version_a) record_experiment_user(:invite_members_version_a)
record_experiment_user(:invite_members_version_b) record_experiment_user(:invite_members_version_b)
......
...@@ -35,8 +35,8 @@ describe('Pipelines Triggerer', () => { ...@@ -35,8 +35,8 @@ describe('Pipelines Triggerer', () => {
wrapper.destroy(); wrapper.destroy();
}); });
it('should render a table cell', () => { it('should render pipeline triggerer table cell', () => {
expect(wrapper.find('.table-section').exists()).toBe(true); expect(wrapper.find('[data-testid="pipeline-triggerer"]').exists()).toBe(true);
}); });
it('should pass triggerer information when triggerer is provided', () => { it('should pass triggerer information when triggerer is provided', () => {
......
...@@ -7,6 +7,7 @@ const projectPath = 'test/test'; ...@@ -7,6 +7,7 @@ const projectPath = 'test/test';
describe('Pipeline Url Component', () => { describe('Pipeline Url Component', () => {
let wrapper; let wrapper;
const findTableCell = () => wrapper.find('[data-testid="pipeline-url-table-cell"]');
const findPipelineUrlLink = () => wrapper.find('[data-testid="pipeline-url-link"]'); const findPipelineUrlLink = () => wrapper.find('[data-testid="pipeline-url-link"]');
const findScheduledTag = () => wrapper.find('[data-testid="pipeline-url-scheduled"]'); const findScheduledTag = () => wrapper.find('[data-testid="pipeline-url-scheduled"]');
const findLatestTag = () => wrapper.find('[data-testid="pipeline-url-latest"]'); const findLatestTag = () => wrapper.find('[data-testid="pipeline-url-latest"]');
...@@ -43,10 +44,10 @@ describe('Pipeline Url Component', () => { ...@@ -43,10 +44,10 @@ describe('Pipeline Url Component', () => {
wrapper = null; wrapper = null;
}); });
it('should render a table cell', () => { it('should render pipeline url table cell', () => {
createComponent(); createComponent();
expect(wrapper.attributes('class')).toContain('table-section'); expect(findTableCell().exists()).toBe(true);
}); });
it('should render a link the provided path and id', () => { it('should render a link the provided path and id', () => {
......
import { GlFilteredSearch, GlButton, GlLoadingIcon, GlPagination } from '@gitlab/ui'; import { GlButton, GlFilteredSearch, GlLoadingIcon, GlPagination } from '@gitlab/ui';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import { chunk } from 'lodash'; import { chunk } from 'lodash';
......
import { GlTable } from '@gitlab/ui'; import { GlTable } from '@gitlab/ui';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import PipelineOperations from '~/pipelines/components/pipelines_list/pipeline_operations.vue';
import PipelineTriggerer from '~/pipelines/components/pipelines_list/pipeline_triggerer.vue';
import PipelineUrl from '~/pipelines/components/pipelines_list/pipeline_url.vue';
import PipelinesStatusBadge from '~/pipelines/components/pipelines_list/pipelines_status_badge.vue';
import PipelinesTable from '~/pipelines/components/pipelines_list/pipelines_table.vue'; import PipelinesTable from '~/pipelines/components/pipelines_list/pipelines_table.vue';
import PipelinesTimeago from '~/pipelines/components/pipelines_list/time_ago.vue';
import CommitComponent from '~/vue_shared/components/commit.vue';
describe('Pipelines Table', () => { describe('Pipelines Table', () => {
let pipeline; let pipeline;
...@@ -29,7 +35,22 @@ describe('Pipelines Table', () => { ...@@ -29,7 +35,22 @@ describe('Pipelines Table', () => {
const findRows = () => wrapper.findAll('.commit.gl-responsive-table-row'); const findRows = () => wrapper.findAll('.commit.gl-responsive-table-row');
const findGlTable = () => wrapper.findComponent(GlTable); const findGlTable = () => wrapper.findComponent(GlTable);
const findLegacyTable = () => wrapper.findByTestId('ci-table'); const findStatusBadge = () => wrapper.findComponent(PipelinesStatusBadge);
const findPipelineInfo = () => wrapper.findComponent(PipelineUrl);
const findTriggerer = () => wrapper.findComponent(PipelineTriggerer);
const findCommit = () => wrapper.findComponent(CommitComponent);
const findTimeAgo = () => wrapper.findComponent(PipelinesTimeago);
const findActions = () => wrapper.findComponent(PipelineOperations);
const findLegacyTable = () => wrapper.findByTestId('legacy-ci-table');
const findTableRows = () => wrapper.findAll('[data-testid="pipeline-table-row"]');
const findStatusTh = () => wrapper.findByTestId('status-th');
const findPipelineTh = () => wrapper.findByTestId('pipeline-th');
const findTriggererTh = () => wrapper.findByTestId('triggerer-th');
const findCommitTh = () => wrapper.findByTestId('commit-th');
const findStagesTh = () => wrapper.findByTestId('stages-th');
const findTimeAgoTh = () => wrapper.findByTestId('timeago-th');
const findActionsTh = () => wrapper.findByTestId('actions-th');
preloadFixtures(jsonFixtureName); preloadFixtures(jsonFixtureName);
...@@ -82,11 +103,82 @@ describe('Pipelines Table', () => { ...@@ -82,11 +103,82 @@ describe('Pipelines Table', () => {
}); });
describe('table with feature flag on', () => { describe('table with feature flag on', () => {
it('displays new table', () => { beforeEach(() => {
createComponent(defaultProps, true); createComponent({ pipelines: [pipeline], viewType: 'root' }, true);
});
it('displays new table', () => {
expect(findGlTable().exists()).toBe(true); expect(findGlTable().exists()).toBe(true);
expect(findLegacyTable().exists()).toBe(false); expect(findLegacyTable().exists()).toBe(false);
}); });
it('should render table head with correct columns', () => {
expect(findStatusTh().text()).toBe('Status');
expect(findPipelineTh().text()).toBe('Pipeline');
expect(findTriggererTh().text()).toBe('Triggerer');
expect(findCommitTh().text()).toBe('Commit');
expect(findStagesTh().text()).toBe('Stages');
expect(findTimeAgoTh().text()).toBe('Duration');
// last column should have no text in th
expect(findActionsTh().text()).toBe('');
});
it('should display a table row', () => {
expect(findTableRows()).toHaveLength(1);
});
describe('status cell', () => {
it('should render a status badge', () => {
expect(findStatusBadge().exists()).toBe(true);
});
it('should render status badge with correct path', () => {
expect(findStatusBadge().attributes('href')).toBe(pipeline.path);
});
});
describe('pipeline cell', () => {
it('should render pipeline information', () => {
expect(findPipelineInfo().exists()).toBe(true);
});
it('should display the pipeline id', () => {
expect(findPipelineInfo().text()).toContain(`#${pipeline.id}`);
});
});
describe('triggerer cell', () => {
it('should render the pipeline triggerer', () => {
expect(findTriggerer().exists()).toBe(true);
});
});
describe('commit cell', () => {
it('should render commit information', () => {
expect(findCommit().exists()).toBe(true);
});
it('should display and link to commit', () => {
expect(findCommit().text()).toContain(pipeline.commit.short_id);
expect(findCommit().props('commitUrl')).toBe(pipeline.commit.commit_path);
});
it('should display the commit author', () => {
expect(findCommit().props('author')).toEqual(pipeline.commit.author);
});
});
describe('duration cell', () => {
it('should render duration information', () => {
expect(findTimeAgo().exists()).toBe(true);
});
});
describe('operations cell', () => {
it('should render pipeline operations', () => {
expect(findActions().exists()).toBe(true);
});
});
}); });
}); });
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment