Commit c1d3a3ac authored by Scott Hampton's avatar Scott Hampton

Merge branch '263141-ci-cd-home-mvc-disabled-editor' into 'master'

Add readonly editor to display current CI config

See merge request gitlab-org/gitlab!46119
parents 33daaeb6 acd018f9
<script>
import EditorLite from '~/vue_shared/components/editor_lite.vue';
export default {
components: {
EditorLite,
},
props: {
value: {
type: String,
required: false,
default: '',
},
},
};
</script>
<template>
<div class="gl-border-solid gl-border-gray-100 gl-border-1">
<editor-lite v-model="value" file-name="*.yml" :editor-options="{ readOnly: true }" />
</div>
</template>
query getBlobContent($projectPath: ID!, $path: String, $ref: String!) {
blobContent(projectPath: $projectPath, path: $path, ref: $ref) @client {
rawData
}
}
import Api from '~/api';
export const resolvers = {
Query: {
blobContent(_, { projectPath, path, ref }) {
return {
__typename: 'BlobContent',
rawData: Api.getRawFile(projectPath, path, { ref }).then(({ data }) => {
return data;
}),
};
},
},
};
export default resolvers;
type BlobContent {
rawData: String!
}
extend type Query {
blobContent: BlobContent
}
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import typeDefs from './graphql/typedefs.graphql';
import { resolvers } from './graphql/resolvers';
import PipelineEditorApp from './pipeline_editor_app.vue'; import PipelineEditorApp from './pipeline_editor_app.vue';
export const initPipelineEditor = (selector = '#js-pipeline-editor') => { export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
const el = document.querySelector(selector); const el = document.querySelector(selector);
const { projectPath, defaultBranch, ciConfigPath } = el?.dataset;
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(resolvers, { typeDefs }),
});
return new Vue({ return new Vue({
el, el,
apolloProvider,
render(h) { render(h) {
return h(PipelineEditorApp); return h(PipelineEditorApp, {
props: {
projectPath,
defaultBranch,
ciConfigPath,
},
});
}, },
}); });
}; };
<script> <script>
import { GlEmptyState } from '@gitlab/ui'; import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
import { __, s__ } from '~/locale'; import { __, s__, sprintf } from '~/locale';
import TextEditor from './components/text_editor.vue';
import getBlobContent from './graphql/queries/blob_content.graphql';
export default { export default {
components: { components: {
GlEmptyState, GlLoadingIcon,
GlAlert,
TextEditor,
},
props: {
projectPath: {
type: String,
required: true,
},
defaultBranch: {
type: String,
required: false,
default: null,
},
ciConfigPath: {
type: String,
required: true,
},
},
data() {
return {
error: null,
content: '',
};
},
apollo: {
content: {
query: getBlobContent,
variables() {
return {
projectPath: this.projectPath,
path: this.ciConfigPath,
ref: this.defaultBranch,
};
},
update(data) {
return data?.blobContent?.rawData;
},
error(error) {
this.error = error;
},
},
},
computed: {
loading() {
return this.$apollo.queries.content.loading;
},
errorMessage() {
const { message: generalReason, networkError } = this.error ?? {};
const { data } = networkError?.response ?? {};
// 404 for missing file uses `message`
// 400 for a missing ref uses `error`
const networkReason = data?.message ?? data?.error;
const reason = networkReason ?? generalReason ?? this.$options.i18n.unknownError;
return sprintf(this.$options.i18n.errorMessageWithReason, { reason });
},
}, },
i18n: { i18n: {
title: s__('Pipelines|Pipeline Editor'), unknownError: __('Unknown Error'),
description: s__( errorMessageWithReason: s__('Pipelines|CI file could not be loaded: %{reason}'),
'Pipelines|We are beginning our work around building the foundation for our dedicated pipeline editor.',
),
primaryButtonText: __('Learn more'),
}, },
}; };
</script> </script>
<template> <template>
<gl-empty-state <div class="gl-mt-4">
:title="$options.i18n.title" <gl-alert v-if="error" :dismissible="false" variant="danger">{{ errorMessage }}</gl-alert>
:description="$options.i18n.description" <div class="gl-mt-4">
:primary-button-text="$options.i18n.primaryButtonText" <gl-loading-icon v-if="loading" size="lg" />
primary-button-link="https://about.gitlab.com/direction/verify/pipeline_authoring/" <text-editor v-else v-model="content" />
/> </div>
</div>
</template> </template>
- page_title s_('Pipelines|Pipeline Editor') - page_title s_('Pipelines|Pipeline Editor')
#js-pipeline-editor #js-pipeline-editor{ data: { "ci-config-path": @project.ci_config_path_or_default,
"project-path" => @project.full_path,
"default-branch" => @project.default_branch,
} }
...@@ -19528,6 +19528,9 @@ msgstr "" ...@@ -19528,6 +19528,9 @@ msgstr ""
msgid "Pipelines|CI Lint" msgid "Pipelines|CI Lint"
msgstr "" msgstr ""
msgid "Pipelines|CI file could not be loaded: %{reason}"
msgstr ""
msgid "Pipelines|Child pipeline" msgid "Pipelines|Child pipeline"
msgstr "" msgstr ""
...@@ -19618,9 +19621,6 @@ msgstr "" ...@@ -19618,9 +19621,6 @@ msgstr ""
msgid "Pipelines|Trigger user has insufficient permissions to project" msgid "Pipelines|Trigger user has insufficient permissions to project"
msgstr "" msgstr ""
msgid "Pipelines|We are beginning our work around building the foundation for our dedicated pipeline editor."
msgstr ""
msgid "Pipelines|invalid" msgid "Pipelines|invalid"
msgstr "" msgstr ""
......
import { shallowMount } from '@vue/test-utils';
import EditorLite from '~/vue_shared/components/editor_lite.vue';
import { mockCiYml } from '../mock_data';
import TextEditor from '~/pipeline_editor/components/text_editor.vue';
describe('~/pipeline_editor/components/text_editor.vue', () => {
let wrapper;
const createComponent = (props = {}, mountFn = shallowMount) => {
wrapper = mountFn(TextEditor, {
propsData: {
value: mockCiYml,
...props,
},
});
};
const findEditor = () => wrapper.find(EditorLite);
it('contains an editor', () => {
createComponent();
expect(findEditor().exists()).toBe(true);
});
it('editor contains the value provided', () => {
expect(findEditor().props('value')).toBe(mockCiYml);
});
it('editor is readony and configured for .yml', () => {
expect(findEditor().props('editorOptions')).toEqual({ readOnly: true });
expect(findEditor().props('fileName')).toBe('*.yml');
});
});
import Api from '~/api';
import { mockProjectPath, mockDefaultBranch, mockCiConfigPath, mockCiYml } from '../mock_data';
import { resolvers } from '~/pipeline_editor/graphql/resolvers';
jest.mock('~/api', () => {
return {
getRawFile: jest.fn(),
};
});
describe('~/pipeline_editor/graphql/resolvers', () => {
describe('Query', () => {
describe('blobContent', () => {
beforeEach(() => {
Api.getRawFile.mockResolvedValue({
data: mockCiYml,
});
});
afterEach(() => {
Api.getRawFile.mockReset();
});
it('resolves lint data with type names', async () => {
const result = resolvers.Query.blobContent(null, {
projectPath: mockProjectPath,
path: mockCiConfigPath,
ref: mockDefaultBranch,
});
expect(Api.getRawFile).toHaveBeenCalledWith(mockProjectPath, mockCiConfigPath, {
ref: mockDefaultBranch,
});
// eslint-disable-next-line no-underscore-dangle
expect(result.__typename).toBe('BlobContent');
await expect(result.rawData).resolves.toBe(mockCiYml);
});
});
});
});
export const mockProjectPath = 'user1/project1';
export const mockDefaultBranch = 'master';
export const mockCiConfigPath = '.gitlab-ci.yml';
export const mockCiYml = `
job1:
stage: test
script:
- echo 'test'
`;
import { nextTick } from 'vue';
import { mount, shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import { GlEmptyState } from '@gitlab/ui'; import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { mockProjectPath, mockDefaultBranch, mockCiConfigPath, mockCiYml } from './mock_data';
import TextEditor from '~/pipeline_editor/components/text_editor.vue';
import EditorLite from '~/vue_shared/components/editor_lite.vue';
import PipelineEditorApp from '~/pipeline_editor/pipeline_editor_app.vue'; import PipelineEditorApp from '~/pipeline_editor/pipeline_editor_app.vue';
describe('~/pipeline_editor/pipeline_editor_app.vue', () => { describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
let wrapper; let wrapper;
const createComponent = (mountFn = shallowMount) => { const createComponent = ({ props = {}, loading = false } = {}, mountFn = shallowMount) => {
wrapper = mountFn(PipelineEditorApp); wrapper = mountFn(PipelineEditorApp, {
propsData: {
projectPath: mockProjectPath,
defaultBranch: mockDefaultBranch,
ciConfigPath: mockCiConfigPath,
...props,
},
stubs: {
TextEditor,
},
mocks: {
$apollo: {
queries: {
content: {
loading,
},
},
},
},
});
}; };
const findEmptyState = () => wrapper.find(GlEmptyState); const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findAlert = () => wrapper.find(GlAlert);
const findEditor = () => wrapper.find(EditorLite);
it('contains an empty state', () => { it('displays content', async () => {
createComponent(); createComponent();
wrapper.setData({ content: mockCiYml });
await nextTick();
expect(findEmptyState().exists()).toBe(true); expect(findLoadingIcon().exists()).toBe(false);
expect(findEditor().props('value')).toBe(mockCiYml);
}); });
it('contains a text description', () => { it('displays a loading icon if the query is loading', async () => {
createComponent({ loading: true });
expect(findLoadingIcon().exists()).toBe(true);
});
describe('when in error state', () => {
class MockError extends Error {
constructor(message, data) {
super(message);
if (data) {
this.networkError = {
response: { data },
};
}
}
}
beforeEach(() => {
createComponent(mount); createComponent(mount);
});
it('shows a generic error', async () => {
wrapper.setData({ error: new MockError('An error message') });
await nextTick();
expect(findAlert().text()).toBe('CI file could not be loaded: An error message');
});
expect(findEmptyState().text()).toMatchInterpolatedText( it('shows a ref missing error state', async () => {
'Pipeline Editor We are beginning our work around building the foundation for our dedicated pipeline editor. Learn more', const error = new MockError('Ref missing!', {
error: 'ref is missing, ref is empty',
});
wrapper.setData({ error });
await nextTick();
expect(findAlert().text()).toMatch(
'CI file could not be loaded: ref is missing, ref is empty',
); );
}); });
it('shows a file missing error state', async () => {
const error = new MockError('File missing!', {
message: 'file not found',
});
wrapper.setData({ error });
await nextTick();
expect(findAlert().text()).toMatch('CI file could not be loaded: file not found');
});
});
}); });
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