Commit c3dfb2ec authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 715349d6 619f2044
......@@ -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'),
}),
);
};
......
......@@ -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: {
......
......@@ -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',
},
];
<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>
......
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', {}, [
......
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,
......
<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 };
/* 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;
}
......@@ -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])
......
......@@ -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|
......
......@@ -6,7 +6,6 @@ module Clusters
ALLOWED_PARAMS = %i[agent_id description name].freeze
def execute
return error_feature_not_available unless container.feature_available?(:cluster_agents)
return error_no_permissions unless current_user.can?(:create_cluster, container)
token = ::Clusters::AgentToken.new(filtered_params.merge(created_by_user: current_user))
......@@ -20,10 +19,6 @@ module Clusters
private
def error_feature_not_available
ServiceResponse.error(message: s_('ClusterAgent|This feature is only available for premium plans'))
end
def error_no_permissions
ServiceResponse.error(message: s_('ClusterAgent|User has insufficient permissions to create a token for this project'))
end
......
......@@ -4,7 +4,6 @@ module Clusters
module Agents
class CreateService < BaseService
def execute(name:)
return error_not_premium_plan unless project.feature_available?(:cluster_agents)
return error_no_permissions unless cluster_agent_permissions?
agent = ::Clusters::Agent.new(name: name, project: project, created_by_user: current_user)
......@@ -25,10 +24,6 @@ module Clusters
def error_no_permissions
error(s_('ClusterAgent|You have insufficient permissions to create a cluster agent for this project'))
end
def error_not_premium_plan
error(s_('ClusterAgent|This feature is only available for premium plans'))
end
end
end
end
......@@ -83,7 +83,7 @@ module Users
end
def lazy_user_availability(user)
BatchLoader.for(user.id).batch(replace_methods: false) do |user_ids, loader|
BatchLoader.for(user.id).batch do |user_ids, loader|
user_ids.each_slice(1_000) do |sliced_user_ids|
UserStatus
.select(:user_id, :availability)
......
<%= _(" %{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 } %>
......
# 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)
......@@ -298,6 +298,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
resources :cluster_agents, only: [:show], param: :name
concerns :clusterable
namespace :serverless do
......
......@@ -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),
......
......@@ -3,11 +3,11 @@ import { PIPELINE_SOURCES as CE_PIPELINE_SOURCES } from '~/pipelines/components/
const EE_PIPELINE_SOURCES = [
{
text: s__('Pipeline|Source|On-Demand DAST Scan'),
text: s__('PipelineSource|On-Demand DAST Scan'),
value: 'ondemand_dast_scan',
},
{
text: s__('Pipeline|Source|On-Demand DAST Validation'),
text: s__('PipelineSource|On-Demand DAST Validation'),
value: 'ondemand_dast_validation',
},
{
......
......@@ -6,10 +6,6 @@ module EE
extend ActiveSupport::Concern
prepended do
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::ComplianceManagement::Frameworks::Destroy
mount_mutation ::Mutations::ComplianceManagement::Frameworks::Update
mount_mutation ::Mutations::ComplianceManagement::Frameworks::Create
......
......@@ -63,9 +63,9 @@ module EE
return super unless gitlab_com_snippet_db_search?
if collection.total_pages > 1
s_("SearchResults|Showing %{from} - %{to} of %{count} %{scope} for%{term_element} in your personal and project snippets").html_safe
s_("SearchResults|Showing %{from} - %{to} of %{count} %{scope} for %{term_element} in your personal and project snippets").html_safe
else
s_("SearchResults|Showing %{count} %{scope} for%{term_element} in your personal and project snippets").html_safe
s_("SearchResults|Showing %{count} %{scope} for %{term_element} in your personal and project snippets").html_safe
end
end
......
......@@ -47,7 +47,7 @@ module EE
def lazy_entity
BatchLoader.for(entity_id)
.batch(
key: entity_type, default_value: ::Gitlab::Audit::NullEntity.new, replace_methods: false
key: entity_type, default_value: ::Gitlab::Audit::NullEntity.new
) do |ids, loader, args|
model = Object.const_get(args[:key], false)
model.where(id: ids).find_each { |record| loader.call(record.id, record) }
......
......@@ -153,7 +153,7 @@ module Vulnerabilities
end
def load_feedback
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] }
......
......@@ -136,8 +136,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resources :oncall_schedules, only: [:index], path: 'oncall_schedules'
resources :escalation_policies, only: [:index], path: 'escalation_policies'
end
resources :cluster_agents, only: [:show], param: :name
end
# End of the /-/ scope.
......
......@@ -190,7 +190,7 @@ module EE
scope :without_uuid, -> { where(uuid: nil) }
def feedback
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] }
......
......@@ -149,7 +149,7 @@ RSpec.describe SearchHelper do
let(:show_snippets) { true }
let(:collection) { Kaminari.paginate_array([:foo]).page(1).per(10) }
let(:user) { create(:user) }
let(:message) { "Showing %{count} %{scope} for%{term_element}" }
let(:message) { "Showing %{count} %{scope} for %{term_element}" }
let(:new_message) { message + " in your personal and project snippets" }
subject { search_entries_info_template(collection) }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Clusters::AgentTokens::CreateService do
subject(:service) { described_class.new(container: project, current_user: user, params: params) }
let_it_be(:user) { create(:user) }
let(:cluster_agent) { create(:cluster_agent) }
let(:project) { cluster_agent.project }
let(:params) { { agent_id: cluster_agent.id, description: 'token description', name: 'token name' } }
before do
stub_licensed_features(cluster_agents: false)
end
describe '#execute' do
subject { service.execute }
context 'without premium plan' do
it 'does not create a new token' do
expect { subject }.not_to change(Clusters::AgentToken, :count)
end
it 'returns missing license error' do
expect(subject.status).to eq(:error)
expect(subject.message).to eq('This feature is only available for premium plans')
end
context 'with premium plan' do
before do
stub_licensed_features(cluster_agents: true)
end
it 'does not create a new token due to user permissions' do
expect { subject }.not_to change(::Clusters::AgentToken, :count)
end
it 'returns permission errors', :aggregate_failures do
expect(subject.status).to eq(:error)
expect(subject.message).to eq('User has insufficient permissions to create a token for this project')
end
context 'with user permissions' do
before do
project.add_maintainer(user)
end
it 'creates a new token' do
expect { subject }.to change { ::Clusters::AgentToken.count }.by(1)
end
it 'returns success status', :aggregate_failures do
expect(subject.status).to eq(:success)
expect(subject.message).to be_nil
end
it 'returns token information', :aggregate_failures do
token = subject.payload[:token]
expect(subject.payload[:secret]).not_to be_nil
expect(token.created_by_user).to eq(user)
expect(token.description).to eq(params[:description])
expect(token.name).to eq(params[:name])
end
context 'when params are invalid' do
let(:params) { { agent_id: 'bad_id' } }
it 'does not create a new token' do
expect { subject }.not_to change(::Clusters::AgentToken, :count)
end
it 'returns validation errors', :aggregate_failures do
expect(subject.status).to eq(:error)
expect(subject.message).to eq(["Agent must exist", "Name can't be blank"])
end
end
end
end
end
end
end
......@@ -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)
......
......@@ -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]
......
......@@ -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"
......@@ -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"
......@@ -24874,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 ""
......@@ -25174,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 ""
......@@ -29763,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"
......
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',
......
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');
});
});
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');
});
});
});
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',
},
},
]);
},
},
};
......@@ -30,19 +30,8 @@ RSpec.describe Mutations::Clusters::AgentTokens::Create do
end
end
context 'without premium plan' do
context 'with user permissions' do
before do
stub_licensed_features(cluster_agents: false)
cluster_agent.project.add_maintainer(user)
end
it { expect(subject[:secret]).to be_nil }
it { expect(subject[:errors]).to eq(['This feature is only available for premium plans']) }
end
context 'with premium plan and user permissions' do
before do
stub_licensed_features(cluster_agents: true)
cluster_agent.project.add_maintainer(user)
end
......
......@@ -26,19 +26,8 @@ RSpec.describe Mutations::Clusters::Agents::Create do
end
end
context 'without premium plan' do
context 'with user permissions' do
before do
allow(License).to receive(:current).and_return(create(:license, plan: ::License::STARTER_PLAN))
project.add_maintainer(user)
end
it { expect(subject[:clusters_agent]).to be_nil }
it { expect(subject[:errors]).to eq(['This feature is only available for premium plans']) }
end
context 'with premium plan and user permissions' do
before do
allow(License).to receive(:current).and_return(create(:license, plan: ::License::PREMIUM_PLAN))
project.add_maintainer(user)
end
......
......@@ -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>&nbsp;<code>foo</code>&nbsp;</span>")
expect(search_entries_info(collection, scope, 'foo')).to eq("Showing 1 #{label} for <span>&nbsp;<code>foo</code>&nbsp;</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>&nbsp;<code>foo</code>&nbsp;</span>")
expect(search_entries_info(collection, scope, 'foo')).to eq("Showing 1 - 10 of 23 #{label.pluralize} for <span>&nbsp;<code>foo</code>&nbsp;</span>")
end
end
......
......@@ -32,8 +32,11 @@ RSpec.describe Gitlab::Pagination::Keyset::Iterator do
])
end
let(:iterator_params) { nil }
let(:scope) { project.issues.reorder(custom_reorder) }
subject(:iterator) { described_class.new(**iterator_params) }
shared_examples 'iterator examples' do
describe '.each_batch' do
it 'yields an ActiveRecord::Relation when a block is given' do
......@@ -56,6 +59,30 @@ RSpec.describe Gitlab::Pagination::Keyset::Iterator do
expect(count).to eq(9)
end
it 'continues after the cursor' do
loaded_records = []
cursor = nil
# stopping the iterator after the first batch and storing the cursor
iterator.each_batch(of: 2) do |relation| # rubocop: disable Lint/UnreachableLoop
loaded_records.concat(relation.to_a)
record = loaded_records.last
cursor = custom_reorder.cursor_attributes_for_node(record)
break
end
expect(loaded_records).to eq(project.issues.order(custom_reorder).take(2))
# continuing the iteration
new_iterator = described_class.new(**iterator_params.merge(cursor: cursor))
new_iterator.each_batch(of: 2) do |relation|
loaded_records.concat(relation.to_a)
end
expect(loaded_records).to eq(project.issues.order(custom_reorder))
end
it 'allows updating of the yielded relations' do
time = Time.current
......@@ -131,13 +158,13 @@ RSpec.describe Gitlab::Pagination::Keyset::Iterator do
end
context 'when use_union_optimization is used' do
subject(:iterator) { described_class.new(scope: scope, use_union_optimization: true) }
let(:iterator_params) { { scope: scope, use_union_optimization: true } }
include_examples 'iterator examples'
end
context 'when use_union_optimization is not used' do
subject(:iterator) { described_class.new(scope: scope, use_union_optimization: false) }
let(:iterator_params) { { scope: scope, use_union_optimization: false } }
include_examples 'iterator examples'
end
......
......@@ -432,9 +432,9 @@ RSpec.describe Namespace do
end
describe '.search' do
let_it_be(:first_group) { build(:group, name: 'my first namespace', path: 'old-path').tap(&:save!) }
let_it_be(:parent_group) { build(:group, name: 'my parent namespace', path: 'parent-path').tap(&:save!) }
let_it_be(:second_group) { build(:group, name: 'my second namespace', path: 'new-path', parent: parent_group).tap(&:save!) }
let_it_be(:first_group) { create(:group, name: 'my first namespace', path: 'old-path') }
let_it_be(:parent_group) { create(:group, name: 'my parent namespace', path: 'parent-path') }
let_it_be(:second_group) { create(:group, name: 'my second namespace', path: 'new-path', parent: parent_group) }
let_it_be(:project_with_same_path) { create(:project, id: second_group.id, path: first_group.path) }
it 'returns namespaces with a matching name' do
......
......@@ -31,21 +31,8 @@ RSpec.describe 'Create a new cluster agent token' do
end
end
context 'without premium plan' do
before do
stub_licensed_features(cluster_agents: false)
cluster_agent.project.add_maintainer(current_user)
end
it 'does not create a token and returns error message', :aggregate_failures do
expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change(Clusters::AgentToken, :count)
expect(mutation_response['errors']).to eq(['This feature is only available for premium plans'])
end
end
context 'with project permissions' do
before do
stub_licensed_features(cluster_agents: true)
cluster_agent.project.add_maintainer(current_user)
end
......
......@@ -28,21 +28,8 @@ RSpec.describe 'Create a new cluster agent' do
end
end
context 'without premium plan' do
context 'with user permissions' do
before do
allow(License).to receive(:current).and_return(create(:license, plan: ::License::STARTER_PLAN))
project.add_maintainer(current_user)
end
it 'does not create cluster agent and returns error message', :aggregate_failures do
expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change(Clusters::Agent, :count)
expect(mutation_response['errors']).to eq(['This feature is only available for premium plans'])
end
end
context 'with premium plan and user permissions' do
before do
allow(License).to receive(:current).and_return(create(:license, plan: ::License::PREMIUM_PLAN))
project.add_maintainer(current_user)
end
......
......@@ -30,9 +30,8 @@ RSpec.describe 'Delete a cluster agent' do
end
end
context 'with premium plan and project permissions' do
context 'with project permissions' do
before do
allow(License).to receive(:current).and_return(create(:license, plan: ::License::PREMIUM_PLAN))
project.add_maintainer(current_user)
end
......
......@@ -23,7 +23,6 @@ RSpec.describe 'Project.cluster_agents' do
before do
allow(Gitlab::Kas::Client).to receive(:new).and_return(double(get_connected_agents: []))
stub_licensed_features(cluster_agents: true)
end
it 'can retrieve cluster agents' do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Clusters::AgentTokens::CreateService do
subject(:service) { described_class.new(container: project, current_user: user, params: params) }
let_it_be(:user) { create(:user) }
let(:cluster_agent) { create(:cluster_agent) }
let(:project) { cluster_agent.project }
let(:params) { { agent_id: cluster_agent.id, description: 'token description', name: 'token name' } }
describe '#execute' do
subject { service.execute }
it 'does not create a new token due to user permissions' do
expect { subject }.not_to change(::Clusters::AgentToken, :count)
end
it 'returns permission errors', :aggregate_failures do
expect(subject.status).to eq(:error)
expect(subject.message).to eq('User has insufficient permissions to create a token for this project')
end
context 'with user permissions' do
before do
project.add_maintainer(user)
end
it 'creates a new token' do
expect { subject }.to change { ::Clusters::AgentToken.count }.by(1)
end
it 'returns success status', :aggregate_failures do
expect(subject.status).to eq(:success)
expect(subject.message).to be_nil
end
it 'returns token information', :aggregate_failures do
token = subject.payload[:token]
expect(subject.payload[:secret]).not_to be_nil
expect(token.created_by_user).to eq(user)
expect(token.description).to eq(params[:description])
expect(token.name).to eq(params[:name])
end
context 'when params are invalid' do
let(:params) { { agent_id: 'bad_id' } }
it 'does not create a new token' do
expect { subject }.not_to change(::Clusters::AgentToken, :count)
end
it 'returns validation errors', :aggregate_failures do
expect(subject.status).to eq(:error)
expect(subject.message).to eq(["Agent must exist", "Name can't be blank"])
end
end
end
end
end
......@@ -7,27 +7,9 @@ RSpec.describe Clusters::Agents::CreateService do
let(:project) { create(:project, :public, :repository) }
let(:user) { create(:user) }
let(:license) { create(:license, plan: ::License::PREMIUM_PLAN) }
describe '#execute' do
context 'without premium plan' do
before do
allow(License).to receive(:current).and_return(create(:license, plan: ::License::STARTER_PLAN))
end
it 'returns missing plan error' do
expect(service.execute(name: 'without-license')).to eq({
status: :error,
message: 'This feature is only available for premium plans'
})
end
end
context 'without user permissions' do
before do
allow(License).to receive(:current).and_return(license)
end
it 'returns errors when user does not have permissions' do
expect(service.execute(name: 'missing-permissions')).to eq({
status: :error,
......@@ -36,14 +18,13 @@ RSpec.describe Clusters::Agents::CreateService do
end
end
context 'with premium plan and user permissions' do
context 'with user permissions' do
before do
allow(License).to receive(:current).and_return(license)
project.add_maintainer(user)
end
it 'creates a new clusters_agent' do
expect { service.execute(name: 'with-license-and-user') }.to change { ::Clusters::Agent.count }.by(1)
expect { service.execute(name: 'with-user') }.to change { ::Clusters::Agent.count }.by(1)
end
it 'returns success status', :aggregate_failures do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment