Commit b3a736ed authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 5366964a
...@@ -70,7 +70,7 @@ export default { ...@@ -70,7 +70,7 @@ export default {
:title="$options.currentBranchPermissionsTooltip" :title="$options.currentBranchPermissionsTooltip"
> >
<span <span
class="ide-radio-label" class="ide-option-label"
data-qa-selector="commit_to_current_branch_radio" data-qa-selector="commit_to_current_branch_radio"
v-html="commitToCurrentBranchText" v-html="commitToCurrentBranchText"
></span> ></span>
......
<script> <script>
import { createNamespacedHelpers } from 'vuex'; import { createNamespacedHelpers } from 'vuex';
import { GlTooltipDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
const { const { mapActions: mapCommitActions, mapGetters: mapCommitGetters } = createNamespacedHelpers(
mapState: mapCommitState, 'commit',
mapActions: mapCommitActions, );
mapGetters: mapCommitGetters,
} = createNamespacedHelpers('commit');
export default { export default {
directives: {
GlTooltip: GlTooltipDirective,
},
computed: { computed: {
...mapCommitState(['shouldCreateMR']), ...mapCommitGetters(['shouldHideNewMrOption', 'shouldDisableNewMrOption', 'shouldCreateMR']),
...mapCommitGetters(['shouldHideNewMrOption']), tooltipText() {
if (this.shouldDisableNewMrOption) {
return s__(
'IDE|This option is disabled because you are not allowed to create merge requests in this project.',
);
}
return '';
},
}, },
methods: { methods: {
...mapCommitActions(['toggleShouldCreateMR']), ...mapCommitActions(['toggleShouldCreateMR']),
...@@ -21,14 +32,19 @@ export default { ...@@ -21,14 +32,19 @@ export default {
<template> <template>
<fieldset v-if="!shouldHideNewMrOption"> <fieldset v-if="!shouldHideNewMrOption">
<hr class="my-2" /> <hr class="my-2" />
<label class="mb-0 js-ide-commit-new-mr"> <label
v-gl-tooltip="tooltipText"
class="mb-0 js-ide-commit-new-mr"
:class="{ 'is-disabled': shouldDisableNewMrOption }"
>
<input <input
:disabled="shouldDisableNewMrOption"
:checked="shouldCreateMR" :checked="shouldCreateMR"
type="checkbox" type="checkbox"
data-qa-selector="start_new_mr_checkbox" data-qa-selector="start_new_mr_checkbox"
@change="toggleShouldCreateMR" @change="toggleShouldCreateMR"
/> />
<span class="prepend-left-10"> <span class="prepend-left-10 ide-option-label">
{{ __('Start a new merge request') }} {{ __('Start a new merge request') }}
</span> </span>
</label> </label>
......
...@@ -67,7 +67,7 @@ export default { ...@@ -67,7 +67,7 @@ export default {
@change="updateCommitAction($event.target.value)" @change="updateCommitAction($event.target.value)"
/> />
<span class="prepend-left-10"> <span class="prepend-left-10">
<span v-if="label" class="ide-radio-label"> {{ label }} </span> <slot v-else></slot> <span v-if="label" class="ide-option-label"> {{ label }} </span> <slot v-else></slot>
</span> </span>
</label> </label>
<div v-if="commitAction === value && showInput" class="ide-commit-new-branch"> <div v-if="commitAction === value && showInput" class="ide-commit-new-branch">
......
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import { mapGetters } from 'vuex';
import NavForm from './nav_form.vue'; import NavForm from './nav_form.vue';
import NavDropdownButton from './nav_dropdown_button.vue'; import NavDropdownButton from './nav_dropdown_button.vue';
...@@ -13,6 +14,9 @@ export default { ...@@ -13,6 +14,9 @@ export default {
isVisibleDropdown: false, isVisibleDropdown: false,
}; };
}, },
computed: {
...mapGetters(['canReadMergeRequests']),
},
mounted() { mounted() {
this.addDropdownListeners(); this.addDropdownListeners();
}, },
...@@ -42,7 +46,9 @@ export default { ...@@ -42,7 +46,9 @@ export default {
<template> <template>
<div ref="dropdown" class="btn-group ide-nav-dropdown dropdown"> <div ref="dropdown" class="btn-group ide-nav-dropdown dropdown">
<nav-dropdown-button /> <nav-dropdown-button :show-merge-requests="canReadMergeRequests" />
<div class="dropdown-menu dropdown-menu-left p-0"><nav-form v-if="isVisibleDropdown" /></div> <div class="dropdown-menu dropdown-menu-left p-0">
<nav-form v-if="isVisibleDropdown" :show-merge-requests="canReadMergeRequests" />
</div>
</div> </div>
</template> </template>
...@@ -10,6 +10,13 @@ export default { ...@@ -10,6 +10,13 @@ export default {
Icon, Icon,
DropdownButton, DropdownButton,
}, },
props: {
showMergeRequests: {
type: Boolean,
required: false,
default: true,
},
},
computed: { computed: {
...mapState(['currentBranchId', 'currentMergeRequestId']), ...mapState(['currentBranchId', 'currentMergeRequestId']),
mergeRequestLabel() { mergeRequestLabel() {
...@@ -25,10 +32,10 @@ export default { ...@@ -25,10 +32,10 @@ export default {
<template> <template>
<dropdown-button> <dropdown-button>
<span class="row"> <span class="row">
<span class="col-7 text-truncate"> <span class="col-auto text-truncate" :class="{ 'col-7': showMergeRequests }">
<icon :size="16" :aria-label="__('Current Branch')" name="branch" /> {{ branchLabel }} <icon :size="16" :aria-label="__('Current Branch')" name="branch" /> {{ branchLabel }}
</span> </span>
<span class="col-5 pl-0 text-truncate"> <span v-if="showMergeRequests" class="col-5 pl-0 text-truncate">
<icon :size="16" :aria-label="__('Merge Request')" name="merge-request" /> <icon :size="16" :aria-label="__('Merge Request')" name="merge-request" />
{{ mergeRequestLabel }} {{ mergeRequestLabel }}
</span> </span>
......
...@@ -11,12 +11,19 @@ export default { ...@@ -11,12 +11,19 @@ export default {
BranchesSearchList, BranchesSearchList,
MergeRequestSearchList, MergeRequestSearchList,
}, },
props: {
showMergeRequests: {
type: Boolean,
required: false,
default: true,
},
},
}; };
</script> </script>
<template> <template>
<div class="ide-nav-form p-0"> <div class="ide-nav-form p-0">
<tabs stop-propagation> <tabs v-if="showMergeRequests" stop-propagation>
<tab active> <tab active>
<template slot="title"> <template slot="title">
{{ __('Branches') }} {{ __('Branches') }}
...@@ -30,5 +37,6 @@ export default { ...@@ -30,5 +37,6 @@ export default {
<merge-request-search-list /> <merge-request-search-list />
</tab> </tab>
</tabs> </tabs>
<branches-search-list v-else />
</div> </div>
</template> </template>
...@@ -8,6 +8,9 @@ export const MAX_BODY_LENGTH = 72; ...@@ -8,6 +8,9 @@ export const MAX_BODY_LENGTH = 72;
export const FILE_VIEW_MODE_EDITOR = 'editor'; export const FILE_VIEW_MODE_EDITOR = 'editor';
export const FILE_VIEW_MODE_PREVIEW = 'preview'; export const FILE_VIEW_MODE_PREVIEW = 'preview';
export const PERMISSION_CREATE_MR = 'createMergeRequestIn';
export const PERMISSION_READ_MR = 'readMergeRequest';
export const activityBarViews = { export const activityBarViews = {
edit: 'ide-tree', edit: 'ide-tree',
commit: 'commit-section', commit: 'commit-section',
......
query getUserPermissions($projectPath: ID!) {
project(fullPath: $projectPath) {
userPermissions {
createMergeRequestIn,
readMergeRequest
}
}
}
import createGqClient, { fetchPolicies } from '~/lib/graphql';
export default createGqClient(
{},
{
fetchPolicy: fetchPolicies.NO_CACHE,
},
);
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility'; import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
import Api from '~/api'; import Api from '~/api';
import getUserPermissions from '../queries/getUserPermissions.query.graphql';
import gqClient from './gql';
const fetchApiProjectData = projectPath => Api.project(projectPath).then(({ data }) => data);
const fetchGqlProjectData = projectPath =>
gqClient
.query({
query: getUserPermissions,
variables: { projectPath },
})
.then(({ data }) => data.project);
export default { export default {
getFileData(endpoint) { getFileData(endpoint) {
...@@ -47,7 +59,16 @@ export default { ...@@ -47,7 +59,16 @@ export default {
.then(({ data }) => data); .then(({ data }) => data);
}, },
getProjectData(namespace, project) { getProjectData(namespace, project) {
return Api.project(`${namespace}/${project}`); const projectPath = `${namespace}/${project}`;
return Promise.all([fetchApiProjectData(projectPath), fetchGqlProjectData(projectPath)]).then(
([apiProjectData, gqlProjectData]) => ({
data: {
...apiProjectData,
...gqlProjectData,
},
}),
);
}, },
getProjectMergeRequests(projectId, params = {}) { getProjectMergeRequests(projectId, params = {}) {
return Api.projectMergeRequests(projectId, params); return Api.projectMergeRequests(projectId, params);
......
...@@ -2,10 +2,17 @@ import flash from '~/flash'; ...@@ -2,10 +2,17 @@ import flash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import service from '../../services'; import service from '../../services';
import * as types from '../mutation_types'; import * as types from '../mutation_types';
import { activityBarViews } from '../../constants'; import { activityBarViews, PERMISSION_READ_MR } from '../../constants';
export const getMergeRequestsForBranch = ({ commit, state }, { projectId, branchId } = {}) => export const getMergeRequestsForBranch = (
service { commit, state, getters },
{ projectId, branchId } = {},
) => {
if (!getters.findProjectPermissions(projectId)[PERMISSION_READ_MR]) {
return Promise.resolve();
}
return service
.getProjectMergeRequests(`${projectId}`, { .getProjectMergeRequests(`${projectId}`, {
source_branch: branchId, source_branch: branchId,
source_project_id: state.projects[projectId].id, source_project_id: state.projects[projectId].id,
...@@ -36,6 +43,7 @@ export const getMergeRequestsForBranch = ({ commit, state }, { projectId, branch ...@@ -36,6 +43,7 @@ export const getMergeRequestsForBranch = ({ commit, state }, { projectId, branch
); );
throw e; throw e;
}); });
};
export const getMergeRequestData = ( export const getMergeRequestData = (
{ commit, dispatch, state }, { commit, dispatch, state },
......
import { getChangesCountForFiles, filePathMatches } from './utils'; import { getChangesCountForFiles, filePathMatches } from './utils';
import { activityBarViews, packageJsonPath } from '../constants'; import {
activityBarViews,
packageJsonPath,
PERMISSION_READ_MR,
PERMISSION_CREATE_MR,
} from '../constants';
export const activeFile = state => state.openFiles.find(file => file.active) || null; export const activeFile = state => state.openFiles.find(file => file.active) || null;
...@@ -141,5 +146,14 @@ export const getDiffInfo = (state, getters) => path => { ...@@ -141,5 +146,14 @@ export const getDiffInfo = (state, getters) => path => {
}; };
}; };
export const findProjectPermissions = (state, getters) => projectId =>
getters.findProject(projectId)?.userPermissions || {};
export const canReadMergeRequests = (state, getters) =>
Boolean(getters.findProjectPermissions(state.currentProjectId)[PERMISSION_READ_MR]);
export const canCreateMergeRequests = (state, getters) =>
Boolean(getters.findProjectPermissions(state.currentProjectId)[PERMISSION_CREATE_MR]);
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
...@@ -158,7 +158,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo ...@@ -158,7 +158,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true }); commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true });
}, 5000); }, 5000);
if (state.shouldCreateMR) { if (getters.shouldCreateMR) {
const { currentProject } = rootGetters; const { currentProject } = rootGetters;
const targetBranch = getters.isCreatingNewBranch const targetBranch = getters.isCreatingNewBranch
? rootState.currentBranchId ? rootState.currentBranchId
......
...@@ -54,5 +54,11 @@ export const shouldHideNewMrOption = (_state, getters, _rootState, rootGetters) ...@@ -54,5 +54,11 @@ export const shouldHideNewMrOption = (_state, getters, _rootState, rootGetters)
(!rootGetters.hasMergeRequest && rootGetters.isOnDefaultBranch)) && (!rootGetters.hasMergeRequest && rootGetters.isOnDefaultBranch)) &&
rootGetters.canPushToBranch; rootGetters.canPushToBranch;
export const shouldDisableNewMrOption = (state, getters, rootState, rootGetters) =>
!rootGetters.canCreateMergeRequests;
export const shouldCreateMR = (state, getters) =>
state.shouldCreateMR && !getters.shouldDisableNewMrOption;
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
...@@ -3,9 +3,7 @@ import projectSelect from '~/project_select'; ...@@ -3,9 +3,7 @@ import projectSelect from '~/project_select';
import selfMonitor from '~/self_monitor'; import selfMonitor from '~/self_monitor';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
if (gon.features && gon.features.selfMonitoringProject) { selfMonitor();
selfMonitor();
}
// Initialize expandable settings panels // Initialize expandable settings panels
initSettingsPanels(); initSettingsPanels();
projectSelect(); projectSelect();
......
...@@ -71,7 +71,12 @@ export default { ...@@ -71,7 +71,12 @@ export default {
<template> <template>
<div class="tree-content-holder"> <div class="tree-content-holder">
<div class="table-holder bordered-box"> <div class="table-holder bordered-box">
<table :aria-label="tableCaption" class="table tree-table qa-file-tree" aria-live="polite"> <table
:aria-label="tableCaption"
class="table tree-table"
aria-live="polite"
data-qa-selector="file_tree_table"
>
<table-header v-once /> <table-header v-once />
<tbody> <tbody>
<parent-row <parent-row
......
...@@ -139,7 +139,13 @@ export default { ...@@ -139,7 +139,13 @@ export default {
class="d-inline-block align-text-bottom fa-fw" class="d-inline-block align-text-bottom fa-fw"
/> />
<i v-else :aria-label="type" role="img" :class="iconName" class="fa fa-fw"></i> <i v-else :aria-label="type" role="img" :class="iconName" class="fa fa-fw"></i>
<component :is="linkComponent" :to="routerLinkTo" :href="url" class="str-truncated"> <component
:is="linkComponent"
:to="routerLinkTo"
:href="url"
class="str-truncated"
data-qa-selector="file_name_link"
>
{{ fullPath }} {{ fullPath }}
</component> </component>
<!-- eslint-disable-next-line @gitlab/vue-i18n/no-bare-strings --> <!-- eslint-disable-next-line @gitlab/vue-i18n/no-bare-strings -->
......
...@@ -688,7 +688,7 @@ $ide-commit-header-height: 48px; ...@@ -688,7 +688,7 @@ $ide-commit-header-height: 48px;
font-weight: normal; font-weight: normal;
&.is-disabled { &.is-disabled {
.ide-radio-label { .ide-option-label {
text-decoration: line-through; text-decoration: line-through;
} }
} }
......
...@@ -11,16 +11,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -11,16 +11,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
before_action :set_application_setting before_action :set_application_setting
before_action :whitelist_query_limiting, only: [:usage_data] before_action :whitelist_query_limiting, only: [:usage_data]
before_action :validate_self_monitoring_feature_flag_enabled, only: [
:create_self_monitoring_project,
:status_create_self_monitoring_project,
:delete_self_monitoring_project,
:status_delete_self_monitoring_project
]
before_action do
push_frontend_feature_flag(:self_monitoring_project)
end
VALID_SETTING_PANELS = %w(general integrations repository VALID_SETTING_PANELS = %w(general integrations repository
ci_cd reporting metrics_and_profiling ci_cd reporting metrics_and_profiling
...@@ -163,10 +153,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -163,10 +153,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
private private
def validate_self_monitoring_feature_flag_enabled
self_monitoring_project_not_implemented unless Feature.enabled?(:self_monitoring_project)
end
def self_monitoring_data def self_monitoring_data
{ {
project_id: @application_setting.self_monitoring_project_id, project_id: @application_setting.self_monitoring_project_id,
...@@ -174,16 +160,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -174,16 +160,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
} }
end end
def self_monitoring_project_not_implemented
render(
status: :not_implemented,
json: {
message: _('Self-monitoring is not enabled on this GitLab server, contact your administrator.'),
documentation_url: help_page_path('administration/monitoring/gitlab_self_monitoring_project/index')
}
)
end
def set_application_setting def set_application_setting
@application_setting = ApplicationSetting.current_without_cache @application_setting = ApplicationSetting.current_without_cache
end end
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
module Projects module Projects
class LsifDataService class LsifDataService
attr_reader :file, :project, :path, :commit_id, attr_reader :file, :project, :path, :commit_id,
:docs, :doc_ranges, :ranges, :def_refs :docs, :doc_ranges, :ranges, :def_refs, :hover_refs
CACHE_EXPIRE_IN = 1.hour CACHE_EXPIRE_IN = 1.hour
...@@ -26,7 +26,8 @@ module Projects ...@@ -26,7 +26,8 @@ module Projects
end_line: line_data.last, end_line: line_data.last,
start_char: column_data.first, start_char: column_data.first,
end_char: column_data.last, end_char: column_data.last,
definition_url: definition_url_for(def_refs[ref_id]) definition_url: definition_url_for(def_refs[ref_id]),
hover: highlighted_hover(hover_refs[ref_id])
} }
end end
end end
...@@ -54,6 +55,7 @@ module Projects ...@@ -54,6 +55,7 @@ module Projects
@doc_ranges = data['doc_ranges'] @doc_ranges = data['doc_ranges']
@ranges = data['ranges'] @ranges = data['ranges']
@def_refs = data['def_refs'] @def_refs = data['def_refs']
@hover_refs = data['hover_refs']
end end
def doc_id def doc_id
...@@ -86,5 +88,16 @@ module Projects ...@@ -86,5 +88,16 @@ module Projects
Gitlab::Routing.url_helpers.project_blob_path(project, definition_ref_path, anchor: line_anchor) Gitlab::Routing.url_helpers.project_blob_path(project, definition_ref_path, anchor: line_anchor)
end end
def highlighted_hover(hovers)
hovers&.map do |hover|
# Documentation for a method which is added as comments on top of the method
# is stored as a raw string value in LSIF file
next { value: hover } unless hover.is_a?(Hash)
value = Gitlab::Highlight.highlight(nil, hover['value'], language: hover['language'])
{ language: hover['language'], value: value }
end
end
end end
end end
...@@ -47,8 +47,7 @@ ...@@ -47,8 +47,7 @@
.settings-content .settings-content
= render 'performance_bar' = render 'performance_bar'
- if Feature.enabled?(:self_monitoring_project) .js-self-monitoring-settings{ data: self_monitoring_project_data }
.js-self-monitoring-settings{ data: self_monitoring_project_data }
%section.settings.as-usage.no-animate#js-usage-settings{ class: ('expanded' if expanded_by_default?) } %section.settings.as-usage.no-animate#js-usage-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header#usage-statistics .settings-header#usage-statistics
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
= project_icon(@project, alt: @project.name, class: 'avatar avatar-tile s64', width: 64, height: 64) = project_icon(@project, alt: @project.name, class: 'avatar avatar-tile s64', width: 64, height: 64)
.d-flex.flex-column.flex-wrap.align-items-baseline .d-flex.flex-column.flex-wrap.align-items-baseline
.d-inline-flex.align-items-baseline .d-inline-flex.align-items-baseline
%h1.home-panel-title.prepend-top-8.append-bottom-5.qa-project-name %h1.home-panel-title.prepend-top-8.append-bottom-5{ data: { qa_selector: 'project_name_content' } }
= @project.name = @project.name
%span.visibility-icon.text-secondary.prepend-left-4.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) } %span.visibility-icon.text-secondary.prepend-left-4.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) }
= visibility_level_icon(@project.visibility_level, fw: false, options: {class: 'icon'}) = visibility_level_icon(@project.visibility_level, fw: false, options: {class: 'icon'})
...@@ -70,7 +70,7 @@ ...@@ -70,7 +70,7 @@
- source = visible_fork_source(@project) - source = visible_fork_source(@project)
- if source - if source
#{ s_('ForkedFromProjectPath|Forked from') } #{ s_('ForkedFromProjectPath|Forked from') }
= link_to source.full_name, project_path(source) = link_to source.full_name, project_path(source), data: { qa_selector: 'forked_from_link' }
- else - else
= s_('ForkedFromProjectPath|Forked from an inaccessible project') = s_('ForkedFromProjectPath|Forked from an inaccessible project')
......
.tree-content-holder.js-tree-content{ data: tree_content_data(@logs_path, @project, @path) } .tree-content-holder.js-tree-content{ data: tree_content_data(@logs_path, @project, @path) }
.table-holder.bordered-box .table-holder.bordered-box
%table.table#tree-slider{ class: "table_#{@hex_path} tree-table qa-file-tree" } %table.table#tree-slider{ class: "table_#{@hex_path} tree-table" }
%thead %thead
%tr %tr
%th= s_('ProjectFileTree|Name') %th= s_('ProjectFileTree|Name')
......
...@@ -84,17 +84,16 @@ ...@@ -84,17 +84,16 @@
= render 'projects/find_file_link' = render 'projects/find_file_link'
- if can_create_mr_from_fork - if can_collaborate || current_user&.already_forked?(@project)
- if can_collaborate || current_user&.already_forked?(@project) - if vue_file_list_enabled?
- if vue_file_list_enabled? #js-tree-web-ide-link.d-inline-block
#js-tree-web-ide-link.d-inline-block
- else
= link_to ide_edit_path(@project, @ref, @path), class: 'btn btn-default qa-web-ide-button' do
= _('Web IDE')
- else - else
= link_to '#modal-confirm-fork', class: 'btn btn-default qa-web-ide-button', data: { target: '#modal-confirm-fork', toggle: 'modal'} do = link_to ide_edit_path(@project, @ref, @path), class: 'btn btn-default qa-web-ide-button' do
= _('Web IDE') = _('Web IDE')
= render 'shared/confirm_fork_modal', fork_path: ide_fork_and_edit_path(@project, @ref, @path) - elsif can_create_mr_from_fork
= link_to '#modal-confirm-fork', class: 'btn btn-default qa-web-ide-button', data: { target: '#modal-confirm-fork', toggle: 'modal'} do
= _('Web IDE')
= render 'shared/confirm_fork_modal', fork_path: ide_fork_and_edit_path(@project, @ref, @path)
- if show_xcode_link?(@project) - if show_xcode_link?(@project)
.project-action-button.project-xcode.inline< .project-action-button.project-xcode.inline<
......
---
title: Enable Web IDE on projects without Merge Requests
merge_request: 24508
author:
type: fixed
---
title: When a namespace GitLab Subscription expires, disable SSO enforcement
merge_request: 21135
author:
type: fixed
---
title: Use closest allowed visibility level on group creation when importing groups
using Group Import/Export
merge_request: 25026
author:
type: fixed
---
title: Separate entities into own class files
merge_request: 24985
author: Rajendra Kadam
type: added
---
title: Separate Application and Blob entities into own class files
merge_request: 24997
author: Rajendra Kadam
type: added
---
title: Remove self monitoring feature flag
merge_request: 23631
author:
type: other
...@@ -72,6 +72,7 @@ projects: ...@@ -72,6 +72,7 @@ projects:
effects if the package is included multiple times. effects if the package is included multiple times.
- Use `go fmt` before committing ([Gofmt](https://golang.org/cmd/gofmt/) is a - Use `go fmt` before committing ([Gofmt](https://golang.org/cmd/gofmt/) is a
tool that automatically formats Go source code). tool that automatically formats Go source code).
- Place private methods below the first caller method in the source file.
### Automatic linting ### Automatic linting
......
...@@ -199,50 +199,6 @@ module API ...@@ -199,50 +199,6 @@ module API
end.compact end.compact
end end
end end
class UserAgentDetail < Grape::Entity
expose :user_agent
expose :ip_address
expose :submitted, as: :akismet_submitted
end
class CustomAttribute < Grape::Entity
expose :key
expose :value
end
class PagesDomainCertificateExpiration < Grape::Entity
expose :expired?, as: :expired
expose :expiration
end
class Application < Grape::Entity
expose :id
expose :uid, as: :application_id
expose :name, as: :application_name
expose :redirect_uri, as: :callback_url
expose :confidential
end
# Use with care, this exposes the secret
class ApplicationWithSecret < Application
expose :secret
end
class Blob < Grape::Entity
expose :basename
expose :data
expose :path
# TODO: :filename was renamed to :path but both still return the full path,
# in the future we can only return the filename here without the leading
# directory path.
# https://gitlab.com/gitlab-org/gitlab/issues/34521
expose :filename, &:path
expose :id
expose :ref
expose :startline
expose :project_id
end
end end
end end
......
# frozen_string_literal: true
module API
module Entities
class Application < Grape::Entity
expose :id
expose :uid, as: :application_id
expose :name, as: :application_name
expose :redirect_uri, as: :callback_url
expose :confidential
end
end
end
# frozen_string_literal: true
module API
module Entities
# Use with care, this exposes the secret
class ApplicationWithSecret < Entities::Application
expose :secret
end
end
end
# frozen_string_literal: true
module API
module Entities
class Blob < Grape::Entity
expose :basename
expose :data
expose :path
# TODO: :filename was renamed to :path but both still return the full path,
# in the future we can only return the filename here without the leading
# directory path.
# https://gitlab.com/gitlab-org/gitlab/issues/34521
expose :filename, &:path
expose :id
expose :ref
expose :startline
expose :project_id
end
end
end
# frozen_string_literal: true
module API
module Entities
class CustomAttribute < Grape::Entity
expose :key
expose :value
end
end
end
# frozen_string_literal: true
module API
module Entities
class PagesDomainCertificateExpiration < Grape::Entity
expose :expired?, as: :expired
expose :expiration
end
end
end
# frozen_string_literal: true
module API
module Entities
class UserAgentDetail < Grape::Entity
expose :user_agent
expose :ip_address
expose :submitted, as: :akismet_submitted
end
end
end
...@@ -5,15 +5,25 @@ module API ...@@ -5,15 +5,25 @@ module API
MAXIMUM_FILE_SIZE = 50.megabytes.freeze MAXIMUM_FILE_SIZE = 50.megabytes.freeze
helpers do helpers do
def authorize_create_group! def parent_group
parent_group = find_group!(params[:parent_id]) if params[:parent_id].present? find_group!(params[:parent_id]) if params[:parent_id].present?
end
def authorize_create_group!
if parent_group if parent_group
authorize! :create_subgroup, parent_group authorize! :create_subgroup, parent_group
else else
authorize! :create_group authorize! :create_group
end end
end end
def closest_allowed_visibility_level
if parent_group
Gitlab::VisibilityLevel.closest_allowed_level(parent_group.visibility_level)
else
Gitlab::VisibilityLevel::PRIVATE
end
end
end end
resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
...@@ -59,6 +69,7 @@ module API ...@@ -59,6 +69,7 @@ module API
path: params[:path], path: params[:path],
name: params[:name], name: params[:name],
parent_id: params[:parent_id], parent_id: params[:parent_id],
visibility_level: closest_allowed_visibility_level,
import_export_upload: ImportExportUpload.new(import_file: uploaded_file) import_export_upload: ImportExportUpload.new(import_file: uploaded_file)
} }
......
...@@ -37,6 +37,7 @@ excluded_attributes: ...@@ -37,6 +37,7 @@ excluded_attributes:
- :runners_token - :runners_token
- :runners_token_encrypted - :runners_token_encrypted
- :saml_discovery_token - :saml_discovery_token
- :visibility_level
methods: methods:
labels: labels:
......
...@@ -74,12 +74,23 @@ module Gitlab ...@@ -74,12 +74,23 @@ module Gitlab
group_params = { group_params = {
name: group_hash['name'], name: group_hash['name'],
path: group_hash['path'], path: group_hash['path'],
parent_id: parent_group&.id parent_id: parent_group&.id,
visibility_level: sub_group_visibility_level(group_hash, parent_group)
} }
::Groups::CreateService.new(@user, group_params).execute ::Groups::CreateService.new(@user, group_params).execute
end end
def sub_group_visibility_level(group_hash, parent_group)
original_visibility_level = group_hash['visibility_level'] || Gitlab::VisibilityLevel::PRIVATE
if parent_group && parent_group.visibility_level < original_visibility_level
Gitlab::VisibilityLevel.closest_allowed_level(parent_group.visibility_level)
else
original_visibility_level
end
end
def members_mapper def members_mapper
@members_mapper ||= Gitlab::ImportExport::MembersMapper.new(exported_members: @group_members, user: @user, importable: @group) @members_mapper ||= Gitlab::ImportExport::MembersMapper.new(exported_members: @group_members, user: @user, importable: @group)
end end
......
...@@ -10130,6 +10130,9 @@ msgstr "" ...@@ -10130,6 +10130,9 @@ msgstr ""
msgid "IDE|Successful commit" msgid "IDE|Successful commit"
msgstr "" msgstr ""
msgid "IDE|This option is disabled because you are not allowed to create merge requests in this project."
msgstr ""
msgid "IDE|This option is disabled because you don't have write permissions for the current branch." msgid "IDE|This option is disabled because you don't have write permissions for the current branch."
msgstr "" msgstr ""
...@@ -17112,9 +17115,6 @@ msgstr "" ...@@ -17112,9 +17115,6 @@ msgstr ""
msgid "Self monitoring project does not exist" msgid "Self monitoring project does not exist"
msgstr "" msgstr ""
msgid "Self-monitoring is not enabled on this GitLab server, contact your administrator."
msgstr ""
msgid "Self-monitoring project does not exist. Please check logs for any error messages" msgid "Self-monitoring project does not exist. Please check logs for any error messages"
msgstr "" msgstr ""
......
...@@ -7,6 +7,14 @@ module QA ...@@ -7,6 +7,14 @@ module QA
include Page::Component::ClonePanel include Page::Component::ClonePanel
include Page::Project::SubMenus::Settings include Page::Project::SubMenus::Settings
view 'app/assets/javascripts/repository/components/table/row.vue' do
element :file_name_link
end
view 'app/assets/javascripts/repository/components/table/index.vue' do
element :file_tree_table
end
view 'app/views/layouts/header/_new_dropdown.haml' do view 'app/views/layouts/header/_new_dropdown.haml' do
element :new_menu_toggle element :new_menu_toggle
element :new_issue_link, "link_to _('New issue'), new_project_issue_path(@project)" # rubocop:disable QA/ElementWithPattern element :new_issue_link, "link_to _('New issue'), new_project_issue_path(@project)" # rubocop:disable QA/ElementWithPattern
...@@ -17,7 +25,8 @@ module QA ...@@ -17,7 +25,8 @@ module QA
end end
view 'app/views/projects/_home_panel.html.haml' do view 'app/views/projects/_home_panel.html.haml' do
element :project_name element :forked_from_link
element :project_name_content
end end
view 'app/views/projects/_files.html.haml' do view 'app/views/projects/_files.html.haml' do
...@@ -37,10 +46,6 @@ module QA ...@@ -37,10 +46,6 @@ module QA
element :quick_actions element :quick_actions
end end
view 'app/views/projects/tree/_tree_content.html.haml' do
element :file_tree
end
view 'app/views/projects/tree/_tree_header.html.haml' do view 'app/views/projects/tree/_tree_header.html.haml' do
element :add_to_tree element :add_to_tree
element :new_file_option element :new_file_option
...@@ -79,14 +84,18 @@ module QA ...@@ -79,14 +84,18 @@ module QA
click_on 'Fork' click_on 'Fork'
end end
def forked_from?(parent_project_name)
has_element?(:forked_from_link, text: parent_project_name)
end
def click_file(filename) def click_file(filename)
within_element(:file_tree) do within_element(:file_tree_table) do
click_on filename click_on filename
end end
end end
def click_commit(commit_msg) def click_commit(commit_msg)
within_element(:file_tree) do within_element(:file_tree_table) do
click_on commit_msg click_on commit_msg
end end
end end
...@@ -96,6 +105,16 @@ module QA ...@@ -96,6 +105,16 @@ module QA
click_link 'New issue' click_link 'New issue'
end end
def has_file?(name)
within_element(:file_tree_table) do
has_element?(:file_name_link, text: name)
end
end
def has_name?(name)
has_element?(:project_name_content, text: name)
end
def last_commit_content def last_commit_content
find_element(:commit_content).text find_element(:commit_content).text
end end
...@@ -113,7 +132,7 @@ module QA ...@@ -113,7 +132,7 @@ module QA
end end
def project_name def project_name
find('.qa-project-name').text find_element(:project_name_content).text
end end
def switch_to_branch(branch_name) def switch_to_branch(branch_name)
......
...@@ -8,10 +8,12 @@ module QA ...@@ -8,10 +8,12 @@ module QA
module ApiFabricator module ApiFabricator
include Capybara::DSL include Capybara::DSL
ResourceNotFoundError = Class.new(RuntimeError)
ResourceFabricationFailedError = Class.new(RuntimeError) ResourceFabricationFailedError = Class.new(RuntimeError)
ResourceURLMissingError = Class.new(RuntimeError)
ResourceNotDeletedError = Class.new(RuntimeError) ResourceNotDeletedError = Class.new(RuntimeError)
ResourceNotFoundError = Class.new(RuntimeError)
ResourceQueryError = Class.new(RuntimeError)
ResourceUpdateFailedError = Class.new(RuntimeError)
ResourceURLMissingError = Class.new(RuntimeError)
attr_reader :api_resource, :api_response attr_reader :api_resource, :api_response
attr_writer :api_client attr_writer :api_client
......
...@@ -3,19 +3,24 @@ ...@@ -3,19 +3,24 @@
module QA module QA
module Resource module Resource
class Fork < Base class Fork < Base
attribute :name do
upstream.name
end
attribute :project do attribute :project do
Resource::Project.fabricate! do |resource| Resource::Project.fabricate_via_api! do |resource|
resource.name = upstream.project.name resource.add_name_uuid = false
resource.path_with_namespace = "#{user.name}/#{upstream.project.name}" resource.name = name
resource.path_with_namespace = "#{user.username}/#{name}"
end end
end end
attribute :upstream do attribute :upstream do
Repository::ProjectPush.fabricate! Repository::ProjectPush.fabricate!.project
end end
attribute :user do attribute :user do
User.fabricate! do |resource| User.fabricate_via_api! do |resource|
if Runtime::Env.forker? if Runtime::Env.forker?
resource.username = Runtime::Env.forker_username resource.username = Runtime::Env.forker_username
resource.password = Runtime::Env.forker_password resource.password = Runtime::Env.forker_password
...@@ -33,7 +38,7 @@ module QA ...@@ -33,7 +38,7 @@ module QA
login.sign_in_using_credentials(user: user) login.sign_in_using_credentials(user: user)
end end
upstream.project.visit! upstream.visit!
Page::Project::Show.perform(&:fork_project) Page::Project::Show.perform(&:fork_project)
...@@ -47,6 +52,41 @@ module QA ...@@ -47,6 +52,41 @@ module QA
populate(:project) populate(:project)
end end
def fabricate_via_api!
populate(:upstream, :user)
Runtime::Logger.debug("Forking project #{upstream.name} to namespace #{user.username}...")
super
wait_until_forked
populate(:project)
end
def api_get_path
"/projects/#{CGI.escape(path_with_namespace)}"
end
def api_post_path
"/projects/#{upstream.id}/fork"
end
def api_post_body
{
namespace: user.username,
name: name,
path: name
}
end
def wait_until_forked
Runtime::Logger.debug("Waiting for the fork process to complete...")
forked = wait_until do
project.import_status == "finished"
end
raise "Timed out while waiting for the fork process to complete." unless forked
end
end end
end end
end end
...@@ -8,7 +8,7 @@ module QA ...@@ -8,7 +8,7 @@ module QA
attr_accessor :fork_branch attr_accessor :fork_branch
attribute :fork do attribute :fork do
Fork.fabricate! Fork.fabricate_via_browser_ui!
end end
attribute :push do attribute :push do
......
...@@ -94,6 +94,10 @@ module QA ...@@ -94,6 +94,10 @@ module QA
"#{api_get_path}/runners" "#{api_get_path}/runners"
end end
def api_put_path
"/projects/#{id}"
end
def api_post_path def api_post_path
'/projects' '/projects'
end end
...@@ -115,6 +119,35 @@ module QA ...@@ -115,6 +119,35 @@ module QA
post_body post_body
end end
def change_repository_storage(new_storage)
put_body = { repository_storage: new_storage }
response = put Runtime::API::Request.new(api_client, api_put_path).url, put_body
unless response.code == HTTP_STATUS_OK
raise ResourceUpdateFailedError, "Could not change repository storage to #{new_storage}. Request returned (#{response.code}): `#{response}`."
end
wait_until do
reload!
api_response[:repository_storage] == new_storage
end
end
def import_status
response = get Runtime::API::Request.new(api_client, "/projects/#{id}/import").url
unless response.code == HTTP_STATUS_OK
raise ResourceQueryError, "Could not get import status. Request returned (#{response.code}): `#{response}`."
end
result = parse_body(response)
Runtime::Logger.error("Import failed: #{result[:import_error]}") if result[:import_status] == "failed"
result[:import_status]
end
def runners(tag_list: nil) def runners(tag_list: nil)
response = get Runtime::API::Request.new(api_client, "#{api_runners_path}?tag_list=#{tag_list.compact.join(',')}").url response = get Runtime::API::Request.new(api_client, "#{api_runners_path}?tag_list=#{tag_list.compact.join(',')}").url
parse_body(response) parse_body(response)
......
...@@ -22,6 +22,10 @@ module QA ...@@ -22,6 +22,10 @@ module QA
SUPPORTED_FEATURES SUPPORTED_FEATURES
end end
def additional_repository_storage
ENV['QA_ADDITIONAL_REPOSITORY_STORAGE']
end
def admin_password def admin_password
ENV['GITLAB_ADMIN_PASSWORD'] ENV['GITLAB_ADMIN_PASSWORD']
end end
......
...@@ -6,7 +6,7 @@ module QA ...@@ -6,7 +6,7 @@ module QA
it 'user forks a project, submits a merge request and maintainer merges it' do it 'user forks a project, submits a merge request and maintainer merges it' do
Flow::Login.sign_in Flow::Login.sign_in
merge_request = Resource::MergeRequestFromFork.fabricate! do |merge_request| merge_request = Resource::MergeRequestFromFork.fabricate_via_browser_ui! do |merge_request|
merge_request.fork_branch = 'feature-branch' merge_request.fork_branch = 'feature-branch'
end end
......
# frozen_string_literal: true
module QA
context 'Create' do
describe 'Gitaly repository storage', :orchestrated, :repository_storage, :requires_admin, quarantine: { type: :new } do
let(:user) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) }
let(:parent_project) do
Resource::Project.fabricate_via_api! do |project|
project.name = 'parent-project'
project.initialize_with_readme = true
end
end
let(:fork_project) do
Resource::Fork.fabricate_via_api! do |fork|
fork.user = user
fork.upstream = parent_project
end.project
end
before do
parent_project.add_member(user)
end
it 'creates a 2nd fork after moving the parent project' do
Flow::Login.sign_in(as: user)
fork_project.visit!
parent_project.change_repository_storage(QA::Runtime::Env.additional_repository_storage)
second_fork_project = Resource::Fork.fabricate_via_api! do |fork|
fork.name = "second-fork"
fork.user = user
fork.upstream = parent_project
end.project
Resource::Repository::ProjectPush.fabricate! do |push|
push.project = second_fork_project
push.file_name = 'new_file'
push.file_content = '# This is a new file'
push.commit_message = 'Add new file'
push.new_branch = false
end.project.visit!
Page::Project::Show.perform do |show|
expect(show).to have_file('new_file')
expect(show).to have_name(second_fork_project.name)
expect(show).to be_forked_from(parent_project.name)
end
end
end
end
end
...@@ -3,66 +3,96 @@ ...@@ -3,66 +3,96 @@
require 'spec_helper' require 'spec_helper'
describe 'Projects > Show > Collaboration links', :js do describe 'Projects > Show > Collaboration links', :js do
let(:project) { create(:project, :repository) } using RSpec::Parameterized::TableSyntax
let(:project) { create(:project, :repository, :public) }
let(:user) { create(:user) } let(:user) { create(:user) }
before do before do
project.add_developer(user)
sign_in(user) sign_in(user)
end end
it 'shows all the expected links' do context 'with developer user' do
visit project_path(project) before do
project.add_developer(user)
end
# The navigation bar it 'shows all the expected links' do
page.within('.header-new') do visit project_path(project)
find('.qa-new-menu-toggle').click
aggregate_failures 'dropdown links in the navigation bar' do # The navigation bar
expect(page).to have_link('New issue') page.within('.header-new') do
expect(page).to have_link('New merge request') find('.qa-new-menu-toggle').click
expect(page).to have_link('New snippet', href: new_project_snippet_path(project))
end
find('.qa-new-menu-toggle').click aggregate_failures 'dropdown links in the navigation bar' do
end expect(page).to have_link('New issue')
expect(page).to have_link('New merge request')
expect(page).to have_link('New snippet', href: new_project_snippet_path(project))
end
# The dropdown above the tree find('.qa-new-menu-toggle').click
page.within('.repo-breadcrumb') do end
find('.qa-add-to-tree').click
aggregate_failures 'dropdown links above the repo tree' do # The dropdown above the tree
expect(page).to have_link('New file') page.within('.repo-breadcrumb') do
expect(page).to have_link('Upload file') find('.qa-add-to-tree').click
expect(page).to have_link('New directory')
expect(page).to have_link('New branch') aggregate_failures 'dropdown links above the repo tree' do
expect(page).to have_link('New tag') expect(page).to have_link('New file')
expect(page).to have_link('Upload file')
expect(page).to have_link('New directory')
expect(page).to have_link('New branch')
expect(page).to have_link('New tag')
end
end end
# The Web IDE
expect(page).to have_link('Web IDE')
end end
# The Web IDE it 'hides the links when the project is archived' do
expect(page).to have_link('Web IDE') project.update!(archived: true)
end
it 'hides the links when the project is archived' do visit project_path(project)
project.update!(archived: true)
visit project_path(project) page.within('.header-new') do
find('.qa-new-menu-toggle').click
page.within('.header-new') do aggregate_failures 'dropdown links' do
find('.qa-new-menu-toggle').click expect(page).not_to have_link('New issue')
expect(page).not_to have_link('New merge request')
expect(page).not_to have_link('New snippet', href: new_project_snippet_path(project))
end
aggregate_failures 'dropdown links' do find('.qa-new-menu-toggle').click
expect(page).not_to have_link('New issue')
expect(page).not_to have_link('New merge request')
expect(page).not_to have_link('New snippet', href: new_project_snippet_path(project))
end end
find('.qa-new-menu-toggle').click expect(page).not_to have_selector('.qa-add-to-tree')
expect(page).not_to have_link('Web IDE')
end end
end
expect(page).not_to have_selector('.qa-add-to-tree') context "Web IDE link" do
where(:merge_requests_access_level, :user_level, :expect_ide_link) do
::ProjectFeature::DISABLED | :guest | false
::ProjectFeature::DISABLED | :developer | true
::ProjectFeature::PRIVATE | :guest | false
::ProjectFeature::PRIVATE | :developer | true
::ProjectFeature::ENABLED | :guest | true
::ProjectFeature::ENABLED | :developer | true
end
expect(page).not_to have_link('Web IDE') with_them do
before do
project.project_feature.update!({ merge_requests_access_level: merge_requests_access_level })
project.add_user(user, user_level)
visit project_path(project)
end
it "updates Web IDE link" do
expect(page.has_link?('Web IDE')).to be(expect_ide_link)
end
end
end end
end end
{
"id": 283,
"name": "internal",
"path": "internal",
"owner_id": null,
"created_at": "2020-02-12T16:56:34.924Z",
"updated_at": "2020-02-12T16:56:38.710Z",
"description": "",
"avatar": {
"url": null
},
"membership_lock": false,
"share_with_group_lock": false,
"visibility_level": 10,
"request_access_enabled": true,
"ldap_sync_status": "ready",
"ldap_sync_error": null,
"ldap_sync_last_update_at": null,
"ldap_sync_last_successful_update_at": null,
"ldap_sync_last_sync_at": null,
"lfs_enabled": null,
"parent_id": null,
"shared_runners_minutes_limit": null,
"repository_size_limit": null,
"require_two_factor_authentication": false,
"two_factor_grace_period": 48,
"plan_id": null,
"project_creation_level": 2,
"trial_ends_on": null,
"file_template_project_id": null,
"custom_project_templates_group_id": null,
"auto_devops_enabled": null,
"extra_shared_runners_minutes_limit": null,
"last_ci_minutes_notification_at": null,
"last_ci_minutes_usage_notification_level": null,
"subgroup_creation_level": 1,
"emails_disabled": null,
"max_pages_size": null,
"max_artifacts_size": null,
"mentions_disabled": null,
"children": [
{
"id": 284,
"name": "public",
"path": "public",
"owner_id": null,
"created_at": "2020-02-12T17:33:00.575Z",
"updated_at": "2020-02-12T17:33:00.575Z",
"description": "",
"avatar": {
"url": null
},
"membership_lock": false,
"share_with_group_lock": false,
"visibility_level": 20,
"request_access_enabled": true,
"ldap_sync_status": "ready",
"ldap_sync_error": null,
"ldap_sync_last_update_at": null,
"ldap_sync_last_successful_update_at": null,
"ldap_sync_last_sync_at": null,
"lfs_enabled": null,
"parent_id": 283,
"shared_runners_minutes_limit": null,
"repository_size_limit": null,
"require_two_factor_authentication": false,
"two_factor_grace_period": 48,
"plan_id": null,
"project_creation_level": 2,
"trial_ends_on": null,
"file_template_project_id": null,
"custom_project_templates_group_id": null,
"auto_devops_enabled": null,
"extra_shared_runners_minutes_limit": null,
"last_ci_minutes_notification_at": null,
"last_ci_minutes_usage_notification_level": null,
"subgroup_creation_level": 1,
"emails_disabled": null,
"max_pages_size": null,
"max_artifacts_size": null,
"mentions_disabled": null
},
{
"id": 285,
"name": "internal",
"path": "internal",
"owner_id": null,
"created_at": "2020-02-12T17:33:00.575Z",
"updated_at": "2020-02-12T17:33:00.575Z",
"description": "",
"avatar": {
"url": null
},
"membership_lock": false,
"share_with_group_lock": false,
"visibility_level": 10,
"request_access_enabled": true,
"ldap_sync_status": "ready",
"ldap_sync_error": null,
"ldap_sync_last_update_at": null,
"ldap_sync_last_successful_update_at": null,
"ldap_sync_last_sync_at": null,
"lfs_enabled": null,
"parent_id": 283,
"shared_runners_minutes_limit": null,
"repository_size_limit": null,
"require_two_factor_authentication": false,
"two_factor_grace_period": 48,
"plan_id": null,
"project_creation_level": 2,
"trial_ends_on": null,
"file_template_project_id": null,
"custom_project_templates_group_id": null,
"auto_devops_enabled": null,
"extra_shared_runners_minutes_limit": null,
"last_ci_minutes_notification_at": null,
"last_ci_minutes_usage_notification_level": null,
"subgroup_creation_level": 1,
"emails_disabled": null,
"max_pages_size": null,
"max_artifacts_size": null,
"mentions_disabled": null
},
{
"id": 286,
"name": "private",
"path": "private",
"owner_id": null,
"created_at": "2020-02-12T17:33:00.575Z",
"updated_at": "2020-02-12T17:33:00.575Z",
"description": "",
"avatar": {
"url": null
},
"membership_lock": false,
"share_with_group_lock": false,
"visibility_level": 0,
"request_access_enabled": true,
"ldap_sync_status": "ready",
"ldap_sync_error": null,
"ldap_sync_last_update_at": null,
"ldap_sync_last_successful_update_at": null,
"ldap_sync_last_sync_at": null,
"lfs_enabled": null,
"parent_id": 283,
"shared_runners_minutes_limit": null,
"repository_size_limit": null,
"require_two_factor_authentication": false,
"two_factor_grace_period": 48,
"plan_id": null,
"project_creation_level": 2,
"trial_ends_on": null,
"file_template_project_id": null,
"custom_project_templates_group_id": null,
"auto_devops_enabled": null,
"extra_shared_runners_minutes_limit": null,
"last_ci_minutes_notification_at": null,
"last_ci_minutes_usage_notification_level": null,
"subgroup_creation_level": 1,
"emails_disabled": null,
"max_pages_size": null,
"max_artifacts_size": null,
"mentions_disabled": null
}
]
}
{
"id": 283,
"name": "private",
"path": "private",
"owner_id": null,
"created_at": "2020-02-12T16:56:34.924Z",
"updated_at": "2020-02-12T16:56:38.710Z",
"description": "",
"avatar": {
"url": null
},
"membership_lock": false,
"share_with_group_lock": false,
"visibility_level": 0,
"request_access_enabled": true,
"ldap_sync_status": "ready",
"ldap_sync_error": null,
"ldap_sync_last_update_at": null,
"ldap_sync_last_successful_update_at": null,
"ldap_sync_last_sync_at": null,
"lfs_enabled": null,
"parent_id": null,
"shared_runners_minutes_limit": null,
"repository_size_limit": null,
"require_two_factor_authentication": false,
"two_factor_grace_period": 48,
"plan_id": null,
"project_creation_level": 2,
"trial_ends_on": null,
"file_template_project_id": null,
"custom_project_templates_group_id": null,
"auto_devops_enabled": null,
"extra_shared_runners_minutes_limit": null,
"last_ci_minutes_notification_at": null,
"last_ci_minutes_usage_notification_level": null,
"subgroup_creation_level": 1,
"emails_disabled": null,
"max_pages_size": null,
"max_artifacts_size": null,
"mentions_disabled": null,
"children": [
{
"id": 284,
"name": "public",
"path": "public",
"owner_id": null,
"created_at": "2020-02-12T17:33:00.575Z",
"updated_at": "2020-02-12T17:33:00.575Z",
"description": "",
"avatar": {
"url": null
},
"membership_lock": false,
"share_with_group_lock": false,
"visibility_level": 20,
"request_access_enabled": true,
"ldap_sync_status": "ready",
"ldap_sync_error": null,
"ldap_sync_last_update_at": null,
"ldap_sync_last_successful_update_at": null,
"ldap_sync_last_sync_at": null,
"lfs_enabled": null,
"parent_id": 283,
"shared_runners_minutes_limit": null,
"repository_size_limit": null,
"require_two_factor_authentication": false,
"two_factor_grace_period": 48,
"plan_id": null,
"project_creation_level": 2,
"trial_ends_on": null,
"file_template_project_id": null,
"custom_project_templates_group_id": null,
"auto_devops_enabled": null,
"extra_shared_runners_minutes_limit": null,
"last_ci_minutes_notification_at": null,
"last_ci_minutes_usage_notification_level": null,
"subgroup_creation_level": 1,
"emails_disabled": null,
"max_pages_size": null,
"max_artifacts_size": null,
"mentions_disabled": null
},
{
"id": 285,
"name": "internal",
"path": "internal",
"owner_id": null,
"created_at": "2020-02-12T17:33:00.575Z",
"updated_at": "2020-02-12T17:33:00.575Z",
"description": "",
"avatar": {
"url": null
},
"membership_lock": false,
"share_with_group_lock": false,
"visibility_level": 10,
"request_access_enabled": true,
"ldap_sync_status": "ready",
"ldap_sync_error": null,
"ldap_sync_last_update_at": null,
"ldap_sync_last_successful_update_at": null,
"ldap_sync_last_sync_at": null,
"lfs_enabled": null,
"parent_id": 283,
"shared_runners_minutes_limit": null,
"repository_size_limit": null,
"require_two_factor_authentication": false,
"two_factor_grace_period": 48,
"plan_id": null,
"project_creation_level": 2,
"trial_ends_on": null,
"file_template_project_id": null,
"custom_project_templates_group_id": null,
"auto_devops_enabled": null,
"extra_shared_runners_minutes_limit": null,
"last_ci_minutes_notification_at": null,
"last_ci_minutes_usage_notification_level": null,
"subgroup_creation_level": 1,
"emails_disabled": null,
"max_pages_size": null,
"max_artifacts_size": null,
"mentions_disabled": null
},
{
"id": 286,
"name": "private",
"path": "private",
"owner_id": null,
"created_at": "2020-02-12T17:33:00.575Z",
"updated_at": "2020-02-12T17:33:00.575Z",
"description": "",
"avatar": {
"url": null
},
"membership_lock": false,
"share_with_group_lock": false,
"visibility_level": 0,
"request_access_enabled": true,
"ldap_sync_status": "ready",
"ldap_sync_error": null,
"ldap_sync_last_update_at": null,
"ldap_sync_last_successful_update_at": null,
"ldap_sync_last_sync_at": null,
"lfs_enabled": null,
"parent_id": 283,
"shared_runners_minutes_limit": null,
"repository_size_limit": null,
"require_two_factor_authentication": false,
"two_factor_grace_period": 48,
"plan_id": null,
"project_creation_level": 2,
"trial_ends_on": null,
"file_template_project_id": null,
"custom_project_templates_group_id": null,
"auto_devops_enabled": null,
"extra_shared_runners_minutes_limit": null,
"last_ci_minutes_notification_at": null,
"last_ci_minutes_usage_notification_level": null,
"subgroup_creation_level": 1,
"emails_disabled": null,
"max_pages_size": null,
"max_artifacts_size": null,
"mentions_disabled": null
}
]
}
{
"id": 283,
"name": "public",
"path": "public",
"owner_id": null,
"created_at": "2020-02-12T16:56:34.924Z",
"updated_at": "2020-02-12T16:56:38.710Z",
"description": "",
"avatar": {
"url": null
},
"membership_lock": false,
"share_with_group_lock": false,
"visibility_level": 20,
"request_access_enabled": true,
"ldap_sync_status": "ready",
"ldap_sync_error": null,
"ldap_sync_last_update_at": null,
"ldap_sync_last_successful_update_at": null,
"ldap_sync_last_sync_at": null,
"lfs_enabled": null,
"parent_id": null,
"shared_runners_minutes_limit": null,
"repository_size_limit": null,
"require_two_factor_authentication": false,
"two_factor_grace_period": 48,
"plan_id": null,
"project_creation_level": 2,
"trial_ends_on": null,
"file_template_project_id": null,
"custom_project_templates_group_id": null,
"auto_devops_enabled": null,
"extra_shared_runners_minutes_limit": null,
"last_ci_minutes_notification_at": null,
"last_ci_minutes_usage_notification_level": null,
"subgroup_creation_level": 1,
"emails_disabled": null,
"max_pages_size": null,
"max_artifacts_size": null,
"mentions_disabled": null,
"children": [
{
"id": 284,
"name": "public",
"path": "public",
"owner_id": null,
"created_at": "2020-02-12T17:33:00.575Z",
"updated_at": "2020-02-12T17:33:00.575Z",
"description": "",
"avatar": {
"url": null
},
"membership_lock": false,
"share_with_group_lock": false,
"visibility_level": 20,
"request_access_enabled": true,
"ldap_sync_status": "ready",
"ldap_sync_error": null,
"ldap_sync_last_update_at": null,
"ldap_sync_last_successful_update_at": null,
"ldap_sync_last_sync_at": null,
"lfs_enabled": null,
"parent_id": 283,
"shared_runners_minutes_limit": null,
"repository_size_limit": null,
"require_two_factor_authentication": false,
"two_factor_grace_period": 48,
"plan_id": null,
"project_creation_level": 2,
"trial_ends_on": null,
"file_template_project_id": null,
"custom_project_templates_group_id": null,
"auto_devops_enabled": null,
"extra_shared_runners_minutes_limit": null,
"last_ci_minutes_notification_at": null,
"last_ci_minutes_usage_notification_level": null,
"subgroup_creation_level": 1,
"emails_disabled": null,
"max_pages_size": null,
"max_artifacts_size": null,
"mentions_disabled": null
},
{
"id": 285,
"name": "internal",
"path": "internal",
"owner_id": null,
"created_at": "2020-02-12T17:33:00.575Z",
"updated_at": "2020-02-12T17:33:00.575Z",
"description": "",
"avatar": {
"url": null
},
"membership_lock": false,
"share_with_group_lock": false,
"visibility_level": 10,
"request_access_enabled": true,
"ldap_sync_status": "ready",
"ldap_sync_error": null,
"ldap_sync_last_update_at": null,
"ldap_sync_last_successful_update_at": null,
"ldap_sync_last_sync_at": null,
"lfs_enabled": null,
"parent_id": 283,
"shared_runners_minutes_limit": null,
"repository_size_limit": null,
"require_two_factor_authentication": false,
"two_factor_grace_period": 48,
"plan_id": null,
"project_creation_level": 2,
"trial_ends_on": null,
"file_template_project_id": null,
"custom_project_templates_group_id": null,
"auto_devops_enabled": null,
"extra_shared_runners_minutes_limit": null,
"last_ci_minutes_notification_at": null,
"last_ci_minutes_usage_notification_level": null,
"subgroup_creation_level": 1,
"emails_disabled": null,
"max_pages_size": null,
"max_artifacts_size": null,
"mentions_disabled": null
},
{
"id": 286,
"name": "private",
"path": "private",
"owner_id": null,
"created_at": "2020-02-12T17:33:00.575Z",
"updated_at": "2020-02-12T17:33:00.575Z",
"description": "",
"avatar": {
"url": null
},
"membership_lock": false,
"share_with_group_lock": false,
"visibility_level": 0,
"request_access_enabled": true,
"ldap_sync_status": "ready",
"ldap_sync_error": null,
"ldap_sync_last_update_at": null,
"ldap_sync_last_successful_update_at": null,
"ldap_sync_last_sync_at": null,
"lfs_enabled": null,
"parent_id": 283,
"shared_runners_minutes_limit": null,
"repository_size_limit": null,
"require_two_factor_authentication": false,
"two_factor_grace_period": 48,
"plan_id": null,
"project_creation_level": 2,
"trial_ends_on": null,
"file_template_project_id": null,
"custom_project_templates_group_id": null,
"auto_devops_enabled": null,
"extra_shared_runners_minutes_limit": null,
"last_ci_minutes_notification_at": null,
"last_ci_minutes_usage_notification_level": null,
"subgroup_creation_level": 1,
"emails_disabled": null,
"max_pages_size": null,
"max_artifacts_size": null,
"mentions_disabled": null
}
]
}
...@@ -18,6 +18,7 @@ export const projectData = { ...@@ -18,6 +18,7 @@ export const projectData = {
}, },
mergeRequests: {}, mergeRequests: {},
merge_requests_enabled: true, merge_requests_enabled: true,
userPermissions: {},
default_branch: 'master', default_branch: 'master',
}; };
......
...@@ -2,11 +2,17 @@ import axios from 'axios'; ...@@ -2,11 +2,17 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import services from '~/ide/services'; import services from '~/ide/services';
import Api from '~/api'; import Api from '~/api';
import gqClient from '~/ide/services/gql';
import { escapeFileUrl } from '~/lib/utils/url_utility'; import { escapeFileUrl } from '~/lib/utils/url_utility';
import getUserPermissions from '~/ide/queries/getUserPermissions.query.graphql';
import { projectData } from '../mock_data';
jest.mock('~/api'); jest.mock('~/api');
jest.mock('~/ide/services/gql');
const TEST_PROJECT_ID = 'alice/wonderland'; const TEST_NAMESPACE = 'alice';
const TEST_PROJECT = 'wonderland';
const TEST_PROJECT_ID = `${TEST_NAMESPACE}/${TEST_PROJECT}`;
const TEST_BRANCH = 'master-patch-123'; const TEST_BRANCH = 'master-patch-123';
const TEST_COMMIT_SHA = '123456789'; const TEST_COMMIT_SHA = '123456789';
const TEST_FILE_PATH = 'README2.md'; const TEST_FILE_PATH = 'README2.md';
...@@ -111,4 +117,27 @@ describe('IDE services', () => { ...@@ -111,4 +117,27 @@ describe('IDE services', () => {
}, },
); );
}); });
describe('getProjectData', () => {
it('combines gql and API requests', () => {
const gqlProjectData = {
userPermissions: {
bogus: true,
},
};
Api.project.mockReturnValue(Promise.resolve({ data: { ...projectData } }));
gqClient.query.mockReturnValue(Promise.resolve({ data: { project: gqlProjectData } }));
return services.getProjectData(TEST_NAMESPACE, TEST_PROJECT).then(response => {
expect(response).toEqual({ data: { ...projectData, ...gqlProjectData } });
expect(Api.project).toHaveBeenCalledWith(TEST_PROJECT_ID);
expect(gqClient.query).toHaveBeenCalledWith({
query: getUserPermissions,
variables: {
projectPath: TEST_PROJECT_ID,
},
});
});
});
});
}); });
...@@ -2,6 +2,8 @@ import * as getters from '~/ide/stores/getters'; ...@@ -2,6 +2,8 @@ import * as getters from '~/ide/stores/getters';
import { createStore } from '~/ide/stores'; import { createStore } from '~/ide/stores';
import { file } from '../helpers'; import { file } from '../helpers';
const TEST_PROJECT_ID = 'test_project';
describe('IDE store getters', () => { describe('IDE store getters', () => {
let localState; let localState;
let localStore; let localStore;
...@@ -398,4 +400,38 @@ describe('IDE store getters', () => { ...@@ -398,4 +400,38 @@ describe('IDE store getters', () => {
}, },
); );
}); });
describe('findProjectPermissions', () => {
it('returns false if project not found', () => {
expect(localStore.getters.findProjectPermissions(TEST_PROJECT_ID)).toEqual({});
});
it('finds permission in given project', () => {
const userPermissions = {
readMergeRequest: true,
createMergeRequestsIn: false,
};
localState.projects[TEST_PROJECT_ID] = { userPermissions };
expect(localStore.getters.findProjectPermissions(TEST_PROJECT_ID)).toBe(userPermissions);
});
});
describe.each`
getterName | permissionKey
${'canReadMergeRequests'} | ${'readMergeRequest'}
${'canCreateMergeRequests'} | ${'createMergeRequestIn'}
`('$getterName', ({ getterName, permissionKey }) => {
it.each([true, false])('finds permission for current project (%s)', val => {
localState.projects[TEST_PROJECT_ID] = {
userPermissions: {
[permissionKey]: val,
},
};
localState.currentProjectId = TEST_PROJECT_ID;
expect(localStore.getters[getterName]).toBe(val);
});
});
}); });
...@@ -15,6 +15,7 @@ exports[`Repository table row component renders table row 1`] = ` ...@@ -15,6 +15,7 @@ exports[`Repository table row component renders table row 1`] = `
<a <a
class="str-truncated" class="str-truncated"
data-qa-selector="file_name_link"
href="https://test.com" href="https://test.com"
> >
...@@ -64,6 +65,7 @@ exports[`Repository table row component renders table row for path with special ...@@ -64,6 +65,7 @@ exports[`Repository table row component renders table row for path with special
<a <a
class="str-truncated" class="str-truncated"
data-qa-selector="file_name_link"
href="https://test.com" href="https://test.com"
> >
......
import Vue from 'vue'; import Vue from 'vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { projectData, branches } from 'spec/ide/mock_data'; import { projectData, branches } from 'spec/ide/mock_data';
import { resetStore } from 'spec/ide/helpers';
import NewMergeRequestOption from '~/ide/components/commit_sidebar/new_merge_request_option.vue'; import NewMergeRequestOption from '~/ide/components/commit_sidebar/new_merge_request_option.vue';
import store from '~/ide/stores'; import { createStore } from '~/ide/stores';
import consts from '../../../../../app/assets/javascripts/ide/stores/modules/commit/constants'; import { PERMISSION_CREATE_MR } from '~/ide/constants';
import consts from '~/ide/stores/modules/commit/constants';
describe('create new MR checkbox', () => { describe('create new MR checkbox', () => {
let store;
let vm; let vm;
const setMR = () => { const setMR = () => {
vm.$store.state.currentMergeRequestId = '1'; vm.$store.state.currentMergeRequestId = '1';
vm.$store.state.projects[store.state.currentProjectId].mergeRequests[ vm.$store.state.projects[store.state.currentProjectId].mergeRequests[
...@@ -15,6 +17,10 @@ describe('create new MR checkbox', () => { ...@@ -15,6 +17,10 @@ describe('create new MR checkbox', () => {
] = { foo: 'bar' }; ] = { foo: 'bar' };
}; };
const setPermissions = permissions => {
store.state.projects[store.state.currentProjectId].userPermissions = permissions;
};
const createComponent = ({ currentBranchId = 'master', createNewBranch = false } = {}) => { const createComponent = ({ currentBranchId = 'master', createNewBranch = false } = {}) => {
const Component = Vue.extend(NewMergeRequestOption); const Component = Vue.extend(NewMergeRequestOption);
...@@ -25,20 +31,29 @@ describe('create new MR checkbox', () => { ...@@ -25,20 +31,29 @@ describe('create new MR checkbox', () => {
: consts.COMMIT_TO_CURRENT_BRANCH; : consts.COMMIT_TO_CURRENT_BRANCH;
vm.$store.state.currentBranchId = currentBranchId; vm.$store.state.currentBranchId = currentBranchId;
vm.$store.state.currentProjectId = 'abcproject';
const proj = JSON.parse(JSON.stringify(projectData)); store.state.projects.abcproject.branches[currentBranchId] = branches.find(
proj.branches[currentBranchId] = branches.find(branch => branch.name === currentBranchId); branch => branch.name === currentBranchId,
);
Vue.set(vm.$store.state.projects, 'abcproject', proj);
return vm.$mount(); return vm.$mount();
}; };
const findInput = () => vm.$el.querySelector('input[type="checkbox"]');
const findLabel = () => vm.$el.querySelector('.js-ide-commit-new-mr');
beforeEach(() => {
store = createStore();
store.state.currentProjectId = 'abcproject';
const proj = JSON.parse(JSON.stringify(projectData));
proj.userPermissions[PERMISSION_CREATE_MR] = true;
Vue.set(store.state.projects, 'abcproject', proj);
});
afterEach(() => { afterEach(() => {
vm.$destroy(); vm.$destroy();
resetStore(vm.$store);
}); });
describe('for default branch', () => { describe('for default branch', () => {
...@@ -160,6 +175,24 @@ describe('create new MR checkbox', () => { ...@@ -160,6 +175,24 @@ describe('create new MR checkbox', () => {
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
}); });
it('shows enablded checkbox', () => {
expect(findLabel().classList.contains('is-disabled')).toBe(false);
expect(findInput().disabled).toBe(false);
});
});
describe('when user cannot create MR', () => {
beforeEach(() => {
setPermissions({ [PERMISSION_CREATE_MR]: false });
createComponent({ currentBranchId: 'regular' });
});
it('disabled checkbox', () => {
expect(findLabel().classList.contains('is-disabled')).toBe(true);
expect(findInput().disabled).toBe(true);
});
}); });
it('dispatches toggleShouldCreateMR when clicking checkbox', () => { it('dispatches toggleShouldCreateMR when clicking checkbox', () => {
......
...@@ -2,62 +2,92 @@ import Vue from 'vue'; ...@@ -2,62 +2,92 @@ import Vue from 'vue';
import { trimText } from 'spec/helpers/text_helper'; import { trimText } from 'spec/helpers/text_helper';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import NavDropdownButton from '~/ide/components/nav_dropdown_button.vue'; import NavDropdownButton from '~/ide/components/nav_dropdown_button.vue';
import store from '~/ide/stores'; import { createStore } from '~/ide/stores';
import { resetStore } from '../helpers';
describe('NavDropdown', () => { describe('NavDropdown', () => {
const TEST_BRANCH_ID = 'lorem-ipsum-dolar'; const TEST_BRANCH_ID = 'lorem-ipsum-dolar';
const TEST_MR_ID = '12345'; const TEST_MR_ID = '12345';
const Component = Vue.extend(NavDropdownButton); let store;
let vm; let vm;
beforeEach(() => { beforeEach(() => {
vm = mountComponentWithStore(Component, { store }); store = createStore();
vm.$mount();
}); });
afterEach(() => { afterEach(() => {
vm.$destroy(); vm.$destroy();
resetStore(store);
}); });
it('renders empty placeholders, if state is falsey', () => { const createComponent = (props = {}) => {
expect(trimText(vm.$el.textContent)).toEqual('- -'); vm = mountComponentWithStore(Vue.extend(NavDropdownButton), { props, store });
}); vm.$mount();
};
it('renders branch name, if state has currentBranchId', done => { const findIcon = name => vm.$el.querySelector(`.ic-${name}`);
vm.$store.state.currentBranchId = TEST_BRANCH_ID; const findMRIcon = () => findIcon('merge-request');
const findBranchIcon = () => findIcon('branch');
vm.$nextTick() describe('normal', () => {
.then(() => { beforeEach(() => {
expect(trimText(vm.$el.textContent)).toEqual(`${TEST_BRANCH_ID} -`); createComponent();
}) });
.then(done)
.catch(done.fail); it('renders empty placeholders, if state is falsey', () => {
}); expect(trimText(vm.$el.textContent)).toEqual('- -');
});
it('renders mr id, if state has currentMergeRequestId', done => { it('renders branch name, if state has currentBranchId', done => {
vm.$store.state.currentMergeRequestId = TEST_MR_ID; vm.$store.state.currentBranchId = TEST_BRANCH_ID;
vm.$nextTick() vm.$nextTick()
.then(() => { .then(() => {
expect(trimText(vm.$el.textContent)).toEqual(`- !${TEST_MR_ID}`); expect(trimText(vm.$el.textContent)).toEqual(`${TEST_BRANCH_ID} -`);
}) })
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
});
it('renders mr id, if state has currentMergeRequestId', done => {
vm.$store.state.currentMergeRequestId = TEST_MR_ID;
vm.$nextTick()
.then(() => {
expect(trimText(vm.$el.textContent)).toEqual(`- !${TEST_MR_ID}`);
})
.then(done)
.catch(done.fail);
});
it('renders branch and mr, if state has both', done => {
vm.$store.state.currentBranchId = TEST_BRANCH_ID;
vm.$store.state.currentMergeRequestId = TEST_MR_ID;
vm.$nextTick()
.then(() => {
expect(trimText(vm.$el.textContent)).toEqual(`${TEST_BRANCH_ID} !${TEST_MR_ID}`);
})
.then(done)
.catch(done.fail);
});
it('shows icons', () => {
expect(findBranchIcon()).toBeTruthy();
expect(findMRIcon()).toBeTruthy();
});
}); });
it('renders branch and mr, if state has both', done => { describe('with showMergeRequests false', () => {
vm.$store.state.currentBranchId = TEST_BRANCH_ID; beforeEach(() => {
vm.$store.state.currentMergeRequestId = TEST_MR_ID; createComponent({ showMergeRequests: false });
});
it('shows single empty placeholder, if state is falsey', () => {
expect(trimText(vm.$el.textContent)).toEqual('-');
});
vm.$nextTick() it('shows only branch icon', () => {
.then(() => { expect(findBranchIcon()).toBeTruthy();
expect(trimText(vm.$el.textContent)).toEqual(`${TEST_BRANCH_ID} !${TEST_MR_ID}`); expect(findMRIcon()).toBe(null);
}) });
.then(done)
.catch(done.fail);
}); });
}); });
...@@ -3,6 +3,9 @@ import Vue from 'vue'; ...@@ -3,6 +3,9 @@ import Vue from 'vue';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import store from '~/ide/stores'; import store from '~/ide/stores';
import NavDropdown from '~/ide/components/nav_dropdown.vue'; import NavDropdown from '~/ide/components/nav_dropdown.vue';
import { PERMISSION_READ_MR } from '~/ide/constants';
const TEST_PROJECT_ID = 'lorem-ipsum';
describe('IDE NavDropdown', () => { describe('IDE NavDropdown', () => {
const Component = Vue.extend(NavDropdown); const Component = Vue.extend(NavDropdown);
...@@ -10,6 +13,12 @@ describe('IDE NavDropdown', () => { ...@@ -10,6 +13,12 @@ describe('IDE NavDropdown', () => {
let $dropdown; let $dropdown;
beforeEach(() => { beforeEach(() => {
store.state.currentProjectId = TEST_PROJECT_ID;
Vue.set(store.state.projects, TEST_PROJECT_ID, {
userPermissions: {
[PERMISSION_READ_MR]: true,
},
});
vm = mountComponentWithStore(Component, { store }); vm = mountComponentWithStore(Component, { store });
$dropdown = $(vm.$el); $dropdown = $(vm.$el);
...@@ -21,6 +30,9 @@ describe('IDE NavDropdown', () => { ...@@ -21,6 +30,9 @@ describe('IDE NavDropdown', () => {
vm.$destroy(); vm.$destroy();
}); });
const findIcon = name => vm.$el.querySelector(`.ic-${name}`);
const findMRIcon = () => findIcon('merge-request');
it('renders nothing initially', () => { it('renders nothing initially', () => {
expect(vm.$el).not.toContainElement('.ide-nav-form'); expect(vm.$el).not.toContainElement('.ide-nav-form');
}); });
...@@ -47,4 +59,22 @@ describe('IDE NavDropdown', () => { ...@@ -47,4 +59,22 @@ describe('IDE NavDropdown', () => {
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
}); });
it('renders merge request icon', () => {
expect(findMRIcon()).not.toBeNull();
});
describe('when user cannot read merge requests', () => {
beforeEach(done => {
store.state.projects[TEST_PROJECT_ID].userPermissions = {};
vm.$nextTick()
.then(done)
.catch(done.fail);
});
it('does not render merge requests', () => {
expect(findMRIcon()).toBeNull();
});
});
}); });
...@@ -8,7 +8,7 @@ import actions, { ...@@ -8,7 +8,7 @@ import actions, {
openMergeRequest, openMergeRequest,
} from '~/ide/stores/actions/merge_request'; } from '~/ide/stores/actions/merge_request';
import service from '~/ide/services'; import service from '~/ide/services';
import { activityBarViews } from '~/ide/constants'; import { activityBarViews, PERMISSION_READ_MR } from '~/ide/constants';
import { resetStore } from '../../helpers'; import { resetStore } from '../../helpers';
const TEST_PROJECT = 'abcproject'; const TEST_PROJECT = 'abcproject';
...@@ -23,6 +23,9 @@ describe('IDE store merge request actions', () => { ...@@ -23,6 +23,9 @@ describe('IDE store merge request actions', () => {
store.state.projects[TEST_PROJECT] = { store.state.projects[TEST_PROJECT] = {
id: TEST_PROJECT_ID, id: TEST_PROJECT_ID,
mergeRequests: {}, mergeRequests: {},
userPermissions: {
[PERMISSION_READ_MR]: true,
},
}; };
}); });
...@@ -79,6 +82,19 @@ describe('IDE store merge request actions', () => { ...@@ -79,6 +82,19 @@ describe('IDE store merge request actions', () => {
}) })
.catch(done.fail); .catch(done.fail);
}); });
it('does nothing if user cannot read MRs', done => {
store.state.projects[TEST_PROJECT].userPermissions[PERMISSION_READ_MR] = false;
store
.dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' })
.then(() => {
expect(service.getProjectMergeRequests).not.toHaveBeenCalled();
expect(store.state.currentMergeRequestId).toBe('');
})
.then(done)
.catch(done.fail);
});
}); });
describe('no merge requests for branch available case', () => { describe('no merge requests for branch available case', () => {
......
...@@ -7,7 +7,7 @@ import eventHub from '~/ide/eventhub'; ...@@ -7,7 +7,7 @@ import eventHub from '~/ide/eventhub';
import consts from '~/ide/stores/modules/commit/constants'; import consts from '~/ide/stores/modules/commit/constants';
import * as mutationTypes from '~/ide/stores/modules/commit/mutation_types'; import * as mutationTypes from '~/ide/stores/modules/commit/mutation_types';
import * as actions from '~/ide/stores/modules/commit/actions'; import * as actions from '~/ide/stores/modules/commit/actions';
import { commitActionTypes } from '~/ide/constants'; import { commitActionTypes, PERMISSION_CREATE_MR } from '~/ide/constants';
import testAction from '../../../../helpers/vuex_action_helper'; import testAction from '../../../../helpers/vuex_action_helper';
const TEST_COMMIT_SHA = '123456789'; const TEST_COMMIT_SHA = '123456789';
...@@ -313,6 +313,9 @@ describe('IDE commit module actions', () => { ...@@ -313,6 +313,9 @@ describe('IDE commit module actions', () => {
}, },
}, },
}, },
userPermissions: {
[PERMISSION_CREATE_MR]: true,
},
}, },
}, },
}); });
......
...@@ -125,4 +125,31 @@ describe Gitlab::ImportExport::GroupTreeRestorer do ...@@ -125,4 +125,31 @@ describe Gitlab::ImportExport::GroupTreeRestorer do
end end
end end
end end
context 'group visibility levels' do
let(:user) { create(:user) }
let(:shared) { Gitlab::ImportExport::Shared.new(group) }
let(:group_tree_restorer) { described_class.new(user: user, shared: shared, group: group, group_hash: nil) }
before do
setup_import_export_config(filepath)
group_tree_restorer.restore
end
shared_examples 'with visibility level' do |visibility_level, expected_visibilities|
context "when visibility level is #{visibility_level}" do
let(:group) { create(:group, visibility_level) }
let(:filepath) { "group_exports/visibility_levels/#{visibility_level}" }
it "imports all subgroups as #{visibility_level}" do
expect(group.children.map(&:visibility_level)).to eq(expected_visibilities)
end
end
end
include_examples 'with visibility level', :public, [20, 10, 0]
include_examples 'with visibility level', :private, [0, 0, 0]
include_examples 'with visibility level', :internal, [10, 10, 0]
end
end end
...@@ -80,7 +80,7 @@ describe Gitlab::ImportExport::GroupTreeSaver do ...@@ -80,7 +80,7 @@ describe Gitlab::ImportExport::GroupTreeSaver do
end end
it 'saves the correct json' do it 'saves the correct json' do
expect(saved_group_json).to include({ 'description' => 'description', 'visibility_level' => 20 }) expect(saved_group_json).to include({ 'description' => 'description' })
end end
it 'has milestones' do it 'has milestones' do
......
...@@ -45,6 +45,14 @@ describe API::GroupImport do ...@@ -45,6 +45,14 @@ describe API::GroupImport do
expect(response).to have_gitlab_http_status(202) expect(response).to have_gitlab_http_status(202)
end end
it 'creates private group' do
expect { subject }.to change { Group.count }.by(1)
group = Group.find_by(name: 'test-import-group')
expect(group.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
context 'when importing to a parent group' do context 'when importing to a parent group' do
before do before do
group.add_owner(user) group.add_owner(user)
...@@ -59,6 +67,34 @@ describe API::GroupImport do ...@@ -59,6 +67,34 @@ describe API::GroupImport do
expect(group.children.count).to eq(1) expect(group.children.count).to eq(1)
end end
context 'when parent group is private or internal' do
let(:public_parent_group) { create(:group, :public) }
let(:internal_parent_group) { create(:group, :internal) }
before do
public_parent_group.add_owner(user)
internal_parent_group.add_owner(user)
end
it 'imports public group' do
params[:parent_id] = public_parent_group.id
subject
expect(response).to have_gitlab_http_status(202)
expect(public_parent_group.children.first.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
end
it 'imports internal group' do
params[:parent_id] = internal_parent_group.id
subject
expect(response).to have_gitlab_http_status(202)
expect(internal_parent_group.children.first.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL)
end
end
context 'when parent group is invalid' do context 'when parent group is invalid' do
it 'returns 404 and does not create new group' do it 'returns 404 and does not create new group' do
params[:parent_id] = 99999 params[:parent_id] = 99999
......
...@@ -61,7 +61,11 @@ describe API::LsifData do ...@@ -61,7 +61,11 @@ describe API::LsifData do
'end_line' => 8, 'end_line' => 8,
'start_char' => 13, 'start_char' => 13,
'start_line' => 8, 'start_line' => 8,
'definition_url' => project_blob_path(project, "#{commit.id}/morestrings/reverse.go", anchor: 'L5') 'definition_url' => project_blob_path(project, "#{commit.id}/morestrings/reverse.go", anchor: 'L5'),
'hover' => [{
'language' => 'go',
'value' => Gitlab::Highlight.highlight(nil, 'func Func2(i int) string', language: 'go')
}]
}) })
end end
......
This diff is collapsed.
...@@ -17,11 +17,7 @@ describe 'Self-Monitoring project requests' do ...@@ -17,11 +17,7 @@ describe 'Self-Monitoring project requests' do
login_as(admin) login_as(admin)
end end
context 'with feature flag disabled' do context 'when the self monitoring project is created' do
it_behaves_like 'not accessible if feature flag is disabled'
end
context 'with feature flag enabled' do
let(:status_api) { status_create_self_monitoring_project_admin_application_settings_path } let(:status_api) { status_create_self_monitoring_project_admin_application_settings_path }
it_behaves_like 'triggers async worker, returns sidekiq job_id with response accepted' it_behaves_like 'triggers async worker, returns sidekiq job_id with response accepted'
...@@ -45,11 +41,7 @@ describe 'Self-Monitoring project requests' do ...@@ -45,11 +41,7 @@ describe 'Self-Monitoring project requests' do
login_as(admin) login_as(admin)
end end
context 'with feature flag disabled' do context 'when the self monitoring project is being created' do
it_behaves_like 'not accessible if feature flag is disabled'
end
context 'with feature flag enabled' do
it_behaves_like 'handles invalid job_id' it_behaves_like 'handles invalid job_id'
context 'when job is in progress' do context 'when job is in progress' do
...@@ -129,11 +121,7 @@ describe 'Self-Monitoring project requests' do ...@@ -129,11 +121,7 @@ describe 'Self-Monitoring project requests' do
login_as(admin) login_as(admin)
end end
context 'with feature flag disabled' do context 'when the self monitoring project is deleted' do
it_behaves_like 'not accessible if feature flag is disabled'
end
context 'with feature flag enabled' do
let(:status_api) { status_delete_self_monitoring_project_admin_application_settings_path } let(:status_api) { status_delete_self_monitoring_project_admin_application_settings_path }
it_behaves_like 'triggers async worker, returns sidekiq job_id with response accepted' it_behaves_like 'triggers async worker, returns sidekiq job_id with response accepted'
...@@ -157,11 +145,7 @@ describe 'Self-Monitoring project requests' do ...@@ -157,11 +145,7 @@ describe 'Self-Monitoring project requests' do
login_as(admin) login_as(admin)
end end
context 'with feature flag disabled' do context 'when the self monitoring project is being deleted' do
it_behaves_like 'not accessible if feature flag is disabled'
end
context 'with feature flag enabled' do
it_behaves_like 'handles invalid job_id' it_behaves_like 'handles invalid job_id'
context 'when job is in progress' do context 'when job is in progress' do
......
...@@ -12,6 +12,10 @@ describe Projects::LsifDataService do ...@@ -12,6 +12,10 @@ describe Projects::LsifDataService do
let(:service) { described_class.new(artifact.file, project, params) } let(:service) { described_class.new(artifact.file, project, params) }
describe '#execute' do describe '#execute' do
def highlighted_value(value)
[{ language: 'go', value: Gitlab::Highlight.highlight(nil, value, language: 'go') }]
end
context 'fetched lsif file', :use_clean_rails_memory_store_caching do context 'fetched lsif file', :use_clean_rails_memory_store_caching do
it 'is cached' do it 'is cached' do
service.execute service.execute
...@@ -32,42 +36,48 @@ describe Projects::LsifDataService do ...@@ -32,42 +36,48 @@ describe Projects::LsifDataService do
end_line: 6, end_line: 6,
start_char: 5, start_char: 5,
start_line: 6, start_line: 6,
definition_url: "#{path_prefix}/main.go#L7" definition_url: "#{path_prefix}/main.go#L7",
hover: highlighted_value('func main()')
}, },
{ {
end_char: 36, end_char: 36,
end_line: 3, end_line: 3,
start_char: 1, start_char: 1,
start_line: 3, start_line: 3,
definition_url: "#{path_prefix}/main.go#L4" definition_url: "#{path_prefix}/main.go#L4",
hover: highlighted_value('package "github.com/user/hello/morestrings" ("github.com/user/hello/morestrings")')
}, },
{ {
end_char: 12, end_char: 12,
end_line: 7, end_line: 7,
start_char: 1, start_char: 1,
start_line: 7, start_line: 7,
definition_url: "#{path_prefix}/main.go#L4" definition_url: "#{path_prefix}/main.go#L4",
hover: highlighted_value('package "github.com/user/hello/morestrings" ("github.com/user/hello/morestrings")')
}, },
{ {
end_char: 20, end_char: 20,
end_line: 7, end_line: 7,
start_char: 13, start_char: 13,
start_line: 7, start_line: 7,
definition_url: "#{path_prefix}/morestrings/reverse.go#L11" definition_url: "#{path_prefix}/morestrings/reverse.go#L11",
hover: highlighted_value('func Reverse(s string) string') + [{ value: "This method reverses a string \n\n" }]
}, },
{ {
end_char: 12, end_char: 12,
end_line: 8, end_line: 8,
start_char: 1, start_char: 1,
start_line: 8, start_line: 8,
definition_url: "#{path_prefix}/main.go#L4" definition_url: "#{path_prefix}/main.go#L4",
hover: highlighted_value('package "github.com/user/hello/morestrings" ("github.com/user/hello/morestrings")')
}, },
{ {
end_char: 18, end_char: 18,
end_line: 8, end_line: 8,
start_char: 13, start_char: 13,
start_line: 8, start_line: 8,
definition_url: "#{path_prefix}/morestrings/reverse.go#L5" definition_url: "#{path_prefix}/morestrings/reverse.go#L5",
hover: highlighted_value('func Func2(i int) string')
} }
]) ])
end end
...@@ -82,7 +92,8 @@ describe Projects::LsifDataService do ...@@ -82,7 +92,8 @@ describe Projects::LsifDataService do
end_line: 11, end_line: 11,
start_char: 1, start_char: 1,
start_line: 11, start_line: 11,
definition_url: "/#{project.full_path}/-/blob/#{commit_id}/morestrings/reverse.go#L12" definition_url: "/#{project.full_path}/-/blob/#{commit_id}/morestrings/reverse.go#L12",
hover: highlighted_value('var a string')
}) })
end end
end end
......
...@@ -58,6 +58,13 @@ module ApiHelpers ...@@ -58,6 +58,13 @@ module ApiHelpers
expect(json_response.map { |item| item['id'] }).to eq(Array(items)) expect(json_response.map { |item| item['id'] }).to eq(Array(items))
end end
def expect_response_contain_exactly(*items)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
expect(json_response.length).to eq(items.size)
expect(json_response.map { |item| item['id'] }).to contain_exactly(*items)
end
def stub_last_activity_update def stub_last_activity_update
allow_any_instance_of(Users::ActivityService).to receive(:execute) allow_any_instance_of(Users::ActivityService).to receive(:execute)
end end
......
# frozen_string_literal: true # frozen_string_literal: true
RSpec.shared_examples 'not accessible if feature flag is disabled' do
before do
stub_feature_flags(self_monitoring_project: false)
end
it 'returns not_implemented' do
subject
aggregate_failures do
expect(response).to have_gitlab_http_status(:not_implemented)
expect(json_response).to eq(
'message' => _('Self-monitoring is not enabled on this GitLab server, contact your administrator.'),
'documentation_url' => help_page_path('administration/monitoring/gitlab_self_monitoring_project/index')
)
end
end
end
RSpec.shared_examples 'not accessible to non-admin users' do RSpec.shared_examples 'not accessible to non-admin users' do
context 'with unauthenticated user' do context 'with unauthenticated user' do
it 'redirects to signin page' do it 'redirects to signin page' do
......
...@@ -19,12 +19,12 @@ describe 'projects/tree/_tree_header' do ...@@ -19,12 +19,12 @@ describe 'projects/tree/_tree_header' do
allow(view).to receive(:can_collaborate_with_project?) { true } allow(view).to receive(:can_collaborate_with_project?) { true }
end end
it 'does not render the WebIDE button when user cannot create fork or cannot open MR' do it 'renders the WebIDE button when user can collaborate but not create fork or MR' do
allow(view).to receive(:can?) { false } allow(view).to receive(:can?) { false }
render render
expect(rendered).not_to have_link('Web IDE') expect(rendered).to have_link('Web IDE')
end end
it 'renders the WebIDE button when user can create fork and can open MR in project' do it 'renders the WebIDE button when user can create fork and can open MR in project' do
...@@ -43,4 +43,13 @@ describe 'projects/tree/_tree_header' do ...@@ -43,4 +43,13 @@ describe 'projects/tree/_tree_header' do
expect(rendered).to have_link('Web IDE', href: '#modal-confirm-fork') expect(rendered).to have_link('Web IDE', href: '#modal-confirm-fork')
end end
it 'does not render the WebIDE button when user cannot collaborate or create mr' do
allow(view).to receive(:can?) { false }
allow(view).to receive(:can_collaborate_with_project?) { false }
render
expect(rendered).not_to have_link('Web IDE')
end
end end
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