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'; ...@@ -3,6 +3,7 @@ import { GlAlert, GlLoadingIcon, GlTabs } from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue'; import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { getParameterValues, setUrlParams, updateHistory } from '~/lib/utils/url_utility';
import { import {
CREATE_TAB, CREATE_TAB,
EDITOR_APP_STATUS_EMPTY, EDITOR_APP_STATUS_EMPTY,
...@@ -12,6 +13,8 @@ import { ...@@ -12,6 +13,8 @@ import {
EDITOR_APP_STATUS_VALID, EDITOR_APP_STATUS_VALID,
LINT_TAB, LINT_TAB,
MERGED_TAB, MERGED_TAB,
TAB_QUERY_PARAM,
TABS_INDEX,
VISUALIZE_TAB, VISUALIZE_TAB,
} from '../constants'; } from '../constants';
import getAppStatus from '../graphql/queries/client/app_status.graphql'; import getAppStatus from '../graphql/queries/client/app_status.graphql';
...@@ -42,6 +45,9 @@ export default { ...@@ -42,6 +45,9 @@ export default {
errorTexts: { errorTexts: {
loadMergedYaml: s__('Pipelines|Could not load merged YAML content'), loadMergedYaml: s__('Pipelines|Could not load merged YAML content'),
}, },
query: {
TAB_QUERY_PARAM,
},
tabConstants: { tabConstants: {
CREATE_TAB, CREATE_TAB,
LINT_TAB, LINT_TAB,
...@@ -98,15 +104,38 @@ export default { ...@@ -98,15 +104,38 @@ export default {
return this.appStatus === EDITOR_APP_STATUS_LOADING; return this.appStatus === EDITOR_APP_STATUS_LOADING;
}, },
}, },
created() {
const [tabQueryParam] = getParameterValues(TAB_QUERY_PARAM);
if (tabQueryParam && TABS_INDEX[tabQueryParam]) {
this.setDefaultTab(tabQueryParam);
}
},
methods: { methods: {
setCurrentTab(tabName) { setCurrentTab(tabName) {
this.$emit('set-current-tab', 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> </script>
<template> <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 <editor-tab
class="gl-mb-3" class="gl-mb-3"
:title="$options.i18n.tabEdit" :title="$options.i18n.tabEdit"
......
...@@ -22,7 +22,14 @@ export const LINT_TAB = 'LINT_TAB'; ...@@ -22,7 +22,14 @@ export const LINT_TAB = 'LINT_TAB';
export const MERGED_TAB = 'MERGED_TAB'; export const MERGED_TAB = 'MERGED_TAB';
export const VISUALIZE_TAB = 'VISUALIZE_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 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_CREATE = 'CREATE';
export const COMMIT_ACTION_UPDATE = 'UPDATE'; export const COMMIT_ACTION_UPDATE = 'UPDATE';
......
...@@ -4,7 +4,7 @@ import PipelineEditorDrawer from './components/drawer/pipeline_editor_drawer.vue ...@@ -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 PipelineEditorFileNav from './components/file_nav/pipeline_editor_file_nav.vue';
import PipelineEditorHeader from './components/header/pipeline_editor_header.vue'; import PipelineEditorHeader from './components/header/pipeline_editor_header.vue';
import PipelineEditorTabs from './components/pipeline_editor_tabs.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 { export default {
components: { components: {
......
...@@ -22,8 +22,8 @@ ...@@ -22,8 +22,8 @@
%ul %ul
- @pipeline.yaml_errors.split(",").each do |error| - @pipeline.yaml_errors.split(",").each do |error|
%li= error %li= error
- lint_link_url = project_ci_lint_path(@project) - lint_link_url = project_ci_pipeline_editor_path(@project, tab: "LINT_TAB")
- lint_link_start = '<a href="%{url}">'.html_safe % { url: lint_link_url } - 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 } = 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 = 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 { shallowMount, mount } from '@vue/test-utils';
import { nextTick } from 'vue'; 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 CiConfigMergedPreview from '~/pipeline_editor/components/editor/ci_config_merged_preview.vue';
import CiLint from '~/pipeline_editor/components/lint/ci_lint.vue'; import CiLint from '~/pipeline_editor/components/lint/ci_lint.vue';
import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue'; import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
import EditorTab from '~/pipeline_editor/components/ui/editor_tab.vue'; import EditorTab from '~/pipeline_editor/components/ui/editor_tab.vue';
import { import {
CREATE_TAB,
EDITOR_APP_STATUS_EMPTY, EDITOR_APP_STATUS_EMPTY,
EDITOR_APP_STATUS_ERROR, EDITOR_APP_STATUS_ERROR,
EDITOR_APP_STATUS_LOADING, EDITOR_APP_STATUS_LOADING,
EDITOR_APP_STATUS_INVALID, EDITOR_APP_STATUS_INVALID,
EDITOR_APP_STATUS_VALID, EDITOR_APP_STATUS_VALID,
MERGED_TAB,
TAB_QUERY_PARAM,
TABS_INDEX,
} from '~/pipeline_editor/constants'; } from '~/pipeline_editor/constants';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue'; import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
import { mockLintResponse, mockCiYml } from '../mock_data'; import { mockLintResponse, mockCiYml } from '../mock_data';
...@@ -53,6 +58,7 @@ describe('Pipeline editor tabs component', () => { ...@@ -53,6 +58,7 @@ describe('Pipeline editor tabs component', () => {
const findAlert = () => wrapper.findComponent(GlAlert); const findAlert = () => wrapper.findComponent(GlAlert);
const findCiLint = () => wrapper.findComponent(CiLint); const findCiLint = () => wrapper.findComponent(CiLint);
const findGlTabs = () => wrapper.findComponent(GlTabs);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findPipelineGraph = () => wrapper.findComponent(PipelineGraph); const findPipelineGraph = () => wrapper.findComponent(PipelineGraph);
const findTextEditor = () => wrapper.findComponent(MockTextEditor); const findTextEditor = () => wrapper.findComponent(MockTextEditor);
...@@ -181,4 +187,54 @@ describe('Pipeline editor tabs component', () => { ...@@ -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 { shallowMount, createLocalVue } 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 setWindowLocation from 'helpers/set_window_location_helper'; import setWindowLocation from 'helpers/set_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises'; 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 PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
import PipelineEditorEmptyState from '~/pipeline_editor/components/ui/pipeline_editor_empty_state.vue'; import PipelineEditorEmptyState from '~/pipeline_editor/components/ui/pipeline_editor_empty_state.vue';
...@@ -35,10 +33,6 @@ import { ...@@ -35,10 +33,6 @@ import {
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(VueApollo); localVue.use(VueApollo);
const MockSourceEditor = {
template: '<div/>',
};
const mockProvide = { const mockProvide = {
ciConfigPath: mockCiConfigPath, ciConfigPath: mockCiConfigPath,
defaultBranch: mockDefaultBranch, defaultBranch: mockDefaultBranch,
...@@ -55,19 +49,15 @@ describe('Pipeline editor app component', () => { ...@@ -55,19 +49,15 @@ describe('Pipeline editor app component', () => {
let mockLatestCommitShaQuery; let mockLatestCommitShaQuery;
let mockPipelineQuery; let mockPipelineQuery;
const createComponent = ({ blobLoading = false, options = {}, provide = {} } = {}) => { const createComponent = ({
blobLoading = false,
options = {},
provide = {},
stubs = {},
} = {}) => {
wrapper = shallowMount(PipelineEditorApp, { wrapper = shallowMount(PipelineEditorApp, {
provide: { ...mockProvide, ...provide }, provide: { ...mockProvide, ...provide },
stubs: { stubs,
GlTabs,
GlButton,
CommitForm,
PipelineEditorHome,
PipelineEditorTabs,
PipelineEditorMessages,
SourceEditor: MockSourceEditor,
PipelineEditorEmptyState,
},
data() { data() {
return { return {
commitSha: '', commitSha: '',
...@@ -89,7 +79,7 @@ describe('Pipeline editor app component', () => { ...@@ -89,7 +79,7 @@ describe('Pipeline editor app component', () => {
}); });
}; };
const createComponentWithApollo = async ({ props = {}, provide = {} } = {}) => { const createComponentWithApollo = async ({ props = {}, provide = {}, stubs = {} } = {}) => {
const handlers = [ const handlers = [
[getBlobContent, mockBlobContentData], [getBlobContent, mockBlobContentData],
[getCiConfigData, mockCiConfigData], [getCiConfigData, mockCiConfigData],
...@@ -111,7 +101,7 @@ describe('Pipeline editor app component', () => { ...@@ -111,7 +101,7 @@ describe('Pipeline editor app component', () => {
apolloProvider: mockApollo, apolloProvider: mockApollo,
}; };
createComponent({ props, provide, options }); createComponent({ props, provide, stubs, options });
return waitForPromises(); return waitForPromises();
}; };
...@@ -119,7 +109,6 @@ describe('Pipeline editor app component', () => { ...@@ -119,7 +109,6 @@ describe('Pipeline editor app component', () => {
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findAlert = () => wrapper.findComponent(GlAlert); const findAlert = () => wrapper.findComponent(GlAlert);
const findEditorHome = () => wrapper.findComponent(PipelineEditorHome); const findEditorHome = () => wrapper.findComponent(PipelineEditorHome);
const findTextEditor = () => wrapper.findComponent(TextEditor);
const findEmptyState = () => wrapper.findComponent(PipelineEditorEmptyState); const findEmptyState = () => wrapper.findComponent(PipelineEditorEmptyState);
const findEmptyStateButton = () => const findEmptyStateButton = () =>
wrapper.findComponent(PipelineEditorEmptyState).findComponent(GlButton); wrapper.findComponent(PipelineEditorEmptyState).findComponent(GlButton);
...@@ -141,7 +130,7 @@ describe('Pipeline editor app component', () => { ...@@ -141,7 +130,7 @@ describe('Pipeline editor app component', () => {
createComponent({ blobLoading: true }); createComponent({ blobLoading: true });
expect(findLoadingIcon().exists()).toBe(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', () => { ...@@ -185,7 +174,11 @@ describe('Pipeline editor app component', () => {
describe('when no CI config file exists', () => { describe('when no CI config file exists', () => {
beforeEach(async () => { beforeEach(async () => {
mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseNoCiFile); mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseNoCiFile);
await createComponentWithApollo(); await createComponentWithApollo({
stubs: {
PipelineEditorEmptyState,
},
});
jest jest
.spyOn(wrapper.vm.$apollo.queries.commitSha, 'startPolling') .spyOn(wrapper.vm.$apollo.queries.commitSha, 'startPolling')
...@@ -207,7 +200,11 @@ describe('Pipeline editor app component', () => { ...@@ -207,7 +200,11 @@ describe('Pipeline editor app component', () => {
const loadUnknownFailureText = 'The CI configuration was not loaded, please try again.'; const loadUnknownFailureText = 'The CI configuration was not loaded, please try again.';
mockBlobContentData.mockRejectedValueOnce(new Error('My error!')); mockBlobContentData.mockRejectedValueOnce(new Error('My error!'));
await createComponentWithApollo(); await createComponentWithApollo({
stubs: {
PipelineEditorMessages,
},
});
expect(findEmptyState().exists()).toBe(false); expect(findEmptyState().exists()).toBe(false);
...@@ -222,15 +219,20 @@ describe('Pipeline editor app component', () => { ...@@ -222,15 +219,20 @@ describe('Pipeline editor app component', () => {
mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseNoCiFile); mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseNoCiFile);
mockLatestCommitShaQuery.mockResolvedValue(mockEmptyCommitShaResults); mockLatestCommitShaQuery.mockResolvedValue(mockEmptyCommitShaResults);
await createComponentWithApollo(); await createComponentWithApollo({
stubs: {
PipelineEditorHome,
PipelineEditorEmptyState,
},
});
expect(findEmptyState().exists()).toBe(true); expect(findEmptyState().exists()).toBe(true);
expect(findTextEditor().exists()).toBe(false); expect(findEditorHome().exists()).toBe(false);
await findEmptyStateButton().vm.$emit('click'); await findEmptyStateButton().vm.$emit('click');
expect(findEmptyState().exists()).toBe(false); expect(findEmptyState().exists()).toBe(false);
expect(findTextEditor().exists()).toBe(true); expect(findEditorHome().exists()).toBe(true);
}); });
}); });
...@@ -241,7 +243,7 @@ describe('Pipeline editor app component', () => { ...@@ -241,7 +243,7 @@ describe('Pipeline editor app component', () => {
describe('and the commit mutation succeeds', () => { describe('and the commit mutation succeeds', () => {
beforeEach(async () => { beforeEach(async () => {
window.scrollTo = jest.fn(); window.scrollTo = jest.fn();
await createComponentWithApollo(); await createComponentWithApollo({ stubs: { PipelineEditorMessages } });
findEditorHome().vm.$emit('commit', { type: COMMIT_SUCCESS }); findEditorHome().vm.$emit('commit', { type: COMMIT_SUCCESS });
}); });
...@@ -295,7 +297,7 @@ describe('Pipeline editor app component', () => { ...@@ -295,7 +297,7 @@ describe('Pipeline editor app component', () => {
beforeEach(async () => { beforeEach(async () => {
window.scrollTo = jest.fn(); window.scrollTo = jest.fn();
await createComponentWithApollo(); await createComponentWithApollo({ stubs: { PipelineEditorMessages } });
findEditorHome().vm.$emit('showError', { findEditorHome().vm.$emit('showError', {
type: COMMIT_FAILURE, type: COMMIT_FAILURE,
...@@ -319,7 +321,7 @@ describe('Pipeline editor app component', () => { ...@@ -319,7 +321,7 @@ describe('Pipeline editor app component', () => {
beforeEach(async () => { beforeEach(async () => {
window.scrollTo = jest.fn(); window.scrollTo = jest.fn();
await createComponentWithApollo(); await createComponentWithApollo({ stubs: { PipelineEditorMessages } });
findEditorHome().vm.$emit('showError', { findEditorHome().vm.$emit('showError', {
type: COMMIT_FAILURE, type: COMMIT_FAILURE,
...@@ -386,7 +388,9 @@ describe('Pipeline editor app component', () => { ...@@ -386,7 +388,9 @@ describe('Pipeline editor app component', () => {
}); });
it('renders the given template', async () => { it('renders the given template', async () => {
await createComponentWithApollo(); await createComponentWithApollo({
stubs: { PipelineEditorHome, PipelineEditorTabs },
});
expect(mockGetTemplate).toHaveBeenCalledWith({ expect(mockGetTemplate).toHaveBeenCalledWith({
projectPath: mockProjectFullPath, projectPath: mockProjectFullPath,
...@@ -394,7 +398,7 @@ describe('Pipeline editor app component', () => { ...@@ -394,7 +398,7 @@ describe('Pipeline editor app component', () => {
}); });
expect(findEmptyState().exists()).toBe(false); expect(findEmptyState().exists()).toBe(false);
expect(findTextEditor().exists()).toBe(true); expect(findEditorHome().exists()).toBe(true);
}); });
}); });
}); });
...@@ -39,7 +39,6 @@ describe('Pipeline editor home wrapper', () => { ...@@ -39,7 +39,6 @@ describe('Pipeline editor home wrapper', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
describe('renders', () => { 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