Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-03-10 06:09:00 +00:00
parent 981fb44c36
commit 2332c32045
34 changed files with 348 additions and 284 deletions

View file

@ -1,10 +1,6 @@
---
Database/MultipleDatabases:
Exclude:
- ee/lib/gitlab/geo/database_tasks.rb
- ee/lib/gitlab/geo/geo_tasks.rb
- ee/lib/gitlab/geo/health_check.rb
- ee/lib/gitlab/geo/log_cursor/daemon.rb
- ee/spec/services/ee/merge_requests/update_service_spec.rb
- lib/backup/database.rb
- lib/backup/manager.rb

View file

@ -34,7 +34,10 @@ export default {
};
},
update(data) {
return data.localWorkItem;
return data.workItem;
},
skip() {
return !this.workItemId;
},
error() {
this.$emit(
@ -46,10 +49,7 @@ export default {
},
computed: {
workItemTitle() {
return this.workItem?.widgets?.nodes.find(
// eslint-disable-next-line no-underscore-dangle
(widget) => widget.__typename === 'LocalTitleWidget',
)?.contentText;
return this.workItem?.title;
},
},
};

View file

@ -1,16 +1,16 @@
#import './widget.fragment.graphql'
mutation createWorkItem($input: LocalCreateWorkItemInput) {
localCreateWorkItem(input: $input) @client {
mutation createWorkItem($input: WorkItemCreateInput!) {
workItemCreate(input: $input) {
workItem {
id
type
widgets {
title
workItemType {
id
}
widgets @client {
nodes {
...WidgetBase
... on LocalTitleWidget {
contentText
}
}
}
}

View file

@ -20,24 +20,18 @@ export function createApolloProvider() {
defaultClient.cache.writeQuery({
query: workItemQuery,
variables: {
id: '1',
id: 'gid://gitlab/WorkItem/1',
},
data: {
localWorkItem: {
__typename: 'LocalWorkItem',
id: '1',
id: 'gid://gitlab/WorkItem/1',
type: 'FEATURE',
// eslint-disable-next-line @gitlab/require-i18n-strings
title: 'Test Work Item',
widgets: {
__typename: 'LocalWorkItemWidgetConnection',
nodes: [
{
__typename: 'LocalTitleWidget',
type: 'TITLE',
enabled: true,
// eslint-disable-next-line @gitlab/require-i18n-strings
contentText: 'Test Work Item Title',
},
],
nodes: [],
},
},
},

View file

@ -1,53 +1,16 @@
import { uuids } from '~/lib/utils/uuids';
import workItemQuery from './work_item.query.graphql';
export const resolvers = {
Mutation: {
localCreateWorkItem(_, { input }, { cache }) {
const id = uuids()[0];
const workItem = {
__typename: 'LocalWorkItem',
type: 'FEATURE',
id,
widgets: {
__typename: 'LocalWorkItemWidgetConnection',
nodes: [
{
__typename: 'LocalTitleWidget',
type: 'TITLE',
enabled: true,
contentText: input.title,
},
],
},
};
cache.writeQuery({
query: workItemQuery,
variables: { id },
data: { localWorkItem: workItem },
});
return {
__typename: 'LocalCreateWorkItemPayload',
workItem,
};
},
localUpdateWorkItem(_, { input }, { cache }) {
const workItemTitle = {
__typename: 'LocalTitleWidget',
type: 'TITLE',
enabled: true,
contentText: input.title,
};
const workItem = {
__typename: 'LocalWorkItem',
type: 'FEATURE',
id: input.id,
title: input.title,
widgets: {
__typename: 'LocalWorkItemWidgetConnection',
nodes: [workItemTitle],
nodes: [],
},
};

View file

@ -22,14 +22,10 @@ type LocalWorkItemWidgetConnection {
pageInfo: PageInfo!
}
type LocalTitleWidget implements LocalWorkItemWidget {
type: LocalWidgetType!
contentText: String!
}
type LocalWorkItem {
id: ID!
type: LocalWorkItemType!
title: String!
widgets: [LocalWorkItemWidgetConnection]
}

View file

@ -1,16 +1,16 @@
#import './widget.fragment.graphql'
mutation updateWorkItem($input: LocalUpdateWorkItemInput) {
localUpdateWorkItem(input: $input) @client {
mutation workItemUpdate($input: WorkItemUpdateInput!) {
workItemUpdate(input: $input) {
workItem {
id
type
widgets {
title
workItemType {
id
}
widgets @client {
nodes {
...WidgetBase
... on LocalTitleWidget {
contentText
}
}
}
}

View file

@ -1,15 +1,15 @@
#import './widget.fragment.graphql'
query WorkItem($id: ID!) {
localWorkItem(id: $id) @client {
workItem(id: $id) {
id
type
widgets {
title
workItemType {
id
}
widgets @client {
nodes {
...WidgetBase
... on LocalTitleWidget {
contentText
}
}
}
}

View file

@ -1,6 +1,8 @@
<script>
import { GlButton, GlAlert, GlLoadingIcon, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import workItemQuery from '../graphql/work_item.query.graphql';
import createWorkItemMutation from '../graphql/create_work_item.mutation.graphql';
import projectWorkItemTypesQuery from '../graphql/project_work_item_types.query.graphql';
@ -67,19 +69,43 @@ export default {
variables: {
input: {
title: this.title,
projectPath: this.fullPath,
workItemTypeId: this.selectedWorkItemType?.id,
},
},
update(store, { data: { workItemCreate } }) {
const { id, title, workItemType } = workItemCreate.workItem;
store.writeQuery({
query: workItemQuery,
variables: {
id,
},
data: {
workItem: {
__typename: 'WorkItem',
id,
title,
workItemType,
widgets: {
__typename: 'LocalWorkItemWidgetConnection',
nodes: [],
},
},
},
});
},
});
const {
data: {
localCreateWorkItem: {
workItemCreate: {
workItem: { id, type },
},
},
} = response;
if (!this.isModal) {
this.$router.push({ name: 'workItem', params: { id } });
this.$router.push({ name: 'workItem', params: { id: `${getIdFromGraphQLId(id)}` } });
} else {
this.$emit('onCreate', { id, title: this.title, type });
}

View file

@ -1,9 +1,10 @@
<script>
import { GlAlert } from '@gitlab/ui';
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import Tracking from '~/tracking';
import workItemQuery from '../graphql/work_item.query.graphql';
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
import { widgetTypes, WI_TITLE_TRACK_LABEL } from '../constants';
import { WI_TITLE_TRACK_LABEL } from '../constants';
import ItemTitle from '../components/item_title.vue';
@ -14,6 +15,7 @@ export default {
components: {
ItemTitle,
GlAlert,
GlLoadingIcon,
},
mixins: [trackingMixin],
props: {
@ -24,7 +26,7 @@ export default {
},
data() {
return {
workItem: null,
workItem: {},
error: false,
};
},
@ -33,12 +35,9 @@ export default {
query: workItemQuery,
variables() {
return {
id: this.id,
id: this.gid,
};
},
update(data) {
return data.localWorkItem;
},
},
},
computed: {
@ -50,19 +49,19 @@ export default {
property: '[type_work_item]',
};
},
titleWidgetData() {
return this.workItem?.widgets?.nodes?.find((widget) => widget.type === widgetTypes.title);
gid() {
return convertToGraphQLId('WorkItem', this.id);
},
},
methods: {
async updateWorkItem(title) {
async updateWorkItem(updatedTitle) {
try {
await this.$apollo.mutate({
mutation: updateWorkItemMutation,
variables: {
input: {
id: this.id,
title,
id: this.gid,
title: updatedTitle,
},
},
});
@ -82,12 +81,18 @@ export default {
}}</gl-alert>
<!-- Title widget placeholder -->
<div>
<item-title
v-if="titleWidgetData"
:initial-title="titleWidgetData.contentText"
data-testid="title"
@title-changed="updateWorkItem"
<gl-loading-icon
v-if="$apollo.queries.workItem.loading"
size="md"
data-testid="loading-types"
/>
<template v-else>
<item-title
:initial-title="workItem.title"
data-testid="title"
@title-changed="updateWorkItem"
/>
</template>
</div>
</section>
</template>

View file

@ -243,51 +243,6 @@ module SortingHelper
sort_options_hash[sort_value]
end
def issuable_sort_options(viewing_issues, viewing_merge_requests)
options = [
{ value: sort_value_priority, text: sort_title_priority, href: page_filter_path(sort: sort_value_priority) },
{ value: sort_value_created_date, text: sort_title_created_date, href: page_filter_path(sort: sort_value_created_date) },
{ value: sort_value_recently_updated, text: sort_title_recently_updated, href: page_filter_path(sort: sort_value_recently_updated) },
{ value: sort_value_milestone, text: sort_title_milestone, href: page_filter_path(sort: sort_value_milestone) }
]
options.concat([due_date_option]) if viewing_issues
options.concat([popularity_option, label_priority_option])
options.concat([merged_option, closed_option]) if viewing_merge_requests
options.concat([relative_position_option]) if viewing_issues
options.concat([title_option])
end
def due_date_option
{ value: sort_value_due_date, text: sort_title_due_date, href: page_filter_path(sort: sort_value_due_date) }
end
def popularity_option
{ value: sort_value_popularity, text: sort_title_popularity, href: page_filter_path(sort: sort_value_popularity) }
end
def label_priority_option
{ value: sort_value_label_priority, text: sort_title_label_priority, href: page_filter_path(sort: sort_value_label_priority) }
end
def merged_option
{ value: sort_value_merged_date, text: sort_title_merged_date, href: page_filter_path(sort: sort_value_merged_date) }
end
def closed_option
{ value: sort_value_closed_date, text: sort_title_closed_date, href: page_filter_path(sort: sort_value_closed_date) }
end
def relative_position_option
{ value: sort_value_relative_position, text: sort_title_relative_position, href: page_filter_path(sort: sort_value_relative_position) }
end
def title_option
{ value: sort_value_title, text: sort_title_title, href: page_filter_path(sort: sort_value_title) }
end
def sort_direction_icon(sort_value)
case sort_value
when sort_value_milestone, sort_value_due_date, sort_value_merged_date, sort_value_closed_date, /_asc\z/

View file

@ -6,12 +6,12 @@
= cache(cache_key, expires_in: 1.day) do
- if @merge_request.closed_or_merged_without_fork?
.gl-alert.gl-alert-danger.gl-mb-5
.gl-alert-container
= sprite_icon('error', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
.gl-alert-content
.gl-alert-body
The source project of this merge request has been removed.
= render 'shared/global_alert',
alert_class: 'gl-mb-5',
variant: :danger,
dismissible: false do
.gl-alert-body
= _('The source project of this merge request has been removed.')
.detail-page-header.border-bottom-0.pt-0.pb-0
.detail-page-header-body
@ -45,9 +45,9 @@
%li= link_to _('Report abuse'), new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request))
- if can_update_merge_request
= link_to _('Edit'), edit_project_merge_request_path(@project, @merge_request), class: "d-none d-md-block btn gl-button btn-default btn-grouped js-issuable-edit", data: { qa_selector: "edit_button" }
= link_to _('Edit'), edit_project_merge_request_path(@project, @merge_request), class: "gl-display-none gl-md-display-block btn gl-button btn-default btn-grouped js-issuable-edit", data: { qa_selector: "edit_button" }
- if can_update_merge_request && !are_close_and_open_buttons_hidden
= render 'projects/merge_requests/close_reopen_draft_report_toggle'
- elsif !@merge_request.merged?
= link_to _('Report abuse'), new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request)), class: 'gl-display-none gl-md-display-block gl-button btn btn-default float-right gl-ml-3', title: _('Report abuse')
= link_to _('Report abuse'), new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request)), class: 'gl-display-none gl-md-display-block gl-button btn btn-default gl-float-right gl-ml-3', title: _('Report abuse')

View file

@ -1,9 +1,26 @@
- sort_value = @sort
- sort_title = issuable_sort_option_title(sort_value)
- viewing_issues = controller.controller_name == 'issues' || controller.action_name == 'issues'
- viewing_merge_requests = controller.controller_name == 'merge_requests'
- items = issuable_sort_options(viewing_issues, viewing_merge_requests)
- selected = issuable_sort_option_overrides[@sort] || @sort
.gl-ml-3
.dropdown.inline.gl-ml-3.issue-sort-dropdown
.btn-group{ role: 'group' }
= gl_redirect_listbox_tag(items, selected, data: { right: true })
= issuable_sort_direction_button(@sort)
.btn-group{ role: 'group' }
%button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' }, class: 'gl-button btn btn-default' }
= sort_title
= sprite_icon('chevron-down', css_class: "dropdown-menu-toggle-icon gl-top-3")
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort
%li
= sortable_item(sort_title_priority, page_filter_path(sort: sort_value_priority), sort_title)
= sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_created_date), sort_title)
= sortable_item(sort_title_recently_updated, page_filter_path(sort: sort_value_recently_updated), sort_title)
= sortable_item(sort_title_milestone, page_filter_path(sort: sort_value_milestone), sort_title)
= sortable_item(sort_title_due_date, page_filter_path(sort: sort_value_due_date), sort_title) if viewing_issues
= sortable_item(sort_title_popularity, page_filter_path(sort: sort_value_popularity), sort_title)
= sortable_item(sort_title_label_priority, page_filter_path(sort: sort_value_label_priority), sort_title)
= sortable_item(sort_title_merged_date, page_filter_path(sort: sort_value_merged_date), sort_title) if viewing_merge_requests
= sortable_item(sort_title_closed_date, page_filter_path(sort: sort_value_closed_date), sort_title) if viewing_merge_requests
= sortable_item(sort_title_relative_position, page_filter_path(sort: sort_value_relative_position), sort_title) if viewing_issues
= sortable_item(sort_title_title, page_filter_path(sort: sort_value_title), sort_title)
= render_if_exists('shared/ee/issuable/sort_dropdown', viewing_issues: viewing_issues, sort_title: sort_title)
= issuable_sort_direction_button(sort_value)

View file

@ -163,9 +163,8 @@ For more information, see [Deployment safety](deployment_safety.md).
Typically, large enterprise organizations have an explicit permission boundary
between [developers and operators](https://about.gitlab.com/topics/devops/).
Developers build and test their code, and operators deploy and monitor the
application. With group-level protected environments, the permission of each
group is carefully configured in order to prevent unauthorized access and
maintain proper separation of duty. Group-level protected environments
application. With group-level protected environments, operators can
restrict access to critical environments from developers. Group-level protected environments
extend the [project-level protected environments](#protecting-environments)
to the group-level.
@ -194,12 +193,6 @@ and are protected at the same time.
### Configure group-level memberships
In an enterprise organization, with thousands of projects under a single group,
ensuring that all of the [project-level protected environments](#protecting-environments)
are properly configured is not a scalable solution. For example, a developer
might gain privileged access to a higher environment when they are given the Maintainer role
for a new project. Group-level protected environments can be a solution in this situation.
To maximize the effectiveness of group-level protected environments,
[group-level memberships](../../user/group/index.md) must be correctly
configured:
@ -237,7 +230,7 @@ Having this configuration in place:
- If a user is about to run a deployment job in a project but disallowed to
deploy to the environment, the deployment job fails with an error message.
### Protect a group-level environment
### Protect critical environments under a group
To protect a group-level environment:

View file

@ -36,6 +36,6 @@ git tag
git push origin --tags
```
## Additional resources
## Related topics
- [Tagging](https://git-scm.com/book/en/v2/Git-Basics-Tagging) Git reference page

View file

@ -51,7 +51,7 @@ secret_detection:
else
# determine commit range so that we can fetch the appropriate depth
# check the exit code to determine if we need to limit the commit_list.txt to CI_COMMIT_SHA.
if ! git log --no-merges --pretty=format:"%H" ${CI_COMMIT_BEFORE_SHA}..${CI_COMMIT_SHA} >${CI_COMMIT_SHA}_commit_list.txt;
if ! git log --pretty=format:"%H" ${CI_COMMIT_BEFORE_SHA}..${CI_COMMIT_SHA} >${CI_COMMIT_SHA}_commit_list.txt;
then
echo "unable to determine commit range, limiting to ${CI_COMMIT_SHA}"
echo ${CI_COMMIT_SHA} >${CI_COMMIT_SHA}_commit_list.txt

View file

@ -19,8 +19,7 @@ module Gitlab
variables.concat(project.predefined_variables)
variables.concat(pipeline.predefined_variables)
variables.concat(job.runner.predefined_variables) if job.runnable? && job.runner
variables.concat(kubernetes_variables(job))
variables.concat(deployment_variables(environment: environment, job: job))
variables.concat(kubernetes_variables(environment: environment, job: job))
variables.concat(job.yaml_variables)
variables.concat(user_variables(job.user))
variables.concat(job.dependency_variables) if dependencies
@ -33,11 +32,15 @@ module Gitlab
end
end
def kubernetes_variables(job)
def kubernetes_variables(environment:, job:)
::Gitlab::Ci::Variables::Collection.new.tap do |collection|
# Should get merged with the cluster kubeconfig in deployment_variables, see
# https://gitlab.com/gitlab-org/gitlab/-/issues/335089
# NOTE: deployment_variables will be removed as part of cleanup for
# https://gitlab.com/groups/gitlab-org/configure/-/epics/8
# Until then, we need to make both the old and the new KUBECONFIG contexts available
collection.concat(deployment_variables(environment: environment, job: job))
template = ::Ci::GenerateKubeconfigService.new(job).execute
kubeconfig_yaml = collection['KUBECONFIG']&.value
template.merge_yaml(kubeconfig_yaml) if kubeconfig_yaml.present?
if template.valid?
collection.append(key: 'KUBECONFIG', value: template.to_yaml, public: false, file: true)

View file

@ -14,6 +14,7 @@ module Gitlab
@clusters = []
@users = []
@contexts = []
@current_context = nil
end
def valid?
@ -32,14 +33,45 @@ module Gitlab
contexts << new_entry(:context, **args)
end
def merge_yaml(kubeconfig_yaml)
return unless kubeconfig_yaml
kubeconfig_yaml = YAML.safe_load(kubeconfig_yaml, symbolize_names: true)
kubeconfig_yaml[:users].each do |user|
add_user(
name: user[:name],
token: user.dig(:user, :token)
)
end
kubeconfig_yaml[:clusters].each do |cluster|
ca_pem = cluster.dig(:cluster, :'certificate-authority-data')&.yield_self do |data|
Base64.strict_decode64(data)
end
add_cluster(
name: cluster[:name],
url: cluster.dig(:cluster, :server),
ca_pem: ca_pem
)
end
kubeconfig_yaml[:contexts].each do |context|
add_context(
name: context[:name],
**context[:context]&.slice(:cluster, :user, :namespace)
)
end
@current_context = kubeconfig_yaml[:'current-context']
end
def to_h
{
apiVersion: 'v1',
kind: 'Config',
clusters: clusters.map(&:to_h),
users: users.map(&:to_h),
contexts: contexts.map(&:to_h)
}
contexts: contexts.map(&:to_h),
'current-context': current_context
}.compact
end
def to_yaml
@ -48,7 +80,7 @@ module Gitlab
private
attr_reader :clusters, :users, :contexts
attr_reader :clusters, :users, :contexts, :current_context
def new_entry(entry, **args)
ENTRIES.fetch(entry).new(**args)

View file

@ -36924,6 +36924,9 @@ msgstr ""
msgid "The snippet is visible to any logged in user except external users."
msgstr ""
msgid "The source project of this merge request has been removed."
msgstr ""
msgid "The specified tab is invalid, please select another"
msgstr ""

View file

@ -78,14 +78,14 @@ RSpec.describe 'Dashboard Issues filtering', :js do
end
it 'remembers last sorting value' do
pajamas_sort_by(s_('SortOptions|Created date'))
sort_by('Created date')
visit_issues(assignee_username: user.username)
expect(page).to have_button('Created date')
end
it 'keeps sorting issues after visiting Projects Issues page' do
pajamas_sort_by(s_('SortOptions|Created date'))
sort_by('Created date')
visit project_issues_path(project)
expect(page).to have_button('Created date')

View file

@ -115,9 +115,6 @@ RSpec.describe 'Dashboard Merge Requests' do
within("span[aria-label='#{n_("%d merge request", "%d merge requests", 3) % 3}']") do
expect(page).to have_content('3')
end
find('.dashboard-shortcuts-merge_requests').click
expect(find('.js-assigned-mr-count')).to have_content('2')
expect(find('.js-reviewer-mr-count')).to have_content('1')
end
@ -168,16 +165,16 @@ RSpec.describe 'Dashboard Merge Requests' do
expect(page).to have_content('Please select at least one filter to see results')
end
it 'shows sorted merge requests', :js do
pajamas_sort_by(s_('SortOptions|Created date'))
it 'shows sorted merge requests' do
sort_by('Created date')
visit merge_requests_dashboard_path(assignee_username: current_user.username)
expect(find('.issues-filters')).to have_content('Created date')
end
it 'keeps sorting merge requests after visiting Projects MR page', :js do
pajamas_sort_by(s_('SortOptions|Created date'))
it 'keeps sorting merge requests after visiting Projects MR page' do
sort_by('Created date')
visit project_merge_requests_path(project)

View file

@ -88,14 +88,14 @@ RSpec.describe 'Sort Issuable List' do
end
end
context 'custom sorting', :js do
context 'custom sorting' do
let(:issuable_type) { :merge_request }
it 'supports sorting in asc and desc order' do
visit_merge_requests_with_state(project, 'open')
click_button('Created date')
find('.dropdown-item', text: 'Updated date').click
click_link('Updated date')
expect(first_merge_request).to include(last_updated_issuable.title)
expect(last_merge_request).to include(first_updated_issuable.title)

View file

@ -556,7 +556,7 @@ RSpec.describe 'Filter issues', :js do
sort_toggle = find('.filter-dropdown-container .dropdown')
sort_toggle.click
find('.dropdown-item', text: 'Created date').click
find('.filter-dropdown-container .dropdown-menu li a', text: 'Created date').click
wait_for_requests
expect(find('.issues-list .issue:first-of-type .issue-title-text a')).to have_content(new_issue.title)

View file

@ -5,7 +5,6 @@ require "spec_helper"
RSpec.describe "User sorts issues" do
include SortingHelper
include IssueHelpers
include Spec::Support::Helpers::Features::SortingHelpers
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
@ -25,22 +24,26 @@ RSpec.describe "User sorts issues" do
sign_in(user)
end
it 'keeps the sort option', :js do
it 'keeps the sort option' do
visit(project_issues_path(project))
pajamas_sort_by(s_('SortOptions|Milestone'))
find('.filter-dropdown-container .dropdown').click
page.within('ul.dropdown-menu.dropdown-menu-right li') do
click_link('Milestone')
end
visit(issues_dashboard_path(assignee_username: user.username))
expect(find('.filter-dropdown-container button.dropdown-toggle')).to have_content('Milestone')
expect(find('.issues-filters a.is-active')).to have_content('Milestone')
visit(project_issues_path(project))
expect(find('.filter-dropdown-container button.dropdown-toggle')).to have_content('Milestone')
expect(find('.issues-filters a.is-active')).to have_content('Milestone')
visit(issues_group_path(group))
expect(find('.filter-dropdown-container button.dropdown-toggle')).to have_content('Milestone')
expect(find('.issues-filters a.is-active')).to have_content('Milestone')
end
it 'sorts by popularity', :js do

View file

@ -109,7 +109,7 @@ RSpec.describe 'Merge requests > User lists merge requests' do
expect(count_merge_requests).to eq(4)
end
it 'sorts by milestone due date' do
it 'sorts by milestone' do
visit_merge_requests(project, sort: sort_value_milestone)
expect(first_merge_request).to include('fix')
@ -130,12 +130,12 @@ RSpec.describe 'Merge requests > User lists merge requests' do
expect(count_merge_requests).to eq(4)
end
it 'filters on one label and sorts by milestone due date' do
it 'filters on one label and sorts by due date' do
label = create(:label, project: project)
create(:label_link, label: label, target: @fix)
visit_merge_requests(project, label_name: [label.name],
sort: sort_value_milestone)
sort: sort_value_due_date)
expect(first_merge_request).to include('fix')
expect(count_merge_requests).to eq(1)
@ -150,19 +150,19 @@ RSpec.describe 'Merge requests > User lists merge requests' do
create(:label_link, label: label2, target: @fix)
end
it 'sorts by milestone due date' do
it 'sorts by due date' do
visit_merge_requests(project, label_name: [label.name, label2.name],
sort: sort_value_milestone)
sort: sort_value_due_date)
expect(first_merge_request).to include('fix')
expect(count_merge_requests).to eq(1)
end
context 'filter on assignee and' do
it 'sorts by milestone due date' do
it 'sorts by due soon' do
visit_merge_requests(project, label_name: [label.name, label2.name],
assignee_id: user.id,
sort: sort_value_milestone)
sort: sort_value_due_date)
expect(first_merge_request).to include('fix')
expect(count_merge_requests).to eq(1)

View file

@ -2,9 +2,8 @@
require 'spec_helper'
RSpec.describe 'User sorts merge requests', :js do
RSpec.describe 'User sorts merge requests' do
include CookieHelper
include Spec::Support::Helpers::Features::SortingHelpers
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let!(:merge_request2) do
@ -23,19 +22,23 @@ RSpec.describe 'User sorts merge requests', :js do
end
it 'keeps the sort option' do
pajamas_sort_by(s_('SortOptions|Milestone'))
find('.filter-dropdown-container .dropdown').click
page.within('ul.dropdown-menu.dropdown-menu-right li') do
click_link('Milestone')
end
visit(merge_requests_dashboard_path(assignee_username: user.username))
expect(find('.filter-dropdown-container button.dropdown-toggle')).to have_content('Milestone')
expect(find('.issues-filters a.is-active')).to have_content('Milestone')
visit(project_merge_requests_path(project))
expect(find('.filter-dropdown-container button.dropdown-toggle')).to have_content('Milestone')
expect(find('.issues-filters a.is-active')).to have_content('Milestone')
visit(merge_requests_group_path(group))
expect(find('.filter-dropdown-container button.dropdown-toggle')).to have_content('Milestone')
expect(find('.issues-filters a.is-active')).to have_content('Milestone')
end
it 'fallbacks to issuable_sort cookie key when remembering the sorting option' do
@ -43,13 +46,17 @@ RSpec.describe 'User sorts merge requests', :js do
visit(merge_requests_dashboard_path(assignee_username: user.username))
expect(find('.filter-dropdown-container button.dropdown-toggle')).to have_content('Milestone')
expect(find('.issues-filters a.is-active')).to have_content('Milestone')
end
it 'separates remember sorting with issues' do
create(:issue, project: project)
pajamas_sort_by(s_('SortOptions|Milestone'))
find('.filter-dropdown-container .dropdown').click
page.within('ul.dropdown-menu.dropdown-menu-right li') do
click_link('Milestone')
end
visit(project_issues_path(project))
@ -66,7 +73,11 @@ RSpec.describe 'User sorts merge requests', :js do
end
it 'sorts by popularity' do
pajamas_sort_by(s_('SortOptions|Popularity'))
find('.filter-dropdown-container .dropdown').click
page.within('ul.dropdown-menu.dropdown-menu-right li') do
click_link('Popularity')
end
page.within('.mr-list') do
page.within('li.merge-request:nth-child(1)') do

View file

@ -6,7 +6,7 @@ require "spec_helper"
# to check if the sorting option set by user is being kept persisted while going through pages.
# The `it`s are named here by convention `starting point -> some pages -> final point`.
# All those specs are moved out to this spec intentionally to keep them all in one place.
RSpec.describe "User sorts things", :js do
RSpec.describe "User sorts things" do
include Spec::Support::Helpers::Features::SortingHelpers
include DashboardHelper
@ -21,11 +21,11 @@ RSpec.describe "User sorts things", :js do
end
it "issues -> project home page -> issues" do
sort_option = s_('SortOptions|Updated date')
sort_option = 'Updated date'
visit(project_issues_path(project))
pajamas_sort_by(sort_option)
sort_by(sort_option)
visit(project_path(project))
visit(project_issues_path(project))
@ -34,11 +34,11 @@ RSpec.describe "User sorts things", :js do
end
it "merge requests -> dashboard merge requests" do
sort_option = s_('SortOptions|Updated date')
sort_option = 'Updated date'
visit(project_merge_requests_path(project))
pajamas_sort_by(sort_option)
sort_by(sort_option)
visit(assigned_mrs_dashboard_path)

View file

@ -1,8 +1,12 @@
export const workItemQueryResponse = {
localWorkItem: {
__typename: 'LocalWorkItem',
workItem: {
__typename: 'WorkItem',
id: '1',
type: 'FEATURE',
title: 'Test',
workItemType: {
__typename: 'WorkItemType',
id: 'work-item-type-1',
},
widgets: {
__typename: 'LocalWorkItemWidgetConnection',
nodes: [
@ -17,20 +21,29 @@ export const workItemQueryResponse = {
};
export const updateWorkItemMutationResponse = {
__typename: 'LocalUpdateWorkItemPayload',
workItem: {
__typename: 'LocalWorkItem',
id: '1',
widgets: {
__typename: 'LocalWorkItemWidgetConnection',
nodes: [
{
__typename: 'LocalTitleWidget',
type: 'TITLE',
enabled: true,
contentText: 'Updated title',
data: {
workItemUpdate: {
__typename: 'LocalUpdateWorkItemPayload',
workItem: {
__typename: 'LocalWorkItem',
id: '1',
title: 'Updated title',
workItemType: {
__typename: 'WorkItemType',
id: 'work-item-type-1',
},
],
widgets: {
__typename: 'LocalWorkItemWidgetConnection',
nodes: [
{
__typename: 'LocalTitleWidget',
type: 'TITLE',
enabled: true,
contentText: 'Updated title',
},
],
},
},
},
},
};
@ -48,3 +61,20 @@ export const projectWorkItemTypesQueryResponse = {
},
},
};
export const createWorkItemMutationResponse = {
data: {
workItemCreate: {
__typename: 'WorkItemCreatePayload',
workItem: {
__typename: 'WorkItem',
id: '1',
title: 'Updated title',
workItemType: {
__typename: 'WorkItemType',
id: 'work-item-type-1',
},
},
},
},
};

View file

@ -8,7 +8,8 @@ import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
import ItemTitle from '~/work_items/components/item_title.vue';
import { resolvers } from '~/work_items/graphql/resolvers';
import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
import { projectWorkItemTypesQueryResponse } from '../mock_data';
import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql';
import { projectWorkItemTypesQueryResponse, createWorkItemMutationResponse } from '../mock_data';
jest.mock('~/lib/utils/uuids', () => ({ uuids: () => ['testuuid'] }));
@ -19,6 +20,7 @@ describe('Create work item component', () => {
let fakeApollo;
const querySuccessHandler = jest.fn().mockResolvedValue(projectWorkItemTypesQueryResponse);
const mutationSuccessHandler = jest.fn().mockResolvedValue(createWorkItemMutationResponse);
const findAlert = () => wrapper.findComponent(GlAlert);
const findTitleInput = () => wrapper.findComponent(ItemTitle);
@ -30,8 +32,19 @@ describe('Create work item component', () => {
const findContent = () => wrapper.find('[data-testid="content"]');
const findLoadingTypesIcon = () => wrapper.find('[data-testid="loading-types"]');
const createComponent = ({ data = {}, props = {}, queryHandler = querySuccessHandler } = {}) => {
fakeApollo = createMockApollo([[projectWorkItemTypesQuery, queryHandler]], resolvers);
const createComponent = ({
data = {},
props = {},
queryHandler = querySuccessHandler,
mutationHandler = mutationSuccessHandler,
} = {}) => {
fakeApollo = createMockApollo(
[
[projectWorkItemTypesQuery, queryHandler],
[createWorkItemMutation, mutationHandler],
],
resolvers,
);
wrapper = shallowMount(CreateWorkItem, {
apolloProvider: fakeApollo,
data() {
@ -126,7 +139,7 @@ describe('Create work item component', () => {
wrapper.find('form').trigger('submit');
await waitForPromises();
const expected = { id: 'testuuid', title: mockTitle, type: 'FEATURE' };
const expected = { id: '1', title: mockTitle };
expect(wrapper.emitted('onCreate')).toEqual([[expected]]);
});

View file

@ -9,11 +9,12 @@ import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutati
import WorkItemsRoot from '~/work_items/pages/work_item_root.vue';
import ItemTitle from '~/work_items/components/item_title.vue';
import { resolvers } from '~/work_items/graphql/resolvers';
import { workItemQueryResponse } from '../mock_data';
import { workItemQueryResponse, updateWorkItemMutationResponse } from '../mock_data';
Vue.use(VueApollo);
const WORK_ITEM_ID = '1';
const WORK_ITEM_GID = `gid://gitlab/WorkItem/${WORK_ITEM_ID}`;
describe('Work items root component', () => {
const mockUpdatedTitle = 'Updated title';
@ -23,15 +24,19 @@ describe('Work items root component', () => {
const findTitle = () => wrapper.findComponent(ItemTitle);
const createComponent = ({ queryResponse = workItemQueryResponse } = {}) => {
fakeApollo = createMockApollo([], resolvers, {
possibleTypes: {
LocalWorkItemWidget: ['LocalTitleWidget'],
fakeApollo = createMockApollo(
[[updateWorkItemMutation, jest.fn().mockResolvedValue(updateWorkItemMutationResponse)]],
resolvers,
{
possibleTypes: {
LocalWorkItemWidget: ['LocalTitleWidget'],
},
},
});
);
fakeApollo.clients.defaultClient.cache.writeQuery({
query: workItemQuery,
variables: {
id: WORK_ITEM_ID,
id: WORK_ITEM_GID,
},
data: queryResponse,
});
@ -49,7 +54,7 @@ describe('Work items root component', () => {
fakeApollo = null;
});
it('renders the title if title is in the widgets list', () => {
it('renders the title', () => {
createComponent();
expect(findTitle().exists()).toBe(true);
@ -66,35 +71,11 @@ describe('Work items root component', () => {
mutation: updateWorkItemMutation,
variables: {
input: {
id: WORK_ITEM_ID,
id: WORK_ITEM_GID,
title: mockUpdatedTitle,
},
},
});
await waitForPromises();
expect(findTitle().props('initialTitle')).toBe(mockUpdatedTitle);
});
it('does not render the title if title is not in the widgets list', () => {
const queryResponse = {
workItem: {
...workItemQueryResponse.workItem,
widgets: {
__typename: 'WorkItemWidgetConnection',
nodes: [
{
__typename: 'SomeOtherWidget',
type: 'OTHER',
contentText: 'Test',
},
],
},
},
};
createComponent({ queryResponse });
expect(findTitle().exists()).toBe(false);
});
describe('tracking', () => {

View file

@ -21,6 +21,7 @@ describe('Work items router', () => {
mocks: {
$apollo: {
queries: {
workItem: {},
workItemTypes: {},
},
},

View file

@ -158,7 +158,6 @@ RSpec.describe Gitlab::Ci::Variables::Builder do
allow(pipeline).to receive(:predefined_variables) { [var('C', 3), var('D', 3)] }
allow(job).to receive(:runner) { double(predefined_variables: [var('D', 4), var('E', 4)]) }
allow(builder).to receive(:kubernetes_variables) { [var('E', 5), var('F', 5)] }
allow(builder).to receive(:deployment_variables) { [var('F', 6), var('G', 6)] }
allow(job).to receive(:yaml_variables) { [var('G', 7), var('H', 7)] }
allow(builder).to receive(:user_variables) { [var('H', 8), var('I', 8)] }
allow(job).to receive(:dependency_variables) { [var('I', 9), var('J', 9)] }
@ -177,7 +176,6 @@ RSpec.describe Gitlab::Ci::Variables::Builder do
var('C', 3), var('D', 3),
var('D', 4), var('E', 4),
var('E', 5), var('F', 5),
var('F', 6), var('G', 6),
var('G', 7), var('H', 7),
var('H', 8), var('I', 8),
var('I', 9), var('J', 9),
@ -193,7 +191,7 @@ RSpec.describe Gitlab::Ci::Variables::Builder do
expect(subject.to_hash).to match(
'A' => '1', 'B' => '2',
'C' => '3', 'D' => '4',
'E' => '5', 'F' => '6',
'E' => '5', 'F' => '5',
'G' => '7', 'H' => '8',
'I' => '9', 'J' => '10',
'K' => '11', 'L' => '12',
@ -231,7 +229,7 @@ RSpec.describe Gitlab::Ci::Variables::Builder do
let(:template) { double(to_yaml: 'example-kubeconfig', valid?: template_valid) }
let(:template_valid) { true }
subject { builder.kubernetes_variables(job) }
subject { builder.kubernetes_variables(environment: nil, job: job) }
before do
allow(Ci::GenerateKubeconfigService).to receive(:new).with(job).and_return(service)
@ -244,6 +242,16 @@ RSpec.describe Gitlab::Ci::Variables::Builder do
it { is_expected.not_to include(key: 'KUBECONFIG', value: 'example-kubeconfig', public: false, file: true) }
end
it 'includes #deployment_variables and merges the KUBECONFIG values', :aggregate_failures do
expect(builder).to receive(:deployment_variables).and_return([
{ key: 'KUBECONFIG', value: 'deployment-kubeconfig' },
{ key: 'OTHER', value: 'some value' }
])
expect(template).to receive(:merge_yaml).with('deployment-kubeconfig')
expect(subject['KUBECONFIG'].value).to eq('example-kubeconfig')
expect(subject['OTHER'].value).to eq('some value')
end
end
describe '#deployment_variables' do

View file

@ -39,6 +39,51 @@ RSpec.describe Gitlab::Kubernetes::Kubeconfig::Template do
it { is_expected.to eq(YAML.dump(template.to_h.deep_stringify_keys)) }
end
describe '#merge_yaml' do
it 'appends to the configuration and overwrites the current context' do
template.add_cluster(name: 'hello-cluster', url: 'hello-url')
template.add_context(name: 'hello-context', cluster: 'hello-cluster', user: 'hello-user')
template.add_user(name: 'hello-user', token: 'hello-token')
ca_pem = Base64.strict_encode64('a certificate')
template.merge_yaml(<<~YAML)
apiVersion: v1
kind: Config
clusters:
- name: 'gitlab-deploy'
cluster:
server: url
certificate-authority-data: #{ca_pem.inspect}
contexts:
- name: gitlab-deploy
context:
cluster: gitlab-deploy
namespace: namespace
user: gitlab-deploy
current-context: gitlab-deploy
users:
- name: 'gitlab-deploy'
user: { token: token }
YAML
expect(template.to_h).to eq({
apiVersion: 'v1',
kind: 'Config',
clusters: [
{ name: 'hello-cluster', cluster: { server: 'hello-url' } },
{ name: 'gitlab-deploy', cluster: { server: 'url', 'certificate-authority-data': ca_pem } }
],
contexts: [
{ name: 'hello-context', context: { cluster: 'hello-cluster', user: 'hello-user' } },
{ name: 'gitlab-deploy', context: { cluster: 'gitlab-deploy', namespace: 'namespace', user: 'gitlab-deploy' } }
],
users: [
{ name: 'hello-user', user: { token: 'hello-token' } },
{ name: 'gitlab-deploy', user: { token: 'token' } }
],
'current-context': 'gitlab-deploy'
})
end
end
describe 'adding entries' do
let(:entry) { instance_double(entry_class, to_h: attributes) }
let(:attributes) do

View file

@ -21,14 +21,6 @@ module Spec
click_link(value)
end
end
# pajamas_sort_by is used to sort new pajamas dropdowns. When
# all of the dropdowns are converted, pajamas_sort_by can be renamed to sort_by
# https://gitlab.com/groups/gitlab-org/-/epics/7551
def pajamas_sort_by(value)
find('.filter-dropdown-container .dropdown').click
find('.dropdown-item', text: value).click
end
end
end
end