Commit 385be9bd authored by Enrique Alcantara's avatar Enrique Alcantara Committed by Francisco Javier López

Defer wiki rendering

Load a Wiki Page’s content using an
asynchronous HTTP request to avoid
page timeouts. This page is behind a
feature flag
parent f4d8114c
import { mountApplications } from '~/pages/shared/wikis/show';
import { mountApplications as mountEditApplications } from '~/pages/shared/wikis/async_edit'; import { mountApplications as mountEditApplications } from '~/pages/shared/wikis/async_edit';
mountApplications();
mountEditApplications(); mountEditApplications();
<script>
import { GlSkeletonLoader, GlSafeHtmlDirective, GlAlert } from '@gitlab/ui';
import createFlash from '~/flash';
import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import { renderGFM } from '../render_gfm_facade';
export default {
components: {
GlSkeletonLoader,
GlAlert,
},
directives: {
SafeHtml: GlSafeHtmlDirective,
},
props: {
getWikiContentUrl: {
type: String,
required: true,
},
},
data() {
return {
isLoadingContent: false,
loadingContentFailed: false,
content: null,
};
},
mounted() {
this.loadWikiContent();
},
methods: {
async loadWikiContent() {
this.loadingContentFailed = false;
this.isLoadingContent = true;
try {
const {
data: { content },
} = await axios.get(this.getWikiContentUrl, { params: { render_html: true } });
this.content = content;
this.$nextTick()
.then(() => {
renderGFM(this.$refs.content);
})
.catch(() =>
createFlash({
message: __('The content for this wiki page failed to render.'),
}),
);
} catch (e) {
this.loadingContentFailed = true;
} finally {
this.isLoadingContent = false;
}
},
},
i18n: {
loadingContentFailed: __(
'The content for this wiki page failed to load. To fix this error, reload the page.',
),
retryLoadingContent: __('Retry'),
},
};
</script>
<template>
<gl-skeleton-loader v-if="isLoadingContent" :width="830" :height="113">
<rect width="540" height="16" rx="4" />
<rect y="49" width="701" height="16" rx="4" />
<rect y="24" width="830" height="16" rx="4" />
<rect y="73" width="540" height="16" rx="4" />
</gl-skeleton-loader>
<gl-alert
v-else-if="loadingContentFailed"
:dismissible="false"
variant="danger"
:primary-button-text="$options.i18n.retryLoadingContent"
@primaryAction="loadWikiContent"
>
{{ $options.i18n.loadingContentFailed }}
</gl-alert>
<div
v-else-if="!loadingContentFailed && !isLoadingContent"
ref="content"
data-qa-selector="wiki_page_content"
data-testid="wiki_page_content"
class="js-wiki-page-content md"
v-html="content /* eslint-disable-line vue/no-v-html */"
></div>
</template>
import $ from 'jquery';
export const renderGFM = (el) => {
return $(el).renderGFM();
};
import Vue from 'vue';
import Wikis from './wikis';
import WikiContent from './components/wiki_content.vue';
const mountWikiContentApp = () => {
const el = document.querySelector('.js-async-wiki-page-content');
if (el) {
const { getWikiContentUrl } = el.dataset;
// eslint-disable-next-line no-new
new Vue({
el,
render(createElement) {
return createElement(WikiContent, {
props: { getWikiContentUrl },
});
},
});
}
};
export const mountApplications = () => {
// eslint-disable-next-line no-new
new Wikis();
mountWikiContentApp();
};
...@@ -223,7 +223,7 @@ module WikiActions ...@@ -223,7 +223,7 @@ module WikiActions
def page def page
strong_memoize(:page) do strong_memoize(:page) do
wiki.find_page(*page_params) wiki.find_page(*page_params, load_content: load_content?)
end end
end end
...@@ -310,6 +310,12 @@ module WikiActions ...@@ -310,6 +310,12 @@ module WikiActions
def send_wiki_file_blob(wiki, file_blob) def send_wiki_file_blob(wiki, file_blob)
send_blob(wiki.repository, file_blob) send_blob(wiki.repository, file_blob)
end end
def load_content?
return false if params[:action] == 'history'
!(params[:action] == 'show' && Feature.enabled?(:wiki_async_load, container, default_enabled: :yaml))
end
end end
WikiActions.prepend_mod WikiActions.prepend_mod
...@@ -134,6 +134,16 @@ module WikiHelper ...@@ -134,6 +134,16 @@ module WikiHelper
current_user&.can?(:admin_project, container) && current_user&.can?(:admin_project, container) &&
!container.has_confluence? !container.has_confluence?
end end
def wiki_page_render_api_endpoint(page)
api_v4_projects_wikis_path(wiki_page_render_api_endpoint_params(page))
end
private
def wiki_page_render_api_endpoint_params(page)
{ id: page.container.id, slug: ERB::Util.url_encode(page.slug), params: { version: page.version.id } }
end
end end
WikiHelper.prepend_mod_with('WikiHelper') WikiHelper.prepend_mod_with('WikiHelper')
...@@ -26,6 +26,10 @@ ...@@ -26,6 +26,10 @@
%div %div
- if can?(current_user, :create_wiki, @wiki.container) && @page.latest? && @valid_encoding - if can?(current_user, :create_wiki, @wiki.container) && @page.latest? && @valid_encoding
= link_to sprite_icon('pencil', css_class: 'gl-icon'), wiki_page_path(@wiki, @page, action: :edit), title: 'Edit', role: "button", class: 'btn gl-button btn-icon btn-default js-wiki-edit', data: { qa_selector: 'edit_page_button', testid: 'wiki_edit_button' } = link_to sprite_icon('pencil', css_class: 'gl-icon'), wiki_page_path(@wiki, @page, action: :edit), title: 'Edit', role: "button", class: 'btn gl-button btn-icon btn-default js-wiki-edit', data: { qa_selector: 'edit_page_button', testid: 'wiki_edit_button' }
= render 'shared/wikis/wiki_content'
- if Feature.enabled?(:wiki_async_load, @wiki.container, default_enabled: :yaml)
.js-async-wiki-page-content.md.gl-pt-2{ data: { qa_selector: 'wiki_page_content', testid: 'wiki_page_content', tracking_context: wiki_page_tracking_context(@page).to_json, get_wiki_content_url: wiki_page_render_api_endpoint(@page) } }
- else
= render 'shared/wikis/wiki_content'
= render 'shared/wikis/sidebar' = render 'shared/wikis/sidebar'
---
name: wiki_async_load
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82394
rollout_issue_url:
milestone: '14.9'
type: development
group: group::editor
default_enabled: false
import { mountApplications } from '~/pages/shared/wikis/show';
import { mountApplications as mountEditApplications } from '~/pages/shared/wikis/async_edit'; import { mountApplications as mountEditApplications } from '~/pages/shared/wikis/async_edit';
mountApplications();
mountEditApplications(); mountEditApplications();
...@@ -13,5 +13,12 @@ module EE ...@@ -13,5 +13,12 @@ module EE
super super
end end
end end
override :wiki_page_render_api_endpoint
def wiki_page_render_api_endpoint(page)
return super if page.wiki.is_a?(ProjectWiki)
api_v4_groups_wikis_path(wiki_page_render_api_endpoint_params(page))
end
end end
end end
...@@ -14,14 +14,26 @@ RSpec.describe 'Group wikis', :js do ...@@ -14,14 +14,26 @@ RSpec.describe 'Group wikis', :js do
wiki.container.add_owner(user) wiki.container.add_owner(user)
end end
it_behaves_like 'User creates wiki page' shared_examples 'wiki feature tests' do
it_behaves_like 'User deletes wiki page' it_behaves_like 'User creates wiki page'
it_behaves_like 'User previews wiki changes' it_behaves_like 'User deletes wiki page'
it_behaves_like 'User updates wiki page' it_behaves_like 'User previews wiki changes'
it_behaves_like 'User uses wiki shortcuts' it_behaves_like 'User updates wiki page'
it_behaves_like 'User views AsciiDoc page with includes' it_behaves_like 'User uses wiki shortcuts'
it_behaves_like 'User views a wiki page' it_behaves_like 'User views AsciiDoc page with includes'
it_behaves_like 'User views wiki pages' it_behaves_like 'User views a wiki page'
it_behaves_like 'User views wiki sidebar' it_behaves_like 'User views wiki pages'
it_behaves_like 'User views Git access wiki page' it_behaves_like 'User views wiki sidebar'
it_behaves_like 'User views Git access wiki page'
end
it_behaves_like 'wiki feature tests'
context 'when feature flag :wiki_async_load is disabled' do
before do
stub_feature_flags(wiki_async_load: false)
end
it_behaves_like 'wiki feature tests'
end
end end
...@@ -37131,6 +37131,12 @@ msgstr "" ...@@ -37131,6 +37131,12 @@ msgstr ""
msgid "The content editor may change the markdown formatting style of the document, which may not match your original markdown style." msgid "The content editor may change the markdown formatting style of the document, which may not match your original markdown style."
msgstr "" msgstr ""
msgid "The content for this wiki page failed to load. To fix this error, reload the page."
msgstr ""
msgid "The content for this wiki page failed to render."
msgstr ""
msgid "The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository." msgid "The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository."
msgstr "" msgstr ""
......
...@@ -56,8 +56,11 @@ module DeprecationToolkitEnv ...@@ -56,8 +56,11 @@ module DeprecationToolkitEnv
# In this case, we recommend to add a silence together with an issue to patch or update # In this case, we recommend to add a silence together with an issue to patch or update
# the dependency causing the problem. # the dependency causing the problem.
# See https://gitlab.com/gitlab-org/gitlab/-/commit/aea37f506bbe036378998916d374966c031bf347#note_647515736 # See https://gitlab.com/gitlab-org/gitlab/-/commit/aea37f506bbe036378998916d374966c031bf347#note_647515736
#
# - lib/gitlab/lazy.rb: https://gitlab.com/gitlab-org/gitlab/-/issues/356367
def self.allowed_kwarg_warning_paths def self.allowed_kwarg_warning_paths
%w[ %w[
lib/gitlab/lazy.rb
] ]
end end
......
...@@ -8,14 +8,26 @@ RSpec.describe 'Project wikis', :js do ...@@ -8,14 +8,26 @@ RSpec.describe 'Project wikis', :js do
let(:wiki) { create(:project_wiki, user: user, project: project) } let(:wiki) { create(:project_wiki, user: user, project: project) }
let(:project) { create(:project, namespace: user.namespace, creator: user) } let(:project) { create(:project, namespace: user.namespace, creator: user) }
it_behaves_like 'User creates wiki page' shared_examples 'wiki feature tests' do
it_behaves_like 'User deletes wiki page' it_behaves_like 'User creates wiki page'
it_behaves_like 'User previews wiki changes' it_behaves_like 'User deletes wiki page'
it_behaves_like 'User updates wiki page' it_behaves_like 'User previews wiki changes'
it_behaves_like 'User uses wiki shortcuts' it_behaves_like 'User updates wiki page'
it_behaves_like 'User views AsciiDoc page with includes' it_behaves_like 'User uses wiki shortcuts'
it_behaves_like 'User views a wiki page' it_behaves_like 'User views AsciiDoc page with includes'
it_behaves_like 'User views wiki pages' it_behaves_like 'User views a wiki page'
it_behaves_like 'User views wiki sidebar' it_behaves_like 'User views wiki pages'
it_behaves_like 'User views Git access wiki page' it_behaves_like 'User views wiki sidebar'
it_behaves_like 'User views Git access wiki page'
end
it_behaves_like 'wiki feature tests'
context 'when feature flag :wiki_async_load is disabled' do
before do
stub_feature_flags(wiki_async_load: false)
end
it_behaves_like 'wiki feature tests'
end
end end
import { GlSkeletonLoader, GlAlert } from '@gitlab/ui';
import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import WikiContent from '~/pages/shared/wikis/components/wiki_content.vue';
import { renderGFM } from '~/pages/shared/wikis/render_gfm_facade';
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
import waitForPromises from 'helpers/wait_for_promises';
jest.mock('~/pages/shared/wikis/render_gfm_facade');
describe('pages/shared/wikis/components/wiki_content', () => {
const PATH = '/test';
let wrapper;
let mock;
function buildWrapper(propsData = {}) {
wrapper = shallowMount(WikiContent, {
propsData: { getWikiContentUrl: PATH, ...propsData },
stubs: {
GlSkeletonLoader,
GlAlert,
},
});
}
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findGlAlert = () => wrapper.findComponent(GlAlert);
const findGlSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findContent = () => wrapper.find('[data-testid="wiki_page_content"]');
describe('when loading content', () => {
beforeEach(() => {
buildWrapper();
});
it('renders skeleton loader', () => {
expect(findGlSkeletonLoader().exists()).toBe(true);
});
it('does not render content container or error alert', () => {
expect(findGlAlert().exists()).toBe(false);
expect(findContent().exists()).toBe(false);
});
});
describe('when content loads successfully', () => {
const content = 'content';
beforeEach(() => {
mock.onGet(PATH, { params: { render_html: true } }).replyOnce(httpStatus.OK, { content });
buildWrapper();
return waitForPromises();
});
it('renders content container', () => {
expect(findContent().text()).toBe(content);
});
it('does not render skeleton loader or error alert', () => {
expect(findGlAlert().exists()).toBe(false);
expect(findGlSkeletonLoader().exists()).toBe(false);
});
it('calls renderGFM after nextTick', async () => {
await nextTick();
expect(renderGFM).toHaveBeenCalledWith(wrapper.element);
});
});
describe('when loading content fails', () => {
beforeEach(() => {
mock.onGet(PATH).replyOnce(httpStatus.INTERNAL_SERVER_ERROR, '');
buildWrapper();
return waitForPromises();
});
it('renders error alert', () => {
expect(findGlAlert().exists()).toBe(true);
});
it('does not render skeleton loader or content container', () => {
expect(findContent().exists()).toBe(false);
expect(findGlSkeletonLoader().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