Commit 3d9b6bc2 authored by Bryce Johnson's avatar Bryce Johnson

Merge branch 'master' into backport-issues-controller-changes

parents e4348ae8 bc955cfc
...@@ -643,7 +643,7 @@ Metrics/ClassLength: ...@@ -643,7 +643,7 @@ Metrics/ClassLength:
# of test cases needed to validate a method. # of test cases needed to validate a method.
Metrics/CyclomaticComplexity: Metrics/CyclomaticComplexity:
Enabled: true Enabled: true
Max: 15 Max: 14
# Limit lines to 80 characters. # Limit lines to 80 characters.
Metrics/LineLength: Metrics/LineLength:
......
...@@ -407,3 +407,4 @@ gem 'flipper-active_record', '~> 0.10.2' ...@@ -407,3 +407,4 @@ gem 'flipper-active_record', '~> 0.10.2'
# Structured logging # Structured logging
gem 'lograge', '~> 0.5' gem 'lograge', '~> 0.5'
gem 'grape_logging', '~> 1.6'
...@@ -355,6 +355,8 @@ GEM ...@@ -355,6 +355,8 @@ GEM
activesupport activesupport
grape (>= 0.16.0) grape (>= 0.16.0)
rake rake
grape_logging (1.6.0)
grape
grpc (1.4.5) grpc (1.4.5)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
googleauth (~> 0.5.1) googleauth (~> 0.5.1)
...@@ -1035,6 +1037,7 @@ DEPENDENCIES ...@@ -1035,6 +1037,7 @@ DEPENDENCIES
grape (~> 1.0) grape (~> 1.0)
grape-entity (~> 0.6.0) grape-entity (~> 0.6.0)
grape-route-helpers (~> 2.1.0) grape-route-helpers (~> 2.1.0)
grape_logging (~> 1.6)
haml_lint (~> 0.26.0) haml_lint (~> 0.26.0)
hamlit (~> 2.6.1) hamlit (~> 2.6.1)
hashie-forbidden_attributes hashie-forbidden_attributes
......
...@@ -35,6 +35,7 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -35,6 +35,7 @@ document.addEventListener('DOMContentLoaded', () => {
propsData: { propsData: {
endpoint: pipelineTableViewEl.dataset.endpoint, endpoint: pipelineTableViewEl.dataset.endpoint,
helpPagePath: pipelineTableViewEl.dataset.helpPagePath, helpPagePath: pipelineTableViewEl.dataset.helpPagePath,
autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath,
}, },
}).$mount(); }).$mount();
pipelineTableViewEl.appendChild(table.$el); pipelineTableViewEl.appendChild(table.$el);
......
...@@ -13,6 +13,10 @@ ...@@ -13,6 +13,10 @@
type: String, type: String,
required: true, required: true,
}, },
autoDevopsHelpPath: {
type: String,
required: true,
},
}, },
mixins: [ mixins: [
pipelinesMixin, pipelinesMixin,
...@@ -95,6 +99,7 @@ ...@@ -95,6 +99,7 @@
<pipelines-table-component <pipelines-table-component
:pipelines="state.pipelines" :pipelines="state.pipelines"
:update-graph-dropdown="updateGraphDropdown" :update-graph-dropdown="updateGraphDropdown"
:auto-devops-help-path="autoDevopsHelpPath"
/> />
</div> </div>
</div> </div>
......
...@@ -160,6 +160,9 @@ import initChangesDropdown from './init_changes_dropdown'; ...@@ -160,6 +160,9 @@ import initChangesDropdown from './init_changes_dropdown';
const filteredSearchManager = new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests'); const filteredSearchManager = new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests');
filteredSearchManager.setup(); filteredSearchManager.setup();
} }
if (page === 'projects:merge_requests:index') {
new UserCallout({ setCalloutPerProject: true });
}
const pagePrefix = page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_'; const pagePrefix = page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_';
IssuableIndex.init(pagePrefix); IssuableIndex.init(pagePrefix);
...@@ -342,6 +345,7 @@ import initChangesDropdown from './init_changes_dropdown'; ...@@ -342,6 +345,7 @@ import initChangesDropdown from './init_changes_dropdown';
case 'projects:show': case 'projects:show':
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
new NotificationsForm(); new NotificationsForm();
new UserCallout({ setCalloutPerProject: true });
if ($('#tree-slider').length) new TreeView(); if ($('#tree-slider').length) new TreeView();
if ($('.blob-viewer').length) new BlobViewer(); if ($('.blob-viewer').length) new BlobViewer();
...@@ -361,6 +365,9 @@ import initChangesDropdown from './init_changes_dropdown'; ...@@ -361,6 +365,9 @@ import initChangesDropdown from './init_changes_dropdown';
case 'projects:pipelines:new': case 'projects:pipelines:new':
new NewBranchForm($('.js-new-pipeline-form')); new NewBranchForm($('.js-new-pipeline-form'));
break; break;
case 'projects:pipelines:index':
new UserCallout({ setCalloutPerProject: true });
break;
case 'projects:pipelines:builds': case 'projects:pipelines:builds':
case 'projects:pipelines:failures': case 'projects:pipelines:failures':
case 'projects:pipelines:show': case 'projects:pipelines:show':
...@@ -418,6 +425,7 @@ import initChangesDropdown from './init_changes_dropdown'; ...@@ -418,6 +425,7 @@ import initChangesDropdown from './init_changes_dropdown';
new TreeView(); new TreeView();
new BlobViewer(); new BlobViewer();
new NewCommitForm($('.js-create-dir-form')); new NewCommitForm($('.js-create-dir-form'));
new UserCallout({ setCalloutPerProject: true });
$('#tree-slider').waitForImages(function() { $('#tree-slider').waitForImages(function() {
gl.utils.ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath); gl.utils.ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath);
}); });
...@@ -569,6 +577,9 @@ import initChangesDropdown from './init_changes_dropdown'; ...@@ -569,6 +577,9 @@ import initChangesDropdown from './init_changes_dropdown';
case 'edit': case 'edit':
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
new ProjectNew(); new ProjectNew();
import(/* webpackChunkName: 'project_permissions' */ './projects/permissions')
.then(permissions => permissions.default())
.catch(() => {});
break; break;
case 'new': case 'new':
new ProjectNew(); new ProjectNew();
......
...@@ -243,6 +243,7 @@ import bp from './breakpoints'; ...@@ -243,6 +243,7 @@ import bp from './breakpoints';
propsData: { propsData: {
endpoint: pipelineTableViewEl.dataset.endpoint, endpoint: pipelineTableViewEl.dataset.endpoint,
helpPagePath: pipelineTableViewEl.dataset.helpPagePath, helpPagePath: pipelineTableViewEl.dataset.helpPagePath,
autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath,
}, },
}).$mount(); }).$mount();
......
<script> <script>
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
import popover from '../../vue_shared/directives/popover';
export default { export default {
props: { props: {
pipeline: { pipeline: {
type: Object, type: Object,
required: true, required: true,
}, },
autoDevopsHelpPath: {
type: String,
required: true,
},
}, },
components: { components: {
userAvatarLink, userAvatarLink,
}, },
directives: { directives: {
tooltip, tooltip,
popover,
}, },
computed: { computed: {
user() { user() {
return this.pipeline.user; return this.pipeline.user;
}, },
popoverOptions() {
return {
html: true,
delay: { hide: 600 },
trigger: 'hover',
placement: 'top',
title: '<div class="autodevops-title">This pipeline makes use of a predefined CI/CD configuration enabled by <b>Auto DevOps.</b></div>',
content: `<a class="autodevops-link" href="${this.autoDevopsHelpPath}" target="_blank" rel="noopener noreferrer nofollow">Learn more about Auto DevOps</a>`,
};
},
}, },
}; };
</script> </script>
<template> <template>
<div class="table-section section-15 hidden-xs hidden-sm"> <div class="table-section section-15 hidden-xs hidden-sm pipeline-tags">
<a <a
:href="pipeline.path" :href="pipeline.path"
class="js-pipeline-url-link"> class="js-pipeline-url-link">
...@@ -57,6 +73,13 @@ export default { ...@@ -57,6 +73,13 @@ export default {
:title="pipeline.yaml_errors"> :title="pipeline.yaml_errors">
yaml invalid yaml invalid
</span> </span>
<a
v-if="pipeline.flags.auto_devops"
class="js-pipeline-url-autodevops label label-info autodevops-badge"
v-popover="popoverOptions"
role="button">
Auto DevOps
</a>
<span <span
v-if="pipeline.flags.stuck" v-if="pipeline.flags.stuck"
class="js-pipeline-url-stuck label label-warning"> class="js-pipeline-url-stuck label label-warning">
......
...@@ -25,8 +25,8 @@ ...@@ -25,8 +25,8 @@
return { return {
endpoint: pipelinesData.endpoint, endpoint: pipelinesData.endpoint,
cssClass: pipelinesData.cssClass,
helpPagePath: pipelinesData.helpPagePath, helpPagePath: pipelinesData.helpPagePath,
autoDevopsPath: pipelinesData.helpAutoDevopsPath,
newPipelinePath: pipelinesData.newPipelinePath, newPipelinePath: pipelinesData.newPipelinePath,
canCreatePipeline: pipelinesData.canCreatePipeline, canCreatePipeline: pipelinesData.canCreatePipeline,
allPath: pipelinesData.allPath, allPath: pipelinesData.allPath,
...@@ -139,9 +139,7 @@ ...@@ -139,9 +139,7 @@
}; };
</script> </script>
<template> <template>
<div <div class="pipelines-container">
class="pipelines-container"
:class="cssClass">
<div <div
class="top-area scrolling-tabs-container inner-page-scroll-tabs" class="top-area scrolling-tabs-container inner-page-scroll-tabs"
v-if="!isLoading && !shouldRenderEmptyState"> v-if="!isLoading && !shouldRenderEmptyState">
...@@ -200,6 +198,7 @@ ...@@ -200,6 +198,7 @@
<pipelines-table-component <pipelines-table-component
:pipelines="state.pipelines" :pipelines="state.pipelines"
:update-graph-dropdown="updateGraphDropdown" :update-graph-dropdown="updateGraphDropdown"
:auto-devops-help-path="autoDevopsPath"
/> />
</div> </div>
......
...@@ -17,6 +17,10 @@ ...@@ -17,6 +17,10 @@
required: false, required: false,
default: false, default: false,
}, },
autoDevopsHelpPath: {
type: String,
required: true,
},
}, },
components: { components: {
pipelinesTableRowComponent, pipelinesTableRowComponent,
...@@ -54,6 +58,7 @@ ...@@ -54,6 +58,7 @@
:key="model.id" :key="model.id"
:pipeline="model" :pipeline="model"
:update-graph-dropdown="updateGraphDropdown" :update-graph-dropdown="updateGraphDropdown"
:auto-devops-help-path="autoDevopsHelpPath"
/> />
</div> </div>
</template> </template>
...@@ -25,6 +25,10 @@ export default { ...@@ -25,6 +25,10 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
autoDevopsHelpPath: {
type: String,
required: true,
},
}, },
components: { components: {
asyncButtonComponent, asyncButtonComponent,
...@@ -218,7 +222,10 @@ export default { ...@@ -218,7 +222,10 @@ export default {
</div> </div>
</div> </div>
<pipeline-url :pipeline="pipeline" /> <pipeline-url
:pipeline="pipeline"
:auto-devops-help-path="autoDevopsHelpPath"
/>
<div class="table-section section-25"> <div class="table-section section-25">
<div <div
......
<script>
import projectFeatureToggle from './project_feature_toggle.vue';
export default {
props: {
name: {
type: String,
required: false,
default: '',
},
options: {
type: Array,
required: false,
default: () => [],
},
value: {
type: Number,
required: false,
default: 0,
},
disabledInput: {
type: Boolean,
required: false,
default: false,
},
},
components: {
projectFeatureToggle,
},
computed: {
featureEnabled() {
return this.value !== 0;
},
displayOptions() {
if (this.featureEnabled) {
return this.options;
}
return [
[0, 'Enable feature to choose access level'],
];
},
displaySelectInput() {
return this.disabledInput || !this.featureEnabled || this.displayOptions.length < 2;
},
},
model: {
prop: 'value',
event: 'change',
},
methods: {
toggleFeature(featureEnabled) {
if (featureEnabled === false || this.options.length < 1) {
this.$emit('change', 0);
} else {
const [firstOptionValue] = this.options[this.options.length - 1];
this.$emit('change', firstOptionValue);
}
},
selectOption(e) {
this.$emit('change', Number(e.target.value));
},
},
};
</script>
<template>
<div class="project-feature-controls" :data-for="name">
<input
v-if="name"
type="hidden"
:name="name"
:value="value"
/>
<project-feature-toggle
:value="featureEnabled"
@change="toggleFeature"
:disabledInput="disabledInput"
/>
<div class="select-wrapper">
<select
class="form-control project-repo-select select-control"
@change="selectOption"
:disabled="displaySelectInput"
>
<option
v-for="[optionValue, optionName] in displayOptions"
:key="optionValue"
:value="optionValue"
:selected="optionValue === value"
>
{{optionName}}
</option>
</select>
<i aria-hidden="true" class="fa fa-chevron-down"></i>
</div>
</div>
</template>
<script>
export default {
props: {
name: {
type: String,
required: false,
default: '',
},
value: {
type: Boolean,
required: true,
},
disabledInput: {
type: Boolean,
required: false,
default: false,
},
},
model: {
prop: 'value',
event: 'change',
},
methods: {
toggleFeature() {
if (!this.disabledInput) this.$emit('change', !this.value);
},
},
};
</script>
<template>
<label class="toggle-wrapper">
<input
v-if="name"
type="hidden"
:name="name"
:value="value"
/>
<button
type="button"
aria-label="Toggle"
class="project-feature-toggle"
data-enabled-text="Enabled"
data-disabled-text="Disabled"
:class="{ checked: value, disabled: disabledInput }"
@click="toggleFeature"
/>
</label>
</template>
<script>
export default {
props: {
label: {
type: String,
required: false,
default: null,
},
helpPath: {
type: String,
required: false,
default: null,
},
helpText: {
type: String,
required: false,
default: null,
},
},
};
</script>
<template>
<div class="project-feature-row">
<label v-if="label" class="label-light">
{{label}}
<a v-if="helpPath" :href="helpPath" target="_blank">
<i aria-hidden="true" data-hidden="true" class="fa fa-question-circle"></i>
</a>
</label>
<span v-if="helpText" class="help-block">
{{helpText}}
</span>
<slot />
</div>
</template>
export const visibilityOptions = {
PRIVATE: 0,
INTERNAL: 10,
PUBLIC: 20,
};
export const visibilityLevelDescriptions = {
[visibilityOptions.PRIVATE]: 'The project is accessible only by members of the project. Access must be granted explicitly to each user.',
[visibilityOptions.INTERNAL]: 'The project can be accessed by any user who is logged in.',
[visibilityOptions.PUBLIC]: 'The project can be accessed by anyone, regardless of authentication.',
};
const selectorCache = [];
// workaround since we don't have a polyfill for classList.toggle 2nd parameter
export function toggleHiddenClass(element, hidden) {
if (hidden) {
element.classList.add('hidden');
} else {
element.classList.remove('hidden');
}
}
// hide external feature-specific settings when a given feature is disabled
export function toggleHiddenClassBySelector(selector, hidden) {
if (!selectorCache[selector]) {
selectorCache[selector] = document.querySelectorAll(selector);
}
selectorCache[selector].forEach(elm => toggleHiddenClass(elm, hidden));
}
import Vue from 'vue';
import settingsPanel from './components/settings_panel.vue';
export default function initProjectPermissionsSettings() {
const mountPoint = document.querySelector('.js-project-permissions-form');
const componentPropsEl = document.querySelector('.js-project-permissions-form-data');
const componentProps = JSON.parse(componentPropsEl.innerHTML);
return new Vue({
el: mountPoint,
render: createElement => createElement(settingsPanel, { props: { ...componentProps } }),
});
}
...@@ -27,7 +27,7 @@ export default { ...@@ -27,7 +27,7 @@ export default {
listEmptyMessage() { listEmptyMessage() {
return this.searchFailed ? return this.searchFailed ?
s__('ProjectsDropdown|Something went wrong on our end.') : s__('ProjectsDropdown|Something went wrong on our end.') :
s__('ProjectsDropdown|No projects matched your query'); s__('ProjectsDropdown|Sorry, no projects matched your search');
}, },
}, },
}; };
......
...@@ -53,7 +53,7 @@ export default { ...@@ -53,7 +53,7 @@ export default {
class="form-control" class="form-control"
ref="search" ref="search"
v-model="searchQuery" v-model="searchQuery"
:placeholder="s__('ProjectsDropdown|Search projects')" :placeholder="s__('ProjectsDropdown|Search your projects')"
/> />
<i <i
v-if="!searchQuery" v-if="!searchQuery"
......
...@@ -41,4 +41,8 @@ export default function initSettingsPanels() { ...@@ -41,4 +41,8 @@ export default function initSettingsPanels() {
$section.on('click.toggleSection', '.js-settings-toggle', () => toggleSection($section)); $section.on('click.toggleSection', '.js-settings-toggle', () => toggleSection($section));
$section.find('.settings-content:not(.expanded)').on('scroll.expandSection', () => expandSection($section)); $section.find('.settings-content:not(.expanded)').on('scroll.expandSection', () => expandSection($section));
}); });
if (location.hash) {
expandSection($(location.hash));
}
} }
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
export default class UserCallout { export default class UserCallout {
constructor(className = 'user-callout') { constructor(options = {}) {
this.options = options;
const className = this.options.className || 'user-callout';
this.userCalloutBody = $(`.${className}`); this.userCalloutBody = $(`.${className}`);
this.cookieName = this.userCalloutBody.data('uid'); this.cookieName = this.userCalloutBody.data('uid');
this.isCalloutDismissed = Cookies.get(this.cookieName); this.isCalloutDismissed = Cookies.get(this.cookieName);
...@@ -17,7 +21,11 @@ export default class UserCallout { ...@@ -17,7 +21,11 @@ export default class UserCallout {
dismissCallout(e) { dismissCallout(e) {
const $currentTarget = $(e.currentTarget); const $currentTarget = $(e.currentTarget);
if (this.options.setCalloutPerProject) {
Cookies.set(this.cookieName, 'true', { expires: 365, path: this.userCalloutBody.data('project-path') });
} else {
Cookies.set(this.cookieName, 'true', { expires: 365 }); Cookies.set(this.cookieName, 'true', { expires: 365 });
}
if ($currentTarget.hasClass('close')) { if ($currentTarget.hasClass('close')) {
this.userCalloutBody.remove(); this.userCalloutBody.remove();
......
/**
* Helper to user bootstrap popover in vue.js.
* Follow docs for html attributes: https://getbootstrap.com/docs/3.3/javascript/#static-popover
*
* @example
* import popover from 'vue_shared/directives/popover.js';
* {
* directives: [popover]
* }
* <a v-popover="{options}">popover</a>
*/
export default {
bind(el, binding) {
$(el).popover(binding.value);
},
unbind(el) {
$(el).popover('destroy');
},
};
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
@import "framework/flash"; @import "framework/flash";
@import "framework/forms"; @import "framework/forms";
@import "framework/gfm"; @import "framework/gfm";
@import "framework/gitlab-theme";
@import "framework/header"; @import "framework/header";
@import "framework/highlight"; @import "framework/highlight";
@import "framework/issue_box"; @import "framework/issue_box";
......
...@@ -734,6 +734,11 @@ ...@@ -734,6 +734,11 @@
overflow: hidden; overflow: hidden;
} }
@mixin dropdown-item-hover {
background-color: $dropdown-item-hover-bg;
color: $gl-text-color;
}
// TODO: change global style and remove mixin // TODO: change global style and remove mixin
@mixin new-style-dropdown($selector: '') { @mixin new-style-dropdown($selector: '') {
#{$selector}.dropdown-menu, #{$selector}.dropdown-menu,
...@@ -760,6 +765,10 @@ ...@@ -760,6 +765,10 @@
padding: 8px 16px; padding: 8px 16px;
} }
&.droplab-item-active button {
@include dropdown-item-hover;
}
a, a,
button, button,
.menu-item { .menu-item {
...@@ -779,6 +788,8 @@ ...@@ -779,6 +788,8 @@
&:hover, &:hover,
&:active, &:active,
&:focus { &:focus {
@include dropdown-item-hover;
background-color: $dropdown-item-hover-bg; background-color: $dropdown-item-hover-bg;
color: $gl-text-color; color: $gl-text-color;
...@@ -837,17 +848,30 @@ ...@@ -837,17 +848,30 @@
} }
} }
@media (max-width: $screen-xs-max) {
.navbar-gitlab {
li.header-projects,
li.header-more,
li.header-new,
li.header-user {
position: static;
}
}
header.navbar-gitlab .dropdown {
.dropdown-menu,
.dropdown-menu-nav {
width: 100%;
min-width: 100%;
}
}
}
@include new-style-dropdown('.breadcrumbs-list .dropdown '); @include new-style-dropdown('.breadcrumbs-list .dropdown ');
@include new-style-dropdown('.js-namespace-select + '); @include new-style-dropdown('.js-namespace-select + ');
header.navbar-gitlab-new .header-content .dropdown-menu.projects-dropdown-menu { header.navbar-gitlab-new .header-content .dropdown-menu.projects-dropdown-menu {
padding: 0; padding: 0;
@media (max-width: $screen-xs-max) {
display: table;
left: -50px;
min-width: 300px;
}
} }
.projects-dropdown-container { .projects-dropdown-container {
......
/**
* Styles the GitLab application with a specific color theme
*/
@mixin gitlab-theme($color-100, $color-200, $color-500, $color-700, $color-800, $color-900, $color-alternate) {
// Header
header.navbar-gitlab-new {
background: linear-gradient(to right, $color-900, $color-800);
.navbar-collapse {
color: $color-200;
}
.container-fluid {
.navbar-toggle {
border-left: 1px solid lighten($color-700, 10%);
}
}
.navbar-sub-nav,
.navbar-nav {
> li {
> a:hover,
> a:focus {
background-color: rgba($color-200, .2);
}
&.active > a,
&.dropdown.open > a {
color: $color-900;
background-color: $color-alternate;
svg {
fill: currentColor;
}
}
&.line-separator {
border-left: 1px solid rgba($color-200, .2);
}
}
}
.navbar-sub-nav {
color: $color-200;
}
.nav {
> li {
color: $color-200;
> a {
svg {
fill: $color-200;
}
&.header-user-dropdown-toggle {
.header-user-avatar {
border-color: $color-200;
}
}
&:hover,
&:focus {
@media (min-width: $screen-sm-min) {
background-color: rgba($color-200, .2);
}
svg {
fill: currentColor;
}
}
}
&.active > a,
&.dropdown.open > a {
color: $color-900;
background-color: $color-alternate;
&:hover {
svg {
fill: $color-900;
}
}
}
.impersonated-user,
.impersonated-user:hover {
svg {
fill: $color-900;
}
}
}
}
}
.title {
> a {
&:hover,
&:focus {
background-color: rgba($color-200, .2);
}
}
}
.search {
form {
background-color: rgba($color-200, .2);
&:hover {
background-color: rgba($color-200, .3);
}
}
.location-badge {
color: $color-100;
background-color: rgba($color-200, .1);
border-right: 1px solid $color-800;
}
.search-input::placeholder {
color: rgba($color-200, .8);
}
.search-input-wrap {
.search-icon,
.clear-icon {
color: rgba($color-200, .8);
}
}
&.search-active {
form {
background-color: $white-light;
}
.location-badge {
color: $gl-text-color;
}
.search-input-wrap {
.search-icon {
color: rgba($color-200, .8);
}
}
}
}
.btn-sign-in {
background-color: $color-100;
color: $color-900;
}
// Sidebar
.nav-sidebar li.active {
box-shadow: inset 4px 0 0 $color-700;
> a {
color: $color-900;
}
svg {
fill: $color-900;
}
}
}
body {
&.ui_indigo {
@include gitlab-theme($indigo-100, $indigo-200, $indigo-500, $indigo-700, $indigo-800, $indigo-900, $white-light);
}
&.ui_dark {
@include gitlab-theme($theme-gray-100, $theme-gray-200, $theme-gray-500, $theme-gray-700, $theme-gray-800, $theme-gray-900, $white-light);
}
&.ui_blue {
@include gitlab-theme($theme-blue-100, $theme-blue-200, $theme-blue-500, $theme-blue-700, $theme-blue-800, $theme-blue-900, $white-light);
}
&.ui_green {
@include gitlab-theme($theme-green-100, $theme-green-200, $theme-green-500, $theme-green-700, $theme-green-800, $theme-green-900, $white-light);
}
&.ui_light {
@include gitlab-theme($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-700, $theme-gray-700, $theme-gray-100, $theme-gray-700);
header.navbar-gitlab-new {
background: $theme-gray-100;
box-shadow: 0 2px 0 0 $border-color;
.logo-text svg {
fill: $theme-gray-900;
}
.navbar-sub-nav,
.navbar-nav {
> li {
> a:hover,
> a:focus {
color: $theme-gray-900;
}
&.active > a {
color: $white-light;
&:hover {
color: $white-light;
}
}
}
}
.container-fluid {
.navbar-toggle,
.navbar-toggle:hover {
color: $theme-gray-700;
border-left: 1px solid $theme-gray-200;
}
}
}
.search {
form {
background-color: $white-light;
box-shadow: inset 0 0 0 1px $border-color;
&:hover {
background-color: $white-light;
box-shadow: inset 0 0 0 1px $blue-100;
.location-badge {
box-shadow: inset 0 0 0 1px $blue-100;
}
}
}
.search-input-wrap {
.search-icon {
color: $theme-gray-200;
}
}
.location-badge {
color: $theme-gray-700;
box-shadow: inset 0 0 0 1px $border-color;
background-color: $nav-badge-bg;
border-right: 0;
}
}
.nav-sidebar li.active {
> a {
color: $theme-gray-900;
}
svg {
fill: $theme-gray-900;
}
}
}
}
...@@ -111,7 +111,6 @@ header { ...@@ -111,7 +111,6 @@ header {
svg { svg {
height: 16px; height: 16px;
width: 23px; width: 23px;
fill: currentColor;
} }
} }
......
...@@ -74,6 +74,8 @@ $red-700: #a62d19; ...@@ -74,6 +74,8 @@ $red-700: #a62d19;
$red-800: #8b2615; $red-800: #8b2615;
$red-900: #711e11; $red-900: #711e11;
// GitLab themes
$indigo-50: #f7f7ff; $indigo-50: #f7f7ff;
$indigo-100: #ebebfa; $indigo-100: #ebebfa;
$indigo-200: #d1d1f0; $indigo-200: #d1d1f0;
...@@ -86,6 +88,43 @@ $indigo-800: #393982; ...@@ -86,6 +88,43 @@ $indigo-800: #393982;
$indigo-900: #292961; $indigo-900: #292961;
$indigo-950: #1a1a40; $indigo-950: #1a1a40;
$theme-gray-50: #fafafa;
$theme-gray-100: #f2f2f2;
$theme-gray-200: #dfdfdf;
$theme-gray-300: #cccccc;
$theme-gray-400: #bababa;
$theme-gray-500: #a7a7a7;
$theme-gray-600: #949494;
$theme-gray-700: #707070;
$theme-gray-800: #4f4f4f;
$theme-gray-900: #2e2e2e;
$theme-gray-950: #1f1f1f;
$theme-blue-50: #f4f8fc;
$theme-blue-100: #e6edf5;
$theme-blue-200: #c8d7e6;
$theme-blue-300: #97b3cf;
$theme-blue-400: #648cb4;
$theme-blue-500: #4a79a8;
$theme-blue-600: #3e6fa0;
$theme-blue-700: #305c88;
$theme-blue-800: #25496e;
$theme-blue-900: #1a3652;
$theme-blue-950: #0f2235;
$theme-green-50: #f2faf6;
$theme-green-100: #e4f3ea;
$theme-green-200: #c0dfcd;
$theme-green-300: #8ac2a1;
$theme-green-400: #52a274;
$theme-green-500: #35935c;
$theme-green-600: #288a50;
$theme-green-700: #1c7441;
$theme-green-800: #145d33;
$theme-green-900: #0d4524;
$theme-green-950: #072d16;
$black: #000; $black: #000;
$black-transparent: rgba(0, 0, 0, 0.3); $black-transparent: rgba(0, 0, 0, 0.3);
$almost-black: #242424; $almost-black: #242424;
...@@ -540,6 +579,11 @@ $project-breadcrumb-color: #999; ...@@ -540,6 +579,11 @@ $project-breadcrumb-color: #999;
$project-private-forks-notice-odd: $green-600; $project-private-forks-notice-odd: $green-600;
$project-network-controls-color: #888; $project-network-controls-color: #888;
$feature-toggle-color: #fff;
$feature-toggle-text-color: #fff;
$feature-toggle-color-disabled: #999;
$feature-toggle-color-enabled: #4a8bee;
/* /*
* Runners * Runners
*/ */
......
...@@ -9,10 +9,20 @@ ...@@ -9,10 +9,20 @@
header.navbar-gitlab-new { header.navbar-gitlab-new {
color: $white-light; color: $white-light;
background: linear-gradient(to right, $indigo-900, $indigo-800);
border-bottom: 0; border-bottom: 0;
min-height: $new-navbar-height; min-height: $new-navbar-height;
.logo-text {
line-height: initial;
svg {
width: 55px;
height: 14px;
margin: 0;
fill: $white-light;
}
}
.header-content { .header-content {
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
...@@ -38,10 +48,10 @@ header.navbar-gitlab-new { ...@@ -38,10 +48,10 @@ header.navbar-gitlab-new {
img { img {
height: 28px; height: 28px;
margin-right: 10px; margin-right: 8px;
} }
> a { a {
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
align-items: center; align-items: center;
...@@ -54,22 +64,6 @@ header.navbar-gitlab-new { ...@@ -54,22 +64,6 @@ header.navbar-gitlab-new {
margin-right: 8px; margin-right: 8px;
} }
} }
.logo-text {
line-height: initial;
svg {
width: 55px;
height: 14px;
margin: 0;
fill: $white-light;
}
}
&:hover,
&:focus {
background-color: rgba($indigo-200, .2);
}
} }
} }
...@@ -106,7 +100,6 @@ header.navbar-gitlab-new { ...@@ -106,7 +100,6 @@ header.navbar-gitlab-new {
.navbar-collapse { .navbar-collapse {
padding-left: 0; padding-left: 0;
color: $indigo-200;
box-shadow: 0; box-shadow: 0;
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
...@@ -132,7 +125,6 @@ header.navbar-gitlab-new { ...@@ -132,7 +125,6 @@ header.navbar-gitlab-new {
font-size: 14px; font-size: 14px;
text-align: center; text-align: center;
color: currentColor; color: currentColor;
border-left: 1px solid lighten($indigo-700, 10%);
&:hover, &:hover,
&:focus, &:focus,
...@@ -167,42 +159,27 @@ header.navbar-gitlab-new { ...@@ -167,42 +159,27 @@ header.navbar-gitlab-new {
will-change: color; will-change: color;
margin: 4px 2px; margin: 4px 2px;
padding: 6px 8px; padding: 6px 8px;
color: $indigo-200;
height: 32px; height: 32px;
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
padding: 0; padding: 0;
} }
svg {
fill: $indigo-200;
}
&.header-user-dropdown-toggle { &.header-user-dropdown-toggle {
margin-left: 2px; margin-left: 2px;
.header-user-avatar { .header-user-avatar {
border-color: $indigo-200;
margin-right: 0; margin-right: 0;
} }
} }
}
.header-new-dropdown-toggle {
margin-right: 0;
}
> a:hover, &:hover,
> a:focus { &:focus {
text-decoration: none; text-decoration: none;
outline: 0; outline: 0;
opacity: 1; opacity: 1;
color: $white-light; color: $white-light;
@media (min-width: $screen-sm-min) {
background-color: rgba($indigo-200, .2);
}
svg { svg {
fill: currentColor; fill: currentColor;
} }
...@@ -213,6 +190,11 @@ header.navbar-gitlab-new { ...@@ -213,6 +190,11 @@ header.navbar-gitlab-new {
} }
} }
} }
}
.header-new-dropdown-toggle {
margin-right: 0;
}
.impersonated-user, .impersonated-user,
.impersonated-user:hover { .impersonated-user:hover {
...@@ -220,10 +202,6 @@ header.navbar-gitlab-new { ...@@ -220,10 +202,6 @@ header.navbar-gitlab-new {
background-color: $white-light; background-color: $white-light;
border-top-right-radius: 0; border-top-right-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
svg {
fill: $indigo-900;
}
} }
.impersonation-btn, .impersonation-btn,
...@@ -241,8 +219,6 @@ header.navbar-gitlab-new { ...@@ -241,8 +219,6 @@ header.navbar-gitlab-new {
&.active > a, &.active > a,
&.dropdown.open > a { &.dropdown.open > a {
color: $indigo-900;
background-color: $white-light;
svg { svg {
fill: currentColor; fill: currentColor;
...@@ -256,7 +232,6 @@ header.navbar-gitlab-new { ...@@ -256,7 +232,6 @@ header.navbar-gitlab-new {
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
margin: 0 0 0 6px; margin: 0 0 0 6px;
color: $indigo-200;
.dropdown-chevron { .dropdown-chevron {
position: relative; position: relative;
...@@ -274,17 +249,6 @@ header.navbar-gitlab-new { ...@@ -274,17 +249,6 @@ header.navbar-gitlab-new {
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,
&.dropdown.open > a {
color: $indigo-900;
background-color: $white-light;
svg { svg {
fill: currentColor; fill: currentColor;
...@@ -309,7 +273,6 @@ header.navbar-gitlab-new { ...@@ -309,7 +273,6 @@ header.navbar-gitlab-new {
} }
&.line-separator { &.line-separator {
border-left: 1px solid rgba($indigo-200, .2);
margin: 8px; margin: 8px;
} }
} }
...@@ -339,17 +302,14 @@ header.navbar-gitlab-new { ...@@ -339,17 +302,14 @@ header.navbar-gitlab-new {
height: 32px; height: 32px;
border: 0; border: 0;
border-radius: $border-radius-default; border-radius: $border-radius-default;
background-color: rgba($indigo-200, .2);
transition: border-color 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);
box-shadow: none; box-shadow: none;
} }
} }
&.search-active form { &.search-active form {
background-color: $white-light;
box-shadow: none; box-shadow: none;
.search-input { .search-input {
...@@ -377,43 +337,26 @@ header.navbar-gitlab-new { ...@@ -377,43 +337,26 @@ header.navbar-gitlab-new {
} }
.search-input::placeholder { .search-input::placeholder {
color: rgba($indigo-200, .8);
transition: color ease-in-out 0.15s; transition: color ease-in-out 0.15s;
} }
.location-badge { .location-badge {
font-size: 12px; font-size: 12px;
color: $indigo-100;
background-color: rgba($indigo-200, .1);
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;
height: 32px; height: 32px;
transition: border-color ease-in-out 0.15s; transition: border-color ease-in-out 0.15s;
} }
.search-input-wrap {
.search-icon,
.clear-icon {
color: rgba($indigo-200, .8);
}
}
&.search-active { &.search-active {
.location-badge { .location-badge {
color: $gl-text-color;
background-color: $nav-badge-bg; background-color: $nav-badge-bg;
border-color: $border-color; border-color: $border-color;
} }
.search-input-wrap { .search-input-wrap {
.search-icon {
color: rgba($indigo-200, .8);
}
.clear-icon { .clear-icon {
color: $white-light; color: $white-light;
} }
...@@ -517,8 +460,6 @@ header.navbar-gitlab-new { ...@@ -517,8 +460,6 @@ header.navbar-gitlab-new {
.btn-sign-in { .btn-sign-in {
margin-top: 3px; margin-top: 3px;
background-color: $indigo-100;
color: $indigo-900;
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
&:hover { &:hover {
......
...@@ -155,16 +155,9 @@ $new-sidebar-collapsed-width: 50px; ...@@ -155,16 +155,9 @@ $new-sidebar-collapsed-width: 50px;
} }
li.active { li.active {
box-shadow: inset 4px 0 0 $active-border;
> a { > a {
color: $active-color;
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
} }
svg {
fill: $active-color;
}
} }
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
......
...@@ -226,6 +226,14 @@ ...@@ -226,6 +226,14 @@
vertical-align: baseline; vertical-align: baseline;
} }
a.autodevops-badge {
color: $white-light;
}
a.autodevops-link {
color: $gl-link-color;
}
.commit-row-description { .commit-row-description {
font-size: 14px; font-size: 14px;
padding: 10px 15px; padding: 10px 15px;
......
...@@ -202,6 +202,10 @@ ...@@ -202,6 +202,10 @@
.btn-group.open .dropdown-toggle { .btn-group.open .dropdown-toggle {
box-shadow: none; box-shadow: none;
} }
.pipeline-tags .label-container {
white-space: normal;
}
} }
.stage-cell { .stage-cell {
...@@ -932,3 +936,8 @@ button.mini-pipeline-graph-dropdown-toggle { ...@@ -932,3 +936,8 @@ button.mini-pipeline-graph-dropdown-toggle {
.pipelines-container .top-area .nav-controls > .btn:last-child { .pipelines-container .top-area .nav-controls > .btn:last-child {
float: none; float: none;
} }
.autodevops-title {
font-weight: $gl-font-weight-normal;
line-height: 1.5;
}
@mixin application-theme-preview($color-1, $color-2, $color-3, $color-4) {
.one {
background-color: $color-1;
border-top-left-radius: $border-radius-default;
}
.two {
background-color: $color-2;
border-top-right-radius: $border-radius-default;
}
.three {
background-color: $color-3;
border-bottom-left-radius: $border-radius-default;
}
.four {
background-color: $color-4;
border-bottom-right-radius: $border-radius-default;
}
}
.application-theme {
label {
margin-right: 20px;
text-align: center;
}
.preview {
font-size: 0;
margin-bottom: 10px;
&.indigo {
@include application-theme-preview($indigo-900, $indigo-700, $indigo-800, $indigo-500);
}
&.dark {
@include application-theme-preview($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-600);
}
&.light {
@include application-theme-preview($theme-gray-600, $theme-gray-200, $theme-gray-400, $theme-gray-100);
}
&.blue {
@include application-theme-preview($theme-blue-900, $theme-blue-700, $theme-blue-800, $theme-blue-500);
}
&.green {
@include application-theme-preview($theme-green-900, $theme-green-700, $theme-green-800, $theme-green-500);
}
}
.preview-row {
display: block;
}
.quadrant {
display: inline-block;
height: 50px;
width: 80px;
}
}
.syntax-theme { .syntax-theme {
label { label {
margin-right: 20px; margin-right: 20px;
......
...@@ -10,41 +10,6 @@ ...@@ -10,41 +10,6 @@
.edit-project, .edit-project,
.import-project { .import-project {
.sharing-and-permissions {
.header {
padding-top: $gl-vert-padding;
}
.label-light {
margin-bottom: 0;
}
.help-block {
margin-top: 0;
}
.form-group {
margin-bottom: 5px;
}
> .form-group {
padding-left: 0;
}
select option[disabled] {
display: none;
}
}
select {
transition: background 2s ease-out;
&.highlight-changes {
background: $highlight-changes-color;
transition: none;
}
}
.help-block { .help-block {
margin-bottom: 10px; margin-bottom: 10px;
} }
...@@ -90,6 +55,162 @@ ...@@ -90,6 +55,162 @@
} }
} }
.toggle-wrapper {
margin-top: 5px;
}
.project-feature-row > .toggle-wrapper {
margin: 10px 0;
}
.project-visibility-setting,
.project-feature-settings {
border: 1px solid $border-color;
padding: 10px 32px;
@media (max-width: $screen-xs-min) {
padding: 10px 20px;
}
}
.project-visibility-setting .request-access {
line-height: 2;
}
.project-feature-settings {
background: $gray-lighter;
border-top: none;
margin-bottom: 16px;
}
.project-repo-select {
transition: background 2s ease-out;
&:disabled {
opacity: 0.75;
}
.highlight-changes & {
background: $highlight-changes-color;
transition: none;
}
}
.project-feature-controls {
display: flex;
align-items: center;
margin: 8px 0;
max-width: 432px;
.toggle-wrapper {
flex: 0;
margin-right: 10px;
}
.select-wrapper {
flex: 1;
}
}
.project-feature-setting-group {
padding-left: 32px;
.project-feature-controls {
max-width: 400px;
}
@media (max-width: $screen-xs-min) {
padding-left: 20px;
}
}
.project-feature-toggle {
position: relative;
border: none;
outline: 0;
display: block;
width: 100px;
height: 24px;
cursor: pointer;
user-select: none;
background: $feature-toggle-color-disabled;
border-radius: 12px;
padding: 3px;
transition: all .4s ease;
&::selection,
&::before::selection,
&::after::selection {
background: none;
}
&::before {
color: $feature-toggle-text-color;
font-size: 12px;
line-height: 24px;
position: absolute;
top: 0;
left: 25px;
right: 5px;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
animation: animate-disabled .2s ease-in;
content: attr(data-disabled-text);
}
&::after {
position: relative;
display: block;
content: "";
width: 22px;
height: 18px;
left: 0;
border-radius: 9px;
background: $feature-toggle-color;
transition: all .2s ease;
}
&.checked {
background: $feature-toggle-color-enabled;
&::before {
left: 5px;
right: 25px;
animation: animate-enabled .2s ease-in;
content: attr(data-enabled-text);
}
&::after {
left: calc(100% - 22px);
}
}
&.disabled {
opacity: 0.4;
cursor: not-allowed;
}
@media (max-width: $screen-xs-min) {
width: 50px;
&::before,
&.checked::before {
display: none;
}
}
@keyframes animate-enabled {
0%, 35% { opacity: 0; }
100% { opacity: 1; }
}
@keyframes animate-disabled {
0%, 35% { opacity: 0; }
100% { opacity: 1; }
}
}
.project-home-panel, .project-home-panel,
.group-home-panel { .group-home-panel {
padding-top: 24px; padding-top: 24px;
......
...@@ -43,8 +43,10 @@ ...@@ -43,8 +43,10 @@
display: inline-block; display: inline-block;
} }
.blob-viewer[data-type="rich"] { @media (min-width: $screen-md-min) {
.blob-viewer[data-type="rich"] {
margin: 20px; margin: 20px;
}
} }
.repository-view { .repository-view {
......
...@@ -2,7 +2,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController ...@@ -2,7 +2,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController
before_action :finder, only: [:edit, :update, :destroy] before_action :finder, only: [:edit, :update, :destroy]
def index def index
@broadcast_messages = BroadcastMessage.reorder("ends_at DESC").page(params[:page]) @broadcast_messages = BroadcastMessage.order(ends_at: :desc).page(params[:page])
@broadcast_message = BroadcastMessage.new @broadcast_message = BroadcastMessage.new
end end
......
class Admin::DashboardController < Admin::ApplicationController class Admin::DashboardController < Admin::ApplicationController
def index def index
@projects = Project.without_deleted.with_route.limit(10) @projects = Project.order_id_desc.without_deleted.with_route.limit(10)
@users = User.limit(10) @users = User.order_id_desc.limit(10)
@groups = Group.with_route.limit(10) @groups = Group.order_id_desc.with_route.limit(10)
end end
end end
...@@ -17,7 +17,7 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -17,7 +17,7 @@ class Admin::UsersController < Admin::ApplicationController
end end
def keys def keys
@keys = user.keys @keys = user.keys.order_id_desc
end end
def new def new
...@@ -211,6 +211,7 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -211,6 +211,7 @@ class Admin::UsersController < Admin::ApplicationController
:provider, :provider,
:remember_me, :remember_me,
:skype, :skype,
:theme_id,
:twitter, :twitter,
:username, :username,
:website_url :website_url
......
class Dashboard::GroupsController < Dashboard::ApplicationController class Dashboard::GroupsController < Dashboard::ApplicationController
def index def index
@sort = params[:sort] || 'id_desc'
@groups = @groups =
if params[:parent_id] && Group.supports_nested_groups? if params[:parent_id] && Group.supports_nested_groups?
parent = Group.find_by(id: params[:parent_id]) parent = Group.find_by(id: params[:parent_id])
...@@ -15,7 +17,7 @@ class Dashboard::GroupsController < Dashboard::ApplicationController ...@@ -15,7 +17,7 @@ class Dashboard::GroupsController < Dashboard::ApplicationController
@groups = @groups.search(params[:filter_groups]) if params[:filter_groups].present? @groups = @groups.search(params[:filter_groups]) if params[:filter_groups].present?
@groups = @groups.includes(:route) @groups = @groups.includes(:route)
@groups = @groups.sort(@sort = params[:sort]) @groups = @groups.sort(@sort)
@groups = @groups.page(params[:page]) @groups = @groups.page(params[:page])
respond_to do |format| respond_to do |format|
......
class Profiles::EmailsController < Profiles::ApplicationController class Profiles::EmailsController < Profiles::ApplicationController
def index def index
@primary = current_user.email @primary = current_user.email
@emails = current_user.emails @emails = current_user.emails.order_id_desc
end end
def create def create
......
...@@ -2,7 +2,7 @@ class Profiles::KeysController < Profiles::ApplicationController ...@@ -2,7 +2,7 @@ class Profiles::KeysController < Profiles::ApplicationController
skip_before_action :authenticate_user!, only: [:get_keys] skip_before_action :authenticate_user!, only: [:get_keys]
def index def index
@keys = current_user.keys @keys = current_user.keys.order_id_desc
@key = Key.new @key = Key.new
end end
......
...@@ -35,7 +35,8 @@ class Profiles::PreferencesController < Profiles::ApplicationController ...@@ -35,7 +35,8 @@ class Profiles::PreferencesController < Profiles::ApplicationController
:color_scheme_id, :color_scheme_id,
:layout, :layout,
:dashboard, :dashboard,
:project_view :project_view,
:theme_id
) )
end end
end end
...@@ -27,7 +27,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic ...@@ -27,7 +27,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
@merge_request.merge_request_diff @merge_request.merge_request_diff
end end
@merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff @merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff.order_id_desc
@comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id } @comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id }
if params[:start_sha].present? if params[:start_sha].present?
......
...@@ -6,7 +6,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController ...@@ -6,7 +6,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
end end
def update def update
if @project.update_attributes(update_params) if @project.update(update_params)
flash[:notice] = "Pipelines settings for '#{@project.name}' were successfully updated." flash[:notice] = "Pipelines settings for '#{@project.name}' were successfully updated."
redirect_to project_settings_ci_cd_path(@project) redirect_to project_settings_ci_cd_path(@project)
else else
...@@ -16,14 +16,12 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController ...@@ -16,14 +16,12 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
private private
def create_params
params.require(:pipeline).permit(:ref)
end
def update_params def update_params
params.require(:project).permit( params.require(:project).permit(
:runners_token, :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex, :runners_token, :builds_enabled, :build_allow_git_fetch,
:public_builds, :auto_cancel_pending_pipelines, :ci_config_path :build_timeout_in_minutes, :build_coverage_regex, :public_builds,
:auto_cancel_pending_pipelines, :ci_config_path,
auto_devops_attributes: [:id, :domain, :enabled]
) )
end end
end end
...@@ -47,6 +47,10 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -47,6 +47,10 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end end
end end
def import
@projects = current_user.authorized_projects.order_id_desc
end
def apply_import def apply_import
source_project = Project.find(params[:source_project_id]) source_project = Project.find(params[:source_project_id])
......
...@@ -8,6 +8,7 @@ module Projects ...@@ -8,6 +8,7 @@ module Projects
define_secret_variables define_secret_variables
define_triggers_variables define_triggers_variables
define_badges_variables define_badges_variables
define_auto_devops_variables
end end
private private
...@@ -42,6 +43,10 @@ module Projects ...@@ -42,6 +43,10 @@ module Projects
badge.new(@project, @ref).metadata badge.new(@project, @ref).metadata
end end
end end
def define_auto_devops_variables
@auto_devops = @project.auto_devops || ProjectAutoDevops.new
end
end end
end end
end end
...@@ -9,6 +9,7 @@ class MoveToProjectFinder ...@@ -9,6 +9,7 @@ class MoveToProjectFinder
projects = @user.projects_where_can_admin_issues projects = @user.projects_where_can_admin_issues
projects = projects.search(search) if search.present? projects = projects.search(search) if search.present?
projects = projects.excluding_project(from_project) projects = projects.excluding_project(from_project)
projects = projects.order_id_desc
# infinite scroll using offset # infinite scroll using offset
projects = projects.where('projects.id < ?', offset_id) if offset_id.present? projects = projects.where('projects.id < ?', offset_id) if offset_id.present?
......
...@@ -121,7 +121,7 @@ class ProjectsFinder < UnionFinder ...@@ -121,7 +121,7 @@ class ProjectsFinder < UnionFinder
end end
def sort(items) def sort(items)
params[:sort].present? ? items.sort(params[:sort]) : items params[:sort].present? ? items.sort(params[:sort]) : items.order_id_desc
end end
def by_archived(projects) def by_archived(projects)
......
...@@ -118,7 +118,7 @@ class TodosFinder ...@@ -118,7 +118,7 @@ class TodosFinder
end end
def sort(items) def sort(items)
params[:sort] ? items.sort(params[:sort]) : items.reorder(id: :desc) params[:sort] ? items.sort(params[:sort]) : items.order_id_desc
end end
def by_action(items) def by_action(items)
......
...@@ -115,6 +115,7 @@ module ApplicationSettingsHelper ...@@ -115,6 +115,7 @@ module ApplicationSettingsHelper
:after_sign_up_text, :after_sign_up_text,
:akismet_api_key, :akismet_api_key,
:akismet_enabled, :akismet_enabled,
:auto_devops_enabled,
:clientside_sentry_dsn, :clientside_sentry_dsn,
:clientside_sentry_enabled, :clientside_sentry_enabled,
:container_registry_token_expire_delay, :container_registry_token_expire_delay,
......
module AutoDevopsHelper
def show_auto_devops_callout?(project)
show_callout?('auto_devops_settings_dismissed') &&
can?(current_user, :admin_pipeline, project) &&
project.has_auto_devops_implicitly_disabled?
end
end
...@@ -3,6 +3,10 @@ module GroupsHelper ...@@ -3,6 +3,10 @@ module GroupsHelper
can?(current_user, :change_visibility_level, group) can?(current_user, :change_visibility_level, group)
end end
def can_change_share_with_group_lock?(group)
can?(current_user, :change_share_with_group_lock, group)
end
def group_icon(group) def group_icon(group)
if group.is_a?(String) if group.is_a?(String)
group = Group.find_by_full_path(group) group = Group.find_by_full_path(group)
...@@ -65,6 +69,20 @@ module GroupsHelper ...@@ -65,6 +69,20 @@ module GroupsHelper
{ group_name: group.name } { group_name: group.name }
end end
def share_with_group_lock_help_text(group)
return default_help unless group.parent&.share_with_group_lock?
if group.share_with_group_lock?
if can?(current_user, :change_share_with_group_lock, group.parent)
ancestor_locked_but_you_can_override(group)
else
ancestor_locked_so_ask_the_owner(group)
end
else
ancestor_locked_and_has_been_overridden(group)
end
end
private private
def group_title_link(group, hidable: false, show_avatar: false) def group_title_link(group, hidable: false, show_avatar: false)
...@@ -80,4 +98,45 @@ module GroupsHelper ...@@ -80,4 +98,45 @@ module GroupsHelper
output.html_safe output.html_safe
end end
end end
def ancestor_group(group)
ancestor = oldest_consecutively_locked_ancestor(group)
if can?(current_user, :read_group, ancestor)
link_to ancestor.name, group_path(ancestor)
else
ancestor.name
end
end
def remove_the_share_with_group_lock_from_ancestor(group)
ancestor = oldest_consecutively_locked_ancestor(group)
text = s_("GroupSettings|remove the share with group lock from %{ancestor_group_name}") % { ancestor_group_name: ancestor.name }
if can?(current_user, :admin_group, ancestor)
link_to text, edit_group_path(ancestor)
else
text
end
end
def oldest_consecutively_locked_ancestor(group)
group.ancestors.find do |group|
!group.has_parent? || !group.parent.share_with_group_lock?
end
end
def default_help
s_("GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner.")
end
def ancestor_locked_but_you_can_override(group)
s_("GroupSettings|This setting is applied on %{ancestor_group}. You can override the setting or %{remove_ancestor_share_with_group_lock}.").html_safe % { ancestor_group: ancestor_group(group), remove_ancestor_share_with_group_lock: remove_the_share_with_group_lock_from_ancestor(group) }
end
def ancestor_locked_so_ask_the_owner(group)
s_("GroupSettings|This setting is applied on %{ancestor_group}. To share projects in this group with another group, ask the owner to override the setting or %{remove_ancestor_share_with_group_lock}.").html_safe % { ancestor_group: ancestor_group(group), remove_ancestor_share_with_group_lock: remove_the_share_with_group_lock_from_ancestor(group) }
end
def ancestor_locked_and_has_been_overridden(group)
s_("GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup.").html_safe % { ancestor_group: ancestor_group(group) }
end
end end
...@@ -56,7 +56,7 @@ module IssuesHelper ...@@ -56,7 +56,7 @@ module IssuesHelper
end end
def project_options(issuable, current_user, ability: :read_project) def project_options(issuable, current_user, ability: :read_project)
projects = current_user.authorized_projects projects = current_user.authorized_projects.order_id_desc
projects = projects.select do |project| projects = projects.select do |project|
current_user.can?(ability, project) current_user.can?(ability, project)
end end
......
...@@ -40,6 +40,10 @@ module PreferencesHelper ...@@ -40,6 +40,10 @@ module PreferencesHelper
] ]
end end
def user_application_theme
@user_application_theme ||= Gitlab::Themes.for_user(current_user).css_class
end
def user_color_scheme def user_color_scheme
Gitlab::ColorSchemes.for_user(current_user).css_class Gitlab::ColorSchemes.for_user(current_user).css_class
end end
......
...@@ -15,9 +15,13 @@ module ProjectsHelper ...@@ -15,9 +15,13 @@ module ProjectsHelper
end end
def link_to_member_avatar(author, opts = {}) def link_to_member_avatar(author, opts = {})
default_opts = { avatar: true, name: true, size: 16, author_class: 'author', title: ":name" } default_opts = { size: 16 }
opts = default_opts.merge(opts) opts = default_opts.merge(opts)
image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]}", alt: '') if opts[:avatar]
classes = %W[avatar avatar-inline s#{opts[:size]}]
classes << opts[:avatar_class] if opts[:avatar_class]
image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: classes, alt: '')
end end
def link_to_member(project, author, opts = {}, &block) def link_to_member(project, author, opts = {}, &block)
...@@ -29,7 +33,7 @@ module ProjectsHelper ...@@ -29,7 +33,7 @@ module ProjectsHelper
author_html = "" author_html = ""
# Build avatar image tag # Build avatar image tag
author_html << image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]} #{opts[:avatar_class] if opts[:avatar_class]}", alt: '') if opts[:avatar] author_html << link_to_member_avatar(author, opts) if opts[:avatar]
# Build name span tag # Build name span tag
if opts[:by_username] if opts[:by_username]
...@@ -541,6 +545,43 @@ module ProjectsHelper ...@@ -541,6 +545,43 @@ module ProjectsHelper
current_application_settings.restricted_visibility_levels || [] current_application_settings.restricted_visibility_levels || []
end end
def project_permissions_settings(project)
feature = project.project_feature
{
visibilityLevel: project.visibility_level,
requestAccessEnabled: !!project.request_access_enabled,
issuesAccessLevel: feature.issues_access_level,
repositoryAccessLevel: feature.repository_access_level,
mergeRequestsAccessLevel: feature.merge_requests_access_level,
buildsAccessLevel: feature.builds_access_level,
wikiAccessLevel: feature.wiki_access_level,
snippetsAccessLevel: feature.snippets_access_level,
containerRegistryEnabled: !!project.container_registry_enabled,
lfsEnabled: !!project.lfs_enabled
}
end
def project_permissions_panel_data(project)
data = {
currentSettings: project_permissions_settings(project),
canChangeVisibilityLevel: can_change_visibility_level?(project, current_user),
allowedVisibilityOptions: project_allowed_visibility_levels(project),
visibilityHelpPath: help_page_path('public_access/public_access'),
registryAvailable: Gitlab.config.registry.enabled,
registryHelpPath: help_page_path('user/project/container_registry'),
lfsAvailable: Gitlab.config.lfs.enabled && current_user.admin?,
lfsHelpPath: help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
}
data.to_json.html_safe
end
def project_allowed_visibility_levels(project)
Gitlab::VisibilityLevel.values.select do |level|
project.visibility_level_allowed?(level) && !restricted_levels.include?(level)
end
end
def find_file_path def find_file_path
return unless @project && !@project.empty_repo? return unless @project && !@project.empty_repo?
......
...@@ -92,7 +92,7 @@ module SearchHelper ...@@ -92,7 +92,7 @@ module SearchHelper
# Autocomplete results for the current user's groups # Autocomplete results for the current user's groups
def groups_autocomplete(term, limit = 5) def groups_autocomplete(term, limit = 5)
current_user.authorized_groups.search(term).limit(limit).map do |group| current_user.authorized_groups.order_id_desc.search(term).limit(limit).map do |group|
{ {
category: "Groups", category: "Groups",
id: group.id, id: group.id,
...@@ -104,7 +104,7 @@ module SearchHelper ...@@ -104,7 +104,7 @@ module SearchHelper
# Autocomplete results for the current user's projects # Autocomplete results for the current user's projects
def projects_autocomplete(term, limit = 5) def projects_autocomplete(term, limit = 5)
current_user.authorized_projects.search_by_title(term) current_user.authorized_projects.order_id_desc.search_by_title(term)
.sorted_by_stars.non_archived.limit(limit).map do |p| .sorted_by_stars.non_archived.limit(limit).map do |p|
{ {
category: "Projects", category: "Projects",
......
...@@ -33,7 +33,7 @@ class BroadcastMessage < ActiveRecord::Base ...@@ -33,7 +33,7 @@ class BroadcastMessage < ActiveRecord::Base
end end
def self.current_and_future_messages def self.current_and_future_messages
where('ends_at > :now', now: Time.zone.now).reorder(id: :asc) where('ends_at > :now', now: Time.zone.now).order_id_asc
end end
def active? def active?
......
...@@ -216,6 +216,7 @@ module Ci ...@@ -216,6 +216,7 @@ module Ci
variables += runner.predefined_variables if runner variables += runner.predefined_variables if runner
variables += project.container_registry_variables variables += project.container_registry_variables
variables += project.deployment_variables if has_environment? variables += project.deployment_variables if has_environment?
variables += project.auto_devops_variables
variables += yaml_variables variables += yaml_variables
variables += user_variables variables += user_variables
variables += project.group.secret_variables_for(ref, project).map(&:to_runner_variable) if project.group variables += project.group.secret_variables_for(ref, project).map(&:to_runner_variable) if project.group
......
...@@ -38,6 +38,7 @@ module Ci ...@@ -38,6 +38,7 @@ module Ci
validates :status, presence: { unless: :importing? } validates :status, presence: { unless: :importing? }
validate :valid_commit_sha, unless: :importing? validate :valid_commit_sha, unless: :importing?
after_initialize :set_config_source, if: :new_record?
after_create :keep_around_commits, unless: :importing? after_create :keep_around_commits, unless: :importing?
enum source: { enum source: {
...@@ -50,6 +51,12 @@ module Ci ...@@ -50,6 +51,12 @@ module Ci
external: 6 external: 6
} }
enum config_source: {
unknown_source: nil,
repository_source: 1,
auto_devops_source: 2
}
state_machine :status, initial: :created do state_machine :status, initial: :created do
event :enqueue do event :enqueue do
transition created: :pending transition created: :pending
...@@ -316,6 +323,14 @@ module Ci ...@@ -316,6 +323,14 @@ module Ci
builds.latest.failed_but_allowed.any? builds.latest.failed_but_allowed.any?
end end
def set_config_source
if ci_yaml_from_repo
self.config_source = :repository_source
elsif implied_ci_yaml_file
self.config_source = :auto_devops_source
end
end
def config_processor def config_processor
return unless ci_yaml_file return unless ci_yaml_file
return @config_processor if defined?(@config_processor) return @config_processor if defined?(@config_processor)
...@@ -342,11 +357,17 @@ module Ci ...@@ -342,11 +357,17 @@ module Ci
def ci_yaml_file def ci_yaml_file
return @ci_yaml_file if defined?(@ci_yaml_file) return @ci_yaml_file if defined?(@ci_yaml_file)
@ci_yaml_file = begin @ci_yaml_file =
project.repository.gitlab_ci_yml_for(sha, ci_yaml_file_path) if auto_devops_source?
rescue Rugged::ReferenceError, GRPC::NotFound, GRPC::Internal implied_ci_yaml_file
self.yaml_errors = else
"Failed to load CI/CD config file at #{ci_yaml_file_path}" ci_yaml_from_repo
end
if @ci_yaml_file
@ci_yaml_file
else
self.yaml_errors = "Failed to load CI/CD config file for #{sha}"
nil nil
end end
end end
...@@ -434,6 +455,23 @@ module Ci ...@@ -434,6 +455,23 @@ module Ci
private private
def ci_yaml_from_repo
return unless project
return unless sha
project.repository.gitlab_ci_yml_for(sha, ci_yaml_file_path)
rescue GRPC::NotFound, Rugged::ReferenceError, GRPC::Internal
nil
end
def implied_ci_yaml_file
return unless project
if project.auto_devops_enabled?
Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content
end
end
def pipeline_data def pipeline_data
Gitlab::DataBuilder::Pipeline.build(self) Gitlab::DataBuilder::Pipeline.build(self)
end end
......
...@@ -6,10 +6,6 @@ module Sortable ...@@ -6,10 +6,6 @@ module Sortable
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do included do
# By default all models should be ordered
# by created_at field starting from newest
default_scope { order_id_desc }
scope :order_id_desc, -> { reorder(id: :desc) } scope :order_id_desc, -> { reorder(id: :desc) }
scope :order_id_asc, -> { reorder(id: :asc) } scope :order_id_asc, -> { reorder(id: :asc) }
scope :order_created_desc, -> { reorder(created_at: :desc) } scope :order_created_desc, -> { reorder(created_at: :desc) }
......
...@@ -44,6 +44,10 @@ class Namespace < ActiveRecord::Base ...@@ -44,6 +44,10 @@ class Namespace < ActiveRecord::Base
after_commit :refresh_access_of_projects_invited_groups, on: :update, if: -> { previous_changes.key?('share_with_group_lock') } after_commit :refresh_access_of_projects_invited_groups, on: :update, if: -> { previous_changes.key?('share_with_group_lock') }
before_create :sync_share_with_group_lock_with_parent
before_update :sync_share_with_group_lock_with_parent, if: :parent_changed?
after_update :force_share_with_group_lock_on_descendants, if: -> { share_with_group_lock_changed? && share_with_group_lock? }
# Legacy Storage specific hooks # Legacy Storage specific hooks
after_update :move_dir, if: :path_changed? after_update :move_dir, if: :path_changed?
...@@ -219,4 +223,14 @@ class Namespace < ActiveRecord::Base ...@@ -219,4 +223,14 @@ class Namespace < ActiveRecord::Base
errors.add(:parent_id, "has too deep level of nesting") errors.add(:parent_id, "has too deep level of nesting")
end end
end end
def sync_share_with_group_lock_with_parent
if parent&.share_with_group_lock?
self.share_with_group_lock = true
end
end
def force_share_with_group_lock_on_descendants
descendants.update_all(share_with_group_lock: true)
end
end end
...@@ -187,9 +187,12 @@ class Project < ActiveRecord::Base ...@@ -187,9 +187,12 @@ class Project < ActiveRecord::Base
has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner' has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
has_one :auto_devops, class_name: 'ProjectAutoDevops'
accepts_nested_attributes_for :variables, allow_destroy: true accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature accepts_nested_attributes_for :project_feature
accepts_nested_attributes_for :import_data accepts_nested_attributes_for :import_data
accepts_nested_attributes_for :auto_devops
delegate :name, to: :owner, allow_nil: true, prefix: true delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true delegate :members, to: :team, prefix: true
...@@ -466,6 +469,18 @@ class Project < ActiveRecord::Base ...@@ -466,6 +469,18 @@ class Project < ActiveRecord::Base
self[:lfs_enabled] && Gitlab.config.lfs.enabled self[:lfs_enabled] && Gitlab.config.lfs.enabled
end end
def auto_devops_enabled?
if auto_devops&.enabled.nil?
current_application_settings.auto_devops_enabled?
else
auto_devops.enabled?
end
end
def has_auto_devops_implicitly_disabled?
auto_devops&.enabled.nil? && !current_application_settings.auto_devops_enabled?
end
def repository_storage_path def repository_storage_path
Gitlab.config.repositories.storages[repository_storage].try(:[], 'path') Gitlab.config.repositories.storages[repository_storage].try(:[], 'path')
end end
...@@ -1378,6 +1393,10 @@ class Project < ActiveRecord::Base ...@@ -1378,6 +1393,10 @@ class Project < ActiveRecord::Base
Gitlab::Utils.slugify(full_path.to_s) Gitlab::Utils.slugify(full_path.to_s)
end end
def has_ci?
repository.gitlab_ci_yml || auto_devops_enabled?
end
def predefined_variables def predefined_variables
[ [
{ key: 'CI_PROJECT_ID', value: id.to_s, public: true }, { key: 'CI_PROJECT_ID', value: id.to_s, public: true },
...@@ -1423,6 +1442,12 @@ class Project < ActiveRecord::Base ...@@ -1423,6 +1442,12 @@ class Project < ActiveRecord::Base
deployment_service.predefined_variables deployment_service.predefined_variables
end end
def auto_devops_variables
return [] unless auto_devops_enabled?
auto_devops&.variables || []
end
def append_or_update_attribute(name, value) def append_or_update_attribute(name, value)
old_values = public_send(name.to_s) # rubocop:disable GitlabSecurity/PublicSend old_values = public_send(name.to_s) # rubocop:disable GitlabSecurity/PublicSend
......
class ProjectAutoDevops < ActiveRecord::Base
belongs_to :project
validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true }
def variables
variables = []
variables << { key: 'AUTO_DEVOPS_DOMAIN', value: domain, public: true } if domain.present?
variables
end
end
...@@ -35,6 +35,7 @@ class User < ActiveRecord::Base ...@@ -35,6 +35,7 @@ class User < ActiveRecord::Base
default_value_for :project_view, :files default_value_for :project_view, :files
default_value_for :notified_of_own_activity, false default_value_for :notified_of_own_activity, false
default_value_for :preferred_language, I18n.default_locale default_value_for :preferred_language, I18n.default_locale
default_value_for :theme_id, gitlab_config.default_theme
attr_encrypted :otp_secret, attr_encrypted :otp_secret,
key: Gitlab::Application.secrets.otp_key_base, key: Gitlab::Application.secrets.otp_key_base,
...@@ -72,7 +73,7 @@ class User < ActiveRecord::Base ...@@ -72,7 +73,7 @@ class User < ActiveRecord::Base
# #
# Namespace for personal projects # Namespace for personal projects
has_one :namespace, -> { where type: nil }, dependent: :destroy, foreign_key: :owner_id, autosave: true # rubocop:disable Cop/ActiveRecordDependent has_one :namespace, -> { where(type: nil) }, dependent: :destroy, foreign_key: :owner_id, autosave: true # rubocop:disable Cop/ActiveRecordDependent
# Profile # Profile
has_many :keys, -> do has_many :keys, -> do
...@@ -259,11 +260,13 @@ class User < ActiveRecord::Base ...@@ -259,11 +260,13 @@ class User < ActiveRecord::Base
end end
def sort(method) def sort(method)
case method.to_s order_method = method || 'id_desc'
case order_method.to_s
when 'recent_sign_in' then order_recent_sign_in when 'recent_sign_in' then order_recent_sign_in
when 'oldest_sign_in' then order_oldest_sign_in when 'oldest_sign_in' then order_oldest_sign_in
else else
order_by(method) order_by(order_method)
end end
end end
...@@ -371,7 +374,7 @@ class User < ActiveRecord::Base ...@@ -371,7 +374,7 @@ class User < ActiveRecord::Base
# Returns a user for the given SSH key. # Returns a user for the given SSH key.
def find_by_ssh_key_id(key_id) def find_by_ssh_key_id(key_id)
find_by(id: Key.unscoped.select(:user_id).where(id: key_id)) Key.find_by(id: key_id)&.user
end end
def find_by_full_path(path, follow_redirects: false) def find_by_full_path(path, follow_redirects: false)
......
...@@ -15,6 +15,11 @@ class GroupPolicy < BasePolicy ...@@ -15,6 +15,11 @@ class GroupPolicy < BasePolicy
condition(:nested_groups_supported, scope: :global) { Group.supports_nested_groups? } condition(:nested_groups_supported, scope: :global) { Group.supports_nested_groups? }
condition(:has_parent, scope: :subject) { @subject.has_parent? }
condition(:share_with_group_locked, scope: :subject) { @subject.share_with_group_lock? }
condition(:parent_share_with_group_locked, scope: :subject) { @subject.parent&.share_with_group_lock? }
condition(:can_change_parent_share_with_group_lock) { can?(:change_share_with_group_lock, @subject.parent) }
condition(:has_projects) do condition(:has_projects) do
GroupProjectsFinder.new(group: @subject, current_user: @user).execute.any? GroupProjectsFinder.new(group: @subject, current_user: @user).execute.any?
end end
...@@ -54,6 +59,8 @@ class GroupPolicy < BasePolicy ...@@ -54,6 +59,8 @@ class GroupPolicy < BasePolicy
rule { ~can?(:view_globally) }.prevent :request_access rule { ~can?(:view_globally) }.prevent :request_access
rule { has_access }.prevent :request_access rule { has_access }.prevent :request_access
rule { owner & (~share_with_group_locked | ~has_parent | ~parent_share_with_group_locked | can_change_parent_share_with_group_lock) }.enable :change_share_with_group_lock
def access_level def access_level
return GroupMember::NO_ACCESS if @user.nil? return GroupMember::NO_ACCESS if @user.nil?
......
...@@ -16,6 +16,7 @@ class PipelineEntity < Grape::Entity ...@@ -16,6 +16,7 @@ class PipelineEntity < Grape::Entity
expose :flags do expose :flags do
expose :latest?, as: :latest expose :latest?, as: :latest
expose :stuck?, as: :stuck expose :stuck?, as: :stuck
expose :auto_devops_source?, as: :auto_devops
expose :has_yaml_errors?, as: :yaml_errors expose :has_yaml_errors?, as: :yaml_errors
expose :can_retry?, as: :retryable expose :can_retry?, as: :retryable
expose :can_cancel?, as: :cancelable expose :can_cancel?, as: :cancelable
......
...@@ -16,8 +16,8 @@ module Ci ...@@ -16,8 +16,8 @@ module Ci
protected: project.protected_for?(ref) protected: project.protected_for?(ref)
) )
result = validate(current_user, result = validate_project_and_git_items ||
ignore_skip_ci: ignore_skip_ci, validate_pipeline(ignore_skip_ci: ignore_skip_ci,
save_on_errors: save_on_errors) save_on_errors: save_on_errors)
return result if result return result if result
...@@ -47,13 +47,13 @@ module Ci ...@@ -47,13 +47,13 @@ module Ci
private private
def validate(triggering_user, ignore_skip_ci:, save_on_errors:) def validate_project_and_git_items
unless project.builds_enabled? unless project.builds_enabled?
return error('Pipeline is disabled') return error('Pipeline is disabled')
end end
unless allowed_to_trigger_pipeline?(triggering_user) unless allowed_to_trigger_pipeline?
if can?(triggering_user, :create_pipeline, project) if can?(current_user, :create_pipeline, project)
return error("Insufficient permissions for protected ref '#{ref}'") return error("Insufficient permissions for protected ref '#{ref}'")
else else
return error('Insufficient permissions to create a new pipeline') return error('Insufficient permissions to create a new pipeline')
...@@ -67,7 +67,9 @@ module Ci ...@@ -67,7 +67,9 @@ module Ci
unless commit unless commit
return error('Commit not found') return error('Commit not found')
end end
end
def validate_pipeline(ignore_skip_ci:, save_on_errors:)
unless pipeline.config_processor unless pipeline.config_processor
unless pipeline.ci_yaml_file unless pipeline.ci_yaml_file
return error("Missing #{pipeline.ci_yaml_file_path} file") return error("Missing #{pipeline.ci_yaml_file_path} file")
...@@ -85,18 +87,18 @@ module Ci ...@@ -85,18 +87,18 @@ module Ci
end end
end end
def allowed_to_trigger_pipeline?(triggering_user) def allowed_to_trigger_pipeline?
if triggering_user if current_user
allowed_to_create?(triggering_user) allowed_to_create?
else # legacy triggers don't have a corresponding user else # legacy triggers don't have a corresponding user
!project.protected_for?(ref) !project.protected_for?(ref)
end end
end end
def allowed_to_create?(triggering_user) def allowed_to_create?
access = Gitlab::UserAccess.new(triggering_user, project: project) return unless can?(current_user, :create_pipeline, project)
can?(triggering_user, :create_pipeline, project) && access = Gitlab::UserAccess.new(current_user, project: project)
if branch? if branch?
access.can_update_branch?(ref) access.can_update_branch?(ref)
elsif tag? elsif tag?
......
module UpdateVisibilityLevel
def valid_visibility_level_change?(target, new_visibility)
# check that user is allowed to set specified visibility_level
if new_visibility && new_visibility.to_i != target.visibility_level
unless can?(current_user, :change_visibility_level, target) &&
Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
deny_visibility_level(target, new_visibility)
return false
end
end
true
end
end
module Groups module Groups
class UpdateService < Groups::BaseService class UpdateService < Groups::BaseService
include UpdateVisibilityLevel
def execute def execute
reject_parent_id! reject_parent_id!
# check that user is allowed to set specified visibility_level return false unless valid_visibility_level_change?(group, params[:visibility_level])
new_visibility = params[:visibility_level]
if new_visibility && new_visibility.to_i != group.visibility_level
unless can?(current_user, :change_visibility_level, group) &&
Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
deny_visibility_level(group, new_visibility) return false unless valid_share_with_group_lock_change?
return group
end
end
group.assign_attributes(params) group.assign_attributes(params)
...@@ -30,5 +25,19 @@ module Groups ...@@ -30,5 +25,19 @@ module Groups
def reject_parent_id! def reject_parent_id!
params.except!(:parent_id) params.except!(:parent_id)
end end
def valid_share_with_group_lock_change?
return true unless changing_share_with_group_lock?
return true if can?(current_user, :change_share_with_group_lock, group)
group.errors.add(:share_with_group_lock, s_('GroupSettings|cannot be disabled when the parent group "Share with group lock" is enabled, except by the owner of the parent group'))
false
end
def changing_share_with_group_lock?
return false if params[:share_with_group_lock].nil?
params[:share_with_group_lock] != group.share_with_group_lock
end
end end
end end
module Projects module Projects
class UpdateService < BaseService class UpdateService < BaseService
include UpdateVisibilityLevel
def execute def execute
unless visibility_level_allowed? unless valid_visibility_level_change?(project, params[:visibility_level])
return error('New visibility level not allowed!') return error('New visibility level not allowed!')
end end
...@@ -28,22 +30,6 @@ module Projects ...@@ -28,22 +30,6 @@ module Projects
private private
def visibility_level_allowed?
# check that user is allowed to set specified visibility_level
new_visibility = params[:visibility_level]
if new_visibility && new_visibility.to_i != project.visibility_level
unless can?(current_user, :change_visibility_level, project) &&
Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
deny_visibility_level(project, new_visibility)
return false
end
end
true
end
def renaming_project_with_container_registry_tags? def renaming_project_with_container_registry_tags?
new_path = params[:path] new_path = params[:path]
......
...@@ -115,7 +115,7 @@ module QuickActions ...@@ -115,7 +115,7 @@ module QuickActions
if issuable.allows_multiple_assignees? if issuable.allows_multiple_assignees?
issuable.assignees.pluck(:id) + users.map(&:id) issuable.assignees.pluck(:id) + users.map(&:id)
else else
[users.last.id] [users.first.id]
end end
end end
......
...@@ -9,18 +9,17 @@ module TestHooks ...@@ -9,18 +9,17 @@ module TestHooks
end end
def execute def execute
trigger_key = hook.class::TRIGGERS.key(trigger.to_sym)
trigger_data_method = "#{trigger}_data" trigger_data_method = "#{trigger}_data"
if !self.respond_to?(trigger_data_method, true) || if trigger_key.nil? || !self.respond_to?(trigger_data_method, true)
!hook.class::TRIGGERS.value?(trigger.to_sym)
return error('Testing not available for this hook') return error('Testing not available for this hook')
end end
error_message = catch(:validation_error) do error_message = catch(:validation_error) do
sample_data = self.__send__(trigger_data_method) # rubocop:disable GitlabSecurity/PublicSend sample_data = self.__send__(trigger_data_method) # rubocop:disable GitlabSecurity/PublicSend
return hook.execute(sample_data, trigger) return hook.execute(sample_data, trigger_key)
end end
error(error_message) error(error_message)
......
...@@ -19,7 +19,7 @@ class WebHookService ...@@ -19,7 +19,7 @@ class WebHookService
def initialize(hook, data, hook_name) def initialize(hook, data, hook_name)
@hook = hook @hook = hook
@data = data @data = data
@hook_name = hook_name @hook_name = hook_name.to_s
end end
def execute def execute
......
...@@ -226,7 +226,17 @@ ...@@ -226,7 +226,17 @@
.help-block 0 for unlimited .help-block 0 for unlimited
%fieldset %fieldset
%legend Continuous Integration %legend Continuous Integration and Deployment
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :auto_devops_enabled do
= f.check_box :auto_devops_enabled
Enabled Auto DevOps (Beta) for projects by default
.help-block
It will automatically build, test, and deploy applications based on a predefined CI/CD configuration
= link_to icon('question-circle'), help_page_path('topics/autodevops/index.md')
.form-group .form-group
.col-sm-offset-2.col-sm-10 .col-sm-offset-2.col-sm-10
.checkbox .checkbox
......
...@@ -28,17 +28,20 @@ ...@@ -28,17 +28,20 @@
.col-sm-offset-2.col-sm-10 .col-sm-offset-2.col-sm-10
= render 'shared/allow_request_access', form: f = render 'shared/allow_request_access', form: f
= render 'group_admin_settings', f: f
.form-group .form-group
%hr %label.control-label
= f.label :share_with_group_lock, class: 'control-label' do = s_("GroupSettings|Share with group lock")
Share with group lock
.col-sm-10 .col-sm-10
.checkbox .checkbox
= f.check_box :share_with_group_lock = f.label :share_with_group_lock do
%span.descr Prevent sharing a project with another group within this group = f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group)
%strong
- group_link = link_to @group.name, group_path(@group)
= s_("GroupSettings|Prevent sharing a project within %{group} with other groups").html_safe % { group: group_link }
%br
%span.descr= share_with_group_lock_help_text(@group)
= render 'group_admin_settings', f: f
.form-actions .form-actions
= f.submit 'Save group', class: "btn btn-save" = f.submit 'Save group', class: "btn btn-save"
......
!!! 5 !!! 5
%html{ lang: I18n.locale, class: page_class } %html{ lang: I18n.locale, class: page_class }
= render "layouts/head" = render "layouts/head"
%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: "#{user_application_theme} #{@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'
= render "layouts/header/default" = render "layouts/header/default"
......
%ul.list-unstyled.navbar-sub-nav %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 = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown header-projects" }) do
%a{ href: "#", data: { toggle: "dropdown" } } %a{ href: "#", data: { toggle: "dropdown" } }
Projects Projects
= custom_icon('caret_down') = custom_icon('caret_down')
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
= link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do
Snippets Snippets
%li.dropdown.hidden-lg %li.header-more.dropdown.hidden-lg
%a{ href: "#", data: { toggle: "dropdown" } } %a{ href: "#", data: { toggle: "dropdown" } }
More More
= custom_icon('caret_down') = custom_icon('caret_down')
......
...@@ -3,6 +3,26 @@ ...@@ -3,6 +3,26 @@
= render 'profiles/head' = render 'profiles/head'
= form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row prepend-top-default js-preferences-form' } do |f| = form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row prepend-top-default js-preferences-form' } do |f|
.col-lg-4.application-theme
%h4.prepend-top-0
GitLab navigation theme
%p Customize the appearance of the application header and navigation sidebar.
.col-lg-8.application-theme
- Gitlab::Themes.each do |theme|
= label_tag do
.preview{ class: theme.name.downcase }
.preview-row
.quadrant.one
.quadrant.two
.preview-row
.quadrant.three
.quadrant.four
= f.radio_button :theme_id, theme.id
= theme.name
.col-sm-12
%hr
.col-lg-4.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
%h4.prepend-top-0 %h4.prepend-top-0
Syntax highlighting theme Syntax highlighting theme
......
// Remove body class for any previous theme, re-add current one
$('body').removeClass('<%= Gitlab::Themes.body_classes %>')
$('body').addClass('<%= user_application_theme %>')
// Toggle container-fluid class // Toggle container-fluid class
if ('<%= current_user.layout %>' === 'fluid') { if ('<%= current_user.layout %>' === 'fluid') {
$('.content-wrapper .container-fluid').removeClass('container-limited') $('.content-wrapper .container-fluid').removeClass('container-limited')
......
- form = local_assigns.fetch(:form) - form = local_assigns.fetch(:form)
.form-group .form-group
.checkbox.builds-feature .checkbox.builds-feature{ class: ("hidden" if @project && @project.project_feature.send(:builds_access_level) == 0) }
= form.label :only_allow_merge_if_pipeline_succeeds do = form.label :only_allow_merge_if_pipeline_succeeds do
= form.check_box :only_allow_merge_if_pipeline_succeeds = form.check_box :only_allow_merge_if_pipeline_succeeds
%strong Only allow merge requests to be merged if the pipeline succeeds %strong Only allow merge requests to be merged if the pipeline succeeds
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
#commit-pipeline-table-view{ data: { disable_initialization: disable_initialization, #commit-pipeline-table-view{ data: { disable_initialization: disable_initialization,
endpoint: endpoint, endpoint: endpoint,
"help-page-path" => help_page_path('ci/quick_start/README'), "help-page-path" => help_page_path('ci/quick_start/README'),
"help-auto-devops-path" => help_page_path('topics/autodevops/index.md'),
} } } }
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
......
...@@ -66,90 +66,18 @@ ...@@ -66,90 +66,18 @@
%section.settings.sharing-permissions %section.settings.sharing-permissions
.settings-header .settings-header
%h4 %h4
Sharing and permissions Permissions
%button.btn.js-settings-toggle %button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
Enable or disable certain project features and choose access levels. Enable or disable certain project features and choose access levels.
.settings-content.no-animate{ class: ('expanded' if expanded) } .settings-content.no-animate{ class: ('expanded' if expanded) }
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f| = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f|
.form_group.sharing-and-permissions %script.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project)
.row.js-visibility-select .js-project-permissions-form
.col-md-8
.label-light
= label_tag :project_visibility, 'Project Visibility', class: 'label-light', for: :project_visibility_level
= link_to icon('question-circle'), help_page_path("public_access/public_access")
%span.help-block
.col-md-4.visibility-select-container
= render('projects/visibility_select', model_method: :visibility_level, form: f, selected_level: @project.visibility_level)
= f.fields_for :project_feature do |feature_fields|
%fieldset.features
.row
.col-md-8.project-feature
= feature_fields.label :repository_access_level, "Repository", class: 'label-light'
%span.help-block View and edit files in this project
.col-md-4.js-repo-access-level
= project_feature_access_select(:repository_access_level)
.row
.col-md-8.project-feature.nested
= feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light'
%span.help-block Submit changes to be merged upstream
.col-md-4
= project_feature_access_select(:merge_requests_access_level)
.row
.col-md-8.project-feature.nested
= feature_fields.label :builds_access_level, "Pipelines", class: 'label-light'
%span.help-block Build, test, and deploy your changes
.col-md-4
= project_feature_access_select(:builds_access_level)
.row
.col-md-8.project-feature
= feature_fields.label :snippets_access_level, "Snippets", class: 'label-light'
%span.help-block Share code pastes with others out of Git repository
.col-md-4
= project_feature_access_select(:snippets_access_level)
.row
.col-md-8.project-feature
= feature_fields.label :issues_access_level, "Issues", class: 'label-light'
%span.help-block Lightweight issue tracking system for this project
.col-md-4
= project_feature_access_select(:issues_access_level)
.row
.col-md-8.project-feature
= feature_fields.label :wiki_access_level, "Wiki", class: 'label-light'
%span.help-block Pages for project documentation
.col-md-4
= project_feature_access_select(:wiki_access_level)
.form-group
= render 'shared/allow_request_access', form: f
- if Gitlab.config.lfs.enabled && current_user.admin?
.row.js-lfs-enabled.form-group.sharing-and-permissions
.col-md-8
= f.label :lfs_enabled, 'Git Large File Storage', class: 'label-light'
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
%span.help-block Manages large files such as audio, video and graphics files.
.col-md-4
.select-wrapper
= f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control project-repo-select select-control', data: { field: 'lfs_enabled' }
= icon('chevron-down')
- if Gitlab.config.registry.enabled
.form-group.js-container-registry{ style: ("display: none;" if @project.project_feature.send(:repository_access_level) == 0) }
.checkbox
= f.label :container_registry_enabled do
= f.check_box :container_registry_enabled
%strong Container Registry
%br
%span.descr Enable Container Registry for this project
= link_to icon('question-circle'), help_page_path('user/project/container_registry'), target: '_blank'
= f.submit 'Save changes', class: "btn btn-save" = f.submit 'Save changes', class: "btn btn-save"
%section.settings.merge-requests-feature{ class: ("hidden" if @project.project_feature.send(:merge_requests_access_level) == 0) }
%section.settings.merge-requests-feature{ style: ("display: none;" if @project.project_feature.send(:merge_requests_access_level) == 0) }
.settings-header .settings-header
%h4 %h4
Merge request settings Merge request settings
......
...@@ -16,6 +16,8 @@ ...@@ -16,6 +16,8 @@
- if @project.merge_requests.exists? - if @project.merge_requests.exists?
%div{ class: container_class } %div{ class: container_class }
- if show_auto_devops_callout?(@project)
= render 'shared/auto_devops_callout'
.top-area .top-area
= render 'shared/issuable/nav', type: :merge_requests = render 'shared/issuable/nav', type: :merge_requests
.nav-controls .nav-controls
......
...@@ -2,9 +2,13 @@ ...@@ -2,9 +2,13 @@
- page_title "Pipelines" - page_title "Pipelines"
= render "projects/pipelines/head" = render "projects/pipelines/head"
#pipelines-list-vue{ data: { endpoint: project_pipelines_path(@project, format: :json), %div{ 'class' => container_class }
"css-class" => container_class, - if show_auto_devops_callout?(@project)
= render 'shared/auto_devops_callout'
#pipelines-list-vue{ data: { endpoint: project_pipelines_path(@project, format: :json),
"help-page-path" => help_page_path('ci/quick_start/README'), "help-page-path" => help_page_path('ci/quick_start/README'),
"help-auto-devops-path" => help_page_path('topics/autodevops/index.md'),
"new-pipeline-path" => new_project_pipeline_path(@project), "new-pipeline-path" => new_project_pipeline_path(@project),
"can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s, "can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s,
"all-path" => project_pipelines_path(@project), "all-path" => project_pipelines_path(@project),
...@@ -13,8 +17,8 @@ ...@@ -13,8 +17,8 @@
"finished-path" => project_pipelines_path(@project, scope: :finished), "finished-path" => project_pipelines_path(@project, scope: :finished),
"branches-path" => project_pipelines_path(@project, scope: :branches), "branches-path" => project_pipelines_path(@project, scope: :branches),
"tags-path" => project_pipelines_path(@project, scope: :tags), "tags-path" => project_pipelines_path(@project, scope: :tags),
"has-ci" => @repository.gitlab_ci_yml, "has-ci" => @project.has_ci?,
"ci-lint-path" => ci_lint_path } } "ci-lint-path" => ci_lint_path } }
= page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('pipelines') = page_specific_javascript_bundle_tag('pipelines')
...@@ -2,10 +2,41 @@ ...@@ -2,10 +2,41 @@
.col-lg-12 .col-lg-12
= form_for @project, url: project_pipelines_settings_path(@project) do |f| = form_for @project, url: project_pipelines_settings_path(@project) do |f|
%fieldset.builds-feature %fieldset.builds-feature
- unless @repository.gitlab_ci_yml
.form-group .form-group
%p Pipelines need to be configured before you can begin using Continuous Integration. %p Pipelines need to have Auto DevOps enabled or have a .gitlab-ci.yml configured before you can begin using Continuous Integration and Delivery.
= link_to 'Get started with Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info' %h5 Auto DevOps (Beta)
%p
Auto DevOps will automatically build, test, and deploy your application based on a predefined Continious Integration and Delivery configuration.
= link_to 'Learn more about Auto DevOps', help_page_path('topics/autodevops/index.md')
= f.fields_for :auto_devops_attributes, @auto_devops do |form|
.radio
= form.label :enabled_true do
= form.radio_button :enabled, 'true'
%strong Enable Auto DevOps
%br
%span.descr
The Auto DevOps pipeline configuration will be used when there is no .gitlab-ci.yml
in the project.
.radio
= form.label :enabled_false do
= form.radio_button :enabled, 'false'
%strong Disable Auto DevOps
%br
%span.descr
A specific .gitlab-ci.yml file needs to be specified before you can begin using Continious Integration and Delivery.
.radio
= form.label :enabled do
= form.radio_button :enabled, nil
%strong
Instance default (status: #{current_application_settings.auto_devops_enabled?})
%br
%span.descr
Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific .gitlab-ci.yml file specified.
%br
%p
Define a domain used by Auto DevOps to deploy towards, this is required for deploys to succeed.
= form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
%hr %hr
.form-group.append-bottom-default .form-group.append-bottom-default
= f.label :runners_token, "Runner token", class: 'label-light' = f.label :runners_token, "Runner token", class: 'label-light'
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
= form_tag apply_import_project_project_members_path(@project), method: 'post', class: 'form-horizontal' do = form_tag apply_import_project_project_members_path(@project), method: 'post', class: 'form-horizontal' do
.form-group .form-group
= label_tag :source_project_id, "Project", class: 'control-label' = label_tag :source_project_id, "Project", class: 'control-label'
.col-sm-10= select_tag(:source_project_id, options_from_collection_for_select(current_user.authorized_projects, :id, :name_with_namespace), prompt: "Select project", class: "select2 lg", required: true) .col-sm-10= select_tag(:source_project_id, options_from_collection_for_select(@projects, :id, :name_with_namespace), prompt: "Select project", class: "select2 lg", required: true)
.form-actions .form-actions
= button_tag 'Import project members', class: "btn btn-create" = button_tag 'Import project members', class: "btn btn-create"
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
- expanded = Rails.env.test? - expanded = Rails.env.test?
%section.settings %section.settings#js-general-pipeline-settings
.settings-header .settings-header
%h4 %h4
General pipelines settings General pipelines settings
......
...@@ -82,5 +82,8 @@ ...@@ -82,5 +82,8 @@
- view_path = default_project_view - view_path = default_project_view
- if show_auto_devops_callout?(@project)
= render 'shared/auto_devops_callout'
%div{ class: project_child_container_class(view_path) } %div{ class: project_child_container_class(view_path) }
= render view_path = render view_path
...@@ -14,5 +14,7 @@ ...@@ -14,5 +14,7 @@
= render "projects/commits/head" = render "projects/commits/head"
%div{ class: [container_class, ("limit-container-width" unless fluid_layout)] } %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
- if show_auto_devops_callout?(@project)
= render 'shared/auto_devops_callout'
= render 'projects/last_push' = render 'projects/last_push'
= render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_tree_path(@project, @id) = render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_tree_path(@project, @id)
.user-callout{ data: { uid: 'auto_devops_settings_dismissed', project_path: project_path(@project) } }
.bordered-box.landing.content-block
%button.btn.btn-default.close.js-close-callout{ type: 'button',
'aria-label' => 'Dismiss Auto DevOps box' }
= icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true')
.svg-container
= custom_icon('icon_autodevops')
.user-callout-copy
%h4= _('Auto DevOps (Beta)')
%p= _('Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.')
%p
#{s_('AutoDevOps|Learn more in the')}
= link_to _('Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer'
= link_to _('Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'), class: 'btn btn-primary js-close-callout'
This diff is collapsed.
---
title: Removes Sortable default scope.
merge_request: 13558
author:
type: fixed
---
title: Decrease Cyclomatic Complexity threshold to 14
merge_request: 13972
author: Maxim Rydkin
type: other
---
title: Redesign project feature permissions settings
merge_request: 14062
author:
type: changed
---
title: Add option in preferences to change navigation theme color
merge_request:
author:
type: added
---
title: Created callout for auto devops
merge_request:
author:
type: added
---
title: Fix a wrong `X-Gitlab-Event` header when testing webhooks
merge_request: 14108
author:
type: fixed
---
title: Make blob viewer for rich contents wider for mobile
merge_request: 14011
author: Takuya Noguchi
type: fixed
---
title: Update documentation for confidential issue
merge_request: 14117
author:
type: other
---
title: Gitaly feature toggles are on by default in development
merge_request: 13802
author:
type: other
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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