Commit 30806cda authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch 'add-graphql-etag-caching-frontend' into 'master'

Add graphql etag caching frontend

See merge request gitlab-org/gitlab!54321
parents b727d9b0 35fa1eb3
...@@ -2,6 +2,7 @@ import { InMemoryCache } from 'apollo-cache-inmemory'; ...@@ -2,6 +2,7 @@ import { InMemoryCache } from 'apollo-cache-inmemory';
import { ApolloClient } from 'apollo-client'; import { ApolloClient } from 'apollo-client';
import { ApolloLink } from 'apollo-link'; import { ApolloLink } from 'apollo-link';
import { BatchHttpLink } from 'apollo-link-batch-http'; import { BatchHttpLink } from 'apollo-link-batch-http';
import { createHttpLink } from 'apollo-link-http';
import { createUploadLink } from 'apollo-upload-client'; import { createUploadLink } from 'apollo-upload-client';
import { StartupJSLink } from '~/lib/utils/apollo_startup_js_link'; import { StartupJSLink } from '~/lib/utils/apollo_startup_js_link';
import csrf from '~/lib/utils/csrf'; import csrf from '~/lib/utils/csrf';
...@@ -48,7 +49,7 @@ export default (resolvers = {}, config = {}) => { ...@@ -48,7 +49,7 @@ export default (resolvers = {}, config = {}) => {
const uploadsLink = ApolloLink.split( const uploadsLink = ApolloLink.split(
(operation) => operation.getContext().hasUpload || operation.getContext().isSingleRequest, (operation) => operation.getContext().hasUpload || operation.getContext().isSingleRequest,
createUploadLink(httpOptions), createUploadLink(httpOptions),
new BatchHttpLink(httpOptions), config.useGet ? createHttpLink(httpOptions) : new BatchHttpLink(httpOptions),
); );
const performanceBarLink = new ApolloLink((operation, forward) => { const performanceBarLink = new ApolloLink((operation, forward) => {
......
...@@ -4,7 +4,7 @@ import LinksLayer from '../graph_shared/links_layer.vue'; ...@@ -4,7 +4,7 @@ import LinksLayer from '../graph_shared/links_layer.vue';
import { DOWNSTREAM, MAIN, UPSTREAM, ONE_COL_WIDTH } from './constants'; import { DOWNSTREAM, MAIN, UPSTREAM, ONE_COL_WIDTH } from './constants';
import LinkedPipelinesColumn from './linked_pipelines_column.vue'; import LinkedPipelinesColumn from './linked_pipelines_column.vue';
import StageColumnComponent from './stage_column_component.vue'; import StageColumnComponent from './stage_column_component.vue';
import { reportToSentry } from './utils'; import { reportToSentry, validateConfigPaths } from './utils';
export default { export default {
name: 'PipelineGraph', name: 'PipelineGraph',
...@@ -15,6 +15,11 @@ export default { ...@@ -15,6 +15,11 @@ export default {
StageColumnComponent, StageColumnComponent,
}, },
props: { props: {
configPaths: {
type: Object,
required: true,
validator: validateConfigPaths,
},
pipeline: { pipeline: {
type: Object, type: Object,
required: true, required: true,
...@@ -24,11 +29,6 @@ export default { ...@@ -24,11 +29,6 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
metricsPath: {
type: String,
required: false,
default: '',
},
type: { type: {
type: String, type: String,
required: false, required: false,
...@@ -73,7 +73,7 @@ export default { ...@@ -73,7 +73,7 @@ export default {
}, },
metricsConfig() { metricsConfig() {
return { return {
path: this.metricsPath, path: this.configPaths.metricsPath,
collectMetrics: true, collectMetrics: true,
}; };
}, },
...@@ -142,6 +142,7 @@ export default { ...@@ -142,6 +142,7 @@ export default {
<template #upstream> <template #upstream>
<linked-pipelines-column <linked-pipelines-column
v-if="showUpstreamPipelines" v-if="showUpstreamPipelines"
:config-paths="configPaths"
:linked-pipelines="upstreamPipelines" :linked-pipelines="upstreamPipelines"
:column-title="__('Upstream')" :column-title="__('Upstream')"
:type="$options.pipelineTypeConstants.UPSTREAM" :type="$options.pipelineTypeConstants.UPSTREAM"
...@@ -182,6 +183,7 @@ export default { ...@@ -182,6 +183,7 @@ export default {
<linked-pipelines-column <linked-pipelines-column
v-if="showDownstreamPipelines" v-if="showDownstreamPipelines"
class="gl-mr-6" class="gl-mr-6"
:config-paths="configPaths"
:linked-pipelines="downstreamPipelines" :linked-pipelines="downstreamPipelines"
:column-title="__('Downstream')" :column-title="__('Downstream')"
:type="$options.pipelineTypeConstants.DOWNSTREAM" :type="$options.pipelineTypeConstants.DOWNSTREAM"
......
...@@ -4,7 +4,12 @@ import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.qu ...@@ -4,7 +4,12 @@ import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.qu
import { __ } from '~/locale'; import { __ } from '~/locale';
import { DEFAULT, DRAW_FAILURE, LOAD_FAILURE } from '../../constants'; import { DEFAULT, DRAW_FAILURE, LOAD_FAILURE } from '../../constants';
import PipelineGraph from './graph_component.vue'; import PipelineGraph from './graph_component.vue';
import { unwrapPipelineData, toggleQueryPollingByVisibility, reportToSentry } from './utils'; import {
getQueryHeaders,
reportToSentry,
toggleQueryPollingByVisibility,
unwrapPipelineData,
} from './utils';
export default { export default {
name: 'PipelineGraphWrapper', name: 'PipelineGraphWrapper',
...@@ -14,6 +19,9 @@ export default { ...@@ -14,6 +19,9 @@ export default {
PipelineGraph, PipelineGraph,
}, },
inject: { inject: {
graphqlResourceEtag: {
default: '',
},
metricsPath: { metricsPath: {
default: '', default: '',
}, },
...@@ -38,6 +46,9 @@ export default { ...@@ -38,6 +46,9 @@ export default {
}, },
apollo: { apollo: {
pipeline: { pipeline: {
context() {
return getQueryHeaders(this.graphqlResourceEtag);
},
query: getPipelineDetails, query: getPipelineDetails,
pollInterval: 10000, pollInterval: 10000,
variables() { variables() {
...@@ -74,6 +85,12 @@ export default { ...@@ -74,6 +85,12 @@ export default {
}; };
} }
}, },
configPaths() {
return {
graphqlResourceEtag: this.graphqlResourceEtag,
metricsPath: this.metricsPath,
};
},
showLoadingIcon() { showLoadingIcon() {
/* /*
Shows the icon only when the graph is empty, not when it is is Shows the icon only when the graph is empty, not when it is is
...@@ -111,7 +128,7 @@ export default { ...@@ -111,7 +128,7 @@ export default {
<gl-loading-icon v-if="showLoadingIcon" class="gl-mx-auto gl-my-4" size="lg" /> <gl-loading-icon v-if="showLoadingIcon" class="gl-mx-auto gl-my-4" size="lg" />
<pipeline-graph <pipeline-graph
v-if="pipeline" v-if="pipeline"
:metrics-path="metricsPath" :config-paths="configPaths"
:pipeline="pipeline" :pipeline="pipeline"
@error="reportFailure" @error="reportFailure"
@refreshPipelineGraph="refreshPipelineGraph" @refreshPipelineGraph="refreshPipelineGraph"
......
...@@ -3,7 +3,13 @@ import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.qu ...@@ -3,7 +3,13 @@ import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.qu
import { LOAD_FAILURE } from '../../constants'; import { LOAD_FAILURE } from '../../constants';
import { ONE_COL_WIDTH, UPSTREAM } from './constants'; import { ONE_COL_WIDTH, UPSTREAM } from './constants';
import LinkedPipeline from './linked_pipeline.vue'; import LinkedPipeline from './linked_pipeline.vue';
import { unwrapPipelineData, toggleQueryPollingByVisibility, reportToSentry } from './utils'; import {
getQueryHeaders,
reportToSentry,
toggleQueryPollingByVisibility,
unwrapPipelineData,
validateConfigPaths,
} from './utils';
export default { export default {
components: { components: {
...@@ -15,6 +21,11 @@ export default { ...@@ -15,6 +21,11 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
configPaths: {
type: Object,
required: true,
validator: validateConfigPaths,
},
linkedPipelines: { linkedPipelines: {
type: Array, type: Array,
required: true, required: true,
...@@ -72,6 +83,9 @@ export default { ...@@ -72,6 +83,9 @@ export default {
this.$apollo.addSmartQuery('currentPipeline', { this.$apollo.addSmartQuery('currentPipeline', {
query: getPipelineDetails, query: getPipelineDetails,
pollInterval: 10000, pollInterval: 10000,
context() {
return getQueryHeaders(this.configPaths.graphqlResourceEtag);
},
variables() { variables() {
return { return {
projectPath, projectPath,
...@@ -175,6 +189,7 @@ export default { ...@@ -175,6 +189,7 @@ export default {
v-if="isExpanded(pipeline.id)" v-if="isExpanded(pipeline.id)"
:type="type" :type="type"
class="d-inline-block gl-mt-n2" class="d-inline-block gl-mt-n2"
:config-paths="configPaths"
:pipeline="currentPipeline" :pipeline="currentPipeline"
:is-linked-pipeline="true" :is-linked-pipeline="true"
/> />
......
...@@ -10,6 +10,41 @@ const addMulti = (mainPipelineProjectPath, linkedPipeline) => { ...@@ -10,6 +10,41 @@ const addMulti = (mainPipelineProjectPath, linkedPipeline) => {
}; };
}; };
/* eslint-disable @gitlab/require-i18n-strings */
const getQueryHeaders = (etagResource) => {
return {
fetchOptions: {
method: 'GET',
},
headers: {
'X-GITLAB-GRAPHQL-FEATURE-CORRELATION': 'verify/ci/pipeline-graph',
'X-GITLAB-GRAPHQL-RESOURCE-ETAG': etagResource,
'X-REQUESTED_WITH': 'XMLHttpRequest',
},
};
};
/* eslint-enable @gitlab/require-i18n-strings */
const reportToSentry = (component, failureType) => {
Sentry.withScope((scope) => {
scope.setTag('component', component);
Sentry.captureException(failureType);
});
};
const toggleQueryPollingByVisibility = (queryRef, interval = 10000) => {
const stopStartQuery = (query) => {
if (!Visibility.hidden()) {
query.startPolling(interval);
} else {
query.stopPolling();
}
};
stopStartQuery(queryRef);
Visibility.change(stopStartQuery.bind(null, queryRef));
};
const transformId = (linkedPipeline) => { const transformId = (linkedPipeline) => {
return { ...linkedPipeline, id: getIdFromGraphQLId(linkedPipeline.id) }; return { ...linkedPipeline, id: getIdFromGraphQLId(linkedPipeline.id) };
}; };
...@@ -42,24 +77,12 @@ const unwrapPipelineData = (mainPipelineProjectPath, data) => { ...@@ -42,24 +77,12 @@ const unwrapPipelineData = (mainPipelineProjectPath, data) => {
}; };
}; };
const toggleQueryPollingByVisibility = (queryRef, interval = 10000) => { const validateConfigPaths = (value) => value.graphqlResourceEtag?.length > 0;
const stopStartQuery = (query) => {
if (!Visibility.hidden()) {
query.startPolling(interval);
} else {
query.stopPolling();
}
};
stopStartQuery(queryRef);
Visibility.change(stopStartQuery.bind(null, queryRef));
};
export { unwrapPipelineData, toggleQueryPollingByVisibility };
export const reportToSentry = (component, failureType) => { export {
Sentry.withScope((scope) => { getQueryHeaders,
scope.setTag('component', component); reportToSentry,
Sentry.captureException(failureType); toggleQueryPollingByVisibility,
}); unwrapPipelineData,
validateConfigPaths,
}; };
...@@ -93,13 +93,7 @@ export default async function initPipelineDetailsBundle() { ...@@ -93,13 +93,7 @@ export default async function initPipelineDetailsBundle() {
/* webpackChunkName: 'createPipelinesDetailApp' */ './pipeline_details_graph' /* webpackChunkName: 'createPipelinesDetailApp' */ './pipeline_details_graph'
); );
const { metricsPath, pipelineProjectPath, pipelineIid } = dataset; createPipelinesDetailApp(SELECTORS.PIPELINE_GRAPH, dataset);
createPipelinesDetailApp(
SELECTORS.PIPELINE_GRAPH,
pipelineProjectPath,
pipelineIid,
metricsPath,
);
} catch { } catch {
Flash(__('An error occurred while loading the pipeline.')); Flash(__('An error occurred while loading the pipeline.'));
} }
......
...@@ -11,12 +11,15 @@ const apolloProvider = new VueApollo({ ...@@ -11,12 +11,15 @@ const apolloProvider = new VueApollo({
defaultClient: createDefaultClient( defaultClient: createDefaultClient(
{}, {},
{ {
batchMax: 2, useGet: true,
}, },
), ),
}); });
const createPipelinesDetailApp = (selector, pipelineProjectPath, pipelineIid, metricsPath) => { const createPipelinesDetailApp = (
selector,
{ pipelineProjectPath, pipelineIid, metricsPath, graphqlResourceEtag } = {},
) => {
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Vue({ new Vue({
el: selector, el: selector,
...@@ -28,6 +31,7 @@ const createPipelinesDetailApp = (selector, pipelineProjectPath, pipelineIid, me ...@@ -28,6 +31,7 @@ const createPipelinesDetailApp = (selector, pipelineProjectPath, pipelineIid, me
metricsPath, metricsPath,
pipelineProjectPath, pipelineProjectPath,
pipelineIid, pipelineIid,
graphqlResourceEtag,
dataMethod: GRAPHQL, dataMethod: GRAPHQL,
}, },
errorCaptured(err, _vm, info) { errorCaptured(err, _vm, info) {
......
...@@ -347,6 +347,11 @@ module GitlabRoutingHelper ...@@ -347,6 +347,11 @@ module GitlabRoutingHelper
Gitlab::UrlBuilder.wiki_page_url(wiki, page, only_path: true, **options) Gitlab::UrlBuilder.wiki_page_url(wiki, page, only_path: true, **options)
end end
# GraphQL ETag routes
def graphql_etag_pipeline_path(pipeline)
[api_graphql_path, "pipelines/id/#{pipeline.id}"].join(':')
end
private private
def snippet_query_params(snippet, *args) def snippet_query_params(snippet, *args)
......
...@@ -2,6 +2,11 @@ ...@@ -2,6 +2,11 @@
module Ci module Ci
class ExpirePipelineCacheService class ExpirePipelineCacheService
class UrlHelpers
include ::Gitlab::Routing
include ::GitlabRoutingHelper
end
def execute(pipeline, delete: false) def execute(pipeline, delete: false)
store = Gitlab::EtagCaching::Store.new store = Gitlab::EtagCaching::Store.new
...@@ -17,27 +22,27 @@ module Ci ...@@ -17,27 +22,27 @@ module Ci
private private
def project_pipelines_path(project) def project_pipelines_path(project)
Gitlab::Routing.url_helpers.project_pipelines_path(project, format: :json) url_helpers.project_pipelines_path(project, format: :json)
end end
def project_pipeline_path(project, pipeline) def project_pipeline_path(project, pipeline)
Gitlab::Routing.url_helpers.project_pipeline_path(project, pipeline, format: :json) url_helpers.project_pipeline_path(project, pipeline, format: :json)
end end
def commit_pipelines_path(project, commit) def commit_pipelines_path(project, commit)
Gitlab::Routing.url_helpers.pipelines_project_commit_path(project, commit.id, format: :json) url_helpers.pipelines_project_commit_path(project, commit.id, format: :json)
end end
def new_merge_request_pipelines_path(project) def new_merge_request_pipelines_path(project)
Gitlab::Routing.url_helpers.project_new_merge_request_path(project, format: :json) url_helpers.project_new_merge_request_path(project, format: :json)
end end
def pipelines_project_merge_request_path(merge_request) def pipelines_project_merge_request_path(merge_request)
Gitlab::Routing.url_helpers.pipelines_project_merge_request_path(merge_request.target_project, merge_request, format: :json) url_helpers.pipelines_project_merge_request_path(merge_request.target_project, merge_request, format: :json)
end end
def merge_request_widget_path(merge_request) def merge_request_widget_path(merge_request)
Gitlab::Routing.url_helpers.cached_widget_project_json_merge_request_path(merge_request.project, merge_request, format: :json) url_helpers.cached_widget_project_json_merge_request_path(merge_request.project, merge_request, format: :json)
end end
def each_pipelines_merge_request_path(pipeline) def each_pipelines_merge_request_path(pipeline)
...@@ -48,7 +53,7 @@ module Ci ...@@ -48,7 +53,7 @@ module Ci
end end
def graphql_pipeline_path(pipeline) def graphql_pipeline_path(pipeline)
[Gitlab::Routing.url_helpers.api_graphql_path, "pipelines/id/#{pipeline.id}"].join(':') url_helpers.graphql_etag_pipeline_path(pipeline)
end end
# Updates ETag caches of a pipeline. # Updates ETag caches of a pipeline.
...@@ -73,5 +78,9 @@ module Ci ...@@ -73,5 +78,9 @@ module Ci
store.touch(graphql_pipeline_path(relative_pipeline)) store.touch(graphql_pipeline_path(relative_pipeline))
end end
end end
def url_helpers
@url_helpers ||= UrlHelpers.new
end
end end
end end
...@@ -26,4 +26,4 @@ ...@@ -26,4 +26,4 @@
= render "projects/pipelines/with_tabs", pipeline: @pipeline, pipeline_has_errors: pipeline_has_errors = render "projects/pipelines/with_tabs", pipeline: @pipeline, pipeline_has_errors: pipeline_has_errors
.js-pipeline-details-vue{ data: { endpoint: project_pipeline_path(@project, @pipeline, format: :json), metrics_path: namespace_project_ci_prometheus_metrics_histograms_path(namespace_id: @project.namespace, project_id: @project, format: :json), pipeline_project_path: @project.full_path, pipeline_iid: @pipeline.iid } } .js-pipeline-details-vue{ data: { endpoint: project_pipeline_path(@project, @pipeline, format: :json), metrics_path: namespace_project_ci_prometheus_metrics_histograms_path(namespace_id: @project.namespace, project_id: @project, format: :json), pipeline_project_path: @project.full_path, pipeline_iid: @pipeline.iid, graphql_resource_etag: graphql_etag_pipeline_path(@pipeline) } }
...@@ -20,6 +20,10 @@ describe('graph component', () => { ...@@ -20,6 +20,10 @@ describe('graph component', () => {
const defaultProps = { const defaultProps = {
pipeline: generateResponse(mockPipelineResponse, 'root/fungi-xoxo'), pipeline: generateResponse(mockPipelineResponse, 'root/fungi-xoxo'),
configPaths: {
metricsPath: '',
graphqlResourceEtag: 'this/is/a/path',
},
}; };
const defaultData = { const defaultData = {
......
...@@ -9,6 +9,8 @@ import PipelineGraphWrapper from '~/pipelines/components/graph/graph_component_w ...@@ -9,6 +9,8 @@ import PipelineGraphWrapper from '~/pipelines/components/graph/graph_component_w
import { mockPipelineResponse } from './mock_data'; import { mockPipelineResponse } from './mock_data';
const defaultProvide = { const defaultProvide = {
graphqlResourceEtag: 'frog/amphibirama/etag/',
metricsPath: '',
pipelineProjectPath: 'frog/amphibirama', pipelineProjectPath: 'frog/amphibirama',
pipelineIid: '22', pipelineIid: '22',
}; };
...@@ -87,6 +89,13 @@ describe('Pipeline graph wrapper', () => { ...@@ -87,6 +89,13 @@ describe('Pipeline graph wrapper', () => {
it('displays the graph', () => { it('displays the graph', () => {
expect(getGraph().exists()).toBe(true); expect(getGraph().exists()).toBe(true);
}); });
it('passes the etag resource and metrics path to the graph', () => {
expect(getGraph().props('configPaths')).toMatchObject({
graphqlResourceEtag: defaultProvide.graphqlResourceEtag,
metricsPath: defaultProvide.metricsPath,
});
});
}); });
describe('when there is an error', () => { describe('when there is an error', () => {
......
...@@ -20,6 +20,10 @@ describe('Linked Pipelines Column', () => { ...@@ -20,6 +20,10 @@ describe('Linked Pipelines Column', () => {
columnTitle: 'Downstream', columnTitle: 'Downstream',
linkedPipelines: processedPipeline.downstream, linkedPipelines: processedPipeline.downstream,
type: DOWNSTREAM, type: DOWNSTREAM,
configPaths: {
metricsPath: '',
graphqlResourceEtag: 'this/is/a/path',
},
}; };
let wrapper; let wrapper;
......
...@@ -332,4 +332,14 @@ RSpec.describe GitlabRoutingHelper do ...@@ -332,4 +332,14 @@ RSpec.describe GitlabRoutingHelper do
end end
end end
end end
context 'GraphQL ETag paths' do
context 'with pipelines' do
let(:pipeline) { double(id: 5) }
it 'returns an ETag path for pipelines' do
expect(graphql_etag_pipeline_path(pipeline)).to eq('/api/graphql:pipelines/id/5')
end
end
end
end end
...@@ -1976,6 +1976,15 @@ apollo-link-http-common@^0.2.14, apollo-link-http-common@^0.2.16: ...@@ -1976,6 +1976,15 @@ apollo-link-http-common@^0.2.14, apollo-link-http-common@^0.2.16:
ts-invariant "^0.4.0" ts-invariant "^0.4.0"
tslib "^1.9.3" tslib "^1.9.3"
apollo-link-http@^1.5.17:
version "1.5.17"
resolved "https://registry.yarnpkg.com/apollo-link-http/-/apollo-link-http-1.5.17.tgz#499e9f1711bf694497f02c51af12d82de5d8d8ba"
integrity sha512-uWcqAotbwDEU/9+Dm9e1/clO7hTB2kQ/94JYcGouBVLjoKmTeJTUPQKcJGpPwUjZcSqgYicbFqQSoJIW0yrFvg==
dependencies:
apollo-link "^1.2.14"
apollo-link-http-common "^0.2.16"
tslib "^1.9.3"
apollo-link@^1.0.0, apollo-link@^1.2.12, apollo-link@^1.2.14: apollo-link@^1.0.0, apollo-link@^1.2.12, apollo-link@^1.2.14:
version "1.2.14" version "1.2.14"
resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.14.tgz#3feda4b47f9ebba7f4160bef8b977ba725b684d9" resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.14.tgz#3feda4b47f9ebba7f4160bef8b977ba725b684d9"
......
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