diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 0b920ba8e7a..cc2cf787a8f 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -16,6 +16,50 @@ function decodeUrlParameter(val) {
return decodeURIComponent(val.replace(/\+/g, '%20'));
}
+/**
+ * Safely encodes a string to be used as a path
+ *
+ * Note: This function DOES encode typical URL parts like ?, =, &, #, and +
+ * If you need to use search parameters or URL fragments, they should be
+ * added AFTER calling this function, not before.
+ *
+ * Usecase: An image filename is stored verbatim, and you need to load it in
+ * the browser.
+ *
+ * Example: /some_path/file #1.jpg ==> /some_path/file%20%231.jpg
+ * Example: /some-path/file! Final!.jpg ==> /some-path/file%21%20Final%21.jpg
+ *
+ * Essentially, if a character *could* present a problem in a URL, it's escaped
+ * to the hexadecimal representation instead. This means it's a bit more
+ * aggressive than encodeURIComponent: that built-in function doesn't
+ * encode some characters that *could* be problematic, so this function
+ * adds them (#, !, ~, *, ', (, and )).
+ * Additionally, encodeURIComponent *does* encode `/`, but we want safer
+ * URLs, not non-functional URLs, so this function DEcodes slashes ('%2F').
+ *
+ * @param {String} potentiallyUnsafePath
+ * @returns {String}
+ */
+export function encodeSaferUrl(potentiallyUnsafePath) {
+ const unencode = ['%2F'];
+ const encode = ['#', '!', '~', '\\*', "'", '\\(', '\\)'];
+ let saferPath = encodeURIComponent(potentiallyUnsafePath);
+
+ unencode.forEach((code) => {
+ saferPath = saferPath.replace(new RegExp(code, 'g'), decodeURIComponent(code));
+ });
+ encode.forEach((code) => {
+ const encodedValue = code
+ .codePointAt(code.length - 1)
+ .toString(16)
+ .toUpperCase();
+
+ saferPath = saferPath.replace(new RegExp(code, 'g'), `%${encodedValue}`);
+ });
+
+ return saferPath;
+}
+
export function cleanLeadingSeparator(path) {
return path.replace(PATH_SEPARATOR_LEADING_REGEX, '');
}
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_a.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_a.vue
new file mode 100644
index 00000000000..0393793bfe1
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_a.vue
@@ -0,0 +1,27 @@
+
+
+
+ -
+ {{ $options.i18n.ACTION_TEXT[action] }}
+
+ {{ $options.i18n.ACTION_TEXT[action] }}
+
+
+
+
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_b.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_b.vue
new file mode 100644
index 00000000000..0393793bfe1
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_b.vue
@@ -0,0 +1,27 @@
+
+
+
+ -
+ {{ $options.i18n.ACTION_TEXT[action] }}
+
+ {{ $options.i18n.ACTION_TEXT[action] }}
+
+
+
+
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/constants/index.js b/app/assets/javascripts/pages/projects/learn_gitlab/constants/index.js
new file mode 100644
index 00000000000..8606af29785
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/constants/index.js
@@ -0,0 +1,12 @@
+import { s__ } from '~/locale';
+
+export const ACTION_TEXT = {
+ gitWrite: s__('LearnGitLab|Create a repository'),
+ userAdded: s__('LearnGitLab|Invite your colleagues'),
+ pipelineCreated: s__('LearnGitLab|Set-up CI/CD'),
+ trialStarted: s__('LearnGitLab|Start a free trial of GitLab Gold'),
+ codeOwnersEnabled: s__('LearnGitLab|Add code owners'),
+ requiredMrApprovalsEnabled: s__('LearnGitLab|Enable require merge approvals'),
+ mergeRequestCreated: s__('LearnGitLab|Submit a merge request (MR)'),
+ securityScanEnabled: s__('LearnGitLab|Run a Security scan using CI/CD'),
+};
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js b/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js
new file mode 100644
index 00000000000..c4dec89b984
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js
@@ -0,0 +1,25 @@
+import Vue from 'vue';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import LearnGitlabA from '../components/learn_gitlab_a.vue';
+import LearnGitlabB from '../components/learn_gitlab_b.vue';
+
+function initLearnGitlab() {
+ const el = document.getElementById('js-learn-gitlab-app');
+
+ if (!el) {
+ return false;
+ }
+
+ const actions = convertObjectPropsToCamelCase(JSON.parse(el.dataset.actions));
+
+ const { learnGitlabA } = gon.experiments;
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(learnGitlabA ? LearnGitlabA : LearnGitlabB, { props: { actions } });
+ },
+ });
+}
+
+initLearnGitlab();
diff --git a/app/assets/javascripts/projects/settings/access_dropdown.js b/app/assets/javascripts/projects/settings/access_dropdown.js
index 209ae9927cf..a5e53ee3927 100644
--- a/app/assets/javascripts/projects/settings/access_dropdown.js
+++ b/app/assets/javascripts/projects/settings/access_dropdown.js
@@ -11,7 +11,6 @@ export default class AccessDropdown {
const { $dropdown, accessLevel, accessLevelsData, hasLicense = true } = options;
this.options = options;
this.hasLicense = hasLicense;
- this.deployKeysOnProtectedBranchesEnabled = gon.features.deployKeysOnProtectedBranches;
this.groups = [];
this.accessLevel = accessLevel;
this.accessLevelsData = accessLevelsData.roles;
@@ -330,11 +329,7 @@ export default class AccessDropdown {
);
})
.catch(() => {
- if (this.deployKeysOnProtectedBranchesEnabled) {
- createFlash({ message: __('Failed to load groups, users and deploy keys.') });
- } else {
- createFlash({ message: __('Failed to load groups & users.') });
- }
+ createFlash({ message: __('Failed to load groups, users and deploy keys.') });
});
} else {
this.getDeployKeys(query)
@@ -445,35 +440,33 @@ export default class AccessDropdown {
}
}
- if (this.deployKeysOnProtectedBranchesEnabled) {
- const deployKeys = deployKeysResponse.map((response) => {
- const {
- id,
- fingerprint,
- title,
- owner: { avatar_url, name, username },
- } = response;
+ const deployKeys = deployKeysResponse.map((response) => {
+ const {
+ id,
+ fingerprint,
+ title,
+ owner: { avatar_url, name, username },
+ } = response;
- const shortFingerprint = `(${fingerprint.substring(0, 14)}...)`;
+ const shortFingerprint = `(${fingerprint.substring(0, 14)}...)`;
- return {
- id,
- title: title.concat(' ', shortFingerprint),
- avatar_url,
- fullname: name,
- username,
- type: LEVEL_TYPES.DEPLOY_KEY,
- };
- });
+ return {
+ id,
+ title: title.concat(' ', shortFingerprint),
+ avatar_url,
+ fullname: name,
+ username,
+ type: LEVEL_TYPES.DEPLOY_KEY,
+ };
+ });
- if (this.accessLevel === ACCESS_LEVELS.PUSH) {
- if (deployKeys.length) {
- consolidatedData = consolidatedData.concat(
- [{ type: 'divider' }],
- [{ type: 'header', content: s__('AccessDropdown|Deploy Keys') }],
- deployKeys,
- );
- }
+ if (this.accessLevel === ACCESS_LEVELS.PUSH) {
+ if (deployKeys.length) {
+ consolidatedData = consolidatedData.concat(
+ [{ type: 'divider' }],
+ [{ type: 'header', content: s__('AccessDropdown|Deploy Keys') }],
+ deployKeys,
+ );
}
}
@@ -501,19 +494,15 @@ export default class AccessDropdown {
}
getDeployKeys(query) {
- if (this.deployKeysOnProtectedBranchesEnabled) {
- return axios.get(this.buildUrl(gon.relative_url_root, this.deployKeysPath), {
- params: {
- search: query,
- per_page: 20,
- active: true,
- project_id: gon.current_project_id,
- push_code: true,
- },
- });
- }
-
- return Promise.resolve({ data: [] });
+ return axios.get(this.buildUrl(gon.relative_url_root, this.deployKeysPath), {
+ params: {
+ search: query,
+ per_page: 20,
+ active: true,
+ project_id: gon.current_project_id,
+ push_code: true,
+ },
+ });
}
buildUrl(urlRoot, url) {
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
index 9ece6a52805..a49eb7fd611 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
@@ -1,6 +1,7 @@