Commit 9efb39d3 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch '328810-add-jobs-table-empty-state' into 'master'

Add jobs table empty state component

See merge request gitlab-org/gitlab!60750
parents b9600eea 0b5f99f7
...@@ -16,13 +16,21 @@ export default (containerId = 'js-jobs-table') => { ...@@ -16,13 +16,21 @@ export default (containerId = 'js-jobs-table') => {
return false; return false;
} }
const { fullPath, jobCounts, jobStatuses } = containerEl.dataset; const {
fullPath,
jobCounts,
jobStatuses,
pipelineEditorPath,
emptyStateSvgPath,
} = containerEl.dataset;
return new Vue({ return new Vue({
el: containerEl, el: containerEl,
apolloProvider, apolloProvider,
provide: { provide: {
emptyStateSvgPath,
fullPath, fullPath,
pipelineEditorPath,
jobStatuses: JSON.parse(jobStatuses), jobStatuses: JSON.parse(jobStatuses),
jobCounts: JSON.parse(jobCounts), jobCounts: JSON.parse(jobCounts),
}, },
......
<script> <script>
import { GlTable } from '@gitlab/ui'; import { GlTable } from '@gitlab/ui';
import { __ } from '~/locale'; import { s__, __ } from '~/locale';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue'; import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
import ActionsCell from './cells/actions_cell.vue'; import ActionsCell from './cells/actions_cell.vue';
import DurationCell from './cells/duration_cell.vue'; import DurationCell from './cells/duration_cell.vue';
...@@ -13,6 +13,9 @@ const defaultTableClasses = { ...@@ -13,6 +13,9 @@ const defaultTableClasses = {
}; };
export default { export default {
i18n: {
emptyText: s__('Jobs|No jobs to show'),
},
fields: [ fields: [
{ {
key: 'status', key: 'status',
...@@ -90,6 +93,8 @@ export default { ...@@ -90,6 +93,8 @@ export default {
:items="jobs" :items="jobs"
:fields="$options.fields" :fields="$options.fields"
:tbody-tr-attr="{ 'data-testid': 'jobs-table-row' }" :tbody-tr-attr="{ 'data-testid': 'jobs-table-row' }"
:empty-text="$options.i18n.emptyText"
show-empty
stacked="lg" stacked="lg"
fixed fixed
> >
......
...@@ -3,6 +3,7 @@ import { GlAlert, GlSkeletonLoader } from '@gitlab/ui'; ...@@ -3,6 +3,7 @@ import { GlAlert, GlSkeletonLoader } from '@gitlab/ui';
import { __ } from '~/locale'; import { __ } from '~/locale';
import GetJobs from './graphql/queries/get_jobs.query.graphql'; import GetJobs from './graphql/queries/get_jobs.query.graphql';
import JobsTable from './jobs_table.vue'; import JobsTable from './jobs_table.vue';
import JobsTableEmptyState from './jobs_table_empty_state.vue';
import JobsTableTabs from './jobs_table_tabs.vue'; import JobsTableTabs from './jobs_table_tabs.vue';
export default { export default {
...@@ -13,6 +14,7 @@ export default { ...@@ -13,6 +14,7 @@ export default {
GlAlert, GlAlert,
GlSkeletonLoader, GlSkeletonLoader,
JobsTable, JobsTable,
JobsTableEmptyState,
JobsTableTabs, JobsTableTabs,
}, },
inject: { inject: {
...@@ -41,15 +43,21 @@ export default { ...@@ -41,15 +43,21 @@ export default {
jobs: null, jobs: null,
hasError: false, hasError: false,
isAlertDismissed: false, isAlertDismissed: false,
scope: null,
}; };
}, },
computed: { computed: {
shouldShowAlert() { shouldShowAlert() {
return this.hasError && !this.isAlertDismissed; return this.hasError && !this.isAlertDismissed;
}, },
showEmptyState() {
return this.jobs.length === 0 && !this.scope;
},
}, },
methods: { methods: {
fetchJobsByStatus(scope) { fetchJobsByStatus(scope) {
this.scope = scope;
this.$apollo.queries.jobs.refetch({ statuses: scope }); this.$apollo.queries.jobs.refetch({ statuses: scope });
}, },
}, },
...@@ -80,6 +88,8 @@ export default { ...@@ -80,6 +88,8 @@ export default {
/> />
</div> </div>
<jobs-table-empty-state v-else-if="showEmptyState" />
<jobs-table v-else :jobs="jobs" /> <jobs-table v-else :jobs="jobs" />
</div> </div>
</template> </template>
<script>
import { GlEmptyState } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
i18n: {
title: s__('Jobs|Use jobs to automate your tasks'),
description: s__(
'Jobs|Jobs are the building blocks of a GitLab CI/CD pipeline. Each job has a specific task, like testing code. To set up jobs in a CI/CD pipeline, add a CI/CD configuration file to your project.',
),
buttonText: s__('Jobs|Create CI/CD configuration file'),
},
components: {
GlEmptyState,
},
inject: {
pipelineEditorPath: {
default: '',
},
emptyStateSvgPath: {
default: '',
},
},
};
</script>
<template>
<gl-empty-state
:title="$options.i18n.title"
:description="$options.i18n.description"
:svg-path="emptyStateSvgPath"
:primary-button-link="pipelineEditorPath"
:primary-button-text="$options.i18n.buttonText"
/>
</template>
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
- add_page_specific_style 'page_bundles/ci_status' - add_page_specific_style 'page_bundles/ci_status'
- if Feature.enabled?(:jobs_table_vue, @project, default_enabled: :yaml) - if Feature.enabled?(:jobs_table_vue, @project, default_enabled: :yaml)
#js-jobs-table{ data: { full_path: @project.full_path, job_counts: job_counts.to_json, job_statuses: job_statuses.to_json } } #js-jobs-table{ data: { full_path: @project.full_path, job_counts: job_counts.to_json, job_statuses: job_statuses.to_json, pipeline_editor_path: project_ci_pipeline_editor_path(@project), empty_state_svg_path: image_path('jobs-empty-state.svg') } }
- else - else
.top-area .top-area
- build_path_proc = ->(scope) { project_jobs_path(@project, scope: scope) } - build_path_proc = ->(scope) { project_jobs_path(@project, scope: scope) }
......
import { GlSkeletonLoader, GlAlert } from '@gitlab/ui'; import { GlSkeletonLoader, GlAlert, GlEmptyState } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils'; import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
...@@ -7,7 +7,7 @@ import getJobsQuery from '~/jobs/components/table/graphql/queries/get_jobs.query ...@@ -7,7 +7,7 @@ import getJobsQuery from '~/jobs/components/table/graphql/queries/get_jobs.query
import JobsTable from '~/jobs/components/table/jobs_table.vue'; import JobsTable from '~/jobs/components/table/jobs_table.vue';
import JobsTableApp from '~/jobs/components/table/jobs_table_app.vue'; import JobsTableApp from '~/jobs/components/table/jobs_table_app.vue';
import JobsTableTabs from '~/jobs/components/table/jobs_table_tabs.vue'; import JobsTableTabs from '~/jobs/components/table/jobs_table_tabs.vue';
import { mockJobsQueryResponse } from '../../mock_data'; import { mockJobsQueryResponse, mockJobsQueryEmptyResponse } from '../../mock_data';
const projectPath = 'gitlab-org/gitlab'; const projectPath = 'gitlab-org/gitlab';
const localVue = createLocalVue(); const localVue = createLocalVue();
...@@ -18,11 +18,13 @@ describe('Job table app', () => { ...@@ -18,11 +18,13 @@ describe('Job table app', () => {
const successHandler = jest.fn().mockResolvedValue(mockJobsQueryResponse); const successHandler = jest.fn().mockResolvedValue(mockJobsQueryResponse);
const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error')); const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
const emptyHandler = jest.fn().mockResolvedValue(mockJobsQueryEmptyResponse);
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findTable = () => wrapper.findComponent(JobsTable); const findTable = () => wrapper.findComponent(JobsTable);
const findTabs = () => wrapper.findComponent(JobsTableTabs); const findTabs = () => wrapper.findComponent(JobsTableTabs);
const findAlert = () => wrapper.findComponent(GlAlert); const findAlert = () => wrapper.findComponent(GlAlert);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const createMockApolloProvider = (handler) => { const createMockApolloProvider = (handler) => {
const requestHandlers = [[getJobsQuery, handler]]; const requestHandlers = [[getJobsQuery, handler]];
...@@ -30,8 +32,8 @@ describe('Job table app', () => { ...@@ -30,8 +32,8 @@ describe('Job table app', () => {
return createMockApollo(requestHandlers); return createMockApollo(requestHandlers);
}; };
const createComponent = (handler = successHandler) => { const createComponent = (handler = successHandler, mountFn = shallowMount) => {
wrapper = shallowMount(JobsTableApp, { wrapper = mountFn(JobsTableApp, {
provide: { provide: {
projectPath, projectPath,
}, },
...@@ -85,4 +87,24 @@ describe('Job table app', () => { ...@@ -85,4 +87,24 @@ describe('Job table app', () => {
expect(findAlert().exists()).toBe(true); expect(findAlert().exists()).toBe(true);
}); });
}); });
describe('empty state', () => {
it('should display empty state if there are no jobs and tab scope is null', async () => {
createComponent(emptyHandler, mount);
await waitForPromises();
expect(findEmptyState().exists()).toBe(true);
expect(findTable().exists()).toBe(false);
});
it('should not display empty state if there are jobs and tab scope is not null', async () => {
createComponent(successHandler, mount);
await waitForPromises();
expect(findEmptyState().exists()).toBe(false);
expect(findTable().exists()).toBe(true);
});
});
}); });
import { GlEmptyState } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import JobsTableEmptyState from '~/jobs/components/table/jobs_table_empty_state.vue';
describe('Jobs table empty state', () => {
let wrapper;
const pipelineEditorPath = '/root/project/-/ci/editor';
const emptyStateSvgPath = 'assets/jobs-empty-state.svg';
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const createComponent = () => {
wrapper = shallowMount(JobsTableEmptyState, {
provide: {
pipelineEditorPath,
emptyStateSvgPath,
},
});
};
beforeEach(() => {
createComponent();
});
it('displays empty state', () => {
expect(findEmptyState().exists()).toBe(true);
});
it('links to the pipeline editor', () => {
expect(findEmptyState().props('primaryButtonLink')).toBe(pipelineEditorPath);
});
it('shows an empty state image', () => {
expect(findEmptyState().props('svgPath')).toBe(emptyStateSvgPath);
});
});
...@@ -1501,3 +1501,11 @@ export const mockJobsQueryResponse = { ...@@ -1501,3 +1501,11 @@ export const mockJobsQueryResponse = {
}, },
}, },
}; };
export const mockJobsQueryEmptyResponse = {
data: {
project: {
jobs: [],
},
},
};
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