Commit 58c49d9c authored by Douwe Maan's avatar Douwe Maan

Merge branch 'master' into 'ee-dm-remove-copy-link-on-blame-page'

# Conflicts:
#   app/views/projects/blob/_header.html.haml
parents 4d89f081 8eea15c1
...@@ -333,7 +333,7 @@ migration paths: ...@@ -333,7 +333,7 @@ migration paths:
- master@gitlab/gitlabhq - master@gitlab/gitlabhq
- master@gitlab/gitlab-ee - master@gitlab/gitlab-ee
script: script:
- git fetch origin v8.5.9 - git fetch origin v8.14.10
- git checkout -f FETCH_HEAD - git checkout -f FETCH_HEAD
- cp config/resque.yml.example config/resque.yml - cp config/resque.yml.example config/resque.yml
- sed -i 's/localhost/redis/g' config/resque.yml - sed -i 's/localhost/redis/g' config/resque.yml
......
...@@ -154,6 +154,9 @@ gem 'sidekiq-cron', '~> 0.4.4' ...@@ -154,6 +154,9 @@ gem 'sidekiq-cron', '~> 0.4.4'
gem 'redis-namespace', '~> 1.5.2' gem 'redis-namespace', '~> 1.5.2'
gem 'sidekiq-limit_fetch', '~> 3.4' gem 'sidekiq-limit_fetch', '~> 3.4'
# Cron Parser
gem 'rufus-scheduler', '~> 3.1.10'
# HTTP requests # HTTP requests
gem 'httparty', '~> 0.13.3' gem 'httparty', '~> 0.13.3'
......
...@@ -1032,6 +1032,7 @@ DEPENDENCIES ...@@ -1032,6 +1032,7 @@ DEPENDENCIES
rubocop-rspec (~> 1.15.0) rubocop-rspec (~> 1.15.0)
ruby-fogbugz (~> 0.2.1) ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2) ruby-prof (~> 0.16.2)
rufus-scheduler (~> 3.1.10)
rugged (~> 0.25.1.1) rugged (~> 0.25.1.1)
sanitize (~> 2.0) sanitize (~> 2.0)
sass-rails (~> 5.0.6) sass-rails (~> 5.0.6)
......
...@@ -33,7 +33,7 @@ core team members will mention this person. ...@@ -33,7 +33,7 @@ core team members will mention this person.
### Merge request coaching ### Merge request coaching
Several people from the [GitLab team][team] are helping community members to get Several people from the [GitLab team][team] are helping community members to get
their contributions accepted by meeting our [Definition of done](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#definition-of-done). their contributions accepted by meeting our [Definition of done][done].
What you can expect from them is described at https://about.gitlab.com/jobs/merge-request-coach/. What you can expect from them is described at https://about.gitlab.com/jobs/merge-request-coach/.
...@@ -64,6 +64,49 @@ Merge requests may still be merged into master during this period, ...@@ -64,6 +64,49 @@ Merge requests may still be merged into master during this period,
but they will go into the _next_ release, unless they are manually cherry-picked into the stable branch. but they will go into the _next_ release, unless they are manually cherry-picked into the stable branch.
By freezing the stable branches 2 weeks prior to a release, we reduce the risk of a last minute merge request potentially breaking things. By freezing the stable branches 2 weeks prior to a release, we reduce the risk of a last minute merge request potentially breaking things.
### Between the 1st and the 7th
These types of merge requests need special consideration:
* **Large features**: a large feature is one that is highlighted in the kick-off
and the release blogpost; typically this will have its own channel in Slack
and a dedicated team with front-end, back-end, and UX.
* **Small features**: any other feature request.
**Large features** must be with a maintainer **by the 1st**. It's OK if they
aren't completely done, but this allows the maintainer enough time to make the
decision about whether this can make it in before the freeze. If the maintainer
doesn't think it will make it, they should inform the developers working on it
and the Product Manager responsible for the feature.
**Small features** must be with a reviewer (not necessarily maintainer) **by the
3rd**.
Most merge requests from the community do not have a specific release
target. However, if one does and falls into either of the above categories, it's
the reviewer's responsibility to manage the above communication and assignment
on behalf of the community member.
### On the 7th
Merge requests should still be complete, following the
[definition of done][done]. The single exception is documentation, and this can
only be left until after the freeze if:
* There is a follow-up issue to add documentation.
* It is assigned to the person writing documentation for this feature, and they
are aware of it.
* It is in the correct milestone, with the ~Deliverable label.
All Community Edition merge requests from GitLab team members merged on the
freeze date (the 7th) should have a corresponding Enterprise Edition merge
request, even if there are no conflicts. This is to reduce the size of the
subsequent EE merge, as we often merge a lot to CE on the release date. For more
information, see
[limit conflicts with EE when developing on CE][limit_ee_conflicts].
### Between the 7th and the 22nd
Once the stable branch is frozen, only fixes for regressions (bugs introduced in that same release) Once the stable branch is frozen, only fixes for regressions (bugs introduced in that same release)
and security issues will be cherry-picked into the stable branch. and security issues will be cherry-picked into the stable branch.
Any merge requests cherry-picked into the stable branch for a previous release will also be picked into the latest stable branch. Any merge requests cherry-picked into the stable branch for a previous release will also be picked into the latest stable branch.
...@@ -158,3 +201,5 @@ still an issue I encourage you to open it on the [GitLab.com issue tracker](http ...@@ -158,3 +201,5 @@ still an issue I encourage you to open it on the [GitLab.com issue tracker](http
[contribution acceptance criteria]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#contribution-acceptance-criteria [contribution acceptance criteria]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#contribution-acceptance-criteria
["Implement design & UI elements" guidelines]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#implement-design-ui-elements ["Implement design & UI elements" guidelines]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#implement-design-ui-elements
[Thoughtbot code review guide]: https://github.com/thoughtbot/guides/tree/master/code-review [Thoughtbot code review guide]: https://github.com/thoughtbot/guides/tree/master/code-review
[done]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#definition-of-done
[limit_ee_conflicts]: https://docs.gitlab.com/ce/development/limit_ee_conflicts.html
...@@ -51,7 +51,7 @@ function renderCategory(name, emojiList, opts = {}) { ...@@ -51,7 +51,7 @@ function renderCategory(name, emojiList, opts = {}) {
<h5 class="emoji-menu-title"> <h5 class="emoji-menu-title">
${name} ${name}
</h5> </h5>
<ul class="clearfix emoji-menu-list ${opts.menuListClass}"> <ul class="clearfix emoji-menu-list ${opts.menuListClass || ''}">
${emojiList.map(emojiName => ` ${emojiList.map(emojiName => `
<li class="emoji-menu-list-item"> <li class="emoji-menu-list-item">
<button class="emoji-menu-btn text-center js-emoji-btn" type="button"> <button class="emoji-menu-btn text-center js-emoji-btn" type="button">
...@@ -263,7 +263,8 @@ AwardsHandler.prototype.addAward = function addAward( ...@@ -263,7 +263,8 @@ AwardsHandler.prototype.addAward = function addAward(
this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality); this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality);
return typeof callback === 'function' ? callback() : undefined; return typeof callback === 'function' ? callback() : undefined;
}); });
return $('.emoji-menu').removeClass('is-visible'); $('.emoji-menu').removeClass('is-visible');
$('.js-add-award.is-active').removeClass('is-active');
}; };
AwardsHandler.prototype.addAwardToEmojiBar = function addAwardToEmojiBar( AwardsHandler.prototype.addAwardToEmojiBar = function addAwardToEmojiBar(
......
...@@ -20,6 +20,7 @@ import eventHub from '../eventhub'; ...@@ -20,6 +20,7 @@ import eventHub from '../eventhub';
list: { list: {
type: Object, type: Object,
required: false, required: false,
default: () => ({}),
}, },
rootPath: { rootPath: {
type: String, type: String,
...@@ -31,6 +32,26 @@ import eventHub from '../eventhub'; ...@@ -31,6 +32,26 @@ import eventHub from '../eventhub';
default: false, default: false,
}, },
}, },
computed: {
cardUrl() {
return `${this.issueLinkBase}/${this.issue.id}`;
},
assigneeUrl() {
return `${this.rootPath}${this.issue.assignee.username}`;
},
assigneeUrlTitle() {
return `Assigned to ${this.issue.assignee.name}`;
},
avatarUrlTitle() {
return `Avatar for ${this.issue.assignee.name}`;
},
issueId() {
return `#${this.issue.id}`;
},
showLabelFooter() {
return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
},
},
methods: { methods: {
showLabel(label) { showLabel(label) {
if (!this.list) return true; if (!this.list) return true;
...@@ -67,35 +88,41 @@ import eventHub from '../eventhub'; ...@@ -67,35 +88,41 @@ import eventHub from '../eventhub';
}, },
template: ` template: `
<div> <div>
<div class="card-header">
<h4 class="card-title"> <h4 class="card-title">
<i <i
class="fa fa-eye-slash confidential-icon" class="fa fa-eye-slash confidential-icon"
v-if="issue.confidential"></i> v-if="issue.confidential"
aria-hidden="true"
/>
<a <a
:href="issueLinkBase + '/' + issue.id" class="js-no-trigger"
:title="issue.title"> :href="cardUrl"
{{ issue.title }} :title="issue.title">{{ issue.title }}</a>
</a>
</h4>
<div class="card-footer">
<span <span
class="card-number" class="card-number"
v-if="issue.id"> v-if="issue.id"
#{{ issue.id }} >
{{ issueId }}
</span> </span>
</h4>
<a <a
class="card-assignee has-tooltip js-no-trigger" class="card-assignee has-tooltip js-no-trigger"
:href="rootPath + issue.assignee.username" :href="assigneeUrl"
:title="'Assigned to ' + issue.assignee.name" :title="assigneeUrlTitle"
v-if="issue.assignee" v-if="issue.assignee"
data-container="body"> data-container="body"
>
<img <img
class="avatar avatar-inline s20 js-no-trigger" class="avatar avatar-inline s20 js-no-trigger"
:src="issue.assignee.avatar" :src="issue.assignee.avatar"
width="20" width="20"
height="20" height="20"
:alt="'Avatar for ' + issue.assignee.name" /> :alt="avatarUrlTitle"
/>
</a> </a>
</div>
<div class="card-footer" v-if="showLabelFooter">
<button <button
class="label color-label has-tooltip js-no-trigger" class="label color-label has-tooltip js-no-trigger"
v-for="label in issue.labels" v-for="label in issue.labels"
......
This diff is collapsed.
...@@ -8,10 +8,8 @@ Vue.use(VueResource); ...@@ -8,10 +8,8 @@ Vue.use(VueResource);
/** /**
* Commits View > Pipelines Tab > Pipelines Table. * Commits View > Pipelines Tab > Pipelines Table.
* Merge Request View > Pipelines Tab > Pipelines Table.
* *
* Renders Pipelines table in pipelines tab in the commits show view. * Renders Pipelines table in pipelines tab in the commits show view.
* Renders Pipelines table in pipelines tab in the merge request show view.
*/ */
$(() => { $(() => {
...@@ -20,13 +18,14 @@ $(() => { ...@@ -20,13 +18,14 @@ $(() => {
gl.commits.pipelines = gl.commits.pipelines || {}; gl.commits.pipelines = gl.commits.pipelines || {};
if (gl.commits.PipelinesTableBundle) { if (gl.commits.PipelinesTableBundle) {
document.querySelector('#commit-pipeline-table-view').removeChild(this.pipelinesTableBundle.$el);
gl.commits.PipelinesTableBundle.$destroy(true); gl.commits.PipelinesTableBundle.$destroy(true);
} }
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
gl.commits.pipelines.PipelinesTableBundle = new CommitPipelinesTable();
if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) { if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) {
gl.commits.pipelines.PipelinesTableBundle.$mount(pipelineTableViewEl); gl.commits.pipelines.PipelinesTableBundle = new CommitPipelinesTable().$mount();
document.querySelector('#commit-pipeline-table-view').appendChild(gl.commits.pipelines.PipelinesTableBundle.$el);
} }
}); });
import Vue from 'vue'; import Vue from 'vue';
import Visibility from 'visibilityjs';
import PipelinesTableComponent from '../../vue_shared/components/pipelines_table'; import PipelinesTableComponent from '../../vue_shared/components/pipelines_table';
import PipelinesService from '../../vue_pipelines_index/services/pipelines_service'; import PipelinesService from '../../vue_pipelines_index/services/pipelines_service';
import PipelineStore from '../../vue_pipelines_index/stores/pipelines_store'; import PipelineStore from '../../vue_pipelines_index/stores/pipelines_store';
...@@ -7,6 +8,7 @@ import EmptyState from '../../vue_pipelines_index/components/empty_state'; ...@@ -7,6 +8,7 @@ import EmptyState from '../../vue_pipelines_index/components/empty_state';
import ErrorState from '../../vue_pipelines_index/components/error_state'; import ErrorState from '../../vue_pipelines_index/components/error_state';
import '../../lib/utils/common_utils'; import '../../lib/utils/common_utils';
import '../../vue_shared/vue_resource_interceptor'; import '../../vue_shared/vue_resource_interceptor';
import Poll from '../../lib/utils/poll';
/** /**
* *
...@@ -20,6 +22,7 @@ import '../../vue_shared/vue_resource_interceptor'; ...@@ -20,6 +22,7 @@ import '../../vue_shared/vue_resource_interceptor';
*/ */
export default Vue.component('pipelines-table', { export default Vue.component('pipelines-table', {
components: { components: {
'pipelines-table-component': PipelinesTableComponent, 'pipelines-table-component': PipelinesTableComponent,
'error-state': ErrorState, 'error-state': ErrorState,
...@@ -42,6 +45,7 @@ export default Vue.component('pipelines-table', { ...@@ -42,6 +45,7 @@ export default Vue.component('pipelines-table', {
state: store.state, state: store.state,
isLoading: false, isLoading: false,
hasError: false, hasError: false,
isMakingRequest: false,
}; };
}, },
...@@ -64,17 +68,41 @@ export default Vue.component('pipelines-table', { ...@@ -64,17 +68,41 @@ export default Vue.component('pipelines-table', {
* *
*/ */
beforeMount() { beforeMount() {
this.endpoint = this.$el.dataset.endpoint; const element = document.querySelector('#commit-pipeline-table-view');
this.helpPagePath = this.$el.dataset.helpPagePath;
this.endpoint = element.dataset.endpoint;
this.helpPagePath = element.dataset.helpPagePath;
this.service = new PipelinesService(this.endpoint); this.service = new PipelinesService(this.endpoint);
this.fetchPipelines(); this.poll = new Poll({
resource: this.service,
method: 'getPipelines',
successCallback: this.successCallback,
errorCallback: this.errorCallback,
notificationCallback: this.setIsMakingRequest,
});
if (!Visibility.hidden()) {
this.isLoading = true;
this.poll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
eventHub.$on('refreshPipelines', this.fetchPipelines); eventHub.$on('refreshPipelines', this.fetchPipelines);
}, },
beforeUpdate() { beforeUpdate() {
if (this.state.pipelines.length && this.$children) { if (this.state.pipelines.length &&
this.$children &&
!this.isMakingRequest &&
!this.isLoading) {
this.store.startTimeAgoLoops.call(this, Vue); this.store.startTimeAgoLoops.call(this, Vue);
} }
}, },
...@@ -83,21 +111,35 @@ export default Vue.component('pipelines-table', { ...@@ -83,21 +111,35 @@ export default Vue.component('pipelines-table', {
eventHub.$off('refreshPipelines'); eventHub.$off('refreshPipelines');
}, },
destroyed() {
this.poll.stop();
},
methods: { methods: {
fetchPipelines() { fetchPipelines() {
this.isLoading = true; this.isLoading = true;
return this.service.getPipelines() return this.service.getPipelines()
.then(response => response.json()) .then(response => this.successCallback(response))
.then((json) => { .catch(() => this.errorCallback());
},
successCallback(resp) {
const response = resp.json();
// depending of the endpoint the response can either bring a `pipelines` key or not. // depending of the endpoint the response can either bring a `pipelines` key or not.
const pipelines = json.pipelines || json; const pipelines = response.pipelines || response;
this.store.storePipelines(pipelines); this.store.storePipelines(pipelines);
this.isLoading = false; this.isLoading = false;
}) },
.catch(() => {
errorCallback() {
this.hasError = true; this.hasError = true;
this.isLoading = false; this.isLoading = false;
}); },
setIsMakingRequest(isMakingRequest) {
this.isMakingRequest = isMakingRequest;
}, },
}, },
......
...@@ -13,10 +13,6 @@ class Diff { ...@@ -13,10 +13,6 @@ class Diff {
$diffFile.each((index, file) => new gl.ImageFile(file)); $diffFile.each((index, file) => new gl.ImageFile(file));
if (this.diffViewType() === 'parallel') {
$('.content-wrapper .container-fluid').removeClass('container-limited');
}
if (!isBound) { if (!isBound) {
$(document) $(document)
.on('click', '.js-unfold', this.handleClickUnfold.bind(this)) .on('click', '.js-unfold', this.handleClickUnfold.bind(this))
......
...@@ -24,7 +24,6 @@ ...@@ -24,7 +24,6 @@
/* global Search */ /* global Search */
/* global Admin */ /* global Admin */
/* global NamespaceSelects */ /* global NamespaceSelects */
/* global ShortcutsDashboardNavigation */
/* global Project */ /* global Project */
/* global ProjectAvatar */ /* global ProjectAvatar */
/* global CompareAutocomplete */ /* global CompareAutocomplete */
...@@ -412,7 +411,6 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -412,7 +411,6 @@ const ShortcutsBlob = require('./shortcuts_blob');
break; break;
case 'dashboard': case 'dashboard':
case 'root': case 'root':
shortcut_handler = new ShortcutsDashboardNavigation();
new UserCallout(); new UserCallout();
break; break;
case 'groups': case 'groups':
......
...@@ -67,7 +67,7 @@ export default { ...@@ -67,7 +67,7 @@ export default {
}, },
created() { created() {
this.getDeployBoard(); this.getDeployBoard(true);
}, },
updated() { updated() {
...@@ -80,14 +80,14 @@ export default { ...@@ -80,14 +80,14 @@ export default {
this.deployBoardData.completion < 100) { this.deployBoardData.completion < 100) {
// let's wait 1s and make the request again // let's wait 1s and make the request again
setTimeout(() => { setTimeout(() => {
this.getDeployBoard(); this.getDeployBoard(false);
}, 3000); }, 3000);
} }
}, },
methods: { methods: {
getDeployBoard() { getDeployBoard(showLoading) {
this.isLoading = true; this.isLoading = showLoading;
const maxNumberOfRequests = 3; const maxNumberOfRequests = 3;
......
...@@ -32,12 +32,6 @@ export default Vue.component('environment-folder-view', { ...@@ -32,12 +32,6 @@ export default Vue.component('environment-folder-view', {
cssContainerClass: environmentsData.cssClass, cssContainerClass: environmentsData.cssClass,
canCreateDeployment: environmentsData.canCreateDeployment, canCreateDeployment: environmentsData.canCreateDeployment,
canReadEnvironment: environmentsData.canReadEnvironment, canReadEnvironment: environmentsData.canReadEnvironment,
// svgs
commitIconSvg: environmentsData.commitIconSvg,
playIconSvg: environmentsData.playIconSvg,
terminalIconSvg: environmentsData.terminalIconSvg,
// Pagination Properties, // Pagination Properties,
paginationInformation: {}, paginationInformation: {},
pageNumber: 1, pageNumber: 1,
...@@ -175,9 +169,6 @@ export default Vue.component('environment-folder-view', { ...@@ -175,9 +169,6 @@ export default Vue.component('environment-folder-view', {
:environments="state.environments" :environments="state.environments"
:can-create-deployment="canCreateDeploymentParsed" :can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed" :can-read-environment="canReadEnvironmentParsed"
:play-icon-svg="playIconSvg"
:terminal-icon-svg="terminalIconSvg"
:commit-icon-svg="commitIconSvg"
:toggleDeployBoard="toggleDeployBoard" :toggleDeployBoard="toggleDeployBoard"
:store="store" :store="store"
:service="service"/> :service="service"/>
......
...@@ -65,7 +65,6 @@ export default class Poll { ...@@ -65,7 +65,6 @@ export default class Poll {
this.makeRequest(); this.makeRequest();
}, pollInterval); }, pollInterval);
} }
this.options.successCallback(response); this.options.successCallback(response);
} }
...@@ -76,8 +75,14 @@ export default class Poll { ...@@ -76,8 +75,14 @@ export default class Poll {
notificationCallback(true); notificationCallback(true);
return resource[method](data) return resource[method](data)
.then(response => this.checkConditions(response)) .then((response) => {
.catch(error => errorCallback(error)); this.checkConditions(response);
notificationCallback(false);
})
.catch((error) => {
notificationCallback(false);
errorCallback(error);
});
} }
/** /**
......
...@@ -90,6 +90,7 @@ import './flash'; ...@@ -90,6 +90,7 @@ import './flash';
.on('click', this.clickTab); .on('click', this.clickTab);
} }
// Used in tests
unbindEvents() { unbindEvents() {
$(document) $(document)
.off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) .off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
...@@ -99,10 +100,12 @@ import './flash'; ...@@ -99,10 +100,12 @@ import './flash';
.off('click', this.clickTab); .off('click', this.clickTab);
} }
destroy() { destroyPipelinesView() {
this.unbindEvents();
if (this.commitPipelinesTable) { if (this.commitPipelinesTable) {
this.commitPipelinesTable.$destroy(); this.commitPipelinesTable.$destroy();
this.commitPipelinesTable = null;
document.querySelector('#commit-pipeline-table-view').innerHTML = '';
} }
} }
...@@ -128,6 +131,7 @@ import './flash'; ...@@ -128,6 +131,7 @@ import './flash';
this.loadCommits($target.attr('href')); this.loadCommits($target.attr('href'));
this.expandView(); this.expandView();
this.resetViewContainer(); this.resetViewContainer();
this.destroyPipelinesView();
} else if (this.isDiffAction(action)) { } else if (this.isDiffAction(action)) {
this.loadDiff($target.attr('href')); this.loadDiff($target.attr('href'));
if (Breakpoints.get().getBreakpointSize() !== 'lg') { if (Breakpoints.get().getBreakpointSize() !== 'lg') {
...@@ -136,12 +140,14 @@ import './flash'; ...@@ -136,12 +140,14 @@ import './flash';
if (this.diffViewType() === 'parallel') { if (this.diffViewType() === 'parallel') {
this.expandViewContainer(); this.expandViewContainer();
} }
this.destroyPipelinesView();
} else if (action === 'pipelines') { } else if (action === 'pipelines') {
this.resetViewContainer(); this.resetViewContainer();
this.loadPipelines(); this.mountPipelinesView();
} else { } else {
this.expandView(); this.expandView();
this.resetViewContainer(); this.resetViewContainer();
this.destroyPipelinesView();
} }
if (this.setUrl) { if (this.setUrl) {
this.setCurrentAction(action); this.setCurrentAction(action);
...@@ -227,16 +233,12 @@ import './flash'; ...@@ -227,16 +233,12 @@ import './flash';
}); });
} }
loadPipelines() { mountPipelinesView() {
if (this.pipelinesLoaded) { this.commitPipelinesTable = new CommitPipelinesTable().$mount();
return; // $mount(el) replaces the el with the new rendered component. We need it in order to mount
} // it everytime this tab is clicked - https://vuejs.org/v2/api/#vm-mount
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); document.querySelector('#commit-pipeline-table-view')
// Could already be mounted from the `pipelines_bundle` .appendChild(this.commitPipelinesTable.$el);
if (pipelineTableViewEl) {
this.commitPipelinesTable = new CommitPipelinesTable().$mount(pipelineTableViewEl);
}
this.pipelinesLoaded = true;
} }
loadDiff(source) { loadDiff(source) {
......
...@@ -6,7 +6,10 @@ import statusCodes from '~/lib/utils/http_status'; ...@@ -6,7 +6,10 @@ import statusCodes from '~/lib/utils/http_status';
import { formatRelevantDigits } from '~/lib/utils/number_utils'; import { formatRelevantDigits } from '~/lib/utils/number_utils';
import '../flash'; import '../flash';
const prometheusContainer = '.prometheus-container';
const prometheusParentGraphContainer = '.prometheus-graphs';
const prometheusGraphsContainer = '.prometheus-graph'; const prometheusGraphsContainer = '.prometheus-graph';
const prometheusStatesContainer = '.prometheus-state';
const metricsEndpoint = 'metrics.json'; const metricsEndpoint = 'metrics.json';
const timeFormat = d3.time.format('%H:%M'); const timeFormat = d3.time.format('%H:%M');
const dayFormat = d3.time.format('%b %e, %a'); const dayFormat = d3.time.format('%b %e, %a');
...@@ -14,8 +17,15 @@ const bisectDate = d3.bisector(d => d.time).left; ...@@ -14,8 +17,15 @@ const bisectDate = d3.bisector(d => d.time).left;
const extraAddedWidthParent = 100; const extraAddedWidthParent = 100;
class PrometheusGraph { class PrometheusGraph {
constructor() { constructor() {
const $prometheusContainer = $(prometheusContainer);
const hasMetrics = $prometheusContainer.data('has-metrics');
this.docLink = $prometheusContainer.data('doc-link');
this.integrationLink = $prometheusContainer.data('prometheus-integration');
$(document).ajaxError(() => {});
if (hasMetrics) {
this.margin = { top: 80, right: 180, bottom: 80, left: 100 }; this.margin = { top: 80, right: 180, bottom: 80, left: 100 };
this.marginLabelContainer = { top: 40, right: 0, bottom: 40, left: 0 }; this.marginLabelContainer = { top: 40, right: 0, bottom: 40, left: 0 };
const parentContainerWidth = $(prometheusGraphsContainer).parent().width() + const parentContainerWidth = $(prometheusGraphsContainer).parent().width() +
...@@ -27,6 +37,10 @@ class PrometheusGraph { ...@@ -27,6 +37,10 @@ class PrometheusGraph {
this.backOffRequestCounter = 0; this.backOffRequestCounter = 0;
this.configureGraph(); this.configureGraph();
this.init(); this.init();
} else {
this.state = '.js-getting-started';
this.updateState();
}
} }
createGraph() { createGraph() {
...@@ -40,8 +54,19 @@ class PrometheusGraph { ...@@ -40,8 +54,19 @@ class PrometheusGraph {
init() { init() {
this.getData().then((metricsResponse) => { this.getData().then((metricsResponse) => {
if (Object.keys(metricsResponse).length === 0) { let enoughData = true;
new Flash('Empty metrics', 'alert'); Object.keys(metricsResponse.metrics).forEach((key) => {
let currentKey;
if (key === 'cpu_values' || key === 'memory_values') {
currentKey = metricsResponse.metrics[key];
if (Object.keys(currentKey).length === 0) {
enoughData = false;
}
}
});
if (!enoughData) {
this.state = '.js-loading';
this.updateState();
} else { } else {
this.transformData(metricsResponse); this.transformData(metricsResponse);
this.createGraph(); this.createGraph();
...@@ -345,14 +370,17 @@ class PrometheusGraph { ...@@ -345,14 +370,17 @@ class PrometheusGraph {
} }
return resp.metrics; return resp.metrics;
}) })
.catch(() => new Flash('An error occurred while fetching metrics.', 'alert')); .catch(() => {
this.state = '.js-unable-to-connect';
this.updateState();
});
} }
transformData(metricsResponse) { transformData(metricsResponse) {
Object.keys(metricsResponse.metrics).forEach((key) => { Object.keys(metricsResponse.metrics).forEach((key) => {
if (key === 'cpu_values' || key === 'memory_values') { if (key === 'cpu_values' || key === 'memory_values') {
const metricValues = (metricsResponse.metrics[key])[0]; const metricValues = (metricsResponse.metrics[key])[0];
if (typeof metricValues !== 'undefined') { if (metricValues !== undefined) {
this.graphSpecificProperties[key].data = metricValues.values.map(metric => ({ this.graphSpecificProperties[key].data = metricValues.values.map(metric => ({
time: new Date(metric[0] * 1000), time: new Date(metric[0] * 1000),
value: metric[1], value: metric[1],
...@@ -361,6 +389,13 @@ class PrometheusGraph { ...@@ -361,6 +389,13 @@ class PrometheusGraph {
} }
}); });
} }
updateState() {
const $statesContainer = $(prometheusStatesContainer);
$(prometheusParentGraphContainer).hide();
$(`${this.state}`, $statesContainer).removeClass('hidden');
$(prometheusStatesContainer).show();
}
} }
export default PrometheusGraph; export default PrometheusGraph;
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-else-return, comma-dangle, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-else-return, comma-dangle, max-len */
/* global Mousetrap */ /* global Mousetrap */
/* global findFileURL */ /* global findFileURL */
import findAndFollowLink from './shortcuts_dashboard_navigation';
(function() { (function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
...@@ -14,11 +15,33 @@ ...@@ -14,11 +15,33 @@
} }
Mousetrap.bind('?', this.onToggleHelp); Mousetrap.bind('?', this.onToggleHelp);
Mousetrap.bind('s', Shortcuts.focusSearch); Mousetrap.bind('s', Shortcuts.focusSearch);
Mousetrap.bind('f', (function(_this) { Mousetrap.bind('f', (e => this.focusFilter(e)));
return function(e) {
return _this.focusFilter(e); const $globalDropdownMenu = $('.global-dropdown-menu');
}; const $globalDropdownToggle = $('.global-dropdown-toggle');
})(this));
$('.global-dropdown').on('hide.bs.dropdown', () => {
$globalDropdownMenu.removeClass('shortcuts');
});
Mousetrap.bind('n', () => {
$globalDropdownMenu.toggleClass('shortcuts');
$globalDropdownToggle.trigger('click');
if (!$globalDropdownMenu.is(':visible')) {
$globalDropdownToggle.blur();
}
});
Mousetrap.bind('shift+t', () => findAndFollowLink('.shortcuts-todos'));
Mousetrap.bind('shift+a', () => findAndFollowLink('.dashboard-shortcuts-activity'));
Mousetrap.bind('shift+i', () => findAndFollowLink('.dashboard-shortcuts-issues'));
Mousetrap.bind('shift+m', () => findAndFollowLink('.dashboard-shortcuts-merge_requests'));
Mousetrap.bind('shift+p', () => findAndFollowLink('.dashboard-shortcuts-projects'));
Mousetrap.bind('shift+g', () => findAndFollowLink('.dashboard-shortcuts-groups'));
Mousetrap.bind('shift+l', () => findAndFollowLink('.dashboard-shortcuts-milestones'));
Mousetrap.bind('shift+s', () => findAndFollowLink('.dashboard-shortcuts-snippets'));
Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], this.toggleMarkdownPreview); Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], this.toggleMarkdownPreview);
if (typeof findFileURL !== "undefined" && findFileURL !== null) { if (typeof findFileURL !== "undefined" && findFileURL !== null) {
Mousetrap.bind('t', function() { Mousetrap.bind('t', function() {
......
/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign */ /**
/* global Mousetrap */ * Helper function that finds the href of the fiven selector and updates the location.
/* global Shortcuts */ *
* @param {String} selector
*/
export default (selector) => {
const link = document.querySelector(selector).getAttribute('href');
require('./shortcuts');
(function() {
var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
this.ShortcutsDashboardNavigation = (function(superClass) {
extend(ShortcutsDashboardNavigation, superClass);
function ShortcutsDashboardNavigation() {
ShortcutsDashboardNavigation.__super__.constructor.call(this);
Mousetrap.bind('g a', function() {
return ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-activity');
});
Mousetrap.bind('g i', function() {
return ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-issues');
});
Mousetrap.bind('g m', function() {
return ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-merge_requests');
});
Mousetrap.bind('g t', function() {
return ShortcutsDashboardNavigation.findAndFollowLink('.shortcuts-todos');
});
Mousetrap.bind('g p', function() {
return ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-projects');
});
}
ShortcutsDashboardNavigation.findAndFollowLink = function(selector) {
var link;
link = $(selector).attr('href');
if (link) { if (link) {
return window.location = link; window.location = link;
} }
}; };
return ShortcutsDashboardNavigation;
})(Shortcuts);
}).call(window);
/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign */ /* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign */
/* global Mousetrap */ /* global Mousetrap */
/* global Shortcuts */ /* global Shortcuts */
import findAndFollowLink from './shortcuts_dashboard_navigation';
require('./shortcuts'); require('./shortcuts');
...@@ -13,59 +14,23 @@ require('./shortcuts'); ...@@ -13,59 +14,23 @@ require('./shortcuts');
function ShortcutsNavigation() { function ShortcutsNavigation() {
ShortcutsNavigation.__super__.constructor.call(this); ShortcutsNavigation.__super__.constructor.call(this);
Mousetrap.bind('g p', function() { Mousetrap.bind('g p', () => findAndFollowLink('.shortcuts-project'));
return ShortcutsNavigation.findAndFollowLink('.shortcuts-project'); Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-project-activity'));
}); Mousetrap.bind('g f', () => findAndFollowLink('.shortcuts-tree'));
Mousetrap.bind('g e', function() { Mousetrap.bind('g c', () => findAndFollowLink('.shortcuts-commits'));
return ShortcutsNavigation.findAndFollowLink('.shortcuts-project-activity'); Mousetrap.bind('g j', () => findAndFollowLink('.shortcuts-builds'));
}); Mousetrap.bind('g n', () => findAndFollowLink('.shortcuts-network'));
Mousetrap.bind('g f', function() { Mousetrap.bind('g d', () => findAndFollowLink('.shortcuts-repository-charts'));
return ShortcutsNavigation.findAndFollowLink('.shortcuts-tree'); Mousetrap.bind('g i', () => findAndFollowLink('.shortcuts-issues'));
}); Mousetrap.bind('g b', () => findAndFollowLink('.shortcuts-issue-boards'));
Mousetrap.bind('g c', function() { Mousetrap.bind('g m', () => findAndFollowLink('.shortcuts-merge_requests'));
return ShortcutsNavigation.findAndFollowLink('.shortcuts-commits'); Mousetrap.bind('g t', () => findAndFollowLink('.shortcuts-todos'));
}); Mousetrap.bind('g w', () => findAndFollowLink('.shortcuts-wiki'));
Mousetrap.bind('g b', function() { Mousetrap.bind('g s', () => findAndFollowLink('.shortcuts-snippets'));
return ShortcutsNavigation.findAndFollowLink('.shortcuts-builds'); Mousetrap.bind('i', () => findAndFollowLink('.shortcuts-new-issue'));
});
Mousetrap.bind('g n', function() {
return ShortcutsNavigation.findAndFollowLink('.shortcuts-network');
});
Mousetrap.bind('g g', function() {
return ShortcutsNavigation.findAndFollowLink('.shortcuts-repository-charts');
});
Mousetrap.bind('g i', function() {
return ShortcutsNavigation.findAndFollowLink('.shortcuts-issues');
});
Mousetrap.bind('g l', function() {
ShortcutsNavigation.findAndFollowLink('.shortcuts-issue-boards');
});
Mousetrap.bind('g m', function() {
return ShortcutsNavigation.findAndFollowLink('.shortcuts-merge_requests');
});
Mousetrap.bind('g t', function() {
return ShortcutsNavigation.findAndFollowLink('.shortcuts-todos');
});
Mousetrap.bind('g w', function() {
return ShortcutsNavigation.findAndFollowLink('.shortcuts-wiki');
});
Mousetrap.bind('g s', function() {
return ShortcutsNavigation.findAndFollowLink('.shortcuts-snippets');
});
Mousetrap.bind('i', function() {
return ShortcutsNavigation.findAndFollowLink('.shortcuts-new-issue');
});
this.enabledHelp.push('.hidden-shortcut.project'); this.enabledHelp.push('.hidden-shortcut.project');
} }
ShortcutsNavigation.findAndFollowLink = function(selector) {
var link;
link = $(selector).attr('href');
if (link) {
return window.location = link;
}
};
return ShortcutsNavigation; return ShortcutsNavigation;
})(Shortcuts); })(Shortcuts);
}).call(window); }).call(window);
import Vue from 'vue'; import Vue from 'vue';
import Visibility from 'visibilityjs';
import PipelinesService from './services/pipelines_service'; import PipelinesService from './services/pipelines_service';
import eventHub from './event_hub'; import eventHub from './event_hub';
import PipelinesTableComponent from '../vue_shared/components/pipelines_table'; import PipelinesTableComponent from '../vue_shared/components/pipelines_table';
...@@ -7,6 +8,7 @@ import EmptyState from './components/empty_state'; ...@@ -7,6 +8,7 @@ import EmptyState from './components/empty_state';
import ErrorState from './components/error_state'; import ErrorState from './components/error_state';
import NavigationTabs from './components/navigation_tabs'; import NavigationTabs from './components/navigation_tabs';
import NavigationControls from './components/nav_controls'; import NavigationControls from './components/nav_controls';
import Poll from '../lib/utils/poll';
export default { export default {
props: { props: {
...@@ -47,6 +49,7 @@ export default { ...@@ -47,6 +49,7 @@ export default {
pagenum: 1, pagenum: 1,
isLoading: false, isLoading: false,
hasError: false, hasError: false,
isMakingRequest: false,
}; };
}, },
...@@ -120,18 +123,49 @@ export default { ...@@ -120,18 +123,49 @@ export default {
tagsPath: this.tagsPath, tagsPath: this.tagsPath,
}; };
}, },
pageParameter() {
return gl.utils.getParameterByName('page') || this.pagenum;
},
scopeParameter() {
return gl.utils.getParameterByName('scope') || this.apiScope;
},
}, },
created() { created() {
this.service = new PipelinesService(this.endpoint); this.service = new PipelinesService(this.endpoint);
this.fetchPipelines(); const poll = new Poll({
resource: this.service,
method: 'getPipelines',
data: { page: this.pageParameter, scope: this.scopeParameter },
successCallback: this.successCallback,
errorCallback: this.errorCallback,
notificationCallback: this.setIsMakingRequest,
});
if (!Visibility.hidden()) {
this.isLoading = true;
poll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
poll.restart();
} else {
poll.stop();
}
});
eventHub.$on('refreshPipelines', this.fetchPipelines); eventHub.$on('refreshPipelines', this.fetchPipelines);
}, },
beforeUpdate() { beforeUpdate() {
if (this.state.pipelines.length && this.$children) { if (this.state.pipelines.length &&
this.$children &&
!this.isMakingRequest &&
!this.isLoading) {
this.store.startTimeAgoLoops.call(this, Vue); this.store.startTimeAgoLoops.call(this, Vue);
} }
}, },
...@@ -154,27 +188,35 @@ export default { ...@@ -154,27 +188,35 @@ export default {
}, },
fetchPipelines() { fetchPipelines() {
const pageNumber = gl.utils.getParameterByName('page') || this.pagenum; if (!this.isMakingRequest) {
const scope = gl.utils.getParameterByName('scope') || this.apiScope;
this.isLoading = true; this.isLoading = true;
return this.service.getPipelines(scope, pageNumber)
.then(resp => ({ this.service.getPipelines({ scope: this.scopeParameter, page: this.pageParameter })
.then(response => this.successCallback(response))
.catch(() => this.errorCallback());
}
},
successCallback(resp) {
const response = {
headers: resp.headers, headers: resp.headers,
body: resp.json(), body: resp.json(),
})) };
.then((response) => {
this.store.storeCount(response.body.count); this.store.storeCount(response.body.count);
this.store.storePipelines(response.body.pipelines); this.store.storePipelines(response.body.pipelines);
this.store.storePagination(response.headers); this.store.storePagination(response.headers);
})
.then(() => {
this.isLoading = false; this.isLoading = false;
}) },
.catch(() => {
errorCallback() {
this.hasError = true; this.hasError = true;
this.isLoading = false; this.isLoading = false;
}); },
setIsMakingRequest(isMakingRequest) {
this.isMakingRequest = isMakingRequest;
}, },
}, },
......
...@@ -26,7 +26,8 @@ export default class PipelinesService { ...@@ -26,7 +26,8 @@ export default class PipelinesService {
this.pipelines = Vue.resource(endpoint); this.pipelines = Vue.resource(endpoint);
} }
getPipelines(scope, page) { getPipelines(data = {}) {
const { scope, page } = data;
return this.pipelines.get({ scope, page }); return this.pipelines.get({ scope, page });
} }
......
...@@ -91,7 +91,7 @@ ...@@ -91,7 +91,7 @@
.award-menu-holder { .award-menu-holder {
display: inline-block; display: inline-block;
position: relative; position: absolute;
.tooltip { .tooltip {
white-space: nowrap; white-space: nowrap;
...@@ -117,11 +117,41 @@ ...@@ -117,11 +117,41 @@
&.active, &.active,
&:hover, &:hover,
&:active { &:active,
&.is-active {
background-color: $row-hover; background-color: $row-hover;
border-color: $row-hover-border; border-color: $row-hover-border;
box-shadow: none; box-shadow: none;
outline: 0; outline: 0;
.award-control-icon svg {
background: $award-emoji-positive-add-bg;
path {
fill: $award-emoji-positive-add-lines;
}
}
.award-control-icon-neutral {
opacity: 0;
}
.award-control-icon-positive {
opacity: 1;
transform: scale(1.15);
}
}
&.is-active {
.award-control-icon-positive {
opacity: 0;
transform: scale(1);
}
.award-control-icon-super-positive {
opacity: 1;
transform: scale(1);
}
} }
&.btn { &.btn {
...@@ -162,9 +192,33 @@ ...@@ -162,9 +192,33 @@
color: $border-gray-normal; color: $border-gray-normal;
margin-top: 1px; margin-top: 1px;
padding: 0 2px; padding: 0 2px;
svg {
margin-bottom: 1px;
height: 18px;
width: 18px;
border-radius: 50%;
path {
fill: $border-gray-normal;
}
}
}
.award-control-icon-positive,
.award-control-icon-super-positive {
position: absolute;
left: 7px;
bottom: 9px;
opacity: 0;
@include transition(opacity, transform);
} }
.award-control-text { .award-control-text {
vertical-align: middle; vertical-align: middle;
} }
} }
.note-awards .award-control-icon-positive {
left: 6px;
}
...@@ -187,6 +187,15 @@ ...@@ -187,6 +187,15 @@
} }
} }
.shortcut-mappings {
display: none;
}
&.shortcuts .shortcut-mappings {
display: inline-block;
margin-right: 5px;
}
ul { ul {
margin: 0; margin: 0;
padding: 0; padding: 0;
......
...@@ -446,10 +446,8 @@ ...@@ -446,10 +446,8 @@
} }
} }
.filter-dropdown-item.droplab-item-active { .filter-dropdown-item.droplab-item-active .btn {
.btn {
@extend %filter-dropdown-item-btn-hover; @extend %filter-dropdown-item-btn-hover;
}
} }
.filter-dropdown-loading { .filter-dropdown-loading {
......
...@@ -16,6 +16,8 @@ body.modal-open { ...@@ -16,6 +16,8 @@ body.modal-open {
overflow: hidden; overflow: hidden;
} }
.modal .modal-dialog { @media (min-width: $screen-md-min) {
.modal-dialog {
width: 860px; width: 860px;
}
} }
.timeline { .timeline {
@include basic-list; @include basic-list;
margin: 0; margin: 0;
padding: 0; padding: 0;
.timeline-entry { .timeline-entry {
padding: $gl-padding $gl-btn-padding 11px; padding: $gl-padding $gl-btn-padding 14px;
border-color: $white-normal; border-color: $white-normal;
color: $gl-text-color; color: $gl-text-color;
border-bottom: 1px solid $border-white-light; border-bottom: 1px solid $border-white-light;
.timeline-entry-inner {
position: relative;
}
&:target { &:target {
background: $line-target-blue; background: $line-target-blue;
} }
......
...@@ -296,6 +296,8 @@ $badge-color: $gl-text-color-secondary; ...@@ -296,6 +296,8 @@ $badge-color: $gl-text-color-secondary;
* Award emoji * Award emoji
*/ */
$award-emoji-menu-shadow: rgba(0,0,0,.175); $award-emoji-menu-shadow: rgba(0,0,0,.175);
$award-emoji-positive-add-bg: #fed159;
$award-emoji-positive-add-lines: #bb9c13;
/* /*
* Search Box * Search Box
......
...@@ -226,7 +226,7 @@ ...@@ -226,7 +226,7 @@
.card { .card {
position: relative; position: relative;
padding: 10px $gl-padding; padding: 11px 10px 11px $gl-padding;
background: $white-light; background: $white-light;
border-radius: $border-radius-default; border-radius: $border-radius-default;
box-shadow: 0 1px 2px $issue-boards-card-shadow; box-shadow: 0 1px 2px $issue-boards-card-shadow;
...@@ -246,6 +246,8 @@ ...@@ -246,6 +246,8 @@
} }
.confidential-icon { .confidential-icon {
position: relative;
top: 1px;
margin-right: 5px; margin-right: 5px;
} }
} }
...@@ -253,34 +255,43 @@ ...@@ -253,34 +255,43 @@
.card-title { .card-title {
margin: 0; margin: 0;
font-size: 1em; font-size: 1em;
line-height: inherit;
a { a {
color: inherit; color: $gl-text-color;
word-wrap: break-word; word-wrap: break-word;
margin-right: 2px;
} }
} }
.card-footer { .card-header {
margin-top: 5px; display: flex;
line-height: 25px; min-height: 20px;
.label {
margin-right: 5px;
font-size: (14px / $issue-boards-font-size) * 1em;
}
.card-assignee { .card-assignee {
margin-left: auto;
margin-right: 5px; margin-right: 5px;
padding-left: 10px;
height: 20px;
} }
.avatar { .avatar {
margin-left: 0; margin: 0;
margin-right: 0; }
}
.card-footer {
margin: 0 0 5px;
.label {
margin-top: 5px;
margin-right: 6px;
} }
} }
.card-number { .card-number {
margin-right: 5px; font-size: 12px;
color: $gl-text-color-secondary;
} }
.issue-boards-search { .issue-boards-search {
......
...@@ -57,6 +57,37 @@ ...@@ -57,6 +57,37 @@
margin-right: 5px; margin-right: 5px;
} }
} }
.truncated-info {
text-align: center;
border-bottom: 1px solid;
background-color: $black-transparent;
height: 45px;
&.affix {
top: 0;
}
// with sidebar
&.affix.sidebar-expanded {
right: 312px;
left: 22px;
}
// without sidebar
&.affix.sidebar-collapsed {
right: 20px;
left: 20px;
}
&.affix-top {
position: absolute;
top: 0;
margin: 0 auto;
right: 5px;
left: 5px;
}
}
} }
.scroll-controls { .scroll-controls {
...@@ -186,6 +217,7 @@ ...@@ -186,6 +217,7 @@
white-space: pre; white-space: pre;
overflow-x: auto; overflow-x: auto;
font-size: 12px; font-size: 12px;
position: relative;
.fa-refresh { .fa-refresh {
font-size: 24px; font-size: 24px;
......
...@@ -383,6 +383,15 @@ ...@@ -383,6 +383,15 @@
stroke-width: 1; stroke-width: 1;
} }
.prometheus-state {
margin-top: 10px;
display: none;
.state-button-section {
margin-top: 10px;
}
}
.environments-actions { .environments-actions {
.external-url, .external-url,
.monitoring-url, .monitoring-url,
......
...@@ -4,14 +4,14 @@ ...@@ -4,14 +4,14 @@
*/ */
.event-item { .event-item {
font-size: $gl-font-size; font-size: $gl-font-size;
padding: $gl-padding-top 0 $gl-padding-top ($gl-avatar-size + $gl-padding-top); padding: $gl-padding-top 0 $gl-padding-top 40px;
border-bottom: 1px solid $white-normal; border-bottom: 1px solid $white-normal;
color: $list-text-color; color: $list-text-color;
position: relative;
&.event-inline { &.event-inline {
.avatar { .profile-icon {
position: relative; top: 20px;
top: -2px;
} }
.event-title, .event-title,
...@@ -24,8 +24,28 @@ ...@@ -24,8 +24,28 @@
color: $gl-text-color; color: $gl-text-color;
} }
.avatar { .profile-icon {
margin-left: -($gl-avatar-size + $gl-padding-top); position: absolute;
left: 0;
top: 14px;
svg {
width: 20px;
height: auto;
fill: $gl-text-color-secondary;
}
&.open-icon svg {
fill: $green-300;
}
&.closed-icon svg {
fill: $red-300;
}
&.fork-icon svg {
fill: $blue-300;
}
} }
.event-title { .event-title {
...@@ -163,7 +183,7 @@ ...@@ -163,7 +183,7 @@
max-width: 100%; max-width: 100%;
} }
.avatar { .profile-icon {
display: none; display: none;
} }
......
...@@ -196,6 +196,7 @@ ...@@ -196,6 +196,7 @@
transition: width .3s; transition: width .3s;
background: $gray-light; background: $gray-light;
padding: 10px 20px; padding: 10px 20px;
z-index: 2;
&.right-sidebar-expanded { &.right-sidebar-expanded {
width: $gutter_width; width: $gutter_width;
......
...@@ -329,8 +329,6 @@ ...@@ -329,8 +329,6 @@
} }
#modal_merge_info .modal-dialog { #modal_merge_info .modal-dialog {
width: 600px;
.dark { .dark {
margin-right: 40px; margin-right: 40px;
} }
......
...@@ -16,6 +16,15 @@ ul.notes { ...@@ -16,6 +16,15 @@ ul.notes {
.timeline-icon { .timeline-icon {
float: left; float: left;
svg {
width: 18px;
height: auto;
fill: $gray-darkest;
position: absolute;
left: 30px;
top: 15px;
}
} }
.timeline-content { .timeline-content {
...@@ -33,6 +42,103 @@ ul.notes { ...@@ -33,6 +42,103 @@ ul.notes {
white-space: nowrap; white-space: nowrap;
} }
.discussion-body {
padding-top: 15px;
}
.discussion {
overflow: hidden;
display: block;
position: relative;
}
.note {
display: block;
position: relative;
border-bottom: 1px solid $white-normal;
&.note-discussion {
&.timeline-entry {
padding: 14px 10px;
}
.system-note {
padding: 0;
}
}
&.is-editting {
.note-header,
.note-text,
.edited-text {
display: none;
}
.note-edit-form {
display: block;
&.current-note-edit-form + .note-awards {
display: none;
}
}
}
.note-body {
overflow-x: auto;
overflow-y: hidden;
.note-text {
word-wrap: break-word;
@include md-typography;
// Reset ul style types since we're nested inside a ul already
@include bulleted-list;
ul.task-list {
ul:not(.task-list) {
padding-left: 1.3em;
}
}
}
}
.note-awards {
.js-awards-block {
padding: 2px;
margin-top: 10px;
}
}
.note-header {
padding-bottom: 3px;
padding-right: 20px;
@media (min-width: $screen-sm-min) {
padding-right: 0;
}
@media (max-width: $screen-xs-min) {
.inline {
display: block;
}
}
}
.note-emoji-button {
.fa-spinner {
display: none;
}
&.is-loading {
.fa-smile-o {
display: none;
}
.fa-spinner {
display: inline-block;
}
}
}
}
.system-note { .system-note {
font-size: 14px; font-size: 14px;
padding: 0; padding: 0;
...@@ -68,6 +174,10 @@ ul.notes { ...@@ -68,6 +174,10 @@ ul.notes {
padding: 14px 10px; padding: 14px 10px;
} }
.note-header {
padding-bottom: 0;
}
.note-body { .note-body {
overflow: hidden; overflow: hidden;
...@@ -130,116 +240,6 @@ ul.notes { ...@@ -130,116 +240,6 @@ ul.notes {
} }
} }
} }
.timeline-icon {
display: none;
.avatar {
visibility: hidden;
.discussion-body & {
visibility: visible;
}
}
}
}
.discussion-body {
padding-top: 15px;
}
.discussion {
overflow: hidden;
display: block;
position: relative;
}
.note {
display: block;
position: relative;
border-bottom: 1px solid $white-normal;
&.note-discussion {
&.timeline-entry {
padding: 14px 10px;
}
.system-note {
padding: 0;
}
}
&.is-editting {
.note-header,
.note-text,
.edited-text {
display: none;
}
.note-edit-form {
display: block;
&.current-note-edit-form + .note-awards {
display: none;
}
}
}
.note-body {
overflow-x: auto;
overflow-y: hidden;
.note-text {
word-wrap: break-word;
@include md-typography;
// Reset ul style types since we're nested inside a ul already
@include bulleted-list;
ul.task-list {
ul:not(.task-list) {
padding-left: 1.3em;
}
}
}
}
.note-awards {
.js-awards-block {
padding: 2px;
margin-top: 10px;
}
}
.note-header {
padding-bottom: 3px;
padding-right: 20px;
@media (min-width: $screen-sm-min) {
padding-right: 0;
}
@media (max-width: $screen-xs-min) {
.inline {
display: block;
}
}
}
.note-emoji-button {
.fa-spinner {
display: none;
}
&.is-loading {
.fa-smile-o {
display: none;
}
.fa-spinner {
display: inline-block;
}
}
}
} }
} }
...@@ -410,13 +410,50 @@ ul.notes { ...@@ -410,13 +410,50 @@ ul.notes {
font-size: 17px; font-size: 17px;
} }
&:hover { svg {
height: 16px;
width: 16px;
fill: $gray-darkest;
vertical-align: text-top;
}
.award-control-icon-positive,
.award-control-icon-super-positive {
position: absolute;
margin-left: -20px;
opacity: 0;
}
&:hover,
&.is-active {
.danger-highlight { .danger-highlight {
color: $gl-text-red; color: $gl-text-red;
} }
.link-highlight { .link-highlight {
color: $gl-link-color; color: $gl-link-color;
svg {
fill: $gl-link-color;
}
}
.award-control-icon-neutral {
opacity: 0;
}
.award-control-icon-positive {
opacity: 1;
}
}
&.is-active {
.award-control-icon-positive {
opacity: 0;
}
.award-control-icon-super-positive {
opacity: 1;
} }
} }
} }
...@@ -520,7 +557,6 @@ ul.notes { ...@@ -520,7 +557,6 @@ ul.notes {
} }
.line-resolve-all-container { .line-resolve-all-container {
.btn-group { .btn-group {
margin-left: -4px; margin-left: -4px;
} }
...@@ -549,7 +585,6 @@ ul.notes { ...@@ -549,7 +585,6 @@ ul.notes {
fill: $gray-darkest; fill: $gray-darkest;
} }
} }
} }
.line-resolve-all { .line-resolve-all {
......
...@@ -230,6 +230,14 @@ ...@@ -230,6 +230,14 @@
font-size: 0; font-size: 0;
} }
.fade-right {
right: 0;
}
.fade-left {
left: 0;
}
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
.cover-block { .cover-block {
padding-top: 20px; padding-top: 20px;
......
...@@ -6,6 +6,8 @@ ...@@ -6,6 +6,8 @@
} }
.trigger-actions { .trigger-actions {
white-space: nowrap;
.btn { .btn {
margin-left: 10px; margin-left: 10px;
} }
......
...@@ -145,8 +145,6 @@ ...@@ -145,8 +145,6 @@
margin: 0; margin: 0;
} }
#modal-remove-blob > .modal-dialog { width: 850px; }
.blob-upload-dropzone-previews { .blob-upload-dropzone-previews {
text-align: center; text-align: center;
border: 2px; border: 2px;
......
...@@ -72,7 +72,9 @@ class Admin::GroupsController < Admin::ApplicationController ...@@ -72,7 +72,9 @@ class Admin::GroupsController < Admin::ApplicationController
:name, :name,
:path, :path,
:request_access_enabled, :request_access_enabled,
:visibility_level :visibility_level,
:require_two_factor_authentication,
:two_factor_grace_period
] ]
end end
......
...@@ -8,12 +8,12 @@ class ApplicationController < ActionController::Base ...@@ -8,12 +8,12 @@ class ApplicationController < ActionController::Base
include PageLayoutHelper include PageLayoutHelper
include SentryHelper include SentryHelper
include WorkhorseHelper include WorkhorseHelper
include EnforcesTwoFactorAuthentication
before_action :authenticate_user_from_private_token! before_action :authenticate_user_from_private_token!
before_action :authenticate_user! before_action :authenticate_user!
before_action :validate_user_service_ticket! before_action :validate_user_service_ticket!
before_action :check_password_expiration before_action :check_password_expiration
before_action :check_2fa_requirement
before_action :ldap_security_check before_action :ldap_security_check
before_action :sentry_context before_action :sentry_context
before_action :default_headers before_action :default_headers
...@@ -155,12 +155,6 @@ class ApplicationController < ActionController::Base ...@@ -155,12 +155,6 @@ class ApplicationController < ActionController::Base
end end
end end
def check_2fa_requirement
if two_factor_authentication_required? && current_user && !current_user.two_factor_enabled? && !skip_two_factor?
redirect_to profile_two_factor_auth_path
end
end
def ldap_security_check def ldap_security_check
if current_user && current_user.requires_ldap_check? if current_user && current_user.requires_ldap_check?
return unless current_user.try_obtain_ldap_lease return unless current_user.try_obtain_ldap_lease
...@@ -269,23 +263,6 @@ class ApplicationController < ActionController::Base ...@@ -269,23 +263,6 @@ class ApplicationController < ActionController::Base
current_application_settings.import_sources.include?('gitlab_project') current_application_settings.import_sources.include?('gitlab_project')
end end
def two_factor_authentication_required?
current_application_settings.require_two_factor_authentication
end
def two_factor_grace_period
current_application_settings.two_factor_grace_period
end
def two_factor_grace_period_expired?
date = current_user.otp_grace_period_started_at
date && (date + two_factor_grace_period.hours) < Time.current
end
def skip_two_factor?
session[:skip_tfa] && session[:skip_tfa] > Time.current
end
# U2F (universal 2nd factor) devices need a unique identifier for the application # U2F (universal 2nd factor) devices need a unique identifier for the application
# to perform authentication. # to perform authentication.
# https://developers.yubico.com/U2F/App_ID.html # https://developers.yubico.com/U2F/App_ID.html
......
# == EnforcesTwoFactorAuthentication
#
# Controller concern to enforce two-factor authentication requirements
#
# Upon inclusion, adds `check_two_factor_requirement` as a before_action,
# and makes `two_factor_grace_period_expired?` and `two_factor_skippable?`
# available as view helpers.
module EnforcesTwoFactorAuthentication
extend ActiveSupport::Concern
included do
before_action :check_two_factor_requirement
helper_method :two_factor_grace_period_expired?, :two_factor_skippable?
end
def check_two_factor_requirement
if two_factor_authentication_required? && current_user && !current_user.two_factor_enabled? && !skip_two_factor?
redirect_to profile_two_factor_auth_path
end
end
def two_factor_authentication_required?
current_application_settings.require_two_factor_authentication? ||
current_user.try(:require_two_factor_authentication_from_group?)
end
def two_factor_authentication_reason(global: -> {}, group: -> {})
if two_factor_authentication_required?
if current_application_settings.require_two_factor_authentication?
global.call
else
groups = current_user.expanded_groups_requiring_two_factor_authentication.reorder(name: :asc)
group.call(groups)
end
end
end
def two_factor_grace_period
periods = [current_application_settings.two_factor_grace_period]
periods << current_user.two_factor_grace_period if current_user.try(:require_two_factor_authentication_from_group?)
periods.min
end
def two_factor_grace_period_expired?
date = current_user.otp_grace_period_started_at
date && (date + two_factor_grace_period.hours) < Time.current
end
def two_factor_skippable?
two_factor_authentication_required? &&
!current_user.two_factor_enabled? &&
!two_factor_grace_period_expired?
end
def skip_two_factor?
session[:skip_two_factor] && session[:skip_two_factor] > Time.current
end
end
module RequiresHealthToken
extend ActiveSupport::Concern
included do
before_action :validate_health_check_access!
end
private
def validate_health_check_access!
render_404 unless token_valid?
end
def token_valid?
token = params[:token].presence || request.headers['TOKEN']
token.present? &&
ActiveSupport::SecurityUtils.variable_size_secure_compare(
token,
current_application_settings.health_check_access_token
)
end
def render_404
render file: Rails.root.join('public', '404'), layout: false, status: '404'
end
end
...@@ -151,7 +151,9 @@ class GroupsController < Groups::ApplicationController ...@@ -151,7 +151,9 @@ class GroupsController < Groups::ApplicationController
:visibility_level, :visibility_level,
:parent_id, :parent_id,
:create_chat_team, :create_chat_team,
:chat_team_name :chat_team_name,
:require_two_factor_authentication,
:two_factor_grace_period
] ]
end end
......
class HealthCheckController < HealthCheck::HealthCheckController class HealthCheckController < HealthCheck::HealthCheckController
before_action :validate_health_check_access! include RequiresHealthToken
private
def validate_health_check_access!
render_404 unless token_valid?
end
def token_valid?
token = params[:token].presence || request.headers['TOKEN']
token.present? &&
ActiveSupport::SecurityUtils.variable_size_secure_compare(
token,
current_application_settings.health_check_access_token
)
end
def render_404
render file: Rails.root.join('public', '404'), layout: false, status: '404'
end
end end
class HealthController < ActionController::Base
protect_from_forgery with: :exception
include RequiresHealthToken
CHECKS = [
Gitlab::HealthChecks::DbCheck,
Gitlab::HealthChecks::RedisCheck,
Gitlab::HealthChecks::FsShardsCheck,
].freeze
def readiness
results = CHECKS.map { |check| [check.name, check.readiness] }
render_check_results(results)
end
def liveness
results = CHECKS.map { |check| [check.name, check.liveness] }
render_check_results(results)
end
def metrics
results = CHECKS.flat_map(&:metrics)
response = results.map(&method(:metric_to_prom_line)).join("\n")
render text: response, content_type: 'text/plain; version=0.0.4'
end
private
def metric_to_prom_line(metric)
labels = metric.labels&.map { |key, value| "#{key}=\"#{value}\"" }&.join(',') || ''
if labels.empty?
"#{metric.name} #{metric.value}"
else
"#{metric.name}{#{labels}} #{metric.value}"
end
end
def render_check_results(results)
flattened = results.flat_map do |name, result|
if result.is_a?(Gitlab::HealthChecks::Result)
[[name, result]]
else
result.map { |r| [name, r] }
end
end
success = flattened.all? { |name, r| r.success }
response = flattened.map do |name, r|
info = { status: r.success ? 'ok' : 'failed' }
info['message'] = r.message if r.message
info[:labels] = r.labels if r.labels
[name, info]
end
render json: response.to_h, status: success ? :ok : :service_unavailable
end
end
class Profiles::TwoFactorAuthsController < Profiles::ApplicationController class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
skip_before_action :check_2fa_requirement skip_before_action :check_two_factor_requirement
def show def show
unless current_user.otp_secret unless current_user.otp_secret
...@@ -13,11 +13,24 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController ...@@ -13,11 +13,24 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
current_user.save! if current_user.changed? current_user.save! if current_user.changed?
if two_factor_authentication_required? && !current_user.two_factor_enabled? if two_factor_authentication_required? && !current_user.two_factor_enabled?
if two_factor_grace_period_expired? two_factor_authentication_reason(
flash.now[:alert] = 'You must enable Two-Factor Authentication for your account.' global: lambda do
else flash.now[:alert] =
'The global settings require you to enable Two-Factor Authentication for your account.'
end,
group: lambda do |groups|
group_links = groups.map { |group| view_context.link_to group.full_name, group_path(group) }.to_sentence
flash.now[:alert] = %{
The group settings for #{group_links} require you to enable
Two-Factor Authentication for your account.
}.html_safe
end
)
unless two_factor_grace_period_expired?
grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours
flash.now[:alert] = "You must enable Two-Factor Authentication for your account before #{l(grace_period_deadline)}." flash.now[:alert] << " You need to do this before #{l(grace_period_deadline)}."
end end
end end
...@@ -71,7 +84,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController ...@@ -71,7 +84,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
if two_factor_grace_period_expired? if two_factor_grace_period_expired?
redirect_to new_profile_two_factor_auth_path, alert: 'Cannot skip two factor authentication setup' redirect_to new_profile_two_factor_auth_path, alert: 'Cannot skip two factor authentication setup'
else else
session[:skip_tfa] = current_user.otp_grace_period_started_at + two_factor_grace_period.hours session[:skip_two_factor] = current_user.otp_grace_period_started_at + two_factor_grace_period.hours
redirect_to root_path redirect_to root_path
end end
end end
......
...@@ -36,6 +36,8 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -36,6 +36,8 @@ class Projects::CommitController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html format.html
format.json do format.json do
Gitlab::PollingInterval.set_header(response, interval: 10_000)
render json: PipelineSerializer render json: PipelineSerializer
.new(project: @project, user: @current_user) .new(project: @project, user: @current_user)
.represent(@pipelines) .represent(@pipelines)
......
...@@ -236,6 +236,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -236,6 +236,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
format.json do format.json do
Gitlab::PollingInterval.set_header(response, interval: 10_000)
render json: PipelineSerializer render json: PipelineSerializer
.new(project: @project, user: @current_user) .new(project: @project, user: @current_user)
.represent(@pipelines) .represent(@pipelines)
...@@ -249,6 +251,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -249,6 +251,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
format.json do format.json do
define_pipelines_vars define_pipelines_vars
Gitlab::PollingInterval.set_header(response, interval: 10_000)
render json: { render json: {
pipelines: PipelineSerializer pipelines: PipelineSerializer
.new(project: @project, user: @current_user) .new(project: @project, user: @current_user)
...@@ -478,7 +482,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -478,7 +482,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
if pipeline if pipeline
status = pipeline.status status = pipeline.status
coverage = pipeline.try(:coverage) coverage = pipeline.coverage
status = "success_with_warnings" if pipeline.success? && pipeline.has_warnings? status = "success_with_warnings" if pipeline.success? && pipeline.has_warnings?
......
...@@ -29,6 +29,8 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -29,6 +29,8 @@ class Projects::PipelinesController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html format.html
format.json do format.json do
Gitlab::PollingInterval.set_header(response, interval: 10_000)
render json: { render json: {
pipelines: PipelineSerializer pipelines: PipelineSerializer
.new(project: @project, user: @current_user) .new(project: @project, user: @current_user)
...@@ -114,7 +116,7 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -114,7 +116,7 @@ class Projects::PipelinesController < Projects::ApplicationController
end end
def pipeline def pipeline
@pipeline ||= project.pipelines.find_by!(id: params[:id]) @pipeline ||= project.pipelines.find_by!(id: params[:id]).present(current_user: current_user)
end end
def commit def commit
......
...@@ -23,7 +23,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController ...@@ -23,7 +23,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
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, :build_timeout_in_minutes, :build_coverage_regex,
:public_builds :public_builds, :auto_cancel_pending_pipelines
) )
end end
end end
...@@ -3,7 +3,7 @@ class SessionsController < Devise::SessionsController ...@@ -3,7 +3,7 @@ class SessionsController < Devise::SessionsController
include Devise::Controllers::Rememberable include Devise::Controllers::Rememberable
include Recaptcha::ClientHelper include Recaptcha::ClientHelper
skip_before_action :check_2fa_requirement, only: [:destroy] skip_before_action :check_two_factor_requirement, only: [:destroy]
prepend_before_action :check_initial_setup, only: [:new] prepend_before_action :check_initial_setup, only: [:new]
prepend_before_action :authenticate_with_two_factor, prepend_before_action :authenticate_with_two_factor,
......
...@@ -68,18 +68,6 @@ module AuthHelper ...@@ -68,18 +68,6 @@ module AuthHelper
current_user.identities.exists?(provider: provider.to_s) current_user.identities.exists?(provider: provider.to_s)
end end
def two_factor_skippable?
current_application_settings.require_two_factor_authentication &&
!current_user.two_factor_enabled? &&
current_application_settings.two_factor_grace_period &&
!two_factor_grace_period_expired?
end
def two_factor_grace_period_expired?
current_user.otp_grace_period_started_at &&
(current_user.otp_grace_period_started_at + current_application_settings.two_factor_grace_period.hours) < Time.current
end
def unlink_allowed?(provider) def unlink_allowed?(provider)
%w(saml cas3).exclude?(provider.to_s) %w(saml cas3).exclude?(provider.to_s)
end end
......
...@@ -102,7 +102,7 @@ module BlobHelper ...@@ -102,7 +102,7 @@ module BlobHelper
if Gitlab::MarkupHelper.previewable?(filename) if Gitlab::MarkupHelper.previewable?(filename)
'Preview' 'Preview'
else else
'Preview Changes' 'Preview changes'
end end
end end
......
module SystemNoteHelper
ICON_NAMES_BY_ACTION = {
'commit' => 'icon_commit',
'merge' => 'icon_merge',
'merged' => 'icon_merged',
'opened' => 'icon_status_open',
'closed' => 'icon_status_closed',
'time_tracking' => 'icon_stopwatch',
'assignee' => 'icon_user',
'title' => 'icon_pencil',
'task' => 'icon_check_square_o',
'label' => 'icon_tags',
'cross_reference' => 'icon_random',
'branch' => 'icon_code_fork',
'confidential' => 'icon_eye_slash',
'visible' => 'icon_eye',
'milestone' => 'icon_clock_o',
'discussion' => 'icon_comment_o',
'moved' => 'icon_arrow_circle_o_right'
}.freeze
def icon_for_system_note(note)
icon_name = ICON_NAMES_BY_ACTION[note.system_note_metadata&.action]
custom_icon(icon_name) if icon_name
end
end
...@@ -104,7 +104,6 @@ module Ci ...@@ -104,7 +104,6 @@ module Ci
end end
def playable? def playable?
project.builds_enabled? && has_commands? &&
action? && manual? action? && manual?
end end
...@@ -112,10 +111,6 @@ module Ci ...@@ -112,10 +111,6 @@ module Ci
self.when == 'manual' self.when == 'manual'
end end
def has_commands?
commands.present?
end
def play(current_user) def play(current_user)
# Try to queue a current build # Try to queue a current build
if self.enqueue if self.enqueue
...@@ -132,8 +127,7 @@ module Ci ...@@ -132,8 +127,7 @@ module Ci
end end
def retryable? def retryable?
project.builds_enabled? && has_commands? && success? || failed? || canceled?
(success? || failed? || canceled?)
end end
def retried? def retried?
......
...@@ -4,14 +4,25 @@ module Ci ...@@ -4,14 +4,25 @@ module Ci
include HasStatus include HasStatus
include Importable include Importable
include AfterCommitQueue include AfterCommitQueue
include Presentable
belongs_to :project belongs_to :project
belongs_to :user belongs_to :user
belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline'
has_many :auto_canceled_pipelines, class_name: 'Ci::Pipeline', foreign_key: 'auto_canceled_by_id'
has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id'
has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id
has_many :builds, foreign_key: :commit_id has_many :builds, foreign_key: :commit_id
has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id
has_many :pending_builds, -> { pending }, foreign_key: :commit_id, class_name: 'Ci::Build'
has_many :retryable_builds, -> { latest.failed_or_canceled }, foreign_key: :commit_id, class_name: 'Ci::Build'
has_many :cancelable_statuses, -> { cancelable }, foreign_key: :commit_id, class_name: 'CommitStatus'
has_many :manual_actions, -> { latest.manual_actions }, foreign_key: :commit_id, class_name: 'Ci::Build'
has_many :artifacts, -> { latest.with_artifacts_not_expired }, foreign_key: :commit_id, class_name: 'Ci::Build'
delegate :id, to: :project, prefix: true delegate :id, to: :project, prefix: true
validates :sha, presence: { unless: :importing? } validates :sha, presence: { unless: :importing? }
...@@ -65,6 +76,10 @@ module Ci ...@@ -65,6 +76,10 @@ module Ci
pipeline.update_duration pipeline.update_duration
end end
before_transition canceled: any - [:canceled] do |pipeline|
pipeline.auto_canceled_by = nil
end
after_transition [:created, :pending] => :running do |pipeline| after_transition [:created, :pending] => :running do |pipeline|
pipeline.run_after_commit { PipelineMetricsWorker.perform_async(id) } pipeline.run_after_commit { PipelineMetricsWorker.perform_async(id) }
end end
...@@ -82,6 +97,8 @@ module Ci ...@@ -82,6 +97,8 @@ module Ci
pipeline.run_after_commit do pipeline.run_after_commit do
PipelineHooksWorker.perform_async(id) PipelineHooksWorker.perform_async(id)
Ci::ExpirePipelineCacheService.new(project, nil)
.execute(pipeline)
end end
end end
...@@ -160,10 +177,6 @@ module Ci ...@@ -160,10 +177,6 @@ module Ci
end end
end end
def artifacts
builds.latest.with_artifacts_not_expired.includes(project: [:namespace])
end
def valid_commit_sha def valid_commit_sha
if self.sha == Gitlab::Git::BLANK_SHA if self.sha == Gitlab::Git::BLANK_SHA
self.errors.add(:sha, " cant be 00000000 (branch removal)") self.errors.add(:sha, " cant be 00000000 (branch removal)")
...@@ -200,26 +213,36 @@ module Ci ...@@ -200,26 +213,36 @@ module Ci
!tag? !tag?
end end
def manual_actions
builds.latest.manual_actions.includes(project: [:namespace])
end
def stuck? def stuck?
builds.pending.includes(:project).any?(&:stuck?) pending_builds.any?(&:stuck?)
end end
def retryable? def retryable?
builds.latest.failed_or_canceled.any?(&:retryable?) retryable_builds.any?
end end
def cancelable? def cancelable?
statuses.cancelable.any? cancelable_statuses.any?
end
def auto_canceled?
canceled? && auto_canceled_by_id?
end end
def cancel_running def cancel_running
Gitlab::OptimisticLocking.retry_lock( Gitlab::OptimisticLocking.retry_lock(cancelable_statuses) do |cancelable|
statuses.cancelable) do |cancelable| cancelable.find_each do |job|
cancelable.find_each(&:cancel) yield(job) if block_given?
job.cancel
end
end
end
def auto_cancel_running(pipeline)
update(auto_canceled_by: pipeline)
cancel_running do |job|
job.auto_canceled_by = pipeline
end end
end end
......
...@@ -8,6 +8,7 @@ module Ci ...@@ -8,6 +8,7 @@ module Ci
belongs_to :owner, class_name: "User" belongs_to :owner, class_name: "User"
has_many :trigger_requests, dependent: :destroy has_many :trigger_requests, dependent: :destroy
has_one :trigger_schedule, dependent: :destroy
validates :token, presence: true, uniqueness: true validates :token, presence: true, uniqueness: true
......
module Ci
class TriggerSchedule < ActiveRecord::Base
extend Ci::Model
include Importable
acts_as_paranoid
belongs_to :project
belongs_to :trigger
delegate :ref, to: :trigger
validates :trigger, presence: { unless: :importing? }
validates :cron, cron: true, presence: { unless: :importing? }
validates :cron_timezone, cron_timezone: true, presence: { unless: :importing? }
validates :ref, presence: { unless: :importing? }
before_save :set_next_run_at
def set_next_run_at
self.next_run_at = Gitlab::Ci::CronParser.new(cron, cron_timezone).next_time_from(Time.now)
end
def schedule_next_run!
save! # with set_next_run_at
rescue ActiveRecord::RecordInvalid
update_attribute(:next_run_at, nil) # update without validation
end
end
end
...@@ -7,6 +7,7 @@ class CommitStatus < ActiveRecord::Base ...@@ -7,6 +7,7 @@ class CommitStatus < ActiveRecord::Base
belongs_to :project belongs_to :project
belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id
belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline'
belongs_to :user belongs_to :user
delegate :commit, to: :pipeline delegate :commit, to: :pipeline
...@@ -137,6 +138,10 @@ class CommitStatus < ActiveRecord::Base ...@@ -137,6 +138,10 @@ class CommitStatus < ActiveRecord::Base
false false
end end
def auto_canceled?
canceled? && auto_canceled_by_id?
end
# Added in 9.0 to keep backward compatibility for projects exported in 8.17 # Added in 9.0 to keep backward compatibility for projects exported in 8.17
# and prior. # and prior.
def gl_project_id def gl_project_id
......
...@@ -76,6 +76,7 @@ module HasStatus ...@@ -76,6 +76,7 @@ module HasStatus
scope :canceled, -> { where(status: 'canceled') } scope :canceled, -> { where(status: 'canceled') }
scope :skipped, -> { where(status: 'skipped') } scope :skipped, -> { where(status: 'skipped') }
scope :manual, -> { where(status: 'manual') } scope :manual, -> { where(status: 'manual') }
scope :created_or_pending, -> { where(status: [:created, :pending]) }
scope :running_or_pending, -> { where(status: [:running, :pending]) } scope :running_or_pending, -> { where(status: [:running, :pending]) }
scope :finished, -> { where(status: [:success, :failed, :canceled]) } scope :finished, -> { where(status: [:success, :failed, :canceled]) }
scope :failed_or_canceled, -> { where(status: [:failed, :canceled]) } scope :failed_or_canceled, -> { where(status: [:failed, :canceled]) }
......
...@@ -83,6 +83,74 @@ module Routable ...@@ -83,6 +83,74 @@ module Routable
AND members.source_type = r2.source_type"). AND members.source_type = r2.source_type").
where('members.user_id = ?', user_id) where('members.user_id = ?', user_id)
end end
# Builds a relation to find multiple objects that are nested under user
# membership. Includes the parent, as opposed to `#member_descendants`
# which only includes the descendants.
#
# Usage:
#
# Klass.member_self_and_descendants(1)
#
# Returns an ActiveRecord::Relation.
def member_self_and_descendants(user_id)
joins(:route).
joins("INNER JOIN routes r2 ON routes.path LIKE CONCAT(r2.path, '/%')
OR routes.path = r2.path
INNER JOIN members ON members.source_id = r2.source_id
AND members.source_type = r2.source_type").
where('members.user_id = ?', user_id)
end
# Returns all objects in a hierarchy, where any node in the hierarchy is
# under the user membership.
#
# Usage:
#
# Klass.member_hierarchy(1)
#
# Examples:
#
# Given the following group tree...
#
# _______group_1_______
# | |
# | |
# nested_group_1 nested_group_2
# | |
# | |
# nested_group_1_1 nested_group_2_1
#
#
# ... the following results are returned:
#
# * the user is a member of group 1
# => 'group_1',
# 'nested_group_1', nested_group_1_1',
# 'nested_group_2', 'nested_group_2_1'
#
# * the user is a member of nested_group_2
# => 'group1',
# 'nested_group_2', 'nested_group_2_1'
#
# * the user is a member of nested_group_2_1
# => 'group1',
# 'nested_group_2', 'nested_group_2_1'
#
# Returns an ActiveRecord::Relation.
def member_hierarchy(user_id)
paths = member_self_and_descendants(user_id).pluck('routes.path')
return none if paths.empty?
wheres = paths.map do |path|
"#{connection.quote(path)} = routes.path
OR
#{connection.quote(path)} LIKE CONCAT(routes.path, '/%')"
end
joins(:route).where(wheres.join(' OR '))
end
end end
def full_name def full_name
......
...@@ -36,6 +36,8 @@ class Group < Namespace ...@@ -36,6 +36,8 @@ class Group < Namespace
validates :avatar, file_size: { maximum: 200.kilobytes.to_i } validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 }
validates :repository_size_limit, validates :repository_size_limit,
numericality: { only_integer: true, greater_than_or_equal_to: 0, allow_nil: true } numericality: { only_integer: true, greater_than_or_equal_to: 0, allow_nil: true }
...@@ -44,6 +46,7 @@ class Group < Namespace ...@@ -44,6 +46,7 @@ class Group < Namespace
after_create :post_create_hook after_create :post_create_hook
after_destroy :post_destroy_hook after_destroy :post_destroy_hook
after_save :update_two_factor_requirement
scope :where_group_links_with_provider, ->(provider) do scope :where_group_links_with_provider, ->(provider) do
joins(:ldap_group_links).where(ldap_group_links: { provider: provider }) joins(:ldap_group_links).where(ldap_group_links: { provider: provider })
...@@ -267,4 +270,12 @@ class Group < Namespace ...@@ -267,4 +270,12 @@ class Group < Namespace
type: public? ? 'O' : 'I' # Open vs Invite-only type: public? ? 'O' : 'I' # Open vs Invite-only
} }
end end
protected
def update_two_factor_requirement
return unless require_two_factor_authentication_changed? || two_factor_grace_period_changed?
users.find_each(&:update_two_factor_requirement)
end
end end
...@@ -3,11 +3,16 @@ class GroupMember < Member ...@@ -3,11 +3,16 @@ class GroupMember < Member
belongs_to :group, foreign_key: 'source_id' belongs_to :group, foreign_key: 'source_id'
delegate :update_two_factor_requirement, to: :user
# Make sure group member points only to group as it source # Make sure group member points only to group as it source
default_value_for :source_type, SOURCE_TYPE default_value_for :source_type, SOURCE_TYPE
validates :source_type, format: { with: /\ANamespace\z/ } validates :source_type, format: { with: /\ANamespace\z/ }
default_scope { where(source_type: SOURCE_TYPE) } default_scope { where(source_type: SOURCE_TYPE) }
after_create :update_two_factor_requirement, unless: :invite?
after_destroy :update_two_factor_requirement, unless: :invite?
scope :with_ldap_dn, -> { joins(user: :identities).where("identities.provider LIKE ?", 'ldap%') } scope :with_ldap_dn, -> { joins(user: :identities).where("identities.provider LIKE ?", 'ldap%') }
scope :with_identity_provider, ->(provider) do scope :with_identity_provider, ->(provider) do
joins(user: :identities).where(identities: { provider: provider }) joins(user: :identities).where(identities: { provider: provider })
......
...@@ -155,10 +155,6 @@ class Milestone < ActiveRecord::Base ...@@ -155,10 +155,6 @@ class Milestone < ActiveRecord::Base
active? && issues.opened.count.zero? active? && issues.opened.count.zero?
end end
def is_empty?(user = nil)
total_items_count(user).zero?
end
def author_id def author_id
nil nil
end end
......
...@@ -118,6 +118,7 @@ class Project < ActiveRecord::Base ...@@ -118,6 +118,7 @@ class Project < ActiveRecord::Base
has_one :mock_ci_service, dependent: :destroy has_one :mock_ci_service, dependent: :destroy
has_one :mock_deployment_service, dependent: :destroy has_one :mock_deployment_service, dependent: :destroy
has_one :mock_monitoring_service, dependent: :destroy has_one :mock_monitoring_service, dependent: :destroy
has_one :microsoft_teams_service, dependent: :destroy
has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id"
has_one :forked_from_project, through: :forked_project_link has_one :forked_from_project, through: :forked_project_link
...@@ -179,6 +180,8 @@ class Project < ActiveRecord::Base ...@@ -179,6 +180,8 @@ class Project < ActiveRecord::Base
has_many :deployments, dependent: :destroy has_many :deployments, dependent: :destroy
has_many :path_locks, dependent: :destroy has_many :path_locks, dependent: :destroy
has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
accepts_nested_attributes_for :variables, allow_destroy: true accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :remote_mirrors, accepts_nested_attributes_for :remote_mirrors,
allow_destroy: true, reject_if: ->(attrs) { attrs[:id].blank? && attrs[:url].blank? } allow_destroy: true, reject_if: ->(attrs) { attrs[:id].blank? && attrs[:url].blank? }
...@@ -286,6 +289,8 @@ class Project < ActiveRecord::Base ...@@ -286,6 +289,8 @@ class Project < ActiveRecord::Base
scope :with_issues_enabled, -> { with_feature_enabled(:issues) } scope :with_issues_enabled, -> { with_feature_enabled(:issues) }
scope :with_wiki_enabled, -> { with_feature_enabled(:wiki) } scope :with_wiki_enabled, -> { with_feature_enabled(:wiki) }
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
# project features may be "disabled", "internal" or "enabled". If "internal", # project features may be "disabled", "internal" or "enabled". If "internal",
# they are only available to team members. This scope returns projects where # they are only available to team members. This scope returns projects where
# the feature is either enabled, or internal with permission for the user. # the feature is either enabled, or internal with permission for the user.
...@@ -1215,25 +1220,21 @@ class Project < ActiveRecord::Base ...@@ -1215,25 +1220,21 @@ class Project < ActiveRecord::Base
end end
def shared_runners def shared_runners
shared_runners_available? ? Ci::Runner.shared : Ci::Runner.none @shared_runners ||= shared_runners_available? ? Ci::Runner.shared : Ci::Runner.none
end end
def any_runners?(&block) def active_shared_runners
if runners.active.any?(&block) @active_shared_runners ||= shared_runners.active
return true
end end
shared_runners.active.any?(&block) def any_runners?(&block)
active_runners.any?(&block) || active_shared_runners.any?(&block)
end end
def valid_runners_token?(token) def valid_runners_token?(token)
self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token) self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
end end
def build_coverage_enabled?
build_coverage_regex.present?
end
def build_timeout_in_minutes def build_timeout_in_minutes
build_timeout / 60 build_timeout / 60
end end
......
...@@ -2,11 +2,23 @@ require 'slack-notifier' ...@@ -2,11 +2,23 @@ require 'slack-notifier'
module ChatMessage module ChatMessage
class BaseMessage class BaseMessage
attr_reader :markdown
attr_reader :user_name
attr_reader :user_avatar
attr_reader :project_name
attr_reader :project_url
def initialize(params) def initialize(params)
raise NotImplementedError @markdown = params[:markdown] || false
@project_name = params.dig(:project, :path_with_namespace) || params[:project_name]
@project_url = params.dig(:project, :web_url) || params[:project_url]
@user_name = params.dig(:user, :username) || params[:user_name]
@user_avatar = params.dig(:user, :avatar_url) || params[:user_avatar]
end end
def pretext def pretext
return message if markdown
format(message) format(message)
end end
...@@ -17,6 +29,10 @@ module ChatMessage ...@@ -17,6 +29,10 @@ module ChatMessage
raise NotImplementedError raise NotImplementedError
end end
def activity
raise NotImplementedError
end
private private
def message def message
......
module ChatMessage module ChatMessage
class IssueMessage < BaseMessage class IssueMessage < BaseMessage
attr_reader :user_name
attr_reader :title attr_reader :title
attr_reader :project_name
attr_reader :project_url
attr_reader :issue_iid attr_reader :issue_iid
attr_reader :issue_url attr_reader :issue_url
attr_reader :action attr_reader :action
...@@ -11,9 +8,7 @@ module ChatMessage ...@@ -11,9 +8,7 @@ module ChatMessage
attr_reader :description attr_reader :description
def initialize(params) def initialize(params)
@user_name = params[:user][:username] super
@project_name = params[:project_name]
@project_url = params[:project_url]
obj_attr = params[:object_attributes] obj_attr = params[:object_attributes]
obj_attr = HashWithIndifferentAccess.new(obj_attr) obj_attr = HashWithIndifferentAccess.new(obj_attr)
...@@ -27,15 +22,24 @@ module ChatMessage ...@@ -27,15 +22,24 @@ module ChatMessage
def attachments def attachments
return [] unless opened_issue? return [] unless opened_issue?
return description if markdown
description_message description_message
end end
def activity
{
title: "Issue #{state} by #{user_name}",
subtitle: "in #{project_link}",
text: issue_link,
image: user_avatar
}
end
private private
def message def message
case state if state == 'opened'
when "opened"
"[#{project_link}] Issue #{state} by #{user_name}" "[#{project_link}] Issue #{state} by #{user_name}"
else else
"[#{project_link}] Issue #{issue_link} #{state} by #{user_name}" "[#{project_link}] Issue #{issue_link} #{state} by #{user_name}"
...@@ -64,7 +68,7 @@ module ChatMessage ...@@ -64,7 +68,7 @@ module ChatMessage
end end
def issue_title def issue_title
"##{issue_iid} #{title}" "#{Issue.reference_prefix}#{issue_iid} #{title}"
end end
end end
end end
module ChatMessage module ChatMessage
class MergeMessage < BaseMessage class MergeMessage < BaseMessage
attr_reader :user_name attr_reader :merge_request_iid
attr_reader :project_name
attr_reader :project_url
attr_reader :merge_request_id
attr_reader :source_branch attr_reader :source_branch
attr_reader :target_branch attr_reader :target_branch
attr_reader :state attr_reader :state
attr_reader :title attr_reader :title
def initialize(params) def initialize(params)
@user_name = params[:user][:username] super
@project_name = params[:project_name]
@project_url = params[:project_url]
@action = params[:object_attributes][:action] @action = params[:object_attributes][:action]
obj_attr = params[:object_attributes] obj_attr = params[:object_attributes]
obj_attr = HashWithIndifferentAccess.new(obj_attr) obj_attr = HashWithIndifferentAccess.new(obj_attr)
@merge_request_id = obj_attr[:iid] @merge_request_iid = obj_attr[:iid]
@source_branch = obj_attr[:source_branch] @source_branch = obj_attr[:source_branch]
@target_branch = obj_attr[:target_branch] @target_branch = obj_attr[:target_branch]
@state = obj_attr[:state] @state = obj_attr[:state]
@title = format_title(obj_attr[:title]) @title = format_title(obj_attr[:title])
end end
def pretext
format(message)
end
def attachments def attachments
[] []
end end
def activity
{
title: "Merge Request #{state} by #{user_name}",
subtitle: "in #{project_link}",
text: merge_request_link,
image: user_avatar
}
end
private private
def format_title(title) def format_title(title)
...@@ -51,11 +52,15 @@ module ChatMessage ...@@ -51,11 +52,15 @@ module ChatMessage
end end
def merge_request_link def merge_request_link
link("merge request !#{merge_request_id}", merge_request_url) link(merge_request_title, merge_request_url)
end
def merge_request_title
"#{MergeRequest.reference_prefix}#{merge_request_iid} #{title}"
end end
def merge_request_url def merge_request_url
"#{project_url}/merge_requests/#{merge_request_id}" "#{project_url}/merge_requests/#{merge_request_iid}"
end end
def state_or_action_text def state_or_action_text
......
module ChatMessage module ChatMessage
class NoteMessage < BaseMessage class NoteMessage < BaseMessage
attr_reader :message
attr_reader :user_name
attr_reader :project_name
attr_reader :project_url
attr_reader :note attr_reader :note
attr_reader :note_url attr_reader :note_url
attr_reader :title
attr_reader :target
def initialize(params) def initialize(params)
params = HashWithIndifferentAccess.new(params) super
@user_name = params[:user][:username]
@project_name = params[:project_name]
@project_url = params[:project_url]
params = HashWithIndifferentAccess.new(params)
obj_attr = params[:object_attributes] obj_attr = params[:object_attributes]
obj_attr = HashWithIndifferentAccess.new(obj_attr)
@note = obj_attr[:note] @note = obj_attr[:note]
@note_url = obj_attr[:url] @note_url = obj_attr[:url]
noteable_type = obj_attr[:noteable_type] @target, @title = case obj_attr[:noteable_type]
case noteable_type
when "Commit" when "Commit"
create_commit_note(HashWithIndifferentAccess.new(params[:commit])) create_commit_note(params[:commit])
when "Issue" when "Issue"
create_issue_note(HashWithIndifferentAccess.new(params[:issue])) create_issue_note(params[:issue])
when "MergeRequest" when "MergeRequest"
create_merge_note(HashWithIndifferentAccess.new(params[:merge_request])) create_merge_note(params[:merge_request])
when "Snippet" when "Snippet"
create_snippet_note(HashWithIndifferentAccess.new(params[:snippet])) create_snippet_note(params[:snippet])
end end
end end
def attachments def attachments
return note if markdown
description_message description_message
end end
def activity
{
title: "#{user_name} #{link('commented on ' + target, note_url)}",
subtitle: "in #{project_link}",
text: formatted_title,
image: user_avatar
}
end
private private
def message
"#{user_name} #{link('commented on ' + target, note_url)} in #{project_link}: *#{formatted_title}*"
end
def format_title(title) def format_title(title)
title.lines.first.chomp title.lines.first.chomp
end end
def create_commit_note(commit) def formatted_title
commit_sha = commit[:id] format_title(title)
commit_sha = Commit.truncate_sha(commit_sha)
commented_on_message(
"commit #{commit_sha}",
format_title(commit[:message]))
end end
def create_issue_note(issue) def create_issue_note(issue)
commented_on_message( ["issue #{Issue.reference_prefix}#{issue[:iid]}", issue[:title]]
"issue ##{issue[:iid]}", end
format_title(issue[:title]))
def create_commit_note(commit)
commit_sha = Commit.truncate_sha(commit[:id])
["commit #{commit_sha}", commit[:message]]
end end
def create_merge_note(merge_request) def create_merge_note(merge_request)
commented_on_message( ["merge request #{MergeRequest.reference_prefix}#{merge_request[:iid]}", merge_request[:title]]
"merge request !#{merge_request[:iid]}",
format_title(merge_request[:title]))
end end
def create_snippet_note(snippet) def create_snippet_note(snippet)
commented_on_message( ["snippet #{Snippet.reference_prefix}#{snippet[:id]}", snippet[:title]]
"snippet ##{snippet[:id]}",
format_title(snippet[:title]))
end end
def description_message def description_message
...@@ -74,9 +78,5 @@ module ChatMessage ...@@ -74,9 +78,5 @@ module ChatMessage
def project_link def project_link
link(project_name, project_url) link(project_name, project_url)
end end
def commented_on_message(target, title)
@message = "#{user_name} #{link('commented on ' + target, note_url)} in #{project_link}: *#{title}*"
end
end end
end end
module ChatMessage module ChatMessage
class PipelineMessage < BaseMessage class PipelineMessage < BaseMessage
attr_reader :ref_type, :ref, :status, :project_name, :project_url, attr_reader :ref_type
:user_name, :duration, :pipeline_id attr_reader :ref
attr_reader :status
attr_reader :duration
attr_reader :pipeline_id
def initialize(data) def initialize(data)
super
@user_name = data.dig(:user, :name) || 'API'
pipeline_attributes = data[:object_attributes] pipeline_attributes = data[:object_attributes]
@ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch' @ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
@ref = pipeline_attributes[:ref] @ref = pipeline_attributes[:ref]
@status = pipeline_attributes[:status] @status = pipeline_attributes[:status]
@duration = pipeline_attributes[:duration] @duration = pipeline_attributes[:duration]
@pipeline_id = pipeline_attributes[:id] @pipeline_id = pipeline_attributes[:id]
@project_name = data[:project][:path_with_namespace]
@project_url = data[:project][:web_url]
@user_name = (data[:user] && data[:user][:name]) || 'API'
end end
def pretext def pretext
...@@ -25,17 +28,24 @@ module ChatMessage ...@@ -25,17 +28,24 @@ module ChatMessage
end end
def attachments def attachments
return message if markdown
[{ text: format(message), color: attachment_color }] [{ text: format(message), color: attachment_color }]
end end
def activity
{
title: "Pipeline #{pipeline_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status}",
subtitle: "in #{project_link}",
text: "in #{duration} #{time_measure}",
image: user_avatar || ''
}
end
private private
def message def message
"#{project_link}: Pipeline #{pipeline_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{'second'.pluralize(duration)}" "#{project_link}: Pipeline #{pipeline_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{time_measure}"
end
def format(string)
Slack::Notifier::LinkFormatter.format(string)
end end
def humanized_status def humanized_status
...@@ -74,5 +84,9 @@ module ChatMessage ...@@ -74,5 +84,9 @@ module ChatMessage
def pipeline_link def pipeline_link
"[##{pipeline_id}](#{pipeline_url})" "[##{pipeline_id}](#{pipeline_url})"
end end
def time_measure
'second'.pluralize(duration)
end
end end
end end
...@@ -3,33 +3,43 @@ module ChatMessage ...@@ -3,33 +3,43 @@ module ChatMessage
attr_reader :after attr_reader :after
attr_reader :before attr_reader :before
attr_reader :commits attr_reader :commits
attr_reader :project_name
attr_reader :project_url
attr_reader :ref attr_reader :ref
attr_reader :ref_type attr_reader :ref_type
attr_reader :user_name
def initialize(params) def initialize(params)
super
@after = params[:after] @after = params[:after]
@before = params[:before] @before = params[:before]
@commits = params.fetch(:commits, []) @commits = params.fetch(:commits, [])
@project_name = params[:project_name]
@project_url = params[:project_url]
@ref_type = Gitlab::Git.tag_ref?(params[:ref]) ? 'tag' : 'branch' @ref_type = Gitlab::Git.tag_ref?(params[:ref]) ? 'tag' : 'branch'
@ref = Gitlab::Git.ref_name(params[:ref]) @ref = Gitlab::Git.ref_name(params[:ref])
@user_name = params[:user_name]
end
def pretext
format(message)
end end
def attachments def attachments
return [] if new_branch? || removed_branch? return [] if new_branch? || removed_branch?
return commit_messages if markdown
commit_message_attachments commit_message_attachments
end end
def activity
action = if new_branch?
"created"
elsif removed_branch?
"removed"
else
"pushed to"
end
{
title: "#{user_name} #{action} #{ref_type}",
subtitle: "in #{project_link}",
text: compare_link,
image: user_avatar
}
end
private private
def message def message
...@@ -59,7 +69,7 @@ module ChatMessage ...@@ -59,7 +69,7 @@ module ChatMessage
end end
def commit_messages def commit_messages
commits.map { |commit| compose_commit_message(commit) }.join("\n") commits.map { |commit| compose_commit_message(commit) }.join("\n\n")
end end
def commit_message_attachments def commit_message_attachments
......
module ChatMessage module ChatMessage
class WikiPageMessage < BaseMessage class WikiPageMessage < BaseMessage
attr_reader :user_name
attr_reader :title attr_reader :title
attr_reader :project_name
attr_reader :project_url
attr_reader :wiki_page_url attr_reader :wiki_page_url
attr_reader :action attr_reader :action
attr_reader :description attr_reader :description
def initialize(params) def initialize(params)
@user_name = params[:user][:username] super
@project_name = params[:project_name]
@project_url = params[:project_url]
obj_attr = params[:object_attributes] obj_attr = params[:object_attributes]
obj_attr = HashWithIndifferentAccess.new(obj_attr) obj_attr = HashWithIndifferentAccess.new(obj_attr)
...@@ -29,9 +24,20 @@ module ChatMessage ...@@ -29,9 +24,20 @@ module ChatMessage
end end
def attachments def attachments
return description if markdown
description_message description_message
end end
def activity
{
title: "#{user_name} #{action} #{wiki_page_link}",
subtitle: "in #{project_link}",
text: title,
image: user_avatar
}
end
private private
def message def message
......
...@@ -49,10 +49,7 @@ class ChatNotificationService < Service ...@@ -49,10 +49,7 @@ class ChatNotificationService < Service
object_kind = data[:object_kind] object_kind = data[:object_kind]
data = data.merge( data = custom_data(data)
project_url: project_url,
project_name: project_name
)
# WebHook events often have an 'update' event that follows a 'open' or # WebHook events often have an 'update' event that follows a 'open' or
# 'close' action. Ignore update events for now to prevent duplicate # 'close' action. Ignore update events for now to prevent duplicate
...@@ -68,8 +65,7 @@ class ChatNotificationService < Service ...@@ -68,8 +65,7 @@ class ChatNotificationService < Service
opts[:channel] = channel_name if channel_name opts[:channel] = channel_name if channel_name
opts[:username] = username if username opts[:username] = username if username
notifier = Slack::Notifier.new(webhook, opts) return false unless notify(message, opts)
notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback)
true true
end end
...@@ -92,6 +88,18 @@ class ChatNotificationService < Service ...@@ -92,6 +88,18 @@ class ChatNotificationService < Service
private private
def notify(message, opts)
Slack::Notifier.new(webhook, opts).ping(
message.pretext,
attachments: message.attachments,
fallback: message.fallback
)
end
def custom_data(data)
data.merge(project_url: project_url, project_name: project_name)
end
def get_message(object_kind, data) def get_message(object_kind, data)
case object_kind case object_kind
when "push", "tag_push" when "push", "tag_push"
......
...@@ -91,7 +91,7 @@ class JiraService < IssueTrackerService ...@@ -91,7 +91,7 @@ class JiraService < IssueTrackerService
{ type: 'text', name: 'project_key', placeholder: 'Project Key' }, { type: 'text', name: 'project_key', placeholder: 'Project Key' },
{ type: 'text', name: 'username', placeholder: '' }, { type: 'text', name: 'username', placeholder: '' },
{ type: 'password', name: 'password', placeholder: '' }, { type: 'password', name: 'password', placeholder: '' },
{ type: 'text', name: 'jira_issue_transition_id', placeholder: '2' } { type: 'text', name: 'jira_issue_transition_id', placeholder: '' }
] ]
end end
......
class MicrosoftTeamsService < ChatNotificationService
def title
'Microsoft Teams Notification'
end
def description
'Receive event notifications in Microsoft Teams'
end
def self.to_param
'microsoft_teams'
end
def help
'This service sends notifications about projects events to Microsoft Teams channels.<br />
To set up this service:
<ol>
<li><a href="https://msdn.microsoft.com/en-us/microsoft-teams/connectors">Getting started with 365 Office Connectors For Microsoft Teams</a>.</li>
<li>Paste the <strong>Webhook URL</strong> into the field below.</li>
<li>Select events below to enable notifications.</li>
</ol>'
end
def webhook_placeholder
'https://outlook.office.com/webhook/…'
end
def event_field(event)
end
def default_channel_placeholder
end
def default_fields
[
{ type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}" },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
{ type: 'checkbox', name: 'notify_only_default_branch' },
]
end
private
def notify(message, opts)
MicrosoftTeams::Notifier.new(webhook).ping(
title: message.project_name,
pretext: message.pretext,
activity: message.activity,
attachments: message.attachments
)
end
def custom_data(data)
super(data).merge(markdown: true)
end
end
...@@ -10,6 +10,8 @@ class Repository ...@@ -10,6 +10,8 @@ class Repository
attr_accessor :path_with_namespace, :project attr_accessor :path_with_namespace, :project
delegate :ref_name_for_sha, to: :raw_repository
CommitError = Class.new(StandardError) CommitError = Class.new(StandardError)
CreateTreeError = Class.new(StandardError) CreateTreeError = Class.new(StandardError)
...@@ -707,14 +709,6 @@ class Repository ...@@ -707,14 +709,6 @@ class Repository
end end
end end
def ref_name_for_sha(ref_path, sha)
args = %W(#{Gitlab.config.git.bin_path} for-each-ref --count=1 #{ref_path} --contains #{sha})
# Not found -> ["", 0]
# Found -> ["b8d95eb4969eefacb0a58f6a28f6803f8070e7b9 commit\trefs/environments/production/77\n", 0]
Gitlab::Popen.popen(args, path_to_repo).first.split.last
end
def refs_contains_sha(ref_type, sha) def refs_contains_sha(ref_type, sha)
args = %W(#{Gitlab.config.git.bin_path} #{ref_type} --contains #{sha}) args = %W(#{Gitlab.config.git.bin_path} #{ref_type} --contains #{sha})
names = Gitlab::Popen.popen(args, path_to_repo).first names = Gitlab::Popen.popen(args, path_to_repo).first
...@@ -1242,6 +1236,8 @@ class Repository ...@@ -1242,6 +1236,8 @@ class Repository
@project.repository_storage_path @project.repository_storage_path
end end
delegate :gitaly_channel, :gitaly_repository, to: :raw_repository
def initialize_raw_repository def initialize_raw_repository
Gitlab::Git::Repository.new(project.repository_storage, path_with_namespace + '.git') Gitlab::Git::Repository.new(project.repository_storage, path_with_namespace + '.git')
end end
......
...@@ -239,6 +239,7 @@ class Service < ActiveRecord::Base ...@@ -239,6 +239,7 @@ class Service < ActiveRecord::Base
slack_slash_commands slack_slash_commands
slack slack
teamcity teamcity
microsoft_teams
] ]
if Rails.env.development? if Rails.env.development?
service_names += %w[mock_ci mock_deployment mock_monitoring] service_names += %w[mock_ci mock_deployment mock_monitoring]
......
...@@ -90,11 +90,7 @@ class User < ActiveRecord::Base ...@@ -90,11 +90,7 @@ class User < ActiveRecord::Base
has_many :events, dependent: :destroy, foreign_key: :author_id has_many :events, dependent: :destroy, foreign_key: :author_id
has_many :subscriptions, dependent: :destroy has_many :subscriptions, dependent: :destroy
has_many :recent_events, -> { order "id DESC" }, foreign_key: :author_id, class_name: "Event" has_many :recent_events, -> { order "id DESC" }, foreign_key: :author_id, class_name: "Event"
has_many :assigned_issues, dependent: :destroy, foreign_key: :assignee_id, class_name: "Issue"
has_many :assigned_merge_requests, dependent: :destroy, foreign_key: :assignee_id, class_name: "MergeRequest"
has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy
has_many :approvals, dependent: :destroy
has_many :approvers, dependent: :destroy
has_one :abuse_report, dependent: :destroy, foreign_key: :user_id has_one :abuse_report, dependent: :destroy, foreign_key: :user_id
has_many :reported_abuse_reports, dependent: :destroy, foreign_key: :reporter_id, class_name: "AbuseReport" has_many :reported_abuse_reports, dependent: :destroy, foreign_key: :reporter_id, class_name: "AbuseReport"
has_many :spam_logs, dependent: :destroy has_many :spam_logs, dependent: :destroy
...@@ -105,6 +101,9 @@ class User < ActiveRecord::Base ...@@ -105,6 +101,9 @@ class User < ActiveRecord::Base
has_many :award_emoji, dependent: :destroy has_many :award_emoji, dependent: :destroy
has_many :path_locks, dependent: :destroy has_many :path_locks, dependent: :destroy
has_many :approvals, dependent: :destroy
has_many :approvers, dependent: :destroy
# Protected Branch Access # Protected Branch Access
has_many :protected_branch_merge_access_levels, dependent: :destroy, class_name: ProtectedBranch::MergeAccessLevel has_many :protected_branch_merge_access_levels, dependent: :destroy, class_name: ProtectedBranch::MergeAccessLevel
has_many :protected_branch_push_access_levels, dependent: :destroy, class_name: ProtectedBranch::PushAccessLevel has_many :protected_branch_push_access_levels, dependent: :destroy, class_name: ProtectedBranch::PushAccessLevel
...@@ -512,6 +511,14 @@ class User < ActiveRecord::Base ...@@ -512,6 +511,14 @@ class User < ActiveRecord::Base
Group.member_descendants(id) Group.member_descendants(id)
end end
def all_expanded_groups
Group.member_hierarchy(id)
end
def expanded_groups_requiring_two_factor_authentication
all_expanded_groups.where(require_two_factor_authentication: true)
end
def nested_groups_projects def nested_groups_projects
Project.joins(:namespace).where('namespaces.parent_id IS NOT NULL'). Project.joins(:namespace).where('namespaces.parent_id IS NOT NULL').
member_descendants(id) member_descendants(id)
...@@ -986,6 +993,15 @@ class User < ActiveRecord::Base ...@@ -986,6 +993,15 @@ class User < ActiveRecord::Base
self.auditor = (new_level == 'auditor') self.auditor = (new_level == 'auditor')
end end
def update_two_factor_requirement
periods = expanded_groups_requiring_two_factor_authentication.pluck(:two_factor_grace_period)
self.require_two_factor_authentication_from_group = periods.any?
self.two_factor_grace_period = periods.min || User.column_defaults['two_factor_grace_period']
save
end
protected protected
# override, from Devise::Validatable # override, from Devise::Validatable
......
...@@ -11,5 +11,11 @@ module Ci ...@@ -11,5 +11,11 @@ module Ci
def erased_by_name def erased_by_name
erased_by.name if erased_by_user? erased_by.name if erased_by_user?
end end
def status_title
if auto_canceled?
"Job is redundant and is auto-canceled by Pipeline ##{auto_canceled_by_id}"
end
end
end end
end end
module Ci
class PipelinePresenter < Gitlab::View::Presenter::Delegated
presents :pipeline
def status_title
if auto_canceled?
"Pipeline is redundant and is auto-canceled by Pipeline ##{auto_canceled_by_id}"
end
end
end
end
...@@ -69,13 +69,13 @@ class PipelineEntity < Grape::Entity ...@@ -69,13 +69,13 @@ class PipelineEntity < Grape::Entity
alias_method :pipeline, :object alias_method :pipeline, :object
def can_retry? def can_retry?
pipeline.retryable? && can?(request.user, :update_pipeline, pipeline) &&
can?(request.user, :update_pipeline, pipeline) pipeline.retryable?
end end
def can_cancel? def can_cancel?
pipeline.cancelable? && can?(request.user, :update_pipeline, pipeline) &&
can?(request.user, :update_pipeline, pipeline) pipeline.cancelable?
end end
def detailed_status def detailed_status
......
...@@ -13,7 +13,15 @@ class PipelineSerializer < BaseSerializer ...@@ -13,7 +13,15 @@ class PipelineSerializer < BaseSerializer
def represent(resource, opts = {}) def represent(resource, opts = {})
if resource.is_a?(ActiveRecord::Relation) if resource.is_a?(ActiveRecord::Relation)
resource = resource.includes(project: :namespace) resource = resource.preload([
:retryable_builds,
:cancelable_statuses,
:trigger_requests,
:project,
{ pending_builds: :project },
{ manual_actions: :project },
{ artifacts: :project }
])
end end
if paginated? if paginated?
......
...@@ -57,6 +57,8 @@ module Ci ...@@ -57,6 +57,8 @@ module Ci
.execute(pipeline) .execute(pipeline)
end end
cancel_pending_pipelines if project.auto_cancel_pending_pipelines?
pipeline.tap(&:process!) pipeline.tap(&:process!)
end end
...@@ -67,6 +69,22 @@ module Ci ...@@ -67,6 +69,22 @@ module Ci
pipeline.git_commit_message =~ /\[(ci[ _-]skip|skip[ _-]ci)\]/i pipeline.git_commit_message =~ /\[(ci[ _-]skip|skip[ _-]ci)\]/i
end end
def cancel_pending_pipelines
Gitlab::OptimisticLocking.retry_lock(auto_cancelable_pipelines) do |cancelables|
cancelables.find_each do |cancelable|
cancelable.auto_cancel_running(pipeline)
end
end
end
def auto_cancelable_pipelines
project.pipelines
.where(ref: pipeline.ref)
.where.not(id: pipeline.id)
.where.not(sha: project.repository.sha_from_ref(pipeline.ref))
.created_or_pending
end
def commit def commit
@commit ||= project.commit(origin_sha || origin_ref) @commit ||= project.commit(origin_sha || origin_ref)
end end
......
module Ci
class ExpirePipelineCacheService < BaseService
attr_reader :pipeline
def execute(pipeline)
@pipeline = pipeline
store = Gitlab::EtagCaching::Store.new
store.touch(project_pipelines_path)
store.touch(commit_pipelines_path) if pipeline.commit
store.touch(new_merge_request_pipelines_path)
merge_requests_pipelines_paths.each { |path| store.touch(path) }
end
private
def project_pipelines_path
Gitlab::Routing.url_helpers.namespace_project_pipelines_path(
project.namespace,
project,
format: :json)
end
def commit_pipelines_path
Gitlab::Routing.url_helpers.pipelines_namespace_project_commit_path(
project.namespace,
project,
pipeline.commit.id,
format: :json)
end
def new_merge_request_pipelines_path
Gitlab::Routing.url_helpers.new_namespace_project_merge_request_path(
project.namespace,
project,
format: :json)
end
def merge_requests_pipelines_paths
pipeline.merge_requests.collect do |merge_request|
Gitlab::Routing.url_helpers.pipelines_namespace_project_merge_request_path(
project.namespace,
project,
merge_request,
format: :json)
end
end
end
end
...@@ -7,9 +7,7 @@ module Ci ...@@ -7,9 +7,7 @@ module Ci
raise Gitlab::Access::AccessDeniedError raise Gitlab::Access::AccessDeniedError
end end
pipeline.builds.latest.failed_or_canceled.find_each do |build| pipeline.retryable_builds.find_each do |build|
next unless build.retryable?
Ci::RetryBuildService.new(project, current_user) Ci::RetryBuildService.new(project, current_user)
.reprocess(build) .reprocess(build)
end end
......
...@@ -68,11 +68,11 @@ module Projects ...@@ -68,11 +68,11 @@ module Projects
def trash_repositories! def trash_repositories!
unless remove_repository(repo_path) unless remove_repository(repo_path)
raise_error('Failed to remove project repository. Please try again or contact administrator') raise_error('Failed to remove project repository. Please try again or contact administrator.')
end end
unless remove_repository(wiki_path) unless remove_repository(wiki_path)
raise_error('Failed to remove wiki repository. Please try again or contact administrator') raise_error('Failed to remove wiki repository. Please try again or contact administrator.')
end end
end end
......
# CronTimezoneValidator
#
# Custom validator for CronTimezone.
class CronTimezoneValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
cron_parser = Gitlab::Ci::CronParser.new(record.cron, record.cron_timezone)
record.errors.add(attribute, " is invalid syntax") unless cron_parser.cron_timezone_valid?
end
end
# CronValidator
#
# Custom validator for Cron.
class CronValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
cron_parser = Gitlab::Ci::CronParser.new(record.cron, record.cron_timezone)
record.errors.add(attribute, " is invalid syntax") unless cron_parser.cron_valid?
end
end
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
.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 'groups/group_lfs_settings', f: f = render 'groups/group_admin_settings', f: f
= render 'namespaces/shared_runners_minutes_setting', f: f = render 'namespaces/shared_runners_minutes_setting', f: f
......
...@@ -13,5 +13,7 @@ ...@@ -13,5 +13,7 @@
%button.btn.award-control.has-tooltip.js-add-award{ type: 'button', %button.btn.award-control.has-tooltip.js-add-award{ type: 'button',
'aria-label': 'Add emoji', 'aria-label': 'Add emoji',
data: { title: 'Add emoji', placement: "bottom" } } data: { title: 'Add emoji', placement: "bottom" } }
= icon('smile-o', class: "award-control-icon award-control-icon-normal") %span{ class: "award-control-icon award-control-icon-neutral" }= custom_icon('emoji_slightly_smiling_face')
%span{ class: "award-control-icon award-control-icon-positive" }= custom_icon('emoji_smiley')
%span{ class: "award-control-icon award-control-icon-super-positive" }= custom_icon('emoji_smile')
= icon('spinner spin', class: "award-control-icon award-control-icon-loading") = icon('spinner spin', class: "award-control-icon award-control-icon-loading")
- status = local_assigns.fetch(:status) - status = local_assigns.fetch(:status)
- link = local_assigns.fetch(:link, true) - link = local_assigns.fetch(:link, true)
- css_classes = "ci-status ci-#{status.group}" - title = local_assigns.fetch(:title, nil)
- css_classes = "ci-status ci-#{status.group} #{'has-tooltip' if title.present?}"
- if link && status.has_details? - if link && status.has_details?
= link_to status.details_path, class: css_classes do = link_to status.details_path, class: css_classes, title: title do
= custom_icon(status.icon) = custom_icon(status.icon)
= status.text = status.text
- else - else
%span{ class: css_classes } %span{ class: css_classes, title: title }
= custom_icon(status.icon) = custom_icon(status.icon)
= status.text = status.text
...@@ -3,8 +3,6 @@ ...@@ -3,8 +3,6 @@
.event-item-timestamp .event-item-timestamp
#{time_ago_with_tooltip(event.created_at)} #{time_ago_with_tooltip(event.created_at)}
= author_avatar(event, size: 40)
- if event.created_project? - if event.created_project?
= render "events/event/created_project", event: event = render "events/event/created_project", event: event
- elsif event.push? - elsif event.push?
......
- if event.target
- if event.action_name == "opened"
.profile-icon.open-icon
= custom_icon("icon_status_open")
- elsif event.action_name == "closed"
.profile-icon.closed-icon
= custom_icon("icon_status_closed")
- else
.profile-icon.fork-icon
= custom_icon("icon_code_fork")
.event-title .event-title
%span.author_name= link_to_author event
%span{ class: event.action_name } %span{ class: event.action_name }
- if event.target - if event.target
= event.action_name = event.action_name
......
.profile-icon.open-icon
= custom_icon("icon_status_open")
.event-title .event-title
%span.author_name= link_to_author event
%span{ class: event.action_name } %span{ class: event.action_name }
= event_action_name(event) = event_action_name(event)
......
.profile-icon
= custom_icon("icon_comment_o")
.event-title .event-title
%span.author_name= link_to_author event
= event.action_name = event.action_name
= event_note_title_html(event) = event_note_title_html(event)
......
- project = event.project - project = event.project
.profile-icon
- if event.action_name == "deleted"
= custom_icon("trash_o")
- else
= custom_icon("icon_commit")
.event-title .event-title
%span.author_name= link_to_author event
%span.pushed #{event.action_name} #{event.ref_type} %span.pushed #{event.action_name} #{event.ref_type}
%strong %strong
- commits_link = namespace_project_commits_path(project.namespace, project, event.ref_name) - commits_link = namespace_project_commits_path(project.namespace, project, event.ref_name)
...@@ -48,4 +53,3 @@ ...@@ -48,4 +53,3 @@
.event-body .event-body
%ul.well-list.event_commits %ul.well-list.event_commits
= render "events/commit", commit: last_commit, project: project, event: event = render "events/commit", commit: last_commit, project: project, event: event
- if current_user.admin? - if current_user.admin?
.form-group .form-group
.col-sm-offset-2.col-sm-10 = f.label :lfs_enabled, 'Large File Storage', class: 'control-label'
.col-sm-10
.checkbox .checkbox
= f.label :lfs_enabled do = f.label :lfs_enabled do
= f.check_box :lfs_enabled, checked: @group.lfs_enabled? = f.check_box :lfs_enabled, checked: @group.lfs_enabled?
...@@ -9,3 +10,19 @@ ...@@ -9,3 +10,19 @@
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
%br/ %br/
%span.descr This setting can be overridden in each project. %span.descr This setting can be overridden in each project.
- if can? current_user, :admin_group, @group
.form-group
= f.label :require_two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2'
.col-sm-10
.checkbox
= f.label :require_two_factor_authentication do
= f.check_box :require_two_factor_authentication
%strong
Require all users in this group to setup Two-factor authentication
= link_to icon('question-circle'), help_page_path('security/two_factor_authentication', anchor: 'enforcing-2fa-for-all-users-in-a-group')
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.text_field :two_factor_grace_period, class: 'form-control'
.help-block Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication
...@@ -29,7 +29,7 @@ ...@@ -29,7 +29,7 @@
.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_lfs_settings', f: f = render 'group_admin_settings', f: f
.form-group .form-group
%hr %hr
......
...@@ -15,6 +15,10 @@ ...@@ -15,6 +15,10 @@
%tr %tr
%th %th
%th Global Shortcuts %th Global Shortcuts
%tr
%td.shortcut
.key n
%td Main Navigation
%tr %tr
%td.shortcut %td.shortcut
.key s .key s
...@@ -39,24 +43,46 @@ ...@@ -39,24 +43,46 @@
.key .key
%i.fa.fa-arrow-up %i.fa.fa-arrow-up
%td Edit last comment (when focused on an empty textarea) %td Edit last comment (when focused on an empty textarea)
%tbody
%tr %tr
%th %td.shortcut
%th Project Files browsing .key shift t
%td
Go to todos
%tr %tr
%td.shortcut %td.shortcut
.key .key shift a
%i.fa.fa-arrow-up %td
%td Move selection up Go to the activity feed
%tr %tr
%td.shortcut %td.shortcut
.key .key shift p
%i.fa.fa-arrow-down %td
%td Move selection down Go to projects
%tr %tr
%td.shortcut %td.shortcut
.key enter .key shift i
%td Open Selection %td
Go to issues
%tr
%td.shortcut
.key shift m
%td
Go to merge requests
%tr
%td.shortcut
.key shift g
%td
Go to groups
%tr
%td.shortcut
.key shift l
%td
Go to milestones
%tr
%td.shortcut
.key shift s
%td
Go to snippets
%tbody %tbody
%tr %tr
%th %th
...@@ -79,51 +105,8 @@ ...@@ -79,51 +105,8 @@
%td.shortcut %td.shortcut
.key esc .key esc
%td Go back %td Go back
%tbody
%tr
%th
%th Project File
%tr
%td.shortcut
.key y
%td Go to file permalink
.col-lg-4 .col-lg-4
%table.shortcut-mappings %table.shortcut-mappings
%tbody.hidden-shortcut.project{ style: 'display:none' }
%tr
%th
%th Global Dashboard
%tr
%td.shortcut
.key g
.key a
%td
Go to the activity feed
%tr
%td.shortcut
.key g
.key p
%td
Go to projects
%tr
%td.shortcut
.key g
.key i
%td
Go to issues
%tr
%td.shortcut
.key g
.key m
%td
Go to merge requests
%tr
%td.shortcut
.key g
.key t
%td
Go to todos
%tbody %tbody
%tr %tr
%th %th
...@@ -155,7 +138,7 @@ ...@@ -155,7 +138,7 @@
%tr %tr
%td.shortcut %td.shortcut
.key g .key g
.key b .key j
%td %td
Go to jobs Go to jobs
%tr %tr
...@@ -167,7 +150,7 @@ ...@@ -167,7 +150,7 @@
%tr %tr
%td.shortcut %td.shortcut
.key g .key g
.key g .key d
%td %td
Go to repository charts Go to repository charts
%tr %tr
...@@ -179,7 +162,7 @@ ...@@ -179,7 +162,7 @@
%tr %tr
%td.shortcut %td.shortcut
.key g .key g
.key l .key b
%td %td
Go to issue boards Go to issue boards
%tr %tr
...@@ -194,6 +177,12 @@ ...@@ -194,6 +177,12 @@
.key s .key s
%td %td
Go to snippets Go to snippets
%tr
%td.shortcut
.key g
.key w
%td
Go to wiki
%tr %tr
%td.shortcut %td.shortcut
.key t .key t
...@@ -202,6 +191,33 @@ ...@@ -202,6 +191,33 @@
%td.shortcut %td.shortcut
.key i .key i
%td New issue %td New issue
%tbody
%tr
%th
%th Project Files browsing
%tr
%td.shortcut
.key
%i.fa.fa-arrow-up
%td Move selection up
%tr
%td.shortcut
.key
%i.fa.fa-arrow-down
%td Move selection down
%tr
%td.shortcut
.key enter
%td Open Selection
%tbody
%tr
%th
%th Project File
%tr
%td.shortcut
.key y
%td Go to file permalink
.col-lg-4 .col-lg-4
%table.shortcut-mappings %table.shortcut-mappings
%tbody.hidden-shortcut.network{ style: 'display:none' } %tbody.hidden-shortcut.network{ style: 'display:none' }
......
...@@ -47,17 +47,19 @@ ...@@ -47,17 +47,19 @@
%li %li
= link_to assigned_issues_dashboard_path, title: 'Issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to assigned_issues_dashboard_path, title: 'Issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('hashtag fw') = icon('hashtag fw')
%span.badge.issues-count - issues_count = cached_assigned_issuables_count(current_user, :issues, :opened)
= number_with_delimiter(cached_assigned_issuables_count(current_user, :issues, :opened)) %span.badge.issues-count{ class: ('hidden' if issues_count.zero?) }
= number_with_delimiter(issues_count)
%li %li
= link_to assigned_mrs_dashboard_path, title: 'Merge requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to assigned_mrs_dashboard_path, title: 'Merge requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= custom_icon('mr_bold') = custom_icon('mr_bold')
%span.badge.merge-requests-count - merge_requests_count = cached_assigned_issuables_count(current_user, :merge_requests, :opened)
= number_with_delimiter(cached_assigned_issuables_count(current_user, :merge_requests, :opened)) %span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) }
= number_with_delimiter(merge_requests_count)
%li %li
= link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('check-circle fw') = icon('check-circle fw')
%span.badge.todos-count %span.badge.todos-count{ class: ('hidden' if todos_pending_count.zero?) }
= todos_count_format(todos_pending_count) = todos_count_format(todos_pending_count)
- if Gitlab::Geo.secondary? - if Gitlab::Geo.secondary?
......
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.
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.
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