Commit 0b5f99f7 authored by pburdette's avatar pburdette

Add empty state component

Create empty state component
and use empty text for the table.
parent 19f4e5c2
......@@ -16,13 +16,21 @@ export default (containerId = 'js-jobs-table') => {
return false;
}
const { fullPath, jobCounts, jobStatuses } = containerEl.dataset;
const {
fullPath,
jobCounts,
jobStatuses,
pipelineEditorPath,
emptyStateSvgPath,
} = containerEl.dataset;
return new Vue({
el: containerEl,
apolloProvider,
provide: {
emptyStateSvgPath,
fullPath,
pipelineEditorPath,
jobStatuses: JSON.parse(jobStatuses),
jobCounts: JSON.parse(jobCounts),
},
......
<script>
import { GlTable } from '@gitlab/ui';
import { __ } from '~/locale';
import { s__, __ } from '~/locale';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
import ActionsCell from './cells/actions_cell.vue';
import DurationCell from './cells/duration_cell.vue';
......@@ -13,6 +13,9 @@ const defaultTableClasses = {
};
export default {
i18n: {
emptyText: s__('Jobs|No jobs to show'),
},
fields: [
{
key: 'status',
......@@ -90,6 +93,8 @@ export default {
:items="jobs"
:fields="$options.fields"
:tbody-tr-attr="{ 'data-testid': 'jobs-table-row' }"
:empty-text="$options.i18n.emptyText"
show-empty
stacked="lg"
fixed
>
......
......@@ -3,6 +3,7 @@ import { GlAlert, GlSkeletonLoader } from '@gitlab/ui';
import { __ } from '~/locale';
import GetJobs from './graphql/queries/get_jobs.query.graphql';
import JobsTable from './jobs_table.vue';
import JobsTableEmptyState from './jobs_table_empty_state.vue';
import JobsTableTabs from './jobs_table_tabs.vue';
export default {
......@@ -13,6 +14,7 @@ export default {
GlAlert,
GlSkeletonLoader,
JobsTable,
JobsTableEmptyState,
JobsTableTabs,
},
inject: {
......@@ -41,15 +43,21 @@ export default {
jobs: null,
hasError: false,
isAlertDismissed: false,
scope: null,
};
},
computed: {
shouldShowAlert() {
return this.hasError && !this.isAlertDismissed;
},
showEmptyState() {
return this.jobs.length === 0 && !this.scope;
},
},
methods: {
fetchJobsByStatus(scope) {
this.scope = scope;
this.$apollo.queries.jobs.refetch({ statuses: scope });
},
},
......@@ -80,6 +88,8 @@ export default {
/>
</div>
<jobs-table-empty-state v-else-if="showEmptyState" />
<jobs-table v-else :jobs="jobs" />
</div>
</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 @@
- add_page_specific_style 'page_bundles/ci_status'
- 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
.top-area
- build_path_proc = ->(scope) { project_jobs_path(@project, scope: scope) }
......
import { GlSkeletonLoader, GlAlert } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlSkeletonLoader, GlAlert, GlEmptyState } from '@gitlab/ui';
import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
......@@ -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 JobsTableApp from '~/jobs/components/table/jobs_table_app.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 localVue = createLocalVue();
......@@ -18,11 +18,13 @@ describe('Job table app', () => {
const successHandler = jest.fn().mockResolvedValue(mockJobsQueryResponse);
const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
const emptyHandler = jest.fn().mockResolvedValue(mockJobsQueryEmptyResponse);
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findTable = () => wrapper.findComponent(JobsTable);
const findTabs = () => wrapper.findComponent(JobsTableTabs);
const findAlert = () => wrapper.findComponent(GlAlert);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const createMockApolloProvider = (handler) => {
const requestHandlers = [[getJobsQuery, handler]];
......@@ -30,8 +32,8 @@ describe('Job table app', () => {
return createMockApollo(requestHandlers);
};
const createComponent = (handler = successHandler) => {
wrapper = shallowMount(JobsTableApp, {
const createComponent = (handler = successHandler, mountFn = shallowMount) => {
wrapper = mountFn(JobsTableApp, {
provide: {
projectPath,
},
......@@ -85,4 +87,24 @@ describe('Job table app', () => {
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 = {
},
},
};
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