Commit 46f810a3 authored by Frédéric Caplette's avatar Frédéric Caplette Committed by Savas Vedova

Style the CI Lint link in the pipeline page error as a link

parent f0dbb59b
......@@ -3,6 +3,7 @@ import { GlAlert, GlLoadingIcon, GlTabs } from '@gitlab/ui';
import { s__ } from '~/locale';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { getParameterValues, setUrlParams, updateHistory } from '~/lib/utils/url_utility';
import {
CREATE_TAB,
EDITOR_APP_STATUS_EMPTY,
......@@ -12,6 +13,8 @@ import {
EDITOR_APP_STATUS_VALID,
LINT_TAB,
MERGED_TAB,
TAB_QUERY_PARAM,
TABS_INDEX,
VISUALIZE_TAB,
} from '../constants';
import getAppStatus from '../graphql/queries/client/app_status.graphql';
......@@ -42,6 +45,9 @@ export default {
errorTexts: {
loadMergedYaml: s__('Pipelines|Could not load merged YAML content'),
},
query: {
TAB_QUERY_PARAM,
},
tabConstants: {
CREATE_TAB,
LINT_TAB,
......@@ -98,15 +104,38 @@ export default {
return this.appStatus === EDITOR_APP_STATUS_LOADING;
},
},
created() {
const [tabQueryParam] = getParameterValues(TAB_QUERY_PARAM);
if (tabQueryParam && TABS_INDEX[tabQueryParam]) {
this.setDefaultTab(tabQueryParam);
}
},
methods: {
setCurrentTab(tabName) {
this.$emit('set-current-tab', tabName);
},
setDefaultTab(tabName) {
// We associate tab name with the index so that we can use tab name
// in other part of the app and load the corresponding tab closer to the
// actual component using a hash that binds the name to the indexes.
// This also means that if we ever changed tab order, we would justs need to
// update `TABS_INDEX` hash instead of all the instances in the app
// where we used the individual indexes
const newUrl = setUrlParams({ [TAB_QUERY_PARAM]: TABS_INDEX[tabName] });
this.setCurrentTab(tabName);
updateHistory({ url: newUrl, title: document.title, replace: true });
},
},
};
</script>
<template>
<gl-tabs class="file-editor gl-mb-3">
<gl-tabs
class="file-editor gl-mb-3"
:query-param-name="$options.query.TAB_QUERY_PARAM"
sync-active-tab-with-query-params
>
<editor-tab
class="gl-mb-3"
:title="$options.i18n.tabEdit"
......
......@@ -22,7 +22,14 @@ export const LINT_TAB = 'LINT_TAB';
export const MERGED_TAB = 'MERGED_TAB';
export const VISUALIZE_TAB = 'VISUALIZE_TAB';
export const TABS_INDEX = {
[CREATE_TAB]: '0',
[VISUALIZE_TAB]: '1',
[LINT_TAB]: '2',
[MERGED_TAB]: '3',
};
export const TABS_WITH_COMMIT_FORM = [CREATE_TAB, LINT_TAB, VISUALIZE_TAB];
export const TAB_QUERY_PARAM = 'tab';
export const COMMIT_ACTION_CREATE = 'CREATE';
export const COMMIT_ACTION_UPDATE = 'UPDATE';
......
......@@ -4,7 +4,7 @@ import PipelineEditorDrawer from './components/drawer/pipeline_editor_drawer.vue
import PipelineEditorFileNav from './components/file_nav/pipeline_editor_file_nav.vue';
import PipelineEditorHeader from './components/header/pipeline_editor_header.vue';
import PipelineEditorTabs from './components/pipeline_editor_tabs.vue';
import { TABS_WITH_COMMIT_FORM, CREATE_TAB } from './constants';
import { CREATE_TAB, TABS_WITH_COMMIT_FORM } from './constants';
export default {
components: {
......
......@@ -22,8 +22,8 @@
%ul
- @pipeline.yaml_errors.split(",").each do |error|
%li= error
- lint_link_url = project_ci_lint_path(@project)
- lint_link_start = '<a href="%{url}">'.html_safe % { url: lint_link_url }
- lint_link_url = project_ci_pipeline_editor_path(@project, tab: "LINT_TAB")
- lint_link_start = '<a href="%{url}" class="gl-text-blue-500!">'.html_safe % { url: lint_link_url }
= s_('You can also test your %{gitlab_ci_yml} in %{lint_link_start}CI Lint%{lint_link_end}').html_safe % { gitlab_ci_yml: '.gitlab-ci.yml', lint_link_start: lint_link_start, lint_link_end: '</a>'.html_safe }
= render "projects/pipelines/with_tabs", pipeline: @pipeline, stages: @stages, pipeline_has_errors: pipeline_has_errors
......
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { GlAlert, GlLoadingIcon, GlTabs } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import setWindowLocation from 'helpers/set_window_location_helper';
import CiConfigMergedPreview from '~/pipeline_editor/components/editor/ci_config_merged_preview.vue';
import CiLint from '~/pipeline_editor/components/lint/ci_lint.vue';
import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
import EditorTab from '~/pipeline_editor/components/ui/editor_tab.vue';
import {
CREATE_TAB,
EDITOR_APP_STATUS_EMPTY,
EDITOR_APP_STATUS_ERROR,
EDITOR_APP_STATUS_LOADING,
EDITOR_APP_STATUS_INVALID,
EDITOR_APP_STATUS_VALID,
MERGED_TAB,
TAB_QUERY_PARAM,
TABS_INDEX,
} from '~/pipeline_editor/constants';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
import { mockLintResponse, mockCiYml } from '../mock_data';
......@@ -53,6 +58,7 @@ describe('Pipeline editor tabs component', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
const findCiLint = () => wrapper.findComponent(CiLint);
const findGlTabs = () => wrapper.findComponent(GlTabs);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findPipelineGraph = () => wrapper.findComponent(PipelineGraph);
const findTextEditor = () => wrapper.findComponent(MockTextEditor);
......@@ -181,4 +187,54 @@ describe('Pipeline editor tabs component', () => {
},
);
});
describe('default tab based on url query param', () => {
const gitlabUrl = 'https://gitlab.test/ci/editor/';
const matchObject = {
hostname: 'gitlab.test',
pathname: '/ci/editor/',
search: '',
};
it(`is ${CREATE_TAB} if the query param ${TAB_QUERY_PARAM} is not present`, () => {
setWindowLocation(gitlabUrl);
createComponent();
expect(window.location).toMatchObject(matchObject);
});
it(`is ${CREATE_TAB} tab if the query param ${TAB_QUERY_PARAM} is invalid`, () => {
const queryValue = 'FOO';
setWindowLocation(`${gitlabUrl}?${TAB_QUERY_PARAM}=${queryValue}`);
createComponent();
// If the query param remains unchanged, then we have ignored it.
expect(window.location).toMatchObject({
...matchObject,
search: `?${TAB_QUERY_PARAM}=${queryValue}`,
});
});
it('is the tab specified in query param and transform it into an index value', async () => {
setWindowLocation(`${gitlabUrl}?${TAB_QUERY_PARAM}=${MERGED_TAB}`);
createComponent();
// If the query param has changed to an index, it means we have synced the
// query with.
expect(window.location).toMatchObject({
...matchObject,
search: `?${TAB_QUERY_PARAM}=${TABS_INDEX[MERGED_TAB]}`,
});
});
});
describe('glTabs', () => {
beforeEach(() => {
createComponent();
});
it('passes the `sync-active-tab-with-query-params` prop', () => {
expect(findGlTabs().props('syncActiveTabWithQueryParams')).toBe(true);
});
});
});
import { GlAlert, GlButton, GlLoadingIcon, GlTabs } from '@gitlab/ui';
import { GlAlert, GlButton, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
import TextEditor from '~/pipeline_editor/components/editor/text_editor.vue';
import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
import PipelineEditorEmptyState from '~/pipeline_editor/components/ui/pipeline_editor_empty_state.vue';
......@@ -35,10 +33,6 @@ import {
const localVue = createLocalVue();
localVue.use(VueApollo);
const MockSourceEditor = {
template: '<div/>',
};
const mockProvide = {
ciConfigPath: mockCiConfigPath,
defaultBranch: mockDefaultBranch,
......@@ -55,19 +49,15 @@ describe('Pipeline editor app component', () => {
let mockLatestCommitShaQuery;
let mockPipelineQuery;
const createComponent = ({ blobLoading = false, options = {}, provide = {} } = {}) => {
const createComponent = ({
blobLoading = false,
options = {},
provide = {},
stubs = {},
} = {}) => {
wrapper = shallowMount(PipelineEditorApp, {
provide: { ...mockProvide, ...provide },
stubs: {
GlTabs,
GlButton,
CommitForm,
PipelineEditorHome,
PipelineEditorTabs,
PipelineEditorMessages,
SourceEditor: MockSourceEditor,
PipelineEditorEmptyState,
},
stubs,
data() {
return {
commitSha: '',
......@@ -89,7 +79,7 @@ describe('Pipeline editor app component', () => {
});
};
const createComponentWithApollo = async ({ props = {}, provide = {} } = {}) => {
const createComponentWithApollo = async ({ props = {}, provide = {}, stubs = {} } = {}) => {
const handlers = [
[getBlobContent, mockBlobContentData],
[getCiConfigData, mockCiConfigData],
......@@ -111,7 +101,7 @@ describe('Pipeline editor app component', () => {
apolloProvider: mockApollo,
};
createComponent({ props, provide, options });
createComponent({ props, provide, stubs, options });
return waitForPromises();
};
......@@ -119,7 +109,6 @@ describe('Pipeline editor app component', () => {
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findAlert = () => wrapper.findComponent(GlAlert);
const findEditorHome = () => wrapper.findComponent(PipelineEditorHome);
const findTextEditor = () => wrapper.findComponent(TextEditor);
const findEmptyState = () => wrapper.findComponent(PipelineEditorEmptyState);
const findEmptyStateButton = () =>
wrapper.findComponent(PipelineEditorEmptyState).findComponent(GlButton);
......@@ -141,7 +130,7 @@ describe('Pipeline editor app component', () => {
createComponent({ blobLoading: true });
expect(findLoadingIcon().exists()).toBe(true);
expect(findTextEditor().exists()).toBe(false);
expect(findEditorHome().exists()).toBe(false);
});
});
......@@ -185,7 +174,11 @@ describe('Pipeline editor app component', () => {
describe('when no CI config file exists', () => {
beforeEach(async () => {
mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseNoCiFile);
await createComponentWithApollo();
await createComponentWithApollo({
stubs: {
PipelineEditorEmptyState,
},
});
jest
.spyOn(wrapper.vm.$apollo.queries.commitSha, 'startPolling')
......@@ -207,7 +200,11 @@ describe('Pipeline editor app component', () => {
const loadUnknownFailureText = 'The CI configuration was not loaded, please try again.';
mockBlobContentData.mockRejectedValueOnce(new Error('My error!'));
await createComponentWithApollo();
await createComponentWithApollo({
stubs: {
PipelineEditorMessages,
},
});
expect(findEmptyState().exists()).toBe(false);
......@@ -222,15 +219,20 @@ describe('Pipeline editor app component', () => {
mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseNoCiFile);
mockLatestCommitShaQuery.mockResolvedValue(mockEmptyCommitShaResults);
await createComponentWithApollo();
await createComponentWithApollo({
stubs: {
PipelineEditorHome,
PipelineEditorEmptyState,
},
});
expect(findEmptyState().exists()).toBe(true);
expect(findTextEditor().exists()).toBe(false);
expect(findEditorHome().exists()).toBe(false);
await findEmptyStateButton().vm.$emit('click');
expect(findEmptyState().exists()).toBe(false);
expect(findTextEditor().exists()).toBe(true);
expect(findEditorHome().exists()).toBe(true);
});
});
......@@ -241,7 +243,7 @@ describe('Pipeline editor app component', () => {
describe('and the commit mutation succeeds', () => {
beforeEach(async () => {
window.scrollTo = jest.fn();
await createComponentWithApollo();
await createComponentWithApollo({ stubs: { PipelineEditorMessages } });
findEditorHome().vm.$emit('commit', { type: COMMIT_SUCCESS });
});
......@@ -295,7 +297,7 @@ describe('Pipeline editor app component', () => {
beforeEach(async () => {
window.scrollTo = jest.fn();
await createComponentWithApollo();
await createComponentWithApollo({ stubs: { PipelineEditorMessages } });
findEditorHome().vm.$emit('showError', {
type: COMMIT_FAILURE,
......@@ -319,7 +321,7 @@ describe('Pipeline editor app component', () => {
beforeEach(async () => {
window.scrollTo = jest.fn();
await createComponentWithApollo();
await createComponentWithApollo({ stubs: { PipelineEditorMessages } });
findEditorHome().vm.$emit('showError', {
type: COMMIT_FAILURE,
......@@ -386,7 +388,9 @@ describe('Pipeline editor app component', () => {
});
it('renders the given template', async () => {
await createComponentWithApollo();
await createComponentWithApollo({
stubs: { PipelineEditorHome, PipelineEditorTabs },
});
expect(mockGetTemplate).toHaveBeenCalledWith({
projectPath: mockProjectFullPath,
......@@ -394,7 +398,7 @@ describe('Pipeline editor app component', () => {
});
expect(findEmptyState().exists()).toBe(false);
expect(findTextEditor().exists()).toBe(true);
expect(findEditorHome().exists()).toBe(true);
});
});
});
......@@ -39,7 +39,6 @@ describe('Pipeline editor home wrapper', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('renders', () => {
......
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