提交 235dc61f 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@master

上级 12866a39
import { __ } from '~/locale';
export const BLOB_EDITOR_ERROR = __('An error occurred while rendering the editor');
export const BLOB_PREVIEW_ERROR = __('An error occurred previewing the blob');
......@@ -3,39 +3,75 @@
import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import { __ } from '~/locale';
import { BLOB_EDITOR_ERROR, BLOB_PREVIEW_ERROR } from './constants';
import TemplateSelectorMediator from '../blob/file_template_mediator';
import getModeByFileExtension from '~/lib/utils/ace_utils';
import { addEditorMarkdownListeners } from '~/lib/utils/text_markdown';
const monacoEnabled = window?.gon?.features?.monacoBlobs;
export default class EditBlob {
// The options object has:
// assetsPath, filePath, currentAction, projectId, isMarkdown
constructor(options) {
this.options = options;
this.configureAceEditor();
this.initModePanesAndLinks();
this.initSoftWrap();
this.initFileSelectors();
const { isMarkdown } = this.options;
Promise.resolve()
.then(() => {
return monacoEnabled ? this.configureMonacoEditor() : this.configureAceEditor();
})
.then(() => {
this.initModePanesAndLinks();
this.initFileSelectors();
this.initSoftWrap();
if (isMarkdown) {
addEditorMarkdownListeners(this.editor);
}
this.editor.focus();
})
.catch(() => createFlash(BLOB_EDITOR_ERROR));
}
configureMonacoEditor() {
return import(/* webpackChunkName: 'monaco_editor_lite' */ '~/editor/editor_lite').then(
EditorModule => {
const EditorLite = EditorModule.default;
const editorEl = document.getElementById('editor');
const fileNameEl =
document.getElementById('file_path') || document.getElementById('file_name');
const fileContentEl = document.getElementById('file-content');
const form = document.querySelector('.js-edit-blob-form');
this.editor = new EditorLite();
this.editor.createInstance({
el: editorEl,
blobPath: fileNameEl.value,
blobContent: editorEl.innerText,
});
fileNameEl.addEventListener('change', () => {
this.editor.updateModelLanguage(fileNameEl.value);
});
form.addEventListener('submit', () => {
fileContentEl.value = this.editor.getValue();
});
},
);
}
configureAceEditor() {
const { filePath, assetsPath, isMarkdown } = this.options;
const { filePath, assetsPath } = this.options;
ace.config.set('modePath', `${assetsPath}/ace`);
ace.config.loadModule('ace/ext/searchbox');
ace.config.loadModule('ace/ext/modelist');
this.editor = ace.edit('editor');
if (isMarkdown) {
addEditorMarkdownListeners(this.editor);
}
// This prevents warnings re: automatic scrolling being logged
this.editor.$blockScrolling = Infinity;
this.editor.focus();
if (filePath) {
this.editor.getSession().setMode(getModeByFileExtension(filePath));
}
......@@ -81,7 +117,7 @@ export default class EditBlob {
currentPane.empty().append(data);
currentPane.renderGFM();
})
.catch(() => createFlash(__('An error occurred previewing the blob')));
.catch(() => createFlash(BLOB_PREVIEW_ERROR));
}
this.$toggleButton.show();
......@@ -90,14 +126,19 @@ export default class EditBlob {
}
initSoftWrap() {
this.isSoftWrapped = false;
this.isSoftWrapped = Boolean(monacoEnabled);
this.$toggleButton = $('.soft-wrap-toggle');
this.$toggleButton.toggleClass('soft-wrap-active', this.isSoftWrapped);
this.$toggleButton.on('click', () => this.toggleSoftWrap());
}
toggleSoftWrap() {
this.isSoftWrapped = !this.isSoftWrapped;
this.$toggleButton.toggleClass('soft-wrap-active', this.isSoftWrapped);
this.editor.getSession().setUseWrapMode(this.isSoftWrapped);
if (monacoEnabled) {
this.editor.updateOptions({ wordWrap: this.isSoftWrapped ? 'on' : 'off' });
} else {
this.editor.getSession().setUseWrapMode(this.isSoftWrapped);
}
}
}
import { editor as monacoEditor, languages as monacoLanguages, Uri } from 'monaco-editor';
import { editor as monacoEditor, languages as monacoLanguages, Position, Uri } from 'monaco-editor';
import { DEFAULT_THEME, themes } from '~/ide/lib/themes';
import languages from '~/ide/lib/languages';
import { defaultEditorOptions } from '~/ide/lib/editor_options';
......@@ -70,6 +70,22 @@ export default class Editor {
}
getValue() {
return this.model.getValue();
return this.instance.getValue();
}
setValue(val) {
this.instance.setValue(val);
}
focus() {
this.instance.focus();
}
navigateFileStart() {
this.instance.setPosition(new Position(1, 1));
}
updateOptions(options = {}) {
this.instance.updateOptions(options);
}
}
......@@ -9,13 +9,15 @@ export default class GLForm {
this.form = form;
this.textarea = this.form.find('textarea.js-gfm-input');
this.enableGFM = { ...defaultAutocompleteConfig, ...enableGFM };
// Disable autocomplete for keywords which do not have dataSources available
const dataSources = (gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources) || {};
Object.keys(this.enableGFM).forEach(item => {
if (item !== 'emojis') {
this.enableGFM[item] = Boolean(dataSources[item]);
if (item !== 'emojis' && !dataSources[item]) {
this.enableGFM[item] = false;
}
});
// Before we start, we should clean up any previous data for this form
this.destroy();
// Set up the form
......
<script>
import { GlLink } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import { GlButton } from '@gitlab/ui';
import { STATUS_PAGE_PUBLISHED, JOIN_ZOOM_MEETING } from '../constants';
export default {
components: {
Icon,
GlLink,
GlButton,
},
props: {
zoomMeetingUrl: {
......@@ -19,32 +18,46 @@ export default {
default: '',
},
},
computed: {
pinnedLinks() {
return [
{
id: 'publishedIncidentUrl',
url: this.publishedIncidentUrl,
text: STATUS_PAGE_PUBLISHED,
icon: 'tanuki',
},
{
id: 'zoomMeetingUrl',
url: this.zoomMeetingUrl,
text: JOIN_ZOOM_MEETING,
icon: 'brand-zoom',
},
];
},
},
methods: {
needsPaddingClass(i) {
return i < this.pinnedLinks.length - 1;
},
},
};
</script>
<template>
<div class="border-bottom gl-mb-6 gl-display-flex gl-justify-content-start">
<div v-if="publishedIncidentUrl" class="gl-pr-3">
<gl-link
:href="publishedIncidentUrl"
target="_blank"
class="btn btn-inverted btn-secondary btn-sm text-dark mb-3"
data-testid="publishedIncidentUrl"
>
<icon name="tanuki" :size="14" />
<strong class="vertical-align-top">{{ __('Published on status page') }}</strong>
</gl-link>
</div>
<div v-if="zoomMeetingUrl">
<gl-link
:href="zoomMeetingUrl"
target="_blank"
class="btn btn-inverted btn-secondary btn-sm text-dark mb-3"
data-testid="zoomMeetingUrl"
>
<icon name="brand-zoom" :size="14" />
<strong class="vertical-align-top">{{ __('Join Zoom meeting') }}</strong>
</gl-link>
</div>
<template v-for="(link, i) in pinnedLinks">
<div v-if="link.url" :key="link.id" :class="{ 'gl-pr-3': needsPaddingClass(i) }">
<gl-button
:href="link.url"
target="_blank"
:icon="link.icon"
size="small"
class="gl-font-weight-bold gl-mb-5"
:data-testid="link.id"
>{{ link.text }}</gl-button
>
</div>
</template>
</div>
</template>
......@@ -15,3 +15,6 @@ export const IssuableType = {
Epic: 'epic',
MergeRequest: 'merge_request',
};
export const STATUS_PAGE_PUBLISHED = __('Published on status page');
export const JOIN_ZOOM_MEETING = __('Join Zoom meeting');
......@@ -3,6 +3,7 @@ import { mapGetters } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import store from '~/pipelines/stores/test_reports';
import { __ } from '~/locale';
import { GlTooltipDirective } from '@gitlab/ui';
import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
export default {
......@@ -11,6 +12,9 @@ export default {
Icon,
SmartVirtualList,
},
directives: {
GlTooltip: GlTooltipDirective,
},
store,
props: {
heading: {
......@@ -69,12 +73,24 @@ export default {
>
<div class="table-section section-20 section-wrap">
<div role="rowheader" class="table-mobile-header">{{ __('Class') }}</div>
<div class="table-mobile-content pr-md-1 text-truncate">{{ testCase.classname }}</div>
<div
v-gl-tooltip
:title="testCase.classname"
class="table-mobile-content pr-md-1 text-truncate"
>
{{ testCase.classname }}
</div>
</div>
<div class="table-section section-20 section-wrap">
<div role="rowheader" class="table-mobile-header">{{ __('Name') }}</div>
<div class="table-mobile-content pr-md-1 text-truncate">{{ testCase.name }}</div>
<div
v-gl-tooltip
:title="testCase.name"
class="table-mobile-content pr-md-1 text-truncate"
>
{{ testCase.name }}
</div>
</div>
<div class="table-section section-10 section-wrap">
......
......@@ -22,7 +22,7 @@ type AppData {
username: String!
}
type SubmitContentChangesInput {
input SubmitContentChangesInput {
project: String!
sourcePath: String!
content: String!
......
......@@ -3,18 +3,19 @@ import { escape } from 'lodash';
import Tribute from 'tributejs';
import axios from '~/lib/utils/axios_utils';
import { spriteIcon } from '~/lib/utils/common_utils';
import SidebarMediator from '~/sidebar/sidebar_mediator';
/**
* Creates the HTML template for each row of the mentions dropdown.
*
* @param original An object from the array returned from the `autocomplete_sources/members` API
* @returns {string} An HTML template
* @param original - An object from the array returned from the `autocomplete_sources/members` API
* @returns {string} - An HTML template
*/
function menuItemTemplate({ original }) {
const rectAvatarClass = original.type === 'Group' ? 'rect-avatar' : '';
const avatarClasses = `avatar avatar-inline center s26 ${rectAvatarClass}
gl-display-inline-flex gl-align-items-center gl-justify-content-center`;
gl-display-inline-flex! gl-align-items-center gl-justify-content-center`;
const avatarTag = original.avatar_url
? `<img
......@@ -48,6 +49,7 @@ export default {
},
data() {
return {
assignees: undefined,
members: undefined,
};
},
......@@ -76,19 +78,37 @@ export default {
*/
getMembers(inputText, processValues) {
if (this.members) {
processValues(this.members);
processValues(this.getFilteredMembers());
} else if (this.dataSources.members) {
axios
.get(this.dataSources.members)
.then(response => {
this.members = response.data;
processValues(response.data);
processValues(this.getFilteredMembers());
})
.catch(() => {});
} else {
processValues([]);
}
},
getFilteredMembers() {
const fullText = this.$slots.default[0].elm.value;
if (!this.assignees) {
this.assignees =
SidebarMediator.singleton?.store?.assignees?.map(assignee => assignee.username) || [];
}
if (fullText.startsWith('/assign @')) {
return this.members.filter(member => !this.assignees.includes(member.username));
}
if (fullText.startsWith('/unassign @')) {
return this.members.filter(member => this.assignees.includes(member.username));
}
return this.members;
},
},
render(createElement) {
return createElement('div', this.$slots.default);
......
......@@ -4,21 +4,25 @@ import '~/behaviors/markdown/render_gfm';
import { unescape } from 'lodash';
import { __, sprintf } from '~/locale';
import { stripHtml } from '~/lib/utils/text_utility';
import Flash from '../../../flash';
import GLForm from '../../../gl_form';
import markdownHeader from './header.vue';
import markdownToolbar from './toolbar.vue';
import icon from '../icon.vue';
import Flash from '~/flash';
import GLForm from '~/gl_form';
import MarkdownHeader from './header.vue';
import MarkdownToolbar from './toolbar.vue';
import Icon from '../icon.vue';
import GlMentions from '~/vue_shared/components/gl_mentions.vue';
import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import axios from '~/lib/utils/axios_utils';
export default {
components: {
markdownHeader,
markdownToolbar,
icon,
GlMentions,
MarkdownHeader,
MarkdownToolbar,
Icon,
Suggestions,
},
mixins: [glFeatureFlagsMixin()],
props: {
isSubmitting: {
type: Boolean,
......@@ -159,12 +163,10 @@ export default {
},
},
mounted() {
/*
GLForm class handles all the toolbar buttons
*/
// GLForm class handles all the toolbar buttons
return new GLForm($(this.$refs['gl-form']), {
emojis: this.enableAutocomplete,
members: this.enableAutocomplete,
members: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete,
issues: this.enableAutocomplete,
mergeRequests: this.enableAutocomplete,
epics: this.enableAutocomplete,
......@@ -243,7 +245,10 @@ export default {
/>
<div v-show="!previewMarkdown" class="md-write-holder">
<div class="zen-backdrop">
<slot name="textarea"></slot>
<gl-mentions v-if="glFeatures.tributeAutocomplete">
<slot name="textarea"></slot>
</gl-mentions>
<slot v-else name="textarea"></slot>
<a
class="zen-control zen-control-leave js-zen-leave gl-text-gray-700"
href="#"
......
......@@ -74,19 +74,6 @@
}
}
&:focus:hover,
&:focus {
&.header-user-dropdown-toggle .header-user-notification-dot {
border-color: $white;
}
}
&:hover {
&.header-user-dropdown-toggle .header-user-notification-dot {
border-color: $nav-svg-color + 33;
}
}
&:hover,
&:focus {
@include media-breakpoint-up(sm) {
......@@ -96,6 +83,10 @@
svg {
fill: currentColor;
}
&.header-user-dropdown-toggle .header-user-notification-dot {
border-color: $nav-svg-color + 33;
}
}
}
......@@ -109,6 +100,10 @@
fill: $nav-svg-color;
}
}
&.header-user-dropdown-toggle .header-user-notification-dot {
border-color: $white;
}
}
.impersonated-user,
......
......@@ -46,6 +46,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action do
push_frontend_feature_flag(:vue_issuable_sidebar, project.group)
push_frontend_feature_flag(:tribute_autocomplete, @project)
end
before_action only: :show do
......
......@@ -27,7 +27,7 @@ class Plan < ApplicationRecord
end
def actual_limits
self.limits || PlanLimits.new
self.limits || self.build_limits
end
def default?
......
......@@ -40,7 +40,7 @@
= select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2', tabindex: '-1'
.file-editor.code
%pre.js-edit-mode-pane.qa-editor#editor= params[:content] || local_assigns[:blob_data]
%pre.js-edit-mode-pane.qa-editor#editor{ data: { 'editor-loading': true } }= params[:content] || local_assigns[:blob_data]
- if local_assigns[:path]
.js-edit-mode-pane#preview.hide
.center
......
- breadcrumb_title "Repository"
- page_title "Edit", @blob.path, @ref
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/ace.js')
- unless Feature.enabled?(:monaco_blobs)
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/ace.js')
- if @conflict
.alert.alert-danger
......
- breadcrumb_title "Repository"
- page_title "New File", @path.presence, @ref
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/ace.js')
- unless Feature.enabled?(:monaco_blobs)
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/ace.js')
.editor-title-row
%h3.page-title.blob-new-page-title
New file
......
---
title: Move Usage activity by stage for Configure to Core
merge_request: 33672
author:
type: changed
---
title: Update pinned links to use GlButton
merge_request: 34620
author:
type: other
---
title: Assign plan_id when building a new plan limit
merge_request: 34845
author:
type: fixed
---
title: Use GpgKeys::CreateService when an admin creates a new GPG key for a user
merge_request: 34737
author: Rajendra Kadam
type: fixed
---
title: Use GpgKeys::CreateService when a user creates GPG keys for themselves via the API
merge_request: 34817
author: Rajendra Kadam
type: fixed
......@@ -77,10 +77,10 @@ To set this limit on a self-managed installation, run the following in the
# Plan.default.create_limits!
# For project webhooks
Plan.default.limits.update!(project_hooks: 100)
Plan.default.actual_limits.update!(project_hooks: 100)
# For group webhooks
Plan.default.limits.update!(group_hooks: 100)
Plan.default.actual_limits.update!(group_hooks: 100)
```
NOTE: **Note:** Set the limit to `0` to disable it.
......@@ -115,7 +115,7 @@ To set this limit on a self-managed installation, run the following in the
# If limits don't exist for the default plan, you can create one with:
# Plan.default.create_limits!
Plan.default.limits.update!(offset_pagination_limit: 10000)
Plan.default.actual_limits.update!(offset_pagination_limit: 10000)
```
- **Default offset pagination limit:** 50000
......@@ -149,7 +149,7 @@ To set this limit on a self-managed installation, run the following in the
# If limits don't exist for the default plan, you can create one with:
# Plan.default.create_limits!
Plan.default.limits.update!(ci_active_jobs: 500)
Plan.default.actual_limits.update!(ci_active_jobs: 500)
```
NOTE: **Note:** Set the limit to `0` to disable it.
......@@ -171,7 +171,7 @@ To set this limit on a self-managed installation, run the following in the
[GitLab Rails console](troubleshooting/debug.md#starting-a-rails-console-session):
```ruby
Plan.default.limits.update!(ci_project_subscriptions: 500)
Plan.default.actual_limits.update!(ci_project_subscriptions: 500)
```
NOTE: **Note:** Set the limit to `0` to disable it.
......@@ -196,7 +196,7 @@ To set this limit on a self-managed installation, run the following in the
[GitLab Rails console](troubleshooting/debug.md#starting-a-rails-console-session):
```ruby
Plan.default.limits.update!(ci_pipeline_schedules: 100)
Plan.default.actual_limits.update!(ci_pipeline_schedules: 100)
```
### Number of instance level variables
......@@ -214,7 +214,7 @@ To update this limit to a new value on a self-managed installation, run the foll
[GitLab Rails console](troubleshooting/debug.md#starting-a-rails-console-session):
```ruby
Plan.default.limits.update!(ci_instance_level_variables: 30)
Plan.default.actual_limits.update!(ci_instance_level_variables: 30)
```
## Instance monitoring and metrics
......
......@@ -6,8 +6,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Resource milestone events API
Resource milestone events keep track of what happens to GitLab [issues](../user/project/issues/),
[merge requests](../user/project/merge_requests/), and [epics](../user/group/epics/).
Resource milestone events keep track of what happens to GitLab [issues](../user/project/issues/) and
[merge requests](../user/project/merge_requests/).
Use them to track which milestone was added or removed, who did it, and when it happened.
......
......@@ -100,6 +100,7 @@ Complementary reads:
- [Renaming features](renaming_features.md)
- [Windows Development on GCP](windows.md)
- [Code Intelligence](code_intelligence/index.md)
- [Approval Rules](approval_rules.md)
## Performance guides
......
# Approval Rules **(STARTER)**
This document explains the backend design and flow of all related functionality
about [merge request approval rules](../user/project/merge_requests/merge_request_approvals.md).
This should help contributors to understand the code design easier and to also
help see if there are parts to improve as the feature and its implementation
evolves.
It's intentional that it doesn't contain too much implementation detail as they
can change often. The code should explain those things better. The components
mentioned here are the major parts of the application for the approval rules
feature to work.
NOTE: **Note:**
This is a living document and should be updated accordingly when parts
of the codebase touched in this document changed/removed or when new components
are added.
## Data Model
```mermaid
erDiagram
Project ||--o{ MergeRequest: " "
Project ||--o{ ApprovalProjectRule: " "
ApprovalProjectRule }o--o{ User: " "
ApprovalProjectRule }o--o{ Group: " "
ApprovalProjectRule }o--o{ ProtectedBranch: " "
MergeRequest ||--|| ApprovalState: " "
ApprovalState ||--o{ ApprovalWrappedRule: " "
MergeRequest ||--o{ Approval: " "
MergeRequest ||--o{ ApprovalMergeRequestRule: " "
ApprovalMergeRequestRule }o--o{ User: " "
ApprovalMergeRequestRule }o--o{ Group: " "
ApprovalMergeRequestRule ||--o| ApprovalProjectRule: " "
```
### `Project` and `MergeRequest`
`Project` and `MergeRequest` models are defined in `ee/app/models/ee/project.rb`
and `ee/app/models/ee/merge_request.rb`. They extend the non-EE versions since
approval rules is an EE only feature. Associations and other related stuff to
merge request approvals are defined here.
### `ApprovalState`
```mermaid
erDiagram
MergeRequest ||--|| ApprovalState: " "
```
`ApprovalState` class is defined in `ee/app/models/approval_state.rb`. It's not
an actual `ActiveRecord` model. This class encapsulates all logic related to the
state of the approvals for a certain merge request like:
- Knowing the approval rules that are applicable to the merge request based on
its target branch.
- Knowing the approval rules that are applicable to a certain target branch.
- Checking if all rules were approved.
- Checking if approval is required.
- Knowing how many approvals were given or still required.
It gets the approval rules data from the project (`ApprovalProjectRule`) or the
merge request (`ApprovalMergeRequestRule`) and wrap it as `ApprovalWrappedRule`.
### `ApprovalProjectRule`
```mermaid
erDiagram
Project ||--o{ ApprovalProjectRule: " "
ApprovalProjectRule }o--o{ User: " "
ApprovalProjectRule }o--o{ Group: " "
ApprovalProjectRule }o--o{ ProtectedBranch: " "
```
`ApprovalProjectRule` model is defined in `ee/app/models/approval_project_rule.rb`.
A record is created/updated/deleted when an approval rule is added/edited/removed
via project settings or the [project level approvals API](../api/merge_request_approvals.md#project-level-mr-approvals).
The `ApprovalState` model get these records when approval rules are not
overwritten.
The `protected_branches` attribute is set and used when a rule is scoped to
protected branches. See [Scoped to Protected Branch doc](../user/project/merge_requests/merge_request_approvals.md#scoped-to-protected-branch-premium)
for more information about the feature.
### `ApprovalMergeRequestRule`
```mermaid
erDiagram
MergeRequest ||--o{ ApprovalMergeRequestRule: " "
ApprovalMergeRequestRule }o--o{ User: " "
ApprovalMergeRequestRule }o--o{ Group: " "
ApprovalMergeRequestRule ||--o| ApprovalProjectRule: " "
```
`ApprovalMergeRequestRule` model is defined in `ee/app/models/approval_merge_request_rule.rb`.
A record is created/updated/deleted when a rule is added/edited/removed via merge
request create/edit form or the [merge request level approvals API](../api/merge_request_approvals.md#merge-request-level-mr-approvals).
The `approval_project_rule` is set when it is based from an existing `ApprovalProjectRule`.
An `ApprovalMergeRequestRule` doesn't have `protected_branches` as it inherits
them from the `approval_project_rule` if not overridden.
### `ApprovalWrappedRule`
```mermaid
erDiagram
ApprovalState ||--o{ ApprovalWrappedRule: " "
```
`ApprovalWrappedRule` is defined in `ee/app/modes/approval_wrapped_rule.rb` and
is not an `ActiveRecord` model. It's used to wrap an `ApprovalProjectRule` or
`ApprovalMergeRequestRule` for common interface. It also has the following sub
types:
- `ApprovalWrappedAnyApprovalRule` - for wrapping an `any_approver` rule.
- `ApprovalWrappedCodeOwnerRule` - for wrapping a `code_owner` rule.
This class delegates most of the responsibilities to the approval rule it wraps
but it's also responsible for:
- Checking if the approval rule is approved.
- Knowing how many approvals were given or still required for the approval rule.
It gets this information from the approval rule and the `Approval` records from
the merge request.
### `Approval`
```mermaid
erDiagram
MergeRequest ||--o{ Approval: " "
```
`Approval` model is defined in `ee/app/models/approval.rb`. This model is
responsible for storing information about an approval made on a merge request.
Whenever an approval is given/revoked, a record is created/deleted.
## Controllers and Services
The following controllers and services below are being utilized for the approval
rules feature to work.
### `API::ProjectApprovalSettings`
This private API is defined in `ee/lib/api/project_approval_settings.rb`.
This is used for the following:
- Listing the approval rules in project settings.
- Creating/updating/deleting rules in project settings.
- Listing the approval rules on create merge request form.
### `Projects::MergeRequests::CreationsController`
This controller is defined in `app/controllers/projects/merge_requests/creations_controller.rb`.
The `create` action of this controller is used when create merge request form is
submitted. It accepts the `approval_rules_attributes` parameter for creating/updating/deleting
`ApprovalMergeRequestRule` records. It passes the parameter along when it executes
`MergeRequests::CreateService`.
### `Projects::MergeRequestsController`
This controller is defined in `app/controllers/projects/merge_requests_controller.rb`.
The `update` action of this controller is used when edit merge request form is
submitted. It's like `Projects::MergeRequests::CreationsController` but it executes
`MergeRequests::UpdateService` instead.
### `API::MergeRequestApprovals`
This API is defined in `ee/lib/api/merge_request_approvals.rb`.
The [Approvals API endpoint](../api/merge_request_approvals.md#get-configuration-1)
is requested when merge request page loads.
The `/projects/:id/merge_requests/:merge_request_iid/approval_settings` is a
private API endpoint used for the following:
- Listing the approval rules on edit merge request form.
- Listing the approval rules on the merge request page.
When approving/unapproving MR via UI and API, the [Approve Merge Request](../api/merge_request_approvals.md#approve-merge-request)
API endpoint and the [Unapprove Merge Request](../api/merge_request_approvals.md#unapprove-merge-request)
API endpoint are requested. They execute `MergeRequests::ApprovalService` and
`MergeRequests::RemoveApprovalService` accordingly.
### `API::ProjectApprovalRules` and `API::MergeRequestApprovalRules`
These APIs are defined in `ee/lib/api/project_approval_rules.rb` and
`ee/lib/api/merge_request_approval_rules.rb`.
Used to list/create/update/delete project and merge request level rules via
[Merge request approvals API](../api/merge_request_approvals.md).
Executes `ApprovalRules::CreateService`, `ApprovalRules::UpdateService`,
`ApprovalRules::ProjectRuleDestroyService`, and `ApprovalRules::MergeRequestRuleDestroyService`
accordingly.
### `ApprovalRules::ParamsFilteringService`
This service is defined in `ee/app/services/approval_rules/params_filtering_service.rb`.
It is called only when `MergeRequests::CreateService` and
`MergeRequests::UpdateService` are executed.
It is responsible for parsing `approval_rules_attributes` parameter to:
- Remove it when user can't update approval rules.
- Filter the user IDs whether they are members of the project or not.
- Filter the group IDs whether they are visible to user.
- Identify the `any_approver` rule.
- Append hidden groups to it when specified.
- Append user defined inapplicable (rules that does not apply to MR's target
branch) approval rules.
## Flow
These flowcharts should help explain the flow from the controllers down to the
models for different functionalities.
Some CRUD API endpoints are intentionally skipped because they are pretty
straightforward.
### Creating a merge request with approval rules via web UI
```mermaid
graph LR
Projects::MergeRequests::CreationsController --> MergeRequests::CreateService
MergeRequests::CreateService --> ApprovalRules::ParamsFilteringService
ApprovalRules::ParamsFilteringService --> MergeRequests::CreateService
MergeRequests::CreateService --> MergeRequest
MergeRequest --> db[(Database)]
MergeRequest --> User
MergeRequest --> Group
MergeRequest --> ApprovalProjectRule
User --> db[(Database)]
Group --> db[(Database)]
ApprovalProjectRule --> db[(Database)]
```
When updating, same flow is followed but it starts at `Projects::MergeRequestsController`
and executes `MergeRequests::UpdateService` instead.
### Viewing the merge request approval rules on an MR page
```mermaid
graph LR
API::MergeRequestApprovals --> MergeRequest
MergeRequest --> ApprovalState
ApprovalState --> id1{approval rules are overridden}
id1{approval rules are overridden} --> |No| ApprovalProjectRule & ApprovalMergeRequestRule
id1{approval rules are overridden} --> |Yes| ApprovalMergeRequestRule
ApprovalState --> ApprovalWrappedRule
ApprovalWrappedRule --> Approval
```
This flow gets initiated by the frontend component. The data returned will
then be used to display information on the MR widget.
### Approving a merge request
```mermaid
graph LR
API::MergeRequestApprovals --> MergeRequests::ApprovalService
MergeRequests::ApprovalService --> Approval
Approval --> db[(Database)]
```
When unapproving, same flow is followed but the `MergeRequests::RemoveApprovalService`
is executed instead.
## TODO
1. Add information related to other rule types (e.g. `code_owner` and `report_approver`).
1. Add information about side effects of approving/unapproving merge request.
......@@ -491,7 +491,10 @@ introduced by [#25381](https://gitlab.com/gitlab-org/gitlab/issues/25381).
### Batch Suggestions
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/25486) in GitLab 13.1.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/25486) in GitLab 13.1 as an [alpha feature](https://about.gitlab.com/handbook/product/#alpha).
> - It's deployed behind a feature flag, disabled by default.
> - It's disabled on GitLab.com.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-batch-suggestions).
You can apply multiple suggestions at once to reduce the number of commits added
to your branch to address your reviewers' requests.
......@@ -512,6 +515,27 @@ to your branch to address your reviewers' requests.
![A code change suggestion displayed, with the button to apply the batch of suggestions highlighted.](img/apply_batch_of_suggestions_v13_1.jpg "Apply a batch of suggestions")
#### Enable or disable Batch Suggestions
Batch Suggestions is
deployed behind a feature flag that is **disabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
can enable it for your instance.
To enable it:
```ruby
# Instance-wide
Feature.enable(:batched_suggestions)
```
To disable it:
```ruby
# Instance-wide
Feature.disable(:batched_suggestions)
```
## Start a thread by replying to a standard comment
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/30299) in GitLab 11.9
......
......@@ -87,15 +87,17 @@ Below are the current settings regarding [GitLab CI/CD](../../ci/README.md).
## Repository size limit
The maximum size your Git repository is allowed to be, including LFS. If you are near
or over the size limit, you can [reduce your repository size with Git](../project/repository/reducing_the_repo_size_using_git.md).
GitLab.com has the following [account limits](../admin_area/settings/account_and_limit_settings.md) enabled. If a setting is not listed, it is set to the default value.
| Setting | GitLab.com | Default |
| ----------- | ----------------- | ------------- |
| Repository size including LFS | 10G | Unlimited |
If you are near
or over the repository size limit, you can [reduce your repository size with Git](../project/repository/reducing_the_repo_size_using_git.md).
| Setting | GitLab.com | Default |
| ----------- | ----------- | ------------- |
| Repository size including LFS | 10 GB | Unlimited |
NOTE: **Note:**
`git push` and GitLab project imports are limited to 5GB per request. Git LFS and imports other than a file upload are not affected by this limit.
`git push` and GitLab project imports are limited to 5 GB per request through Cloudflare. Git LFS and imports other than a file upload are not affected by this limit.
## IP range
......
......@@ -186,7 +186,8 @@ updated every 15 minutes at most, so may not reflect recent activity. The displa
The project size may differ slightly from one instance to another due to compression, housekeeping, and other factors.
[Repository size limit](../../admin_area/settings/account_and_limit_settings.md) may be set by admins. GitLab.com's repository size limit [is set by GitLab](../../gitlab_com/index.md#repository-size-limit).
[Repository size limit](../../admin_area/settings/account_and_limit_settings.md) may be set by admins.
GitLab.com's repository size limit [is set by GitLab](../../gitlab_com/index.md#repository-size-limit).
## Contributors
......
......@@ -328,9 +328,9 @@ module API
user = User.find_by(id: params.delete(:id))
not_found!('User') unless user
key = user.gpg_keys.new(declared_params(include_missing: false))
key = ::GpgKeys::CreateService.new(user, declared_params(include_missing: false)).execute
if key.save
if key.persisted?
present key, with: Entities::GpgKey
else
render_validation_error!(key)
......@@ -792,9 +792,9 @@ module API
requires :key, type: String, desc: 'The new GPG key'
end
post 'gpg_keys' do
key = current_user.gpg_keys.new(declared_params)
key = ::GpgKeys::CreateService.new(current_user, declared_params(include_missing: false)).execute
if key.save
if key.persisted?
present key, with: Entities::GpgKey
else
render_validation_error!(key)
......
......@@ -37,6 +37,8 @@ module Gitlab
.merge(cycle_analytics_usage_data)
.merge(object_store_usage_data)
.merge(topology_usage_data)
.merge(usage_activity_by_stage)
.merge(usage_activity_by_stage(:usage_activity_by_stage_monthly, default_time_period))
end
end
......@@ -427,6 +429,88 @@ module Gitlab
{ created_at: 28.days.ago..Time.current }
end
# Source: https://gitlab.com/gitlab-data/analytics/blob/master/transform/snowflake-dbt/data/ping_metrics_to_stage_mapping_data.csv
def usage_activity_by_stage(key = :usage_activity_by_stage, time_period = {})
{
key => {
configure: usage_activity_by_stage_configure(time_period),
create: usage_activity_by_stage_create(time_period),
manage: usage_activity_by_stage_manage(time_period),
monitor: usage_activity_by_stage_monitor(time_period),
package: usage_activity_by_stage_package(time_period),
plan: usage_activity_by_stage_plan(time_period),
release: usage_activity_by_stage_release(time_period),
secure: usage_activity_by_stage_secure(time_period),
verify: usage_activity_by_stage_verify(time_period)
}
}
end
# rubocop: disable CodeReuse/ActiveRecord
def usage_activity_by_stage_configure(time_period)
{
clusters_applications_cert_managers: cluster_applications_user_distinct_count(::Clusters::Applications::CertManager, time_period),
clusters_applications_helm: cluster_applications_user_distinct_count(::Clusters::Applications::Helm, time_period),
clusters_applications_ingress: cluster_applications_user_distinct_count(::Clusters::Applications::Ingress, time_period),
clusters_applications_knative: cluster_applications_user_distinct_count(::Clusters::Applications::Knative, time_period),
clusters_management_project: clusters_user_distinct_count(::Clusters::Cluster.with_management_project, time_period),
clusters_disabled: clusters_user_distinct_count(::Clusters::Cluster.disabled, time_period),
clusters_enabled: clusters_user_distinct_count(::Clusters::Cluster.enabled, time_period),
clusters_platforms_gke: clusters_user_distinct_count(::Clusters::Cluster.gcp_installed.enabled, time_period),
clusters_platforms_eks: clusters_user_distinct_count(::Clusters::Cluster.aws_installed.enabled, time_period),
clusters_platforms_user: clusters_user_distinct_count(::Clusters::Cluster.user_provided.enabled, time_period),
instance_clusters_disabled: clusters_user_distinct_count(::Clusters::Cluster.disabled.instance_type, time_period),
instance_clusters_enabled: clusters_user_distinct_count(::Clusters::Cluster.enabled.instance_type, time_period),
group_clusters_disabled: clusters_user_distinct_count(::Clusters::Cluster.disabled.group_type, time_period),
group_clusters_enabled: clusters_user_distinct_count(::Clusters::Cluster.enabled.group_type, time_period),
project_clusters_disabled: clusters_user_distinct_count(::Clusters::Cluster.disabled.project_type, time_period),
project_clusters_enabled: clusters_user_distinct_count(::Clusters::Cluster.enabled.project_type, time_period)
}
end
# rubocop: enable CodeReuse/ActiveRecord
# Omitted because no user, creator or author associated: `lfs_objects`, `pool_repositories`, `web_hooks`
def usage_activity_by_stage_create(time_period)
{}
end
# Omitted because no user, creator or author associated: `campaigns_imported_from_github`, `ldap_group_links`
def usage_activity_by_stage_manage(time_period)
{}
end
def usage_activity_by_stage_monitor(time_period)
{}
end
def usage_activity_by_stage_package(time_period)
{}
end
# Omitted because no user, creator or author associated: `boards`, `labels`, `milestones`, `uploads`
# Omitted because too expensive: `epics_deepest_relationship_level`
# Omitted because of encrypted properties: `projects_jira_cloud_active`, `projects_jira_server_active`
def usage_activity_by_stage_plan(time_period)
{}
end
# Omitted because no user, creator or author associated: `environments`, `feature_flags`, `in_review_folder`, `pages_domains`
def usage_activity_by_stage_release(time_period)
{}
end
# Omitted because no user, creator or author associated: `ci_runners`
def usage_activity_by_stage_verify(time_period)
{}
end
# Currently too complicated and to get reliable counts for these stats:
# container_scanning_jobs, dast_jobs, dependency_scanning_jobs, license_management_jobs, sast_jobs, secret_detection_jobs
# Once https://gitlab.com/gitlab-org/gitlab/merge_requests/17568 is merged, this might be doable
def usage_activity_by_stage_secure(time_period)
{}
end
private
def total_alert_issues
......@@ -455,6 +539,16 @@ module Gitlab
clear_memoization(:user_minimum_id)
clear_memoization(:user_maximum_id)
end
# rubocop: disable CodeReuse/ActiveRecord
def cluster_applications_user_distinct_count(applications, time_period)
distinct_count(applications.where(time_period).available.joins(:cluster), 'clusters.user_id')
end
def clusters_user_distinct_count(clusters, time_period)
distinct_count(clusters.where(time_period), :user_id)
end
# rubocop: enable CodeReuse/ActiveRecord
end
end
end
......
......@@ -520,6 +520,9 @@ msgstr ""
msgid "%{primary} (%{secondary})"
msgstr ""
msgid "%{ref} cannot be added: %{error}"
msgstr ""
msgid "%{releases} release"
msgid_plural "%{releases} releases"
msgstr[0] ""
......@@ -2443,6 +2446,9 @@ msgstr ""
msgid "An error occurred while rendering preview broadcast message"
msgstr ""
msgid "An error occurred while rendering the editor"
msgstr ""
msgid "An error occurred while reordering issues."
msgstr ""
......
......@@ -37,7 +37,7 @@ RSpec.describe 'a maintainer edits files on a source-branch of an MR from a fork
end
it 'allows committing to the source branch' do
find('.ace_text-input', visible: false).send_keys('Updated the readme')
execute_script("monaco.editor.getModels()[0].setValue('Updated the readme')")
click_button 'Commit changes'
wait_for_requests
......
......@@ -36,12 +36,35 @@ RSpec.describe 'Member autocomplete', :js do
let(:noteable) { create(:issue, author: author, project: project) }
before do
stub_feature_flags(tribute_autocomplete: false)
visit project_issue_path(project, noteable)
end
include_examples "open suggestions when typing @", 'issue'
end
describe 'when tribute_autocomplete feature flag is on' do
context 'adding a new note on a Issue' do
let(:noteable) { create(:issue, author: author, project: project) }
before do
stub_feature_flags(tribute_autocomplete: true)
visit project_issue_path(project, noteable)
page.within('.new-note') do
find('#note-body').send_keys('@')
end
end
it 'suggests noteable author and note author' do
page.within('.tribute-container', visible: true) do
expect(page).to have_content(author.username)
expect(page).to have_content(note.author.username)
end
end
end
end
context 'adding a new note on a Merge Request' do
let(:project) { create(:project, :public, :repository) }
let(:noteable) do
......
......@@ -36,8 +36,7 @@ RSpec.describe 'Editing file blob', :js do
def fill_editor(content: 'class NextFeature\\nend\\n')
wait_for_requests
find('#editor')
execute_script("ace.edit('editor').setValue('#{content}')")
execute_script("monaco.editor.getModels()[0].setValue('#{content}')")
end
context 'from MR diff' do
......@@ -67,6 +66,15 @@ RSpec.describe 'Editing file blob', :js do
expect(find_by_id('file_path').value).to eq('ci/.gitlab-ci.yml')
end
it 'updating file path updates syntax highlighting' do
visit project_edit_blob_path(project, tree_join(branch, readme_file_path))
expect(find('#editor')['data-mode-id']).to eq('markdown')
find('#file_path').send_keys('foo.txt') do
expect(find('#editor')['data-mode-id']).to eq('plaintext')
end
end
context 'from blob file path' do
before do
stub_feature_flags(code_navigation: false)
......
......@@ -16,8 +16,7 @@ RSpec.describe 'User creates blob in new project', :js do
it 'allows the user to add a new file' do
click_link 'New file'
find('#editor')
execute_script('ace.edit("editor").setValue("Hello world")')
execute_script("monaco.editor.getModels()[0].setValue('Hello world')")
fill_in(:file_name, with: 'dummy-file')
......
......@@ -32,6 +32,8 @@ RSpec.describe 'User follows pipeline suggest nudge spec when feature is enabled
end
it 'displays suggest_gitlab_ci_yml popover' do
page.find(:css, '.gitlab-ci-yml-selector').click
popover_selector = '.suggest-gitlab-ci-yml'
expect(page).to have_css(popover_selector, visible: true)
......
......@@ -8,8 +8,9 @@ RSpec.describe 'Projects > Files > User uses soft wrap while editing file', :js
user = project.owner
sign_in user
visit project_new_blob_path(project, 'master', file_name: 'test_file-name')
page.within('.file-editor.code') do
find('.ace_text-input', visible: false).send_keys 'Touch water with paw then recoil in horror chase dog then
find('.inputarea', visible: false).send_keys 'Touch water with paw then recoil in horror chase dog then
run away chase the pig around the house eat owner\'s food, and knock
dish off table head butt cant eat out of my own dish. Cat is love, cat
is life rub face on everything poop on grasses so meow. Playing with
......@@ -26,17 +27,20 @@ RSpec.describe 'Projects > Files > User uses soft wrap while editing file', :js
it 'user clicks the "Soft wrap" button and then "No wrap" button' do
wrapped_content_width = get_content_width
toggle_button.click
expect(toggle_button).to have_content 'No wrap'
unwrapped_content_width = get_content_width
expect(unwrapped_content_width).to be < wrapped_content_width
toggle_button.click
expect(toggle_button).to have_content 'Soft wrap'
expect(get_content_width).to be > unwrapped_content_width
toggle_button.click do
expect(toggle_button).to have_content 'Soft wrap'
unwrapped_content_width = get_content_width
expect(unwrapped_content_width).to be > wrapped_content_width
end
toggle_button.click do
expect(toggle_button).to have_content 'No wrap'
expect(get_content_width).to be < unwrapped_content_width
end
end
def get_content_width
find('.ace_content')[:style].slice!(/width: \d+/).slice!(/\d+/).to_i
find('.view-lines', visible: false)[:style].slice!(/width: \d+/).slice!(/\d+/).to_i
end
end
......@@ -25,6 +25,6 @@ RSpec.describe 'Projects > Files > User wants to add a .gitignore file' do
expect(page).to have_css('.gitignore-selector .dropdown-toggle-text', text: 'Apply a template')
expect(page).to have_content('/.bundle')
expect(page).to have_content('# Gemfile.lock, .ruby-version, .ruby-gemset')
expect(page).to have_content('config/initializers/secret_token.rb')
end
end
......@@ -67,7 +67,7 @@ RSpec.describe 'Projects > Files > User creates files', :js do
file_name = find('#file_name')
file_name.set options[:file_name] || 'README.md'
find('.ace_text-input', visible: false).send_keys.native.send_keys options[:file_content] || 'Some content'
find('.monaco-editor textarea').send_keys.native.send_keys options[:file_content] || 'Some content'
click_button 'Commit changes'
end
......@@ -89,7 +89,7 @@ RSpec.describe 'Projects > Files > User creates files', :js do
it 'creates and commit a new file' do
find('#editor')
execute_script("ace.edit('editor').setValue('*.rbca')")
execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
fill_in(:file_name, with: 'not_a_file.md')
fill_in(:commit_message, with: 'New commit message', visible: true)
click_button('Commit changes')
......@@ -105,7 +105,7 @@ RSpec.describe 'Projects > Files > User creates files', :js do
it 'creates and commit a new file with new lines at the end of file' do
find('#editor')
execute_script('ace.edit("editor").setValue("Sample\n\n\n")')
execute_script('monaco.editor.getModels()[0].setValue("Sample\n\n\n")')
fill_in(:file_name, with: 'not_a_file.md')
fill_in(:commit_message, with: 'New commit message', visible: true)
click_button('Commit changes')
......@@ -117,7 +117,7 @@ RSpec.describe 'Projects > Files > User creates files', :js do
find('.js-edit-blob').click
find('#editor')
expect(evaluate_script('ace.edit("editor").getValue()')).to eq("Sample\n\n\n")
expect(evaluate_script('monaco.editor.getModels()[0].getValue()')).to eq("Sample\n\n\n")
end
it 'creates and commit a new file with a directory name' do
......@@ -126,7 +126,7 @@ RSpec.describe 'Projects > Files > User creates files', :js do
expect(page).to have_selector('.file-editor')
find('#editor')
execute_script("ace.edit('editor').setValue('*.rbca')")
execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
fill_in(:commit_message, with: 'New commit message', visible: true)
click_button('Commit changes')
......@@ -141,7 +141,7 @@ RSpec.describe 'Projects > Files > User creates files', :js do
expect(page).to have_selector('.file-editor')
find('#editor')
execute_script("ace.edit('editor').setValue('*.rbca')")
execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
fill_in(:file_name, with: 'not_a_file.md')
fill_in(:commit_message, with: 'New commit message', visible: true)
fill_in(:branch_name, with: 'new_branch_name', visible: true)
......@@ -176,7 +176,7 @@ RSpec.describe 'Projects > Files > User creates files', :js do
expect(page).to have_selector('.file-editor')
find('#editor')
execute_script("ace.edit('editor').setValue('*.rbca')")
execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
fill_in(:file_name, with: 'not_a_file.md')
fill_in(:commit_message, with: 'New commit message', visible: true)
......
......@@ -46,9 +46,9 @@ RSpec.describe 'Projects > Files > User edits files', :js do
find('.file-editor', match: :first)
find('#editor')
execute_script("ace.edit('editor').setValue('*.rbca')")
execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
expect(evaluate_script('ace.edit("editor").getValue()')).to eq('*.rbca')
expect(evaluate_script('monaco.editor.getModels()[0].getValue()')).to eq('*.rbca')
end
it 'does not show the edit link if a file is binary' do
......@@ -67,7 +67,7 @@ RSpec.describe 'Projects > Files > User edits files', :js do
find('.file-editor', match: :first)
find('#editor')
execute_script("ace.edit('editor').setValue('*.rbca')")
execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
fill_in(:commit_message, with: 'New commit message', visible: true)
click_button('Commit changes')
......@@ -85,7 +85,7 @@ RSpec.describe 'Projects > Files > User edits files', :js do
find('.file-editor', match: :first)
find('#editor')
execute_script("ace.edit('editor').setValue('*.rbca')")
execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
fill_in(:commit_message, with: 'New commit message', visible: true)
fill_in(:branch_name, with: 'new_branch_name', visible: true)
click_button('Commit changes')
......@@ -103,7 +103,7 @@ RSpec.describe 'Projects > Files > User edits files', :js do
find('.file-editor', match: :first)
find('#editor')
execute_script("ace.edit('editor').setValue('*.rbca')")
execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
click_link('Preview changes')
expect(page).to have_css('.line_holder.new')
......@@ -148,9 +148,9 @@ RSpec.describe 'Projects > Files > User edits files', :js do
find('.file-editor', match: :first)
find('#editor')
execute_script("ace.edit('editor').setValue('*.rbca')")
execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
expect(evaluate_script('ace.edit("editor").getValue()')).to eq('*.rbca')
expect(evaluate_script('monaco.editor.getModels()[0].getValue()')).to eq('*.rbca')
end
it 'opens the Web IDE in a forked project', :sidekiq_might_not_need_inline do
......@@ -178,7 +178,7 @@ RSpec.describe 'Projects > Files > User edits files', :js do
find('.file-editor', match: :first)
find('#editor')
execute_script("ace.edit('editor').setValue('*.rbca')")
execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
fill_in(:commit_message, with: 'New commit message', visible: true)
click_button('Commit changes')
......@@ -207,7 +207,7 @@ RSpec.describe 'Projects > Files > User edits files', :js do
expect(page).not_to have_button('Cancel')
find('#editor')
execute_script("ace.edit('editor').setValue('*.rbca')")
execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
fill_in(:commit_message, with: 'Another commit', visible: true)
click_button('Commit changes')
......
import { shallowMount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
import { GlButton } from '@gitlab/ui';
import PinnedLinks from '~/issue_show/components/pinned_links.vue';
import { STATUS_PAGE_PUBLISHED, JOIN_ZOOM_MEETING } from '~/issue_show/constants';
const plainZoomUrl = 'https://zoom.us/j/123456789';
const plainStatusUrl = 'https://status.com';
......@@ -8,7 +9,7 @@ const plainStatusUrl = 'https://status.com';
describe('PinnedLinks', () => {
let wrapper;
const findLinks = () => wrapper.findAll(GlLink);
const findButtons = () => wrapper.findAll(GlButton);
const createComponent = props => {
wrapper = shallowMount(PinnedLinks, {
......@@ -26,10 +27,10 @@ describe('PinnedLinks', () => {
});
expect(
findLinks()
findButtons()
.at(0)
.text(),
).toBe('Join Zoom meeting');
).toBe(JOIN_ZOOM_MEETING);
});
it('displays Status link', () => {
......@@ -38,10 +39,10 @@ describe('PinnedLinks', () => {
});
expect(
findLinks()
findButtons()
.at(0)
.text(),
).toBe('Published on status page');
).toBe(STATUS_PAGE_PUBLISHED);
});
it('does not render if there are no links', () => {
......@@ -50,6 +51,6 @@ describe('PinnedLinks', () => {
publishedIncidentUrl: '',
});
expect(wrapper.find(GlLink).exists()).toBe(false);
expect(findButtons()).toHaveLength(0);
});
});
......@@ -10,7 +10,73 @@ describe Gitlab::UsageData, :aggregate_failures do
stub_object_store_settings
end
describe '#uncached_data' do
describe '.uncached_data' do
describe '.usage_activity_by_stage' do
it 'includes usage_activity_by_stage data' do
expect(described_class.uncached_data).to include(:usage_activity_by_stage)
expect(described_class.uncached_data).to include(:usage_activity_by_stage_monthly)
end
context 'for configure' do
it 'includes accurate usage_activity_by_stage data' do
for_defined_days_back do
user = create(:user)
cluster = create(:cluster, user: user)
create(:clusters_applications_cert_manager, :installed, cluster: cluster)
create(:clusters_applications_helm, :installed, cluster: cluster)
create(:clusters_applications_ingress, :installed, cluster: cluster)
create(:clusters_applications_knative, :installed, cluster: cluster)
create(:cluster, :disabled, user: user)
create(:cluster_provider_gcp, :created)
create(:cluster_provider_aws, :created)
create(:cluster_platform_kubernetes)
create(:cluster, :group, :disabled, user: user)
create(:cluster, :group, user: user)
create(:cluster, :instance, :disabled, :production_environment)
create(:cluster, :instance, :production_environment)
create(:cluster, :management_project)
end
expect(described_class.uncached_data[:usage_activity_by_stage][:configure]).to include(
clusters_applications_cert_managers: 2,
clusters_applications_helm: 2,
clusters_applications_ingress: 2,
clusters_applications_knative: 2,
clusters_management_project: 2,
clusters_disabled: 4,
clusters_enabled: 12,
clusters_platforms_gke: 2,
clusters_platforms_eks: 2,
clusters_platforms_user: 2,
instance_clusters_disabled: 2,
instance_clusters_enabled: 2,
group_clusters_disabled: 2,
group_clusters_enabled: 2,
project_clusters_disabled: 2,
project_clusters_enabled: 10
)
expect(described_class.uncached_data[:usage_activity_by_stage_monthly][:configure]).to include(
clusters_applications_cert_managers: 1,
clusters_applications_helm: 1,
clusters_applications_ingress: 1,
clusters_applications_knative: 1,
clusters_management_project: 1,
clusters_disabled: 2,
clusters_enabled: 6,
clusters_platforms_gke: 1,
clusters_platforms_eks: 1,
clusters_platforms_user: 1,
instance_clusters_disabled: 1,
instance_clusters_enabled: 1,
group_clusters_disabled: 1,
group_clusters_enabled: 1,
project_clusters_disabled: 1,
project_clusters_enabled: 5
)
end
end
end
it 'ensures recorded_at is set before any other usage data calculation' do
%i(alt_usage_data redis_usage_data distinct_count count).each do |method|
expect(described_class).not_to receive(method)
......@@ -598,4 +664,12 @@ describe Gitlab::UsageData, :aggregate_failures do
)
end
end
def for_defined_days_back(days: [29, 2])
days.each do |n|
Timecop.travel(n.days.ago) do
yield
end
end
end
end
......@@ -14,4 +14,16 @@ describe Plan do
end
end
end
context 'when updating plan limits' do
let(:plan) { described_class.default }
it { expect(plan).to be_persisted }
it { expect(plan.actual_limits).not_to be_persisted }
it 'successfully updates the limits' do
expect(plan.actual_limits.update!(ci_instance_level_variables: 100)).to be_truthy
end
end
end
......@@ -840,10 +840,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.140.0.tgz#593f1f65b0df57c3399fcfb9f472f59aa64da074"
integrity sha512-6gANJGi2QkpvOgFTMcY3SIwEqhO69i6R3jU4BSskkVziwDdAWxGonln22a4Iu//Iv0NrsFDpAA0jIVfnJzw0iA==
"@gitlab/ui@17.0.1":
version "17.0.1"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-17.0.1.tgz#daf036dfdc095f94123c80c3fb1ab5fe4dcbf95b"
integrity sha512-JSUGruV6oploADF0Sc0BBY43Des3utU9iWCnR8BAmttKFXFFNUKwTf908yZPGJtfnVyjJkVioOCOYkvUZ0jngg==
"@gitlab/ui@17.1.0":
version "17.1.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-17.1.0.tgz#522912caee7689a0fde1e58cd6d4e4163e39ca7f"
integrity sha512-KruPE0I4qU4LP+pPIzhCY0xbNDcB7gUHaXMO1we2ssK9lc+NJxeLt9gr/ctOX1/Rzgau93+i9QvekeNg0wvqGA==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.3.0"
......@@ -11507,10 +11507,10 @@ tr46@^2.0.2:
dependencies:
punycode "^2.1.1"
tributejs@4.1.3:
version "4.1.3"
resolved "https://registry.yarnpkg.com/tributejs/-/tributejs-4.1.3.tgz#2e1be7d9a1e403ed4c394f91d859812267e4691c"
integrity sha512-+VUqyi8p7tCdaqCINCWHf95E2hJFMIML180BhplTpXNooz3E2r96AONXI9qO2Ru6Ugp7MsMPJjB+rnBq+hAmzA==
tributejs@5.1.3:
version "5.1.3"
resolved "https://registry.yarnpkg.com/tributejs/-/tributejs-5.1.3.tgz#980600fc72865be5868893078b4bfde721129eae"
integrity sha512-B5CXihaVzXw+1UHhNFyAwUTMDk1EfoLP5Tj1VhD9yybZ1I8DZJEv8tZ1l0RJo0t0tk9ZhR8eG5tEsaCvRigmdQ==
trim-newlines@^1.0.0:
version "1.0.0"
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册