Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-10-31 12:09:32 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-10-31 12:09:32 +0300
commit5025412fc4ab16cc7049a38d43fdc2e4095a1f87 (patch)
treeecec75618d069e02ba0ebcf36db6630150a9d073
parent853c0c530b624a2f94ce85acbbdffc70510bdba3 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_gfm.js2
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js129
-rw-r--r--app/assets/javascripts/related_issues/components/related_issues_block.vue14
-rw-r--r--app/assets/javascripts/related_issues/components/related_issues_root.vue10
-rw-r--r--app/assets/javascripts/webhooks/components/form_url_app.vue15
-rw-r--r--app/assets/javascripts/webhooks/components/form_url_mask_item.vue20
-rw-r--r--app/assets/javascripts/webhooks/constants.js2
-rw-r--r--app/assets/javascripts/webhooks/index.js7
-rw-r--r--app/helpers/hooks_helper.rb2
-rw-r--r--app/services/clusters/applications/patch_service.rb32
-rw-r--r--app/services/clusters/applications/update_service.rb17
-rw-r--r--app/workers/cluster_patch_app_worker.rb9
-rw-r--r--db/post_migrate/20221018232820_add_temp_index_for_user_details_fields.rb22
-rw-r--r--db/post_migrate/20221019002459_queue_backfill_user_details_fields.rb16
-rw-r--r--db/schema_migrations/202210182328201
-rw-r--r--db/schema_migrations/202210190024591
-rw-r--r--db/structure.sql2
-rw-r--r--doc/.markdownlint/require_helper.js14
-rw-r--r--doc/.markdownlint/rules/tabs_blank_lines.js26
-rw-r--r--doc/.markdownlint/rules/tabs_title_markup.js31
-rw-r--r--doc/.markdownlint/rules/tabs_title_text.js23
-rw-r--r--doc/.markdownlint/rules/tabs_wrapper_tags.js21
-rw-r--r--lib/api/entities/pull_mirror.rb16
-rw-r--r--lib/gitlab/background_migration/backfill_user_details_fields.rb61
-rw-r--r--lib/sidebars/projects/menus/deployments_menu.rb2
-rw-r--r--locale/gitlab.pot6
-rw-r--r--package.json2
-rwxr-xr-xscripts/lint-doc.sh2
-rw-r--r--spec/factories/project_hooks.rb4
-rw-r--r--spec/frontend/behaviors/markdown/render_sandboxed_mermaid_spec.js145
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_root_spec.js8
-rw-r--r--spec/frontend/webhooks/components/form_url_app_spec.js38
-rw-r--r--spec/frontend/webhooks/components/form_url_mask_item_spec.js37
-rw-r--r--spec/helpers/hooks_helper_spec.rb35
-rw-r--r--spec/lib/gitlab/background_migration/backfill_user_details_fields_spec.rb222
-rw-r--r--spec/migrations/queue_backfill_user_details_fields_spec.rb24
-rw-r--r--spec/services/clusters/applications/patch_service_spec.rb80
-rw-r--r--spec/services/clusters/applications/update_service_spec.rb91
-rw-r--r--spec/support/shared_contexts/navbar_structure_context.rb2
-rw-r--r--spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb2
-rw-r--r--yarn.lock8
41 files changed, 729 insertions, 472 deletions
diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js
index a08cf48c327..ee5c0fe5ef3 100644
--- a/app/assets/javascripts/behaviors/markdown/render_gfm.js
+++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js
@@ -15,7 +15,7 @@ $.fn.renderGFM = function renderGFM() {
syntaxHighlight(this.find('.js-syntax-highlight').get());
renderKroki(this.find('.js-render-kroki[hidden]').get());
renderMath(this.find('.js-render-math'));
- renderSandboxedMermaid(this.find('.js-render-mermaid').get());
+ renderSandboxedMermaid(this.find('.js-render-mermaid'));
renderJSONTable(
Array.from(this.find('[lang="json"][data-lang-params="table"]').get()).map((e) => e.parentNode),
);
diff --git a/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js
index 031b03e0d59..077e96b2fee 100644
--- a/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js
+++ b/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js
@@ -1,4 +1,5 @@
-import { countBy } from 'lodash';
+import $ from 'jquery';
+import { once, countBy } from 'lodash';
import { __ } from '~/locale';
import {
getBaseURL,
@@ -7,8 +8,7 @@ import {
joinPaths,
} from '~/lib/utils/url_utility';
import { darkModeEnabled } from '~/lib/utils/color_utils';
-import { setAttributes, isElementVisible } from '~/lib/utils/dom_utils';
-import { createAlert, VARIANT_WARNING } from '~/flash';
+import { setAttributes } from '~/lib/utils/dom_utils';
import { unrestrictedPages } from './constants';
// Renders diagrams and flowcharts from text using Mermaid in any element with the
@@ -27,30 +27,17 @@ import { unrestrictedPages } from './constants';
const SANDBOX_FRAME_PATH = '/-/sandbox/mermaid';
// This is an arbitrary number; Can be iterated upon when suitable.
-export const MAX_CHAR_LIMIT = 2000;
+const MAX_CHAR_LIMIT = 2000;
// Max # of mermaid blocks that can be rendered in a page.
-export const MAX_MERMAID_BLOCK_LIMIT = 50;
+const MAX_MERMAID_BLOCK_LIMIT = 50;
// Max # of `&` allowed in Chaining of links syntax
const MAX_CHAINING_OF_LINKS_LIMIT = 30;
-
export const BUFFER_IFRAME_HEIGHT = 10;
export const SANDBOX_ATTRIBUTES = 'allow-scripts allow-popups';
-
-const ALERT_CONTAINER_CLASS = 'mermaid-alert-container';
-export const LAZY_ALERT_SHOWN_CLASS = 'lazy-alert-shown';
-
// Keep a map of mermaid blocks we've already rendered.
const elsProcessingMap = new WeakMap();
let renderedMermaidBlocks = 0;
-/**
- * Determines whether a given Mermaid diagram is visible.
- *
- * @param {Element} el The Mermaid DOM node
- * @returns
- */
-const isVisibleMermaid = (el) => el.closest('details') === null && isElementVisible(el);
-
function shouldLazyLoadMermaidBlock(source) {
/**
* If source contains `&`, which means that it might
@@ -117,8 +104,8 @@ function renderMermaidEl(el, source) {
);
}
-function renderMermaids(els) {
- if (!els.length) return;
+function renderMermaids($els) {
+ if (!$els.length) return;
const pageName = document.querySelector('body').dataset.page;
@@ -127,7 +114,7 @@ function renderMermaids(els) {
let renderedChars = 0;
- els.forEach((el) => {
+ $els.each((i, el) => {
// Skipping all the elements which we've already queued in requestIdleCallback
if (elsProcessingMap.has(el)) {
return;
@@ -146,29 +133,33 @@ function renderMermaids(els) {
renderedMermaidBlocks >= MAX_MERMAID_BLOCK_LIMIT ||
shouldLazyLoadMermaidBlock(source))
) {
- const parent = el.parentNode;
-
- if (!parent.classList.contains(LAZY_ALERT_SHOWN_CLASS)) {
- const alertContainer = document.createElement('div');
- alertContainer.classList.add(ALERT_CONTAINER_CLASS);
- alertContainer.classList.add('gl-mb-5');
- parent.after(alertContainer);
- createAlert({
- message: __(
- 'Warning: Displaying this diagram might cause performance issues on this page.',
- ),
- variant: VARIANT_WARNING,
- parent: parent.parentNode,
- containerSelector: `.${ALERT_CONTAINER_CLASS}`,
- primaryButton: {
- text: __('Display'),
- clickHandler: () => {
- alertContainer.remove();
- renderMermaidEl(el, source);
- },
- },
- });
- parent.classList.add(LAZY_ALERT_SHOWN_CLASS);
+ const html = `
+ <div class="alert gl-alert gl-alert-warning alert-dismissible lazy-render-mermaid-container js-lazy-render-mermaid-container fade show" role="alert">
+ <div>
+ <div>
+ <div class="js-warning-text"></div>
+ <div class="gl-alert-actions">
+ <button type="button" class="js-lazy-render-mermaid btn gl-alert-action btn-confirm btn-md gl-button">Display</button>
+ </div>
+ </div>
+ <button type="button" class="close" data-dismiss="alert" aria-label="Close">
+ <span aria-hidden="true">&times;</span>
+ </button>
+ </div>
+ </div>
+ `;
+
+ const $parent = $(el).parent();
+
+ if (!$parent.hasClass('lazy-alert-shown')) {
+ $parent.after(html);
+ $parent
+ .siblings()
+ .find('.js-warning-text')
+ .text(
+ __('Warning: Displaying this diagram might cause performance issues on this page.'),
+ );
+ $parent.addClass('lazy-alert-shown');
}
return;
@@ -185,33 +176,37 @@ function renderMermaids(els) {
});
}
-export default function renderMermaid(els) {
- if (!els.length) return;
+const hookLazyRenderMermaidEvent = once(() => {
+ $(document.body).on('click', '.js-lazy-render-mermaid', function eventHandler() {
+ const parent = $(this).closest('.js-lazy-render-mermaid-container');
+ const pre = parent.prev();
- const visibleMermaids = [];
- const hiddenMermaids = [];
+ const el = pre.find('.js-render-mermaid');
- for (const el of els) {
- if (isVisibleMermaid(el)) {
- visibleMermaids.push(el);
- } else {
- hiddenMermaids.push(el);
- }
- }
+ parent.remove();
+
+ // sandbox update
+ const element = el.get(0);
+ const { source } = fixElementSource(element);
+
+ renderMermaidEl(element, source);
+ });
+});
+
+export default function renderMermaid($els) {
+ if (!$els.length) return;
+
+ const visibleMermaids = $els.filter(function filter() {
+ return $(this).closest('details').length === 0 && $(this).is(':visible');
+ });
renderMermaids(visibleMermaids);
- hiddenMermaids.forEach((el) => {
- el.closest('details').addEventListener(
- 'toggle',
- ({ target: details }) => {
- if (details.open) {
- renderMermaids([...details.querySelectorAll('.js-render-mermaid')]);
- }
- },
- {
- once: true,
- },
- );
+ $els.closest('details').one('toggle', function toggle() {
+ if (this.open) {
+ renderMermaids($(this).find('.js-render-mermaid'));
+ }
});
+
+ hookLazyRenderMermaidEvent();
}
diff --git a/app/assets/javascripts/related_issues/components/related_issues_block.vue b/app/assets/javascripts/related_issues/components/related_issues_block.vue
index 1ab41ee2f0a..c3726ebc14a 100644
--- a/app/assets/javascripts/related_issues/components/related_issues_block.vue
+++ b/app/assets/javascripts/related_issues/components/related_issues_block.vue
@@ -95,6 +95,16 @@ export default {
required: false,
default: true,
},
+ hasError: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ itemAddFailureMessage: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -233,7 +243,7 @@ export default {
<div
v-if="isFormVisible"
class="js-add-related-issues-form-area card-body bordered-box bg-white"
- :class="{ 'gl-mb-5': shouldShowTokenBody }"
+ :class="{ 'gl-mb-5': shouldShowTokenBody, 'gl-show-field-errors': hasError }"
>
<add-issuable-form
:show-categorized-issues="showCategorizedIssues"
@@ -245,6 +255,8 @@ export default {
:auto-complete-epics="autoCompleteEpics"
:auto-complete-issues="autoCompleteIssues"
:path-id-separator="pathIdSeparator"
+ :has-error="hasError"
+ :item-add-failure-message="itemAddFailureMessage"
@pendingIssuableRemoveRequest="$emit('pendingIssuableRemoveRequest', $event)"
@addIssuableFormInput="$emit('addIssuableFormInput', $event)"
@addIssuableFormBlur="$emit('addIssuableFormBlur', $event)"
diff --git a/app/assets/javascripts/related_issues/components/related_issues_root.vue b/app/assets/javascripts/related_issues/components/related_issues_root.vue
index 38e1d6e9d4f..795eb3b0083 100644
--- a/app/assets/javascripts/related_issues/components/related_issues_root.vue
+++ b/app/assets/javascripts/related_issues/components/related_issues_root.vue
@@ -107,6 +107,8 @@ export default {
isSubmitting: false,
isFormVisible: false,
inputValue: '',
+ hasError: false,
+ errorMessage: null,
};
},
computed: {
@@ -170,11 +172,11 @@ export default {
this.isFormVisible = false;
})
.catch(({ response }) => {
- let errorMessage = addRelatedIssueErrorMap[this.issuableType];
+ this.hasError = true;
+ this.errorMessage = addRelatedIssueErrorMap[this.issuableType];
if (response && response.data && response.data.message) {
- errorMessage = response.data.message;
+ this.errorMessage = response.data.message;
}
- createAlert({ message: errorMessage });
})
.finally(() => {
this.isSubmitting = false;
@@ -266,6 +268,8 @@ export default {
:issuable-type="issuableType"
:path-id-separator="pathIdSeparator"
:show-categorized-issues="showCategorizedIssues"
+ :has-error="hasError"
+ :item-add-failure-message="errorMessage"
@saveReorder="saveIssueOrder"
@toggleAddRelatedIssuesForm="onToggleAddRelatedIssuesForm"
@addIssuableFormInput="onInput"
diff --git a/app/assets/javascripts/webhooks/components/form_url_app.vue b/app/assets/javascripts/webhooks/components/form_url_app.vue
index 5ec16d4ba15..45526ff9080 100644
--- a/app/assets/javascripts/webhooks/components/form_url_app.vue
+++ b/app/assets/javascripts/webhooks/components/form_url_app.vue
@@ -1,5 +1,5 @@
<script>
-import { isEmpty } from 'lodash';
+import { cloneDeep, isEmpty } from 'lodash';
import { GlFormGroup, GlFormInput, GlFormRadio, GlFormRadioGroup, GlLink } from '@gitlab/ui';
import { __, s__ } from '~/locale';
@@ -30,7 +30,7 @@ export default {
return {
maskEnabled: !isEmpty(this.initialUrlVariables),
url: this.initialUrl,
- items: isEmpty(this.initialUrlVariables) ? [{}] : this.initialUrlVariables,
+ items: this.getInitialItems(),
};
},
computed: {
@@ -54,6 +54,16 @@ export default {
},
},
methods: {
+ getInitialItems() {
+ return isEmpty(this.initialUrlVariables) ? [{}] : cloneDeep(this.initialUrlVariables);
+ },
+ isEditingItem(key) {
+ if (isEmpty(this.initialUrlVariables)) {
+ return false;
+ }
+
+ return this.initialUrlVariables.some((item) => item.key === key);
+ },
onItemInput({ index, key, value }) {
this.$set(this.items, index, { key, value });
},
@@ -112,6 +122,7 @@ export default {
:index="index"
:item-key="key"
:item-value="value"
+ :is-editing="isEditingItem(key)"
@input="onItemInput"
@remove="removeItem"
/>
diff --git a/app/assets/javascripts/webhooks/components/form_url_mask_item.vue b/app/assets/javascripts/webhooks/components/form_url_mask_item.vue
index 3b75f9b6c0d..aa5d9ce57f4 100644
--- a/app/assets/javascripts/webhooks/components/form_url_mask_item.vue
+++ b/app/assets/javascripts/webhooks/components/form_url_mask_item.vue
@@ -1,6 +1,7 @@
<script>
import { GlButton, GlFormGroup, GlFormInput } from '@gitlab/ui';
import { s__ } from '~/locale';
+import { MASK_ITEM_VALUE_HIDDEN } from '../constants';
export default {
components: {
@@ -24,6 +25,11 @@ export default {
required: false,
default: null,
},
+ isEditing: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
keyInputId() {
@@ -32,6 +38,9 @@ export default {
valueInputId() {
return this.inputId('value');
},
+ displayValue() {
+ return this.isEditing ? MASK_ITEM_VALUE_HIDDEN : this.itemValue;
+ },
},
methods: {
inputId(type) {
@@ -68,7 +77,8 @@ export default {
<gl-form-input
:id="valueInputId"
:name="inputName('value')"
- :value="itemValue"
+ :value="displayValue"
+ :disabled="isEditing"
@input="onValueInput"
/>
</gl-form-group>
@@ -82,9 +92,15 @@ export default {
:id="keyInputId"
:name="inputName('key')"
:value="itemKey"
+ :disabled="isEditing"
@input="onKeyInput"
/>
</gl-form-group>
- <gl-button icon="remove" :aria-label="__('Remove')" @click="onRemoveClick" />
+ <gl-button
+ icon="remove"
+ :aria-label="__('Remove')"
+ :disabled="isEditing"
+ @click="onRemoveClick"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/webhooks/constants.js b/app/assets/javascripts/webhooks/constants.js
index abef16545bc..6710a418117 100644
--- a/app/assets/javascripts/webhooks/constants.js
+++ b/app/assets/javascripts/webhooks/constants.js
@@ -15,3 +15,5 @@ export const descriptionText = {
),
[BRANCH_FILTER_REGEX]: s__('Webhooks|Regex such as %{REGEX_CODE} is supported.'),
};
+
+export const MASK_ITEM_VALUE_HIDDEN = '************';
diff --git a/app/assets/javascripts/webhooks/index.js b/app/assets/javascripts/webhooks/index.js
index 1b2b33e44c1..d90680a9bac 100644
--- a/app/assets/javascripts/webhooks/index.js
+++ b/app/assets/javascripts/webhooks/index.js
@@ -10,6 +10,11 @@ export default () => {
const { url: initialUrl, urlVariables } = el.dataset;
+ // Convert the array of 'key' strings to array of { key } objects
+ const initialUrlVariables = urlVariables
+ ? JSON.parse(urlVariables)?.map((key) => ({ key }))
+ : undefined;
+
return new Vue({
el,
name: 'WebhookFormRoot',
@@ -17,7 +22,7 @@ export default () => {
return createElement(FormUrlApp, {
props: {
initialUrl,
- initialUrlVariables: urlVariables ? JSON.parse(urlVariables) : undefined,
+ initialUrlVariables,
},
});
},
diff --git a/app/helpers/hooks_helper.rb b/app/helpers/hooks_helper.rb
index e050ccc0e40..44ed61b8fde 100644
--- a/app/helpers/hooks_helper.rb
+++ b/app/helpers/hooks_helper.rb
@@ -4,7 +4,7 @@ module HooksHelper
def webhook_form_data(hook)
{
url: hook.url,
- url_variables: nil
+ url_variables: Gitlab::Json.dump(hook.url_variables.keys)
}
end
diff --git a/app/services/clusters/applications/patch_service.rb b/app/services/clusters/applications/patch_service.rb
deleted file mode 100644
index fbea18bae6b..00000000000
--- a/app/services/clusters/applications/patch_service.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-module Clusters
- module Applications
- class PatchService < BaseHelmService
- def execute
- return unless app.scheduled?
-
- app.make_updating!
-
- patch
- end
-
- private
-
- def patch
- log_event(:begin_patch)
- helm_api.update(update_command)
-
- log_event(:schedule_wait_for_patch)
- ClusterWaitForAppInstallationWorker.perform_in(
- ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id)
- rescue Kubeclient::HttpError => e
- log_error(e)
- app.make_errored!(_('Kubernetes error: %{error_code}') % { error_code: e.error_code })
- rescue StandardError => e
- log_error(e)
- app.make_errored!(_('Failed to update.'))
- end
- end
- end
-end
diff --git a/app/services/clusters/applications/update_service.rb b/app/services/clusters/applications/update_service.rb
deleted file mode 100644
index 7a36401f156..00000000000
--- a/app/services/clusters/applications/update_service.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-module Clusters
- module Applications
- class UpdateService < Clusters::Applications::BaseService
- private
-
- def worker_class(application)
- ClusterPatchAppWorker
- end
-
- def builder
- cluster.public_send(application_class.association_name) # rubocop:disable GitlabSecurity/PublicSend
- end
- end
- end
-end
diff --git a/app/workers/cluster_patch_app_worker.rb b/app/workers/cluster_patch_app_worker.rb
index bb16cf7a5e6..1ef9dc7f6fe 100644
--- a/app/workers/cluster_patch_app_worker.rb
+++ b/app/workers/cluster_patch_app_worker.rb
@@ -1,5 +1,8 @@
# frozen_string_literal: true
+# DEPRECATED
+#
+# To be removed by https://gitlab.com/gitlab-org/gitlab/-/issues/366573
class ClusterPatchAppWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
@@ -12,9 +15,5 @@ class ClusterPatchAppWorker # rubocop:disable Scalability/IdempotentWorker
worker_has_external_dependencies!
loggable_arguments 0
- def perform(app_name, app_id)
- find_application(app_name, app_id) do |app|
- Clusters::Applications::PatchService.new(app).execute
- end
- end
+ def perform(app_name, app_id); end
end
diff --git a/db/post_migrate/20221018232820_add_temp_index_for_user_details_fields.rb b/db/post_migrate/20221018232820_add_temp_index_for_user_details_fields.rb
new file mode 100644
index 00000000000..b46b316981d
--- /dev/null
+++ b/db/post_migrate/20221018232820_add_temp_index_for_user_details_fields.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class AddTempIndexForUserDetailsFields < Gitlab::Database::Migration[2.0]
+ INDEX_NAME = 'tmp_idx_where_user_details_fields_filled'
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :users, :id, name: INDEX_NAME, where: <<~QUERY
+ (COALESCE(linkedin, '') IS DISTINCT FROM '')
+ OR (COALESCE(twitter, '') IS DISTINCT FROM '')
+ OR (COALESCE(skype, '') IS DISTINCT FROM '')
+ OR (COALESCE(website_url, '') IS DISTINCT FROM '')
+ OR (COALESCE(location, '') IS DISTINCT FROM '')
+ OR (COALESCE(organization, '') IS DISTINCT FROM '')
+ QUERY
+ end
+
+ def down
+ remove_concurrent_index_by_name :users, INDEX_NAME
+ end
+end
diff --git a/db/post_migrate/20221019002459_queue_backfill_user_details_fields.rb b/db/post_migrate/20221019002459_queue_backfill_user_details_fields.rb
new file mode 100644
index 00000000000..8ed4416a98d
--- /dev/null
+++ b/db/post_migrate/20221019002459_queue_backfill_user_details_fields.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class QueueBackfillUserDetailsFields < Gitlab::Database::Migration[2.0]
+ MIGRATION = 'BackfillUserDetailsFields'
+ INTERVAL = 2.minutes
+
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
+ def up
+ queue_batched_background_migration(MIGRATION, :users, :id, job_interval: INTERVAL)
+ end
+
+ def down
+ delete_batched_background_migration(MIGRATION, :users, :id, [])
+ end
+end
diff --git a/db/schema_migrations/20221018232820 b/db/schema_migrations/20221018232820
new file mode 100644
index 00000000000..870de8adb4a
--- /dev/null
+++ b/db/schema_migrations/20221018232820
@@ -0,0 +1 @@
+cdf3e65f07f700617f47435b79743b4b35307f47cf46a9696350e55af1774d42 \ No newline at end of file
diff --git a/db/schema_migrations/20221019002459 b/db/schema_migrations/20221019002459
new file mode 100644
index 00000000000..cab21003736
--- /dev/null
+++ b/db/schema_migrations/20221019002459
@@ -0,0 +1 @@
+6c3fe5bf01ac9e74f142ddb3e093867b62cf430f24ba885f8475ccf7f73899cb \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index e23b88fe4fd..c06dedf9115 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -31130,6 +31130,8 @@ CREATE INDEX tmp_idx_project_features_on_releases_al_and_repo_al_partial ON proj
CREATE INDEX tmp_idx_vulnerabilities_on_id_where_report_type_7_99 ON vulnerabilities USING btree (id) WHERE (report_type = ANY (ARRAY[7, 99]));
+CREATE INDEX tmp_idx_where_user_details_fields_filled ON users USING btree (id) WHERE (((COALESCE(linkedin, ''::character varying))::text IS DISTINCT FROM ''::text) OR ((COALESCE(twitter, ''::character varying))::text IS DISTINCT FROM ''::text) OR ((COALESCE(skype, ''::character varying))::text IS DISTINCT FROM ''::text) OR ((COALESCE(website_url, ''::character varying))::text IS DISTINCT FROM ''::text) OR ((COALESCE(location, ''::character varying))::text IS DISTINCT FROM ''::text) OR ((COALESCE(organization, ''::character varying))::text IS DISTINCT FROM ''::text));
+
CREATE INDEX tmp_index_ci_job_artifacts_on_expire_at_where_locked_unknown ON ci_job_artifacts USING btree (expire_at, job_id) WHERE ((locked = 2) AND (expire_at IS NOT NULL));
CREATE INDEX tmp_index_ci_job_artifacts_on_id_expire_at_file_type_trace ON ci_job_artifacts USING btree (id) WHERE (((date_part('day'::text, timezone('UTC'::text, expire_at)) = ANY (ARRAY[(21)::double precision, (22)::double precision, (23)::double precision])) AND (date_part('minute'::text, timezone('UTC'::text, expire_at)) = ANY (ARRAY[(0)::double precision, (30)::double precision, (45)::double precision])) AND (date_part('second'::text, timezone('UTC'::text, expire_at)) = (0)::double precision)) OR (file_type = 3));
diff --git a/doc/.markdownlint/require_helper.js b/doc/.markdownlint/require_helper.js
new file mode 100644
index 00000000000..7d06cf67419
--- /dev/null
+++ b/doc/.markdownlint/require_helper.js
@@ -0,0 +1,14 @@
+/**
+ * Look up the global node modules directory.
+ *
+ * Because we install markdownlint packages globally
+ * in the Docker image where this runs, we need to
+ * provide the path to the global install location
+ * when referencing global functions from our own node
+ * modules.
+ *
+ * Image:
+ * https://gitlab.com/gitlab-org/gitlab-docs/-/blob/main/dockerfiles/gitlab-docs-lint-markdown.Dockerfile
+ */
+const { execSync } = require('child_process');
+module.exports.globalPath = execSync('yarn global dir').toString().trim() + '/node_modules/';
diff --git a/doc/.markdownlint/rules/tabs_blank_lines.js b/doc/.markdownlint/rules/tabs_blank_lines.js
new file mode 100644
index 00000000000..e0e2c1a0a9b
--- /dev/null
+++ b/doc/.markdownlint/rules/tabs_blank_lines.js
@@ -0,0 +1,26 @@
+const { globalPath } = require('../require_helper');
+const {
+ forEachLine,
+ getLineMetadata,
+ isBlankLine,
+} = require(`${globalPath}/markdownlint-rule-helpers`);
+
+module.exports = {
+ names: ['tabs-blank-lines'],
+ description: 'Tab elements must be surrounded by blank lines',
+ tags: ['gitlab-docs', 'tabs'],
+ function: (params, onError) => {
+ const tabElements = ['::Tabs', '::EndTabs', ':::TabTitle'];
+ forEachLine(getLineMetadata(params), (line, lineIndex) => {
+ const lineHasTab = tabElements.includes(line.split(' ')[0]);
+ const prevLine = params.lines[lineIndex - 1];
+ const nextLine = params.lines[lineIndex + 1];
+
+ if (lineHasTab && (!isBlankLine(prevLine) || !isBlankLine(nextLine))) {
+ onError({
+ lineNumber: lineIndex + 1,
+ });
+ }
+ });
+ },
+};
diff --git a/doc/.markdownlint/rules/tabs_title_markup.js b/doc/.markdownlint/rules/tabs_title_markup.js
new file mode 100644
index 00000000000..9c1de1e630d
--- /dev/null
+++ b/doc/.markdownlint/rules/tabs_title_markup.js
@@ -0,0 +1,31 @@
+const { globalPath } = require('../require_helper');
+const { forEachLine, getLineMetadata } = require(`${globalPath}/markdownlint-rule-helpers`);
+
+module.exports = {
+ names: ['tabs-title-markup'],
+ description: 'Incorrect number of colon characters for tag',
+ information: new URL('https://docs.gitlab.com/ee/development/documentation/styleguide/#tabs'),
+ tags: ['gitlab-docs', 'tabs'],
+ function: (params, onError) => {
+ // Note the correct number of colons in each tab tag type.
+ const wrapperColons = 2;
+ const titleColons = 3;
+
+ forEachLine(getLineMetadata(params), (line, lineIndex) => {
+ // Get the number of colons in this line.
+ const colonCount = [...line].filter((x) => x === ':').length;
+
+ // Throw an error in the case of a mismatch.
+ if (
+ ((line.includes(':Tabs') || line.includes(':EndTabs')) && colonCount !== wrapperColons) ||
+ (line.includes(':TabTitle') && colonCount !== titleColons)
+ ) {
+ const correctColonCount = line.includes(':TabTitle') ? wrapperColons : titleColons;
+ onError({
+ lineNumber: lineIndex + 1,
+ detail: `Actual: ${colonCount}; Expected: ${correctColonCount}`,
+ });
+ }
+ });
+ },
+};
diff --git a/doc/.markdownlint/rules/tabs_title_text.js b/doc/.markdownlint/rules/tabs_title_text.js
new file mode 100644
index 00000000000..672aa70f562
--- /dev/null
+++ b/doc/.markdownlint/rules/tabs_title_text.js
@@ -0,0 +1,23 @@
+const { globalPath } = require('../require_helper');
+const {
+ forEachLine,
+ getLineMetadata,
+ isBlankLine,
+} = require(`${globalPath}/markdownlint-rule-helpers`);
+
+module.exports = {
+ names: ['tabs-title-text'],
+ description: 'Tab without title text',
+ information: new URL('https://docs.gitlab.com/ee/development/documentation/styleguide/#tabs'),
+ tags: ['gitlab-docs', 'tabs'],
+ function: (params, onError) => {
+ forEachLine(getLineMetadata(params), (line, lineIndex) => {
+ if (!isBlankLine(line) && line.replace(':::TabTitle', '').trim() === '') {
+ onError({
+ lineNumber: lineIndex + 1,
+ detail: 'Expected: :::TabTitle <your title here>; Actual: :::TabTitle',
+ });
+ }
+ });
+ },
+};
diff --git a/doc/.markdownlint/rules/tabs_wrapper_tags.js b/doc/.markdownlint/rules/tabs_wrapper_tags.js
new file mode 100644
index 00000000000..beacec0b737
--- /dev/null
+++ b/doc/.markdownlint/rules/tabs_wrapper_tags.js
@@ -0,0 +1,21 @@
+module.exports = {
+ names: ['tabs-wrapper-tags'],
+ description: 'Unequal number of tab start and end tags',
+ information: new URL('https://docs.gitlab.com/ee/development/documentation/styleguide/#tabs'),
+ tags: ['gitlab-docs', 'tabs'],
+ function: function rule(params, onError) {
+ const tabStarts = params.lines.filter((line) => line === '::Tabs');
+ const tabEnds = params.lines.filter((line) => line === '::EndTabs');
+
+ if (tabStarts.length !== tabEnds.length) {
+ const errorIndex =
+ params.lines.indexOf('::Tabs') > 0
+ ? params.lines.indexOf('::Tabs')
+ : params.lines.indexOf('::EndTabs');
+ onError({
+ lineNumber: errorIndex + 1,
+ detail: `Opening tags: ${tabStarts.length}; Closing tags: ${tabEnds.length}`,
+ });
+ }
+ },
+};
diff --git a/lib/api/entities/pull_mirror.rb b/lib/api/entities/pull_mirror.rb
index 6914a79b18e..72a5220987e 100644
--- a/lib/api/entities/pull_mirror.rb
+++ b/lib/api/entities/pull_mirror.rb
@@ -3,15 +3,17 @@
module API
module Entities
class PullMirror < Grape::Entity
- expose :id
- expose :status, as: :update_status
- expose :url do |import_state|
+ expose :id, documentation: { type: 'integer', example: 101486 }
+ expose :status, as: :update_status, documentation: { type: 'string', example: 'finished' }
+ expose :url,
+documentation: { type: 'string',
+ example: 'https://*****:*****@gitlab.com/gitlab-org/security/gitlab.git' } do |import_state|
import_state.project.safe_import_url
end
- expose :last_error
- expose :last_update_at
- expose :last_update_started_at
- expose :last_successful_update_at
+ expose :last_error, documentation: { type: 'string', example: nil }
+ expose :last_update_at, documentation: { type: 'dateTime', example: '2020-01-06T17:32:02.823Z' }
+ expose :last_update_started_at, documentation: { type: 'dateTime', example: '2020-01-06T17:32:02.823Z' }
+ expose :last_successful_update_at, documentation: { type: 'dateTime', example: '2020-01-06T17:32:02.823Z' }
end
end
end
diff --git a/lib/gitlab/background_migration/backfill_user_details_fields.rb b/lib/gitlab/background_migration/backfill_user_details_fields.rb
new file mode 100644
index 00000000000..8d8619256b0
--- /dev/null
+++ b/lib/gitlab/background_migration/backfill_user_details_fields.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # Class that will backfill the following fields from user to user_details
+ # * linkedin
+ # * twitter
+ # * skype
+ # * website_url
+ # * location
+ # * organization
+ class BackfillUserDetailsFields < BatchedMigrationJob
+ operation_name :backfill_user_details_fields
+
+ def perform
+ query = <<~SQL
+ (COALESCE(linkedin, '') IS DISTINCT FROM '')
+ OR (COALESCE(twitter, '') IS DISTINCT FROM '')
+ OR (COALESCE(skype, '') IS DISTINCT FROM '')
+ OR (COALESCE(website_url, '') IS DISTINCT FROM '')
+ OR (COALESCE(location, '') IS DISTINCT FROM '')
+ OR (COALESCE(organization, '') IS DISTINCT FROM '')
+ SQL
+ field_limit = UserDetail::DEFAULT_FIELD_LENGTH
+
+ each_sub_batch(
+ batching_scope: ->(relation) {
+ relation.where(query).select(
+ 'id AS user_id',
+ "substring(COALESCE(linkedin, '') from 1 for #{field_limit}) AS linkedin",
+ "substring(COALESCE(twitter, '') from 1 for #{field_limit}) AS twitter",
+ "substring(COALESCE(skype, '') from 1 for #{field_limit}) AS skype",
+ "substring(COALESCE(website_url, '') from 1 for #{field_limit}) AS website_url",
+ "substring(COALESCE(location, '') from 1 for #{field_limit}) AS location",
+ "substring(COALESCE(organization, '') from 1 for #{field_limit}) AS organization"
+ )
+ }
+ ) do |sub_batch|
+ upsert_user_details_fields(sub_batch)
+ end
+ end
+
+ def upsert_user_details_fields(relation)
+ connection.execute(
+ <<~SQL
+ INSERT INTO user_details (user_id, linkedin, twitter, skype, website_url, location, organization)
+ #{relation.to_sql}
+ ON CONFLICT (user_id)
+ DO UPDATE SET
+ "linkedin" = EXCLUDED."linkedin",
+ "twitter" = EXCLUDED."twitter",
+ "skype" = EXCLUDED."skype",
+ "website_url" = EXCLUDED."website_url",
+ "location" = EXCLUDED."location",
+ "organization" = EXCLUDED."organization"
+ SQL
+ )
+ end
+ end
+ end
+end
diff --git a/lib/sidebars/projects/menus/deployments_menu.rb b/lib/sidebars/projects/menus/deployments_menu.rb
index 24e58e71023..9904d533f47 100644
--- a/lib/sidebars/projects/menus/deployments_menu.rb
+++ b/lib/sidebars/projects/menus/deployments_menu.rb
@@ -6,8 +6,8 @@ module Sidebars
class DeploymentsMenu < ::Sidebars::Menu
override :configure_menu_items
def configure_menu_items
- add_item(feature_flags_menu_item)
add_item(environments_menu_item)
+ add_item(feature_flags_menu_item)
add_item(releases_menu_item)
true
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 132e6b1080d..fcfb4f8ef26 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -16615,9 +16615,6 @@ msgstr ""
msgid "Failed to update the Canary Ingress."
msgstr ""
-msgid "Failed to update."
-msgstr ""
-
msgid "Failed to upgrade."
msgstr ""
@@ -48761,6 +48758,9 @@ msgstr ""
msgid "must be unique by status and elapsed time within a policy"
msgstr ""
+msgid "must belong to same project of the work item."
+msgstr ""
+
msgid "must have a repository"
msgstr ""
diff --git a/package.json b/package.json
index 826c7ca9676..100c111fb0b 100644
--- a/package.json
+++ b/package.json
@@ -54,7 +54,7 @@
"@gitlab/at.js": "1.5.7",
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/svgs": "3.5.0",
- "@gitlab/ui": "49.2.0",
+ "@gitlab/ui": "49.2.1",
"@gitlab/visual-review-tools": "1.7.3",
"@gitlab/web-ide": "0.0.1-dev-20220815034418",
"@rails/actioncable": "6.1.4-7",
diff --git a/scripts/lint-doc.sh b/scripts/lint-doc.sh
index f954b2d8106..68dfac95ef6 100755
--- a/scripts/lint-doc.sh
+++ b/scripts/lint-doc.sh
@@ -151,7 +151,7 @@ if [ -z "${MD_DOC_PATH}" ]
then
echo "Merged results pipeline detected, but no markdown files found. Skipping."
else
- run_locally_or_in_docker 'markdownlint' "--config .markdownlint.yml ${MD_DOC_PATH}"
+ run_locally_or_in_docker 'markdownlint' "--config .markdownlint.yml ${MD_DOC_PATH} --rules doc/.markdownlint/rules"
fi
echo '=> Linting prose...'
diff --git a/spec/factories/project_hooks.rb b/spec/factories/project_hooks.rb
index dbb5c357acb..946b3925ee9 100644
--- a/spec/factories/project_hooks.rb
+++ b/spec/factories/project_hooks.rb
@@ -6,6 +6,10 @@ FactoryBot.define do
enable_ssl_verification { false }
project
+ trait :url_variables do
+ url_variables { { 'abc' => 'supers3cret' } }
+ end
+
trait :token do
token { generate(:token) }
end
diff --git a/spec/frontend/behaviors/markdown/render_sandboxed_mermaid_spec.js b/spec/frontend/behaviors/markdown/render_sandboxed_mermaid_spec.js
index de0e5063e49..2b9442162aa 100644
--- a/spec/frontend/behaviors/markdown/render_sandboxed_mermaid_spec.js
+++ b/spec/frontend/behaviors/markdown/render_sandboxed_mermaid_spec.js
@@ -1,127 +1,34 @@
-import { createWrapper } from '@vue/test-utils';
-import { __ } from '~/locale';
+import $ from 'jquery';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import renderMermaid, {
- MAX_CHAR_LIMIT,
- MAX_MERMAID_BLOCK_LIMIT,
- LAZY_ALERT_SHOWN_CLASS,
-} from '~/behaviors/markdown/render_sandboxed_mermaid';
+import renderMermaid from '~/behaviors/markdown/render_sandboxed_mermaid';
-describe('Mermaid diagrams renderer', () => {
- // Finders
- const findMermaidIframes = () => document.querySelectorAll('iframe[src="/-/sandbox/mermaid"]');
- const findDangerousMermaidAlert = () =>
- createWrapper(document.querySelector('[data-testid="alert-warning"]'));
+describe('Render mermaid diagrams for Gitlab Flavoured Markdown', () => {
+ it('Does something', () => {
+ document.body.dataset.page = '';
+ setHTMLFixture(`
+ <div class="gl-relative markdown-code-block js-markdown-code">
+ <pre data-sourcepos="1:1-7:3" class="code highlight js-syntax-highlight language-mermaid white" lang="mermaid" id="code-4">
+ <code class="js-render-mermaid">
+ <span id="LC1" class="line" lang="mermaid">graph TD;</span>
+ <span id="LC2" class="line" lang="mermaid">A--&gt;B</span>
+ <span id="LC3" class="line" lang="mermaid">A--&gt;C</span>
+ <span id="LC4" class="line" lang="mermaid">B--&gt;D</span>
+ <span id="LC5" class="line" lang="mermaid">C--&gt;D</span>
+ </code>
+ </pre>
+ <copy-code>
+ <button type="button" class="btn btn-default btn-md gl-button btn-icon has-tooltip" data-title="Copy to clipboard" data-clipboard-target="pre#code-4">
+ <svg><use xlink:href="/assets/icons-7f1680a3670112fe4c8ef57b9dfb93f0f61b43a2a479d7abd6c83bcb724b9201.svg#copy-to-clipboard"></use></svg>
+ </button>
+ </copy-code>
+ </div>`);
+ const els = $('pre.js-syntax-highlight').find('.js-render-mermaid');
+
+ renderMermaid(els);
- // Helpers
- const renderDiagrams = () => {
- renderMermaid([...document.querySelectorAll('.js-render-mermaid')]);
jest.runAllTimers();
- };
-
- beforeEach(() => {
- document.body.dataset.page = '';
- });
+ expect(document.querySelector('pre.js-syntax-highlight').classList).toContain('gl-sr-only');
- afterEach(() => {
resetHTMLFixture();
});
-
- it('renders a mermaid diagram', () => {
- setHTMLFixture('<pre><code class="js-render-mermaid"></code></pre>');
-
- expect(findMermaidIframes()).toHaveLength(0);
-
- renderDiagrams();
-
- expect(document.querySelector('pre').classList).toContain('gl-sr-only');
- expect(findMermaidIframes()).toHaveLength(1);
- });
-
- describe('within a details element', () => {
- beforeEach(() => {
- setHTMLFixture('<details><pre><code class="js-render-mermaid"></code></pre></details>');
- renderDiagrams();
- });
-
- it('does not render the diagram on load', () => {
- expect(findMermaidIframes()).toHaveLength(0);
- });
-
- it('render the diagram when the details element is opened', () => {
- document.querySelector('details').setAttribute('open', true);
- document.querySelector('details').dispatchEvent(new Event('toggle'));
- jest.runAllTimers();
-
- expect(findMermaidIframes()).toHaveLength(1);
- });
- });
-
- describe('dangerous diagrams', () => {
- describe(`when the diagram's source exceeds ${MAX_CHAR_LIMIT} characters`, () => {
- beforeEach(() => {
- setHTMLFixture(
- `<pre>
- <code class="js-render-mermaid">${Array(MAX_CHAR_LIMIT + 1)
- .fill('a')
- .join('')}</code>
- </pre>`,
- );
- renderDiagrams();
- });
- it('does not render the diagram on load', () => {
- expect(findMermaidIframes()).toHaveLength(0);
- });
-
- it('shows a warning about performance impact when rendering the diagram', () => {
- expect(document.querySelector('pre').classList).toContain(LAZY_ALERT_SHOWN_CLASS);
- expect(findDangerousMermaidAlert().exists()).toBe(true);
- expect(findDangerousMermaidAlert().text()).toContain(
- __('Warning: Displaying this diagram might cause performance issues on this page.'),
- );
- });
-
- it("renders the diagram when clicking on the alert's button", () => {
- findDangerousMermaidAlert().find('button').trigger('click');
- jest.runAllTimers();
-
- expect(findMermaidIframes()).toHaveLength(1);
- });
- });
-
- it(`stops rendering diagrams once the total rendered source exceeds ${MAX_CHAR_LIMIT} characters`, () => {
- setHTMLFixture(
- `<pre>
- <code class="js-render-mermaid">${Array(MAX_CHAR_LIMIT - 1)
- .fill('a')
- .join('')}</code>
- <code class="js-render-mermaid">2</code>
- <code class="js-render-mermaid">3</code>
- <code class="js-render-mermaid">4</code>
- </pre>`,
- );
- renderDiagrams();
-
- expect(findMermaidIframes()).toHaveLength(3);
- });
-
- // Note: The test case below is provided for convenience but should remain skipped as the DOM
- // operations it requires are too expensive and would significantly slow down the test suite.
- // eslint-disable-next-line jest/no-disabled-tests
- it.skip(`stops rendering diagrams when the rendered diagrams count exceeds ${MAX_MERMAID_BLOCK_LIMIT}`, () => {
- setHTMLFixture(
- `<pre>
- ${Array(MAX_MERMAID_BLOCK_LIMIT + 1)
- .fill('<code class="js-render-mermaid"></code>')
- .join('')}
- </pre>`,
- );
- renderDiagrams();
-
- expect([...document.querySelectorAll('.js-render-mermaid')]).toHaveLength(
- MAX_MERMAID_BLOCK_LIMIT + 1,
- );
- expect(findMermaidIframes()).toHaveLength(MAX_MERMAID_BLOCK_LIMIT);
- });
- });
});
diff --git a/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js b/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
index 680dbd68493..bedf8bcaf34 100644
--- a/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
+++ b/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
@@ -201,18 +201,20 @@ describe('RelatedIssuesRoot', () => {
]);
});
- it('displays a message from the backend upon error', async () => {
+ it('passes an error message from the backend upon error', async () => {
const input = '#123';
const message = 'error';
mock.onPost(defaultProps.endpoint).reply(409, { message });
wrapper.vm.store.setPendingReferences([issuable1.reference, issuable2.reference]);
- expect(createAlert).not.toHaveBeenCalled();
+ expect(findRelatedIssuesBlock().props('hasError')).toBe(false);
+ expect(findRelatedIssuesBlock().props('itemAddFailureMessage')).toBe(null);
findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', input);
await waitForPromises();
- expect(createAlert).toHaveBeenCalledWith({ message });
+ expect(findRelatedIssuesBlock().props('hasError')).toBe(true);
+ expect(findRelatedIssuesBlock().props('itemAddFailureMessage')).toBe(message);
});
});
diff --git a/spec/frontend/webhooks/components/form_url_app_spec.js b/spec/frontend/webhooks/components/form_url_app_spec.js
index 16e0a3f549e..76812097748 100644
--- a/spec/frontend/webhooks/components/form_url_app_spec.js
+++ b/spec/frontend/webhooks/components/form_url_app_spec.js
@@ -60,8 +60,10 @@ describe('FormUrlApp', () => {
expect(findAllUrlMaskItems()).toHaveLength(1);
const firstItem = findAllUrlMaskItems().at(0);
- expect(firstItem.props('itemKey')).toBeNull();
- expect(firstItem.props('itemValue')).toBeNull();
+ expect(firstItem.props()).toMatchObject({
+ itemKey: null,
+ itemValue: null,
+ });
});
});
@@ -90,12 +92,18 @@ describe('FormUrlApp', () => {
expect(findAllUrlMaskItems()).toHaveLength(2);
const firstItem = findAllUrlMaskItems().at(0);
- expect(firstItem.props('itemKey')).toBe(mockItem1.key);
- expect(firstItem.props('itemValue')).toBe(mockItem1.value);
+ expect(firstItem.props()).toMatchObject({
+ itemKey: mockItem1.key,
+ itemValue: mockItem1.value,
+ isEditing: true,
+ });
const secondItem = findAllUrlMaskItems().at(1);
- expect(secondItem.props('itemKey')).toBe(mockItem2.key);
- expect(secondItem.props('itemValue')).toBe(mockItem2.value);
+ expect(secondItem.props()).toMatchObject({
+ itemKey: mockItem2.key,
+ itemValue: mockItem2.value,
+ isEditing: true,
+ });
});
describe('on mask item input', () => {
@@ -106,8 +114,10 @@ describe('FormUrlApp', () => {
firstItem.vm.$emit('input', mockInput);
await nextTick();
- expect(firstItem.props('itemKey')).toBe(mockInput.key);
- expect(firstItem.props('itemValue')).toBe(mockInput.value);
+ expect(firstItem.props()).toMatchObject({
+ itemKey: mockInput.key,
+ itemValue: mockInput.value,
+ });
});
});
@@ -119,8 +129,10 @@ describe('FormUrlApp', () => {
expect(findAllUrlMaskItems()).toHaveLength(3);
const lastItem = findAllUrlMaskItems().at(-1);
- expect(lastItem.props('itemKey')).toBeNull();
- expect(lastItem.props('itemValue')).toBeNull();
+ expect(lastItem.props()).toMatchObject({
+ itemKey: null,
+ itemValue: null,
+ });
});
});
@@ -133,8 +145,10 @@ describe('FormUrlApp', () => {
expect(findAllUrlMaskItems()).toHaveLength(1);
const newFirstItem = findAllUrlMaskItems().at(0);
- expect(newFirstItem.props('itemKey')).toBe(mockItem2.key);
- expect(newFirstItem.props('itemValue')).toBe(mockItem2.value);
+ expect(newFirstItem.props()).toMatchObject({
+ itemKey: mockItem2.key,
+ itemValue: mockItem2.value,
+ });
});
});
});
diff --git a/spec/frontend/webhooks/components/form_url_mask_item_spec.js b/spec/frontend/webhooks/components/form_url_mask_item_spec.js
index ab028ef2997..a6323928d22 100644
--- a/spec/frontend/webhooks/components/form_url_mask_item_spec.js
+++ b/spec/frontend/webhooks/components/form_url_mask_item_spec.js
@@ -31,19 +31,42 @@ describe('FormUrlMaskItem', () => {
describe('template', () => {
it('renders input for key and value', () => {
- createComponent();
+ createComponent({ props: { itemKey: mockKey, itemValue: mockValue } });
const keyInput = findMaskItemKey();
expect(keyInput.attributes('label')).toBe(FormUrlMaskItem.i18n.keyLabel);
- expect(keyInput.findComponent(GlFormInput).attributes('name')).toBe(
- 'hook[url_variables][][key]',
- );
+ expect(keyInput.findComponent(GlFormInput).attributes()).toMatchObject({
+ name: 'hook[url_variables][][key]',
+ value: mockKey,
+ });
const valueInput = findMaskItemValue();
expect(valueInput.attributes('label')).toBe(FormUrlMaskItem.i18n.valueLabel);
- expect(valueInput.findComponent(GlFormInput).attributes('name')).toBe(
- 'hook[url_variables][][value]',
- );
+ expect(valueInput.findComponent(GlFormInput).attributes()).toMatchObject({
+ name: 'hook[url_variables][][value]',
+ value: mockValue,
+ });
+ });
+
+ describe('when isEditing is true', () => {
+ beforeEach(() => {
+ createComponent({ props: { isEditing: true } });
+ });
+
+ it('renders disabled key and value', () => {
+ expect(findMaskItemKey().findComponent(GlFormInput).attributes('disabled')).toBe('true');
+ expect(findMaskItemValue().findComponent(GlFormInput).attributes('disabled')).toBe('true');
+ });
+
+ it('renders disabled remove button', () => {
+ expect(findRemoveButton().attributes('disabled')).toBe('true');
+ });
+
+ it('displays ************ as input value', () => {
+ expect(findMaskItemValue().findComponent(GlFormInput).attributes('value')).toBe(
+ '************',
+ );
+ });
});
describe('on key input', () => {
diff --git a/spec/helpers/hooks_helper_spec.rb b/spec/helpers/hooks_helper_spec.rb
index 8f438a3ddc8..28f6322466f 100644
--- a/spec/helpers/hooks_helper_spec.rb
+++ b/spec/helpers/hooks_helper_spec.rb
@@ -3,16 +3,33 @@
require 'spec_helper'
RSpec.describe HooksHelper do
- let(:project) { create(:project) }
- let(:project_hook) { create(:project_hook, project: project) }
- let(:service_hook) { create(:service_hook, integration: create(:drone_ci_integration)) }
- let(:system_hook) { create(:system_hook) }
+ let(:project) { build_stubbed(:project) }
+ let(:project_hook) { build_stubbed(:project_hook, project: project) }
+ let(:service_hook) { build_stubbed(:service_hook, integration: build_stubbed(:drone_ci_integration)) }
+ let(:system_hook) { build_stubbed(:system_hook) }
describe '#webhook_form_data' do
subject { helper.webhook_form_data(project_hook) }
- it { expect(subject[:url]).to eq(project_hook.url) }
- it { expect(subject[:url_variables]).to be_nil }
+ context 'when there are no URL variables' do
+ it 'returns proper data' do
+ expect(subject).to match(
+ url: project_hook.url,
+ url_variables: Gitlab::Json.dump([])
+ )
+ end
+ end
+
+ context 'when there are URL variables' do
+ let(:project_hook) { build_stubbed(:project_hook, :url_variables, project: project) }
+
+ it 'returns proper data' do
+ expect(subject).to match(
+ url: project_hook.url,
+ url_variables: Gitlab::Json.dump(['abc'])
+ )
+ end
+ end
end
describe '#link_to_test_hook' do
@@ -31,7 +48,7 @@ RSpec.describe HooksHelper do
describe '#hook_log_path' do
context 'with a project hook' do
- let(:web_hook_log) { create(:web_hook_log, web_hook: project_hook) }
+ let(:web_hook_log) { build_stubbed(:web_hook_log, web_hook: project_hook) }
it 'returns project-namespaced link' do
expect(helper.hook_log_path(project_hook, web_hook_log))
@@ -40,7 +57,7 @@ RSpec.describe HooksHelper do
end
context 'with a service hook' do
- let(:web_hook_log) { create(:web_hook_log, web_hook: service_hook) }
+ let(:web_hook_log) { build_stubbed(:web_hook_log, web_hook: service_hook) }
it 'returns project-namespaced link' do
expect(helper.hook_log_path(project_hook, web_hook_log))
@@ -49,7 +66,7 @@ RSpec.describe HooksHelper do
end
context 'with a system hook' do
- let(:web_hook_log) { create(:web_hook_log, web_hook: system_hook) }
+ let(:web_hook_log) { build_stubbed(:web_hook_log, web_hook: system_hook) }
it 'returns admin-namespaced link' do
expect(helper.hook_log_path(system_hook, web_hook_log))
diff --git a/spec/lib/gitlab/background_migration/backfill_user_details_fields_spec.rb b/spec/lib/gitlab/background_migration/backfill_user_details_fields_spec.rb
new file mode 100644
index 00000000000..04ada1703bc
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_user_details_fields_spec.rb
@@ -0,0 +1,222 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BackfillUserDetailsFields, :migration, schema: 20221018232820 do
+ let(:users) { table(:users) }
+ let(:user_details) { table(:user_details) }
+
+ let!(:user_all_fields_backfill) do
+ users.create!(
+ name: generate(:name),
+ email: generate(:email),
+ projects_limit: 1,
+ linkedin: 'linked-in',
+ twitter: '@twitter',
+ skype: 'skype',
+ website_url: 'https://example.com',
+ location: 'Antarctica',
+ organization: 'Gitlab'
+ )
+ end
+
+ let!(:user_long_details_fields) do
+ length = UserDetail::DEFAULT_FIELD_LENGTH + 1
+ users.create!(
+ name: generate(:name),
+ email: generate(:email),
+ projects_limit: 1,
+ linkedin: 'l' * length,
+ twitter: 't' * length,
+ skype: 's' * length,
+ website_url: "https://#{'a' * (length - 12)}.com",
+ location: 'l' * length,
+ organization: 'o' * length
+ )
+ end
+
+ let!(:user_nil_details_fields) do
+ users.create!(
+ name: generate(:name),
+ email: generate(:email),
+ projects_limit: 1
+ )
+ end
+
+ let!(:user_empty_details_fields) do
+ users.create!(
+ name: generate(:name),
+ email: generate(:email),
+ projects_limit: 1,
+ linkedin: '',
+ twitter: '',
+ skype: '',
+ website_url: '',
+ location: '',
+ organization: ''
+ )
+ end
+
+ let!(:user_with_bio) do
+ users.create!(
+ name: generate(:name),
+ email: generate(:email),
+ projects_limit: 1,
+ linkedin: 'linked-in',
+ twitter: '@twitter',
+ skype: 'skype',
+ website_url: 'https://example.com',
+ location: 'Antarctica',
+ organization: 'Gitlab'
+ )
+ end
+
+ let!(:bio_user_details) do
+ user_details
+ .find_or_create_by!(user_id: user_with_bio.id)
+ .update!(bio: 'bio')
+ end
+
+ let!(:user_with_details) do
+ users.create!(
+ name: generate(:name),
+ email: generate(:email),
+ projects_limit: 1,
+ linkedin: 'linked-in',
+ twitter: '@twitter',
+ skype: 'skype',
+ website_url: 'https://example.com',
+ location: 'Antarctica',
+ organization: 'Gitlab'
+ )
+ end
+
+ let!(:existing_user_details) do
+ user_details
+ .find_or_create_by!(user_id: user_with_details.id)
+ .update!(
+ linkedin: 'linked-in',
+ twitter: '@twitter',
+ skype: 'skype',
+ website_url: 'https://example.com',
+ location: 'Antarctica',
+ organization: 'Gitlab'
+ )
+ end
+
+ let!(:user_different_details) do
+ users.create!(
+ name: generate(:name),
+ email: generate(:email),
+ projects_limit: 1,
+ linkedin: 'linked-in',
+ twitter: '@twitter',
+ skype: 'skype',
+ website_url: 'https://example.com',
+ location: 'Antarctica',
+ organization: 'Gitlab'
+ )
+ end
+
+ let!(:differing_details) do
+ user_details
+ .find_or_create_by!(user_id: user_different_details.id)
+ .update!(
+ linkedin: 'details-in',
+ twitter: '@details',
+ skype: 'details_skype',
+ website_url: 'https://details.site',
+ location: 'Details Location',
+ organization: 'Details Organization'
+ )
+ end
+
+ let(:user_ids) do
+ [
+ user_all_fields_backfill,
+ user_long_details_fields,
+ user_nil_details_fields,
+ user_empty_details_fields,
+ user_with_bio,
+ user_with_details,
+ user_different_details
+ ].map(&:id)
+ end
+
+ subject do
+ described_class.new(
+ start_id: user_ids.min,
+ end_id: user_ids.max,
+ batch_table: 'users',
+ batch_column: 'id',
+ sub_batch_size: 1_000,
+ pause_ms: 0,
+ connection: ApplicationRecord.connection
+ )
+ end
+
+ it 'processes all relevant records' do
+ expect { subject.perform }.to change { user_details.all.size }.to(5)
+ end
+
+ it 'backfills new user_details fields' do
+ subject.perform
+
+ user_detail = user_details.find_by!(user_id: user_all_fields_backfill.id)
+ expect(user_detail.linkedin).to eq('linked-in')
+ expect(user_detail.twitter).to eq('@twitter')
+ expect(user_detail.skype).to eq('skype')
+ expect(user_detail.website_url).to eq('https://example.com')
+ expect(user_detail.location).to eq('Antarctica')
+ expect(user_detail.organization).to eq('Gitlab')
+ end
+
+ it 'does not migrate nil fields' do
+ subject.perform
+
+ expect(user_details.find_by(user_id: user_nil_details_fields)).to be_nil
+ end
+
+ it 'does not migrate empty fields' do
+ subject.perform
+
+ expect(user_details.find_by(user_id: user_empty_details_fields)).to be_nil
+ end
+
+ it 'backfills new fields without overwriting existing `bio` field' do
+ subject.perform
+
+ user_detail = user_details.find_by!(user_id: user_with_bio.id)
+ expect(user_detail.bio).to eq('bio')
+ expect(user_detail.linkedin).to eq('linked-in')
+ expect(user_detail.twitter).to eq('@twitter')
+ expect(user_detail.skype).to eq('skype')
+ expect(user_detail.website_url).to eq('https://example.com')
+ expect(user_detail.location).to eq('Antarctica')
+ expect(user_detail.organization).to eq('Gitlab')
+ end
+
+ context 'when user details are unchanged' do
+ it 'does not change existing details' do
+ expect { subject.perform }.not_to change {
+ user_details.find_by!(user_id: user_with_details.id).attributes
+ }
+ end
+ end
+
+ context 'when user details are changed' do
+ it 'updates existing user details' do
+ expect { subject.perform }.to change {
+ user_details.find_by!(user_id: user_different_details.id).attributes
+ }
+
+ user_detail = user_details.find_by!(user_id: user_different_details.id)
+ expect(user_detail.linkedin).to eq('linked-in')
+ expect(user_detail.twitter).to eq('@twitter')
+ expect(user_detail.skype).to eq('skype')
+ expect(user_detail.website_url).to eq('https://example.com')
+ expect(user_detail.location).to eq('Antarctica')
+ expect(user_detail.organization).to eq('Gitlab')
+ end
+ end
+end
diff --git a/spec/migrations/queue_backfill_user_details_fields_spec.rb b/spec/migrations/queue_backfill_user_details_fields_spec.rb
new file mode 100644
index 00000000000..388ac6d1bce
--- /dev/null
+++ b/spec/migrations/queue_backfill_user_details_fields_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe QueueBackfillUserDetailsFields do
+ let_it_be(:batched_migration) { described_class::MIGRATION }
+
+ it 'schedules a new batched migration' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(batched_migration).not_to have_scheduled_batched_migration
+ }
+
+ migration.after -> {
+ expect(batched_migration).to have_scheduled_batched_migration(
+ table_name: :users,
+ column_name: :id,
+ interval: described_class::INTERVAL
+ )
+ }
+ end
+ end
+end
diff --git a/spec/services/clusters/applications/patch_service_spec.rb b/spec/services/clusters/applications/patch_service_spec.rb
deleted file mode 100644
index 281da62b80b..00000000000
--- a/spec/services/clusters/applications/patch_service_spec.rb
+++ /dev/null
@@ -1,80 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Applications::PatchService do
- describe '#execute' do
- let(:application) { create(:clusters_applications_knative, :scheduled) }
- let!(:update_command) { application.update_command }
- let(:service) { described_class.new(application) }
- let(:helm_client) { instance_double(Gitlab::Kubernetes::Helm::API) }
-
- before do
- allow(service).to receive(:update_command).and_return(update_command)
- allow(service).to receive(:helm_api).and_return(helm_client)
- end
-
- context 'when there are no errors' do
- before do
- expect(helm_client).to receive(:update).with(update_command)
- allow(ClusterWaitForAppInstallationWorker).to receive(:perform_in).and_return(nil)
- end
-
- it 'make the application updating' do
- expect(application.cluster).not_to be_nil
- service.execute
-
- expect(application).to be_updating
- end
-
- it 'schedule async installation status check' do
- expect(ClusterWaitForAppInstallationWorker).to receive(:perform_in).once
-
- service.execute
- end
- end
-
- context 'when kubernetes cluster communication fails' do
- let(:error) { Kubeclient::HttpError.new(500, 'system failure', nil) }
-
- before do
- expect(helm_client).to receive(:update).with(update_command).and_raise(error)
- end
-
- include_examples 'logs kubernetes errors' do
- let(:error_name) { 'Kubeclient::HttpError' }
- let(:error_message) { 'system failure' }
- let(:error_code) { 500 }
- end
-
- it 'make the application errored' do
- service.execute
-
- expect(application).to be_update_errored
- expect(application.status_reason).to eq(_('Kubernetes error: %{error_code}') % { error_code: 500 })
- end
- end
-
- context 'a non kubernetes error happens' do
- let(:application) { create(:clusters_applications_knative, :scheduled) }
- let(:error) { StandardError.new('something bad happened') }
-
- include_examples 'logs kubernetes errors' do
- let(:error_name) { 'StandardError' }
- let(:error_message) { 'something bad happened' }
- let(:error_code) { nil }
- end
-
- before do
- expect(helm_client).to receive(:update).with(update_command).and_raise(error)
- end
-
- it 'make the application errored' do
- service.execute
-
- expect(application).to be_update_errored
- expect(application.status_reason).to eq(_('Failed to update.'))
- end
- end
- end
-end
diff --git a/spec/services/clusters/applications/update_service_spec.rb b/spec/services/clusters/applications/update_service_spec.rb
deleted file mode 100644
index 4c05a12a4a1..00000000000
--- a/spec/services/clusters/applications/update_service_spec.rb
+++ /dev/null
@@ -1,91 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Applications::UpdateService do
- include TestRequestHelpers
-
- let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- let(:user) { create(:user) }
- let(:params) { { application: 'knative', hostname: 'update.example.com', pages_domain_id: domain.id } }
- let(:service) { described_class.new(cluster, user, params) }
- let(:domain) { create(:pages_domain, :instance_serverless) }
-
- subject { service.execute(test_request) }
-
- describe '#execute' do
- before do
- allow(ClusterPatchAppWorker).to receive(:perform_async)
- end
-
- context 'application is not installed' do
- it 'raises Clusters::Applications::BaseService::InvalidApplicationError' do
- expect(ClusterPatchAppWorker).not_to receive(:perform_async)
-
- expect { subject }
- .to raise_exception { Clusters::Applications::BaseService::InvalidApplicationError }
- .and not_change { Clusters::Applications::Knative.count }
- .and not_change { Clusters::Applications::Knative.with_status(:scheduled).count }
- end
- end
-
- context 'application is installed' do
- context 'application is schedulable' do
- let!(:application) do
- create(:clusters_applications_knative, status: 3, cluster: cluster)
- end
-
- it 'updates the application data' do
- expect do
- subject
- end.to change { application.reload.hostname }.to(params[:hostname])
- end
-
- it 'makes application scheduled!' do
- subject
-
- expect(application.reload).to be_scheduled
- end
-
- it 'schedules ClusterPatchAppWorker' do
- expect(ClusterPatchAppWorker).to receive(:perform_async)
-
- subject
- end
-
- context 'knative application' do
- let(:associate_domain_service) { double('AssociateDomainService') }
-
- it 'executes AssociateDomainService' do
- expect(Serverless::AssociateDomainService).to receive(:new) do |knative, args|
- expect(knative.id).to eq(application.id)
- expect(args[:pages_domain_id]).to eq(params[:pages_domain_id])
- expect(args[:creator]).to eq(user)
-
- associate_domain_service
- end
-
- expect(associate_domain_service).to receive(:execute)
-
- subject
- end
- end
- end
-
- context 'application is not schedulable' do
- let!(:application) do
- create(:clusters_applications_knative, status: 4, cluster: cluster)
- end
-
- it 'raises StateMachines::InvalidTransition' do
- expect(ClusterPatchAppWorker).not_to receive(:perform_async)
-
- expect { subject }
- .to raise_exception { StateMachines::InvalidTransition }
- .and not_change { application.reload.hostname }
- .and not_change { Clusters::Applications::Knative.with_status(:scheduled).count }
- end
- end
- end
- end
-end
diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb
index 138150a371a..af35a5ff068 100644
--- a/spec/support/shared_contexts/navbar_structure_context.rb
+++ b/spec/support/shared_contexts/navbar_structure_context.rb
@@ -67,8 +67,8 @@ RSpec.shared_context 'project navbar structure' do
{
nav_item: _('Deployments'),
nav_sub_items: [
- _('Feature Flags'),
_('Environments'),
+ _('Feature Flags'),
_('Releases')
]
},
diff --git a/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb b/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb
index 31ec25249d7..a764d47d7c0 100644
--- a/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb
+++ b/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb
@@ -38,7 +38,7 @@ RSpec.shared_examples Integrations::HasWebHook do
end
describe '#url_variables' do
- it 'returns a string' do
+ it 'returns a hash' do
expect(integration.url_variables).to be_a(Hash)
end
end
diff --git a/yarn.lock b/yarn.lock
index 2f7bc645901..41e06df5fab 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1113,10 +1113,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.5.0.tgz#226240b7aa93db986f4c6f7738ca2a1846b5234d"
integrity sha512-/djPsJzUY7i/FaydRVt3ZyXiFf5HGNo1rg2mfLn1EpXvT4zc2ag5ECwnYcPb97KgqFCJX6Tk+Ndu8Wh3GoOW1g==
-"@gitlab/ui@49.2.0":
- version "49.2.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-49.2.0.tgz#45eedbe943bccbb6d986d66bf7c6294c82e89366"
- integrity sha512-S7jfYtmh2Z36bum48aqb+NFLl/WAqow5gOXfWjdl1lGXjpKZ27neJPTWfpYi2PRyhmPs8ptVg7zKaxXJMZ7cgA==
+"@gitlab/ui@49.2.1":
+ version "49.2.1"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-49.2.1.tgz#362dda68799d6ecfd32c8e0a4eb1409f20ddec4d"
+ integrity sha512-dutmZTGQDDn7nPzGFtI6YEnqF7yhnD6tY6ymGQ1U0bkdDcjR8GOMvDn3Gc09505go6ESt0A4dXwleboDgoFP0w==
dependencies:
"@popperjs/core" "^2.11.2"
bootstrap-vue "2.20.1"