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';
import { ApolloClient } from 'apollo-client';
import { ApolloLink } from 'apollo-link';
import { BatchHttpLink } from 'apollo-link-batch-http';
import { createHttpLink } from 'apollo-link-http';
import { createUploadLink } from 'apollo-upload-client';
import { StartupJSLink } from '~/lib/utils/apollo_startup_js_link';
import csrf from '~/lib/utils/csrf';
......@@ -48,7 +49,7 @@ export default (resolvers = {}, config = {}) => {
const uploadsLink = ApolloLink.split(
(operation) => operation.getContext().hasUpload || operation.getContext().isSingleRequest,
createUploadLink(httpOptions),
new BatchHttpLink(httpOptions),
config.useGet ? createHttpLink(httpOptions) : new BatchHttpLink(httpOptions),
);
const performanceBarLink = new ApolloLink((operation, forward) => {
......
......@@ -4,7 +4,7 @@ import LinksLayer from '../graph_shared/links_layer.vue';
import { DOWNSTREAM, MAIN, UPSTREAM, ONE_COL_WIDTH } from './constants';
import LinkedPipelinesColumn from './linked_pipelines_column.vue';
import StageColumnComponent from './stage_column_component.vue';
import { reportToSentry } from './utils';
import { reportToSentry, validateConfigPaths } from './utils';
export default {
name: 'PipelineGraph',
......@@ -15,6 +15,11 @@ export default {
StageColumnComponent,
},
props: {
configPaths: {
type: Object,
required: true,
validator: validateConfigPaths,
},
pipeline: {
type: Object,
required: true,
......@@ -24,11 +29,6 @@ export default {
required: false,
default: false,
},
metricsPath: {
type: String,
required: false,
default: '',
},
type: {
type: String,
required: false,
......@@ -73,7 +73,7 @@ export default {
},
metricsConfig() {
return {
path: this.metricsPath,
path: this.configPaths.metricsPath,
collectMetrics: true,
};
},
......@@ -142,6 +142,7 @@ export default {
<template #upstream>
<linked-pipelines-column
v-if="showUpstreamPipelines"
:config-paths="configPaths"
:linked-pipelines="upstreamPipelines"
:column-title="__('Upstream')"
:type="$options.pipelineTypeConstants.UPSTREAM"
......@@ -182,6 +183,7 @@ export default {
<linked-pipelines-column
v-if="showDownstreamPipelines"
class="gl-mr-6"
:config-paths="configPaths"
:linked-pipelines="downstreamPipelines"
:column-title="__('Downstream')"
:type="$options.pipelineTypeConstants.DOWNSTREAM"
......
......@@ -4,7 +4,12 @@ import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.qu
import { __ } from '~/locale';
import { DEFAULT, DRAW_FAILURE, LOAD_FAILURE } from '../../constants';
import PipelineGraph from './graph_component.vue';
import { unwrapPipelineData, toggleQueryPollingByVisibility, reportToSentry } from './utils';
import {
getQueryHeaders,
reportToSentry,
toggleQueryPollingByVisibility,
unwrapPipelineData,
} from './utils';
export default {
name: 'PipelineGraphWrapper',
......@@ -14,6 +19,9 @@ export default {
PipelineGraph,
},
inject: {
graphqlResourceEtag: {
default: '',
},
metricsPath: {
default: '',
},
......@@ -38,6 +46,9 @@ export default {
},
apollo: {
pipeline: {
context() {
return getQueryHeaders(this.graphqlResourceEtag);
},
query: getPipelineDetails,
pollInterval: 10000,
variables() {
......@@ -74,6 +85,12 @@ export default {
};
}
},
configPaths() {
return {
graphqlResourceEtag: this.graphqlResourceEtag,
metricsPath: this.metricsPath,
};
},
showLoadingIcon() {
/*
Shows the icon only when the graph is empty, not when it is is
......@@ -111,7 +128,7 @@ export default {
<gl-loading-icon v-if="showLoadingIcon" class="gl-mx-auto gl-my-4" size="lg" />
<pipeline-graph
v-if="pipeline"
:metrics-path="metricsPath"
:config-paths="configPaths"
:pipeline="pipeline"
@error="reportFailure"
@refreshPipelineGraph="refreshPipelineGraph"
......
......@@ -3,7 +3,13 @@ import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.qu
import { LOAD_FAILURE } from '../../constants';
import { ONE_COL_WIDTH, UPSTREAM } from './constants';
import LinkedPipeline from './linked_pipeline.vue';
import { unwrapPipelineData, toggleQueryPollingByVisibility, reportToSentry } from './utils';
import {
getQueryHeaders,
reportToSentry,
toggleQueryPollingByVisibility,
unwrapPipelineData,
validateConfigPaths,
} from './utils';
export default {
components: {
......@@ -15,6 +21,11 @@ export default {
type: String,
required: true,
},
configPaths: {
type: Object,
required: true,
validator: validateConfigPaths,
},
linkedPipelines: {
type: Array,
required: true,
......@@ -72,6 +83,9 @@ export default {
this.$apollo.addSmartQuery('currentPipeline', {
query: getPipelineDetails,
pollInterval: 10000,
context() {
return getQueryHeaders(this.configPaths.graphqlResourceEtag);
},
variables() {
return {
projectPath,
......@@ -175,6 +189,7 @@ export default {
v-if="isExpanded(pipeline.id)"
:type="type"
class="d-inline-block gl-mt-n2"
:config-paths="configPaths"
:pipeline="currentPipeline"
:is-linked-pipeline="true"
/>
......
......@@ -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) => {
return { ...linkedPipeline, id: getIdFromGraphQLId(linkedPipeline.id) };
};
......@@ -42,24 +77,12 @@ const unwrapPipelineData = (mainPipelineProjectPath, data) => {
};
};
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));
};
export { unwrapPipelineData, toggleQueryPollingByVisibility };
const validateConfigPaths = (value) => value.graphqlResourceEtag?.length > 0;
export const reportToSentry = (component, failureType) => {
Sentry.withScope((scope) => {
scope.setTag('component', component);
Sentry.captureException(failureType);
});
export {
getQueryHeaders,
reportToSentry,
toggleQueryPollingByVisibility,
unwrapPipelineData,
validateConfigPaths,
};
......@@ -93,13 +93,7 @@ export default async function initPipelineDetailsBundle() {
/* webpackChunkName: 'createPipelinesDetailApp' */ './pipeline_details_graph'
);
const { metricsPath, pipelineProjectPath, pipelineIid } = dataset;
createPipelinesDetailApp(
SELECTORS.PIPELINE_GRAPH,
pipelineProjectPath,
pipelineIid,
metricsPath,
);
createPipelinesDetailApp(SELECTORS.PIPELINE_GRAPH, dataset);
} catch {
Flash(__('An error occurred while loading the pipeline.'));
}
......
......@@ -11,12 +11,15 @@ const apolloProvider = new VueApollo({
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
new Vue({
el: selector,
......@@ -28,6 +31,7 @@ const createPipelinesDetailApp = (selector, pipelineProjectPath, pipelineIid, me
metricsPath,
pipelineProjectPath,
pipelineIid,
graphqlResourceEtag,
dataMethod: GRAPHQL,
},
errorCaptured(err, _vm, info) {
......
......@@ -347,6 +347,11 @@ module GitlabRoutingHelper
Gitlab::UrlBuilder.wiki_page_url(wiki, page, only_path: true, **options)
end
# GraphQL ETag routes
def graphql_etag_pipeline_path(pipeline)
[api_graphql_path, "pipelines/id/#{pipeline.id}"].join(':')
end
private
def snippet_query_params(snippet, *args)
......
......@@ -2,6 +2,11 @@
module Ci
class ExpirePipelineCacheService
class UrlHelpers
include ::Gitlab::Routing
include ::GitlabRoutingHelper
end
def execute(pipeline, delete: false)
store = Gitlab::EtagCaching::Store.new
......@@ -17,27 +22,27 @@ module Ci
private
def project_pipelines_path(project)
Gitlab::Routing.url_helpers.project_pipelines_path(project, format: :json)
url_helpers.project_pipelines_path(project, format: :json)
end
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
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
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
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
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
def each_pipelines_merge_request_path(pipeline)
......@@ -48,7 +53,7 @@ module Ci
end
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
# Updates ETag caches of a pipeline.
......@@ -73,5 +78,9 @@ module Ci
store.touch(graphql_pipeline_path(relative_pipeline))
end
end
def url_helpers
@url_helpers ||= UrlHelpers.new
end
end
end
......@@ -26,4 +26,4 @@
= 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', () => {
const defaultProps = {
pipeline: generateResponse(mockPipelineResponse, 'root/fungi-xoxo'),
configPaths: {
metricsPath: '',
graphqlResourceEtag: 'this/is/a/path',
},
};
const defaultData = {
......
......@@ -9,6 +9,8 @@ import PipelineGraphWrapper from '~/pipelines/components/graph/graph_component_w
import { mockPipelineResponse } from './mock_data';
const defaultProvide = {
graphqlResourceEtag: 'frog/amphibirama/etag/',
metricsPath: '',
pipelineProjectPath: 'frog/amphibirama',
pipelineIid: '22',
};
......@@ -87,6 +89,13 @@ describe('Pipeline graph wrapper', () => {
it('displays the graph', () => {
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', () => {
......
......@@ -20,6 +20,10 @@ describe('Linked Pipelines Column', () => {
columnTitle: 'Downstream',
linkedPipelines: processedPipeline.downstream,
type: DOWNSTREAM,
configPaths: {
metricsPath: '',
graphqlResourceEtag: 'this/is/a/path',
},
};
let wrapper;
......
......@@ -332,4 +332,14 @@ RSpec.describe GitlabRoutingHelper do
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
......@@ -1976,6 +1976,15 @@ apollo-link-http-common@^0.2.14, apollo-link-http-common@^0.2.16:
ts-invariant "^0.4.0"
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:
version "1.2.14"
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