Commit 2aa8a75f authored by Phil Hughes's avatar Phil Hughes

Merge branch 'master' into breadcrumbs-improvements

parents de82bd8e 1632ffa6
...@@ -208,7 +208,7 @@ update-tests-metadata: ...@@ -208,7 +208,7 @@ update-tests-metadata:
- '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH' - '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH'
- '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $FLAKY_RSPEC_SUITE_REPORT_PATH' - '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $FLAKY_RSPEC_SUITE_REPORT_PATH'
- rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json - rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json
- rm -f rspec_flaky/${CI_PROJECT_NAME}/all_node_*.json - rm -f rspec_flaky/${CI_PROJECT_NAME}/*_node_*.json
flaky-examples-check: flaky-examples-check:
<<: *dedicated-runner <<: *dedicated-runner
......
...@@ -181,7 +181,7 @@ gem 'connection_pool', '~> 2.0' ...@@ -181,7 +181,7 @@ gem 'connection_pool', '~> 2.0'
gem 'hipchat', '~> 1.5.0' gem 'hipchat', '~> 1.5.0'
# JIRA integration # JIRA integration
gem 'jira-ruby', '~> 1.1.2' gem 'jira-ruby', '~> 1.4'
# Flowdock integration # Flowdock integration
gem 'gitlab-flowdock-git-hook', '~> 1.0.1' gem 'gitlab-flowdock-git-hook', '~> 1.0.1'
...@@ -397,7 +397,7 @@ group :ed25519 do ...@@ -397,7 +397,7 @@ group :ed25519 do
end end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly-proto', '~> 0.31.0', require: 'gitaly' gem 'gitaly-proto', '~> 0.32.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false gem 'toml-rb', '~> 0.3.15', require: false
......
...@@ -275,7 +275,7 @@ GEM ...@@ -275,7 +275,7 @@ GEM
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly-proto (0.31.0) gitaly-proto (0.32.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.0) grpc (~> 1.0)
github-linguist (4.7.6) github-linguist (4.7.6)
...@@ -404,8 +404,9 @@ GEM ...@@ -404,8 +404,9 @@ GEM
cause cause
json json
ipaddress (0.8.3) ipaddress (0.8.3)
jira-ruby (1.1.2) jira-ruby (1.4.1)
activesupport activesupport
multipart-post
oauth (~> 0.5, >= 0.5.0) oauth (~> 0.5, >= 0.5.0)
jquery-atwho-rails (1.3.2) jquery-atwho-rails (1.3.2)
jquery-rails (4.1.1) jquery-rails (4.1.1)
...@@ -1020,7 +1021,7 @@ DEPENDENCIES ...@@ -1020,7 +1021,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0) gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.31.0) gitaly-proto (~> 0.32.0)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.5.1) gitlab-markup (~> 1.5.1)
...@@ -1042,7 +1043,7 @@ DEPENDENCIES ...@@ -1042,7 +1043,7 @@ DEPENDENCIES
html2text html2text
httparty (~> 0.13.3) httparty (~> 0.13.3)
influxdb (~> 0.2) influxdb (~> 0.2)
jira-ruby (~> 1.1.2) jira-ruby (~> 1.4)
jquery-atwho-rails (~> 1.3.2) jquery-atwho-rails (~> 1.3.2)
jquery-rails (~> 4.1.0) jquery-rails (~> 4.1.0)
json-schema (~> 2.6.2) json-schema (~> 2.6.2)
......
...@@ -61,7 +61,7 @@ class DropdownHint extends gl.FilteredSearchDropdown { ...@@ -61,7 +61,7 @@ class DropdownHint extends gl.FilteredSearchDropdown {
.map(tokenKey => ({ .map(tokenKey => ({
icon: `fa-${tokenKey.icon}`, icon: `fa-${tokenKey.icon}`,
hint: tokenKey.key, hint: tokenKey.key,
tag: `<${tokenKey.tag}>`, tag: `:${tokenKey.tag}`,
type: tokenKey.type, type: tokenKey.type,
})); }));
......
...@@ -637,12 +637,16 @@ GitLabDropdown = (function() { ...@@ -637,12 +637,16 @@ GitLabDropdown = (function() {
value = this.options.id ? this.options.id(data) : data.id; value = this.options.id ? this.options.id(data) : data.id;
fieldName = this.options.fieldName; fieldName = this.options.fieldName;
if (value) { value = value.toString().replace(/'/g, '\\\''); } if (value) {
value = value.toString().replace(/'/g, '\\\'');
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']"); field = this.dropdown.parent().find(`input[name='${fieldName}'][value='${value}']`);
if (field.length) { if (field.length) {
selected = true; selected = true;
} }
} else {
field = this.dropdown.parent().find(`input[name='${fieldName}']`);
selected = !field.length;
}
} }
// Set URL // Set URL
if (this.options.url != null) { if (this.options.url != null) {
......
...@@ -5,7 +5,6 @@ ...@@ -5,7 +5,6 @@
/* global SubscriptionSelect */ /* global SubscriptionSelect */
import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import SidebarHeightManager from './sidebar_height_manager';
const HIDDEN_CLASS = 'hidden'; const HIDDEN_CLASS = 'hidden';
const DISABLED_CONTENT_CLASS = 'disabled-content'; const DISABLED_CONTENT_CLASS = 'disabled-content';
...@@ -50,13 +49,6 @@ export default class IssuableBulkUpdateSidebar { ...@@ -50,13 +49,6 @@ export default class IssuableBulkUpdateSidebar {
new SubscriptionSelect(); new SubscriptionSelect();
} }
getNavHeight() {
const navbarHeight = $('.navbar-gitlab').outerHeight();
const layoutNavHeight = $('.layout-nav').outerHeight();
const subNavScroll = $('.sub-nav-scroll').outerHeight();
return navbarHeight + layoutNavHeight + subNavScroll;
}
setupBulkUpdateActions() { setupBulkUpdateActions() {
IssuableBulkUpdateActions.setOriginalDropdownData(); IssuableBulkUpdateActions.setOriginalDropdownData();
} }
...@@ -84,23 +76,6 @@ export default class IssuableBulkUpdateSidebar { ...@@ -84,23 +76,6 @@ export default class IssuableBulkUpdateSidebar {
this.toggleBulkEditButtonDisabled(enable); this.toggleBulkEditButtonDisabled(enable);
this.toggleOtherFiltersDisabled(enable); this.toggleOtherFiltersDisabled(enable);
this.toggleCheckboxDisplay(enable); this.toggleCheckboxDisplay(enable);
if (enable) {
this.initAffix();
SidebarHeightManager.init();
}
}
initAffix() {
if (!this.$sidebar.hasClass('affix-top')) {
const offsetTop = $('.scrolling-tabs-container').outerHeight() + $('.sub-nav-scroll').outerHeight();
this.$sidebar.affix({
offset: {
top: offsetTop,
},
});
}
} }
updateSelectedIssuableIds() { updateSelectedIssuableIds() {
......
...@@ -50,19 +50,10 @@ import initFlyOutNav from './fly_out_nav'; ...@@ -50,19 +50,10 @@ import initFlyOutNav from './fly_out_nav';
}); });
}); });
function applyScrollNavClass() {
const scrollOpacityHeight = 40;
$('.navbar-border').css('opacity', Math.min($(window).scrollTop() / scrollOpacityHeight, 1));
}
$(() => { $(() => {
if (Cookies.get('new_nav') === 'true') {
const newNavSidebar = new NewNavSidebar(); const newNavSidebar = new NewNavSidebar();
newNavSidebar.bindEvents(); newNavSidebar.bindEvents();
initFlyOutNav(); initFlyOutNav();
}
$(window).on('scroll', _.throttle(applyScrollNavClass, 100));
}); });
}).call(window); }).call(window);
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
import statusCodes from '../../lib/utils/http_status'; import statusCodes from '../../lib/utils/http_status';
import MonitoringService from '../services/monitoring_service'; import MonitoringService from '../services/monitoring_service';
import GraphGroup from './graph_group.vue'; import GraphGroup from './graph_group.vue';
import GraphRow from './graph_row.vue'; import Graph from './graph.vue';
import EmptyState from './empty_state.vue'; import EmptyState from './empty_state.vue';
import MonitoringStore from '../stores/monitoring_store'; import MonitoringStore from '../stores/monitoring_store';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
...@@ -32,8 +32,8 @@ ...@@ -32,8 +32,8 @@
}, },
components: { components: {
Graph,
GraphGroup, GraphGroup,
GraphRow,
EmptyState, EmptyState,
}, },
...@@ -127,10 +127,10 @@ ...@@ -127,10 +127,10 @@
:key="index" :key="index"
:name="groupData.group" :name="groupData.group"
> >
<graph-row <graph
v-for="(row, index) in groupData.metrics" v-for="(graphData, index) in groupData.metrics"
:key="index" :key="index"
:row-data="row" :graph-data="graphData"
:update-aspect-ratio="updateAspectRatio" :update-aspect-ratio="updateAspectRatio"
:deployment-data="store.deploymentData" :deployment-data="store.deploymentData"
/> />
......
...@@ -19,10 +19,6 @@ ...@@ -19,10 +19,6 @@
type: Object, type: Object,
required: true, required: true,
}, },
classType: {
type: String,
required: true,
},
updateAspectRatio: { updateAspectRatio: {
type: Boolean, type: Boolean,
required: true, required: true,
...@@ -207,11 +203,10 @@ ...@@ -207,11 +203,10 @@
}, },
}; };
</script> </script>
<template> <template>
<div <div class="prometheus-graph">
:class="classType"> <h5 class="text-center graph-title">
<h5
class="text-center graph-title">
{{graphData.title}} {{graphData.title}}
</h5> </h5>
<div <div
......
...@@ -14,7 +14,7 @@ export default { ...@@ -14,7 +14,7 @@ export default {
<div class="panel-heading"> <div class="panel-heading">
<h4>{{name}}</h4> <h4>{{name}}</h4>
</div> </div>
<div class="panel-body"> <div class="panel-body prometheus-graph-group">
<slot /> <slot />
</div> </div>
</div> </div>
......
<script>
import Graph from './graph.vue';
export default {
props: {
rowData: {
type: Array,
required: true,
},
updateAspectRatio: {
type: Boolean,
required: true,
},
deploymentData: {
type: Array,
required: true,
},
},
components: {
Graph,
},
computed: {
bootstrapClass() {
return this.rowData.length >= 2 ? 'col-md-6' : 'col-md-12';
},
},
};
</script>
<template>
<div class="prometheus-row row">
<graph
v-for="(graphData, index) in rowData"
:graph-data="graphData"
:class-type="bootstrapClass"
:key="index"
:update-aspect-ratio="updateAspectRatio"
:deployment-data="deploymentData"
/>
</div>
</template>
...@@ -20,22 +20,6 @@ function normalizeMetrics(metrics) { ...@@ -20,22 +20,6 @@ function normalizeMetrics(metrics) {
})); }));
} }
function collate(array, rows = 2) {
const collatedArray = [];
let row = [];
array.forEach((value, index) => {
row.push(value);
if ((index + 1) % rows === 0) {
collatedArray.push(row);
row = [];
}
});
if (row.length > 0) {
collatedArray.push(row);
}
return collatedArray;
}
export default class MonitoringStore { export default class MonitoringStore {
constructor() { constructor() {
this.groups = []; this.groups = [];
...@@ -45,7 +29,7 @@ export default class MonitoringStore { ...@@ -45,7 +29,7 @@ export default class MonitoringStore {
storeMetrics(groups = []) { storeMetrics(groups = []) {
this.groups = groups.map(group => ({ this.groups = groups.map(group => ({
...group, ...group,
metrics: collate(normalizeMetrics(sortMetrics(group.metrics))), metrics: normalizeMetrics(sortMetrics(group.metrics)),
})); }));
} }
...@@ -54,12 +38,6 @@ export default class MonitoringStore { ...@@ -54,12 +38,6 @@ export default class MonitoringStore {
} }
getMetricsCount() { getMetricsCount() {
let metricsCount = 0; return this.groups.reduce((count, group) => count + group.metrics.length, 0);
this.groups.forEach((group) => {
group.metrics.forEach((metric) => {
metricsCount = metricsCount += metric.length;
});
});
return metricsCount;
} }
} }
...@@ -63,7 +63,7 @@ export default class NewNavSidebar { ...@@ -63,7 +63,7 @@ export default class NewNavSidebar {
if (breakpoint === 'sm' || breakpoint === 'md') { if (breakpoint === 'sm' || breakpoint === 'md') {
this.toggleCollapsedSidebar(true); this.toggleCollapsedSidebar(true);
} else if (breakpoint === 'lg') { } else if (breakpoint === 'lg') {
const collapse = Cookies.get('sidebar_collapsed') === 'true'; const collapse = this.$sidebar.hasClass('sidebar-icons-only');
this.toggleCollapsedSidebar(collapse); this.toggleCollapsedSidebar(collapse);
} }
} }
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
import _ from 'underscore'; import _ from 'underscore';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import SidebarHeightManager from './sidebar_height_manager';
(function() { (function() {
this.Sidebar = (function() { this.Sidebar = (function() {
...@@ -23,7 +22,6 @@ import SidebarHeightManager from './sidebar_height_manager'; ...@@ -23,7 +22,6 @@ import SidebarHeightManager from './sidebar_height_manager';
}; };
Sidebar.prototype.addEventListeners = function() { Sidebar.prototype.addEventListeners = function() {
SidebarHeightManager.init();
const $document = $(document); const $document = $(document);
this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked); this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
......
import _ from 'underscore';
import Cookies from 'js-cookie';
export default {
init() {
if (!this.initialized) {
if (Cookies.get('new_nav') === 'true' && $('.js-issuable-sidebar').length) return;
this.$window = $(window);
this.$rightSidebar = $('.js-right-sidebar');
this.$navHeight = $('.navbar-gitlab').outerHeight() +
$('.layout-nav').outerHeight() +
$('.sub-nav-scroll').outerHeight();
const throttledSetSidebarHeight = _.throttle(() => this.setSidebarHeight(), 20);
const debouncedSetSidebarHeight = _.debounce(() => this.setSidebarHeight(), 200);
this.$window.on('scroll', throttledSetSidebarHeight);
this.$window.on('resize', debouncedSetSidebarHeight);
this.initialized = true;
}
},
setSidebarHeight() {
const currentScrollDepth = window.pageYOffset || 0;
const diff = this.$navHeight - currentScrollDepth;
if (diff > 0) {
const newSidebarHeight = window.innerHeight - diff;
this.$rightSidebar.outerHeight(newSidebarHeight);
this.sidebarHeightIsCustom = true;
} else if (this.sidebarHeightIsCustom) {
this.$rightSidebar.outerHeight('100%');
this.sidebarHeightIsCustom = false;
}
},
};
...@@ -766,6 +766,7 @@ ...@@ -766,6 +766,7 @@
box-shadow: none; box-shadow: none;
padding: 8px 16px; padding: 8px 16px;
text-align: left; text-align: left;
white-space: normal;
width: 100%; width: 100%;
// make sure the text color is not overriden // make sure the text color is not overriden
......
...@@ -105,12 +105,11 @@ header { ...@@ -105,12 +105,11 @@ header {
top: -3px; top: -3px;
font-size: 10px; font-size: 10px;
} }
}
.user-counter {
svg { svg {
position: relative; height: 16px;
top: 2px;
height: 17px;
// hack to get SVG to line up with FA icons
width: 23px; width: 23px;
fill: currentColor; fill: currentColor;
} }
...@@ -325,12 +324,12 @@ header { ...@@ -325,12 +324,12 @@ header {
li { li {
.badge { .badge {
position: inherit; position: inherit;
top: -8px;
font-weight: $gl-font-weight-normal; font-weight: $gl-font-weight-normal;
margin-left: -11px; margin-left: -6px;
font-size: 11px; font-size: 11px;
color: $white-light; color: $white-light;
padding: 1px 5px 2px; padding: 0 5px;
line-height: 12px;
border-radius: 7px; border-radius: 7px;
box-shadow: 0 1px 0 rgba($gl-header-color, .2); box-shadow: 0 1px 0 rgba($gl-header-color, .2);
......
...@@ -177,13 +177,14 @@ $row-hover: $blue-25; ...@@ -177,13 +177,14 @@ $row-hover: $blue-25;
$row-hover-border: $blue-100; $row-hover-border: $blue-100;
$progress-color: #c0392b; $progress-color: #c0392b;
$header-height: 50px; $header-height: 50px;
$new-navbar-height: 40px;
$fixed-layout-width: 1280px; $fixed-layout-width: 1280px;
$limited-layout-width: 990px; $limited-layout-width: 990px;
$limited-layout-width-sm: 790px; $limited-layout-width-sm: 790px;
$container-text-max-width: 540px; $container-text-max-width: 540px;
$gl-avatar-size: 40px; $gl-avatar-size: 40px;
$error-exclamation-point: $red-500; $error-exclamation-point: $red-500;
$border-radius-default: 3px; $border-radius-default: 4px;
$settings-icon-size: 18px; $settings-icon-size: 18px;
$provider-btn-not-active-color: $blue-500; $provider-btn-not-active-color: $blue-500;
$link-underline-blue: $blue-500; $link-underline-blue: $blue-500;
......
...@@ -3,15 +3,21 @@ ...@@ -3,15 +3,21 @@
@import "bootstrap/variables"; @import "bootstrap/variables";
@import "framework/mixins"; @import "framework/mixins";
.content-wrapper.page-with-new-nav {
margin-top: $new-navbar-height;
}
header.navbar-gitlab-new { header.navbar-gitlab-new {
color: $white-light; color: $white-light;
background: linear-gradient(to right, $indigo-900, $indigo-800); background: linear-gradient(to right, $indigo-900, $indigo-800);
border-bottom: 0; border-bottom: 0;
min-height: $new-navbar-height;
.header-content { .header-content {
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
padding-left: 0; padding-left: 0;
min-height: $new-navbar-height;
.title-container { .title-container {
display: -webkit-flex; display: -webkit-flex;
...@@ -39,20 +45,13 @@ header.navbar-gitlab-new { ...@@ -39,20 +45,13 @@ header.navbar-gitlab-new {
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
align-items: center; align-items: center;
padding-right: $gl-padding; padding: 2px 8px;
padding-left: $gl-padding; margin: 5px 2px 5px -8px;
margin-left: -$gl-padding; border-radius: $border-radius-default;
@media (min-width: $screen-sm-min) {
padding-right: $gl-padding;
padding-left: $gl-padding;
}
svg { svg {
margin-top: -3px;
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
margin-right: 10px; margin-right: 8px;
} }
} }
...@@ -61,7 +60,7 @@ header.navbar-gitlab-new { ...@@ -61,7 +60,7 @@ header.navbar-gitlab-new {
svg { svg {
width: 55px; width: 55px;
height: 15px; height: 14px;
margin: 0; margin: 0;
fill: $white-light; fill: $white-light;
} }
...@@ -69,9 +68,7 @@ header.navbar-gitlab-new { ...@@ -69,9 +68,7 @@ header.navbar-gitlab-new {
&:hover, &:hover,
&:focus { &:focus {
.logo-text svg { background-color: rgba($indigo-200, .2);
fill: $tanuki-yellow;
}
} }
} }
} }
...@@ -91,6 +88,20 @@ header.navbar-gitlab-new { ...@@ -91,6 +88,20 @@ header.navbar-gitlab-new {
right: 0; right: 0;
} }
} }
&.menu-expanded {
@media (max-width: $screen-xs-max) {
.title-container,
.header-logo, {
display: none;
}
}
}
}
.dropdown-bold-header {
color: $gl-text-color-secondary;
font-size: 12px;
} }
.navbar-collapse { .navbar-collapse {
...@@ -99,14 +110,10 @@ header.navbar-gitlab-new { ...@@ -99,14 +110,10 @@ header.navbar-gitlab-new {
box-shadow: 0; box-shadow: 0;
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
margin-left: -$gl-padding; margin-left: -8px;
margin-right: -10px; margin-right: -10px;
} }
.dropdown-bold-header {
color: initial;
}
.nav { .nav {
> li:not(.hidden-xs) a { > li:not(.hidden-xs) a {
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
...@@ -120,7 +127,7 @@ header.navbar-gitlab-new { ...@@ -120,7 +127,7 @@ header.navbar-gitlab-new {
.container-fluid { .container-fluid {
.navbar-toggle { .navbar-toggle {
min-width: 45px; min-width: 45px;
padding: 6px $gl-padding; padding: 4px $gl-padding;
margin-right: -7px; margin-right: -7px;
font-size: 14px; font-size: 14px;
text-align: center; text-align: center;
...@@ -157,22 +164,47 @@ header.navbar-gitlab-new { ...@@ -157,22 +164,47 @@ header.navbar-gitlab-new {
} }
> a { > a {
background: none;
will-change: color; will-change: color;
margin: 4px 2px;
padding: 6px 8px;
color: $indigo-200;
height: 32px;
@media (max-width: $screen-xs-max) {
padding: 0;
}
svg {
fill: $indigo-200;
}
&.header-user-dropdown-toggle { &.header-user-dropdown-toggle {
margin-left: 2px;
.header-user-avatar { .header-user-avatar {
border-color: $indigo-200; border-color: $indigo-200;
margin-right: 0;
}
} }
} }
&:hover, .header-new-dropdown-toggle {
&:focus { margin-right: 0;
color: $white-light; }
> a:hover,
> a:focus {
text-decoration: none;
outline: 0;
opacity: 1; opacity: 1;
color: $white-light;
> svg { @media (min-width: $screen-sm-min) {
fill: $white-light; background-color: rgba($indigo-200, .2);
}
svg {
fill: currentColor;
} }
&.header-user-dropdown-toggle { &.header-user-dropdown-toggle {
...@@ -181,6 +213,40 @@ header.navbar-gitlab-new { ...@@ -181,6 +213,40 @@ header.navbar-gitlab-new {
} }
} }
} }
.impersonated-user,
.impersonated-user:hover {
margin-right: 1px;
background-color: $white-light;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
svg {
fill: $indigo-900;
}
}
.impersonation-btn,
.impersonation-btn:hover {
background-color: $white-light;
margin-left: 0;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
i {
color: $orange-500;
font-size: 20px;
}
}
&.active > a,
&.dropdown.open > a {
color: $indigo-900;
background-color: $white-light;
svg {
fill: currentColor;
}
} }
} }
} }
...@@ -189,45 +255,76 @@ header.navbar-gitlab-new { ...@@ -189,45 +255,76 @@ header.navbar-gitlab-new {
.navbar-sub-nav { .navbar-sub-nav {
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
margin-bottom: 0; margin: 0 0 0 6px;
color: $indigo-200; color: $indigo-200;
.dropdown-chevron {
position: relative;
top: -1px;
font-size: 10px;
}
}
.navbar-gitlab-new {
.navbar-sub-nav,
.navbar-nav {
> li { > li {
> a:hover, > a:hover,
> a:focus { > a:focus {
box-shadow: inset 0 -3px 0 rgba($indigo-200, .4);
text-decoration: none; text-decoration: none;
outline: 0; outline: 0;
color: $white-light; color: $white-light;
background-color: rgba($indigo-200, .2);
svg {
fill: currentColor;
}
} }
&.active > a { &.active > a,
box-shadow: inset 0 -3px 0 $indigo-500; &.dropdown.open > a {
color: $white-light; color: $indigo-900;
font-weight: $gl-font-weight-bold; background-color: $white-light;
svg {
fill: currentColor;
}
} }
> a { > a {
display: block; display: flex;
padding: 16px 10px; align-items: center;
font-size: 13px; justify-content: center;
padding: 6px 8px;
margin: 4px 2px;
font-size: 12px;
color: currentColor; color: currentColor;
box-shadow: inset 0 0 0 transparent; border-radius: $border-radius-default;
will-change: box-shadow; height: 32px;
transition: box-shadow 0.15s; font-weight: $gl-font-weight-bold;
@media (min-width: $screen-sm-min) { svg {
padding: 15px $gl-padding; fill: currentColor;
font-size: 14px;
}
} }
} }
.dropdown-chevron { &.line-separator {
position: relative; border-left: 1px solid rgba($indigo-200, .2);
top: -1px; margin: 8px;
font-size: 10px; }
} }
}
}
.admin-icon i {
font-size: 18px;
}
.caret-down {
height: 11px;
width: 11px;
margin-left: 4px;
fill: currentColor;
} }
.header-user .dropdown-menu-nav, .header-user .dropdown-menu-nav,
...@@ -236,10 +333,14 @@ header.navbar-gitlab-new { ...@@ -236,10 +333,14 @@ header.navbar-gitlab-new {
} }
.search { .search {
margin: 4px 8px 0;
form { form {
height: 32px;
border: 0; border: 0;
border-radius: $border-radius-default;
background-color: rgba($indigo-200, .2); background-color: rgba($indigo-200, .2);
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s, background-color ease-in-out 0.15s; transition: border-color ease-in-out 0.15s, background-color ease-in-out 0.15s;
&:hover { &:hover {
background-color: rgba($indigo-200, .3); background-color: rgba($indigo-200, .3);
...@@ -248,31 +349,50 @@ header.navbar-gitlab-new { ...@@ -248,31 +349,50 @@ header.navbar-gitlab-new {
} }
&.search-active form { &.search-active form {
background-color: rgba($indigo-200, .3); background-color: $white-light;
box-shadow: none; box-shadow: none;
.search-input {
color: $gl-text-color;
transition: color ease-in-out 0.15s;
}
.search-input::placeholder {
color: $gl-text-color-tertiary;
}
.search-input-wrap {
.search-icon,
.clear-icon {
color: $gl-text-color-tertiary;
transition: color ease-in-out 0.15s;
}
}
} }
.search-input { .search-input {
color: $white-light; color: $white-light;
background: none; background: none;
transition: color ease-in-out 0.15s;
} }
.search-input::placeholder { .search-input::placeholder {
color: rgba($indigo-200, .8); color: rgba($indigo-200, .8);
transition: color ease-in-out 0.15s;
} }
.location-badge { .location-badge {
font-size: 12px; font-size: 12px;
color: $indigo-100; color: $indigo-100;
background-color: rgba($indigo-200, .1); background-color: rgba($indigo-200, .1);
transition: color 0.15s;
will-change: color; will-change: color;
margin: -4px 4px -4px -4px; margin: -4px 4px -4px -4px;
line-height: 25px; line-height: 25px;
padding: 4px 8px; padding: 4px 8px;
border-radius: 2px 0 0 2px; border-radius: 2px 0 0 2px;
border-right: 1px solid $indigo-800; border-right: 1px solid $indigo-800;
height: 34px; height: 32px;
transition: border-color ease-in-out 0.15s;
} }
.search-input-wrap { .search-input-wrap {
...@@ -284,8 +404,9 @@ header.navbar-gitlab-new { ...@@ -284,8 +404,9 @@ header.navbar-gitlab-new {
&.search-active { &.search-active {
.location-badge { .location-badge {
color: $white-light; color: $gl-text-color;
background-color: rgba($indigo-200, .2); background-color: $nav-badge-bg;
border-color: $border-color;
} }
.search-input-wrap { .search-input-wrap {
...@@ -403,3 +524,14 @@ header.navbar-gitlab-new { ...@@ -403,3 +524,14 @@ header.navbar-gitlab-new {
} }
} }
} }
.btn-sign-in {
margin-top: 3px;
background-color: $indigo-100;
color: $indigo-900;
font-weight: $gl-font-weight-bold;
&:hover {
background-color: $white-light;
}
}
...@@ -26,7 +26,7 @@ $new-sidebar-collapsed-width: 50px; ...@@ -26,7 +26,7 @@ $new-sidebar-collapsed-width: 50px;
// Override position: absolute // Override position: absolute
.right-sidebar { .right-sidebar {
position: fixed; position: fixed;
height: calc(100% - #{$header-height}); height: calc(100% - #{$new-navbar-height});
} }
.issues-bulk-update.right-sidebar.right-sidebar-expanded .issuable-sidebar-header { .issues-bulk-update.right-sidebar.right-sidebar-expanded .issuable-sidebar-header {
...@@ -92,7 +92,7 @@ $new-sidebar-collapsed-width: 50px; ...@@ -92,7 +92,7 @@ $new-sidebar-collapsed-width: 50px;
z-index: 400; z-index: 400;
width: $new-sidebar-width; width: $new-sidebar-width;
transition: left $sidebar-transition-duration; transition: left $sidebar-transition-duration;
top: $header-height; top: $new-navbar-height;
bottom: 0; bottom: 0;
left: 0; left: 0;
background-color: $gray-normal; background-color: $gray-normal;
...@@ -188,7 +188,7 @@ $new-sidebar-collapsed-width: 50px; ...@@ -188,7 +188,7 @@ $new-sidebar-collapsed-width: 50px;
} }
.with-performance-bar .nav-sidebar { .with-performance-bar .nav-sidebar {
top: $header-height + $performance-bar-height; top: $new-navbar-height + $performance-bar-height;
} }
.sidebar-sub-level-items { .sidebar-sub-level-items {
...@@ -452,7 +452,7 @@ $new-sidebar-collapsed-width: 50px; ...@@ -452,7 +452,7 @@ $new-sidebar-collapsed-width: 50px;
// Make issue boards full-height now that sub-nav is gone // Make issue boards full-height now that sub-nav is gone
.boards-list { .boards-list {
height: calc(100vh - #{$header-height}); height: calc(100vh - #{$new-navbar-height});
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
height: 475px; // Needed for PhantomJS height: 475px; // Needed for PhantomJS
...@@ -463,7 +463,7 @@ $new-sidebar-collapsed-width: 50px; ...@@ -463,7 +463,7 @@ $new-sidebar-collapsed-width: 50px;
} }
.with-performance-bar .boards-list { .with-performance-bar .boards-list {
height: calc(100vh - #{$header-height} - #{$performance-bar-height}); height: calc(100vh - #{$new-navbar-height} - #{$performance-bar-height});
} }
......
...@@ -440,6 +440,7 @@ ...@@ -440,6 +440,7 @@
&.right-sidebar { &.right-sidebar {
top: 0; top: 0;
bottom: 0; bottom: 0;
height: 100%;
} }
.issuable-sidebar-header { .issuable-sidebar-header {
......
...@@ -227,6 +227,26 @@ ...@@ -227,6 +227,26 @@
margin-top: 20px; margin-top: 20px;
} }
.prometheus-graph-group {
display: flex;
flex-wrap: wrap;
padding: $gl-padding / 2;
}
.prometheus-graph {
flex: 1 0 auto;
min-width: 450px;
padding: $gl-padding / 2;
h5 {
font-size: 16px;
}
@media (max-width: $screen-sm-max) {
min-width: 100%;
}
}
.prometheus-svg-container { .prometheus-svg-container {
position: relative; position: relative;
height: 0; height: 0;
...@@ -297,9 +317,3 @@ ...@@ -297,9 +317,3 @@
} }
} }
} }
.prometheus-row {
h5 {
font-size: 16px;
}
}
...@@ -190,6 +190,8 @@ input[type="checkbox"]:hover { ...@@ -190,6 +190,8 @@ input[type="checkbox"]:hover {
} }
.search-holder { .search-holder {
@include new-style-dropdown;
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
......
...@@ -302,10 +302,6 @@ module ApplicationHelper ...@@ -302,10 +302,6 @@ module ApplicationHelper
end end
end end
def show_new_nav?
true
end
def collapsed_sidebar? def collapsed_sidebar?
cookies["sidebar_collapsed"] == "true" cookies["sidebar_collapsed"] == "true"
end end
......
...@@ -86,7 +86,7 @@ module GroupsHelper ...@@ -86,7 +86,7 @@ module GroupsHelper
def group_title_link(group, hidable: false, show_avatar: false) def group_title_link(group, hidable: false, show_avatar: false)
link_to(group_path(group), class: "group-path breadcrumb-item-text js-breadcrumb-item-text #{'hidable' if hidable}") do link_to(group_path(group), class: "group-path breadcrumb-item-text js-breadcrumb-item-text #{'hidable' if hidable}") do
output = output =
if (show_new_nav? && group.try(:avatar_url) || (show_new_nav? && show_avatar)) && !Rails.env.test? if (group.try(:avatar_url) || show_avatar)) && !Rails.env.test?
image_tag(group_icon(group), class: "avatar-tile", width: 15, height: 15) image_tag(group_icon(group), class: "avatar-tile", width: 15, height: 15)
else else
"" ""
......
module NavHelper module NavHelper
def page_with_sidebar_class def page_with_sidebar_class
class_name = page_gutter_class class_name = page_gutter_class
class_name << 'page-with-new-sidebar' if defined?(@new_sidebar) && @new_sidebar class_name << 'page-with-new-sidebar' if defined?(@left_sidebar) && @left_sidebar
class_name << 'page-with-icon-sidebar' if collapsed_sidebar? && @new_sidebar class_name << 'page-with-icon-sidebar' if collapsed_sidebar? && @left_sidebar
class_name class_name
end end
...@@ -30,24 +30,15 @@ module NavHelper ...@@ -30,24 +30,15 @@ module NavHelper
end end
end end
def nav_header_class def nav_control_class
class_names = [] "nav-control" if current_user
class_names << 'with-horizontal-nav' if defined?(nav) && nav
class_names
end end
def layout_nav_class def user_dropdown_class
return [] if show_new_nav?
class_names = [] class_names = []
class_names << 'page-with-layout-nav' if defined?(nav) && nav class_names << 'header-user-dropdown-toggle'
class_names << 'page-with-sub-nav' if content_for?(:sub_nav) class_names << 'impersonated-user' if session[:impersonator_id]
class_names class_names
end end
def nav_control_class
"nav-control" if current_user
end
end end
...@@ -4,7 +4,7 @@ module PageLayoutHelper ...@@ -4,7 +4,7 @@ module PageLayoutHelper
@page_title.push(*titles.compact) if titles.any? @page_title.push(*titles.compact) if titles.any?
if show_new_nav? && titles.any? && !defined?(@breadcrumb_title) if titles.any? && !defined?(@breadcrumb_title)
@breadcrumb_title = @page_title.last @breadcrumb_title = @page_title.last
end end
......
...@@ -62,7 +62,7 @@ module ProjectsHelper ...@@ -62,7 +62,7 @@ module ProjectsHelper
project_link = link_to project_path(project), { class: ("project-item-select-holder" unless show_new_nav?) } do project_link = link_to project_path(project), { class: ("project-item-select-holder" unless show_new_nav?) } do
output = output =
if show_new_nav? && project.avatar_url && !Rails.env.test? if project.avatar_url && !Rails.env.test?
project_icon(project, alt: project.name, class: 'avatar-tile', width: 15, height: 15) project_icon(project, alt: project.name, class: 'avatar-tile', width: 15, height: 15)
else else
"" ""
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
# #
module Issuable module Issuable
extend ActiveSupport::Concern extend ActiveSupport::Concern
include Gitlab::SQL::Pattern
include CacheMarkdownField include CacheMarkdownField
include Participable include Participable
include Mentionable include Mentionable
...@@ -122,7 +123,9 @@ module Issuable ...@@ -122,7 +123,9 @@ module Issuable
# #
# Returns an ActiveRecord::Relation. # Returns an ActiveRecord::Relation.
def search(query) def search(query)
where(arel_table[:title].matches("%#{query}%")) title = to_fuzzy_arel(:title, query)
where(title)
end end
# Searches for records with a matching title or description. # Searches for records with a matching title or description.
...@@ -133,10 +136,10 @@ module Issuable ...@@ -133,10 +136,10 @@ module Issuable
# #
# Returns an ActiveRecord::Relation. # Returns an ActiveRecord::Relation.
def full_search(query) def full_search(query)
t = arel_table title = to_fuzzy_arel(:title, query)
pattern = "%#{query}%" description = to_fuzzy_arel(:description, query)
where(t[:title].matches(pattern).or(t[:description].matches(pattern))) where(title&.or(description))
end end
def sort(method, excluded_labels: []) def sort(method, excluded_labels: [])
......
...@@ -16,6 +16,7 @@ class Group < Namespace ...@@ -16,6 +16,7 @@ class Group < Namespace
source: :user source: :user
has_many :requesters, -> { where.not(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent has_many :requesters, -> { where.not(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent
has_many :members_and_requesters, as: :source, class_name: 'GroupMember'
has_many :milestones has_many :milestones
has_many :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
......
...@@ -126,20 +126,11 @@ class Member < ActiveRecord::Base ...@@ -126,20 +126,11 @@ class Member < ActiveRecord::Base
find_by(invite_token: invite_token) find_by(invite_token: invite_token)
end end
def add_user(source, user, access_level, current_user: nil, expires_at: nil) def add_user(source, user, access_level, existing_members: nil, current_user: nil, expires_at: nil)
user = retrieve_user(user) # `user` can be either a User object, User ID or an email to be invited
member = retrieve_member(source, user, existing_members)
access_level = retrieve_access_level(access_level) access_level = retrieve_access_level(access_level)
# `user` can be either a User object or an email to be invited
member =
if user.is_a?(User)
source.members.find_by(user_id: user.id) ||
source.requesters.find_by(user_id: user.id) ||
source.members.build(user_id: user.id)
else
source.members.build(invite_email: user)
end
return member unless can_update_member?(current_user, member) return member unless can_update_member?(current_user, member)
member.attributes = { member.attributes = {
...@@ -165,17 +156,15 @@ class Member < ActiveRecord::Base ...@@ -165,17 +156,15 @@ class Member < ActiveRecord::Base
def add_users(source, users, access_level, current_user: nil, expires_at: nil) def add_users(source, users, access_level, current_user: nil, expires_at: nil)
return [] unless users.present? return [] unless users.present?
# Collect all user ids into separate array emails, users, existing_members = parse_users_list(source, users)
# so we can use single sql query to get user objects
user_ids = users.select { |user| user =~ /\A\d+\Z/ }
users = users - user_ids + User.where(id: user_ids)
self.transaction do self.transaction do
users.map do |user| (emails + users).map! do |user|
add_user( add_user(
source, source,
user, user,
access_level, access_level,
existing_members: existing_members,
current_user: current_user, current_user: current_user,
expires_at: expires_at expires_at: expires_at
) )
...@@ -189,6 +178,31 @@ class Member < ActiveRecord::Base ...@@ -189,6 +178,31 @@ class Member < ActiveRecord::Base
private private
def parse_users_list(source, list)
emails, user_ids, users = [], [], []
existing_members = {}
list.each do |item|
case item
when User
users << item
when Integer
user_ids << item
when /\A\d+\Z/
user_ids << item.to_i
when Devise.email_regexp
emails << item
end
end
if user_ids.present?
users.concat(User.where(id: user_ids))
existing_members = source.members_and_requesters.where(user_id: user_ids).index_by(&:user_id)
end
[emails, users, existing_members]
end
# This method is used to find users that have been entered into the "Add members" field. # This method is used to find users that have been entered into the "Add members" field.
# These can be the User objects directly, their IDs, their emails, or new emails to be invited. # These can be the User objects directly, their IDs, their emails, or new emails to be invited.
def retrieve_user(user) def retrieve_user(user)
...@@ -197,6 +211,20 @@ class Member < ActiveRecord::Base ...@@ -197,6 +211,20 @@ class Member < ActiveRecord::Base
User.find_by(id: user) || User.find_by(email: user) || user User.find_by(id: user) || User.find_by(email: user) || user
end end
def retrieve_member(source, user, existing_members)
user = retrieve_user(user)
if user.is_a?(User)
if existing_members
existing_members[user.id] || source.members.build(user_id: user.id)
else
source.members_and_requesters.find_or_initialize_by(user_id: user.id)
end
else
source.members.build(invite_email: user)
end
end
def retrieve_access_level(access_level) def retrieve_access_level(access_level)
access_levels.fetch(access_level) { access_level.to_i } access_levels.fetch(access_level) { access_level.to_i }
end end
......
...@@ -144,6 +144,7 @@ class Project < ActiveRecord::Base ...@@ -144,6 +144,7 @@ class Project < ActiveRecord::Base
has_many :requesters, -> { where.not(requested_at: nil) }, has_many :requesters, -> { where.not(requested_at: nil) },
as: :source, class_name: 'ProjectMember', dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent as: :source, class_name: 'ProjectMember', dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :members_and_requesters, as: :source, class_name: 'ProjectMember'
has_many :deploy_keys_projects has_many :deploy_keys_projects
has_many :deploy_keys, through: :deploy_keys_projects has_many :deploy_keys, through: :deploy_keys_projects
......
- if show_new_nav? && current_user.can_create_group? - if current_user.can_create_group?
- content_for :breadcrumbs_extra do - content_for :breadcrumbs_extra do
= link_to "New group", new_group_path, class: "btn btn-new" = link_to "New group", new_group_path, class: "btn btn-new"
...@@ -10,8 +10,8 @@ ...@@ -10,8 +10,8 @@
= nav_link(page: explore_groups_path) do = nav_link(page: explore_groups_path) do
= link_to explore_groups_path, title: 'Explore public groups' do = link_to explore_groups_path, title: 'Explore public groups' do
Explore public groups Explore public groups
.nav-controls{ class: ("nav-controls-new-nav" if show_new_nav?) } .nav-controls.nav-controls-new-nav
= render 'shared/groups/search_form' = render 'shared/groups/search_form'
= render 'shared/groups/dropdown' = render 'shared/groups/dropdown'
- if current_user.can_create_group? - if current_user.can_create_group?
= link_to "New group", new_group_path, class: "btn btn-new #{("visible-xs" if show_new_nav?)}" = link_to "New group", new_group_path, class: "btn btn-new visible-xs"
= content_for :flash_message do = content_for :flash_message do
= render 'shared/project_limit' = render 'shared/project_limit'
- if show_new_nav? && current_user.can_create_project? - if current_user.can_create_project?
- content_for :breadcrumbs_extra do - content_for :breadcrumbs_extra do
= link_to "New project", new_project_path, class: 'btn btn-new' = link_to "New project", new_project_path, class: 'btn btn-new'
...@@ -19,8 +19,8 @@ ...@@ -19,8 +19,8 @@
= link_to explore_root_path, title: 'Explore', data: {placement: 'right'} do = link_to explore_root_path, title: 'Explore', data: {placement: 'right'} do
Explore projects Explore projects
.nav-controls{ class: ("nav-controls-new-nav" if show_new_nav?) } .nav-controls.nav-controls-new-nav
= render 'shared/projects/search_form' = render 'shared/projects/search_form'
= render 'shared/projects/dropdown' = render 'shared/projects/dropdown'
- if current_user.can_create_project? - if current_user.can_create_project?
= link_to "New project", new_project_path, class: "btn btn-new #{("visible-xs" if show_new_nav?)}" = link_to "New project", new_project_path, class: "btn btn-new visible-xs"
- if show_new_nav? && current_user - if current_user
- content_for :breadcrumbs_extra do - content_for :breadcrumbs_extra do
= link_to "New snippet", new_snippet_path, class: "btn btn-new", title: "New snippet" = link_to "New snippet", new_snippet_path, class: "btn btn-new", title: "New snippet"
...@@ -10,7 +10,3 @@ ...@@ -10,7 +10,3 @@
= nav_link(page: explore_snippets_path) do = nav_link(page: explore_snippets_path) do
= link_to explore_snippets_path, title: 'Explore snippets', data: {placement: 'right'} do = link_to explore_snippets_path, title: 'Explore snippets', data: {placement: 'right'} do
Explore Snippets Explore Snippets
- if current_user
.nav-controls.hidden-xs{ class: ("hidden-sm hidden-md hidden-lg" if show_new_nav?) }
= link_to "New snippet", new_snippet_path, class: "btn btn-new", title: "New snippet"
...@@ -4,15 +4,14 @@ ...@@ -4,15 +4,14 @@
= content_for :meta_tags do = content_for :meta_tags do
= auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{current_user.name} issues") = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{current_user.name} issues")
- if show_new_nav? - content_for :breadcrumbs_extra do
- content_for :breadcrumbs_extra do
= link_to params.merge(rss_url_options), class: 'btn has-tooltip append-right-10', title: 'Subscribe' do = link_to params.merge(rss_url_options), class: 'btn has-tooltip append-right-10', title: 'Subscribe' do
= icon('rss') = icon('rss')
= render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", with_feature_enabled: 'issues', type: :issues = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", with_feature_enabled: 'issues', type: :issues
.top-area .top-area
= render 'shared/issuable/nav', type: :issues = render 'shared/issuable/nav', type: :issues
.nav-controls{ class: ("visible-xs" if show_new_nav?) } .nav-controls.visible-xs
= link_to params.merge(rss_url_options), class: 'btn has-tooltip', title: 'Subscribe' do = link_to params.merge(rss_url_options), class: 'btn has-tooltip', title: 'Subscribe' do
= icon('rss') = icon('rss')
= render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", with_feature_enabled: 'issues', type: :issues = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", with_feature_enabled: 'issues', type: :issues
......
...@@ -2,13 +2,12 @@ ...@@ -2,13 +2,12 @@
- page_title "Merge Requests" - page_title "Merge Requests"
- header_title "Merge Requests", merge_requests_dashboard_path(assignee_id: current_user.id) - header_title "Merge Requests", merge_requests_dashboard_path(assignee_id: current_user.id)
- if show_new_nav? - content_for :breadcrumbs_extra do
- content_for :breadcrumbs_extra do
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", with_feature_enabled: 'merge_requests', type: :merge_requests = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", with_feature_enabled: 'merge_requests', type: :merge_requests
.top-area .top-area
= render 'shared/issuable/nav', type: :merge_requests = render 'shared/issuable/nav', type: :merge_requests
.nav-controls{ class: ("visible-xs" if show_new_nav?) } .nav-controls.visible-xs
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", with_feature_enabled: 'merge_requests', type: :merge_requests = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", with_feature_enabled: 'merge_requests', type: :merge_requests
= render 'shared/issuable/filter', type: :merge_requests = render 'shared/issuable/filter', type: :merge_requests
......
...@@ -2,14 +2,13 @@ ...@@ -2,14 +2,13 @@
- page_title 'Milestones' - page_title 'Milestones'
- header_title 'Milestones', dashboard_milestones_path - header_title 'Milestones', dashboard_milestones_path
- if show_new_nav? - content_for :breadcrumbs_extra do
- content_for :breadcrumbs_extra do
= render 'shared/new_project_item_select', path: 'milestones/new', label: 'New milestone', include_groups: true, type: :milestones = render 'shared/new_project_item_select', path: 'milestones/new', label: 'New milestone', include_groups: true, type: :milestones
.top-area .top-area
= render 'shared/milestones_filter', counts: @milestone_states = render 'shared/milestones_filter', counts: @milestone_states
.nav-controls{ class: ("visible-xs" if show_new_nav?) } .nav-controls.visible-xs
= render 'shared/new_project_item_select', path: 'milestones/new', label: 'New milestone', include_groups: true, type: :milestones = render 'shared/new_project_item_select', path: 'milestones/new', label: 'New milestone', include_groups: true, type: :milestones
.milestones .milestones
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
= webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'filtered_search' = webpack_bundle_tag 'filtered_search'
- if show_new_nav? && group_issues_exists - if group_issues_exists
- content_for :breadcrumbs_extra do - content_for :breadcrumbs_extra do
= link_to params.merge(rss_url_options), class: 'btn btn-default append-right-10' do = link_to params.merge(rss_url_options), class: 'btn btn-default append-right-10' do
= icon('rss') = icon('rss')
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
- if group_issues_exists - if group_issues_exists
.top-area .top-area
= render 'shared/issuable/nav', type: :issues = render 'shared/issuable/nav', type: :issues
.nav-controls{ class: ("visible-xs" if show_new_nav?) } .nav-controls.visible-xs
= link_to params.merge(rss_url_options), class: 'btn' do = link_to params.merge(rss_url_options), class: 'btn' do
= icon('rss') = icon('rss')
%span.icon-label %span.icon-label
......
- page_title 'Labels' - page_title 'Labels'
- if show_new_nav? && can?(current_user, :admin_label, @group) - if can?(current_user, :admin_label, @group)
- content_for :breadcrumbs_extra do - content_for :breadcrumbs_extra do
= link_to "New label", new_group_label_path(@group), class: "btn btn-new" = link_to "New label", new_group_label_path(@group), class: "btn btn-new"
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
.nav-text .nav-text
Labels can be applied to issues and merge requests. Group labels are available for any project within the group. Labels can be applied to issues and merge requests. Group labels are available for any project within the group.
.nav-controls{ class: ("visible-xs" if show_new_nav?) } .nav-controls.visible-xs
- if can?(current_user, :admin_label, @group) - if can?(current_user, :admin_label, @group)
= link_to "New label", new_group_label_path(@group), class: "btn btn-new" = link_to "New label", new_group_label_path(@group), class: "btn btn-new"
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
= webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'filtered_search' = webpack_bundle_tag 'filtered_search'
- if show_new_nav? && current_user - if current_user
- content_for :breadcrumbs_extra do - content_for :breadcrumbs_extra do
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", type: :merge_requests = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", type: :merge_requests
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
.top-area .top-area
= render 'shared/issuable/nav', type: :merge_requests = render 'shared/issuable/nav', type: :merge_requests
- if current_user - if current_user
.nav-controls{ class: ("visible-xs" if show_new_nav?) } .nav-controls.visible-xs
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", type: :merge_requests = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", type: :merge_requests
= render 'shared/issuable/search_bar', type: :merge_requests = render 'shared/issuable/search_bar', type: :merge_requests
......
- page_title "Milestones" - page_title "Milestones"
- if show_new_nav? && can?(current_user, :admin_milestones, @group) - if can?(current_user, :admin_milestones, @group)
- content_for :breadcrumbs_extra do - content_for :breadcrumbs_extra do
= link_to "New milestone", new_group_milestone_path(@group), class: "btn btn-new" = link_to "New milestone", new_group_milestone_path(@group), class: "btn btn-new"
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
.top-area .top-area
= render 'shared/milestones_filter', counts: @milestone_states = render 'shared/milestones_filter', counts: @milestone_states
.nav-controls{ class: ("visible-xs" if show_new_nav?) } .nav-controls.visible-xs
- if can?(current_user, :admin_milestones, @group) - if can?(current_user, :admin_milestones, @group)
= link_to "New milestone", new_group_milestone_path(@group), class: "btn btn-new" = link_to "New milestone", new_group_milestone_path(@group), class: "btn btn-new"
......
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
= stylesheet_link_tag "test", media: "all" if Rails.env.test? = stylesheet_link_tag "test", media: "all" if Rails.env.test?
= stylesheet_link_tag 'performance_bar' if performance_bar_enabled? = stylesheet_link_tag 'performance_bar' if performance_bar_enabled?
- if show_new_nav? // TODO: Combine these 2 stylesheets into application.scss
= stylesheet_link_tag "new_nav", media: "all" = stylesheet_link_tag "new_nav", media: "all"
= stylesheet_link_tag "new_sidebar", media: "all" = stylesheet_link_tag "new_sidebar", media: "all"
......
.page-with-sidebar{ class: page_with_sidebar_class } .page-with-sidebar{ class: page_with_sidebar_class }
- if show_new_nav?
- if defined?(nav) && nav - if defined?(nav) && nav
= render "layouts/nav/#{nav}" = render "layouts/nav/sidebar/#{nav}"
- else .content-wrapper.page-with-new-nav
- if defined?(nav) && nav
.layout-nav
.container-fluid
= render "layouts/nav/#{nav}"
- if content_for?(:sub_nav)
= yield :sub_nav
.content-wrapper{ class: layout_nav_class }
- if show_new_nav?
.mobile-overlay .mobile-overlay
.alert-wrapper .alert-wrapper
= render "layouts/broadcast" = render "layouts/broadcast"
- if show_new_nav? = yield :flash_message
- if content_for?(:new_global_flash)
= yield :new_global_flash
- unless @hide_breadcrumbs - unless @hide_breadcrumbs
= render "layouts/nav/breadcrumbs" = render "layouts/nav/breadcrumbs"
= render "layouts/flash" = render "layouts/flash"
= yield :flash_message
%div{ class: "#{(container_class unless @no_container)} #{@content_class}" } %div{ class: "#{(container_class unless @no_container)} #{@content_class}" }
.content{ id: "content-body" } .content{ id: "content-body" }
= yield = yield
- page_title "Admin Area" - page_title "Admin Area"
- header_title "Admin Area", admin_root_path - header_title "Admin Area", admin_root_path
- if show_new_nav? - nav "admin"
- nav "new_admin_sidebar" - @left_sidebar = true
- @new_sidebar = true
- else
- nav "admin"
= render template: "layouts/application" = render template: "layouts/application"
...@@ -4,10 +4,7 @@ ...@@ -4,10 +4,7 @@
%body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}", find_file: find_file_path } } %body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}", find_file: find_file_path } }
= render "layouts/init_auto_complete" if @gfm_form = render "layouts/init_auto_complete" if @gfm_form
= render 'peek/bar' = render 'peek/bar'
- if show_new_nav? = render "layouts/header/default"
= render "layouts/header/new"
- else
= render "layouts/header/default", title: header_title
= render 'layouts/page', sidebar: sidebar, nav: nav = render 'layouts/page', sidebar: sidebar, nav: nav
= yield :scripts_body = yield :scripts_body
- page_title @group.name - page_title @group.name
- page_description @group.description unless page_description - page_description @group.description unless page_description
- header_title group_title(@group) unless header_title - header_title group_title(@group) unless header_title
- if show_new_nav? - nav "group"
- nav "new_group_sidebar" - @left_sidebar = true
- @new_sidebar = true
- else
- nav "group"
= render template: "layouts/application" = render template: "layouts/application"
%header.navbar.navbar-gitlab{ class: nav_header_class } %header.navbar.navbar-gitlab.navbar-gitlab-new
.navbar-border
%a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content %a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content
.container-fluid .container-fluid
.header-content .header-content
.dropdown.global-dropdown .title-container
%button.global-dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' } %h1.title
%span.sr-only Toggle navigation = link_to root_path, title: 'Dashboard', id: 'logo' do
= icon('bars')
.dropdown-menu-nav.global-dropdown-menu
- if current_user
= render 'layouts/nav/dashboard'
- else
= render 'layouts/nav/explore'
.header-logo
= link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do
= brand_header_logo = brand_header_logo
%span.logo-text.hidden-xs
= render 'shared/logo_type.svg'
.title-container.js-title-container - if current_user
%h1.title{ class: ('initializing' if @has_group_title) }= title = render "layouts/nav/dashboard"
- else
= render "layouts/nav/explore"
.navbar-collapse.collapse .navbar-collapse.collapse
%ul.nav.navbar-nav %ul.nav.navbar-nav
- if current_user
= render 'layouts/header/new_dropdown'
%li.hidden-sm.hidden-xs %li.hidden-sm.hidden-xs
= render 'layouts/search' unless current_controller?(:search) = render 'layouts/search' unless current_controller?(:search)
%li.visible-sm-inline-block.visible-xs-inline-block %li.visible-sm-inline-block.visible-xs-inline-block
= link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('search') = icon('search')
- if current_user - if current_user
- if session[:impersonator_id] %li.user-counter
%li.impersonation = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= link_to admin_impersonation_path, method: :delete, title: "Stop impersonation", aria: { label: 'Stop impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
= icon('user-secret fw')
- if current_user.admin?
%li
= link_to admin_root_path, title: 'Admin area', aria: { label: "Admin area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('wrench fw')
= render 'layouts/header/new_dropdown'
- if Gitlab::Sherlock.enabled?
%li
= link_to sherlock_transactions_path, title: 'Sherlock Transactions',
data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('tachometer fw')
%li
= link_to assigned_issues_dashboard_path, title: 'Issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= custom_icon('issues') = custom_icon('issues')
- issues_count = assigned_issuables_count(:issues) - issues_count = assigned_issuables_count(:issues)
%span.badge.issues-count{ class: ('hidden' if issues_count.zero?) } %span.badge.issues-count{ class: ('hidden' if issues_count.zero?) }
= number_with_delimiter(issues_count) = number_with_delimiter(issues_count)
%li %li.user-counter
= link_to assigned_mrs_dashboard_path, title: 'Merge requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to assigned_mrs_dashboard_path, title: 'Merge requests', class: 'dashboard-shortcuts-merge_requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= custom_icon('mr_bold') = custom_icon('mr_bold')
- merge_requests_count = assigned_issuables_count(:merge_requests) - merge_requests_count = assigned_issuables_count(:merge_requests)
%span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) } %span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) }
= number_with_delimiter(merge_requests_count) = number_with_delimiter(merge_requests_count)
%li %li.user-counter
= link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('check-circle fw') = custom_icon('todo_done')
%span.badge.todos-count{ class: ('hidden' if todos_pending_count.zero?) } %span.badge.todos-count{ class: ('hidden' if todos_pending_count.zero?) }
= todos_count_format(todos_pending_count) = todos_count_format(todos_pending_count)
%li.header-user.dropdown %li.header-user.dropdown
= link_to current_user, class: "header-user-dropdown-toggle", data: { toggle: "dropdown" } do = link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
= image_tag avatar_icon(current_user, 26), width: 26, height: 26, class: "header-user-avatar" = image_tag avatar_icon(current_user, 23), width: 23, height: 23, class: "header-user-avatar"
= icon('caret-down') = custom_icon('caret_down')
.dropdown-menu-nav.dropdown-menu-align-right .dropdown-menu-nav.dropdown-menu-align-right
%ul %ul
%li.current-user %li.current-user
...@@ -74,18 +56,24 @@ ...@@ -74,18 +56,24 @@
= link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username } = link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username }
%li %li
= link_to "Settings", profile_path = link_to "Settings", profile_path
- if current_user
%li
= link_to "Help", help_path
%li.divider %li.divider
%li %li
= link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link" = link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link"
- if session[:impersonator_id]
%li.impersonation
= link_to admin_impersonation_path, class: 'impersonation-btn', method: :delete, title: "Stop impersonation", aria: { label: 'Stop impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
= icon('user-secret')
- else - else
%li %li
%div %div
= link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success' = link_to "Sign in / Register", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in'
%button.navbar-toggle{ type: 'button' } %button.navbar-toggle.hidden-sm.hidden-md.hidden-lg{ type: 'button' }
%span.sr-only Toggle navigation %span.sr-only Toggle navigation
= icon('ellipsis-v') = icon('ellipsis-v', class: 'js-navbar-toggle-right')
= icon('times', class: 'js-navbar-toggle-left')
= yield :header_content
= render 'shared/outdated_browser' = render 'shared/outdated_browser'
%header.navbar.navbar-gitlab.navbar-gitlab-new{ class: nav_header_class }
%a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content
.container-fluid
.header-content
.title-container
%h1.title
= link_to root_path, title: 'Dashboard', id: 'logo' do
= brand_header_logo
%span.logo-text.hidden-xs
= render 'shared/logo_type.svg'
- if current_user
= render "layouts/nav/new_dashboard"
- else
= render "layouts/nav/new_explore"
.navbar-collapse.collapse
%ul.nav.navbar-nav
%li.hidden-sm.hidden-xs
= render 'layouts/search' unless current_controller?(:search)
%li.visible-sm-inline-block.visible-xs-inline-block
= link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('search')
- if current_user
- if session[:impersonator_id]
%li.impersonation
= link_to admin_impersonation_path, method: :delete, title: "Stop impersonation", aria: { label: 'Stop impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
= icon('user-secret fw')
- if current_user.admin?
%li
= link_to admin_root_path, title: 'Admin area', aria: { label: "Admin area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('wrench fw')
= render 'layouts/header/new_dropdown'
- if Gitlab::Sherlock.enabled?
%li
= link_to sherlock_transactions_path, title: 'Sherlock Transactions',
data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('tachometer fw')
%li
= link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= custom_icon('issues')
- issues_count = assigned_issuables_count(:issues)
%span.badge.issues-count{ class: ('hidden' if issues_count.zero?) }
= number_with_delimiter(issues_count)
%li
= link_to assigned_mrs_dashboard_path, title: 'Merge requests', class: 'dashboard-shortcuts-merge_requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= custom_icon('mr_bold')
- merge_requests_count = assigned_issuables_count(:merge_requests)
%span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) }
= number_with_delimiter(merge_requests_count)
%li
= link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('check-circle fw')
%span.badge.todos-count{ class: ('hidden' if todos_pending_count.zero?) }
= todos_count_format(todos_pending_count)
%li.header-user.dropdown
= link_to current_user, class: "header-user-dropdown-toggle", data: { toggle: "dropdown" } do
= image_tag avatar_icon(current_user, 26), width: 26, height: 26, class: "header-user-avatar"
= icon('chevron-down')
.dropdown-menu-nav.dropdown-menu-align-right
%ul
%li.current-user
.user-name.bold
= current_user.name
@#{current_user.username}
%li.divider
%li
= link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username }
%li
= link_to "Settings", profile_path
%li.divider
%li
= link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link"
- else
%li
%div
= link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success'
%button.navbar-toggle.hidden-sm.hidden-md.hidden-lg{ type: 'button' }
%span.sr-only Toggle navigation
= icon('ellipsis-v', class: 'js-navbar-toggle-right')
= icon('times', class: 'js-navbar-toggle-left')
= render 'shared/outdated_browser'
%li.header-new.dropdown %li.header-new.dropdown
= link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip", title: "New...", ref: 'tooltip', aria: { label: "New..." }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body' } do = link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip", title: "New...", ref: 'tooltip', aria: { label: "New..." }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body' } do
- if show_new_nav? = custom_icon('plus_square')
= icon('plus') = custom_icon('caret_down')
= icon('chevron-down')
- else
= icon('plus fw')
= icon('caret-down')
.dropdown-menu-nav.dropdown-menu-align-right .dropdown-menu-nav.dropdown-menu-align-right
%ul %ul
- if @group&.persisted? - if @group&.persisted?
......
= render 'layouts/nav/admin_settings'
.scrolling-tabs-container{ class: nav_control_class }
.fade-left
= icon('angle-left')
.fade-right
= icon('angle-right')
%ul.nav-links.scrolling-tabs
= nav_link(controller: %w(dashboard admin projects users groups builds runners cohorts), html_options: {class: 'home'}) do
= link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do
%span
Overview
= nav_link(controller: %w(conversational_development_index system_info background_jobs logs health_check requests_profiles)) do
= link_to admin_conversational_development_index_path, title: 'Monitoring' do
%span
Monitoring
= nav_link(controller: :broadcast_messages) do
= link_to admin_broadcast_messages_path, title: 'Messages' do
%span
Messages
= nav_link(controller: [:hooks, :hook_logs]) do
= link_to admin_hooks_path, title: 'Hooks' do
%span
System Hooks
= nav_link(controller: :applications) do
= link_to admin_applications_path, title: 'Applications' do
%span
Applications
= nav_link(controller: :abuse_reports) do
= link_to admin_abuse_reports_path, title: "Abuse Reports" do
%span
Abuse Reports
%span.badge.count= number_with_delimiter(AbuseReport.count(:all))
- if akismet_enabled?
= nav_link(controller: :spam_logs) do
= link_to admin_spam_logs_path, title: "Spam Logs" do
%span
Spam Logs
.controls
.dropdown.admin-settings-dropdown
%a.dropdown-new.btn.btn-default{ href: '#', 'data-toggle' => 'dropdown' }
= icon('cog')
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
= nav_link(controller: :deploy_keys) do
= link_to admin_deploy_keys_path, title: 'Deploy Keys' do
%span
Deploy Keys
= nav_link(controller: :services) do
= link_to admin_application_settings_services_path, title: 'Service Templates' do
%span
Service Templates
= nav_link(controller: :labels) do
= link_to admin_labels_path, title: 'Labels' do
%span
Labels
= nav_link(controller: :appearances) do
= link_to admin_appearances_path, title: 'Appearances' do
%span
Appearance
%li.divider
= nav_link(controller: :application_settings) do
= link_to admin_application_settings_path, title: 'Settings' do
%span
Settings
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
- hide_top_links = @hide_top_links || false - hide_top_links = @hide_top_links || false
%nav.breadcrumbs{ role: "navigation", class: [container, @content_class] } %nav.breadcrumbs{ role: "navigation", class: [container, @content_class] }
.breadcrumbs-container .breadcrumbs-container{ class: [container, @content_class] }
- if defined?(@new_sidebar) - if defined?(@left_sidebar)
= button_tag class: 'toggle-mobile-nav', type: 'button' do = button_tag class: 'toggle-mobile-nav', type: 'button' do
%span.sr-only Open sidebar %span.sr-only Open sidebar
= icon ('bars') = icon ('bars')
......
%ul %ul.list-unstyled.navbar-sub-nav
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown" }) do
= link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do %a{ href: "#", data: { toggle: "dropdown" } }
.shortcut-mappings
.key
= icon('arrow-up', 'aria-label' => 'hidden')
P
%span
Projects Projects
= nav_link(path: 'dashboard#activity') do = custom_icon('caret_down')
.dropdown-menu.projects-dropdown-menu
= render "layouts/nav/projects_dropdown/show"
= nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "hidden-xs" }) do
= link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do
Groups
= nav_link(path: 'dashboard#activity', html_options: { class: "visible-lg" }) do
= link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
.shortcut-mappings
.key
= icon('arrow-up', 'aria-label' => 'hidden')
A
%span
Activity Activity
- if koding_enabled?
= nav_link(controller: :koding) do = nav_link(controller: 'dashboard/milestones', html_options: { class: "visible-lg" }) do
= link_to koding_path, title: 'Koding' do = link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones', title: 'Milestones' do
%span Milestones
Koding
= nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do = nav_link(controller: 'dashboard/snippets', html_options: { class: "visible-lg" }) do
= link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do
Snippets
%li.dropdown.hidden-lg
%a{ href: "#", data: { toggle: "dropdown" } }
More
= custom_icon('caret_down')
.dropdown-menu
%ul
= nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "visible-xs" }) do
= link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do = link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do
.shortcut-mappings
.key
= icon('arrow-up', 'aria-label' => 'hidden')
G
%span
Groups Groups
= nav_link(path: 'dashboard#activity') do
= link_to activity_dashboard_path, title: 'Activity' do
Activity
= nav_link(controller: 'dashboard/milestones') do = nav_link(controller: 'dashboard/milestones') do
= link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones', title: 'Milestones' do = link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones', title: 'Milestones' do
.shortcut-mappings
.key
= icon('arrow-up', 'aria-label' => 'hidden')
L
%span
Milestones Milestones
= nav_link(path: 'dashboard#issues') do
= link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do
.shortcut-mappings
.key
= icon('arrow-up', 'aria-label' => 'hidden')
I
%span.badge.pull-right= number_with_delimiter(assigned_issuables_count(:issues))
%span
Issues
= nav_link(path: 'dashboard#merge_requests') do
= link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do
.shortcut-mappings
.key
= icon('arrow-up', 'aria-label' => 'hidden')
M
%span.badge.pull-right= number_with_delimiter(assigned_issuables_count(:merge_requests))
%span
Merge Requests
= nav_link(controller: 'dashboard/snippets') do = nav_link(controller: 'dashboard/snippets') do
= link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do
.shortcut-mappings
.key
= icon('arrow-up', 'aria-label' => 'hidden')
S
%span
Snippets Snippets
%li.divider
-# Shortcut to Dashboard > Projects
%li.hidden
= link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
Projects
- if current_user.admin? || Gitlab::Sherlock.enabled?
%li.line-separator.hidden-xs
- if current_user.admin?
= nav_link(controller: 'admin/dashboard') do
= link_to admin_root_path, class: 'admin-icon', title: 'Admin area', aria: { label: "Admin area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('wrench fw')
- if Gitlab::Sherlock.enabled?
%li %li
= link_to "Help", help_path, title: 'About GitLab CE', class: 'about-gitlab' = link_to sherlock_transactions_path, class: 'admin-icon', title: 'Sherlock Transactions',
data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('tachometer fw')
%ul %ul.list-unstyled.navbar-sub-nav
= nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do = nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do
= link_to explore_root_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do = link_to explore_root_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
.shortcut-mappings
.key
= icon('arrow-up', 'aria-label' => 'hidden')
P
%span
Projects Projects
= nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
= link_to explore_groups_path, title: 'Groups', class: 'dashboard-shortcuts-groups' do = link_to explore_groups_path, title: 'Groups', class: 'dashboard-shortcuts-groups' do
.shortcut-mappings
.key
= icon('arrow-up', 'aria-label' => 'hidden')
G
%span
Groups Groups
= nav_link(controller: :snippets) do = nav_link(controller: :snippets) do
= link_to explore_snippets_path, title: 'Snippets', class: 'dashboard-shortcuts-snippets' do = link_to explore_snippets_path, title: 'Snippets', class: 'dashboard-shortcuts-snippets' do
.shortcut-mappings
.key
= icon('arrow-up', 'aria-label' => 'hidden')
S
%span
Snippets Snippets
%li.divider %li
= nav_link(controller: :help) do = link_to "Help", help_path, title: 'About GitLab CE'
= link_to help_path, title: 'Help' do
%span
Help
.scrolling-tabs-container{ class: nav_control_class }
.fade-left
= icon('angle-left')
.fade-right
= icon('angle-right')
%ul.nav-links.scrolling-tabs
= nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: 'home' }) do
= link_to group_path(@group), title: 'Home' do
%span
Group
= nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do
= link_to issues_group_path(@group), title: 'Issues' do
%span
Issues
- issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute
%span.badge.count= number_with_delimiter(issues.count)
= nav_link(path: 'groups#merge_requests') do
= link_to merge_requests_group_path(@group), title: 'Merge Requests' do
%span
Merge Requests
- merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute
%span.badge.count= number_with_delimiter(merge_requests.count)
= nav_link(path: 'group_members#index') do
= link_to group_group_members_path(@group), title: 'Members' do
%span
Members
- if current_user && can?(current_user, :admin_group, @group)
= nav_link(path: %w[groups#projects groups#edit ci_cd#show]) do
= link_to edit_group_path(@group), title: 'Settings' do
%span
Settings
%ul.list-unstyled.navbar-sub-nav
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown" }) do
%a{ href: '#', title: 'Projects', data: { toggle: 'dropdown' } }
Projects
= icon("chevron-down", class: "dropdown-chevron")
.dropdown-menu.projects-dropdown-menu
= render "layouts/nav/projects_dropdown/show"
= nav_link(controller: ['dashboard/groups', 'explore/groups']) do
= link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do
Groups
= nav_link(path: 'dashboard#activity', html_options: { class: "hidden-xs hidden-sm hidden-md" }) do
= link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
Activity
%li.dropdown
%a{ href: "#", data: { toggle: "dropdown" } }
More
= icon("chevron-down", class: "dropdown-chevron")
.dropdown-menu
%ul
= nav_link(path: 'dashboard#activity', html_options: { class: "visible-xs visible-sm visible-md" }) do
= link_to activity_dashboard_path, title: 'Activity' do
Activity
= nav_link(controller: 'dashboard/milestones') do
= link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones', title: 'Milestones' do
Milestones
= nav_link(controller: 'dashboard/snippets') do
= link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do
Snippets
%li.divider
%li
= link_to "Help", help_path, title: 'About GitLab CE'
-# Shortcut to Dashboard > Projects
%li.hidden
= link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
Projects
%ul.list-unstyled.navbar-sub-nav
= nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do
= link_to explore_root_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
Projects
= nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
= link_to explore_groups_path, title: 'Groups', class: 'dashboard-shortcuts-groups' do
Groups
%li.dropdown
%a{ href: "#", data: { toggle: "dropdown" } }
More
= icon("chevron-down", class: "dropdown-chevron")
.dropdown-menu
%ul
= nav_link(controller: :snippets) do
= link_to explore_snippets_path, title: 'Snippets', class: 'dashboard-shortcuts-snippets' do
Snippets
%li.divider
%li
= link_to "Help", help_path, title: 'About GitLab CE'
.scrolling-tabs-container
.fade-left
= icon('angle-left')
.fade-right
= icon('angle-right')
%ul.nav-links.scrolling-tabs
= nav_link(path: 'profiles#show', html_options: {class: 'home'}) do
= link_to profile_path, title: 'Profile Settings' do
%span
Profile
= nav_link(controller: [:accounts, :two_factor_auths]) do
= link_to profile_account_path, title: 'Account' do
%span
Account
- if current_application_settings.user_oauth_applications?
= nav_link(controller: 'oauth/applications') do
= link_to applications_profile_path, title: 'Applications' do
%span
Applications
= nav_link(controller: :chat_names) do
= link_to profile_chat_names_path, title: 'Chat' do
%span
Chat
= nav_link(controller: :personal_access_tokens) do
= link_to profile_personal_access_tokens_path, title: 'Access Tokens' do
%span
Access Tokens
= nav_link(controller: :emails) do
= link_to profile_emails_path, title: 'Emails' do
%span
Emails
- unless current_user.ldap_user?
= nav_link(controller: :passwords) do
= link_to edit_profile_password_path, title: 'Password' do
%span
Password
= nav_link(controller: :notifications) do
= link_to profile_notifications_path, title: 'Notifications' do
%span
Notifications
= nav_link(controller: :keys) do
= link_to profile_keys_path, title: 'SSH Keys' do
%span
SSH Keys
= nav_link(controller: :gpg_keys) do
= link_to profile_gpg_keys_path, title: 'GPG Keys' do
%span
GPG Keys
= nav_link(controller: :preferences) do
= link_to profile_preferences_path, title: 'Preferences' do
%span
Preferences
= nav_link(path: 'profiles#audit_log') do
= link_to audit_log_profile_path, title: 'Authentication log' do
%span
Authentication log
- can_edit = can?(current_user, :admin_project, @project)
.scrolling-tabs-container{ class: nav_control_class }
.fade-left
= icon('angle-left')
.fade-right
= icon('angle-right')
%ul.nav-links.scrolling-tabs
= nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do
= link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do
%span
Project
- if project_nav_tab? :files
= nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare projects/repositories tags branches releases graphs network)) do
= link_to project_tree_path(@project), title: 'Repository', class: 'shortcuts-tree' do
%span
Repository
- if project_nav_tab? :container_registry
= nav_link(controller: %w[projects/registry/repositories]) do
= link_to project_container_registry_index_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do
%span
Registry
- if project_nav_tab? :issues
= nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do
= link_to project_issues_path(@project), title: 'Issues', class: 'shortcuts-issues' do
%span
Issues
- if @project.issues_enabled?
%span.badge.count.issue_counter
= number_with_delimiter(@project.open_issues_count)
- if project_nav_tab? :merge_requests
- controllers = [:merge_requests, 'projects/merge_requests/conflicts']
- controllers.push(:merge_requests, :labels, :milestones) unless @project.issues_enabled?
= nav_link(controller: controllers) do
= link_to project_merge_requests_path(@project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do
%span
Merge Requests
%span.badge.count.merge_counter.js-merge-counter
= number_with_delimiter(@project.open_merge_requests_count)
- if project_nav_tab? :pipelines
= nav_link(controller: [:pipelines, :builds, :environments, :artifacts]) do
= link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
%span
Pipelines
- if project_nav_tab? :wiki
= nav_link(controller: :wikis) do
= link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do
%span
Wiki
- if project_nav_tab? :snippets
= nav_link(controller: :snippets) do
= link_to project_snippets_path(@project), title: 'Snippets', class: 'shortcuts-snippets' do
%span
Snippets
- if project_nav_tab? :project_members
= nav_link(controller: :project_members) do
= link_to project_project_members_path(@project), title: 'Members', class: 'shortcuts-members' do
%span
Members
- if project_nav_tab? :settings
= nav_link(path: %w[projects#edit members#show integrations#show services#edit repository#show ci_cd#show pages#show]) do
= link_to edit_project_path(@project), title: 'Settings', class: 'shortcuts-tree' do
%span
Settings
-# Shortcut to Project > Activity
%li.hidden
= link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do
%span
Activity
-# Shortcut to Repository > Graph (formerly, Network)
- if project_nav_tab? :network
%li.hidden
= link_to project_network_path(@project, current_ref), title: 'Network', class: 'shortcuts-network' do
Graph
-# Shortcut to Repository > Charts (formerly, top-nav item "Graphs")
- unless @project.empty_repo?
%li.hidden
= link_to charts_project_graph_path(@project, current_ref), title: 'Charts', class: 'shortcuts-repository-charts' do
Charts
-# Shortcut to Issues > New Issue
%li.hidden
= link_to new_project_issue_path(@project), class: 'shortcuts-new-issue' do
Create a new issue
-# Shortcut to Pipelines > Jobs
- if project_nav_tab? :builds
%li.hidden
= link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do
Jobs
-# Shortcut to commits page
- if project_nav_tab? :commits
%li.hidden
= link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do
Commits
-# Shortcut to issue boards
%li.hidden
= link_to 'Issue Boards', project_boards_path(@project), title: 'Issue Boards', class: 'shortcuts-issue-boards'
- page_title "User Settings" - page_title "User Settings"
- header_title "User Settings", profile_path unless header_title - header_title "User Settings", profile_path unless header_title
- sidebar "dashboard" - sidebar "dashboard"
- if show_new_nav? - nav "profile"
- nav "new_profile_sidebar" - @left_sidebar = true
- @new_sidebar = true
- else
- nav "profile"
= render template: "layouts/application" = render template: "layouts/application"
- page_title @project.name_with_namespace - page_title @project.name_with_namespace
- page_description @project.description unless page_description - page_description @project.description unless page_description
- header_title project_title(@project) unless header_title - header_title project_title(@project) unless header_title
- if show_new_nav? - nav "project"
- nav "new_project_sidebar" - @left_sidebar = true
- @new_sidebar = true
- else
- nav "project"
- content_for :project_javascripts do - content_for :project_javascripts do
- project = @target_project || @project - project = @target_project || @project
......
- page_title 'Two-Factor Authentication', 'Account' - page_title 'Two-Factor Authentication', 'Account'
- if show_new_nav? - add_to_breadcrumbs("Account", profile_account_path)
- add_to_breadcrumbs("Account", profile_account_path)
- else
- header_title "Two-Factor Authentication", profile_two_factor_auth_path
- @content_class = "limit-container-width" unless fluid_layout - @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head' = render 'profiles/head'
......
- project = local_assigns.fetch(:project) - project = local_assigns.fetch(:project)
- flash_message_container = show_new_nav? ? :new_global_flash : :flash_message
= content_for flash_message_container do = content_for :flash_message do
= render partial: 'deletion_failed', locals: { project: project } = render partial: 'deletion_failed', locals: { project: project }
- if current_user && can?(current_user, :download_code, project) - if current_user && can?(current_user, :download_code, project)
= render 'shared/no_ssh' = render 'shared/no_ssh'
......
- @no_container = true - @no_container = true
- page_title "Environments" - page_title "Environments"
- add_to_breadcrumbs("Pipelines", project_pipelines_path(@project))
= render "projects/pipelines/head" = render "projects/pipelines/head"
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
......
...@@ -13,15 +13,14 @@ ...@@ -13,15 +13,14 @@
= content_for :meta_tags do = content_for :meta_tags do
= auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@project.name} issues") = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@project.name} issues")
- if show_new_nav? - content_for :breadcrumbs_extra do
- content_for :breadcrumbs_extra do
= render "projects/issues/nav_btns" = render "projects/issues/nav_btns"
- if project_issues(@project).exists? - if project_issues(@project).exists?
%div{ class: (container_class) } %div{ class: (container_class) }
.top-area .top-area
= render 'shared/issuable/nav', type: :issues = render 'shared/issuable/nav', type: :issues
.nav-controls{ class: ("visible-xs" if show_new_nav?) } .nav-controls.visible-xs
= render "projects/issues/nav_btns" = render "projects/issues/nav_btns"
= render 'shared/issuable/search_bar', type: :issues = render 'shared/issuable/search_bar', type: :issues
......
...@@ -39,8 +39,7 @@ ...@@ -39,8 +39,7 @@
%ul %ul
- if can_update_issue - if can_update_issue
%li= link_to 'Edit', edit_project_issue_path(@project, @issue) %li= link_to 'Edit', edit_project_issue_path(@project, @issue)
/ TODO: simplify condition back #36860 - unless current_user == @issue.author
- if @issue.author && current_user != @issue.author
%li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue)) %li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue))
- if can_update_issue - if can_update_issue
%li= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close js-btn-issue-action #{issue_button_visibility(@issue, true)}", title: 'Close issue' %li= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close js-btn-issue-action #{issue_button_visibility(@issue, true)}", title: 'Close issue'
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
- hide_class = '' - hide_class = ''
- can_admin_label = can?(current_user, :admin_label, @project) - can_admin_label = can?(current_user, :admin_label, @project)
- if show_new_nav? && can?(current_user, :admin_label, @project) - if can?(current_user, :admin_label, @project)
- content_for :breadcrumbs_extra do - content_for :breadcrumbs_extra do
= link_to "New label", new_namespace_project_label_path(@project.namespace, @project), class: "btn btn-new" = link_to "New label", new_namespace_project_label_path(@project.namespace, @project), class: "btn btn-new"
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging. Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging.
- if can_admin_label - if can_admin_label
.nav-controls{ class: ("visible-xs" if show_new_nav?) } .nav-controls.visible-xs
= link_to new_project_label_path(@project), class: "btn btn-new" do = link_to new_project_label_path(@project), class: "btn btn-new" do
New label New label
......
...@@ -12,8 +12,7 @@ ...@@ -12,8 +12,7 @@
= webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'filtered_search' = webpack_bundle_tag 'filtered_search'
- if show_new_nav? - content_for :breadcrumbs_extra do
- content_for :breadcrumbs_extra do
= render "projects/merge_requests/nav_btns", merge_project: merge_project, new_merge_request_path: new_merge_request_path = render "projects/merge_requests/nav_btns", merge_project: merge_project, new_merge_request_path: new_merge_request_path
= render 'projects/last_push' = render 'projects/last_push'
...@@ -22,7 +21,7 @@ ...@@ -22,7 +21,7 @@
%div{ class: container_class } %div{ class: container_class }
.top-area .top-area
= render 'shared/issuable/nav', type: :merge_requests = render 'shared/issuable/nav', type: :merge_requests
.nav-controls{ class: ("visible-xs" if show_new_nav?) } .nav-controls.visible-xs
= render "projects/merge_requests/nav_btns", merge_project: merge_project, new_merge_request_path: new_merge_request_path = render "projects/merge_requests/nav_btns", merge_project: merge_project, new_merge_request_path: new_merge_request_path
= render 'shared/issuable/search_bar', type: :merge_requests = render 'shared/issuable/search_bar', type: :merge_requests
......
- @no_container = true - @no_container = true
- page_title 'Milestones' - page_title 'Milestones'
- if show_new_nav? && can?(current_user, :admin_milestone, @project) - if can?(current_user, :admin_milestone, @project)
- content_for :breadcrumbs_extra do - content_for :breadcrumbs_extra do
= link_to "New milestone", new_namespace_project_milestone_path(@project.namespace, @project), class: 'btn btn-new', title: 'New milestone' = link_to "New milestone", new_namespace_project_milestone_path(@project.namespace, @project), class: 'btn btn-new', title: 'New milestone'
...@@ -11,10 +11,10 @@ ...@@ -11,10 +11,10 @@
.top-area .top-area
= render 'shared/milestones_filter', counts: milestone_counts(@project.milestones) = render 'shared/milestones_filter', counts: milestone_counts(@project.milestones)
.nav-controls{ class: ("nav-controls-new-nav" if show_new_nav?) } .nav-controls.nav-controls-new-nav
= render 'shared/milestones_sort_dropdown' = render 'shared/milestones_sort_dropdown'
- if can?(current_user, :admin_milestone, @project) - if can?(current_user, :admin_milestone, @project)
= link_to new_project_milestone_path(@project), class: "btn btn-new #{("visible-xs" if show_new_nav?)}", title: 'New milestone' do = link_to new_project_milestone_path(@project), class: "btn btn-new visible-xs", title: 'New milestone' do
New milestone New milestone
.milestones .milestones
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
- @no_container = true - @no_container = true
- page_title _("Pipeline Schedules") - page_title _("Pipeline Schedules")
- if show_new_nav? && can?(current_user, :create_pipeline_schedule, @project) - if can?(current_user, :create_pipeline_schedule, @project)
- content_for :breadcrumbs_extra do - content_for :breadcrumbs_extra do
= link_to _('New schedule'), new_namespace_project_pipeline_schedule_path(@project.namespace, @project), class: 'btn btn-create' = link_to _('New schedule'), new_namespace_project_pipeline_schedule_path(@project.namespace, @project), class: 'btn btn-create'
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
= render "tabs", schedule_path_proc: schedule_path_proc, all_schedules: @all_schedules, scope: @scope = render "tabs", schedule_path_proc: schedule_path_proc, all_schedules: @all_schedules, scope: @scope
- if can?(current_user, :create_pipeline_schedule, @project) - if can?(current_user, :create_pipeline_schedule, @project)
.nav-controls{ class: ("visible-xs" if show_new_nav?) } .nav-controls.visible-xs
= link_to new_project_pipeline_schedule_path(@project), class: 'btn btn-create' do = link_to new_project_pipeline_schedule_path(@project), class: 'btn btn-create' do
%span= _('New schedule') %span= _('New schedule')
......
...@@ -2,8 +2,7 @@ ...@@ -2,8 +2,7 @@
- @breadcrumb_link = namespace_project_pipeline_schedules_path(@project.namespace, @project) - @breadcrumb_link = namespace_project_pipeline_schedules_path(@project.namespace, @project)
- page_title _("New Pipeline Schedule") - page_title _("New Pipeline Schedule")
- if show_new_nav? - add_to_breadcrumbs("Pipelines", project_pipelines_path(@project))
- add_to_breadcrumbs("Pipelines", project_pipelines_path(@project))
%h3.page-title %h3.page-title
= _("Schedule a new pipeline") = _("Schedule a new pipeline")
......
...@@ -60,8 +60,21 @@ ...@@ -60,8 +60,21 @@
= f.check_box :public_builds = f.check_box :public_builds
%strong Public pipelines %strong Public pipelines
.help-block .help-block
Allow everyone to access pipelines for public and internal projects Allow public access to pipelines and job details, including output logs and artifacts
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'visibility-of-pipelines'), target: '_blank' = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'visibility-of-pipelines'), target: '_blank'
.bs-callout.bs-callout-info
%p If enabled:
%ul
%li
For public projects, anyone can view pipelines and access job details (output logs and artifacts)
%li
For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)
%li
For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts)
%p
If disabled, the access level will depend on the user's
permissions in the project.
%hr %hr
.form-group .form-group
.checkbox .checkbox
......
- breadcrumb_title "Integrations" - breadcrumb_title "Integrations"
- page_title @service.title, "Services" - page_title @service.title, "Services"
- add_to_breadcrumbs("Settings", edit_project_path(@project))
- if show_new_nav?
- add_to_breadcrumbs("Settings", edit_project_path(@project))
= render "projects/settings/head" = render "projects/settings/head"
= render 'form' = render 'form'
- page_title "Snippets" - page_title "Snippets"
- if show_new_nav? && can?(current_user, :create_project_snippet, @project) - if can?(current_user, :create_project_snippet, @project)
- content_for :breadcrumbs_extra do - content_for :breadcrumbs_extra do
= link_to "New snippet", new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New snippet" = link_to "New snippet", new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New snippet"
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
- include_private = @project.team.member?(current_user) || current_user.admin? - include_private = @project.team.member?(current_user) || current_user.admin?
= render partial: 'snippets/snippets_scope_menu', locals: { subject: @project, include_private: include_private } = render partial: 'snippets/snippets_scope_menu', locals: { subject: @project, include_private: include_private }
.nav-controls{ class: ("visible-xs" if show_new_nav?) } .nav-controls.visible-xs
- if can?(current_user, :create_project_snippet, @project) - if can?(current_user, :create_project_snippet, @project)
= link_to "New snippet", new_project_snippet_path(@project), class: "btn btn-new", title: "New snippet" = link_to "New snippet", new_project_snippet_path(@project), class: "btn btn-new", title: "New snippet"
......
- @no_container = true - @no_container = true
- @sort ||= sort_value_recently_updated - @sort ||= sort_value_recently_updated
- page_title "Tags" - page_title "Tags"
- add_to_breadcrumbs("Repository", project_tree_path(@project))
= render "projects/commits/head" = render "projects/commits/head"
.flex-list{ class: container_class } .flex-list{ class: container_class }
......
<svg width="28" height="28" class="tanuki-logo" viewBox="0 0 36 36"> <svg width="24" height="24" class="tanuki-logo" viewBox="0 0 36 36">
<path class="tanuki-shape tanuki-left-ear" fill="#e24329" d="M2 14l9.38 9v-9l-4-12.28c-.205-.632-1.176-.632-1.38 0z"/> <path class="tanuki-shape tanuki-left-ear" fill="#e24329" d="M2 14l9.38 9v-9l-4-12.28c-.205-.632-1.176-.632-1.38 0z"/>
<path class="tanuki-shape tanuki-right-ear" fill="#e24329" d="M34 14l-9.38 9v-9l4-12.28c.205-.632 1.176-.632 1.38 0z"/> <path class="tanuki-shape tanuki-right-ear" fill="#e24329" d="M34 14l-9.38 9v-9l4-12.28c.205-.632 1.176-.632 1.38 0z"/>
<path class="tanuki-shape tanuki-nose" fill="#e24329" d="M18,34.38 3,14 33,14 Z"/> <path class="tanuki-shape tanuki-nose" fill="#e24329" d="M18,34.38 3,14 33,14 Z"/>
......
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" class="caret-down" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8 10.243l-4.95-4.95a1 1 0 0 0-1.414 1.414l5.657 5.657a.997.997 0 0 0 1.414 0l5.657-5.657a1 1 0 0 0-1.414-1.414L8 10.243z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="m5 5.563v4.875c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-4.875c-1.024-.4-1.75-1.397-1.75-2.563 0-1.519 1.231-2.75 2.75-2.75 1.519 0 2.75 1.231 2.75 2.75 0 1.166-.726 2.162-1.75 2.563m-1 8.687c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25m0-10c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/><path d="m10.501 2c1.381.001 2.499 1.125 2.499 2.506v5.931c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-5.931c0-.279-.225-.506-.499-.506v.926c0 .346-.244.474-.569.271l-2.952-1.844c-.314-.196-.325-.507 0-.71l2.952-1.844c.314-.196.569-.081.569.271v.93m1.499 12.25c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/></svg> <svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 16 16"><path d="m5 5.563v4.875c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-4.875c-1.024-.4-1.75-1.397-1.75-2.563 0-1.519 1.231-2.75 2.75-2.75 1.519 0 2.75 1.231 2.75 2.75 0 1.166-.726 2.162-1.75 2.563m-1 8.687c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25m0-10c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/><path d="m10.501 2c1.381.001 2.499 1.125 2.499 2.506v5.931c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-5.931c0-.279-.225-.506-.499-.506v.926c0 .346-.244.474-.569.271l-2.952-1.844c-.314-.196-.325-.507 0-.71l2.952-1.844c.314-.196.569-.081.569.271v.93m1.499 12.25c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M9 7V4c0-.552-.448-1-1-1s-1 .448-1 1v3H4c-.552 0-1 .448-1 1s.448 1 1 1h3v3c0 .552.448 1 1 1s1-.448 1-1V9h3c.552 0 1-.448 1-1s-.448-1-1-1H9zM3 0h10c1.657 0 3 1.343 3 3v10c0 1.657-1.343 3-3 3H3c-1.657 0-3-1.343-3-3V3c0-1.657 1.343-3 3-3z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M8.243 7.485l4.95-4.95a1 1 0 1 1 1.414 1.415L8.95 9.607a.997.997 0 0 1-1.414 0l-2.83-2.83a1 1 0 0 1 1.415-1.413l2.123 2.12zM12 11a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></svg>
...@@ -9,7 +9,6 @@ ...@@ -9,7 +9,6 @@
class: "hidden-xs hidden-sm btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}" class: "hidden-xs hidden-sm btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}"
- elsif can_update && !is_current_user - elsif can_update && !is_current_user
= render 'shared/issuable/close_reopen_report_toggle', issuable: issuable = render 'shared/issuable/close_reopen_report_toggle', issuable: issuable
- elsif issuable.author - else
/ TODO: change back to else #36860
= link_to 'Report abuse', new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)), = link_to 'Report abuse', new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)),
class: 'hidden-xs hidden-sm btn btn-grouped btn-close-color', title: 'Report abuse' class: 'hidden-xs hidden-sm btn btn-grouped btn-close-color', title: 'Report abuse'
...@@ -37,8 +37,6 @@ ...@@ -37,8 +37,6 @@
%li.divider.droplab-item-ignore %li.divider.droplab-item-ignore
/ TODO: remove condition #36860
- if issuable.author
%li.report-item{ data: { text: 'Report abuse', url: new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)), %li.report-item{ data: { text: 'Report abuse', url: new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)),
button_class: "#{button_class} btn-close-color", toggle_class: "#{toggle_class} btn-close-color", method: '' } } button_class: "#{button_class} btn-close-color", toggle_class: "#{toggle_class} btn-close-color", method: '' } }
%button.btn.btn-transparent %button.btn.btn-transparent
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
= page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('sidebar') = page_specific_javascript_bundle_tag('sidebar')
%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { "offset-top" => ("50" unless show_new_nav?), "spy" => ("affix" unless show_new_nav?), signed: { in: current_user.present? } }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } %aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: current_user.present? } }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
.issuable-sidebar{ data: { endpoint: "#{issuable_json_path(issuable)}" } } .issuable-sidebar{ data: { endpoint: "#{issuable_json_path(issuable)}" } }
- can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project) - can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
.block.issuable-sidebar-header .block.issuable-sidebar-header
......
---
title: Update gpg documentation with gpg2
merge_request: 13851
author: M M Arif
type: other
---
title: Migrate issues authored by deleted user to the Ghost user
merge_request:
author:
type: fixed
---
title: 'API: Add GPG key management'
merge_request: 13828
author: Robert Schilling
type: added
---
title: Support a multi-word fuzzy seach issues/merge requests on search bar
merge_request: 13780
author: Hiroyuki Sato
type: changed
---
title: Bump jira-ruby gem to 1.4.1 to fix issues with HTTP proxies
merge_request:
author:
type: fixed
class MigrateIssuesToGhostUser < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
class User < ActiveRecord::Base
self.table_name = 'users'
end
class Issue < ActiveRecord::Base
self.table_name = 'issues'
include ::EachBatch
end
def reset_column_in_migration_models
ActiveRecord::Base.clear_cache!
::User.reset_column_information
end
def up
reset_column_in_migration_models
# we use the model method because rewriting it is too complicated and would require copying multiple methods
ghost_id = ::User.ghost.id
Issue.where('NOT EXISTS (?)', User.unscoped.select(1).where('issues.author_id = users.id')).each_batch do |relation|
relation.update_all(author_id: ghost_id)
end
end
def down
end
end
class AddForeignKeyToIssueAuthor < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_foreign_key(:issues, :users, column: :author_id, on_delete: :nullify)
end
def down
remove_foreign_key(:issues, column: :author_id)
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170831195038) do ActiveRecord::Schema.define(version: 20170901071411) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -1708,6 +1708,7 @@ ActiveRecord::Schema.define(version: 20170831195038) do ...@@ -1708,6 +1708,7 @@ ActiveRecord::Schema.define(version: 20170831195038) do
add_foreign_key "issue_assignees", "users", name: "fk_5e0c8d9154", on_delete: :cascade add_foreign_key "issue_assignees", "users", name: "fk_5e0c8d9154", on_delete: :cascade
add_foreign_key "issue_metrics", "issues", on_delete: :cascade add_foreign_key "issue_metrics", "issues", on_delete: :cascade
add_foreign_key "issues", "projects", name: "fk_899c8f3231", on_delete: :cascade add_foreign_key "issues", "projects", name: "fk_899c8f3231", on_delete: :cascade
add_foreign_key "issues", "users", column: "author_id", name: "fk_05f1e72feb", on_delete: :cascade
add_foreign_key "label_priorities", "labels", on_delete: :cascade add_foreign_key "label_priorities", "labels", on_delete: :cascade
add_foreign_key "label_priorities", "projects", on_delete: :cascade add_foreign_key "label_priorities", "projects", on_delete: :cascade
add_foreign_key "labels", "namespaces", column: "group_id", on_delete: :cascade add_foreign_key "labels", "namespaces", column: "group_id", on_delete: :cascade
......
...@@ -61,16 +61,7 @@ following locations: ...@@ -61,16 +61,7 @@ following locations:
## Road to GraphQL ## Road to GraphQL
Going forward, we will start on moving to We have changed our plans to move to GraphQL. After reviewing the GraphQL license, anything related to the Facebook BSD plus patent license will not be allowed at GitLab.
[GraphQL](http://graphql.org/learn/best-practices/) and deprecate the use of
controller-specific endpoints. GraphQL has a number of benefits:
1. We avoid having to maintain two different APIs.
2. Callers of the API can request only what they need.
3. It is versioned by default.
It will co-exist with the current v4 REST API. If we have a v5 API, this should
be a compatibility layer on top of GraphQL.
## Basic usage ## Basic usage
...@@ -246,8 +237,8 @@ The following table gives an overview of how the API functions generally behave. ...@@ -246,8 +237,8 @@ The following table gives an overview of how the API functions generally behave.
| ------------ | ----------- | | ------------ | ----------- |
| `GET` | Access one or more resources and return the result as JSON. | | `GET` | Access one or more resources and return the result as JSON. |
| `POST` | Return `201 Created` if the resource is successfully created and return the newly created resource as JSON. | | `POST` | Return `201 Created` if the resource is successfully created and return the newly created resource as JSON. |
| `GET` / `PUT` / `DELETE` | Return `200 OK` if the resource is accessed, modified or deleted successfully. The (modified) result is returned as JSON. | | `GET` / `PUT` | Return `200 OK` if the resource is accessed or modified successfully. The (modified) result is returned as JSON. |
| `DELETE` | Designed to be idempotent, meaning a request to a resource still returns `200 OK` even it was deleted before or is not available. The reasoning behind this, is that the user is not really interested if the resource existed before or not. | | `DELETE` | Returns `204 No Content` if the resuource was deleted successfully. |
The following table shows the possible return codes for API requests. The following table shows the possible return codes for API requests.
......
...@@ -94,7 +94,7 @@ Example response: ...@@ -94,7 +94,7 @@ Example response:
## Delete an environment ## Delete an environment
It returns `200` if the environment was successfully deleted, and `404` if the environment does not exist. It returns `204` if the environment was successfully deleted, and `404` if the environment does not exist.
``` ```
DELETE /projects/:id/environments/:environment_id DELETE /projects/:id/environments/:environment_id
......
...@@ -95,8 +95,7 @@ Parameters: ...@@ -95,8 +95,7 @@ Parameters:
## Delete snippet ## Delete snippet
Deletes an existing project snippet. This is an idempotent function and deleting a non-existent Deletes an existing project snippet. This returns a `204 No Content` status code if the operation was successfully or `404` if the resource was not found.
snippet still returns a `200 OK` status code.
``` ```
DELETE /projects/:id/snippets/:snippet_id DELETE /projects/:id/snippets/:snippet_id
......
...@@ -299,10 +299,7 @@ e.g. when renaming the email address to some existing one. ...@@ -299,10 +299,7 @@ e.g. when renaming the email address to some existing one.
## User deletion ## User deletion
Deletes a user. Available only for administrators. Deletes a user. Available only for administrators.
This is an idempotent function, calling this function for a non-existent user id This returns a `204 No Content` status code if the operation was successfully or `404` if the resource was not found.
still returns a status code `200 OK`.
The JSON response differs if the user was actually deleted or not.
In the former the user is returned and in the latter not.
``` ```
DELETE /users/:id DELETE /users/:id
...@@ -524,8 +521,7 @@ Parameters: ...@@ -524,8 +521,7 @@ Parameters:
## Delete SSH key for current user ## Delete SSH key for current user
Deletes key owned by currently authenticated user. Deletes key owned by currently authenticated user.
This is an idempotent function and calling it on a key that is already deleted This returns a `204 No Content` status code if the operation was successfully or `404` if the resource was not found.
or not available results in `200 OK`.
``` ```
DELETE /user/keys/:key_id DELETE /user/keys/:key_id
...@@ -548,7 +544,216 @@ Parameters: ...@@ -548,7 +544,216 @@ Parameters:
- `id` (required) - id of specified user - `id` (required) - id of specified user
- `key_id` (required) - SSH key ID - `key_id` (required) - SSH key ID
Will return `200 OK` on success, or `404 Not found` if either user or key cannot be found. ## List all GPG keys
Get a list of currently authenticated user's GPG keys.
```
GET /user/gpg_keys
```
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/user/gpg_keys
```
Example response:
```json
[
{
"id": 1,
"key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFVjnlIBCACibzXOLCiZiL2oyzYUaTOCkYnSUhymg3pdbfKtd4mpBa58xKBj\r\nt1pTHVpw3Sk03wmzhM/Ndlt1AV2YhLv++83WKr+gAHFYFiCV/tnY8bx3HqvVoy8O\r\nCfxWhw4QZK7+oYzVmJj8ZJm3ZjOC4pzuegNWlNLCUdZDx9OKlHVXLCX1iUbjdYWa\r\nqKV6tdV8hZolkbyjedQgrpvoWyeSHHpwHF7yk4gNJWMMI5rpcssL7i6mMXb/sDzO\r\nVaAtU5wiVducsOa01InRFf7QSTxoAm6Xy0PGv/k48M6xCALa9nY+BzlOv47jUT57\r\nvilf4Szy9dKD0v9S0mQ+IHB+gNukWrnwtXx5ABEBAAHNFm5hbWUgKGNvbW1lbnQp\r\nIDxlbUBpbD7CwHUEEwECACkFAlVjnlIJEINgJNgv009/AhsDAhkBBgsJCAcDAgYV\r\nCAIJCgsEFgIDAQAAxqMIAFBHuBA8P1v8DtHonIK8Lx2qU23t8Mh68HBIkSjk2H7/\r\noO2cDWCw50jZ9D91PXOOyMPvBWV2IE3tARzCvnNGtzEFRtpIEtZ0cuctxeIF1id5\r\ncrfzdMDsmZyRHAOoZ9VtuD6mzj0ybQWMACb7eIHjZDCee3Slh3TVrLy06YRdq2I4\r\nbjMOPePtK5xnIpHGpAXkB3IONxyITpSLKsA4hCeP7gVvm7r7TuQg1ygiUBlWbBYn\r\niE5ROzqZjG1s7dQNZK/riiU2umGqGuwAb2IPvNiyuGR3cIgRE4llXH/rLuUlspAp\r\no4nlxaz65VucmNbN1aMbDXLJVSqR1DuE00vEsL1AItI=\r\n=XQoy\r\n-----END PGP PUBLIC KEY BLOCK-----",
"created_at": "2017-09-05T09:17:46.264Z"
}
]
```
## Get a specific GPG key
Get a specific GPG key of currently authenticated user.
```
GET /user/gpg_keys/:key_id
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `key_id` | integer | yes | The ID of the GPG key |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/user/gpg_keys/1
```
Example response:
```json
{
"id": 1,
"key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFVjnlIBCACibzXOLCiZiL2oyzYUaTOCkYnSUhymg3pdbfKtd4mpBa58xKBj\r\nt1pTHVpw3Sk03wmzhM/Ndlt1AV2YhLv++83WKr+gAHFYFiCV/tnY8bx3HqvVoy8O\r\nCfxWhw4QZK7+oYzVmJj8ZJm3ZjOC4pzuegNWlNLCUdZDx9OKlHVXLCX1iUbjdYWa\r\nqKV6tdV8hZolkbyjedQgrpvoWyeSHHpwHF7yk4gNJWMMI5rpcssL7i6mMXb/sDzO\r\nVaAtU5wiVducsOa01InRFf7QSTxoAm6Xy0PGv/k48M6xCALa9nY+BzlOv47jUT57\r\nvilf4Szy9dKD0v9S0mQ+IHB+gNukWrnwtXx5ABEBAAHNFm5hbWUgKGNvbW1lbnQp\r\nIDxlbUBpbD7CwHUEEwECACkFAlVjnlIJEINgJNgv009/AhsDAhkBBgsJCAcDAgYV\r\nCAIJCgsEFgIDAQAAxqMIAFBHuBA8P1v8DtHonIK8Lx2qU23t8Mh68HBIkSjk2H7/\r\noO2cDWCw50jZ9D91PXOOyMPvBWV2IE3tARzCvnNGtzEFRtpIEtZ0cuctxeIF1id5\r\ncrfzdMDsmZyRHAOoZ9VtuD6mzj0ybQWMACb7eIHjZDCee3Slh3TVrLy06YRdq2I4\r\nbjMOPePtK5xnIpHGpAXkB3IONxyITpSLKsA4hCeP7gVvm7r7TuQg1ygiUBlWbBYn\r\niE5ROzqZjG1s7dQNZK/riiU2umGqGuwAb2IPvNiyuGR3cIgRE4llXH/rLuUlspAp\r\no4nlxaz65VucmNbN1aMbDXLJVSqR1DuE00vEsL1AItI=\r\n=XQoy\r\n-----END PGP PUBLIC KEY BLOCK-----",
"created_at": "2017-09-05T09:17:46.264Z"
}
```
## Add a GPG key
Creates a new GPG key owned by the currently authenticated user.
```
POST /user/gpg_keys
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| key | string | yes | The new GPG key |
```bash
curl --data "key=-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFV..." --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/user/gpg_keys
```
Example response:
```json
[
{
"id": 1,
"key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFVjnlIBCACibzXOLCiZiL2oyzYUaTOCkYnSUhymg3pdbfKtd4mpBa58xKBj\r\nt1pTHVpw3Sk03wmzhM/Ndlt1AV2YhLv++83WKr+gAHFYFiCV/tnY8bx3HqvVoy8O\r\nCfxWhw4QZK7+oYzVmJj8ZJm3ZjOC4pzuegNWlNLCUdZDx9OKlHVXLCX1iUbjdYWa\r\nqKV6tdV8hZolkbyjedQgrpvoWyeSHHpwHF7yk4gNJWMMI5rpcssL7i6mMXb/sDzO\r\nVaAtU5wiVducsOa01InRFf7QSTxoAm6Xy0PGv/k48M6xCALa9nY+BzlOv47jUT57\r\nvilf4Szy9dKD0v9S0mQ+IHB+gNukWrnwtXx5ABEBAAHNFm5hbWUgKGNvbW1lbnQp\r\nIDxlbUBpbD7CwHUEEwECACkFAlVjnlIJEINgJNgv009/AhsDAhkBBgsJCAcDAgYV\r\nCAIJCgsEFgIDAQAAxqMIAFBHuBA8P1v8DtHonIK8Lx2qU23t8Mh68HBIkSjk2H7/\r\noO2cDWCw50jZ9D91PXOOyMPvBWV2IE3tARzCvnNGtzEFRtpIEtZ0cuctxeIF1id5\r\ncrfzdMDsmZyRHAOoZ9VtuD6mzj0ybQWMACb7eIHjZDCee3Slh3TVrLy06YRdq2I4\r\nbjMOPePtK5xnIpHGpAXkB3IONxyITpSLKsA4hCeP7gVvm7r7TuQg1ygiUBlWbBYn\r\niE5ROzqZjG1s7dQNZK/riiU2umGqGuwAb2IPvNiyuGR3cIgRE4llXH/rLuUlspAp\r\no4nlxaz65VucmNbN1aMbDXLJVSqR1DuE00vEsL1AItI=\r\n=XQoy\r\n-----END PGP PUBLIC KEY BLOCK-----",
"created_at": "2017-09-05T09:17:46.264Z"
}
]
```
## Delete a GPG key
Delete a GPG key owned by currently authenticated user.
```
DELETE /user/gpg_keys/:key_id
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `key_id` | integer | yes | The ID of the GPG key |
```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/user/gpg_keys/1
```
Returns `204 No Content` on success, or `404 Not found` if the key cannot be found.
## List all GPG keys for given user
Get a list of a specified user's GPG keys. Available only for admins.
```
GET /users/:id/gpg_keys
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of the user |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/2/gpg_keys
```
Example response:
```json
[
{
"id": 1,
"key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFVjnlIBCACibzXOLCiZiL2oyzYUaTOCkYnSUhymg3pdbfKtd4mpBa58xKBj\r\nt1pTHVpw3Sk03wmzhM/Ndlt1AV2YhLv++83WKr+gAHFYFiCV/tnY8bx3HqvVoy8O\r\nCfxWhw4QZK7+oYzVmJj8ZJm3ZjOC4pzuegNWlNLCUdZDx9OKlHVXLCX1iUbjdYWa\r\nqKV6tdV8hZolkbyjedQgrpvoWyeSHHpwHF7yk4gNJWMMI5rpcssL7i6mMXb/sDzO\r\nVaAtU5wiVducsOa01InRFf7QSTxoAm6Xy0PGv/k48M6xCALa9nY+BzlOv47jUT57\r\nvilf4Szy9dKD0v9S0mQ+IHB+gNukWrnwtXx5ABEBAAHNFm5hbWUgKGNvbW1lbnQp\r\nIDxlbUBpbD7CwHUEEwECACkFAlVjnlIJEINgJNgv009/AhsDAhkBBgsJCAcDAgYV\r\nCAIJCgsEFgIDAQAAxqMIAFBHuBA8P1v8DtHonIK8Lx2qU23t8Mh68HBIkSjk2H7/\r\noO2cDWCw50jZ9D91PXOOyMPvBWV2IE3tARzCvnNGtzEFRtpIEtZ0cuctxeIF1id5\r\ncrfzdMDsmZyRHAOoZ9VtuD6mzj0ybQWMACb7eIHjZDCee3Slh3TVrLy06YRdq2I4\r\nbjMOPePtK5xnIpHGpAXkB3IONxyITpSLKsA4hCeP7gVvm7r7TuQg1ygiUBlWbBYn\r\niE5ROzqZjG1s7dQNZK/riiU2umGqGuwAb2IPvNiyuGR3cIgRE4llXH/rLuUlspAp\r\no4nlxaz65VucmNbN1aMbDXLJVSqR1DuE00vEsL1AItI=\r\n=XQoy\r\n-----END PGP PUBLIC KEY BLOCK-----",
"created_at": "2017-09-05T09:17:46.264Z"
}
]
```
## Get a specific GPG key for a given user
Get a specific GPG key for a given user. Available only for admins.
```
GET /users/:id/gpg_keys/:key_id
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of the user |
| `key_id` | integer | yes | The ID of the GPG key |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/2/gpg_keys/1
```
Example response:
```json
{
"id": 1,
"key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFVjnlIBCACibzXOLCiZiL2oyzYUaTOCkYnSUhymg3pdbfKtd4mpBa58xKBj\r\nt1pTHVpw3Sk03wmzhM/Ndlt1AV2YhLv++83WKr+gAHFYFiCV/tnY8bx3HqvVoy8O\r\nCfxWhw4QZK7+oYzVmJj8ZJm3ZjOC4pzuegNWlNLCUdZDx9OKlHVXLCX1iUbjdYWa\r\nqKV6tdV8hZolkbyjedQgrpvoWyeSHHpwHF7yk4gNJWMMI5rpcssL7i6mMXb/sDzO\r\nVaAtU5wiVducsOa01InRFf7QSTxoAm6Xy0PGv/k48M6xCALa9nY+BzlOv47jUT57\r\nvilf4Szy9dKD0v9S0mQ+IHB+gNukWrnwtXx5ABEBAAHNFm5hbWUgKGNvbW1lbnQp\r\nIDxlbUBpbD7CwHUEEwECACkFAlVjnlIJEINgJNgv009/AhsDAhkBBgsJCAcDAgYV\r\nCAIJCgsEFgIDAQAAxqMIAFBHuBA8P1v8DtHonIK8Lx2qU23t8Mh68HBIkSjk2H7/\r\noO2cDWCw50jZ9D91PXOOyMPvBWV2IE3tARzCvnNGtzEFRtpIEtZ0cuctxeIF1id5\r\ncrfzdMDsmZyRHAOoZ9VtuD6mzj0ybQWMACb7eIHjZDCee3Slh3TVrLy06YRdq2I4\r\nbjMOPePtK5xnIpHGpAXkB3IONxyITpSLKsA4hCeP7gVvm7r7TuQg1ygiUBlWbBYn\r\niE5ROzqZjG1s7dQNZK/riiU2umGqGuwAb2IPvNiyuGR3cIgRE4llXH/rLuUlspAp\r\no4nlxaz65VucmNbN1aMbDXLJVSqR1DuE00vEsL1AItI=\r\n=XQoy\r\n-----END PGP PUBLIC KEY BLOCK-----",
"created_at": "2017-09-05T09:17:46.264Z"
}
```
## Add a GPG key for a given user
Create new GPG key owned by the specified user. Available only for admins.
```
POST /users/:id/gpg_keys
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of the user |
| `key_id` | integer | yes | The ID of the GPG key |
```bash
curl --data "key=-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFV..." --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/2/gpg_keys
```
Example response:
```json
[
{
"id": 1,
"key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFVjnlIBCACibzXOLCiZiL2oyzYUaTOCkYnSUhymg3pdbfKtd4mpBa58xKBj\r\nt1pTHVpw3Sk03wmzhM/Ndlt1AV2YhLv++83WKr+gAHFYFiCV/tnY8bx3HqvVoy8O\r\nCfxWhw4QZK7+oYzVmJj8ZJm3ZjOC4pzuegNWlNLCUdZDx9OKlHVXLCX1iUbjdYWa\r\nqKV6tdV8hZolkbyjedQgrpvoWyeSHHpwHF7yk4gNJWMMI5rpcssL7i6mMXb/sDzO\r\nVaAtU5wiVducsOa01InRFf7QSTxoAm6Xy0PGv/k48M6xCALa9nY+BzlOv47jUT57\r\nvilf4Szy9dKD0v9S0mQ+IHB+gNukWrnwtXx5ABEBAAHNFm5hbWUgKGNvbW1lbnQp\r\nIDxlbUBpbD7CwHUEEwECACkFAlVjnlIJEINgJNgv009/AhsDAhkBBgsJCAcDAgYV\r\nCAIJCgsEFgIDAQAAxqMIAFBHuBA8P1v8DtHonIK8Lx2qU23t8Mh68HBIkSjk2H7/\r\noO2cDWCw50jZ9D91PXOOyMPvBWV2IE3tARzCvnNGtzEFRtpIEtZ0cuctxeIF1id5\r\ncrfzdMDsmZyRHAOoZ9VtuD6mzj0ybQWMACb7eIHjZDCee3Slh3TVrLy06YRdq2I4\r\nbjMOPePtK5xnIpHGpAXkB3IONxyITpSLKsA4hCeP7gVvm7r7TuQg1ygiUBlWbBYn\r\niE5ROzqZjG1s7dQNZK/riiU2umGqGuwAb2IPvNiyuGR3cIgRE4llXH/rLuUlspAp\r\no4nlxaz65VucmNbN1aMbDXLJVSqR1DuE00vEsL1AItI=\r\n=XQoy\r\n-----END PGP PUBLIC KEY BLOCK-----",
"created_at": "2017-09-05T09:17:46.264Z"
}
]
```
## Delete a GPG key for a given user
Delete a GPG key owned by a specified user. Available only for admins.
```
DELETE /users/:id/gpg_keys/:key_id
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of the user |
| `key_id` | integer | yes | The ID of the GPG key |
```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/2/gpg_keys/1
```
## List emails ## List emails
...@@ -654,8 +859,7 @@ Parameters: ...@@ -654,8 +859,7 @@ Parameters:
## Delete email for current user ## Delete email for current user
Deletes email owned by currently authenticated user. Deletes email owned by currently authenticated user.
This is an idempotent function and calling it on a email that is already deleted This returns a `204 No Content` status code if the operation was successfully or `404` if the resource was not found.
or not available results in `200 OK`.
``` ```
DELETE /user/emails/:email_id DELETE /user/emails/:email_id
...@@ -678,8 +882,6 @@ Parameters: ...@@ -678,8 +882,6 @@ Parameters:
- `id` (required) - id of specified user - `id` (required) - id of specified user
- `email_id` (required) - email ID - `email_id` (required) - email ID
Will return `200 OK` on success, or `404 Not found` if either user or email cannot be found.
## Block user ## Block user
Blocks the specified user. Available only for admin. Blocks the specified user. Available only for admin.
......
doc/user/project/issues/img/confidential_issues_system_notes.png

2.28 KB | W: | H:

doc/user/project/issues/img/confidential_issues_system_notes.png

4.12 KB | W: | H:

doc/user/project/issues/img/confidential_issues_system_notes.png
doc/user/project/issues/img/confidential_issues_system_notes.png
doc/user/project/issues/img/confidential_issues_system_notes.png
doc/user/project/issues/img/confidential_issues_system_notes.png
  • 2-up
  • Swipe
  • Onion skin
...@@ -66,10 +66,30 @@ in the pipelines settings page. ...@@ -66,10 +66,30 @@ in the pipelines settings page.
## Visibility of pipelines ## Visibility of pipelines
For public and internal projects, the pipelines page can be accessed by Access to pipelines and job details (including output of logs and artifacts)
anyone and those logged in respectively. If you wish to hide it so that only is checked against your current user access level and the **Public pipelines**
the members of the project or group have access to it, uncheck the **Public project setting.
pipelines** checkbox and save the changes.
If **Public pipelines** is enabled (default):
- for **public** projects, anyone can view the pipelines and access the job details
(output logs and artifacts)
- for **internal** projects, any logged in user can view the pipelines
and access the job details
(output logs and artifacts)
- for **private** projects, any member (guest or higher) can view the pipelines
and access the job details
(output logs and artifacts)
If **Public pipelines** is disabled:
- for **public** projects, anyone can view the pipelines, but only members
(reporter or higher) can access the job details (output logs and artifacts)
- for **internal** projects, any logged in user can view the pipelines,
but only members (reporter or higher) can access the job details (output logs
and artifacts)
- for **private** projects, only members (reporter or higher)
can view the pipelines and access the job details (output logs and artifacts)
## Auto-cancel pending pipelines ## Auto-cancel pending pipelines
......
...@@ -31,6 +31,16 @@ to be met: ...@@ -31,6 +31,16 @@ to be met:
## Generating a GPG key ## Generating a GPG key
>**Notes:**
- If your Operating System has `gpg2` installed, replace `gpg` with `gpg2` in
the following commands.
- If Git is using `gpg` and you get errors like `secret key not available` or
`gpg: signing failed: secret key not available`, run the following command to
change to `gpg2`:
```
git config --global gpg.program gpg2
```
If you don't already have a GPG key, the following steps will help you get If you don't already have a GPG key, the following steps will help you get
started: started:
......
...@@ -40,6 +40,20 @@ The same process is valid for merge requests. Navigate to your project's **Merge ...@@ -40,6 +40,20 @@ The same process is valid for merge requests. Navigate to your project's **Merge
and click **Search or filter results...**. Merge requests can be filtered by author, assignee, and click **Search or filter results...**. Merge requests can be filtered by author, assignee,
milestone, and label. milestone, and label.
### Searching for specific terms
You can filter issues and merge requests by specific terms included in titles or descriptions.
* Syntax
* Searches look for all the words in a query, in any order. E.g.: searching
issues for `display bug` will return all issues matching both those words, in any order.
* To find the exact term, use double quotes: `"display bug"`
* Limitation
* For performance reasons, terms shorter than 3 chars are ignored. E.g.: searching
issues for `included in titles` is same as `included titles`
![filter issues by specific terms](img/issue_search_by_term.png)
### Issues and merge requests per group ### Issues and merge requests per group
Similar to **Issues and merge requests per project**, you can also search for issues Similar to **Issues and merge requests per project**, you can also search for issues
......
Spinach.hooks.before_scenario do
allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(true)
end
...@@ -491,6 +491,10 @@ module API ...@@ -491,6 +491,10 @@ module API
expose :user, using: Entities::UserPublic expose :user, using: Entities::UserPublic
end end
class GPGKey < Grape::Entity
expose :id, :key, :created_at
end
class Note < Grape::Entity class Note < Grape::Entity
# Only Issue and MergeRequest have iid # Only Issue and MergeRequest have iid
NOTEABLE_TYPES_WITH_IID = %w(Issue MergeRequest).freeze NOTEABLE_TYPES_WITH_IID = %w(Issue MergeRequest).freeze
......
...@@ -233,6 +233,86 @@ module API ...@@ -233,6 +233,86 @@ module API
destroy_conditionally!(key) destroy_conditionally!(key)
end end
desc 'Add a GPG key to a specified user. Available only for admins.' do
detail 'This feature was added in GitLab 10.0'
success Entities::GPGKey
end
params do
requires :id, type: Integer, desc: 'The ID of the user'
requires :key, type: String, desc: 'The new GPG key'
end
post ':id/gpg_keys' do
authenticated_as_admin!
user = User.find_by(id: params.delete(:id))
not_found!('User') unless user
key = user.gpg_keys.new(declared_params(include_missing: false))
if key.save
present key, with: Entities::GPGKey
else
render_validation_error!(key)
end
end
desc 'Get the GPG keys of a specified user. Available only for admins.' do
detail 'This feature was added in GitLab 10.0'
success Entities::GPGKey
end
params do
requires :id, type: Integer, desc: 'The ID of the user'
use :pagination
end
get ':id/gpg_keys' do
authenticated_as_admin!
user = User.find_by(id: params[:id])
not_found!('User') unless user
present paginate(user.gpg_keys), with: Entities::GPGKey
end
desc 'Delete an existing GPG key from a specified user. Available only for admins.' do
detail 'This feature was added in GitLab 10.0'
end
params do
requires :id, type: Integer, desc: 'The ID of the user'
requires :key_id, type: Integer, desc: 'The ID of the GPG key'
end
delete ':id/gpg_keys/:key_id' do
authenticated_as_admin!
user = User.find_by(id: params[:id])
not_found!('User') unless user
key = user.gpg_keys.find_by(id: params[:key_id])
not_found!('GPG Key') unless key
status 204
key.destroy
end
desc 'Revokes an existing GPG key from a specified user. Available only for admins.' do
detail 'This feature was added in GitLab 10.0'
end
params do
requires :id, type: Integer, desc: 'The ID of the user'
requires :key_id, type: Integer, desc: 'The ID of the GPG key'
end
post ':id/gpg_keys/:key_id/revoke' do
authenticated_as_admin!
user = User.find_by(id: params[:id])
not_found!('User') unless user
key = user.gpg_keys.find_by(id: params[:key_id])
not_found!('GPG Key') unless key
key.revoke
status :accepted
end
desc 'Add an email address to a specified user. Available only for admins.' do desc 'Add an email address to a specified user. Available only for admins.' do
success Entities::Email success Entities::Email
end end
...@@ -492,6 +572,76 @@ module API ...@@ -492,6 +572,76 @@ module API
destroy_conditionally!(key) destroy_conditionally!(key)
end end
desc "Get the currently authenticated user's GPG keys" do
detail 'This feature was added in GitLab 10.0'
success Entities::GPGKey
end
params do
use :pagination
end
get 'gpg_keys' do
present paginate(current_user.gpg_keys), with: Entities::GPGKey
end
desc 'Get a single GPG key owned by currently authenticated user' do
detail 'This feature was added in GitLab 10.0'
success Entities::GPGKey
end
params do
requires :key_id, type: Integer, desc: 'The ID of the GPG key'
end
get 'gpg_keys/:key_id' do
key = current_user.gpg_keys.find_by(id: params[:key_id])
not_found!('GPG Key') unless key
present key, with: Entities::GPGKey
end
desc 'Add a new GPG key to the currently authenticated user' do
detail 'This feature was added in GitLab 10.0'
success Entities::GPGKey
end
params do
requires :key, type: String, desc: 'The new GPG key'
end
post 'gpg_keys' do
key = current_user.gpg_keys.new(declared_params)
if key.save
present key, with: Entities::GPGKey
else
render_validation_error!(key)
end
end
desc 'Revoke a GPG key owned by currently authenticated user' do
detail 'This feature was added in GitLab 10.0'
end
params do
requires :key_id, type: Integer, desc: 'The ID of the GPG key'
end
post 'gpg_keys/:key_id/revoke' do
key = current_user.gpg_keys.find_by(id: params[:key_id])
not_found!('GPG Key') unless key
key.revoke
status :accepted
end
desc 'Delete a GPG key from the currently authenticated user' do
detail 'This feature was added in GitLab 10.0'
end
params do
requires :key_id, type: Integer, desc: 'The ID of the SSH key'
end
delete 'gpg_keys/:key_id' do
key = current_user.gpg_keys.find_by(id: params[:key_id])
not_found!('GPG Key') unless key
status 204
key.destroy
end
desc "Get the currently authenticated user's email addresses" do desc "Get the currently authenticated user's email addresses" do
success Entities::Email success Entities::Email
end end
......
...@@ -134,9 +134,11 @@ module Gitlab ...@@ -134,9 +134,11 @@ module Gitlab
# This is to work around a bug in libgit2 that causes in-memory refs to # This is to work around a bug in libgit2 that causes in-memory refs to
# be stale/invalid when packed-refs is changed. # be stale/invalid when packed-refs is changed.
# See https://gitlab.com/gitlab-org/gitlab-ce/issues/15392#note_14538333 # See https://gitlab.com/gitlab-org/gitlab-ce/issues/15392#note_14538333
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/474
def find_branch(name, force_reload = false) def find_branch(name, force_reload = false)
gitaly_migrate(:find_branch) do |is_enabled|
if is_enabled
gitaly_ref_client.find_branch(name)
else
reload_rugged if force_reload reload_rugged if force_reload
rugged_ref = rugged.branches[name] rugged_ref = rugged.branches[name]
...@@ -145,6 +147,8 @@ module Gitlab ...@@ -145,6 +147,8 @@ module Gitlab
Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target, target_commit) Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target, target_commit)
end end
end end
end
end
def local_branches(sort_by: nil) def local_branches(sort_by: nil)
gitaly_migrate(:local_branches) do |is_enabled| gitaly_migrate(:local_branches) do |is_enabled|
......
...@@ -78,6 +78,20 @@ module Gitlab ...@@ -78,6 +78,20 @@ module Gitlab
raise ArgumentError, e.message raise ArgumentError, e.message
end end
def find_branch(branch_name)
request = Gitaly::DeleteBranchRequest.new(
repository: @gitaly_repo,
name: GitalyClient.encode(branch_name)
)
response = GitalyClient.call(@repository.storage, :ref_service, :find_branch, request)
branch = response.branch
return unless branch
target_commit = Gitlab::Git::Commit.decorate(@repository, branch.target_commit)
Gitlab::Git::Branch.new(@repository, encode!(branch.name.dup), branch.target_commit.id, target_commit)
end
private private
def consume_refs_response(response) def consume_refs_response(response)
......
...@@ -4,6 +4,7 @@ module Gitlab ...@@ -4,6 +4,7 @@ module Gitlab
extend ActiveSupport::Concern extend ActiveSupport::Concern
MIN_CHARS_FOR_PARTIAL_MATCHING = 3 MIN_CHARS_FOR_PARTIAL_MATCHING = 3
REGEX_QUOTED_WORD = /(?<=^| )"[^"]+"(?= |$)/
class_methods do class_methods do
def to_pattern(query) def to_pattern(query)
...@@ -17,6 +18,28 @@ module Gitlab ...@@ -17,6 +18,28 @@ module Gitlab
def partial_matching?(query) def partial_matching?(query)
query.length >= MIN_CHARS_FOR_PARTIAL_MATCHING query.length >= MIN_CHARS_FOR_PARTIAL_MATCHING
end end
def to_fuzzy_arel(column, query)
words = select_fuzzy_words(query)
matches = words.map { |word| arel_table[column].matches(to_pattern(word)) }
matches.reduce { |result, match| result.and(match) }
end
def select_fuzzy_words(query)
quoted_words = query.scan(REGEX_QUOTED_WORD)
query = quoted_words.reduce(query) { |q, quoted_word| q.sub(quoted_word, '') }
words = query.split(/\s+/)
quoted_words.map! { |quoted_word| quoted_word[1..-2] }
words.concat(quoted_words)
words.select { |word| partial_matching?(word) }
end
end end
end end
end end
......
...@@ -7,26 +7,22 @@ module SystemCheck ...@@ -7,26 +7,22 @@ module SystemCheck
set_skip_reason 'skipped (omnibus-gitlab has no init script)' set_skip_reason 'skipped (omnibus-gitlab has no init script)'
def skip? def skip?
omnibus_gitlab? return true if omnibus_gitlab?
end
def multi_check unless init_file_exists?
recipe_path = Rails.root.join('lib/support/init.d/', 'gitlab') self.skip_reason = "can't check because of previous errors"
unless File.exist?(SCRIPT_PATH) true
$stdout.puts "can't check because of previous errors".color(:magenta) end
return
end end
def check?
recipe_path = Rails.root.join('lib/support/init.d/', 'gitlab')
recipe_content = File.read(recipe_path) recipe_content = File.read(recipe_path)
script_content = File.read(SCRIPT_PATH) script_content = File.read(SCRIPT_PATH)
if recipe_content == script_content recipe_content == script_content
$stdout.puts 'yes'.color(:green)
else
$stdout.puts 'no'.color(:red)
show_error
end
end end
def show_error def show_error
...@@ -38,6 +34,12 @@ module SystemCheck ...@@ -38,6 +34,12 @@ module SystemCheck
) )
fix_and_rerun fix_and_rerun
end end
private
def init_file_exists?
File.exist?(SCRIPT_PATH)
end
end end
end end
end end
...@@ -62,6 +62,25 @@ module SystemCheck ...@@ -62,6 +62,25 @@ module SystemCheck
call_or_return(@skip_reason) || 'skipped' call_or_return(@skip_reason) || 'skipped'
end end
# Define a reason why we skipped the SystemCheck (during runtime)
#
# This is used when you need dynamic evaluation like when you have
# multiple reasons why a check can fail
#
# @param [String] reason to be displayed
def skip_reason=(reason)
@skip_reason = reason
end
# Skip reason defined during runtime
#
# This value have precedence over the one defined in the subclass
#
# @return [String] the reason
def skip_reason
@skip_reason
end
# Does the check support automatically repair routine? # Does the check support automatically repair routine?
# #
# @return [Boolean] whether check implemented `#repair!` method or not # @return [Boolean] whether check implemented `#repair!` method or not
......
module SystemCheck
module IncomingEmail
class ForemanConfiguredCheck < SystemCheck::BaseCheck
set_name 'Foreman configured correctly?'
def check?
path = Rails.root.join('Procfile')
File.exist?(path) && File.read(path) =~ /^mail_room:/
end
def show_error
try_fixing_it(
'Enable mail_room in your Procfile.'
)
for_more_information(
'doc/administration/reply_by_email.md'
)
fix_and_rerun
end
end
end
end
module SystemCheck
module IncomingEmail
class ImapAuthenticationCheck < SystemCheck::BaseCheck
set_name 'IMAP server credentials are correct?'
def check?
if mailbox_config
begin
imap = Net::IMAP.new(config[:host], port: config[:port], ssl: config[:ssl])
imap.starttls if config[:start_tls]
imap.login(config[:email], config[:password])
connected = true
rescue
connected = false
end
end
connected
end
def show_error
try_fixing_it(
'Check that the information in config/gitlab.yml is correct'
)
for_more_information(
'doc/administration/reply_by_email.md'
)
fix_and_rerun
end
private
def mailbox_config
return @config if @config
config_path = Rails.root.join('config', 'mail_room.yml').to_s
erb = ERB.new(File.read(config_path))
erb.filename = config_path
config_file = YAML.load(erb.result)
@config = config_file[:mailboxes]&.first
end
end
end
end
module SystemCheck
module IncomingEmail
class InitdConfiguredCheck < SystemCheck::BaseCheck
set_name 'Init.d configured correctly?'
def skip?
omnibus_gitlab?
end
def check?
mail_room_configured?
end
def show_error
try_fixing_it(
'Enable mail_room in the init.d configuration.'
)
for_more_information(
'doc/administration/reply_by_email.md'
)
fix_and_rerun
end
private
def mail_room_configured?
path = '/etc/default/gitlab'
File.exist?(path) && File.read(path).include?('mail_room_enabled=true')
end
end
end
end
module SystemCheck
module IncomingEmail
class MailRoomRunningCheck < SystemCheck::BaseCheck
set_name 'MailRoom running?'
def skip?
return true if omnibus_gitlab?
unless mail_room_configured?
self.skip_reason = "can't check because of previous errors"
true
end
end
def check?
mail_room_running?
end
def show_error
try_fixing_it(
sudo_gitlab('RAILS_ENV=production bin/mail_room start')
)
for_more_information(
see_installation_guide_section('Install Init Script'),
'see log/mail_room.log for possible errors'
)
fix_and_rerun
end
private
def mail_room_configured?
path = '/etc/default/gitlab'
File.exist?(path) && File.read(path).include?('mail_room_enabled=true')
end
def mail_room_running?
ps_ux, _ = Gitlab::Popen.popen(%w(ps uxww))
ps_ux.include?("mail_room")
end
end
end
end
...@@ -23,7 +23,7 @@ module SystemCheck ...@@ -23,7 +23,7 @@ module SystemCheck
# #
# @param [BaseCheck] check class # @param [BaseCheck] check class
def <<(check) def <<(check)
raise ArgumentError unless check < BaseCheck raise ArgumentError unless check.is_a?(Class) && check < BaseCheck
@checks << check @checks << check
end end
...@@ -48,7 +48,7 @@ module SystemCheck ...@@ -48,7 +48,7 @@ module SystemCheck
# When implements skip method, we run it first, and if true, skip the check # When implements skip method, we run it first, and if true, skip the check
if check.can_skip? && check.skip? if check.can_skip? && check.skip?
$stdout.puts check_klass.skip_reason.color(:magenta) $stdout.puts check.skip_reason.try(:color, :magenta) || check_klass.skip_reason.color(:magenta)
return return
end end
......
...@@ -309,132 +309,23 @@ namespace :gitlab do ...@@ -309,132 +309,23 @@ namespace :gitlab do
desc "GitLab | Check the configuration of Reply by email" desc "GitLab | Check the configuration of Reply by email"
task check: :environment do task check: :environment do
warn_user_is_not_gitlab warn_user_is_not_gitlab
start_checking "Reply by email"
if Gitlab.config.incoming_email.enabled if Gitlab.config.incoming_email.enabled
check_imap_authentication checks = [
SystemCheck::IncomingEmail::ImapAuthenticationCheck
]
if Rails.env.production? if Rails.env.production?
check_initd_configured_correctly checks << SystemCheck::IncomingEmail::InitdConfiguredCheck
check_mail_room_running checks << SystemCheck::IncomingEmail::MailRoomRunningCheck
else
check_foreman_configured_correctly
end
else
puts 'Reply by email is disabled in config/gitlab.yml'
end
finished_checking "Reply by email"
end
# Checks
########################
def check_initd_configured_correctly
return if omnibus_gitlab?
print "Init.d configured correctly? ... "
path = "/etc/default/gitlab"
if File.exist?(path) && File.read(path).include?("mail_room_enabled=true")
puts "yes".color(:green)
else
puts "no".color(:red)
try_fixing_it(
"Enable mail_room in the init.d configuration."
)
for_more_information(
"doc/administration/reply_by_email.md"
)
fix_and_rerun
end
end
def check_foreman_configured_correctly
print "Foreman configured correctly? ... "
path = Rails.root.join("Procfile")
if File.exist?(path) && File.read(path) =~ /^mail_room:/
puts "yes".color(:green)
else
puts "no".color(:red)
try_fixing_it(
"Enable mail_room in your Procfile."
)
for_more_information(
"doc/administration/reply_by_email.md"
)
fix_and_rerun
end
end
def check_mail_room_running
return if omnibus_gitlab?
print "MailRoom running? ... "
path = "/etc/default/gitlab"
unless File.exist?(path) && File.read(path).include?("mail_room_enabled=true")
puts "can't check because of previous errors".color(:magenta)
return
end
if mail_room_running?
puts "yes".color(:green)
else else
puts "no".color(:red) checks << SystemCheck::IncomingEmail::ForemanConfiguredCheck
try_fixing_it(
sudo_gitlab("RAILS_ENV=production bin/mail_room start")
)
for_more_information(
see_installation_guide_section("Install Init Script"),
"see log/mail_room.log for possible errors"
)
fix_and_rerun
end
end end
def check_imap_authentication SystemCheck.run('Reply by email', checks)
print "IMAP server credentials are correct? ... "
config_path = Rails.root.join('config', 'mail_room.yml').to_s
erb = ERB.new(File.read(config_path))
erb.filename = config_path
config_file = YAML.load(erb.result)
config = config_file[:mailboxes].first
if config
begin
imap = Net::IMAP.new(config[:host], port: config[:port], ssl: config[:ssl])
imap.starttls if config[:start_tls]
imap.login(config[:email], config[:password])
connected = true
rescue
connected = false
end
end
if connected
puts "yes".color(:green)
else else
puts "no".color(:red) puts 'Reply by email is disabled in config/gitlab.yml'
try_fixing_it(
"Check that the information in config/gitlab.yml is correct"
)
for_more_information(
"doc/administration/reply_by_email.md"
)
fix_and_rerun
end
end end
def mail_room_running?
ps_ux, _ = Gitlab::Popen.popen(%w(ps uxww))
ps_ux.include?("mail_room")
end end
end end
......
...@@ -8,8 +8,8 @@ describe 'Issue Boards add issue modal', :js do ...@@ -8,8 +8,8 @@ describe 'Issue Boards add issue modal', :js do
let!(:label) { create(:label, project: project) } let!(:label) { create(:label, project: project) }
let!(:list1) { create(:list, board: board, label: planning, position: 0) } let!(:list1) { create(:list, board: board, label: planning, position: 0) }
let!(:list2) { create(:list, board: board, label: label, position: 1) } let!(:list2) { create(:list, board: board, label: label, position: 1) }
let!(:issue) { create(:issue, project: project) } let!(:issue) { create(:issue, project: project, title: 'abc', description: 'def') }
let!(:issue2) { create(:issue, project: project) } let!(:issue2) { create(:issue, project: project, title: 'hij', description: 'klm') }
before do before do
project.team << [user, :master] project.team << [user, :master]
......
...@@ -73,15 +73,15 @@ describe 'Issue Boards', js: true do ...@@ -73,15 +73,15 @@ describe 'Issue Boards', js: true do
let!(:list2) { create(:list, board: board, label: development, position: 1) } let!(:list2) { create(:list, board: board, label: development, position: 1) }
let!(:confidential_issue) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning], relative_position: 9) } let!(:confidential_issue) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning], relative_position: 9) }
let!(:issue1) { create(:labeled_issue, project: project, assignees: [user], labels: [planning], relative_position: 8) } let!(:issue1) { create(:labeled_issue, project: project, title: 'aaa', description: '111', assignees: [user], labels: [planning], relative_position: 8) }
let!(:issue2) { create(:labeled_issue, project: project, author: user2, labels: [planning], relative_position: 7) } let!(:issue2) { create(:labeled_issue, project: project, title: 'bbb', description: '222', author: user2, labels: [planning], relative_position: 7) }
let!(:issue3) { create(:labeled_issue, project: project, labels: [planning], relative_position: 6) } let!(:issue3) { create(:labeled_issue, project: project, title: 'ccc', description: '333', labels: [planning], relative_position: 6) }
let!(:issue4) { create(:labeled_issue, project: project, labels: [planning], relative_position: 5) } let!(:issue4) { create(:labeled_issue, project: project, title: 'ddd', description: '444', labels: [planning], relative_position: 5) }
let!(:issue5) { create(:labeled_issue, project: project, labels: [planning], milestone: milestone, relative_position: 4) } let!(:issue5) { create(:labeled_issue, project: project, title: 'eee', description: '555', labels: [planning], milestone: milestone, relative_position: 4) }
let!(:issue6) { create(:labeled_issue, project: project, labels: [planning, development], relative_position: 3) } let!(:issue6) { create(:labeled_issue, project: project, title: 'fff', description: '666', labels: [planning, development], relative_position: 3) }
let!(:issue7) { create(:labeled_issue, project: project, labels: [development], relative_position: 2) } let!(:issue7) { create(:labeled_issue, project: project, title: 'ggg', description: '777', labels: [development], relative_position: 2) }
let!(:issue8) { create(:closed_issue, project: project) } let!(:issue8) { create(:closed_issue, project: project, title: 'hhh', description: '888') }
let!(:issue9) { create(:labeled_issue, project: project, labels: [planning, testing, bug, accepting], relative_position: 1) } let!(:issue9) { create(:labeled_issue, project: project, title: 'iii', description: '999', labels: [planning, testing, bug, accepting], relative_position: 1) }
before do before do
visit project_board_path(project, board) visit project_board_path(project, board)
......
...@@ -40,18 +40,4 @@ feature 'Issue Detail', :js do ...@@ -40,18 +40,4 @@ feature 'Issue Detail', :js do
end end
end end
end end
context 'when authored by a user who is later deleted' do
before do
issue.update_attribute(:author_id, nil)
sign_in(user)
visit project_issue_path(project, issue)
end
it 'shows the issue' do
page.within('.issuable-details') do
expect(find('h2')).to have_content(issue.title)
end
end
end
end end
...@@ -4,7 +4,10 @@ import '~/gl_dropdown'; ...@@ -4,7 +4,10 @@ import '~/gl_dropdown';
import '~/lib/utils/common_utils'; import '~/lib/utils/common_utils';
import '~/lib/utils/url_utility'; import '~/lib/utils/url_utility';
(() => { describe('glDropdown', function describeDropdown() {
preloadFixtures('static/gl_dropdown.html.raw');
loadJSONFixtures('projects.json');
const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link'; const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link';
const SEARCH_INPUT_SELECTOR = '.dropdown-input-field'; const SEARCH_INPUT_SELECTOR = '.dropdown-input-field';
const ITEM_SELECTOR = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES})`; const ITEM_SELECTOR = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES})`;
...@@ -39,10 +42,6 @@ import '~/lib/utils/url_utility'; ...@@ -39,10 +42,6 @@ import '~/lib/utils/url_utility';
remoteCallback = callback.bind({}, data); remoteCallback = callback.bind({}, data);
}; };
describe('Dropdown', function describeDropdown() {
preloadFixtures('static/gl_dropdown.html.raw');
loadJSONFixtures('projects.json');
function initDropDown(hasRemote, isFilterable, extraOpts = {}) { function initDropDown(hasRemote, isFilterable, extraOpts = {}) {
const options = Object.assign({ const options = Object.assign({
selectable: true, selectable: true,
...@@ -221,5 +220,39 @@ import '~/lib/utils/url_utility'; ...@@ -221,5 +220,39 @@ import '~/lib/utils/url_utility';
.trigger('focus'); .trigger('focus');
expect($searchInput.val()).toEqual('g'); expect($searchInput.val()).toEqual('g');
}); });
describe('renderItem', () => {
describe('without selected value', () => {
let dropdown;
beforeEach(() => {
const dropdownOptions = {
};
const $dropdownDiv = $('<div />');
$dropdownDiv.glDropdown(dropdownOptions);
dropdown = $dropdownDiv.data('glDropdown');
});
it('marks items without ID as active', () => {
const dummyData = { };
const html = dropdown.renderItem(dummyData, null, null);
const link = html.querySelector('a');
expect(link).toHaveClass('is-active');
});
it('does not mark items with ID as active', () => {
const dummyData = {
id: 'ea'
};
const html = dropdown.renderItem(dummyData, null, null);
const link = html.querySelector('a');
expect(link).not.toHaveClass('is-active');
});
});
}); });
})(); });
import Vue from 'vue';
import GraphRow from '~/monitoring/components/graph_row.vue';
import MonitoringMixins from '~/monitoring/mixins/monitoring_mixins';
import { deploymentData, convertDatesMultipleSeries, singleRowMetricsMultipleSeries } from './mock_data';
const createComponent = (propsData) => {
const Component = Vue.extend(GraphRow);
return new Component({
propsData,
}).$mount();
};
const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries);
describe('GraphRow', () => {
beforeEach(() => {
spyOn(MonitoringMixins.methods, 'formatDeployments').and.returnValue({});
});
describe('Computed props', () => {
it('bootstrapClass is set to col-md-6 when rowData is higher/equal to 2', () => {
const component = createComponent({
rowData: convertedMetrics,
updateAspectRatio: false,
deploymentData,
});
expect(component.bootstrapClass).toEqual('col-md-6');
});
it('bootstrapClass is set to col-md-12 when rowData is lower than 2', () => {
const component = createComponent({
rowData: [convertedMetrics[0]],
updateAspectRatio: false,
deploymentData,
});
expect(component.bootstrapClass).toEqual('col-md-12');
});
});
it('has one column', () => {
const component = createComponent({
rowData: convertedMetrics,
updateAspectRatio: false,
deploymentData,
});
expect(component.$el.querySelectorAll('.prometheus-svg-container').length)
.toEqual(component.rowData.length);
});
it('has two columns', () => {
const component = createComponent({
rowData: convertedMetrics,
updateAspectRatio: false,
deploymentData,
});
expect(component.$el.querySelectorAll('.col-md-6').length)
.toEqual(component.rowData.length);
});
});
...@@ -5,10 +5,10 @@ describe('MonitoringStore', () => { ...@@ -5,10 +5,10 @@ describe('MonitoringStore', () => {
this.store = new MonitoringStore(); this.store = new MonitoringStore();
this.store.storeMetrics(MonitoringMock.data); this.store.storeMetrics(MonitoringMock.data);
it('contains one group that contains two queries sorted by priority in one row', () => { it('contains one group that contains two queries sorted by priority', () => {
expect(this.store.groups).toBeDefined(); expect(this.store.groups).toBeDefined();
expect(this.store.groups.length).toEqual(1); expect(this.store.groups.length).toEqual(1);
expect(this.store.groups[0].metrics.length).toEqual(1); expect(this.store.groups[0].metrics.length).toEqual(2);
}); });
it('gets the metrics count for every group', () => { it('gets the metrics count for every group', () => {
......
...@@ -916,6 +916,7 @@ describe Gitlab::Git::Repository, seed_helper: true do ...@@ -916,6 +916,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
end end
describe '#find_branch' do describe '#find_branch' do
shared_examples 'finding a branch' do
it 'should return a Branch for master' do it 'should return a Branch for master' do
branch = repository.find_branch('master') branch = repository.find_branch('master')
...@@ -928,6 +929,14 @@ describe Gitlab::Git::Repository, seed_helper: true do ...@@ -928,6 +929,14 @@ describe Gitlab::Git::Repository, seed_helper: true do
expect(branch).to eq(nil) expect(branch).to eq(nil)
end end
end
context 'when Gitaly find_branch feature is enabled' do
it_behaves_like 'finding a branch'
end
context 'when Gitaly find_branch feature is disabled', skip_gitaly_mock: true do
it_behaves_like 'finding a branch'
it 'should reload Rugged::Repository and return master' do it 'should reload Rugged::Repository and return master' do
expect(Rugged::Repository).to receive(:new).twice.and_call_original expect(Rugged::Repository).to receive(:new).twice.and_call_original
...@@ -939,6 +948,7 @@ describe Gitlab::Git::Repository, seed_helper: true do ...@@ -939,6 +948,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
expect(branch.name).to eq('master') expect(branch.name).to eq('master')
end end
end end
end
describe '#ref_name_for_sha' do describe '#ref_name_for_sha' do
let(:ref_path) { 'refs/heads' } let(:ref_path) { 'refs/heads' }
......
...@@ -264,6 +264,7 @@ project: ...@@ -264,6 +264,7 @@ project:
- statistics - statistics
- container_repositories - container_repositories
- uploads - uploads
- members_and_requesters
award_emoji: award_emoji:
- awardable - awardable
- user - user
......
...@@ -52,4 +52,124 @@ describe Gitlab::SQL::Pattern do ...@@ -52,4 +52,124 @@ describe Gitlab::SQL::Pattern do
end end
end end
end end
describe '.select_fuzzy_words' do
subject(:select_fuzzy_words) { Issue.select_fuzzy_words(query) }
context 'with a word equal to 3 chars' do
let(:query) { 'foo' }
it 'returns array cotaining a word' do
expect(select_fuzzy_words).to match_array(['foo'])
end
end
context 'with a word shorter than 3 chars' do
let(:query) { 'fo' }
it 'returns empty array' do
expect(select_fuzzy_words).to match_array([])
end
end
context 'with two words both equal to 3 chars' do
let(:query) { 'foo baz' }
it 'returns array containing two words' do
expect(select_fuzzy_words).to match_array(%w[foo baz])
end
end
context 'with two words divided by two spaces both equal to 3 chars' do
let(:query) { 'foo baz' }
it 'returns array containing two words' do
expect(select_fuzzy_words).to match_array(%w[foo baz])
end
end
context 'with two words equal to 3 chars and shorter than 3 chars' do
let(:query) { 'foo ba' }
it 'returns array containing a word' do
expect(select_fuzzy_words).to match_array(['foo'])
end
end
context 'with a multi-word surrounded by double quote' do
let(:query) { '"really bar"' }
it 'returns array containing a multi-word' do
expect(select_fuzzy_words).to match_array(['really bar'])
end
end
context 'with a multi-word surrounded by double quote and two words' do
let(:query) { 'foo "really bar" baz' }
it 'returns array containing a multi-word and tow words' do
expect(select_fuzzy_words).to match_array(['foo', 'really bar', 'baz'])
end
end
context 'with a multi-word surrounded by double quote missing a spece before the first double quote' do
let(:query) { 'foo"really bar"' }
it 'returns array containing two words with double quote' do
expect(select_fuzzy_words).to match_array(['foo"really', 'bar"'])
end
end
context 'with a multi-word surrounded by double quote missing a spece after the second double quote' do
let(:query) { '"really bar"baz' }
it 'returns array containing two words with double quote' do
expect(select_fuzzy_words).to match_array(['"really', 'bar"baz'])
end
end
context 'with two multi-word surrounded by double quote and two words' do
let(:query) { 'foo "really bar" baz "awesome feature"' }
it 'returns array containing two multi-words and tow words' do
expect(select_fuzzy_words).to match_array(['foo', 'really bar', 'baz', 'awesome feature'])
end
end
end
describe '.to_fuzzy_arel' do
subject(:to_fuzzy_arel) { Issue.to_fuzzy_arel(:title, query) }
context 'with a word equal to 3 chars' do
let(:query) { 'foo' }
it 'returns a single ILIKE condition' do
expect(to_fuzzy_arel.to_sql).to match(/title.*I?LIKE '\%foo\%'/)
end
end
context 'with a word shorter than 3 chars' do
let(:query) { 'fo' }
it 'returns nil' do
expect(to_fuzzy_arel).to be_nil
end
end
context 'with two words both equal to 3 chars' do
let(:query) { 'foo baz' }
it 'returns a joining LIKE condition using a AND' do
expect(to_fuzzy_arel.to_sql).to match(/title.+I?LIKE '\%foo\%' AND .*title.*I?LIKE '\%baz\%'/)
end
end
context 'with a multi-word surrounded by double quote and two words' do
let(:query) { 'foo "really bar" baz' }
it 'returns a joining LIKE condition using a AND' do
expect(to_fuzzy_arel.to_sql).to match(/title.+I?LIKE '\%foo\%' AND .*title.*I?LIKE '\%baz\%' AND .*title.*I?LIKE '\%really bar\%'/)
end
end
end
end end
...@@ -35,6 +35,20 @@ describe SystemCheck::SimpleExecutor do ...@@ -35,6 +35,20 @@ describe SystemCheck::SimpleExecutor do
end end
end end
class DynamicSkipCheck < SystemCheck::BaseCheck
set_name 'dynamic skip check'
set_skip_reason 'this is a skip reason'
def skip?
self.skip_reason = 'this is a dynamic skip reason'
true
end
def check?
raise 'should not execute this'
end
end
class MultiCheck < SystemCheck::BaseCheck class MultiCheck < SystemCheck::BaseCheck
set_name 'multi check' set_name 'multi check'
...@@ -127,6 +141,10 @@ describe SystemCheck::SimpleExecutor do ...@@ -127,6 +141,10 @@ describe SystemCheck::SimpleExecutor do
expect(subject.checks.size).to eq(1) expect(subject.checks.size).to eq(1)
end end
it 'errors out when passing multiple items' do
expect { subject << [SimpleCheck, OtherCheck] }.to raise_error(ArgumentError)
end
end end
subject { described_class.new('Test') } subject { described_class.new('Test') }
...@@ -205,10 +223,14 @@ describe SystemCheck::SimpleExecutor do ...@@ -205,10 +223,14 @@ describe SystemCheck::SimpleExecutor do
subject.run_check(SkipCheck) subject.run_check(SkipCheck)
end end
it 'displays #skip_reason' do it 'displays .skip_reason' do
expect { subject.run_check(SkipCheck) }.to output(/this is a skip reason/).to_stdout expect { subject.run_check(SkipCheck) }.to output(/this is a skip reason/).to_stdout
end end
it 'displays #skip_reason' do
expect { subject.run_check(DynamicSkipCheck) }.to output(/this is a dynamic skip reason/).to_stdout
end
it 'does not execute #check when #skip? is true' do it 'does not execute #check when #skip? is true' do
expect_any_instance_of(SkipCheck).not_to receive(:check?) expect_any_instance_of(SkipCheck).not_to receive(:check?)
......
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20170825104051_migrate_issues_to_ghost_user.rb')
describe MigrateIssuesToGhostUser, :migration do
describe '#up' do
let(:projects) { table(:projects) }
let(:issues) { table(:issues) }
let(:users) { table(:users) }
before do
projects.create!(name: 'gitlab')
user = users.create(email: 'test@example.com')
issues.create(title: 'Issue 1', author_id: nil, project_id: 1)
issues.create(title: 'Issue 2', author_id: user.id, project_id: 1)
end
context 'when ghost user exists' do
let!(:ghost) { users.create(ghost: true, email: 'ghost@example.com') }
it 'does not create a new user' do
expect { schema_migrate_up! }.not_to change { User.count }
end
it 'migrates issues where author = nil to the ghost user' do
schema_migrate_up!
expect(issues.first.reload.author_id).to eq(ghost.id)
end
it 'does not change issues authored by an existing user' do
expect { schema_migrate_up! }.not_to change { issues.second.reload.author_id}
end
end
context 'when ghost user does not exist' do
it 'creates a new user' do
expect { schema_migrate_up! }.to change { User.count }.by(1)
end
it 'migrates issues where author = nil to the ghost user' do
schema_migrate_up!
expect(issues.first.reload.author_id).to eq(User.ghost.id)
end
it 'does not change issues authored by an existing user' do
expect { schema_migrate_up! }.not_to change { issues.second.reload.author_id}
end
end
end
end
...@@ -66,56 +66,76 @@ describe Issuable do ...@@ -66,56 +66,76 @@ describe Issuable do
end end
describe ".search" do describe ".search" do
let!(:searchable_issue) { create(:issue, title: "Searchable issue") } let!(:searchable_issue) { create(:issue, title: "Searchable awesome issue") }
it 'returns notes with a matching title' do it 'returns issues with a matching title' do
expect(issuable_class.search(searchable_issue.title)) expect(issuable_class.search(searchable_issue.title))
.to eq([searchable_issue]) .to eq([searchable_issue])
end end
it 'returns notes with a partially matching title' do it 'returns issues with a partially matching title' do
expect(issuable_class.search('able')).to eq([searchable_issue]) expect(issuable_class.search('able')).to eq([searchable_issue])
end end
it 'returns notes with a matching title regardless of the casing' do it 'returns issues with a matching title regardless of the casing' do
expect(issuable_class.search(searchable_issue.title.upcase)) expect(issuable_class.search(searchable_issue.title.upcase))
.to eq([searchable_issue]) .to eq([searchable_issue])
end end
it 'returns issues with a fuzzy matching title' do
expect(issuable_class.search('searchable issue')).to eq([searchable_issue])
end
it 'returns all issues with a query shorter than 3 chars' do
expect(issuable_class.search('zz')).to eq(issuable_class.all)
end
end end
describe ".full_search" do describe ".full_search" do
let!(:searchable_issue) do let!(:searchable_issue) do
create(:issue, title: "Searchable issue", description: 'kittens') create(:issue, title: "Searchable awesome issue", description: 'Many cute kittens')
end end
it 'returns notes with a matching title' do it 'returns issues with a matching title' do
expect(issuable_class.full_search(searchable_issue.title)) expect(issuable_class.full_search(searchable_issue.title))
.to eq([searchable_issue]) .to eq([searchable_issue])
end end
it 'returns notes with a partially matching title' do it 'returns issues with a partially matching title' do
expect(issuable_class.full_search('able')).to eq([searchable_issue]) expect(issuable_class.full_search('able')).to eq([searchable_issue])
end end
it 'returns notes with a matching title regardless of the casing' do it 'returns issues with a matching title regardless of the casing' do
expect(issuable_class.full_search(searchable_issue.title.upcase)) expect(issuable_class.full_search(searchable_issue.title.upcase))
.to eq([searchable_issue]) .to eq([searchable_issue])
end end
it 'returns notes with a matching description' do it 'returns issues with a fuzzy matching title' do
expect(issuable_class.full_search('searchable issue')).to eq([searchable_issue])
end
it 'returns issues with a matching description' do
expect(issuable_class.full_search(searchable_issue.description)) expect(issuable_class.full_search(searchable_issue.description))
.to eq([searchable_issue]) .to eq([searchable_issue])
end end
it 'returns notes with a partially matching description' do it 'returns issues with a partially matching description' do
expect(issuable_class.full_search(searchable_issue.description)) expect(issuable_class.full_search(searchable_issue.description))
.to eq([searchable_issue]) .to eq([searchable_issue])
end end
it 'returns notes with a matching description regardless of the casing' do it 'returns issues with a matching description regardless of the casing' do
expect(issuable_class.full_search(searchable_issue.description.upcase)) expect(issuable_class.full_search(searchable_issue.description.upcase))
.to eq([searchable_issue]) .to eq([searchable_issue])
end end
it 'returns issues with a fuzzy matching description' do
expect(issuable_class.full_search('many kittens')).to eq([searchable_issue])
end
it 'returns all issues with a query shorter than 3 chars' do
expect(issuable_class.search('zz')).to eq(issuable_class.all)
end
end end
describe '.to_ability_name' do describe '.to_ability_name' do
......
...@@ -9,6 +9,7 @@ describe Group do ...@@ -9,6 +9,7 @@ describe Group do
it { is_expected.to have_many(:users).through(:group_members) } it { is_expected.to have_many(:users).through(:group_members) }
it { is_expected.to have_many(:owners).through(:group_members) } it { is_expected.to have_many(:owners).through(:group_members) }
it { is_expected.to have_many(:requesters).dependent(:destroy) } it { is_expected.to have_many(:requesters).dependent(:destroy) }
it { is_expected.to have_many(:members_and_requesters) }
it { is_expected.to have_many(:project_group_links).dependent(:destroy) } it { is_expected.to have_many(:project_group_links).dependent(:destroy) }
it { is_expected.to have_many(:shared_projects).through(:project_group_links) } it { is_expected.to have_many(:shared_projects).through(:project_group_links) }
it { is_expected.to have_many(:notification_settings).dependent(:destroy) } it { is_expected.to have_many(:notification_settings).dependent(:destroy) }
...@@ -25,22 +26,8 @@ describe Group do ...@@ -25,22 +26,8 @@ describe Group do
group.add_developer(developer) group.add_developer(developer)
end end
describe '#members' do it_behaves_like 'members and requesters associations' do
it 'includes members and exclude requesters' do let(:namespace) { group }
member_user_ids = group.members.pluck(:user_id)
expect(member_user_ids).to include(developer.id)
expect(member_user_ids).not_to include(requester.id)
end
end
describe '#requesters' do
it 'does not include requesters' do
requester_user_ids = group.requesters.pluck(:user_id)
expect(requester_user_ids).to include(requester.id)
expect(requester_user_ids).not_to include(developer.id)
end
end end
end end
end end
......
...@@ -409,6 +409,15 @@ describe Member do ...@@ -409,6 +409,15 @@ describe Member do
expect(members).to be_a Array expect(members).to be_a Array
expect(members).to be_empty expect(members).to be_empty
end end
it 'supports differents formats' do
list = ['joe@local.test', admin, user1.id, user2.id.to_s]
members = described_class.add_users(source, list, :master)
expect(members.size).to eq(4)
expect(members.first).to be_invite
end
end end
end end
end end
......
...@@ -74,6 +74,7 @@ describe Project do ...@@ -74,6 +74,7 @@ describe Project do
it { is_expected.to have_many(:forks).through(:forked_project_links) } it { is_expected.to have_many(:forks).through(:forked_project_links) }
it { is_expected.to have_many(:uploads).dependent(:destroy) } it { is_expected.to have_many(:uploads).dependent(:destroy) }
it { is_expected.to have_many(:pipeline_schedules) } it { is_expected.to have_many(:pipeline_schedules) }
it { is_expected.to have_many(:members_and_requesters) }
context 'after initialized' do context 'after initialized' do
it "has a project_feature" do it "has a project_feature" do
...@@ -90,22 +91,8 @@ describe Project do ...@@ -90,22 +91,8 @@ describe Project do
project.team << [developer, :developer] project.team << [developer, :developer]
end end
describe '#members' do it_behaves_like 'members and requesters associations' do
it 'includes members and exclude requesters' do let(:namespace) { project }
member_user_ids = project.members.pluck(:user_id)
expect(member_user_ids).to include(developer.id)
expect(member_user_ids).not_to include(requester.id)
end
end
describe '#requesters' do
it 'does not include requesters' do
requester_user_ids = project.requesters.pluck(:user_id)
expect(requester_user_ids).to include(requester.id)
expect(requester_user_ids).not_to include(developer.id)
end
end end
end end
......
...@@ -4,6 +4,7 @@ describe API::Users do ...@@ -4,6 +4,7 @@ describe API::Users do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:admin) { create(:admin) } let(:admin) { create(:admin) }
let(:key) { create(:key, user: user) } let(:key) { create(:key, user: user) }
let(:gpg_key) { create(:gpg_key, user: user) }
let(:email) { create(:email, user: user) } let(:email) { create(:email, user: user) }
let(:omniauth_user) { create(:omniauth_user) } let(:omniauth_user) { create(:omniauth_user) }
let(:ldap_user) { create(:omniauth_user, provider: 'ldapmain') } let(:ldap_user) { create(:omniauth_user, provider: 'ldapmain') }
...@@ -753,6 +754,164 @@ describe API::Users do ...@@ -753,6 +754,164 @@ describe API::Users do
end end
end end
describe 'POST /users/:id/keys' do
before do
admin
end
it 'does not create invalid GPG key' do
post api("/users/#{user.id}/gpg_keys", admin)
expect(response).to have_http_status(400)
expect(json_response['error']).to eq('key is missing')
end
it 'creates GPG key' do
key_attrs = attributes_for :gpg_key
expect do
post api("/users/#{user.id}/gpg_keys", admin), key_attrs
expect(response).to have_http_status(201)
end.to change { user.gpg_keys.count }.by(1)
end
it 'returns 400 for invalid ID' do
post api('/users/999999/gpg_keys', admin)
expect(response).to have_http_status(400)
end
end
describe 'GET /user/:id/gpg_keys' do
before do
admin
end
context 'when unauthenticated' do
it 'returns authentication error' do
get api("/users/#{user.id}/gpg_keys")
expect(response).to have_http_status(401)
end
end
context 'when authenticated' do
it 'returns 404 for non-existing user' do
get api('/users/999999/gpg_keys', admin)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
it 'returns 404 error if key not foud' do
delete api("/users/#{user.id}/gpg_keys/42", admin)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 GPG Key Not Found')
end
it 'returns array of GPG keys' do
user.gpg_keys << gpg_key
user.save
get api("/users/#{user.id}/gpg_keys", admin)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['key']).to eq(gpg_key.key)
end
end
end
describe 'DELETE /user/:id/gpg_keys/:key_id' do
before do
admin
end
context 'when unauthenticated' do
it 'returns authentication error' do
delete api("/users/#{user.id}/keys/42")
expect(response).to have_http_status(401)
end
end
context 'when authenticated' do
it 'deletes existing key' do
user.gpg_keys << gpg_key
user.save
expect do
delete api("/users/#{user.id}/gpg_keys/#{gpg_key.id}", admin)
expect(response).to have_http_status(204)
end.to change { user.gpg_keys.count }.by(-1)
end
it 'returns 404 error if user not found' do
user.keys << key
user.save
delete api("/users/999999/gpg_keys/#{gpg_key.id}", admin)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
it 'returns 404 error if key not foud' do
delete api("/users/#{user.id}/gpg_keys/42", admin)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 GPG Key Not Found')
end
end
end
describe 'POST /user/:id/gpg_keys/:key_id/revoke' do
before do
admin
end
context 'when unauthenticated' do
it 'returns authentication error' do
post api("/users/#{user.id}/gpg_keys/42/revoke")
expect(response).to have_http_status(401)
end
end
context 'when authenticated' do
it 'revokes existing key' do
user.gpg_keys << gpg_key
user.save
expect do
post api("/users/#{user.id}/gpg_keys/#{gpg_key.id}/revoke", admin)
expect(response).to have_http_status(:accepted)
end.to change { user.gpg_keys.count }.by(-1)
end
it 'returns 404 error if user not found' do
user.gpg_keys << gpg_key
user.save
post api("/users/999999/gpg_keys/#{gpg_key.id}/revoke", admin)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
it 'returns 404 error if key not foud' do
post api("/users/#{user.id}/gpg_keys/42/revoke", admin)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 GPG Key Not Found')
end
end
end
describe "POST /users/:id/emails" do describe "POST /users/:id/emails" do
before do before do
admin admin
...@@ -1153,6 +1312,173 @@ describe API::Users do ...@@ -1153,6 +1312,173 @@ describe API::Users do
end end
end end
describe 'GET /user/gpg_keys' do
context 'when unauthenticated' do
it 'returns authentication error' do
get api('/user/gpg_keys')
expect(response).to have_http_status(401)
end
end
context 'when authenticated' do
it 'returns array of GPG keys' do
user.gpg_keys << gpg_key
user.save
get api('/user/gpg_keys', user)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['key']).to eq(gpg_key.key)
end
context 'scopes' do
let(:path) { '/user/gpg_keys' }
let(:api_call) { method(:api) }
include_examples 'allows the "read_user" scope'
end
end
end
describe 'GET /user/gpg_keys/:key_id' do
it 'returns a single key' do
user.gpg_keys << gpg_key
user.save
get api("/user/gpg_keys/#{gpg_key.id}", user)
expect(response).to have_http_status(200)
expect(json_response['key']).to eq(gpg_key.key)
end
it 'returns 404 Not Found within invalid ID' do
get api('/user/gpg_keys/42', user)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 GPG Key Not Found')
end
it "returns 404 error if admin accesses user's GPG key" do
user.gpg_keys << gpg_key
user.save
get api("/user/gpg_keys/#{gpg_key.id}", admin)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 GPG Key Not Found')
end
it 'returns 404 for invalid ID' do
get api('/users/gpg_keys/ASDF', admin)
expect(response).to have_http_status(404)
end
context 'scopes' do
let(:path) { "/user/gpg_keys/#{gpg_key.id}" }
let(:api_call) { method(:api) }
include_examples 'allows the "read_user" scope'
end
end
describe 'POST /user/gpg_keys' do
it 'creates a GPG key' do
key_attrs = attributes_for :gpg_key
expect do
post api('/user/gpg_keys', user), key_attrs
expect(response).to have_http_status(201)
end.to change { user.gpg_keys.count }.by(1)
end
it 'returns a 401 error if unauthorized' do
post api('/user/gpg_keys'), key: 'some key'
expect(response).to have_http_status(401)
end
it 'does not create GPG key without key' do
post api('/user/gpg_keys', user)
expect(response).to have_http_status(400)
expect(json_response['error']).to eq('key is missing')
end
end
describe 'POST /user/gpg_keys/:key_id/revoke' do
it 'revokes existing GPG key' do
user.gpg_keys << gpg_key
user.save
expect do
post api("/user/gpg_keys/#{gpg_key.id}/revoke", user)
expect(response).to have_http_status(:accepted)
end.to change { user.gpg_keys.count}.by(-1)
end
it 'returns 404 if key ID not found' do
post api('/user/gpg_keys/42/revoke', user)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 GPG Key Not Found')
end
it 'returns 401 error if unauthorized' do
user.gpg_keys << gpg_key
user.save
post api("/user/gpg_keys/#{gpg_key.id}/revoke")
expect(response).to have_http_status(401)
end
it 'returns a 404 for invalid ID' do
post api('/users/gpg_keys/ASDF/revoke', admin)
expect(response).to have_http_status(404)
end
end
describe 'DELETE /user/gpg_keys/:key_id' do
it 'deletes existing GPG key' do
user.gpg_keys << gpg_key
user.save
expect do
delete api("/user/gpg_keys/#{gpg_key.id}", user)
expect(response).to have_http_status(204)
end.to change { user.gpg_keys.count}.by(-1)
end
it 'returns 404 if key ID not found' do
delete api('/user/gpg_keys/42', user)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 GPG Key Not Found')
end
it 'returns 401 error if unauthorized' do
user.gpg_keys << gpg_key
user.save
delete api("/user/gpg_keys/#{gpg_key.id}")
expect(response).to have_http_status(401)
end
it 'returns a 404 for invalid ID' do
delete api('/users/gpg_keys/ASDF', admin)
expect(response).to have_http_status(404)
end
end
describe "GET /user/emails" do describe "GET /user/emails" do
context "when unauthenticated" do context "when unauthenticated" do
it "returns authentication error" do it "returns authentication error" do
......
RSpec.shared_examples 'members and requesters associations' do
describe '#members_and_requesters' do
it 'includes members and requesters' do
member_and_requester_user_ids = namespace.members_and_requesters.pluck(:user_id)
expect(member_and_requester_user_ids).to include(requester.id, developer.id)
end
end
describe '#members' do
it 'includes members and exclude requesters' do
member_user_ids = namespace.members.pluck(:user_id)
expect(member_user_ids).to include(developer.id)
expect(member_user_ids).not_to include(requester.id)
end
end
describe '#requesters' do
it 'does not include requesters' do
requester_user_ids = namespace.requesters.pluck(:user_id)
expect(requester_user_ids).to include(requester.id)
expect(requester_user_ids).not_to include(developer.id)
end
end
end
require 'spec_helper' require 'spec_helper'
describe 'layouts/nav/_project' do describe 'layouts/nav/sidebar/_project' do
describe 'container registry tab' do describe 'container registry tab' do
before do before do
project = create(:project, :repository)
stub_container_registry_config(enabled: true) stub_container_registry_config(enabled: true)
assign(:project, create(:project, :repository)) assign(:project, project)
assign(:repository, project.repository)
allow(view).to receive(:current_ref).and_return('master') allow(view).to receive(:current_ref).and_return('master')
allow(view).to receive(:can?).and_return(true) allow(view).to receive(:can?).and_return(true)
......
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