Commit b9d37b74 authored by Sarah Groff Hennigh-Palermo's avatar Sarah Groff Hennigh-Palermo

Merge branch 'move-pipeline-editor-blob-content-to-graphql' into 'master'

Update Pipeline editor getBlobContent to use graphQL

See merge request gitlab-org/gitlab!64527
parents 083e149a 87b05b06
query getBlobContent($projectPath: ID!, $path: String, $ref: String!) { query getBlobContent($projectPath: ID!, $path: String!, $ref: String) {
blobContent(projectPath: $projectPath, path: $path, ref: $ref) @client { project(fullPath: $projectPath) {
rawData repository {
blobs(paths: [$path], ref: $ref) {
nodes {
rawBlob
}
}
}
} }
} }
import produce from 'immer'; import produce from 'immer';
import Api from '~/api';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import getCurrentBranchQuery from './queries/client/current_branch.graphql'; import getCurrentBranchQuery from './queries/client/current_branch.graphql';
import getLastCommitBranchQuery from './queries/client/last_commit_branch.query.graphql'; import getLastCommitBranchQuery from './queries/client/last_commit_branch.query.graphql';
export const resolvers = { export const resolvers = {
Query: {
blobContent(_, { projectPath, path, ref }) {
return {
__typename: 'BlobContent',
rawData: Api.getRawFile(projectPath, path, { ref }).then(({ data }) => {
return data;
}),
};
},
},
Mutation: { Mutation: {
lintCI: (_, { endpoint, content, dry_run }) => { lintCI: (_, { endpoint, content, dry_run }) => {
return axios.post(endpoint, { content, dry_run }).then(({ data }) => ({ return axios.post(endpoint, { content, dry_run }).then(({ data }) => ({
......
<script> <script>
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import { fetchPolicies } from '~/lib/graphql'; import { fetchPolicies } from '~/lib/graphql';
import httpStatusCodes from '~/lib/utils/http_status';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils'; import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils';
...@@ -76,22 +75,40 @@ export default { ...@@ -76,22 +75,40 @@ export default {
}; };
}, },
update(data) { update(data) {
return data?.blobContent?.rawData; return data?.project?.repository?.blobs?.nodes[0]?.rawBlob;
}, },
result({ data }) { result({ data }) {
const fileContent = data?.blobContent?.rawData ?? ''; const nodes = data?.project?.repository?.blobs?.nodes;
if (!nodes) {
this.reportFailure(LOAD_FAILURE_UNKNOWN);
} else {
const rawBlob = nodes[0]?.rawBlob;
const fileContent = rawBlob ?? '';
this.lastCommittedContent = fileContent; this.lastCommittedContent = fileContent;
this.currentCiFileContent = fileContent; this.currentCiFileContent = fileContent;
// make sure to reset the start screen flag during a refetch // If rawBlob is defined and returns a string, it means that there is
// e.g. when switching branches // a CI config file with empty content. If `rawBlob` is not defined
if (fileContent.length) { // at all, it means there was no file found.
this.showStartScreen = false; const hasCIFile = rawBlob === '' || fileContent.length > 0;
if (!fileContent.length) {
this.setAppStatus(EDITOR_APP_STATUS_EMPTY);
}
if (!hasCIFile) {
this.showStartScreen = true;
} else if (fileContent.length) {
// If the file content is > 0, then we make sure to reset the
// start screen flag during a refetch
// e.g. when switching branches
this.showStartScreen = false;
}
} }
}, },
error(error) { error() {
this.handleBlobContentError(error); this.reportFailure(LOAD_FAILURE_UNKNOWN);
}, },
watchLoading(isLoading) { watchLoading(isLoading) {
if (isLoading) { if (isLoading) {
...@@ -187,22 +204,6 @@ export default { ...@@ -187,22 +204,6 @@ export default {
}, },
}, },
methods: { methods: {
handleBlobContentError(error = {}) {
const { networkError } = error;
const { response } = networkError;
// 404 for missing CI file
// 400 for blank projects with no repository
if (
response?.status === httpStatusCodes.NOT_FOUND ||
response?.status === httpStatusCodes.BAD_REQUEST
) {
this.setAppStatus(EDITOR_APP_STATUS_EMPTY);
this.showStartScreen = true;
} else {
this.reportFailure(LOAD_FAILURE_UNKNOWN);
}
},
hideFailure() { hideFailure() {
this.showFailure = false; this.showFailure = false;
}, },
......
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import Api from '~/api';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status'; import httpStatus from '~/lib/utils/http_status';
import { resolvers } from '~/pipeline_editor/graphql/resolvers'; import { resolvers } from '~/pipeline_editor/graphql/resolvers';
import { import { mockLintResponse } from '../mock_data';
mockCiConfigPath,
mockCiYml,
mockDefaultBranch,
mockLintResponse,
mockProjectFullPath,
} from '../mock_data';
jest.mock('~/api', () => { jest.mock('~/api', () => {
return { return {
...@@ -18,36 +11,6 @@ jest.mock('~/api', () => { ...@@ -18,36 +11,6 @@ jest.mock('~/api', () => {
}); });
describe('~/pipeline_editor/graphql/resolvers', () => { 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: mockProjectFullPath,
path: mockCiConfigPath,
ref: mockDefaultBranch,
});
expect(Api.getRawFile).toHaveBeenCalledWith(mockProjectFullPath, mockCiConfigPath, {
ref: mockDefaultBranch,
});
// eslint-disable-next-line no-underscore-dangle
expect(result.__typename).toBe('BlobContent');
await expect(result.rawData).resolves.toBe(mockCiYml);
});
});
});
describe('Mutation', () => { describe('Mutation', () => {
describe('lintCI', () => { describe('lintCI', () => {
let mock; let mock;
......
...@@ -35,6 +35,23 @@ job_build: ...@@ -35,6 +35,23 @@ job_build:
- echo "build" - echo "build"
needs: ["job_test_2"] needs: ["job_test_2"]
`; `;
export const mockBlobContentQueryResponse = {
data: {
project: { repository: { blobs: { nodes: [{ rawBlob: mockCiYml }] } } },
},
};
export const mockBlobContentQueryResponseNoCiFile = {
data: {
project: { repository: { blobs: { nodes: [] } } },
},
};
export const mockBlobContentQueryResponseEmptyCiFile = {
data: {
project: { repository: { blobs: { nodes: [{ rawBlob: '' }] } } },
},
};
const mockJobFields = { const mockJobFields = {
beforeScript: [], beforeScript: [],
......
...@@ -3,7 +3,6 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; ...@@ -3,7 +3,6 @@ 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 waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import httpStatusCodes from '~/lib/utils/http_status';
import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue'; import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
import TextEditor from '~/pipeline_editor/components/editor/text_editor.vue'; import TextEditor from '~/pipeline_editor/components/editor/text_editor.vue';
...@@ -11,15 +10,19 @@ import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tab ...@@ -11,15 +10,19 @@ import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tab
import PipelineEditorEmptyState from '~/pipeline_editor/components/ui/pipeline_editor_empty_state.vue'; import PipelineEditorEmptyState from '~/pipeline_editor/components/ui/pipeline_editor_empty_state.vue';
import PipelineEditorMessages from '~/pipeline_editor/components/ui/pipeline_editor_messages.vue'; import PipelineEditorMessages from '~/pipeline_editor/components/ui/pipeline_editor_messages.vue';
import { COMMIT_SUCCESS, COMMIT_FAILURE } from '~/pipeline_editor/constants'; import { COMMIT_SUCCESS, COMMIT_FAILURE } from '~/pipeline_editor/constants';
import getBlobContent from '~/pipeline_editor/graphql/queries/blob_content.graphql';
import getCiConfigData from '~/pipeline_editor/graphql/queries/ci_config.graphql'; import getCiConfigData from '~/pipeline_editor/graphql/queries/ci_config.graphql';
import PipelineEditorApp from '~/pipeline_editor/pipeline_editor_app.vue'; import PipelineEditorApp from '~/pipeline_editor/pipeline_editor_app.vue';
import PipelineEditorHome from '~/pipeline_editor/pipeline_editor_home.vue'; import PipelineEditorHome from '~/pipeline_editor/pipeline_editor_home.vue';
import { import {
mockCiConfigPath, mockCiConfigPath,
mockCiConfigQueryResponse, mockCiConfigQueryResponse,
mockCiYml, mockBlobContentQueryResponse,
mockBlobContentQueryResponseEmptyCiFile,
mockBlobContentQueryResponseNoCiFile,
mockDefaultBranch, mockDefaultBranch,
mockProjectFullPath, mockProjectFullPath,
mockCiYml,
} from './mock_data'; } from './mock_data';
const localVue = createLocalVue(); const localVue = createLocalVue();
...@@ -75,19 +78,12 @@ describe('Pipeline editor app component', () => { ...@@ -75,19 +78,12 @@ describe('Pipeline editor app component', () => {
}; };
const createComponentWithApollo = async ({ props = {}, provide = {} } = {}) => { const createComponentWithApollo = async ({ props = {}, provide = {} } = {}) => {
const handlers = [[getCiConfigData, mockCiConfigData]]; const handlers = [
const resolvers = { [getBlobContent, mockBlobContentData],
Query: { [getCiConfigData, mockCiConfigData],
blobContent() { ];
return {
__typename: 'BlobContent',
rawData: mockBlobContentData(),
};
},
},
};
mockApollo = createMockApollo(handlers, resolvers); mockApollo = createMockApollo(handlers);
const options = { const options = {
localVue, localVue,
...@@ -133,7 +129,7 @@ describe('Pipeline editor app component', () => { ...@@ -133,7 +129,7 @@ describe('Pipeline editor app component', () => {
describe('when queries are called', () => { describe('when queries are called', () => {
beforeEach(() => { beforeEach(() => {
mockBlobContentData.mockResolvedValue(mockCiYml); mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
mockCiConfigData.mockResolvedValue(mockCiConfigQueryResponse); mockCiConfigData.mockResolvedValue(mockCiConfigQueryResponse);
}); });
...@@ -159,34 +155,13 @@ describe('Pipeline editor app component', () => { ...@@ -159,34 +155,13 @@ describe('Pipeline editor app component', () => {
}); });
describe('when no CI config file exists', () => { describe('when no CI config file exists', () => {
describe('in a project without a repository', () => { it('shows an empty state and does not show editor home component', async () => {
it('shows an empty state and does not show editor home component', async () => { mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseNoCiFile);
mockBlobContentData.mockRejectedValueOnce({ await createComponentWithApollo();
response: {
status: httpStatusCodes.BAD_REQUEST,
},
});
await createComponentWithApollo();
expect(findEmptyState().exists()).toBe(true);
expect(findAlert().exists()).toBe(false);
expect(findEditorHome().exists()).toBe(false);
});
});
describe('in a project with a repository', () => {
it('shows an empty state and does not show editor home component', async () => {
mockBlobContentData.mockRejectedValueOnce({
response: {
status: httpStatusCodes.NOT_FOUND,
},
});
await createComponentWithApollo();
expect(findEmptyState().exists()).toBe(true); expect(findEmptyState().exists()).toBe(true);
expect(findAlert().exists()).toBe(false); expect(findAlert().exists()).toBe(false);
expect(findEditorHome().exists()).toBe(false); expect(findEditorHome().exists()).toBe(false);
});
}); });
describe('because of a fetching error', () => { describe('because of a fetching error', () => {
...@@ -204,13 +179,28 @@ describe('Pipeline editor app component', () => { ...@@ -204,13 +179,28 @@ describe('Pipeline editor app component', () => {
}); });
}); });
describe('with an empty CI config file', () => {
describe('with empty state feature flag on', () => {
it('does not show the empty screen state', async () => {
mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseEmptyCiFile);
await createComponentWithApollo({
provide: {
glFeatures: {
pipelineEditorEmptyStateAction: true,
},
},
});
expect(findEmptyState().exists()).toBe(false);
expect(findTextEditor().exists()).toBe(true);
});
});
});
describe('when landing on the empty state with feature flag on', () => { describe('when landing on the empty state with feature flag on', () => {
it('user can click on CTA button and see an empty editor', async () => { it('user can click on CTA button and see an empty editor', async () => {
mockBlobContentData.mockRejectedValueOnce({ mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseNoCiFile);
response: {
status: httpStatusCodes.NOT_FOUND,
},
});
await createComponentWithApollo({ await createComponentWithApollo({
provide: { provide: {
...@@ -315,17 +305,13 @@ describe('Pipeline editor app component', () => { ...@@ -315,17 +305,13 @@ describe('Pipeline editor app component', () => {
}); });
it('hides start screen when refetch fetches CI file', async () => { it('hides start screen when refetch fetches CI file', async () => {
mockBlobContentData.mockRejectedValue({ mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseNoCiFile);
response: {
status: httpStatusCodes.NOT_FOUND,
},
});
await createComponentWithApollo(); await createComponentWithApollo();
expect(findEmptyState().exists()).toBe(true); expect(findEmptyState().exists()).toBe(true);
expect(findEditorHome().exists()).toBe(false); expect(findEditorHome().exists()).toBe(false);
mockBlobContentData.mockResolvedValue(mockCiYml); mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
await wrapper.vm.$apollo.queries.initialCiFileContent.refetch(); await wrapper.vm.$apollo.queries.initialCiFileContent.refetch();
expect(findEmptyState().exists()).toBe(false); expect(findEmptyState().exists()).toBe(false);
......
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