Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
c724e639a9
commit
5f8d4d631d
113 changed files with 2070 additions and 324 deletions
|
@ -653,7 +653,6 @@
|
|||
.rails:rules:decomposed-databases:
|
||||
rules:
|
||||
- <<: *if-merge-request-run-decomposed
|
||||
allow_failure: true
|
||||
|
||||
.rails:rules:ee-and-foss-migration:
|
||||
rules:
|
||||
|
|
|
@ -81,7 +81,7 @@ MarkdownPreview.prototype.fetchMarkdownPreview = function (text, url, success) {
|
|||
})
|
||||
.catch(() =>
|
||||
createFlash({
|
||||
message: __('An error occurred while fetching markdown preview'),
|
||||
message: __('An error occurred while fetching Markdown preview'),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
|
|
@ -45,6 +45,7 @@ export default () => {
|
|||
new Vue({
|
||||
el,
|
||||
name: 'CycleAnalytics',
|
||||
apolloProvider: {},
|
||||
store,
|
||||
render: (createElement) =>
|
||||
createElement(CycleAnalytics, {
|
||||
|
|
|
@ -25,7 +25,7 @@ export default {
|
|||
lazy: true,
|
||||
},
|
||||
translations: {
|
||||
cronPlaceholder: __('* * * * *'),
|
||||
cronPlaceholder: '* * * * *',
|
||||
cronSyntaxInstructions: __(
|
||||
'Define a custom deploy freeze pattern with %{cronSyntaxStart}cron syntax%{cronSyntaxEnd}',
|
||||
),
|
||||
|
|
|
@ -31,7 +31,7 @@ export const i18n = {
|
|||
title: __('Custom notification events'),
|
||||
bodyTitle: __('Notification events'),
|
||||
bodyMessage: __(
|
||||
'Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notificationLinkStart} notification emails%{notificationLinkEnd}.',
|
||||
'Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notificationLinkStart}notification emails%{notificationLinkEnd}.',
|
||||
),
|
||||
},
|
||||
eventNames: {
|
||||
|
|
|
@ -73,6 +73,7 @@ export const OLDER_THAN_OPTIONS = [
|
|||
{ key: 'SEVEN_DAYS', variable: 7, default: false },
|
||||
{ key: 'FOURTEEN_DAYS', variable: 14, default: false },
|
||||
{ key: 'THIRTY_DAYS', variable: 30, default: false },
|
||||
{ key: 'SIXTY_DAYS', variable: 60, default: false },
|
||||
{ key: 'NINETY_DAYS', variable: 90, default: true },
|
||||
];
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import $ from 'jquery';
|
|||
import '~/profile/gl_crop';
|
||||
import Profile from '~/profile/profile';
|
||||
import initSearchSettings from '~/search_settings';
|
||||
import initPasswordPrompt from './password_prompt';
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
$(document).on('input.ssh_key', '#key_key', function () {
|
||||
|
@ -19,3 +20,4 @@ $(document).on('input.ssh_key', '#key_key', function () {
|
|||
new Profile(); // eslint-disable-line no-new
|
||||
|
||||
initSearchSettings();
|
||||
initPasswordPrompt();
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import { __, s__ } from '~/locale';
|
||||
|
||||
export const I18N_PASSWORD_PROMPT_TITLE = s__('PasswordPrompt|Confirm password to continue');
|
||||
export const I18N_PASSWORD_PROMPT_FORM_LABEL = s__(
|
||||
'PasswordPrompt|Please enter your password to confirm',
|
||||
);
|
||||
export const I18N_PASSWORD_PROMPT_ERROR_MESSAGE = s__('PasswordPrompt|Password is required');
|
||||
export const I18N_PASSWORD_PROMPT_CONFIRM_BUTTON = s__('PasswordPrompt|Confirm password');
|
||||
export const I18N_PASSWORD_PROMPT_CANCEL_BUTTON = __('Cancel');
|
|
@ -0,0 +1,58 @@
|
|||
import Vue from 'vue';
|
||||
import Translate from '~/vue_shared/translate';
|
||||
import PasswordPromptModal from './password_prompt_modal.vue';
|
||||
|
||||
Vue.use(Translate);
|
||||
|
||||
const emailFieldSelector = '#user_email';
|
||||
const editFormSelector = '.js-password-prompt-form';
|
||||
const passwordPromptFieldSelector = '.js-password-prompt-field';
|
||||
const passwordPromptBtnSelector = '.js-password-prompt-btn';
|
||||
|
||||
const passwordPromptModalId = 'password-prompt-modal';
|
||||
|
||||
const getEmailValue = () => document.querySelector(emailFieldSelector).value.trim();
|
||||
const passwordPromptButton = document.querySelector(passwordPromptBtnSelector);
|
||||
const field = document.querySelector(passwordPromptFieldSelector);
|
||||
const form = document.querySelector(editFormSelector);
|
||||
|
||||
const handleConfirmPassword = (pw) => {
|
||||
// update the validation_password field
|
||||
field.value = pw;
|
||||
// submit the form
|
||||
form.submit();
|
||||
};
|
||||
|
||||
export default () => {
|
||||
const passwordPromptModalEl = document.getElementById(passwordPromptModalId);
|
||||
|
||||
if (passwordPromptModalEl && field) {
|
||||
return new Vue({
|
||||
el: passwordPromptModalEl,
|
||||
data() {
|
||||
return {
|
||||
initialEmail: '',
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.initialEmail = getEmailValue();
|
||||
passwordPromptButton.addEventListener('click', this.handleSettingsUpdate);
|
||||
},
|
||||
methods: {
|
||||
handleSettingsUpdate(ev) {
|
||||
const email = getEmailValue();
|
||||
if (email !== this.initialEmail) {
|
||||
ev.preventDefault();
|
||||
this.$root.$emit('bv::show::modal', passwordPromptModalId, passwordPromptBtnSelector);
|
||||
}
|
||||
},
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(PasswordPromptModal, {
|
||||
props: { handleConfirmPassword },
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
return null;
|
||||
};
|
|
@ -0,0 +1,82 @@
|
|||
<script>
|
||||
import { GlModal, GlForm, GlFormGroup, GlFormInput } from '@gitlab/ui';
|
||||
import {
|
||||
I18N_PASSWORD_PROMPT_TITLE,
|
||||
I18N_PASSWORD_PROMPT_FORM_LABEL,
|
||||
I18N_PASSWORD_PROMPT_ERROR_MESSAGE,
|
||||
I18N_PASSWORD_PROMPT_CANCEL_BUTTON,
|
||||
I18N_PASSWORD_PROMPT_CONFIRM_BUTTON,
|
||||
} from './constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlModal,
|
||||
GlForm,
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
},
|
||||
props: {
|
||||
handleConfirmPassword: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
passwordCheck: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isValid() {
|
||||
return Boolean(this.passwordCheck.length);
|
||||
},
|
||||
primaryProps() {
|
||||
return {
|
||||
text: I18N_PASSWORD_PROMPT_CONFIRM_BUTTON,
|
||||
attributes: [{ variant: 'danger' }, { category: 'primary' }, { disabled: !this.isValid }],
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onConfirmPassword() {
|
||||
this.handleConfirmPassword(this.passwordCheck);
|
||||
},
|
||||
},
|
||||
cancelProps: {
|
||||
text: I18N_PASSWORD_PROMPT_CANCEL_BUTTON,
|
||||
},
|
||||
i18n: {
|
||||
title: I18N_PASSWORD_PROMPT_TITLE,
|
||||
formLabel: I18N_PASSWORD_PROMPT_FORM_LABEL,
|
||||
errorMessage: I18N_PASSWORD_PROMPT_ERROR_MESSAGE,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-modal
|
||||
data-testid="password-prompt-modal"
|
||||
modal-id="password-prompt-modal"
|
||||
:title="$options.i18n.title"
|
||||
:action-primary="primaryProps"
|
||||
:action-cancel="$options.cancelProps"
|
||||
@primary="onConfirmPassword"
|
||||
>
|
||||
<gl-form @submit.prevent="onConfirmPassword">
|
||||
<gl-form-group
|
||||
:label="$options.i18n.formLabel"
|
||||
label-for="password-prompt-confirmation"
|
||||
:invalid-feedback="$options.i18n.errorMessage"
|
||||
:state="isValid"
|
||||
>
|
||||
<gl-form-input
|
||||
id="password-prompt-confirmation"
|
||||
v-model="passwordCheck"
|
||||
name="password-confirmation"
|
||||
type="password"
|
||||
data-testid="password-prompt-field"
|
||||
/>
|
||||
</gl-form-group>
|
||||
</gl-form>
|
||||
</gl-modal>
|
||||
</template>
|
|
@ -2,51 +2,51 @@ import { s__ } from '~/locale';
|
|||
|
||||
export const PIPELINE_SOURCES = [
|
||||
{
|
||||
text: s__('Pipeline|Source|Push'),
|
||||
text: s__('PipelineSource|Push'),
|
||||
value: 'push',
|
||||
},
|
||||
{
|
||||
text: s__('Pipeline|Source|Web'),
|
||||
text: s__('PipelineSource|Web'),
|
||||
value: 'web',
|
||||
},
|
||||
{
|
||||
text: s__('Pipeline|Source|Trigger'),
|
||||
text: s__('PipelineSource|Trigger'),
|
||||
value: 'trigger',
|
||||
},
|
||||
{
|
||||
text: s__('Pipeline|Source|Schedule'),
|
||||
text: s__('PipelineSource|Schedule'),
|
||||
value: 'schedule',
|
||||
},
|
||||
{
|
||||
text: s__('Pipeline|Source|API'),
|
||||
text: s__('PipelineSource|API'),
|
||||
value: 'api',
|
||||
},
|
||||
{
|
||||
text: s__('Pipeline|Source|External'),
|
||||
text: s__('PipelineSource|External'),
|
||||
value: 'external',
|
||||
},
|
||||
{
|
||||
text: s__('Pipeline|Source|Pipeline'),
|
||||
text: s__('PipelineSource|Pipeline'),
|
||||
value: 'pipeline',
|
||||
},
|
||||
{
|
||||
text: s__('Pipeline|Source|Chat'),
|
||||
text: s__('PipelineSource|Chat'),
|
||||
value: 'chat',
|
||||
},
|
||||
{
|
||||
text: s__('Pipeline|Source|Web IDE'),
|
||||
text: s__('PipelineSource|Web IDE'),
|
||||
value: 'webide',
|
||||
},
|
||||
{
|
||||
text: s__('Pipeline|Source|Merge Request'),
|
||||
text: s__('PipelineSource|Merge Request'),
|
||||
value: 'merge_request_event',
|
||||
},
|
||||
{
|
||||
text: s__('Pipeline|Source|External Pull Request'),
|
||||
text: s__('PipelineSource|External Pull Request'),
|
||||
value: 'external_pull_request_event',
|
||||
},
|
||||
{
|
||||
text: s__('Pipeline|Source|Parent Pipeline'),
|
||||
text: s__('PipelineSource|Parent Pipeline'),
|
||||
value: 'parent_pipeline',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -123,7 +123,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div id="related-issues" class="related-issues-block">
|
||||
<div id="related-issues" class="related-issues-block gl-mt-5">
|
||||
<div class="card card-slim gl-overflow-hidden">
|
||||
<div
|
||||
:class="{ 'panel-empty-heading border-bottom-0': !hasBody }"
|
||||
|
|
|
@ -97,11 +97,7 @@ export default {
|
|||
class="related-issues-token-body bordered-box bg-white"
|
||||
:class="{ 'sortable-container': canReorder }"
|
||||
>
|
||||
<div
|
||||
v-if="isFetching"
|
||||
class="related-issues-loading-icon"
|
||||
data-qa-selector="related_issues_loading_placeholder"
|
||||
>
|
||||
<div v-if="isFetching" class="gl-mb-2" data-qa-selector="related_issues_loading_placeholder">
|
||||
<gl-loading-icon
|
||||
ref="loadingIcon"
|
||||
size="sm"
|
||||
|
|
|
@ -1,30 +1,31 @@
|
|||
<script>
|
||||
import { sprintf, s__ } from '~/locale';
|
||||
import { GlSprintf } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
|
||||
const timeSpent = s__('TimeTracking|%{spentStart}Spent: %{spentEnd}');
|
||||
|
||||
export default {
|
||||
name: 'TimeTrackingSpentOnlyPane',
|
||||
timeSpent,
|
||||
components: {
|
||||
GlSprintf,
|
||||
},
|
||||
props: {
|
||||
timeSpentHumanReadable: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
timeSpent() {
|
||||
return sprintf(
|
||||
s__('TimeTracking|%{startTag}Spent: %{endTag}%{timeSpentHumanReadable}'),
|
||||
{
|
||||
startTag: '<span class="gl-font-weight-bold">',
|
||||
endTag: '</span>',
|
||||
timeSpentHumanReadable: this.timeSpentHumanReadable,
|
||||
},
|
||||
false,
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div data-testid="spentOnlyPane" v-html="timeSpent /* eslint-disable-line vue/no-v-html */"></div>
|
||||
<div data-testid="spentOnlyPane">
|
||||
<gl-sprintf :message="$options.timeSpent">
|
||||
<template #spent="{ content }">
|
||||
<span class="gl-font-weight-bold">{{ content }}</span
|
||||
>{{ timeSpentHumanReadable }}
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<script>
|
||||
import { GlButton, GlLoadingIcon, GlIcon, GlLink, GlBadge, GlSafeHtmlDirective } from '@gitlab/ui';
|
||||
import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
|
||||
import StatusIcon from '../mr_widget_status_icon.vue';
|
||||
import { EXTENSION_ICON_CLASS } from '../../constants';
|
||||
import StatusIcon from './status_icon.vue';
|
||||
|
||||
export const LOADING_STATES = {
|
||||
collapsedLoading: 'collapsedLoading',
|
||||
|
@ -45,14 +46,6 @@ export default {
|
|||
return true;
|
||||
},
|
||||
statusIconName() {
|
||||
if (this.isLoadingSummary) {
|
||||
return 'loading';
|
||||
}
|
||||
|
||||
if (this.loadingState === LOADING_STATES.collapsedError) {
|
||||
return 'warning';
|
||||
}
|
||||
|
||||
return this.statusIcon(this.collapsedData);
|
||||
},
|
||||
},
|
||||
|
@ -96,13 +89,18 @@ export default {
|
|||
});
|
||||
},
|
||||
},
|
||||
EXTENSION_ICON_CLASS,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="media-section mr-widget-border-top">
|
||||
<section class="media-section mr-widget-border-top" data-testid="widget-extension">
|
||||
<div class="media gl-p-5">
|
||||
<status-icon :status="statusIconName" class="align-self-center" />
|
||||
<status-icon
|
||||
:name="$options.name"
|
||||
:is-loading="isLoadingSummary"
|
||||
:icon-name="statusIconName"
|
||||
/>
|
||||
<div class="media-body d-flex flex-align-self-center align-items-center">
|
||||
<div class="code-text">
|
||||
<template v-if="isLoadingSummary">
|
||||
|
@ -114,13 +112,18 @@ export default {
|
|||
v-if="isCollapsible"
|
||||
size="small"
|
||||
class="float-right align-self-center"
|
||||
data-testid="toggle-button"
|
||||
@click="toggleCollapsed"
|
||||
>
|
||||
{{ isCollapsed ? __('Expand') : __('Collapse') }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isCollapsed" class="mr-widget-grouped-section">
|
||||
<div
|
||||
v-if="!isCollapsed"
|
||||
class="mr-widget-grouped-section"
|
||||
data-testid="widget-extension-collapsed-section"
|
||||
>
|
||||
<div v-if="isLoadingExpanded" class="report-block-container">
|
||||
<gl-loading-icon size="sm" inline /> {{ __('Loading...') }}
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { extensions } from './index';
|
||||
import { registeredExtensions } from './index';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
|
@ -8,6 +8,8 @@ export default {
|
|||
},
|
||||
},
|
||||
render(h) {
|
||||
const { extensions } = registeredExtensions;
|
||||
|
||||
if (extensions.length === 0) return null;
|
||||
|
||||
return h('div', {}, [
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import Vue from 'vue';
|
||||
import ExtensionBase from './base.vue';
|
||||
|
||||
// Holds all the currently registered extensions
|
||||
export const extensions = [];
|
||||
export const registeredExtensions = Vue.observable({ extensions: [] });
|
||||
|
||||
export const registerExtension = (extension) => {
|
||||
// Pushes into the extenions array a dynamically created Vue component
|
||||
// that gets exteneded from `base.vue`
|
||||
extensions.push({
|
||||
registeredExtensions.extensions.push({
|
||||
extends: ExtensionBase,
|
||||
name: extension.name,
|
||||
props: extension.props,
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
<script>
|
||||
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
|
||||
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
|
||||
import { EXTENSION_ICON_CLASS, EXTENSION_ICONS } from '../../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlLoadingIcon,
|
||||
GlIcon,
|
||||
},
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
iconName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
iconAriaLabel() {
|
||||
const statusLabel = Object.keys(EXTENSION_ICONS).find(
|
||||
(k) => EXTENSION_ICONS[k] === this.iconName,
|
||||
);
|
||||
|
||||
return `${capitalizeFirstCharacter(statusLabel)} ${this.name}`;
|
||||
},
|
||||
},
|
||||
EXTENSION_ICON_CLASS,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="[$options.EXTENSION_ICON_CLASS[iconName], { 'mr-widget-extension-icon': !isLoading }]"
|
||||
class="align-self-center gl-rounded-full gl-mr-3 gl-relative gl-p-2"
|
||||
>
|
||||
<gl-loading-icon v-if="isLoading" size="md" inline class="gl-display-block" />
|
||||
<gl-icon
|
||||
v-else
|
||||
:name="iconName"
|
||||
:size="16"
|
||||
:aria-label="iconAriaLabel"
|
||||
class="gl-display-block"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -91,4 +91,19 @@ export const stateToTransitionMap = {
|
|||
export const stateToComponentMap = {
|
||||
[states.MERGING]: classStateMap[stateKey.merging],
|
||||
};
|
||||
|
||||
export const EXTENSION_ICONS = {
|
||||
failed: 'status-failed',
|
||||
warning: 'status-alert',
|
||||
success: 'status-success',
|
||||
neutral: 'status-neutral',
|
||||
};
|
||||
|
||||
export const EXTENSION_ICON_CLASS = {
|
||||
[EXTENSION_ICONS.failed]: 'gl-text-red-500',
|
||||
[EXTENSION_ICONS.warning]: 'gl-text-orange-500',
|
||||
[EXTENSION_ICONS.success]: 'gl-text-green-500',
|
||||
[EXTENSION_ICONS.neutral]: 'gl-text-gray-400',
|
||||
};
|
||||
|
||||
export { STATE_MACHINE };
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
/* eslint-disable */
|
||||
import { EXTENSION_ICONS } from '../constants';
|
||||
import issuesCollapsedQuery from './issues_collapsed.query.graphql';
|
||||
import issuesQuery from './issues.query.graphql';
|
||||
|
||||
export default {
|
||||
// Give the extension a name
|
||||
// Make it easier to track in Vue dev tools
|
||||
name: 'WidgetIssues',
|
||||
name: 'Issues',
|
||||
// Add an array of props
|
||||
// These then get mapped to values stored in the MR Widget store
|
||||
props: ['targetProjectFullPath'],
|
||||
|
@ -14,12 +15,12 @@ export default {
|
|||
// Small summary text to be displayed in the collapsed state
|
||||
// Receives the collapsed data as an argument
|
||||
summary(count) {
|
||||
return `<strong>${count}</strong> open issue`;
|
||||
return 'Summary text';
|
||||
},
|
||||
// Status icon to be used next to the summary text
|
||||
// Receives the collapsed data as an argument
|
||||
statusIcon(count) {
|
||||
return count > 0 ? 'warning' : 'success';
|
||||
return EXTENSION_ICONS.warning;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -97,7 +97,7 @@ export default {
|
|||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.previewContent = __('An error occurred while fetching markdown preview');
|
||||
this.previewContent = __('An error occurred while fetching Markdown preview');
|
||||
this.isLoading = false;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -254,7 +254,7 @@ export default {
|
|||
.then(() => $(this.$refs['markdown-preview']).renderGFM())
|
||||
.catch(() =>
|
||||
createFlash({
|
||||
message: __('Error rendering markdown preview'),
|
||||
message: __('Error rendering Markdown preview'),
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
|
|
@ -1040,3 +1040,17 @@ $tabs-holder-z-index: 250;
|
|||
margin-bottom: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.mr-widget-extension-icon::before {
|
||||
@include gl-content-empty;
|
||||
@include gl-absolute;
|
||||
@include gl-left-0;
|
||||
@include gl-top-0;
|
||||
@include gl-opacity-3;
|
||||
@include gl-border-solid;
|
||||
@include gl-border-4;
|
||||
@include gl-rounded-full;
|
||||
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ class ProfilesController < Profiles::ApplicationController
|
|||
|
||||
def update
|
||||
respond_to do |format|
|
||||
result = Users::UpdateService.new(current_user, user_params.merge(user: @user)).execute
|
||||
result = Users::UpdateService.new(current_user, user_params.merge(user: @user)).execute(check_password: true)
|
||||
|
||||
if result[:status] == :success
|
||||
message = s_("Profiles|Profile was successfully updated")
|
||||
|
@ -129,6 +129,7 @@ class ProfilesController < Profiles::ApplicationController
|
|||
:job_title,
|
||||
:pronouns,
|
||||
:pronunciation,
|
||||
:validation_password,
|
||||
status: [:emoji, :message, :availability]
|
||||
]
|
||||
end
|
||||
|
|
|
@ -70,11 +70,16 @@ class MembersFinder
|
|||
end
|
||||
|
||||
def project_invited_groups
|
||||
invited_groups_ids_including_ancestors = Gitlab::ObjectHierarchy
|
||||
.new(project.invited_groups)
|
||||
.base_and_ancestors
|
||||
.public_or_visible_to_user(current_user)
|
||||
.select(:id)
|
||||
invited_groups_and_ancestors = if ::Feature.enabled?(:linear_members_finder_ancestor_scopes, current_user, default_enabled: :yaml)
|
||||
project.invited_groups
|
||||
.self_and_ancestors
|
||||
else
|
||||
Gitlab::ObjectHierarchy
|
||||
.new(project.invited_groups)
|
||||
.base_and_ancestors
|
||||
end
|
||||
|
||||
invited_groups_ids_including_ancestors = invited_groups_and_ancestors.public_or_visible_to_user(current_user).select(:id)
|
||||
|
||||
GroupMember.with_source_id(invited_groups_ids_including_ancestors).non_minimal_access
|
||||
end
|
||||
|
|
69
app/graphql/mutations/clusters/agent_tokens/create.rb
Normal file
69
app/graphql/mutations/clusters/agent_tokens/create.rb
Normal file
|
@ -0,0 +1,69 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Mutations
|
||||
module Clusters
|
||||
module AgentTokens
|
||||
class Create < BaseMutation
|
||||
graphql_name 'ClusterAgentTokenCreate'
|
||||
|
||||
authorize :create_cluster
|
||||
|
||||
ClusterAgentID = ::Types::GlobalIDType[::Clusters::Agent]
|
||||
|
||||
argument :cluster_agent_id,
|
||||
ClusterAgentID,
|
||||
required: true,
|
||||
description: 'Global ID of the cluster agent that will be associated with the new token.'
|
||||
|
||||
argument :description,
|
||||
GraphQL::Types::String,
|
||||
required: false,
|
||||
description: 'Description of the token.'
|
||||
|
||||
argument :name,
|
||||
GraphQL::Types::String,
|
||||
required: true,
|
||||
description: 'Name of the token.'
|
||||
|
||||
field :secret,
|
||||
GraphQL::Types::String,
|
||||
null: true,
|
||||
description: "Token secret value. Make sure you save it - you won't be able to access it again."
|
||||
|
||||
field :token,
|
||||
Types::Clusters::AgentTokenType,
|
||||
null: true,
|
||||
description: 'Token created after mutation.'
|
||||
|
||||
def resolve(args)
|
||||
cluster_agent = authorized_find!(id: args[:cluster_agent_id])
|
||||
|
||||
result = ::Clusters::AgentTokens::CreateService
|
||||
.new(
|
||||
container: cluster_agent.project,
|
||||
current_user: current_user,
|
||||
params: args.merge(agent_id: cluster_agent.id)
|
||||
)
|
||||
.execute
|
||||
|
||||
payload = result.payload
|
||||
|
||||
{
|
||||
secret: payload[:secret],
|
||||
token: payload[:token],
|
||||
errors: Array.wrap(result.message)
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_object(id:)
|
||||
# TODO: remove this line when the compatibility layer is removed
|
||||
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
|
||||
id = ClusterAgentID.coerce_isolated_input(id)
|
||||
GitlabSchema.find_by_gid(id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
35
app/graphql/mutations/clusters/agent_tokens/delete.rb
Normal file
35
app/graphql/mutations/clusters/agent_tokens/delete.rb
Normal file
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Mutations
|
||||
module Clusters
|
||||
module AgentTokens
|
||||
class Delete < BaseMutation
|
||||
graphql_name 'ClusterAgentTokenDelete'
|
||||
|
||||
authorize :admin_cluster
|
||||
|
||||
TokenID = ::Types::GlobalIDType[::Clusters::AgentToken]
|
||||
|
||||
argument :id, TokenID,
|
||||
required: true,
|
||||
description: 'Global ID of the cluster agent token that will be deleted.'
|
||||
|
||||
def resolve(id:)
|
||||
token = authorized_find!(id: id)
|
||||
token.destroy
|
||||
|
||||
{ errors: errors_on_object(token) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_object(id:)
|
||||
# TODO: remove this line when the compatibility layer is removed
|
||||
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
|
||||
id = TokenID.coerce_isolated_input(id)
|
||||
GitlabSchema.find_by_gid(id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
38
app/graphql/mutations/clusters/agents/create.rb
Normal file
38
app/graphql/mutations/clusters/agents/create.rb
Normal file
|
@ -0,0 +1,38 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Mutations
|
||||
module Clusters
|
||||
module Agents
|
||||
class Create < BaseMutation
|
||||
include FindsProject
|
||||
|
||||
authorize :create_cluster
|
||||
|
||||
graphql_name 'CreateClusterAgent'
|
||||
|
||||
argument :project_path, GraphQL::Types::ID,
|
||||
required: true,
|
||||
description: 'Full path of the associated project for this cluster agent.'
|
||||
|
||||
argument :name, GraphQL::Types::String,
|
||||
required: true,
|
||||
description: 'Name of the cluster agent.'
|
||||
|
||||
field :cluster_agent,
|
||||
Types::Clusters::AgentType,
|
||||
null: true,
|
||||
description: 'Cluster agent created after mutation.'
|
||||
|
||||
def resolve(project_path:, name:)
|
||||
project = authorized_find!(project_path)
|
||||
result = ::Clusters::Agents::CreateService.new(project, current_user).execute(name: name)
|
||||
|
||||
{
|
||||
cluster_agent: result[:cluster_agent],
|
||||
errors: Array.wrap(result[:message])
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
39
app/graphql/mutations/clusters/agents/delete.rb
Normal file
39
app/graphql/mutations/clusters/agents/delete.rb
Normal file
|
@ -0,0 +1,39 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Mutations
|
||||
module Clusters
|
||||
module Agents
|
||||
class Delete < BaseMutation
|
||||
graphql_name 'ClusterAgentDelete'
|
||||
|
||||
authorize :admin_cluster
|
||||
|
||||
AgentID = ::Types::GlobalIDType[::Clusters::Agent]
|
||||
|
||||
argument :id, AgentID,
|
||||
required: true,
|
||||
description: 'Global ID of the cluster agent that will be deleted.'
|
||||
|
||||
def resolve(id:)
|
||||
cluster_agent = authorized_find!(id: id)
|
||||
result = ::Clusters::Agents::DeleteService
|
||||
.new(container: cluster_agent.project, current_user: current_user)
|
||||
.execute(cluster_agent)
|
||||
|
||||
{
|
||||
errors: Array.wrap(result.message)
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_object(id:)
|
||||
# TODO: remove this line when the compatibility layer is removed
|
||||
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
|
||||
id = AgentID.coerce_isolated_input(id)
|
||||
GitlabSchema.find_by_gid(id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -6,6 +6,7 @@ module Types
|
|||
'7d': 'SEVEN_DAYS',
|
||||
'14d': 'FOURTEEN_DAYS',
|
||||
'30d': 'THIRTY_DAYS',
|
||||
'60d': 'SIXTY_DAYS',
|
||||
'90d': 'NINETY_DAYS'
|
||||
}.freeze
|
||||
|
||||
|
|
|
@ -31,6 +31,10 @@ module Types
|
|||
mount_mutation Mutations::Boards::Lists::Update
|
||||
mount_mutation Mutations::Boards::Lists::Destroy
|
||||
mount_mutation Mutations::Branches::Create, calls_gitaly: true
|
||||
mount_mutation Mutations::Clusters::Agents::Create
|
||||
mount_mutation Mutations::Clusters::Agents::Delete
|
||||
mount_mutation Mutations::Clusters::AgentTokens::Create
|
||||
mount_mutation Mutations::Clusters::AgentTokens::Delete
|
||||
mount_mutation Mutations::Commits::Create, calls_gitaly: true
|
||||
mount_mutation Mutations::CustomEmoji::Create, feature_flag: :custom_emoji
|
||||
mount_mutation Mutations::CustomEmoji::Destroy, feature_flag: :custom_emoji
|
||||
|
|
|
@ -87,9 +87,9 @@ module SearchHelper
|
|||
|
||||
def search_entries_info_template(collection)
|
||||
if collection.total_pages > 1
|
||||
s_("SearchResults|Showing %{from} - %{to} of %{count} %{scope} for%{term_element}").html_safe
|
||||
s_("SearchResults|Showing %{from} - %{to} of %{count} %{scope} for %{term_element}").html_safe
|
||||
else
|
||||
s_("SearchResults|Showing %{count} %{scope} for%{term_element}").html_safe
|
||||
s_("SearchResults|Showing %{count} %{scope} for %{term_element}").html_safe
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -71,7 +71,7 @@ class AuditEvent < ApplicationRecord
|
|||
end
|
||||
|
||||
def lazy_author
|
||||
BatchLoader.for(author_id).batch(replace_methods: false) do |author_ids, loader|
|
||||
BatchLoader.for(author_id).batch do |author_ids, loader|
|
||||
User.select(:id, :name, :username).where(id: author_ids).find_each do |user|
|
||||
loader.call(user.id, user)
|
||||
end
|
||||
|
|
|
@ -133,7 +133,7 @@ class Commit
|
|||
end
|
||||
|
||||
def lazy(container, oid)
|
||||
BatchLoader.for({ container: container, oid: oid }).batch(replace_methods: false) do |items, loader|
|
||||
BatchLoader.for({ container: container, oid: oid }).batch do |items, loader|
|
||||
items_by_container = items.group_by { |i| i[:container] }
|
||||
|
||||
items_by_container.each do |container, commit_ids|
|
||||
|
|
|
@ -110,7 +110,7 @@ module Avatarable
|
|||
|
||||
def retrieve_upload_from_batch(identifier)
|
||||
BatchLoader.for(identifier: identifier, model: self)
|
||||
.batch(key: self.class, cache: true, replace_methods: false) do |upload_params, loader, args|
|
||||
.batch(key: self.class) do |upload_params, loader, args|
|
||||
model_class = args[:key]
|
||||
paths = upload_params.flat_map do |params|
|
||||
params[:model].upload_paths(params[:identifier])
|
||||
|
|
|
@ -74,6 +74,7 @@ class ContainerExpirationPolicy < ApplicationRecord
|
|||
'7d': _('%{days} days until tags are automatically removed') % { days: 7 },
|
||||
'14d': _('%{days} days until tags are automatically removed') % { days: 14 },
|
||||
'30d': _('%{days} days until tags are automatically removed') % { days: 30 },
|
||||
'60d': _('%{days} days until tags are automatically removed') % { days: 60 },
|
||||
'90d': _('%{days} days until tags are automatically removed') % { days: 90 }
|
||||
}
|
||||
end
|
||||
|
|
|
@ -1791,7 +1791,7 @@ class Project < ApplicationRecord
|
|||
def open_issues_count(current_user = nil)
|
||||
return Projects::OpenIssuesCountService.new(self, current_user).count unless current_user.nil?
|
||||
|
||||
BatchLoader.for(self).batch(replace_methods: false) do |projects, loader|
|
||||
BatchLoader.for(self).batch do |projects, loader|
|
||||
issues_count_per_project = ::Projects::BatchOpenIssuesCountService.new(projects).refresh_cache_and_retrieve_data
|
||||
|
||||
issues_count_per_project.each do |project, count|
|
||||
|
@ -2256,7 +2256,7 @@ class Project < ApplicationRecord
|
|||
|
||||
# rubocop: disable CodeReuse/ServiceClass
|
||||
def forks_count
|
||||
BatchLoader.for(self).batch(replace_methods: false) do |projects, loader|
|
||||
BatchLoader.for(self).batch do |projects, loader|
|
||||
fork_count_per_project = ::Projects::BatchForksCountService.new(projects).refresh_cache_and_retrieve_data
|
||||
|
||||
fork_count_per_project.each do |project, count|
|
||||
|
|
31
app/services/clusters/agent_tokens/create_service.rb
Normal file
31
app/services/clusters/agent_tokens/create_service.rb
Normal file
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Clusters
|
||||
module AgentTokens
|
||||
class CreateService < ::BaseContainerService
|
||||
ALLOWED_PARAMS = %i[agent_id description name].freeze
|
||||
|
||||
def execute
|
||||
return error_no_permissions unless current_user.can?(:create_cluster, container)
|
||||
|
||||
token = ::Clusters::AgentToken.new(filtered_params.merge(created_by_user: current_user))
|
||||
|
||||
if token.save
|
||||
ServiceResponse.success(payload: { secret: token.token, token: token })
|
||||
else
|
||||
ServiceResponse.error(message: token.errors.full_messages)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def error_no_permissions
|
||||
ServiceResponse.error(message: s_('ClusterAgent|User has insufficient permissions to create a token for this project'))
|
||||
end
|
||||
|
||||
def filtered_params
|
||||
params.slice(*ALLOWED_PARAMS)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
29
app/services/clusters/agents/create_service.rb
Normal file
29
app/services/clusters/agents/create_service.rb
Normal file
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Clusters
|
||||
module Agents
|
||||
class CreateService < BaseService
|
||||
def execute(name:)
|
||||
return error_no_permissions unless cluster_agent_permissions?
|
||||
|
||||
agent = ::Clusters::Agent.new(name: name, project: project, created_by_user: current_user)
|
||||
|
||||
if agent.save
|
||||
success.merge(cluster_agent: agent)
|
||||
else
|
||||
error(agent.errors.full_messages)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def cluster_agent_permissions?
|
||||
current_user.can?(:admin_pipeline, project) && current_user.can?(:create_cluster, project)
|
||||
end
|
||||
|
||||
def error_no_permissions
|
||||
error(s_('ClusterAgent|You have insufficient permissions to create a cluster agent for this project'))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
23
app/services/clusters/agents/delete_service.rb
Normal file
23
app/services/clusters/agents/delete_service.rb
Normal file
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Clusters
|
||||
module Agents
|
||||
class DeleteService < ::BaseContainerService
|
||||
def execute(cluster_agent)
|
||||
return error_no_permissions unless current_user.can?(:admin_cluster, cluster_agent)
|
||||
|
||||
if cluster_agent.destroy
|
||||
ServiceResponse.success
|
||||
else
|
||||
ServiceResponse.error(message: cluster_agent.errors.full_messages)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def error_no_permissions
|
||||
ServiceResponse.error(message: s_('ClusterAgent|You have insufficient permissions to delete this cluster agent'))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5,15 +5,18 @@ module Users
|
|||
include NewUserNotifier
|
||||
attr_reader :user, :identity_params
|
||||
|
||||
ATTRS_REQUIRING_PASSWORD_CHECK = %w[email].freeze
|
||||
|
||||
def initialize(current_user, params = {})
|
||||
@current_user = current_user
|
||||
@validation_password = params.delete(:validation_password)
|
||||
@user = params.delete(:user)
|
||||
@status_params = params.delete(:status)
|
||||
@identity_params = params.slice(*identity_attributes)
|
||||
@params = params.dup
|
||||
end
|
||||
|
||||
def execute(validate: true, &block)
|
||||
def execute(validate: true, check_password: false, &block)
|
||||
yield(@user) if block_given?
|
||||
|
||||
user_exists = @user.persisted?
|
||||
|
@ -21,6 +24,11 @@ module Users
|
|||
|
||||
discard_read_only_attributes
|
||||
assign_attributes
|
||||
|
||||
if check_password && require_password_check? && !@user.valid_password?(@validation_password)
|
||||
return error(s_("Profiles|Invalid password"))
|
||||
end
|
||||
|
||||
assign_identity
|
||||
build_canonical_email
|
||||
|
||||
|
@ -32,8 +40,8 @@ module Users
|
|||
end
|
||||
end
|
||||
|
||||
def execute!(*args, &block)
|
||||
result = execute(*args, &block)
|
||||
def execute!(*args, **kargs, &block)
|
||||
result = execute(*args, **kargs, &block)
|
||||
|
||||
raise ActiveRecord::RecordInvalid, @user unless result[:status] == :success
|
||||
|
||||
|
@ -42,6 +50,14 @@ module Users
|
|||
|
||||
private
|
||||
|
||||
def require_password_check?
|
||||
return false unless @user.persisted?
|
||||
return false if @user.password_automatically_set?
|
||||
|
||||
changes = @user.changed
|
||||
ATTRS_REQUIRING_PASSWORD_CHECK.any? { |param| changes.include?(param) }
|
||||
end
|
||||
|
||||
def build_canonical_email
|
||||
return unless @user.email_changed?
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<%= _(" %{name}, confirm your email address now! ") % { name: @resource.user.name } %>
|
||||
<%= _("%{name}, confirm your email address now!") % { name: @resource.user.name } %>
|
||||
|
||||
<%= _("Use the link below to confirm your email address (%{email})") % { email: @resource.email } %>
|
||||
|
||||
|
|
|
@ -3,8 +3,11 @@
|
|||
- email_change_disabled = local_assigns.fetch(:email_change_disabled, nil)
|
||||
- read_only_help_text = readonly ? s_("Profiles|Your email address was automatically set based on your %{provider_label} account") % { provider_label: attribute_provider_label(:email) } : user_email_help_text(@user)
|
||||
- help_text = email_change_disabled ? s_("Your account uses dedicated credentials for the \"%{group_name}\" group and can only be updated through SSO.") % { group_name: @user.managing_group.name } : read_only_help_text
|
||||
- password_automatically_set = @user.password_automatically_set?
|
||||
|
||||
= form.text_field :email, required: true, class: 'input-lg gl-form-input', value: (@user.email unless @user.temp_oauth_email?), help: help_text.html_safe, readonly: readonly || email_change_disabled
|
||||
- unless password_automatically_set
|
||||
= hidden_field_tag 'user[validation_password]', :validation_password, class: 'js-password-prompt-field', help: s_("Profiles|Enter your password to confirm the email change")
|
||||
= form.select :public_email, options_for_select(@user.public_verified_emails, selected: @user.public_email),
|
||||
{ help: s_("Profiles|This email will be displayed on your public profile"), include_blank: s_("Profiles|Do not show on profile") },
|
||||
control_class: 'select2 input-lg', disabled: email_change_disabled
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
- availability = availability_values
|
||||
- custom_emoji = show_status_emoji?(@user.status)
|
||||
|
||||
= bootstrap_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user gl-mt-3 js-quick-submit gl-show-field-errors' }, authenticity_token: true do |f|
|
||||
= bootstrap_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user gl-mt-3 js-quick-submit gl-show-field-errors js-password-prompt-form', remote: true }, authenticity_token: true do |f|
|
||||
= form_errors(@user)
|
||||
|
||||
.row.js-search-settings-section
|
||||
|
@ -124,9 +124,11 @@
|
|||
.help-block
|
||||
= s_("Profiles|Choose to show contributions of private projects on your public profile without any project, repository or organization information")
|
||||
%hr
|
||||
= f.submit s_("Profiles|Update profile settings"), class: 'gl-button btn btn-confirm gl-mr-3'
|
||||
= f.submit s_("Profiles|Update profile settings"), class: 'gl-button btn btn-confirm gl-mr-3 js-password-prompt-btn'
|
||||
= link_to _("Cancel"), user_path(current_user), class: 'gl-button btn btn-default btn-cancel'
|
||||
|
||||
#password-prompt-modal
|
||||
|
||||
.modal.modal-profile-crop{ data: { cropper_css_path: ActionController::Base.helpers.stylesheet_path('lazy_bundles/cropper.css') } }
|
||||
.modal-dialog
|
||||
.modal-content
|
||||
|
|
|
@ -3,4 +3,3 @@
|
|||
can_add_related_issues: "#{can?(current_user, :admin_issue_link, @issue)}",
|
||||
help_path: help_page_path('user/project/issues/related_issues'),
|
||||
show_categorized_issues: "false" } }
|
||||
- render('projects/issues/related_issues_block')
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
.related-issues-block
|
||||
.card.card-slim
|
||||
.card-header.panel-empty-heading.border-bottom-0
|
||||
%h3.card-title.mt-0.mb-0.h5
|
||||
= _('Linked issues')
|
|
@ -13,7 +13,8 @@
|
|||
= highlight_and_truncate_issuable(issuable, @search_term, @search_highlight)
|
||||
.col-sm-3.gl-mt-3.gl-sm-mt-0.gl-text-right
|
||||
- if issuable.respond_to?(:upvotes_count) && issuable.upvotes_count > 0
|
||||
%li.issuable-upvotes.gl-list-style-none.has-tooltip{ title: _('Upvotes') }
|
||||
= sprite_icon('thumb-up', css_class: "gl-vertical-align-middle")
|
||||
= issuable.upvotes_count
|
||||
%li.issuable-upvotes.gl-list-style-none
|
||||
%span.has-tooltip{ title: _('Upvotes') }
|
||||
= sprite_icon('thumb-up', css_class: "gl-vertical-align-middle")
|
||||
= issuable.upvotes_count
|
||||
%span.gl-text-gray-500= sprintf(s_('updated %{time_ago}'), { time_ago: time_ago_with_tooltip(issuable.updated_at, placement: 'bottom') }).html_safe
|
||||
|
|
|
@ -26,8 +26,7 @@ module Gitlab
|
|||
object = representation_class.from_json_hash(hash)
|
||||
|
||||
# To better express in the logs what object is being imported.
|
||||
self.github_id = object.attributes.fetch(:github_id)
|
||||
|
||||
self.github_identifiers = object.github_identifiers
|
||||
info(project.id, message: 'starting importer')
|
||||
|
||||
importer_class.new(object, project, client).execute
|
||||
|
@ -35,10 +34,10 @@ module Gitlab
|
|||
Gitlab::GithubImport::ObjectCounter.increment(project, object_type, :imported)
|
||||
|
||||
info(project.id, message: 'importer finished')
|
||||
rescue KeyError => e
|
||||
rescue NoMethodError => e
|
||||
# This exception will be more useful in development when a new
|
||||
# Representation is created but the developer forgot to add a
|
||||
# `:github_id` field.
|
||||
# `:github_identifiers` field.
|
||||
Gitlab::Import::ImportFailureService.track(
|
||||
project_id: project.id,
|
||||
error_source: importer_class.name,
|
||||
|
@ -72,7 +71,7 @@ module Gitlab
|
|||
|
||||
private
|
||||
|
||||
attr_accessor :github_id
|
||||
attr_accessor :github_identifiers
|
||||
|
||||
def info(project_id, extra = {})
|
||||
Logger.info(log_attributes(project_id, extra))
|
||||
|
@ -82,7 +81,7 @@ module Gitlab
|
|||
extra.merge(
|
||||
project_id: project_id,
|
||||
importer: importer_class.name,
|
||||
github_id: github_id
|
||||
github_identifiers: github_identifiers
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: linear_members_finder_ancestor_scopes
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70583
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/341347
|
||||
milestone: '14.4'
|
||||
type: development
|
||||
group: group::access
|
||||
default_enabled: false
|
|
@ -1,3 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
Rails.application.config.middleware.use(BatchLoader::Middleware)
|
||||
|
||||
# Disables replace_methods by default.
|
||||
# See https://github.com/exAspArk/batch-loader#replacing-methods for more information.
|
||||
module BatchLoaderWithoutMethodReplacementByDefault
|
||||
def batch(replace_methods: false, **kw_args, &batch_block)
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
BatchLoader.prepend(BatchLoaderWithoutMethodReplacementByDefault)
|
||||
|
|
|
@ -1,30 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'gettext_i18n_rails/haml_parser'
|
||||
require 'gettext_i18n_rails_js/parser/javascript'
|
||||
require 'json'
|
||||
|
||||
VUE_TRANSLATE_REGEX = /((%[\w.-]+)(?:\s))?{{ (N|n|s)?__\((.*)\) }}/.freeze
|
||||
|
||||
module GettextI18nRails
|
||||
class HamlParser
|
||||
singleton_class.send(:alias_method, :old_convert_to_code, :convert_to_code)
|
||||
|
||||
# We need to convert text in Mustache format
|
||||
# to a format that can be parsed by Gettext scripts.
|
||||
# If we found a content like "{{ __('Stage') }}"
|
||||
# in a HAML file we convert it to "= _('Stage')", that way
|
||||
# it can be processed by the "rake gettext:find" script.
|
||||
#
|
||||
# Overwrites: https://github.com/grosser/gettext_i18n_rails/blob/8396387a431e0f8ead72fc1cd425cad2fa4992f2/lib/gettext_i18n_rails/haml_parser.rb#L9
|
||||
def self.convert_to_code(text)
|
||||
text.gsub!(VUE_TRANSLATE_REGEX, "\\2= \\3_(\\4)")
|
||||
|
||||
old_convert_to_code(text)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module GettextI18nRailsJs
|
||||
module Parser
|
||||
module Javascript
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
data_category: optional
|
||||
key_path: counts.projects_with_expiration_policy_enabled_with_older_than_set_to_60d
|
||||
description: A count of projects with the cleanup policy set delete tags older than
|
||||
60 days
|
||||
product_section: ops
|
||||
product_stage: package
|
||||
product_group: group::package
|
||||
product_category: container registry
|
||||
value_type: number
|
||||
status: active
|
||||
time_frame: all
|
||||
data_source: database
|
||||
distribution:
|
||||
- ee
|
||||
- ce
|
||||
tier:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
performance_indicator_type: []
|
||||
milestone: "14.4"
|
|
@ -298,6 +298,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
|
|||
end
|
||||
end
|
||||
|
||||
resources :cluster_agents, only: [:show], param: :name
|
||||
|
||||
concerns :clusterable
|
||||
|
||||
namespace :serverless do
|
||||
|
|
|
@ -21,6 +21,7 @@ You can use the following environment variables to override certain values:
|
|||
|--------------------------------------------|---------|---------------------------------------------------------------------------------------------------------|
|
||||
| `DATABASE_URL` | string | The database URL; is of the form: `postgresql://localhost/blog_development`. |
|
||||
| `ENABLE_BOOTSNAP` | string | Enables Bootsnap for speeding up initial Rails boot (`1` to enable). |
|
||||
| `EXTERNAL_URL` | string | Specify the external URL at the [time of installation](https://docs.gitlab.com/omnibus/settings/configuration.html#specifying-the-external-url-at-the-time-of-installation). |
|
||||
| `EXTERNAL_VALIDATION_SERVICE_TIMEOUT` | integer | Timeout, in seconds, for an [external CI/CD pipeline validation service](external_pipeline_validation.md). Default is `5`. |
|
||||
| `EXTERNAL_VALIDATION_SERVICE_URL` | string | URL to an [external CI/CD pipeline validation service](external_pipeline_validation.md). |
|
||||
| `EXTERNAL_VALIDATION_SERVICE_TOKEN` | string | The `X-Gitlab-Token` for authentication with an [external CI/CD pipeline validation service](external_pipeline_validation.md). |
|
||||
|
|
|
@ -15460,6 +15460,7 @@ Conan file types.
|
|||
| <a id="containerexpirationpolicyolderthanenumfourteen_days"></a>`FOURTEEN_DAYS` | 14 days until tags are automatically removed. |
|
||||
| <a id="containerexpirationpolicyolderthanenumninety_days"></a>`NINETY_DAYS` | 90 days until tags are automatically removed. |
|
||||
| <a id="containerexpirationpolicyolderthanenumseven_days"></a>`SEVEN_DAYS` | 7 days until tags are automatically removed. |
|
||||
| <a id="containerexpirationpolicyolderthanenumsixty_days"></a>`SIXTY_DAYS` | 60 days until tags are automatically removed. |
|
||||
| <a id="containerexpirationpolicyolderthanenumthirty_days"></a>`THIRTY_DAYS` | 30 days until tags are automatically removed. |
|
||||
|
||||
### `ContainerRepositoryCleanupStatus`
|
||||
|
|
|
@ -64,7 +64,7 @@ build:
|
|||
entrypoint: [""]
|
||||
script:
|
||||
- mkdir -p /kaniko/.docker
|
||||
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"auth\":\"$(echo -n ${CI_REGISTRY_USER}:${CI_REGISTRY_PASSWORD} | base64)\"}}}" > /kaniko/.docker/config.json
|
||||
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"auth\":\"$(echo -n ${CI_REGISTRY_USER}:${CI_REGISTRY_PASSWORD} | base64 | tr -d '\n')\"}}}" > /kaniko/.docker/config.json
|
||||
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
|
@ -91,7 +91,7 @@ build:
|
|||
- mkdir -p /kaniko/.docker
|
||||
- |-
|
||||
KANIKOPROXYBUILDARGS=""
|
||||
KANIKOCFG="{\"auths\":{\"$CI_REGISTRY\":{\"auth\":\"$(echo -n ${CI_REGISTRY_USER}:${CI_REGISTRY_PASSWORD} | base64)\"}}}"
|
||||
KANIKOCFG="{\"auths\":{\"$CI_REGISTRY\":{\"auth\":\"$(echo -n ${CI_REGISTRY_USER}:${CI_REGISTRY_PASSWORD} | base64 | tr -d '\n')\"}}}"
|
||||
if [ "x${http_proxy}" != "x" -o "x${https_proxy}" != "x" ]; then
|
||||
KANIKOCFG="${KANIKOCFG}, \"proxies\": { \"default\": { \"httpProxy\": \"${http_proxy}\", \"httpsProxy\": \"${https_proxy}\", \"noProxy\": \"${no_proxy}\"}}"
|
||||
KANIKOPROXYBUILDARGS="--build-arg http_proxy=${http_proxy} --build-arg https_proxy=${https_proxy} --build-arg no_proxy=${no_proxy}"
|
||||
|
@ -120,7 +120,7 @@ store:
|
|||
```yaml
|
||||
before_script:
|
||||
- mkdir -p /kaniko/.docker
|
||||
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"auth\":\"$(echo -n ${CI_REGISTRY_USER}:${CI_REGISTRY_PASSWORD} | base64)\"}}}" > /kaniko/.docker/config.json
|
||||
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"auth\":\"$(echo -n ${CI_REGISTRY_USER}:${CI_REGISTRY_PASSWORD} | base64 | tr -d '\n')\"}}}" > /kaniko/.docker/config.json
|
||||
- |
|
||||
echo "-----BEGIN CERTIFICATE-----
|
||||
...
|
||||
|
|
|
@ -106,6 +106,7 @@ with [domain expertise](#domain-experts).
|
|||
1. If your merge request includes user-facing changes (*3*), it must be
|
||||
**approved by a [Product Designer](https://about.gitlab.com/handbook/engineering/projects/#gitlab_reviewers_UX)**,
|
||||
based on assignments in the appropriate [DevOps stage group](https://about.gitlab.com/handbook/product/categories/#devops-stages).
|
||||
See the [design and user interface guidelines](contributing/design.md) for details.
|
||||
1. If your merge request includes adding a new JavaScript library (*1*)...
|
||||
- If the library significantly increases the
|
||||
[bundle size](https://gitlab.com/gitlab-org/frontend/playground/webpack-memory-metrics/-/blob/master/doc/report.md), it must
|
||||
|
|
|
@ -5,34 +5,102 @@ group: Development
|
|||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Implement design & UI elements
|
||||
# Design and user interface changes
|
||||
|
||||
For guidance on UX implementation at GitLab, please refer to our [Design System](https://design.gitlab.com/).
|
||||
Follow these guidelines when contributing or reviewing design and user interface
|
||||
(UI) changes. Refer to our [code review guide](../code_review.md) for broader
|
||||
advice and best practices for code review in general.
|
||||
|
||||
The UX team uses labels to manage their workflow.
|
||||
The basis for most of these guidelines is [Pajamas](https://design.gitlab.com/),
|
||||
GitLab design system. We encourage you to [contribute to Pajamas](https://design.gitlab.com/get-started/contribute)
|
||||
with additions and improvements.
|
||||
|
||||
The `~UX` label on an issue is a signal to the UX team that it will need UX attention.
|
||||
To better understand the priority by which UX tackles issues, see the [UX section](https://about.gitlab.com/handbook/engineering/ux/) of the handbook.
|
||||
## Merge request reviews
|
||||
|
||||
Once an issue has been worked on and is ready for development, a UXer removes the `~UX` label and applies the `~"UX ready"` label to that issue.
|
||||
As a merge request (MR) author, you must include _Before_ and _After_
|
||||
screenshots (or videos) of your changes in the description, as explained in our
|
||||
[MR workflow](merge_request_workflow.md). These screenshots/videos are very helpful
|
||||
for all reviewers and can speed up the review process, especially if the changes
|
||||
are small.
|
||||
|
||||
There is a special type label called `~"product discovery"` intended for UX (user experience),
|
||||
PM (product manager), FE (frontend), and BE (backend). It represents a discovery issue to discuss the problem and
|
||||
potential solutions. The final output for this issue could be a doc of
|
||||
requirements, a design artifact, or even a prototype. The solution will be
|
||||
developed in a subsequent milestone.
|
||||
## Checklist
|
||||
|
||||
`~"product discovery"` issues are like any other issue and should contain a milestone label, `~Deliverable` or `~Stretch`, when scheduled in the current milestone.
|
||||
Check these aspects both when _designing_ and _reviewing_ UI changes.
|
||||
|
||||
The initial issue should be about the problem we are solving. If a separate [product discovery issue](https://about.gitlab.com/handbook/engineering/ux/ux-department-workflow/#how-we-use-labels)
|
||||
is needed for additional research and design work, it will be created by a PM or UX person.
|
||||
Assign the `~UX`, `~"product discovery"` and `~Deliverable` labels, add a milestone and
|
||||
use a title that makes it clear that the scheduled issue is product discovery
|
||||
(for example, `Product discovery for XYZ`).
|
||||
### Writing
|
||||
|
||||
In order to complete a product discovery issue in a release, you must complete the following:
|
||||
- Follow [Pajamas](https://design.gitlab.com/content/punctuation/) as the primary
|
||||
guidelines for UI text and [documentation style guide](../documentation/styleguide/index.md)
|
||||
as the secondary.
|
||||
- Use clear and consistent [terminology](https://design.gitlab.com/content/terminology).
|
||||
- Check grammar and spelling.
|
||||
- Consider help content and follow its [guidelines](https://design.gitlab.com/usability/helping-users).
|
||||
- Request review from the [appropriate Technical Writer](https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers),
|
||||
indicating any specific files or lines they should review, and how to preview
|
||||
or understand the location/context of the text from the user's perspective.
|
||||
|
||||
1. UXer removes the `~UX` label, adds the `~"UX ready"` label.
|
||||
1. Modify the issue description in the product discovery issue to contain the final design. If it makes sense, the original information indicating the need for the design can be moved to a lower "Original Information" section.
|
||||
1. Copy the design to the description of the delivery issue for which the product discovery issue was created. Do not simply refer to the product discovery issue as a separate source of truth.
|
||||
1. In some cases, a product discovery issue also identifies future enhancements that will not go into the issue that originated the product discovery issue. For these items, create new issues containing the designs to ensure they are not lost. Put the issues in the backlog if they are agreed upon as good ideas. Otherwise leave them for triage.
|
||||
### Patterns
|
||||
|
||||
- Consider similar patterns used in the product and justify in the issue when diverging
|
||||
from them.
|
||||
- Use appropriate [components](https://design.gitlab.com/components/overview)
|
||||
and [data visualizations](https://design.gitlab.com/data-visualization/overview).
|
||||
|
||||
### States
|
||||
|
||||
- Account for all applicable states ([error](https://design.gitlab.com/content/error-messages),
|
||||
rest, loading, focus, hover, selected, disabled).
|
||||
- Account for states dependent on data size ([empty](https://design.gitlab.com/regions/empty-states),
|
||||
some data, and lots of data).
|
||||
- Account for states dependent on user role, user preferences, and subscription.
|
||||
- Consider animations and transitions, and follow their [guidelines](https://design.gitlab.com/product-foundations/motion).
|
||||
|
||||
### Visual design
|
||||
|
||||
- Use recommended [colors](https://design.gitlab.com/product-foundations/colors)
|
||||
and [typography](https://design.gitlab.com/product-foundations/type-fundamentals).
|
||||
- Follow [layout guidelines](https://design.gitlab.com/layout/grid).
|
||||
- Use existing [icons](http://gitlab-org.gitlab.io/gitlab-svgs/) and [illustrations](http://gitlab-org.gitlab.io/gitlab-svgs/illustrations)
|
||||
or propose new ones according to [iconography](https://design.gitlab.com/product-foundations/iconography)
|
||||
and [illustration](https://design.gitlab.com/product-foundations/illustration)
|
||||
guidelines.
|
||||
- _Optionally_ consider [dark mode](../../user/profile/preferences.md#dark-mode). [^1]
|
||||
|
||||
[^1]: You're not required to design for [dark mode](../../user/profile/preferences.md#dark-mode) while the feature is in [alpha](https://about.gitlab.com/handbook/product/gitlab-the-product/#alpha). The [UX Foundations team](https://about.gitlab.com/direction/ecosystem/foundations/) plans to improve the dark mode in the future. Until we integrate [Pajamas](https://design.gitlab.com/) components into the product and the underlying design strategy is in place to support dark mode, we cannot guarantee that we won't introduce bugs and debt to this mode. At your discretion, evaluate the need to create dark mode patches.
|
||||
|
||||
### Responsive
|
||||
|
||||
- Account for resizing, collapsing, moving, or wrapping of elements across
|
||||
all breakpoints (even if larger viewports are prioritized).
|
||||
- Provide the same information and actions in all breakpoints.
|
||||
|
||||
### Accessibility
|
||||
|
||||
- Conform to level AA of the World Wide Web Consortium (W3C) [Web Content Accessibility Guidelines 2.1](https://www.w3.org/TR/WCAG21/),
|
||||
according to our [statement of compliance](https://design.gitlab.com/accessibility/a11y).
|
||||
- Follow accessibility [best practices](https://design.gitlab.com/accessibility/best-practices)
|
||||
and [checklist](../fe_guide/accessibility.md#quick-checklist).
|
||||
|
||||
### Handoff
|
||||
|
||||
- Share design specifications in the related issue, preferably through a [Figma link](https://help.figma.com/hc/en-us/articles/360040531773-Share-Files-with-anyone-using-Link-Sharing#Copy_links)
|
||||
link or [GitLab Designs feature](../../user/project/issues/design_management.md#the-design-management-section).
|
||||
See [when you should use each tool](https://about.gitlab.com/handbook/engineering/ux/product-designer/#deliver).
|
||||
- Document user flow and states (for example, using [Mermaid flowcharts in Markdown](../../user/markdown.md#mermaid)).
|
||||
- Document animations and transitions.
|
||||
- Document responsive behaviors.
|
||||
- Document non-evident behaviors (for example, field is auto-focused).
|
||||
- Document accessibility behaviors (for example, using [accessibility annotations in Figma](https://www.figma.com/file/g7QtDbfxF3pCdWiyskIr0X/Accessibility-bluelines)).
|
||||
- Contribute new icons or illustrations to the [GitLab SVGs](https://gitlab.com/gitlab-org/gitlab-svgs)
|
||||
project.
|
||||
|
||||
### Follow-ups
|
||||
|
||||
- Contribute [issues to Pajamas](https://design.gitlab.com/get-started/contribute#contribute-an-issue)
|
||||
for additions or enhancements to the design system.
|
||||
- Create issues with the [`~UX debt`](issue_workflow.md#technical-and-ux-debt)
|
||||
label for intentional deviations from the agreed-upon UX requirements due to
|
||||
time or feasibility challenges, linking back to the corresponding issue(s) or
|
||||
MR(s).
|
||||
- Create issues for [feature additions or enhancements](issue_workflow.md#feature-proposals)
|
||||
outside the agreed-upon UX requirements to avoid scope creep.
|
||||
|
|
|
@ -342,19 +342,22 @@ To create a feature proposal, open an issue on the
|
|||
[issue tracker](https://gitlab.com/gitlab-org/gitlab/-/issues).
|
||||
|
||||
In order to help track the feature proposals, we have created a
|
||||
[`feature`](https://gitlab.com/gitlab-org/gitlab/-/issues?label_name=feature) label. For the time being, users that are not members
|
||||
of the project cannot add labels. You can instead ask one of the [core team](https://about.gitlab.com/community/core-team/)
|
||||
members to add the label ~feature to the issue or add the following
|
||||
[`feature`](https://gitlab.com/gitlab-org/gitlab/-/issues?label_name=feature) label.
|
||||
For the time being, users that are not members of the project cannot add labels.
|
||||
You can instead ask one of the [core team](https://about.gitlab.com/community/core-team/)
|
||||
members to add the label `~feature` to the issue or add the following
|
||||
code snippet right after your description in a new line: `~feature`.
|
||||
|
||||
Please keep feature proposals as small and simple as possible, complex ones
|
||||
might be edited to make them small and simple.
|
||||
|
||||
Please submit Feature Proposals using the ['Feature Proposal' issue template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/Feature%20proposal%20-%20detailed.md) provided on the issue tracker.
|
||||
Please submit feature proposals using the ['Feature Proposal' issue template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/Feature%20proposal%20-%20detailed.md) provided on the issue tracker.
|
||||
|
||||
For changes in the interface, it is helpful to include a mockup. Issues that add to, or change, the interface should
|
||||
be given the ~"UX" label. This will allow the UX team to provide input and guidance. You may
|
||||
need to ask one of the [core team](https://about.gitlab.com/community/core-team/) members to add the label, if you do not have permissions to do it by yourself.
|
||||
For changes to the user interface (UI), follow our [design and UI guidelines](design.md),
|
||||
and include a visual example (screenshot, wireframe, or mockup). Such issues should
|
||||
be given the `~UX"` label for the Product Design team to provide input and guidance.
|
||||
You may need to ask one of the [core team](https://about.gitlab.com/community/core-team/)
|
||||
members to add the label, if you do not have permissions to do it by yourself.
|
||||
|
||||
If you want to create something yourself, consider opening an issue first to
|
||||
discuss whether it is interesting to include this in GitLab.
|
||||
|
|
|
@ -18,8 +18,8 @@ in order to ensure the work is finished before the release date.
|
|||
|
||||
If you want to add a new feature that is not labeled, it is best to first create
|
||||
an issue (if there isn't one already) and leave a comment asking for it
|
||||
to be marked as `Accepting Merge Requests`. Please include screenshots or
|
||||
wireframes of the proposed feature if it will also change the UI.
|
||||
to be marked as `Accepting merge requests`. See the [feature proposals](issue_workflow.md#feature-proposals)
|
||||
section.
|
||||
|
||||
Merge requests should be submitted to the appropriate project at GitLab.com, for example
|
||||
[GitLab](https://gitlab.com/gitlab-org/gitlab/-/merge_requests),
|
||||
|
|
|
@ -71,6 +71,7 @@ topics and use cases. The most frequently required during database reviewing are
|
|||
- [Migrations style guide](../migration_style_guide.md) for creating safe SQL migrations.
|
||||
- [Avoiding downtime in migrations](../avoiding_downtime_in_migrations.md).
|
||||
- [SQL guidelines](../sql.md) for working with SQL queries.
|
||||
- [Guidelines for JiHu contributions with database migrations](https://about.gitlab.com/handbook/ceo/chief-of-staff-team/jihu-support/jihu-database-change-process.html)
|
||||
|
||||
## How to apply to become a database maintainer
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ module API
|
|||
# entity according to the current top-level entity options, such
|
||||
# as the current_user.
|
||||
def lazy_issuable_metadata
|
||||
BatchLoader.for(object).batch(key: [current_user, :issuable_metadata], replace_methods: false) do |models, loader, args|
|
||||
BatchLoader.for(object).batch(key: [current_user, :issuable_metadata]) do |models, loader, args|
|
||||
current_user = args[:key].first
|
||||
|
||||
issuable_metadata = Gitlab::IssuableMetadata.new(current_user, models)
|
||||
|
|
|
@ -1085,7 +1085,6 @@ module API
|
|||
attrs = declared_params(include_missing: false)
|
||||
|
||||
service = ::UserPreferences::UpdateService.new(current_user, attrs).execute
|
||||
|
||||
if service.success?
|
||||
present preferences, with: Entities::UserPreferences
|
||||
else
|
||||
|
|
|
@ -38,7 +38,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def vulnerability_finding
|
||||
BatchLoader.for(finding_key).batch(replace_methods: false) do |finding_keys, loader|
|
||||
BatchLoader.for(finding_key).batch do |finding_keys, loader|
|
||||
project_ids = finding_keys.map { |key| key[:project_id] }
|
||||
categories = finding_keys.map { |key| key[:category] }
|
||||
fingerprints = finding_keys.map { |key| key[:project_fingerprint] }
|
||||
|
|
|
@ -15,7 +15,7 @@ module Gitlab
|
|||
|
||||
def tagline
|
||||
[
|
||||
s_('InProductMarketing|Start a free trial of GitLab Ultimate – no CC required'),
|
||||
s_('InProductMarketing|Start a free trial of GitLab Ultimate – no credit card required'),
|
||||
s_('InProductMarketing|Improve app security with a 30-day trial'),
|
||||
s_('InProductMarketing|Start with a GitLab Ultimate free trial')
|
||||
][series]
|
||||
|
|
|
@ -11,7 +11,7 @@ module Gitlab
|
|||
|
||||
expose_attribute :noteable_type, :noteable_id, :commit_id, :file_path,
|
||||
:diff_hunk, :author, :note, :created_at, :updated_at,
|
||||
:github_id, :original_commit_id
|
||||
:original_commit_id, :note_id
|
||||
|
||||
NOTEABLE_ID_REGEX = %r{/pull/(?<iid>\d+)}i.freeze
|
||||
|
||||
|
@ -40,7 +40,7 @@ module Gitlab
|
|||
note: note.body,
|
||||
created_at: note.created_at,
|
||||
updated_at: note.updated_at,
|
||||
github_id: note.id
|
||||
note_id: note.id
|
||||
}
|
||||
|
||||
new(hash)
|
||||
|
@ -82,6 +82,14 @@ module Gitlab
|
|||
new_file: false
|
||||
}
|
||||
end
|
||||
|
||||
def github_identifiers
|
||||
{
|
||||
note_id: note_id,
|
||||
noteable_id: noteable_id,
|
||||
noteable_type: noteable_type
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -25,7 +25,6 @@ module Gitlab
|
|||
|
||||
hash = {
|
||||
iid: issue.number,
|
||||
github_id: issue.number,
|
||||
title: issue.title,
|
||||
description: issue.body,
|
||||
milestone_number: issue.milestone&.number,
|
||||
|
@ -75,6 +74,13 @@ module Gitlab
|
|||
def issuable_type
|
||||
pull_request? ? 'MergeRequest' : 'Issue'
|
||||
end
|
||||
|
||||
def github_identifiers
|
||||
{
|
||||
iid: iid,
|
||||
issuable_type: issuable_type
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,8 +16,7 @@ module Gitlab
|
|||
new(
|
||||
oid: lfs_object.oid,
|
||||
link: lfs_object.link,
|
||||
size: lfs_object.size,
|
||||
github_id: lfs_object.oid
|
||||
size: lfs_object.size
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -31,6 +30,12 @@ module Gitlab
|
|||
def initialize(attributes)
|
||||
@attributes = attributes
|
||||
end
|
||||
|
||||
def github_identifiers
|
||||
{
|
||||
oid: oid
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,7 +10,7 @@ module Gitlab
|
|||
attr_reader :attributes
|
||||
|
||||
expose_attribute :noteable_id, :noteable_type, :author, :note,
|
||||
:created_at, :updated_at, :github_id
|
||||
:created_at, :updated_at, :note_id
|
||||
|
||||
NOTEABLE_TYPE_REGEX = %r{/(?<type>(pull|issues))/(?<iid>\d+)}i.freeze
|
||||
|
||||
|
@ -42,7 +42,7 @@ module Gitlab
|
|||
note: note.body,
|
||||
created_at: note.created_at,
|
||||
updated_at: note.updated_at,
|
||||
github_id: note.id
|
||||
note_id: note.id
|
||||
}
|
||||
|
||||
new(hash)
|
||||
|
@ -64,6 +64,14 @@ module Gitlab
|
|||
end
|
||||
|
||||
alias_method :issuable_type, :noteable_type
|
||||
|
||||
def github_identifiers
|
||||
{
|
||||
note_id: note_id,
|
||||
noteable_id: noteable_id,
|
||||
noteable_type: noteable_type
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -25,7 +25,6 @@ module Gitlab
|
|||
|
||||
hash = {
|
||||
iid: pr.number,
|
||||
github_id: pr.number,
|
||||
title: pr.title,
|
||||
description: pr.body,
|
||||
source_branch: pr.head.ref,
|
||||
|
@ -108,6 +107,13 @@ module Gitlab
|
|||
def issuable_type
|
||||
'MergeRequest'
|
||||
end
|
||||
|
||||
def github_identifiers
|
||||
{
|
||||
iid: iid,
|
||||
issuable_type: issuable_type
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,7 +9,7 @@ module Gitlab
|
|||
|
||||
attr_reader :attributes
|
||||
|
||||
expose_attribute :author, :note, :review_type, :submitted_at, :github_id, :merge_request_id
|
||||
expose_attribute :author, :note, :review_type, :submitted_at, :merge_request_id, :review_id
|
||||
|
||||
def self.from_api_response(review)
|
||||
user = Representation::User.from_api_response(review.user) if review.user
|
||||
|
@ -20,7 +20,7 @@ module Gitlab
|
|||
note: review.body,
|
||||
review_type: review.state,
|
||||
submitted_at: review.submitted_at,
|
||||
github_id: review.id
|
||||
review_id: review.id
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -43,6 +43,13 @@ module Gitlab
|
|||
def approval?
|
||||
review_type == 'APPROVED'
|
||||
end
|
||||
|
||||
def github_identifiers
|
||||
{
|
||||
review_id: review_id,
|
||||
merge_request_id: merge_request_id
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,7 +17,6 @@ module Gitlab
|
|||
def self.from_api_response(user)
|
||||
new(
|
||||
id: user.id,
|
||||
github_id: user.id,
|
||||
login: user.login
|
||||
)
|
||||
end
|
||||
|
|
|
@ -6,10 +6,11 @@ module Gitlab
|
|||
class Iterator
|
||||
UnsupportedScopeOrder = Class.new(StandardError)
|
||||
|
||||
def initialize(scope:, use_union_optimization: true, in_operator_optimization_options: nil)
|
||||
def initialize(scope:, cursor: {}, use_union_optimization: true, in_operator_optimization_options: nil)
|
||||
@scope, success = Gitlab::Pagination::Keyset::SimpleOrderBuilder.build(scope)
|
||||
raise(UnsupportedScopeOrder, 'The order on the scope does not support keyset pagination') unless success
|
||||
|
||||
@cursor = cursor
|
||||
@order = Gitlab::Pagination::Keyset::Order.extract_keyset_order_object(scope)
|
||||
@use_union_optimization = in_operator_optimization_options ? false : use_union_optimization
|
||||
@in_operator_optimization_options = in_operator_optimization_options
|
||||
|
@ -17,11 +18,9 @@ module Gitlab
|
|||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def each_batch(of: 1000)
|
||||
cursor_attributes = {}
|
||||
|
||||
loop do
|
||||
current_scope = scope.dup
|
||||
relation = order.apply_cursor_conditions(current_scope, cursor_attributes, keyset_options)
|
||||
relation = order.apply_cursor_conditions(current_scope, cursor, keyset_options)
|
||||
relation = relation.reorder(order) unless @in_operator_optimization_options
|
||||
relation = relation.limit(of)
|
||||
|
||||
|
@ -30,14 +29,14 @@ module Gitlab
|
|||
last_record = relation.last
|
||||
break unless last_record
|
||||
|
||||
cursor_attributes = order.cursor_attributes_for_node(last_record)
|
||||
@cursor = order.cursor_attributes_for_node(last_record)
|
||||
end
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
private
|
||||
|
||||
attr_reader :scope, :order
|
||||
attr_reader :scope, :cursor, :order
|
||||
|
||||
def keyset_options
|
||||
{
|
||||
|
|
|
@ -16,9 +16,6 @@ msgstr ""
|
|||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
|
||||
|
||||
msgid " %{name}, confirm your email address now! "
|
||||
msgstr ""
|
||||
|
||||
msgid " %{start} to %{end}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1135,9 +1132,6 @@ msgstr ""
|
|||
msgid "(we need your current password to confirm your changes)"
|
||||
msgstr ""
|
||||
|
||||
msgid "* * * * *"
|
||||
msgstr ""
|
||||
|
||||
msgid "+ %{amount} more"
|
||||
msgstr ""
|
||||
|
||||
|
@ -3587,6 +3581,9 @@ msgstr ""
|
|||
msgid "An error occurred while enabling Service Desk."
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred while fetching Markdown preview"
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred while fetching ancestors"
|
||||
msgstr ""
|
||||
|
||||
|
@ -3617,9 +3614,6 @@ msgstr ""
|
|||
msgid "An error occurred while fetching label colors."
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred while fetching markdown preview"
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred while fetching participants"
|
||||
msgstr ""
|
||||
|
||||
|
@ -7345,9 +7339,6 @@ msgstr ""
|
|||
msgid "ClusterAgents|You will need to create a token to connect to your agent"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterAgent|This feature is only available for premium plans"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterAgent|User has insufficient permissions to create a token for this project"
|
||||
msgstr ""
|
||||
|
||||
|
@ -9999,7 +9990,7 @@ msgstr ""
|
|||
msgid "Custom notification events"
|
||||
msgstr ""
|
||||
|
||||
msgid "Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notificationLinkStart} notification emails%{notificationLinkEnd}."
|
||||
msgid "Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notificationLinkStart}notification emails%{notificationLinkEnd}."
|
||||
msgstr ""
|
||||
|
||||
msgid "Custom project templates"
|
||||
|
@ -10198,10 +10189,10 @@ msgid_plural "CycleAnalytics|Showing %{subjectFilterText} and %{selectedLabelsCo
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "CycleAnalytics|Showing data for group '%{groupName}' and %{selectedProjectCount} projects from %{startDate} to %{endDate}"
|
||||
msgid "CycleAnalytics|Showing data for group '%{groupName}' and %{selectedProjectCount} projects from %{createdAfter} to %{createdBefore}"
|
||||
msgstr ""
|
||||
|
||||
msgid "CycleAnalytics|Showing data for group '%{groupName}' from %{startDate} to %{endDate}"
|
||||
msgid "CycleAnalytics|Showing data for group '%{groupName}' from %{createdAfter} to %{createdBefore}"
|
||||
msgstr ""
|
||||
|
||||
msgid "CycleAnalytics|Stages"
|
||||
|
@ -13366,7 +13357,7 @@ msgstr ""
|
|||
msgid "Error parsing CSV file. Please make sure it has"
|
||||
msgstr ""
|
||||
|
||||
msgid "Error rendering markdown preview"
|
||||
msgid "Error rendering Markdown preview"
|
||||
msgstr ""
|
||||
|
||||
msgid "Error saving label update."
|
||||
|
@ -17606,7 +17597,7 @@ msgstr ""
|
|||
msgid "InProductMarketing|Start a GitLab Ultimate trial today in less than one minute, no credit card required."
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|Start a free trial of GitLab Ultimate – no CC required"
|
||||
msgid "InProductMarketing|Start a free trial of GitLab Ultimate – no credit card required"
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|Start a trial"
|
||||
|
@ -24487,6 +24478,18 @@ msgstr ""
|
|||
msgid "Password was successfully updated. Please sign in again."
|
||||
msgstr ""
|
||||
|
||||
msgid "PasswordPrompt|Confirm password"
|
||||
msgstr ""
|
||||
|
||||
msgid "PasswordPrompt|Confirm password to continue"
|
||||
msgstr ""
|
||||
|
||||
msgid "PasswordPrompt|Password is required"
|
||||
msgstr ""
|
||||
|
||||
msgid "PasswordPrompt|Please enter your password to confirm"
|
||||
msgstr ""
|
||||
|
||||
msgid "Passwords should be unique and not used for any other sites or services."
|
||||
msgstr ""
|
||||
|
||||
|
@ -24862,6 +24865,48 @@ msgstr ""
|
|||
msgid "PipelineSchedules|Variables"
|
||||
msgstr ""
|
||||
|
||||
msgid "PipelineSource|API"
|
||||
msgstr ""
|
||||
|
||||
msgid "PipelineSource|Chat"
|
||||
msgstr ""
|
||||
|
||||
msgid "PipelineSource|External"
|
||||
msgstr ""
|
||||
|
||||
msgid "PipelineSource|External Pull Request"
|
||||
msgstr ""
|
||||
|
||||
msgid "PipelineSource|Merge Request"
|
||||
msgstr ""
|
||||
|
||||
msgid "PipelineSource|On-Demand DAST Scan"
|
||||
msgstr ""
|
||||
|
||||
msgid "PipelineSource|On-Demand DAST Validation"
|
||||
msgstr ""
|
||||
|
||||
msgid "PipelineSource|Parent Pipeline"
|
||||
msgstr ""
|
||||
|
||||
msgid "PipelineSource|Pipeline"
|
||||
msgstr ""
|
||||
|
||||
msgid "PipelineSource|Push"
|
||||
msgstr ""
|
||||
|
||||
msgid "PipelineSource|Schedule"
|
||||
msgstr ""
|
||||
|
||||
msgid "PipelineSource|Trigger"
|
||||
msgstr ""
|
||||
|
||||
msgid "PipelineSource|Web"
|
||||
msgstr ""
|
||||
|
||||
msgid "PipelineSource|Web IDE"
|
||||
msgstr ""
|
||||
|
||||
msgid "PipelineStatusTooltip|Pipeline: %{ciStatus}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -25162,51 +25207,9 @@ msgstr ""
|
|||
msgid "Pipeline|Source"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipeline|Source|API"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipeline|Source|Chat"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipeline|Source|External"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipeline|Source|External Pull Request"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipeline|Source|Merge Request"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipeline|Source|On-Demand DAST Scan"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipeline|Source|On-Demand DAST Validation"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipeline|Source|Parent Pipeline"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipeline|Source|Pipeline"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipeline|Source|Push"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipeline|Source|Schedule"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipeline|Source|Security Policy"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipeline|Source|Trigger"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipeline|Source|Web"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipeline|Source|Web IDE"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used by default."
|
||||
msgstr ""
|
||||
|
||||
|
@ -25933,6 +25936,9 @@ msgstr ""
|
|||
msgid "Profiles|Enter your name, so people you know can recognize you"
|
||||
msgstr ""
|
||||
|
||||
msgid "Profiles|Enter your password to confirm the email change"
|
||||
msgstr ""
|
||||
|
||||
msgid "Profiles|Enter your pronouns to let people know how to refer to you"
|
||||
msgstr ""
|
||||
|
||||
|
@ -29748,16 +29754,16 @@ msgstr ""
|
|||
msgid "SearchCodeResults|of %{link_to_project}"
|
||||
msgstr ""
|
||||
|
||||
msgid "SearchResults|Showing %{count} %{scope} for%{term_element}"
|
||||
msgid "SearchResults|Showing %{count} %{scope} for %{term_element}"
|
||||
msgstr ""
|
||||
|
||||
msgid "SearchResults|Showing %{count} %{scope} for%{term_element} in your personal and project snippets"
|
||||
msgid "SearchResults|Showing %{count} %{scope} for %{term_element} in your personal and project snippets"
|
||||
msgstr ""
|
||||
|
||||
msgid "SearchResults|Showing %{from} - %{to} of %{count} %{scope} for%{term_element}"
|
||||
msgid "SearchResults|Showing %{from} - %{to} of %{count} %{scope} for %{term_element}"
|
||||
msgstr ""
|
||||
|
||||
msgid "SearchResults|Showing %{from} - %{to} of %{count} %{scope} for%{term_element} in your personal and project snippets"
|
||||
msgid "SearchResults|Showing %{from} - %{to} of %{count} %{scope} for %{term_element} in your personal and project snippets"
|
||||
msgstr ""
|
||||
|
||||
msgid "SearchResults|code result"
|
||||
|
@ -31274,6 +31280,9 @@ msgstr ""
|
|||
msgid "Showing all issues"
|
||||
msgstr ""
|
||||
|
||||
msgid "Showing data for group '%{group_name}' from Nov 1, 2019 to Dec 31, 2019"
|
||||
msgstr ""
|
||||
|
||||
msgid "Showing data for workflow items created in this date range. Date range cannot exceed %{maxDateRange} days."
|
||||
msgstr ""
|
||||
|
||||
|
@ -35150,7 +35159,7 @@ msgstr ""
|
|||
msgid "TimeTrackingEstimated|Est"
|
||||
msgstr ""
|
||||
|
||||
msgid "TimeTracking|%{startTag}Spent: %{endTag}%{timeSpentHumanReadable}"
|
||||
msgid "TimeTracking|%{spentStart}Spent: %{spentEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "TimeTracking|Estimated:"
|
||||
|
@ -35388,6 +35397,9 @@ msgstr ""
|
|||
msgid "To connect an SVN repository, check out %{svn_link}."
|
||||
msgstr ""
|
||||
|
||||
msgid "To continue using GitLab Enterprise Edition, upload the %{codeOpen}.gitlab-license%{codeClose} file or enter the license key you have received from GitLab Inc."
|
||||
msgstr ""
|
||||
|
||||
msgid "To continue, you need to select the link in the confirmation email we sent to verify your email address. If you didn't get our email, select %{strongStart}Resend confirmation email.%{strongEnd}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -35493,6 +35505,9 @@ msgstr ""
|
|||
msgid "To specify the notification level per project of a group you belong to, you need to visit project page and change notification level there."
|
||||
msgstr ""
|
||||
|
||||
msgid "To start using GitLab Enterprise Edition, upload the %{codeOpen}.gitlab-license%{codeClose} file or enter the license key you have received from GitLab Inc."
|
||||
msgstr ""
|
||||
|
||||
msgid "To unsubscribe from this issue, please paste the following link into your browser:"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
require('spec_helper')
|
||||
|
||||
RSpec.describe ProfilesController, :request_store do
|
||||
let(:user) { create(:user) }
|
||||
let(:password) { 'longsecret987!' }
|
||||
let(:user) { create(:user, password: password) }
|
||||
|
||||
describe 'POST update' do
|
||||
it 'does not update password' do
|
||||
|
@ -23,7 +24,7 @@ RSpec.describe ProfilesController, :request_store do
|
|||
sign_in(user)
|
||||
|
||||
put :update,
|
||||
params: { user: { email: "john@gmail.com", name: "John" } }
|
||||
params: { user: { email: "john@gmail.com", name: "John", validation_password: password } }
|
||||
|
||||
user.reload
|
||||
|
||||
|
|
|
@ -139,6 +139,8 @@ FactoryBot.define do
|
|||
end
|
||||
|
||||
factory :omniauth_user do
|
||||
password_automatically_set { true }
|
||||
|
||||
transient do
|
||||
extern_uid { '123456' }
|
||||
provider { 'ldapmain' }
|
||||
|
|
|
@ -121,7 +121,7 @@ RSpec.describe 'Admin Mode Login' do
|
|||
end
|
||||
|
||||
context 'when logging in via omniauth' do
|
||||
let(:user) { create(:omniauth_user, :admin, :two_factor, extern_uid: 'my-uid', provider: 'saml')}
|
||||
let(:user) { create(:omniauth_user, :admin, :two_factor, extern_uid: 'my-uid', provider: 'saml', password_automatically_set: false)}
|
||||
let(:mock_saml_response) do
|
||||
File.read('spec/fixtures/authentication/saml_response.xml')
|
||||
end
|
||||
|
|
|
@ -19,6 +19,17 @@ RSpec.describe 'User edit profile' do
|
|||
wait_for_requests if respond_to?(:wait_for_requests)
|
||||
end
|
||||
|
||||
def update_user_email
|
||||
fill_in 'user_email', with: 'new-email@example.com'
|
||||
click_button 'Update profile settings'
|
||||
end
|
||||
|
||||
def confirm_password(password)
|
||||
fill_in 'password-confirmation', with: password
|
||||
click_button 'Confirm password'
|
||||
wait_for_requests if respond_to?(:wait_for_requests)
|
||||
end
|
||||
|
||||
def visit_user
|
||||
visit user_path(user)
|
||||
wait_for_requests
|
||||
|
@ -88,16 +99,42 @@ RSpec.describe 'User edit profile' do
|
|||
expect(page).to have_content('Website url is not a valid URL')
|
||||
end
|
||||
|
||||
describe 'when I change my email' do
|
||||
describe 'when I change my email', :js do
|
||||
before do
|
||||
user.send_reset_password_instructions
|
||||
end
|
||||
|
||||
it 'will prompt to confirm my password' do
|
||||
expect(user.reset_password_token?).to be true
|
||||
|
||||
update_user_email
|
||||
|
||||
expect(page).to have_selector('[data-testid="password-prompt-modal"]')
|
||||
end
|
||||
|
||||
context 'when prompted to confirm password' do
|
||||
before do
|
||||
update_user_email
|
||||
end
|
||||
|
||||
it 'with the correct password successfully updates' do
|
||||
confirm_password(user.password)
|
||||
|
||||
expect(page).to have_text("Profile was successfully updated")
|
||||
end
|
||||
|
||||
it 'with the incorrect password fails to update' do
|
||||
confirm_password("Fake password")
|
||||
|
||||
expect(page).to have_text("Invalid password")
|
||||
end
|
||||
end
|
||||
|
||||
it 'clears the reset password token' do
|
||||
expect(user.reset_password_token?).to be true
|
||||
|
||||
fill_in 'user_email', with: 'new-email@example.com'
|
||||
submit_settings
|
||||
update_user_email
|
||||
confirm_password(user.password)
|
||||
|
||||
user.reload
|
||||
expect(user.confirmation_token).not_to be_nil
|
||||
|
|
|
@ -874,7 +874,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_shared_state do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when the user does not have an email configured' do
|
||||
context 'when the user does not have an email configured', :js do
|
||||
let(:user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'saml', email: 'temp-email-for-oauth-user@gitlab.localhost') }
|
||||
|
||||
before do
|
||||
|
|
|
@ -161,42 +161,54 @@ RSpec.describe MembersFinder, '#execute' do
|
|||
end
|
||||
|
||||
context 'when :invited_groups is passed' do
|
||||
subject { described_class.new(project, user2).execute(include_relations: [:inherited, :direct, :invited_groups]) }
|
||||
shared_examples 'with invited_groups param' do
|
||||
subject { described_class.new(project, user2).execute(include_relations: [:inherited, :direct, :invited_groups]) }
|
||||
|
||||
let_it_be(:linked_group) { create(:group, :public) }
|
||||
let_it_be(:nested_linked_group) { create(:group, parent: linked_group) }
|
||||
let_it_be(:linked_group_member) { linked_group.add_guest(user1) }
|
||||
let_it_be(:nested_linked_group_member) { nested_linked_group.add_guest(user2) }
|
||||
let_it_be(:linked_group) { create(:group, :public) }
|
||||
let_it_be(:nested_linked_group) { create(:group, parent: linked_group) }
|
||||
let_it_be(:linked_group_member) { linked_group.add_guest(user1) }
|
||||
let_it_be(:nested_linked_group_member) { nested_linked_group.add_guest(user2) }
|
||||
|
||||
it 'includes all the invited_groups members including members inherited from ancestor groups' do
|
||||
create(:project_group_link, project: project, group: nested_linked_group)
|
||||
it 'includes all the invited_groups members including members inherited from ancestor groups' do
|
||||
create(:project_group_link, project: project, group: nested_linked_group)
|
||||
|
||||
expect(subject).to contain_exactly(linked_group_member, nested_linked_group_member)
|
||||
end
|
||||
|
||||
it 'includes all the invited_groups members' do
|
||||
create(:project_group_link, project: project, group: linked_group)
|
||||
|
||||
expect(subject).to contain_exactly(linked_group_member)
|
||||
end
|
||||
|
||||
it 'excludes group_members not visible to the user' do
|
||||
create(:project_group_link, project: project, group: linked_group)
|
||||
private_linked_group = create(:group, :private)
|
||||
private_linked_group.add_developer(user3)
|
||||
create(:project_group_link, project: project, group: private_linked_group)
|
||||
|
||||
expect(subject).to contain_exactly(linked_group_member)
|
||||
end
|
||||
|
||||
context 'when the user is a member of invited group and ancestor groups' do
|
||||
it 'returns the highest access_level for the user limited by project_group_link.group_access', :nested_groups do
|
||||
create(:project_group_link, project: project, group: nested_linked_group, group_access: Gitlab::Access::REPORTER)
|
||||
nested_linked_group.add_developer(user1)
|
||||
|
||||
expect(subject.map(&:user)).to contain_exactly(user1, user2)
|
||||
expect(subject.max_by(&:access_level).access_level).to eq(Gitlab::Access::REPORTER)
|
||||
expect(subject).to contain_exactly(linked_group_member, nested_linked_group_member)
|
||||
end
|
||||
|
||||
it 'includes all the invited_groups members' do
|
||||
create(:project_group_link, project: project, group: linked_group)
|
||||
|
||||
expect(subject).to contain_exactly(linked_group_member)
|
||||
end
|
||||
|
||||
it 'excludes group_members not visible to the user' do
|
||||
create(:project_group_link, project: project, group: linked_group)
|
||||
private_linked_group = create(:group, :private)
|
||||
private_linked_group.add_developer(user3)
|
||||
create(:project_group_link, project: project, group: private_linked_group)
|
||||
|
||||
expect(subject).to contain_exactly(linked_group_member)
|
||||
end
|
||||
|
||||
context 'when the user is a member of invited group and ancestor groups' do
|
||||
it 'returns the highest access_level for the user limited by project_group_link.group_access', :nested_groups do
|
||||
create(:project_group_link, project: project, group: nested_linked_group, group_access: Gitlab::Access::REPORTER)
|
||||
nested_linked_group.add_developer(user1)
|
||||
|
||||
expect(subject.map(&:user)).to contain_exactly(user1, user2)
|
||||
expect(subject.max_by(&:access_level).access_level).to eq(Gitlab::Access::REPORTER)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'with invited_groups param'
|
||||
|
||||
context 'when feature flag :linear_members_finder_ancestor_scopes is disabled' do
|
||||
before do
|
||||
stub_feature_flags(linear_members_finder_ancestor_scopes: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'with invited_groups param'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -99,6 +99,12 @@ Array [
|
|||
"label": "30 days",
|
||||
"variable": 30,
|
||||
},
|
||||
Object {
|
||||
"default": false,
|
||||
"key": "SIXTY_DAYS",
|
||||
"label": "60 days",
|
||||
"variable": 60,
|
||||
},
|
||||
Object {
|
||||
"default": true,
|
||||
"key": "NINETY_DAYS",
|
||||
|
|
|
@ -44,7 +44,7 @@ exports[`Settings Form Keep Regex matches snapshot 1`] = `
|
|||
exports[`Settings Form OlderThan matches snapshot 1`] = `
|
||||
<expiration-dropdown-stub
|
||||
data-testid="older-than-dropdown"
|
||||
formoptions="[object Object],[object Object],[object Object],[object Object],[object Object]"
|
||||
formoptions="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]"
|
||||
label="Remove tags older than:"
|
||||
name="older-than"
|
||||
value="FOURTEEN_DAYS"
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
import { GlModal } from '@gitlab/ui';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import {
|
||||
I18N_PASSWORD_PROMPT_CANCEL_BUTTON,
|
||||
I18N_PASSWORD_PROMPT_CONFIRM_BUTTON,
|
||||
} from '~/pages/profiles/password_prompt/constants';
|
||||
import PasswordPromptModal from '~/pages/profiles/password_prompt/password_prompt_modal.vue';
|
||||
|
||||
const createComponent = ({ props }) => {
|
||||
return shallowMountExtended(PasswordPromptModal, {
|
||||
propsData: {
|
||||
...props,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
describe('Password prompt modal', () => {
|
||||
let wrapper;
|
||||
|
||||
const mockPassword = 'not+fake+shady+password';
|
||||
const mockEvent = { preventDefault: jest.fn() };
|
||||
const handleConfirmPasswordSpy = jest.fn();
|
||||
|
||||
const findField = () => wrapper.findByTestId('password-prompt-field');
|
||||
const findModal = () => wrapper.findComponent(GlModal);
|
||||
const findConfirmBtn = () => findModal().props('actionPrimary');
|
||||
const findConfirmBtnDisabledState = () =>
|
||||
findModal().props('actionPrimary').attributes[2].disabled;
|
||||
|
||||
const findCancelBtn = () => findModal().props('actionCancel');
|
||||
|
||||
const submitModal = () => findModal().vm.$emit('primary', mockEvent);
|
||||
const setPassword = (newPw) => findField().vm.$emit('input', newPw);
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent({
|
||||
props: {
|
||||
handleConfirmPassword: handleConfirmPasswordSpy,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('renders the password field', () => {
|
||||
expect(findField().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders the confirm button', () => {
|
||||
expect(findConfirmBtn().text).toEqual(I18N_PASSWORD_PROMPT_CONFIRM_BUTTON);
|
||||
});
|
||||
|
||||
it('renders the cancel button', () => {
|
||||
expect(findCancelBtn().text).toEqual(I18N_PASSWORD_PROMPT_CANCEL_BUTTON);
|
||||
});
|
||||
|
||||
describe('confirm button', () => {
|
||||
describe('with a valid password', () => {
|
||||
it('calls the `handleConfirmPassword` method when clicked', async () => {
|
||||
setPassword(mockPassword);
|
||||
submitModal();
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(handleConfirmPasswordSpy).toHaveBeenCalledTimes(1);
|
||||
expect(handleConfirmPasswordSpy).toHaveBeenCalledWith(mockPassword);
|
||||
});
|
||||
|
||||
it('enables the confirm button', async () => {
|
||||
setPassword(mockPassword);
|
||||
|
||||
expect(findConfirmBtnDisabledState()).toBe(true);
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findConfirmBtnDisabledState()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('without a valid password is disabled', async () => {
|
||||
setPassword('');
|
||||
|
||||
expect(findConfirmBtnDisabledState()).toBe(true);
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findConfirmBtnDisabledState()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,4 +1,7 @@
|
|||
import { registerExtension, extensions } from '~/vue_merge_request_widget/components/extensions';
|
||||
import {
|
||||
registerExtension,
|
||||
registeredExtensions,
|
||||
} from '~/vue_merge_request_widget/components/extensions';
|
||||
import ExtensionBase from '~/vue_merge_request_widget/components/extensions/base.vue';
|
||||
|
||||
describe('MR widget extension registering', () => {
|
||||
|
@ -14,7 +17,7 @@ describe('MR widget extension registering', () => {
|
|||
},
|
||||
});
|
||||
|
||||
expect(extensions[0]).toEqual(
|
||||
expect(registeredExtensions.extensions[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
extends: ExtensionBase,
|
||||
name: 'Test',
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import { GlIcon, GlLoadingIcon } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue';
|
||||
|
||||
let wrapper;
|
||||
|
||||
function factory(propsData = {}) {
|
||||
wrapper = shallowMount(StatusIcon, {
|
||||
propsData,
|
||||
});
|
||||
}
|
||||
|
||||
describe('MR widget extensions status icon', () => {
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('renders loading icon', () => {
|
||||
factory({ name: 'test', isLoading: true, iconName: 'status-failed' });
|
||||
|
||||
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders status icon', () => {
|
||||
factory({ name: 'test', isLoading: false, iconName: 'status-failed' });
|
||||
|
||||
expect(wrapper.findComponent(GlIcon).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(GlIcon).props('name')).toBe('status-failed');
|
||||
});
|
||||
|
||||
it('sets aria-label for status icon', () => {
|
||||
factory({ name: 'test', isLoading: false, iconName: 'status-failed' });
|
||||
|
||||
expect(wrapper.findComponent(GlIcon).props('ariaLabel')).toBe('Failed test');
|
||||
});
|
||||
});
|
|
@ -1,13 +1,16 @@
|
|||
import { GlBadge, GlLink, GlIcon } from '@gitlab/ui';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { securityReportMergeRequestDownloadPathsQueryResponse } from 'jest/vue_shared/security_reports/mock_data';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { setFaviconOverlay } from '~/lib/utils/favicon';
|
||||
import notify from '~/lib/utils/notify';
|
||||
import SmartInterval from '~/smart_interval';
|
||||
import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
|
||||
import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
|
||||
import eventHub from '~/vue_merge_request_widget/event_hub';
|
||||
import MrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue';
|
||||
|
@ -15,6 +18,7 @@ import { stateKey } from '~/vue_merge_request_widget/stores/state_maps';
|
|||
import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql';
|
||||
import { faviconDataUrl, overlayDataUrl } from '../lib/utils/mock_data';
|
||||
import mockData from './mock_data';
|
||||
import testExtension from './test_extension';
|
||||
|
||||
jest.mock('~/smart_interval');
|
||||
|
||||
|
@ -879,4 +883,46 @@ describe('MrWidgetOptions', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mock extension', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders collapsed data', async () => {
|
||||
registerExtension(testExtension);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.text()).toContain('Test extension summary count: 1');
|
||||
});
|
||||
|
||||
it('renders full data', async () => {
|
||||
registerExtension(testExtension);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
wrapper
|
||||
.find('[data-testid="widget-extension"] [data-testid="toggle-button"]')
|
||||
.trigger('click');
|
||||
|
||||
await Vue.nextTick();
|
||||
|
||||
const collapsedSection = wrapper.find('[data-testid="widget-extension-collapsed-section"]');
|
||||
expect(collapsedSection.exists()).toBe(true);
|
||||
expect(collapsedSection.text()).toContain('Hello world');
|
||||
|
||||
// Renders icon in the row
|
||||
expect(collapsedSection.find(GlIcon).exists()).toBe(true);
|
||||
expect(collapsedSection.find(GlIcon).props('name')).toBe('status_failed_borderless');
|
||||
|
||||
// Renders badge in the row
|
||||
expect(collapsedSection.find(GlBadge).exists()).toBe(true);
|
||||
expect(collapsedSection.find(GlBadge).text()).toBe('Closed');
|
||||
|
||||
// Renders a link in the row
|
||||
expect(collapsedSection.find(GlLink).exists()).toBe(true);
|
||||
expect(collapsedSection.find(GlLink).text()).toBe('GitLab.com');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
36
spec/frontend/vue_mr_widget/test_extension.js
Normal file
36
spec/frontend/vue_mr_widget/test_extension.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
export default {
|
||||
name: 'WidgetTestExtension',
|
||||
props: ['targetProjectFullPath'],
|
||||
computed: {
|
||||
summary({ count, targetProjectFullPath }) {
|
||||
return `Test extension summary count: ${count} & ${targetProjectFullPath}`;
|
||||
},
|
||||
statusIcon({ count }) {
|
||||
return count > 0 ? 'warning' : 'success';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
fetchCollapsedData({ targetProjectFullPath }) {
|
||||
return Promise.resolve({ targetProjectFullPath, count: 1 });
|
||||
},
|
||||
fetchFullData() {
|
||||
return Promise.resolve([
|
||||
{
|
||||
id: 1,
|
||||
text: 'Hello world',
|
||||
icon: {
|
||||
name: 'status_failed_borderless',
|
||||
class: 'text-danger',
|
||||
},
|
||||
badge: {
|
||||
text: 'Closed',
|
||||
},
|
||||
link: {
|
||||
href: 'https://gitlab.com',
|
||||
text: 'GitLab.com',
|
||||
},
|
||||
},
|
||||
]);
|
||||
},
|
||||
},
|
||||
};
|
61
spec/graphql/mutations/clusters/agent_tokens/create_spec.rb
Normal file
61
spec/graphql/mutations/clusters/agent_tokens/create_spec.rb
Normal file
|
@ -0,0 +1,61 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Mutations::Clusters::AgentTokens::Create do
|
||||
subject(:mutation) { described_class.new(object: nil, context: context, field: nil) }
|
||||
|
||||
let_it_be(:cluster_agent) { create(:cluster_agent) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
let(:context) do
|
||||
GraphQL::Query::Context.new(
|
||||
query: OpenStruct.new(schema: nil),
|
||||
values: { current_user: user },
|
||||
object: nil
|
||||
)
|
||||
end
|
||||
|
||||
specify { expect(described_class).to require_graphql_authorizations(:create_cluster) }
|
||||
|
||||
describe '#resolve' do
|
||||
let(:description) { 'new token!' }
|
||||
let(:name) { 'new name' }
|
||||
|
||||
subject { mutation.resolve(cluster_agent_id: cluster_agent.to_global_id, description: description, name: name) }
|
||||
|
||||
context 'without token permissions' do
|
||||
it 'raises an error if the resource is not accessible to the user' do
|
||||
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with user permissions' do
|
||||
before do
|
||||
cluster_agent.project.add_maintainer(user)
|
||||
end
|
||||
|
||||
it 'creates a new token', :aggregate_failures do
|
||||
expect { subject }.to change { ::Clusters::AgentToken.count }.by(1)
|
||||
expect(subject[:errors]).to eq([])
|
||||
end
|
||||
|
||||
it 'returns token information', :aggregate_failures do
|
||||
token = subject[:token]
|
||||
|
||||
expect(subject[:secret]).not_to be_nil
|
||||
expect(token.created_by_user).to eq(user)
|
||||
expect(token.description).to eq(description)
|
||||
expect(token.name).to eq(name)
|
||||
end
|
||||
|
||||
context 'invalid params' do
|
||||
subject { mutation.resolve(cluster_agent_id: cluster_agent.id) }
|
||||
|
||||
it 'generates an error message when id invalid', :aggregate_failures do
|
||||
expect { subject }.to raise_error(::GraphQL::CoercionError)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
52
spec/graphql/mutations/clusters/agent_tokens/delete_spec.rb
Normal file
52
spec/graphql/mutations/clusters/agent_tokens/delete_spec.rb
Normal file
|
@ -0,0 +1,52 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Mutations::Clusters::AgentTokens::Delete do
|
||||
let(:token) { create(:cluster_agent_token) }
|
||||
let(:user) { create(:user) }
|
||||
|
||||
let(:mutation) do
|
||||
described_class.new(
|
||||
object: double,
|
||||
context: { current_user: user },
|
||||
field: double
|
||||
)
|
||||
end
|
||||
|
||||
it { expect(described_class.graphql_name).to eq('ClusterAgentTokenDelete') }
|
||||
it { expect(described_class).to require_graphql_authorizations(:admin_cluster) }
|
||||
|
||||
describe '#resolve' do
|
||||
let(:global_id) { token.to_global_id }
|
||||
|
||||
subject { mutation.resolve(id: global_id) }
|
||||
|
||||
context 'without user permissions' do
|
||||
it 'fails to delete the cluster agent', :aggregate_failures do
|
||||
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
|
||||
expect { token.reload }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'with user permissions' do
|
||||
before do
|
||||
token.agent.project.add_maintainer(user)
|
||||
end
|
||||
|
||||
it 'deletes a cluster agent', :aggregate_failures do
|
||||
expect { subject }.to change { ::Clusters::AgentToken.count }.by(-1)
|
||||
expect { token.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid params' do
|
||||
let(:global_id) { token.id }
|
||||
|
||||
it 'raises an error if the cluster agent id is invalid', :aggregate_failures do
|
||||
expect { subject }.to raise_error(::GraphQL::CoercionError)
|
||||
expect { token.reload }.not_to raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
50
spec/graphql/mutations/clusters/agents/create_spec.rb
Normal file
50
spec/graphql/mutations/clusters/agents/create_spec.rb
Normal file
|
@ -0,0 +1,50 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Mutations::Clusters::Agents::Create do
|
||||
subject(:mutation) { described_class.new(object: nil, context: context, field: nil) }
|
||||
|
||||
let(:project) { create(:project, :public, :repository) }
|
||||
let(:user) { create(:user) }
|
||||
let(:context) do
|
||||
GraphQL::Query::Context.new(
|
||||
query: OpenStruct.new(schema: nil),
|
||||
values: { current_user: user },
|
||||
object: nil
|
||||
)
|
||||
end
|
||||
|
||||
specify { expect(described_class).to require_graphql_authorizations(:create_cluster) }
|
||||
|
||||
describe '#resolve' do
|
||||
subject { mutation.resolve(project_path: project.full_path, name: 'test-agent') }
|
||||
|
||||
context 'without project permissions' do
|
||||
it 'raises an error if the resource is not accessible to the user' do
|
||||
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with user permissions' do
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
end
|
||||
|
||||
it 'creates a new clusters_agent', :aggregate_failures do
|
||||
expect { subject }.to change { ::Clusters::Agent.count }.by(1)
|
||||
expect(subject[:cluster_agent].name).to eq('test-agent')
|
||||
expect(subject[:errors]).to eq([])
|
||||
end
|
||||
|
||||
context 'invalid params' do
|
||||
subject { mutation.resolve(project_path: project.full_path, name: '@bad_name!') }
|
||||
|
||||
it 'generates an error message when name is invalid', :aggregate_failures do
|
||||
expect(subject[:clusters_agent]).to be_nil
|
||||
expect(subject[:errors]).to eq(["Name can contain only lowercase letters, digits, and '-', but cannot start or end with '-'"])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
51
spec/graphql/mutations/clusters/agents/delete_spec.rb
Normal file
51
spec/graphql/mutations/clusters/agents/delete_spec.rb
Normal file
|
@ -0,0 +1,51 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Mutations::Clusters::Agents::Delete do
|
||||
subject(:mutation) { described_class.new(object: nil, context: context, field: nil) }
|
||||
|
||||
let(:cluster_agent) { create(:cluster_agent) }
|
||||
let(:project) { cluster_agent.project }
|
||||
let(:user) { create(:user) }
|
||||
let(:context) do
|
||||
GraphQL::Query::Context.new(
|
||||
query: OpenStruct.new(schema: nil),
|
||||
values: { current_user: user },
|
||||
object: nil
|
||||
)
|
||||
end
|
||||
|
||||
specify { expect(described_class).to require_graphql_authorizations(:admin_cluster) }
|
||||
|
||||
describe '#resolve' do
|
||||
subject { mutation.resolve(id: cluster_agent.to_global_id) }
|
||||
|
||||
context 'without user permissions' do
|
||||
it 'fails to delete the cluster agent', :aggregate_failures do
|
||||
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
|
||||
expect { cluster_agent.reload }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'with user permissions' do
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
end
|
||||
|
||||
it 'deletes a cluster agent', :aggregate_failures do
|
||||
expect { subject }.to change { ::Clusters::Agent.count }.by(-1)
|
||||
expect { cluster_agent.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid params' do
|
||||
subject { mutation.resolve(id: cluster_agent.id) }
|
||||
|
||||
it 'raises an error if the cluster agent id is invalid', :aggregate_failures do
|
||||
expect { subject }.to raise_error(::GraphQL::CoercionError)
|
||||
expect { cluster_agent.reload }.not_to raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,7 +3,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe GitlabSchema.types['ContainerExpirationPolicyOlderThanEnum'] do
|
||||
let_it_be(:expected_values) { %w[SEVEN_DAYS FOURTEEN_DAYS THIRTY_DAYS NINETY_DAYS] }
|
||||
let_it_be(:expected_values) { %w[SEVEN_DAYS FOURTEEN_DAYS THIRTY_DAYS SIXTY_DAYS NINETY_DAYS] }
|
||||
|
||||
it_behaves_like 'exposing container expiration policy option', :older_than
|
||||
end
|
||||
|
|
|
@ -40,6 +40,7 @@ RSpec.describe ContainerExpirationPoliciesHelper do
|
|||
{ key: '7d', label: '7 days until tags are automatically removed' },
|
||||
{ key: '14d', label: '14 days until tags are automatically removed' },
|
||||
{ key: '30d', label: '30 days until tags are automatically removed' },
|
||||
{ key: '60d', label: '60 days until tags are automatically removed' },
|
||||
{ key: '90d', label: '90 days until tags are automatically removed', default: true }
|
||||
]
|
||||
|
||||
|
|
|
@ -248,13 +248,13 @@ RSpec.describe SearchHelper do
|
|||
it 'uses the correct singular label' do
|
||||
collection = Kaminari.paginate_array([:foo]).page(1).per(10)
|
||||
|
||||
expect(search_entries_info(collection, scope, 'foo')).to eq("Showing 1 #{label} for<span> <code>foo</code> </span>")
|
||||
expect(search_entries_info(collection, scope, 'foo')).to eq("Showing 1 #{label} for <span> <code>foo</code> </span>")
|
||||
end
|
||||
|
||||
it 'uses the correct plural label' do
|
||||
collection = Kaminari.paginate_array([:foo] * 23).page(1).per(10)
|
||||
|
||||
expect(search_entries_info(collection, scope, 'foo')).to eq("Showing 1 - 10 of 23 #{label.pluralize} for<span> <code>foo</code> </span>")
|
||||
expect(search_entries_info(collection, scope, 'foo')).to eq("Showing 1 - 10 of 23 #{label.pluralize} for <span> <code>foo</code> </span>")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNote do
|
|||
end
|
||||
|
||||
it 'includes the GitHub ID' do
|
||||
expect(note.github_id).to eq(1)
|
||||
expect(note.note_id).to eq(1)
|
||||
end
|
||||
|
||||
it 'returns the noteable type' do
|
||||
|
@ -106,7 +106,7 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNote do
|
|||
'note' => 'Hello world',
|
||||
'created_at' => created_at.to_s,
|
||||
'updated_at' => updated_at.to_s,
|
||||
'github_id' => 1
|
||||
'note_id' => 1
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -124,7 +124,7 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNote do
|
|||
'note' => 'Hello world',
|
||||
'created_at' => created_at.to_s,
|
||||
'updated_at' => updated_at.to_s,
|
||||
'github_id' => 1
|
||||
'note_id' => 1
|
||||
}
|
||||
|
||||
note = described_class.from_json_hash(hash)
|
||||
|
@ -154,7 +154,7 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNote do
|
|||
'note' => 'Hello world',
|
||||
'created_at' => created_at.to_s,
|
||||
'updated_at' => updated_at.to_s,
|
||||
'github_id' => 1
|
||||
'note_id' => 1
|
||||
)
|
||||
|
||||
expect(note.diff_hash).to eq(
|
||||
|
@ -167,4 +167,18 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNote do
|
|||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#github_identifiers' do
|
||||
it 'returns a hash with needed identifiers' do
|
||||
github_identifiers = {
|
||||
noteable_id: 42,
|
||||
noteable_type: 'MergeRequest',
|
||||
note_id: 1
|
||||
}
|
||||
other_attributes = { something_else: '_something_else_' }
|
||||
note = described_class.new(github_identifiers.merge(other_attributes))
|
||||
|
||||
expect(note.github_identifiers).to eq(github_identifiers)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -181,4 +181,17 @@ RSpec.describe Gitlab::GithubImport::Representation::Issue do
|
|||
expect(object.truncated_title).to eq('foo')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#github_identifiers' do
|
||||
it 'returns a hash with needed identifiers' do
|
||||
github_identifiers = {
|
||||
iid: 42,
|
||||
issuable_type: 'MergeRequest'
|
||||
}
|
||||
other_attributes = { pull_request: true, something_else: '_something_else_' }
|
||||
issue = described_class.new(github_identifiers.merge(other_attributes))
|
||||
|
||||
expect(issue.github_identifiers).to eq(github_identifiers)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::GithubImport::Representation::LfsObject do
|
||||
describe '#github_identifiers' do
|
||||
it 'returns a hash with needed identifiers' do
|
||||
github_identifiers = {
|
||||
oid: 42
|
||||
}
|
||||
other_attributes = { something_else: '_something_else_' }
|
||||
lfs_object = described_class.new(github_identifiers.merge(other_attributes))
|
||||
|
||||
expect(lfs_object.github_identifiers).to eq(github_identifiers)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -40,8 +40,8 @@ RSpec.describe Gitlab::GithubImport::Representation::Note do
|
|||
expect(note.updated_at).to eq(updated_at)
|
||||
end
|
||||
|
||||
it 'includes the GitHub ID' do
|
||||
expect(note.github_id).to eq(1)
|
||||
it 'includes the note ID' do
|
||||
expect(note.note_id).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -84,7 +84,7 @@ RSpec.describe Gitlab::GithubImport::Representation::Note do
|
|||
'note' => 'Hello world',
|
||||
'created_at' => created_at.to_s,
|
||||
'updated_at' => updated_at.to_s,
|
||||
'github_id' => 1
|
||||
'note_id' => 1
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -98,7 +98,7 @@ RSpec.describe Gitlab::GithubImport::Representation::Note do
|
|||
'note' => 'Hello world',
|
||||
'created_at' => created_at.to_s,
|
||||
'updated_at' => updated_at.to_s,
|
||||
'github_id' => 1
|
||||
'note_id' => 1
|
||||
}
|
||||
|
||||
note = described_class.from_json_hash(hash)
|
||||
|
@ -106,4 +106,18 @@ RSpec.describe Gitlab::GithubImport::Representation::Note do
|
|||
expect(note.author).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '#github_identifiers' do
|
||||
it 'returns a hash with needed identifiers' do
|
||||
github_identifiers = {
|
||||
noteable_id: 42,
|
||||
noteable_type: 'Issue',
|
||||
note_id: 1
|
||||
}
|
||||
other_attributes = { something_else: '_something_else_' }
|
||||
note = described_class.new(github_identifiers.merge(other_attributes))
|
||||
|
||||
expect(note.github_identifiers).to eq(github_identifiers)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,7 +14,7 @@ RSpec.describe Gitlab::GithubImport::Representation::PullRequestReview do
|
|||
expect(review.note).to eq('note')
|
||||
expect(review.review_type).to eq('APPROVED')
|
||||
expect(review.submitted_at).to eq(submitted_at)
|
||||
expect(review.github_id).to eq(999)
|
||||
expect(review.review_id).to eq(999)
|
||||
expect(review.merge_request_id).to eq(42)
|
||||
end
|
||||
end
|
||||
|
@ -50,7 +50,7 @@ RSpec.describe Gitlab::GithubImport::Representation::PullRequestReview do
|
|||
describe '.from_json_hash' do
|
||||
let(:hash) do
|
||||
{
|
||||
'github_id' => 999,
|
||||
'review_id' => 999,
|
||||
'merge_request_id' => 42,
|
||||
'note' => 'note',
|
||||
'review_type' => 'APPROVED',
|
||||
|
@ -75,4 +75,17 @@ RSpec.describe Gitlab::GithubImport::Representation::PullRequestReview do
|
|||
expect(review.submitted_at).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '#github_identifiers' do
|
||||
it 'returns a hash with needed identifiers' do
|
||||
github_identifiers = {
|
||||
review_id: 999,
|
||||
merge_request_id: 42
|
||||
}
|
||||
other_attributes = { something_else: '_something_else_' }
|
||||
review = described_class.new(github_identifiers.merge(other_attributes))
|
||||
|
||||
expect(review.github_identifiers).to eq(github_identifiers)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -288,4 +288,16 @@ RSpec.describe Gitlab::GithubImport::Representation::PullRequest do
|
|||
expect(object.truncated_title).to eq('foo')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#github_identifiers' do
|
||||
it 'returns a hash with needed identifiers' do
|
||||
github_identifiers = {
|
||||
iid: 1
|
||||
}
|
||||
other_attributes = { something_else: '_something_else_' }
|
||||
pr = described_class.new(github_identifiers.merge(other_attributes))
|
||||
|
||||
expect(pr.github_identifiers).to eq(github_identifiers.merge(issuable_type: 'MergeRequest'))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue