Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
853c0c530b
commit
5025412fc4
|
@ -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),
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
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">×</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 $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();
|
||||
}
|
||||
|
|
|
@ -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)"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 = '************';
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
cdf3e65f07f700617f47435b79743b4b35307f47cf46a9696350e55af1774d42
|
|
@ -0,0 +1 @@
|
|||
6c3fe5bf01ac9e74f142ddb3e093867b62cf430f24ba885f8475ccf7f73899cb
|
|
@ -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));
|
||||
|
|
|
@ -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/';
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
|
@ -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}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
|
@ -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',
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
|
@ -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}`,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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...'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"]'));
|
||||
|
||||
// Helpers
|
||||
const renderDiagrams = () => {
|
||||
renderMermaid([...document.querySelectorAll('.js-render-mermaid')]);
|
||||
jest.runAllTimers();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
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-->B</span>
|
||||
<span id="LC3" class="line" lang="mermaid">A-->C</span>
|
||||
<span id="LC4" class="line" lang="mermaid">B-->D</span>
|
||||
<span id="LC5" class="line" lang="mermaid">C-->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);
|
||||
|
||||
jest.runAllTimers();
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -67,8 +67,8 @@ RSpec.shared_context 'project navbar structure' do
|
|||
{
|
||||
nav_item: _('Deployments'),
|
||||
nav_sub_items: [
|
||||
_('Feature Flags'),
|
||||
_('Environments'),
|
||||
_('Feature Flags'),
|
||||
_('Releases')
|
||||
]
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue